import { flushPromises, mount } from '@vue/test-utils';
|
import { defineComponent, h, reactive } from 'vue';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { workflowFileSubmitPendingKey } from '#/components/Jnpf/upload/src/filePermissionContext';
|
|
import { refreshConfiguredFilePermissions, useFilePermissions } from './useFilePermissions';
|
|
const mocks = vi.hoisted(() => ({ get: vi.fn() }));
|
vi.mock('#/api/workFlow/task', () => ({ getFlowFilePermissions: mocks.get }));
|
const view = (taskId = 'task', revision = 'v1') => ({ enabled: true, taskId, formId: 'form', status: 'ready', revision, fields: {} });
|
const wrappers: ReturnType<typeof mount>[] = [];
|
function setup(initial = view()) {
|
const config = reactive({ id: initial.taskId, formId: 'form', filePermissions: initial });
|
const submission = reactive({ pending: false });
|
let permissions!: ReturnType<typeof useFilePermissions>;
|
const wrapper = mount(
|
defineComponent({
|
setup() {
|
permissions = useFilePermissions(() => config);
|
return () => h('div');
|
},
|
}),
|
{ global: { provide: { [workflowFileSubmitPendingKey as symbol]: () => submission.pending } } },
|
);
|
wrappers.push(wrapper);
|
return { config, permissions, wrapper, submission };
|
}
|
beforeEach(() => {
|
vi.useFakeTimers();
|
mocks.get.mockReset();
|
});
|
afterEach(() => {
|
wrappers.splice(0).forEach((w) => w.unmount());
|
vi.useRealTimers();
|
});
|
describe('flow file permissions refresh', () => {
|
it('updates configured permissions immediately after workflow state changes', async () => {
|
const config = reactive({ id: 'record', formId: 'form', filePermissions: view() });
|
mocks.get.mockResolvedValue({ data: view('task', 'started') });
|
|
await expect(refreshConfiguredFilePermissions(() => config)).resolves.toBe(true);
|
|
expect(mocks.get).toHaveBeenCalledWith('task', { formId: 'form' });
|
expect(config.filePermissions.revision).toBe('started');
|
});
|
it('does not apply an immediate refresh response after switching records', async () => {
|
let resolve!: (value: any) => void;
|
mocks.get.mockReturnValueOnce(
|
new Promise((done) => {
|
resolve = done;
|
}),
|
);
|
const config = reactive({ id: 'record', formId: 'form', filePermissions: view() });
|
const refresh = refreshConfiguredFilePermissions(() => config);
|
config.id = 'other-record';
|
config.filePermissions = view('other-task', 'current');
|
resolve({ data: view('task', 'stale') });
|
|
await expect(refresh).resolves.toBe(false);
|
expect(config.filePermissions.taskId).toBe('other-task');
|
expect(config.filePermissions.revision).toBe('current');
|
});
|
it('keeps an unsaved form native and applies instance permissions after reopening the saved record', async () => {
|
const config = reactive({ id: '', formId: 'form', filePermissions: undefined as ReturnType<typeof view> | undefined });
|
let permissions!: ReturnType<typeof useFilePermissions>;
|
wrappers.push(
|
mount(
|
defineComponent({
|
setup() {
|
permissions = useFilePermissions(() => config);
|
return () => h('div');
|
},
|
}),
|
),
|
);
|
window.dispatchEvent(new Event('focus'));
|
await vi.advanceTimersByTimeAsync(30_000);
|
expect(permissions.value).toBeUndefined();
|
expect(mocks.get).not.toHaveBeenCalled();
|
|
config.id = 'saved-record';
|
config.filePermissions = view();
|
await flushPromises();
|
expect(permissions.value?.enabled).toBe(true);
|
mocks.get.mockRejectedValueOnce(new Error('offline'));
|
window.dispatchEvent(new Event('focus'));
|
await flushPromises();
|
expect(mocks.get).toHaveBeenCalledWith('task', { formId: 'form' });
|
expect(permissions.value?.status).toBe('error');
|
expect(permissions.value?.enabled).toBe(true);
|
|
config.id = '';
|
config.filePermissions = undefined;
|
await flushPromises();
|
expect(permissions.value).toBeUndefined();
|
});
|
it('does not query old workflows or leave listeners after unmount', async () => {
|
const state = setup({ ...view(), enabled: false });
|
window.dispatchEvent(new Event('focus'));
|
await vi.advanceTimersByTimeAsync(30_000);
|
expect(mocks.get).not.toHaveBeenCalled();
|
state.wrapper.unmount();
|
expect(vi.getTimerCount()).toBe(0);
|
window.dispatchEvent(new Event('focus'));
|
expect(mocks.get).not.toHaveBeenCalled();
|
});
|
it('fails closed on request failure and recovers on the next focus', async () => {
|
const state = setup();
|
mocks.get.mockRejectedValueOnce(new Error('offline'));
|
window.dispatchEvent(new Event('focus'));
|
await flushPromises();
|
expect(state.permissions.value?.status).toBe('error');
|
mocks.get.mockResolvedValue({ data: view('task', 'v2') });
|
window.dispatchEvent(new Event('focus'));
|
await flushPromises();
|
expect(state.permissions.value?.revision).toBe('v2');
|
expect(state.permissions.value?.status).toBe('ready');
|
});
|
it('ignores an old response after a form switch and prevents duplicate in-flight requests', async () => {
|
let resolve!: (value: any) => void;
|
mocks.get.mockReturnValueOnce(
|
new Promise((done) => {
|
resolve = done;
|
}),
|
);
|
const state = setup();
|
window.dispatchEvent(new Event('focus'));
|
window.dispatchEvent(new Event('focus'));
|
expect(mocks.get).toHaveBeenCalledTimes(1);
|
state.config.id = 'other';
|
state.config.filePermissions = view('other');
|
await flushPromises();
|
mocks.get.mockResolvedValue({ data: view('other', 'new') });
|
window.dispatchEvent(new Event('focus'));
|
await flushPromises();
|
resolve({ data: view('task', 'stale') });
|
await flushPromises();
|
expect(state.permissions.value?.taskId).toBe('other');
|
expect(state.permissions.value?.revision).toBe('new');
|
});
|
it('rejects another workflow response and controlled-to-legacy downgrade', async () => {
|
const state = setup();
|
mocks.get.mockResolvedValue({ data: view('other') });
|
window.dispatchEvent(new Event('focus'));
|
await flushPromises();
|
expect(state.permissions.value?.status).toBe('error');
|
mocks.get.mockResolvedValue({ data: { ...view(), enabled: false } });
|
window.dispatchEvent(new Event('focus'));
|
await flushPromises();
|
expect(state.permissions.value?.status).toBe('error');
|
expect(state.permissions.value?.enabled).toBe(true);
|
});
|
it('refreshes using the resolved task id rather than the native record alias', async () => {
|
const state = setup();
|
state.config.id = 'business-record';
|
await flushPromises();
|
mocks.get.mockResolvedValue({ data: view() });
|
window.dispatchEvent(new Event('focus'));
|
await flushPromises();
|
expect(mocks.get).toHaveBeenCalledWith('task', { formId: 'form' });
|
expect(state.permissions.value?.status).toBe('ready');
|
});
|
it('fences in-flight permissions while approval submits and refreshes after failure recovery', async () => {
|
const state = setup();
|
let reject!: (error: Error) => void;
|
mocks.get.mockReturnValueOnce(
|
new Promise((_resolve, fail) => {
|
reject = fail;
|
}),
|
);
|
window.dispatchEvent(new Event('focus'));
|
state.submission.pending = true;
|
await vi.advanceTimersByTimeAsync(30_000);
|
window.dispatchEvent(new Event('focus'));
|
expect(mocks.get).toHaveBeenCalledTimes(1);
|
mocks.get.mockResolvedValue({ data: view('task', 'next-node') });
|
state.submission.pending = false;
|
await flushPromises();
|
expect(state.permissions.value?.revision).toBe('next-node');
|
reject(new Error('stale transition failure'));
|
await flushPromises();
|
expect(state.permissions.value?.status).toBe('ready');
|
expect(state.permissions.value?.revision).toBe('next-node');
|
});
|
});
|