import type { OnlyOfficeBridge } from '../src/onlyoffice/bridge';
|
|
import { mount } from '@vue/test-utils';
|
import Antd, { Checkbox, Input, Modal } from 'ant-design-vue';
|
import { createPinia } from 'pinia';
|
import { afterEach, expect, it, vi } from 'vitest';
|
|
import App from '../src/App.vue';
|
|
afterEach(() => {
|
vi.restoreAllMocks();
|
vi.unstubAllGlobals();
|
});
|
|
it('reapplies a pending empty clear on reopening without requesting another synchronization', async () => {
|
const fetch = vi.fn(async () => new Response(JSON.stringify({
|
fields: { 'eln.field.sample': { type: 'text', value: null } }, repeatGroups: [],
|
pendingSourceSync: {
|
changedCount: 1, changedFieldCodes: ['sample'], differences: [], operationCode: 'source-sync-pending',
|
recalculatedCount: 0, recordId: 'record-1', recordVersion: 5, revisionId: 'revision-1',
|
},
|
}), { status: 200 }));
|
vi.stubGlobal('fetch', fetch);
|
let ready = () => {};
|
const bridge: OnlyOfficeBridge = {
|
applyFields: vi.fn(async (changes: Parameters<OnlyOfficeBridge['applyFields']>[0]) => changes.map(({ tag }) => ({ ok: true, tag }))),
|
appendRepeatRow: vi.fn().mockResolvedValue([]),
|
bindLifecycle: (callbacks) => { ready = callbacks.onReady; return () => {}; },
|
getRuntimeOptions: () => ({
|
apiBaseUrl: 'http://localhost:30000', bizDataId: 'record-1', bizModule: 'eln.record',
|
bizScene: 'eln.record.fill', canFill: true, documentId: 'file-1', pluginVersion: '0.1.0',
|
scope: 'onlyoffice:eln-record-fill', ticket: 'test-ticket',
|
}),
|
inspectFields: vi.fn().mockResolvedValue([
|
{ alias: '供试品名称', currentValue: '', internalId: '1', options: [], tag: 'eln.field.sample', type: 'text' },
|
]),
|
removeRepeatRow: vi.fn().mockResolvedValue([]), selectControl: vi.fn().mockResolvedValue(undefined),
|
setFillOnlyMode: vi.fn().mockResolvedValue(undefined),
|
};
|
const wrapper = mount(App, { global: { plugins: [createPinia(), Antd], provide: { onlyOfficeBridge: bridge } } });
|
try {
|
ready();
|
await vi.waitFor(() => expect(wrapper.get('[data-test="connection-status"]').text()).toContain('已连接'));
|
expect(bridge.applyFields).toHaveBeenCalledWith([{ tag: 'eln.field.sample', type: 'text', value: '' }]);
|
expect(fetch).toHaveBeenCalledTimes(1);
|
expect(fetch).toHaveBeenCalledWith(expect.stringMatching(/\/recordFill\/data$/u), expect.anything());
|
} finally {
|
wrapper.unmount();
|
}
|
});
|
|
it.each([
|
['MISSING', '来源缺失,保留原快照', 'Retained sample'],
|
['NOT_APPLICABLE', '来源不适用,保留原快照', 'Retained sample'],
|
['MISSING', '来源缺失,保留原快照', ''],
|
])('retains %s (%s, previous "%s") and clears only after explicit selection with a reason', async (outcome, message, previousValue) => {
|
let cleared = false;
|
let confirmationAttempts = 0;
|
const fetch = vi.fn(async (url: string, _options?: RequestInit) => {
|
if (url.endsWith('/sourceSync')) {
|
if (++confirmationAttempts === 1) return new Response(JSON.stringify({ msg: 'temporary failure' }), { status: 503 });
|
cleared = true;
|
return new Response(JSON.stringify({
|
changedCount: 1, changedFieldCodes: ['sample'], differences: [], operationCode: 'source-sync-1',
|
recalculatedCount: 0, recordId: 'record-1', recordVersion: 5, revisionId: 'revision-1',
|
}), { status: 200 });
|
}
|
return new Response(JSON.stringify(url.endsWith('/sourceSync/preview') ? {
|
changedCount: 0,
|
differences: [{
|
bindingId: 'binding-sample', changed: false, currentValue: previousValue, dataType: 'TEXT',
|
fieldCode: 'sample', fieldName: '供试品名称', previousValue,
|
sourceProperty: 'sample.name', sourcePropertyRevision: 1, sourceType: 'LIMS_TASK',
|
sourceOutcome: outcome, canClear: true,
|
previousFetchedTime: '2026-09-01T08:00:00Z', sourceFetchedAt: '2026-09-06T08:00:00Z',
|
}],
|
previewToken: 'preview-token', recordId: 'record-1', recordVersion: 4, revisionId: 'revision-1',
|
} : { fields: { 'eln.field.sample': { type: 'text', value: cleared ? null : previousValue } }, repeatGroups: [] }), { status: 200 });
|
});
|
vi.stubGlobal('fetch', fetch);
|
let ready = () => {};
|
const bridge: OnlyOfficeBridge = {
|
applyFields: vi.fn(async (changes: Parameters<OnlyOfficeBridge['applyFields']>[0]) => changes.map(({ tag }) => ({ ok: true, tag }))), appendRepeatRow: vi.fn().mockResolvedValue([]),
|
bindLifecycle: (callbacks) => { ready = callbacks.onReady; return () => {}; },
|
getRuntimeOptions: () => ({
|
apiBaseUrl: 'http://localhost:30000', bizDataId: 'record-1', bizModule: 'eln.record',
|
bizScene: 'eln.record.fill', canFill: true, documentId: 'file-1', pluginVersion: '0.1.0',
|
scope: 'onlyoffice:eln-record-fill', ticket: 'test-ticket',
|
}),
|
inspectFields: vi.fn().mockResolvedValue([
|
{ alias: '供试品名称', currentValue: previousValue, internalId: '1', options: [], tag: 'eln.field.sample', type: 'text' },
|
]), removeRepeatRow: vi.fn().mockResolvedValue([]),
|
selectControl: vi.fn().mockResolvedValue(undefined), setFillOnlyMode: vi.fn().mockResolvedValue(undefined),
|
};
|
const wrapper = mount(App, { global: { plugins: [createPinia(), Antd], provide: { onlyOfficeBridge: bridge } } });
|
try {
|
ready();
|
await vi.waitFor(() => expect(wrapper.get('[data-test="connection-status"]').text()).toContain('已连接'));
|
await wrapper.get('[aria-label="重新同步来源快照"]').trigger('click');
|
await vi.waitFor(() => expect(document.body.textContent).toContain(message));
|
const differences = document.body.querySelector('[data-test="source-sync-differences"]');
|
expect(differences?.textContent).toContain(previousValue || '-');
|
expect(differences?.textContent).toContain('sample.name@1');
|
expect(differences?.textContent).toContain('2026-09-01T08:00:00Z');
|
expect(document.body.querySelector('[data-test="source-sync-empty"]')).toBeNull();
|
expect(fetch.mock.calls.some(([url]) => url.endsWith('/sourceSync'))).toBe(false);
|
wrapper.getComponent(Modal).vm.$emit('cancel');
|
await wrapper.vm.$nextTick();
|
await wrapper.get('[aria-label="重新同步来源快照"]').trigger('click');
|
await vi.waitFor(() => expect(wrapper.getComponent(Modal).props('open')).toBe(true));
|
const modal = wrapper.getComponent(Modal);
|
const clear = wrapper.getComponent(Checkbox);
|
expect(clear.props('checked')).toBe(false);
|
clear.vm.$emit('update:checked', true);
|
await wrapper.vm.$nextTick();
|
expect(modal.props('okButtonProps')).toMatchObject({ disabled: true });
|
const reason = wrapper.getComponent(Input.TextArea);
|
reason.vm.$emit('update:value', ' ');
|
await wrapper.vm.$nextTick();
|
expect(modal.props('okButtonProps')).toMatchObject({ disabled: true });
|
reason.vm.$emit('update:value', 'x'.repeat(501));
|
await wrapper.vm.$nextTick();
|
expect(modal.props('okButtonProps')).toMatchObject({ disabled: true });
|
reason.vm.$emit('update:value', ' Confirm source no longer applicable ');
|
await wrapper.vm.$nextTick();
|
expect(modal.props('okButtonProps')).toMatchObject({ disabled: false });
|
modal.vm.$emit('ok');
|
await vi.waitFor(() => expect(confirmationAttempts).toBe(1));
|
await vi.waitFor(() => expect(modal.props('confirmLoading')).toBe(false));
|
expect(modal.props('open')).toBe(true);
|
expect(reason.props('value')).toBe(' Confirm source no longer applicable ');
|
modal.vm.$emit('ok');
|
await vi.waitFor(() => expect(cleared).toBe(true));
|
const request = fetch.mock.calls.find(([url]) => url.endsWith('/sourceSync'));
|
expect(JSON.parse(String(request?.[1]?.body))).toMatchObject({
|
expectedRecordVersion: 4, previewToken: 'preview-token',
|
clearReasons: { 'binding-sample': 'Confirm source no longer applicable' },
|
});
|
const confirmations = fetch.mock.calls.filter(([url]) => url.endsWith('/sourceSync'));
|
expect(confirmations[1]?.[1]?.body).toBe(confirmations[0]?.[1]?.body);
|
await vi.waitFor(() => expect(bridge.applyFields).toHaveBeenCalledWith([{ tag: 'eln.field.sample', type: 'text', value: '' }]));
|
} finally {
|
wrapper.unmount();
|
}
|
});
|