import { flushPromises, mount } from '@vue/test-utils';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import DevApprovalButton from './DevApprovalButton.vue';
|
|
const mocks = vi.hoisted(() => ({
|
inspect: vi.fn(),
|
start: vi.fn(),
|
status: vi.fn(),
|
stop: vi.fn(),
|
sign: vi.fn(),
|
success: vi.fn(),
|
warning: vi.fn(),
|
error: vi.fn(),
|
info: vi.fn(),
|
}));
|
vi.mock('@jnpf/hooks', () => ({ useMessage: () => ({ createMessage: mocks }) }));
|
vi.mock('#/api/x/development/approval', () => ({
|
inspectDevApproval: mocks.inspect,
|
startDevApprovalRun: mocks.start,
|
getDevApprovalRun: mocks.status,
|
stopDevApprovalRun: mocks.stop,
|
}));
|
vi.mock('#/utils/jnpf', () => ({ onlineUtils: { sign: mocks.sign } }));
|
function render(flowState: number | string = 1) {
|
return mount(DevApprovalButton, {
|
props: { taskId: 'task', flowState },
|
global: {
|
stubs: {
|
'a-space': { template: '<div><slot /></div>' },
|
'a-button': { template: '<button><slot /></button>' },
|
},
|
},
|
});
|
}
|
const active = { runId: 'run', status: 'RUNNING', completed: 2, nodeName: '质量审核' };
|
describe('server-owned development approval', () => {
|
beforeEach(() => {
|
vi.useFakeTimers();
|
vi.clearAllMocks();
|
mocks.inspect.mockResolvedValue({ data: { enabled: true, status: 'READY', problems: [] } });
|
mocks.status.mockResolvedValue({ data: { status: 'IDLE', completed: 0 } });
|
mocks.start.mockResolvedValue({ data: active });
|
});
|
afterEach(() => vi.useRealTimers());
|
it.each([0, '0', 2, 3, 4, 5, 6, 7, 8, 9])('does not inspect outside active flow (%s)', async (value) => {
|
const wrapper = render(value);
|
await flushPromises();
|
expect(mocks.inspect).not.toHaveBeenCalled();
|
expect(wrapper.text()).toBe('');
|
wrapper.unmount();
|
});
|
it('waits for state and discards a stale availability response', async () => {
|
const wrapper = render(0);
|
await wrapper.setProps({ flowState: undefined });
|
await flushPromises();
|
expect(mocks.inspect).not.toHaveBeenCalled();
|
let resolve: (value: any) => void = () => {};
|
mocks.inspect.mockImplementationOnce(
|
() =>
|
new Promise((done) => {
|
resolve = done;
|
}),
|
);
|
await wrapper.setProps({ flowState: '1' });
|
await wrapper.setProps({ flowState: 0 });
|
resolve({ data: { enabled: true, status: 'READY' } });
|
await flushPromises();
|
expect(wrapper.text()).toBe('');
|
expect(mocks.status).not.toHaveBeenCalled();
|
wrapper.unmount();
|
});
|
it('signs once, starts one server job and never stops it on page close', async () => {
|
mocks.sign.mockImplementation((config) => config.onSubmit({ biz_sign: 'real-sign' }));
|
const wrapper = render();
|
await flushPromises();
|
await wrapper.find('button').trigger('click');
|
await flushPromises();
|
expect(mocks.sign).toHaveBeenCalledOnce();
|
expect(mocks.start).toHaveBeenCalledWith('task', 'real-sign');
|
expect(wrapper.text()).toContain('已完成 2 个待办');
|
wrapper.unmount();
|
await vi.advanceTimersByTimeAsync(10_000);
|
expect(mocks.stop).not.toHaveBeenCalled();
|
expect(mocks.start).toHaveBeenCalledOnce();
|
});
|
it('reopening restores the existing run without signing or submitting', async () => {
|
mocks.status.mockResolvedValue({ data: active });
|
const wrapper = render();
|
await flushPromises();
|
expect(wrapper.text()).toContain('已完成 2 个待办');
|
expect(mocks.start).not.toHaveBeenCalled();
|
expect(mocks.sign).not.toHaveBeenCalled();
|
wrapper.unmount();
|
});
|
it('polling failure only reconnects and does not resubmit the job', async () => {
|
mocks.status.mockResolvedValueOnce({ data: active }).mockRejectedValueOnce(new Error('offline')).mockResolvedValue({ data: active });
|
const wrapper = render();
|
await flushPromises();
|
await vi.advanceTimersByTimeAsync(2000);
|
await flushPromises();
|
expect(wrapper.text()).toContain('正在重新连接');
|
await vi.advanceTimersByTimeAsync(2000);
|
await flushPromises();
|
expect(wrapper.text()).toContain('已完成 2 个待办');
|
expect(mocks.start).not.toHaveBeenCalled();
|
wrapper.unmount();
|
});
|
it('cancelling signature never starts a job', async () => {
|
mocks.sign.mockImplementation((config) => config.onCancel());
|
const wrapper = render();
|
await flushPromises();
|
await wrapper.find('button').trigger('click');
|
await flushPromises();
|
expect(mocks.start).not.toHaveBeenCalled();
|
expect(wrapper.emitted('busy')?.at(-1)).toEqual([false]);
|
wrapper.unmount();
|
});
|
it('explicit stop targets the restored run', async () => {
|
mocks.status.mockResolvedValue({ data: active });
|
mocks.stop.mockResolvedValue({ data: { ...active, status: 'STOPPING' } });
|
const wrapper = render();
|
await flushPromises();
|
await wrapper.findAll('button').at(-1)!.trigger('click');
|
await flushPromises();
|
expect(mocks.stop).toHaveBeenCalledWith('task', 'run');
|
expect(wrapper.text()).toContain('当前节点完成后停止');
|
wrapper.unmount();
|
});
|
it('observes completion without issuing another approval', async () => {
|
mocks.status.mockResolvedValueOnce({ data: active }).mockResolvedValue({ data: { ...active, status: 'PASSED', completed: 10 } });
|
const wrapper = render();
|
await flushPromises();
|
await vi.advanceTimersByTimeAsync(2000);
|
await flushPromises();
|
expect(mocks.success).toHaveBeenCalledOnce();
|
expect(wrapper.emitted('finished')).toHaveLength(1);
|
expect(mocks.start).not.toHaveBeenCalled();
|
wrapper.unmount();
|
});
|
it('recovers a timed-out start by reading status without submitting again', async () => {
|
mocks.sign.mockImplementation((config) => config.onSubmit({ biz_sign: 'real-sign' }));
|
mocks.start.mockRejectedValueOnce(new Error('timeout'));
|
const wrapper = render();
|
await flushPromises();
|
await wrapper.find('button').trigger('click');
|
await flushPromises();
|
mocks.status.mockResolvedValue({ data: active });
|
await vi.advanceTimersByTimeAsync(2000);
|
await flushPromises();
|
expect(wrapper.text()).toContain('已完成 2 个待办');
|
expect(mocks.start).toHaveBeenCalledOnce();
|
wrapper.unmount();
|
});
|
it('reports a lost server run and releases busy state after a restart', async () => {
|
mocks.status.mockResolvedValueOnce({ data: active }).mockResolvedValue({ data: { status: 'IDLE', completed: 0 } });
|
const wrapper = render();
|
await flushPromises();
|
await vi.advanceTimersByTimeAsync(2000);
|
await flushPromises();
|
expect(mocks.warning).toHaveBeenCalledWith(expect.stringContaining('服务重启不会自动续跑'), 10);
|
expect(wrapper.emitted('busy')?.at(-1)).toEqual([false]);
|
expect(wrapper.emitted('finished')).toHaveLength(1);
|
expect(mocks.start).not.toHaveBeenCalled();
|
wrapper.unmount();
|
});
|
it('clears the old run before signing on a different workflow', async () => {
|
mocks.status.mockResolvedValueOnce({ data: active }).mockResolvedValue({ data: { status: 'IDLE', completed: 0 } });
|
mocks.sign.mockImplementation(() => {});
|
const wrapper = render();
|
await flushPromises();
|
await wrapper.setProps({ taskId: 'other-task' });
|
await flushPromises();
|
await wrapper.find('button').trigger('click');
|
await flushPromises();
|
expect(wrapper.text()).toContain('等待电子签名');
|
expect(wrapper.text()).not.toContain('停止后续审核');
|
expect(mocks.stop).not.toHaveBeenCalled();
|
wrapper.unmount();
|
});
|
});
|