/* @vitest-environment happy-dom */
|
|
import { flushPromises, mount } from '@vue/test-utils';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import RecordView from '../index.vue';
|
import { ElnRecordOpenParamError, resolveElnRecordOpenParams, toElnRecordOfficeConfigQuery, unwrapElnRecordEditorConfig } from '../recordView';
|
|
const apiMocks = vi.hoisted(() => ({
|
getOnlyOfficeConfig: vi.fn(),
|
getOnlyOfficeSaveStatus: vi.fn(),
|
}));
|
const messageMocks = vi.hoisted(() => ({
|
confirmOptions: undefined as any,
|
createConfirm: vi.fn((options: any) => {
|
messageMocks.confirmOptions = options;
|
return { destroy: vi.fn() };
|
}),
|
error: vi.fn(),
|
}));
|
const editorMocks = vi.hoisted(() => ({ requestClose: vi.fn() }));
|
const viewParams = vi.hoisted(() => ({ current: {} as Record<string, unknown> }));
|
|
vi.mock('#/api/x/onlyoffice', () => ({
|
getOnlyOfficeConfig: apiMocks.getOnlyOfficeConfig,
|
getOnlyOfficeSaveStatus: apiMocks.getOnlyOfficeSaveStatus,
|
}));
|
vi.mock('@jnpf/hooks', () => ({
|
useMessage: () => ({ createConfirm: messageMocks.createConfirm, createMessage: { error: messageMocks.error } }),
|
}));
|
vi.mock('#/utils/jnpf', () => ({
|
onlineUtils: {
|
getViewParam: (name: string) => viewParams.current[name],
|
},
|
}));
|
vi.mock('#/components/OnlyOfficeEditor/src/Editor.vue', async () => {
|
const { defineComponent, h } = await import('vue');
|
return {
|
default: defineComponent({
|
name: 'OnlyOfficeEditor',
|
emits: ['close', 'modified'],
|
props: { config: { required: true, type: Object } },
|
setup(_props, { expose }) {
|
expose({ requestClose: editorMocks.requestClose });
|
return () => h('div', { 'data-test': 'onlyoffice-editor' });
|
},
|
}),
|
};
|
});
|
|
const DOCUMENT = { fileExtension: 'docx', fileId: 'file-1', fileSize: 12, name: 'ELN-001.docx' };
|
const EDITOR_CONFIG = {
|
data: {
|
document: { key: 'record-1-r1', title: 'ELN-001.docx', url: 'https://example.test/ELN-001.docx' },
|
documentType: 'word',
|
editorConfig: { mode: 'edit' },
|
token: 'signed-token',
|
},
|
};
|
|
describe('eLN record view', () => {
|
beforeEach(() => {
|
vi.clearAllMocks();
|
messageMocks.confirmOptions = undefined;
|
editorMocks.requestClose.mockReset();
|
viewParams.current = { document: DOCUMENT, recordId: 'record-1', revisionId: 'revision-1' };
|
apiMocks.getOnlyOfficeConfig.mockResolvedValue(EDITOR_CONFIG);
|
apiMocks.getOnlyOfficeSaveStatus.mockResolvedValue({ data: { lastSaveTime: 1000, sessionStatus: 'IDLE', version: 2 } });
|
});
|
|
it('rejects incomplete custom-view parameters before requesting an editor config', () => {
|
expect(() => resolveElnRecordOpenParams(() => undefined)).toThrow(ElnRecordOpenParamError);
|
});
|
|
it('shows the parameter error without requesting an editor config', async () => {
|
viewParams.current = { recordId: 'record-1', revisionId: 'revision-1' };
|
const wrapper = mount(RecordView);
|
await flushPromises();
|
|
expect(wrapper.find('[data-test="record-error"]').text()).toContain('缺少 ELN 记录文档');
|
expect(apiMocks.getOnlyOfficeConfig).not.toHaveBeenCalled();
|
});
|
|
it('requests the fixed record fill context while leaving the final mode to the backend', () => {
|
const source: Record<string, unknown> = { document: DOCUMENT, recordId: 'record-1', revisionId: 'revision-1' };
|
const params = resolveElnRecordOpenParams((name) => source[name]);
|
|
expect(toElnRecordOfficeConfigQuery(params)).toEqual({
|
bizDataId: 'record-1',
|
bizModule: 'eln.record',
|
bizScene: 'eln.record.fill',
|
fileId: 'file-1',
|
fileName: 'ELN-001.docx',
|
fileType: 'annex',
|
mode: 'edit',
|
});
|
});
|
|
it('keeps an explicitly requested readonly record view readonly', () => {
|
const source: Record<string, unknown> = {
|
document: DOCUMENT,
|
mode: 'view',
|
recordId: 'record-1',
|
revisionId: 'revision-1',
|
};
|
const params = resolveElnRecordOpenParams((name) => source[name]);
|
|
expect(params.mode).toBe('view');
|
expect(toElnRecordOfficeConfigQuery(params).mode).toBe('view');
|
});
|
|
it('shows a loading state, then renders the editor after the backend returns an editable config', async () => {
|
let resolveConfig: (value: typeof EDITOR_CONFIG) => void = () => {};
|
apiMocks.getOnlyOfficeConfig.mockReturnValue(
|
new Promise((resolve) => {
|
resolveConfig = resolve;
|
}),
|
);
|
const wrapper = mount(RecordView);
|
|
expect(wrapper.find('[data-test="record-loading"]').exists()).toBe(true);
|
expect(apiMocks.getOnlyOfficeConfig).toHaveBeenCalledWith({
|
bizDataId: 'record-1',
|
bizModule: 'eln.record',
|
bizScene: 'eln.record.fill',
|
fileId: 'file-1',
|
fileName: 'ELN-001.docx',
|
fileType: 'annex',
|
mode: 'edit',
|
});
|
|
resolveConfig(EDITOR_CONFIG);
|
await flushPromises();
|
|
expect(wrapper.find('[data-test="record-editor"]').exists()).toBe(true);
|
});
|
|
it('accepts a backend downgrade to readonly mode', () => {
|
expect(
|
unwrapElnRecordEditorConfig({
|
data: {
|
document: { key: 'record-1-r1' },
|
editorConfig: { mode: 'view' },
|
token: 'signed-token',
|
},
|
}).editorConfig.mode,
|
).toBe('view');
|
});
|
|
it('leaves source synchronization to the record fill plugin', async () => {
|
const wrapper = mount(RecordView);
|
await flushPromises();
|
|
expect(wrapper.find('[data-test="source-sync-button"]').exists()).toBe(false);
|
expect(wrapper.find('.eln-record-view__toolbar').exists()).toBe(false);
|
});
|
|
it('blocks close for unapplied plugin drafts, then waits for the server session to become idle', async () => {
|
const wrapper = mount(RecordView);
|
await flushPromises();
|
|
window.dispatchEvent(
|
new MessageEvent('message', {
|
data: {
|
bizDataId: 'record-1',
|
documentId: 'file-1',
|
pendingCount: 2,
|
type: 'jnpf:eln-record-fill-state',
|
version: 1,
|
},
|
}),
|
);
|
|
const cancelledClose = (wrapper.vm as any).requestClose();
|
await flushPromises();
|
expect(messageMocks.createConfirm).toHaveBeenCalledTimes(1);
|
expect(messageMocks.confirmOptions.content).toContain('2 个字段尚未应用');
|
messageMocks.confirmOptions.onCancel();
|
await expect(cancelledClose).resolves.toBe(false);
|
expect(apiMocks.getOnlyOfficeSaveStatus).not.toHaveBeenCalled();
|
|
wrapper.getComponent({ name: 'OnlyOfficeEditor' }).vm.$emit('modified', true);
|
const confirmedClose = (wrapper.vm as any).requestClose();
|
await flushPromises();
|
await messageMocks.confirmOptions.onOk();
|
await expect(confirmedClose).resolves.toBe('pending');
|
|
expect(editorMocks.requestClose).toHaveBeenCalledTimes(1);
|
expect(apiMocks.getOnlyOfficeSaveStatus).not.toHaveBeenCalled();
|
wrapper.getComponent({ name: 'OnlyOfficeEditor' }).vm.$emit('close');
|
await flushPromises();
|
|
expect(apiMocks.getOnlyOfficeSaveStatus).toHaveBeenCalledWith('file-1', 'annex');
|
expect(wrapper.find('[data-test="record-editor"]').exists()).toBe(false);
|
expect(wrapper.emitted('closeStarted')).toHaveLength(1);
|
expect(wrapper.emitted('closeCancelled')).toBeUndefined();
|
expect(wrapper.emitted('closeReady')).toHaveLength(1);
|
wrapper.unmount();
|
});
|
|
it('closes an unmodified editor without starting a save confirmation flow', async () => {
|
const wrapper = mount(RecordView);
|
await flushPromises();
|
|
await expect((wrapper.vm as any).requestClose()).resolves.toBe(true);
|
|
expect(editorMocks.requestClose).not.toHaveBeenCalled();
|
expect(apiMocks.getOnlyOfficeSaveStatus).not.toHaveBeenCalled();
|
wrapper.unmount();
|
});
|
|
it('closes directly when an unmodified editor requests close', async () => {
|
const wrapper = mount(RecordView);
|
await flushPromises();
|
|
wrapper.getComponent({ name: 'OnlyOfficeEditor' }).vm.$emit('close');
|
await flushPromises();
|
|
expect(apiMocks.getOnlyOfficeSaveStatus).not.toHaveBeenCalled();
|
expect(wrapper.emitted('closeStarted')).toBeUndefined();
|
expect(wrapper.emitted('closeReady')).toHaveLength(1);
|
wrapper.unmount();
|
});
|
|
it('reopens through the backend and stays on the record view when saving fails', async () => {
|
apiMocks.getOnlyOfficeSaveStatus.mockResolvedValue({ data: { lastSaveTime: 1000, sessionStatus: 'SAVE_FAILED', version: 1 } });
|
const wrapper = mount(RecordView);
|
await flushPromises();
|
wrapper.getComponent({ name: 'OnlyOfficeEditor' }).vm.$emit('modified', true);
|
|
await expect((wrapper.vm as any).requestClose()).resolves.toBe('pending');
|
wrapper.getComponent({ name: 'OnlyOfficeEditor' }).vm.$emit('close');
|
await flushPromises();
|
|
expect(apiMocks.getOnlyOfficeConfig).toHaveBeenCalledTimes(2);
|
await vi.waitFor(() => expect(wrapper.find('[data-test="record-editor"]').exists()).toBe(true));
|
expect(wrapper.emitted('closeStarted')).toHaveLength(1);
|
expect(wrapper.emitted('closeCancelled')).toHaveLength(1);
|
expect(wrapper.emitted('closeReady')).toBeUndefined();
|
expect(messageMocks.error).toHaveBeenCalledWith('电子记录保存失败,请联系管理员处理后重试');
|
wrapper.unmount();
|
});
|
|
it('shows a clear error instead of mounting the editor when the config request fails', async () => {
|
apiMocks.getOnlyOfficeConfig.mockRejectedValue(new Error('无权打开该 ELN 记录'));
|
const wrapper = mount(RecordView);
|
await flushPromises();
|
|
expect(wrapper.find('[data-test="record-error"]').text()).toContain('无权打开该 ELN 记录');
|
expect(wrapper.find('[data-test="record-editor"]').exists()).toBe(false);
|
});
|
});
|