/* @vitest-environment happy-dom */
|
|
import type { VueWrapper } from '@vue/test-utils';
|
|
import { mount } from '@vue/test-utils';
|
import { defineComponent, h, nextTick, onMounted } from 'vue';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import GlobalOfficeDocumentModal from '../index.vue';
|
|
const mocks = vi.hoisted(() => ({
|
afterEach: vi.fn(),
|
autoEditorReady: true,
|
closeModal: vi.fn(),
|
confirm: vi.fn(),
|
confirmDestroy: vi.fn(),
|
getOnlyOfficeConfig: vi.fn(),
|
getOnlyOfficeSaveStatus: vi.fn(),
|
grabEditorFocus: vi.fn(),
|
messageError: vi.fn(),
|
messageSuccess: vi.fn(),
|
messageWarning: vi.fn(),
|
openModal: vi.fn(),
|
printDmsDocument: vi.fn(),
|
printManagedDmsDocument: vi.fn(),
|
prepareDmsPrintAttempt: vi.fn(),
|
finishDmsPrintAttempt: vi.fn(),
|
register: vi.fn(),
|
removeRouteHook: vi.fn(),
|
requestEditorClose: vi.fn(),
|
shouldEditorClose: true,
|
}));
|
|
vi.mock('#/router', () => ({ router: { afterEach: mocks.afterEach, push: vi.fn() } }));
|
|
vi.mock('@jnpf/hooks', () => ({
|
useGlobSetting: () => ({ apiURL: '' }),
|
useMessage: () => ({
|
createConfirm: mocks.confirm,
|
createMessage: { error: mocks.messageError, success: mocks.messageSuccess, warning: mocks.messageWarning },
|
}),
|
}));
|
|
vi.mock('@jnpf/ui/modal', async () => {
|
const { defineComponent, h, onMounted } = await import('vue');
|
|
return {
|
BasicModal: defineComponent({
|
name: 'BasicModal',
|
props: { closeFunc: Function },
|
emits: ['register'],
|
setup(_, { emit, slots }) {
|
onMounted(() => emit('register'));
|
|
return () =>
|
h('section', { 'data-test': 'basic-modal' }, [
|
h('header', { 'data-test': 'modal-title' }, slots.title?.()),
|
h('main', { 'data-test': 'modal-content' }, slots.default?.()),
|
]);
|
},
|
}),
|
useModal: () => [mocks.register, { closeModal: mocks.closeModal, openModal: mocks.openModal }],
|
};
|
});
|
|
vi.mock('ant-design-vue', async () => {
|
const { defineComponent, h } = await import('vue');
|
|
return {
|
Button: defineComponent({
|
name: 'AButton',
|
emits: ['click'],
|
setup(_, { emit, slots }) {
|
return () => h('button', { type: 'button', onClick: () => emit('click') }, slots.default?.());
|
},
|
}),
|
InputNumber: defineComponent({
|
props: ['value', 'disabled'],
|
emits: ['update:value'],
|
setup(props, { emit }) {
|
return () => h('input', { value: props.value, disabled: props.disabled, onInput: (event: any) => emit('update:value', Number(event.target.value)) });
|
},
|
}),
|
Space: defineComponent({
|
name: 'ASpace',
|
setup(_, { slots }) {
|
return () => h('div', slots.default?.());
|
},
|
}),
|
Spin: defineComponent({
|
name: 'ASpin',
|
props: { tip: String },
|
setup(props) {
|
return () => h('div', { 'data-test': 'spin' }, props.tip);
|
},
|
}),
|
Tag: defineComponent({
|
name: 'ATag',
|
setup(_, { slots }) {
|
return () => h('span', { 'data-test': 'tag' }, slots.default?.());
|
},
|
}),
|
Tooltip: defineComponent({
|
name: 'ATooltip',
|
props: { placement: String, title: String },
|
setup(_, { slots }) {
|
return () => slots.default?.();
|
},
|
}),
|
};
|
});
|
|
vi.mock('#/api/x/onlyoffice', () => ({
|
getOnlyOfficeConfig: mocks.getOnlyOfficeConfig,
|
getOnlyOfficeSaveStatus: mocks.getOnlyOfficeSaveStatus,
|
}));
|
|
vi.mock('#/views/x/dms/shared/dmsHiprint', () => ({
|
DmsPrintError: class DmsPrintError extends Error {},
|
printDmsDocument: mocks.printDmsDocument,
|
printManagedDmsDocument: mocks.printManagedDmsDocument,
|
}));
|
|
vi.mock('#/views/x/dms/shared/dmsPrintAttempt', () => ({
|
prepareDmsPrintAttempt: mocks.prepareDmsPrintAttempt,
|
finishDmsPrintAttempt: mocks.finishDmsPrintAttempt,
|
}));
|
|
// 真实编辑器会去加载 DS 的 api.js,测试里换成能手动触发事件的桩
|
vi.mock('#/components/OnlyOfficeEditor/src/Editor.vue', () => ({
|
default: defineComponent({
|
name: 'OnlyOfficeEditor',
|
props: { config: { required: true, type: Object } },
|
emits: ['close', 'error', 'modified', 'ready'],
|
setup(_, { emit, expose }) {
|
onMounted(() => {
|
if (mocks.autoEditorReady) emit('ready');
|
});
|
expose({
|
grabFocus: mocks.grabEditorFocus,
|
requestClose() {
|
mocks.requestEditorClose();
|
if (mocks.shouldEditorClose) emit('close');
|
},
|
});
|
return () =>
|
h('div', {
|
'data-test': 'onlyoffice-editor',
|
onClick: () => emit('modified', true),
|
onContextmenu: () => {
|
emit('modified', true);
|
emit('modified', false);
|
},
|
onDblclick: () => {
|
emit('modified', true);
|
emit('modified', false);
|
},
|
onMouseup: () => emit('ready'),
|
});
|
},
|
}),
|
}));
|
|
const { onlineUtils } = await import('#/utils/jnpf');
|
|
const FILE = { fileId: 'file_001', name: '检验报告.docx' };
|
const CONFIG_RESPONSE = {
|
data: {
|
document: { fileType: 'docx', key: 'key_v1', title: '检验报告.docx', url: 'http://gw/api/file/Image/annex/file_001?s=sk' },
|
documentType: 'word',
|
editorConfig: { callbackUrl: 'http://gw/api/biz/onlyoffice/callback', mode: 'edit' },
|
token: 'signed.jwt.value',
|
},
|
};
|
|
let wrapper: VueWrapper<any>;
|
/** 组件注册进 router.afterEach 的那个钩子,测试里手动触发以模拟路由跳转 */
|
let routeHook: (to: any, from: any, failure?: any) => void = () => {};
|
|
async function flush(times = 3) {
|
for (let i = 0; i < times; i++) {
|
await nextTick();
|
}
|
}
|
|
describe('globalOfficeDocumentModal', () => {
|
beforeEach(() => {
|
mocks.openModal.mockClear();
|
mocks.autoEditorReady = true;
|
mocks.closeModal.mockClear();
|
mocks.confirm.mockClear();
|
mocks.confirm.mockImplementation(() => ({ destroy: mocks.confirmDestroy }));
|
mocks.confirmDestroy.mockClear();
|
mocks.removeRouteHook.mockClear();
|
mocks.getOnlyOfficeConfig.mockReset();
|
mocks.getOnlyOfficeConfig.mockResolvedValue(CONFIG_RESPONSE);
|
mocks.getOnlyOfficeSaveStatus.mockReset();
|
mocks.getOnlyOfficeSaveStatus.mockResolvedValue({ data: { lastSaveTime: null, sessionStatus: 'IDLE', version: 1 } });
|
mocks.messageSuccess.mockClear();
|
mocks.messageError.mockClear();
|
mocks.messageWarning.mockClear();
|
mocks.printManagedDmsDocument.mockReset();
|
mocks.prepareDmsPrintAttempt.mockReset();
|
mocks.finishDmsPrintAttempt.mockReset();
|
mocks.printDmsDocument.mockReset();
|
mocks.printDmsDocument.mockResolvedValue({ mode: 'hiprint' });
|
mocks.requestEditorClose.mockClear();
|
mocks.grabEditorFocus.mockClear();
|
mocks.shouldEditorClose = true;
|
mocks.afterEach.mockReset();
|
mocks.afterEach.mockImplementation((hook) => {
|
routeHook = hook;
|
return mocks.removeRouteHook;
|
});
|
wrapper = mount(GlobalOfficeDocumentModal);
|
});
|
|
afterEach(() => {
|
wrapper?.unmount();
|
});
|
|
it('重打预览打开不派发,手动发送成功才刷新关闭,固定一份', async () => {
|
const send = vi.fn().mockResolvedValue(undefined);
|
const refresh = vi.fn();
|
onlineUtils.openOfficeDocument({ file: FILE, fileType: 'dms', mode: 'view', dmsPrint: {
|
fileVersionId: 'version', copies: 1, onPrintSuccess: refresh,
|
reprint: { requestId: 'request', printRecordId: 'record', send },
|
} });
|
await flush();
|
expect(send).not.toHaveBeenCalled();
|
expect(wrapper.text()).toContain('重打份数:1 份');
|
expect(wrapper.find('[data-test="dms-print-icon"]').exists()).toBe(true);
|
expect(wrapper.find('[aria-label="关闭"]').exists()).toBe(true);
|
expect(wrapper.find('#dms-print-copies').exists()).toBe(false);
|
await wrapper.find('[aria-label="打印"]').trigger('click');
|
await flush(6);
|
expect(send).toHaveBeenCalledTimes(1);
|
expect(refresh).toHaveBeenCalledTimes(1);
|
expect(mocks.closeModal).toHaveBeenCalledTimes(1);
|
expect(mocks.printDmsDocument).not.toHaveBeenCalled();
|
expect(mocks.prepareDmsPrintAttempt).not.toHaveBeenCalled();
|
});
|
it('重打预览直接关闭不派发,不更新执行状态', async () => {
|
const send = vi.fn(); const refresh = vi.fn();
|
onlineUtils.openOfficeDocument({ file: FILE, fileType: 'dms', mode: 'view', dmsPrint: {
|
fileVersionId: 'version', onPrintSuccess: refresh, reprint: { requestId: 'request', printRecordId: 'record', send },
|
} });
|
await flush();
|
await wrapper.find('[aria-label="关闭"]').trigger('click');
|
await flush();
|
expect(send).not.toHaveBeenCalled(); expect(refresh).not.toHaveBeenCalled();
|
expect(mocks.closeModal).toHaveBeenCalledTimes(1);
|
});
|
it('重打超时停止loading,迟到成功通知仍关闭且只刷新一次', async () => {
|
const send = vi.fn().mockRejectedValue(new Error('发送结果未知')); const refresh = vi.fn();
|
onlineUtils.openOfficeDocument({ file: FILE, fileType: 'dms', mode: 'view', dmsPrint: {
|
fileVersionId: 'version', onPrintSuccess: refresh, reprint: { requestId: 'request', printRecordId: 'record', send },
|
} });
|
await flush();
|
await wrapper.find('[aria-label="打印"]').trigger('click');
|
await flush(6);
|
expect(wrapper.find('[data-test="dms-print-loading-icon"]').exists()).toBe(false);
|
expect(mocks.closeModal).not.toHaveBeenCalled();
|
onlineUtils.getEmitter().emit('DMS_REPRINT_COMPLETED', { printRecordId: 'other', requestId: 'request' });
|
await flush(); expect(refresh).not.toHaveBeenCalled();
|
onlineUtils.getEmitter().emit('DMS_REPRINT_COMPLETED', { printRecordId: 'record', requestId: 'request' });
|
onlineUtils.getEmitter().emit('DMS_REPRINT_COMPLETED', { printRecordId: 'record', requestId: 'request' });
|
await flush(6);
|
expect(refresh).toHaveBeenCalledTimes(1); expect(mocks.closeModal).toHaveBeenCalledTimes(1);
|
});
|
|
it('拿到后端 config 后才渲染编辑器,且透传文档参数和业务场景', async () => {
|
onlineUtils.openOfficeDocument({ file: FILE, bizScene: 'eln.template.annotate' });
|
await flush();
|
|
expect(mocks.openModal).toHaveBeenCalledWith(true);
|
expect(mocks.getOnlyOfficeConfig).toHaveBeenCalledWith(
|
expect.objectContaining({ bizScene: 'eln.template.annotate', fileId: 'file_001', fileName: '检验报告.docx', fileType: 'annex', mode: 'edit' }),
|
);
|
expect(mocks.getOnlyOfficeSaveStatus).toHaveBeenCalledWith('file_001', 'annex');
|
expect(wrapper.find('[data-test="onlyoffice-editor"]').exists()).toBe(true);
|
});
|
|
it('弹窗打开失败时提示并通知调用方,之后可以重试', async () => {
|
const error = new Error('useModal instance is undefined!');
|
const onOpenError = vi.fn();
|
mocks.openModal.mockImplementationOnce(() => {
|
throw error;
|
});
|
onlineUtils.openOfficeDocument({ file: FILE, onOpenError });
|
await flush();
|
expect(mocks.messageError).toHaveBeenCalledWith(error.message);
|
expect(onOpenError).toHaveBeenCalledWith(error);
|
expect(mocks.getOnlyOfficeConfig).not.toHaveBeenCalled();
|
onlineUtils.openOfficeDocument({ file: FILE, mode: 'view' });
|
await flush();
|
expect(mocks.getOnlyOfficeConfig).toHaveBeenCalled();
|
});
|
|
it('关闭预览后可打开上传附件编辑,并显示配置接口错误', async () => {
|
mocks.getOnlyOfficeConfig.mockResolvedValueOnce({ data: { ...CONFIG_RESPONSE.data, editorConfig: { mode: 'view' } } });
|
onlineUtils.openOfficeDocument({ file: FILE, mode: 'view' });
|
await flush();
|
await wrapper.find('button[aria-label="关闭"]').trigger('click');
|
await flush();
|
mocks.getOnlyOfficeConfig.mockRejectedValueOnce(new Error('系统异常'));
|
onlineUtils.openOfficeDocument({ file: { ...FILE, fileId: 'lease' }, bizScene: 'visualdev.uploadFile', mode: 'edit' });
|
await flush(6);
|
expect(mocks.openModal).toHaveBeenCalledTimes(2);
|
expect(wrapper.find('.state-pane').text()).toBe('系统异常');
|
});
|
|
it('DMS 预览把文件类型和短期票据交给配置签发接口', async () => {
|
onlineUtils.openOfficeDocument({
|
dmsAccessTicket: 'ticket-1',
|
file: { fileId: 'version-1', name: '质量手册.docx' },
|
fileType: 'dms',
|
mode: 'view',
|
});
|
await flush();
|
|
expect(mocks.getOnlyOfficeConfig).toHaveBeenCalledWith(
|
expect.objectContaining({
|
dmsAccessTicket: 'ticket-1',
|
fileId: 'version-1',
|
fileName: '质量手册.docx',
|
fileType: 'dms',
|
mode: 'view',
|
}),
|
);
|
});
|
|
it('DMS 票据失效时静默换票并继续打开预览', async () => {
|
mocks.getOnlyOfficeConfig.mockRejectedValueOnce(new Error('DMS 预览票据无效或已过期'));
|
const refresh = vi.fn().mockResolvedValue({ accessTicket: 'fresh-ticket', fileVersionId: 'version-1', fileName: '质量手册.docx' });
|
onlineUtils.openOfficeDocument({
|
dmsAccessTicket: 'expired-ticket',
|
refreshDmsPreviewTicket: refresh,
|
file: { fileId: 'version-1', name: '质量手册.docx' },
|
fileType: 'dms',
|
mode: 'view',
|
});
|
await flush(12);
|
expect(refresh).toHaveBeenCalledTimes(1);
|
expect(mocks.getOnlyOfficeConfig).toHaveBeenLastCalledWith(expect.objectContaining({ dmsAccessTicket: 'fresh-ticket', fileId: 'version-1' }), {
|
errorMessageMode: 'none',
|
});
|
expect(wrapper.find('[data-test="onlyoffice-editor"]').exists()).toBe(true);
|
expect(mocks.messageError).not.toHaveBeenCalled();
|
});
|
|
it('仅为携带打印上下文的 DMS 预览显示打印按钮', async () => {
|
onlineUtils.openOfficeDocument({ file: FILE, mode: 'view' });
|
await flush();
|
expect(wrapper.find('[aria-label="打印"]').exists()).toBe(false);
|
|
onlineUtils.openOfficeDocument({
|
dmsPrint: { copies: 1, fileVersionId: 'version-1', pageVariables: [] },
|
file: { fileId: 'version-1', name: '质量手册.docx' },
|
fileType: 'dms',
|
mode: 'view',
|
});
|
await flush();
|
expect(wrapper.find('[aria-label="打印"]').exists()).toBe(true);
|
});
|
|
it('OnlyOffice 文档未 ready 前禁用打印,ready 后才允许点击', async () => {
|
mocks.autoEditorReady = false;
|
onlineUtils.openOfficeDocument({
|
dmsPrint: { copies: 1, fileVersionId: 'version-1', pageVariables: [] },
|
file: { fileId: 'version-1', name: '质量手册.docx' },
|
fileType: 'dms',
|
mode: 'view',
|
});
|
await flush();
|
|
const printButton = wrapper.find('[aria-label="打印"]');
|
expect(printButton.attributes('disabled')).toBeDefined();
|
await printButton.trigger('click');
|
expect(mocks.printDmsDocument).not.toHaveBeenCalled();
|
|
await wrapper.find('[data-test="onlyoffice-editor"]').trigger('mouseup');
|
await flush();
|
expect(wrapper.find('[aria-label="打印"]').attributes('disabled')).toBeUndefined();
|
});
|
|
it('Hiprint 离线时不进入打印票据和 PDF 编排链路', async () => {
|
const ready = vi.spyOn(onlineUtils, 'isHiprintClientReady').mockResolvedValue(false);
|
onlineUtils.openOfficeDocument({
|
dmsPrint: { copies: 1, fileVersionId: 'version-1', pageVariables: [] },
|
file: { fileId: 'version-1', name: '质量手册.docx' },
|
fileType: 'dms',
|
mode: 'view',
|
});
|
await flush();
|
|
await wrapper.find('[aria-label="打印"]').trigger('click');
|
await flush();
|
|
expect(mocks.printDmsDocument).not.toHaveBeenCalled();
|
expect(mocks.messageError).toHaveBeenCalledWith('打印客户端未连接,请启动客户端后重试');
|
ready.mockRestore();
|
});
|
|
it('打印前回调成功后才进入 PDF 编排,回调失败时终止打印', async () => {
|
const beforePrint = vi.fn().mockResolvedValueOnce(undefined).mockRejectedValueOnce(new Error('打印确认失败'));
|
const ready = vi.spyOn(onlineUtils, 'isHiprintClientReady').mockResolvedValue(true);
|
onlineUtils.openOfficeDocument({
|
dmsPrint: { beforePrint, copies: 1, fileVersionId: 'version-1', pageVariables: [] },
|
file: { fileId: 'version-1', name: '质量手册.docx' },
|
fileType: 'dms',
|
mode: 'view',
|
});
|
await flush();
|
|
await wrapper.find('[aria-label="打印"]').trigger('click');
|
await flush();
|
expect(beforePrint).toHaveBeenCalledTimes(1);
|
expect(mocks.printDmsDocument).toHaveBeenCalledTimes(1);
|
|
await wrapper.find('[aria-label="打印"]').trigger('click');
|
await flush();
|
expect(beforePrint).toHaveBeenCalledTimes(2);
|
expect(mocks.printDmsDocument).toHaveBeenCalledTimes(1);
|
expect(mocks.messageError).toHaveBeenCalledWith('打印确认失败');
|
ready.mockRestore();
|
});
|
|
it('打印中和打印完成时原子切换加载与打印图标', async () => {
|
let completePrint: (value: unknown) => void = () => {};
|
mocks.printDmsDocument.mockImplementation(
|
() =>
|
new Promise((resolve) => {
|
completePrint = resolve;
|
}),
|
);
|
const ready = vi.spyOn(onlineUtils, 'isHiprintClientReady').mockResolvedValue(true);
|
onlineUtils.openOfficeDocument({
|
dmsPrint: { copies: 1, fileVersionId: 'version-1', pageVariables: [] },
|
file: { fileId: 'version-1', name: '质量手册.docx' },
|
fileType: 'dms',
|
mode: 'view',
|
});
|
await flush();
|
|
await wrapper.find('[aria-label="打印"]').trigger('click');
|
await flush();
|
expect(wrapper.find('[data-test="dms-print-loading-icon"]').exists()).toBe(true);
|
expect(wrapper.find('[data-test="dms-print-icon"]').exists()).toBe(false);
|
|
completePrint({ mode: 'hiprint' });
|
await flush(5);
|
expect(wrapper.find('[data-test="dms-print-loading-icon"]').exists()).toBe(false);
|
expect(wrapper.find('[data-test="dms-print-icon"]').exists()).toBe(true);
|
ready.mockRestore();
|
});
|
|
it('所有 Hiprint 批次确认成功后等待业务回调再关闭预览', async () => {
|
const order: string[] = [];
|
const onPrintSuccess = vi.fn(() => {
|
order.push('refresh');
|
});
|
mocks.closeModal.mockImplementation(() => order.push('close'));
|
mocks.printDmsDocument.mockImplementation(async (_fileName, printConfig) => {
|
await printConfig.onPrintSuccess?.();
|
return { mode: 'hiprint' };
|
});
|
const ready = vi.spyOn(onlineUtils, 'isHiprintClientReady').mockResolvedValue(true);
|
onlineUtils.openOfficeDocument({
|
dmsPrint: {
|
closeOnPrintSuccess: true,
|
copies: 1,
|
fileVersionId: 'version-1',
|
onPrintSuccess,
|
pageVariables: [],
|
},
|
file: { fileId: 'version-1', name: '质量手册.docx' },
|
fileType: 'dms',
|
mode: 'view',
|
});
|
await flush();
|
|
await wrapper.find('[aria-label="打印"]').trigger('click');
|
await flush();
|
|
expect(order).toEqual(['refresh', 'close']);
|
expect(mocks.closeModal).toHaveBeenCalledTimes(1);
|
expect(onPrintSuccess).toHaveBeenCalledTimes(1);
|
ready.mockRestore();
|
});
|
|
it('成功回传后的确认失败仅重试确认,期间阻止关闭和新预览', async () => {
|
const attempt = { id: 'attempt', taskId: 'task', copies: 3, state: 'OPEN' };
|
mocks.prepareDmsPrintAttempt.mockResolvedValue(attempt);
|
mocks.finishDmsPrintAttempt.mockRejectedValueOnce(new Error('确认网络异常')).mockResolvedValueOnce(undefined);
|
const refresh = vi.fn();
|
const ready = vi.spyOn(onlineUtils, 'isHiprintClientReady').mockResolvedValue(true);
|
mocks.printManagedDmsDocument.mockResolvedValue(undefined);
|
onlineUtils.openOfficeDocument({
|
dmsPrint: {
|
printQuantity: { total: 10, printed: 2 },
|
printTaskId: 'task',
|
onPrintSuccess: refresh,
|
closeOnPrintSuccess: true,
|
copies: 1,
|
fileVersionId: 'version-1',
|
},
|
file: { fileId: 'version-1', name: '质量手册.docx' },
|
fileType: 'dms',
|
mode: 'view',
|
});
|
await flush();
|
expect(wrapper.find('[data-test="print-quantity-summary"]').text()).toContain('剩余份数:8');
|
expect((wrapper.find('[aria-label="本次打印份数"]').element as HTMLInputElement).value).toBe('1');
|
await wrapper.find('[aria-label="本次打印份数"]').setValue('3');
|
await wrapper.find('[aria-label="打印"]').trigger('click');
|
await flush(5);
|
expect(mocks.finishDmsPrintAttempt).toHaveBeenCalledWith(attempt);
|
expect(mocks.printManagedDmsDocument.mock.calls[0]![2].copies).toBe(3);
|
expect(wrapper.find('[aria-label="本次打印份数"]').attributes('disabled')).toBeDefined();
|
expect(refresh).not.toHaveBeenCalled();
|
expect(mocks.closeModal).not.toHaveBeenCalled();
|
await wrapper.find('[aria-label="关闭"]').trigger('click');
|
onlineUtils.openOfficeDocument({ file: { fileId: 'other', name: 'other.docx' }, mode: 'view' });
|
await flush();
|
expect(mocks.closeModal).not.toHaveBeenCalled();
|
await wrapper.find('[aria-label="重试记录确认"]').trigger('click');
|
await flush(5);
|
expect(mocks.finishDmsPrintAttempt).toHaveBeenCalledTimes(2);
|
expect(mocks.finishDmsPrintAttempt).toHaveBeenLastCalledWith(attempt);
|
expect(mocks.printManagedDmsDocument).toHaveBeenCalledTimes(1);
|
expect(mocks.prepareDmsPrintAttempt).toHaveBeenCalledTimes(1);
|
expect(refresh).toHaveBeenCalledTimes(1);
|
expect(mocks.closeModal).toHaveBeenCalledTimes(1);
|
ready.mockRestore();
|
});
|
|
it('旧台账配置在发送前阻断,不能先打印后被后端拒绝', async () => {
|
onlineUtils.openOfficeDocument({
|
dmsPrint: { printQuantity: { total: 10, printed: 0 }, confirmPrint: vi.fn(), fileVersionId: 'version-1' },
|
file: { fileId: 'version-1', name: '质量手册.docx' },
|
fileType: 'dms',
|
mode: 'view',
|
});
|
await flush();
|
await wrapper.find('[aria-label="打印"]').trigger('click');
|
await flush();
|
expect(mocks.printDmsDocument).not.toHaveBeenCalled();
|
expect(mocks.messageError).toHaveBeenCalledWith(expect.stringContaining('台账配置需要更新'));
|
});
|
|
it('重新打开后恢复原技术任务份数,未知结果只走恢复入口', async () => {
|
const attempt = { id: 'original', taskId: 'task', copies: 2, state: 'OPEN' };
|
mocks.prepareDmsPrintAttempt.mockResolvedValue(attempt);
|
mocks.printManagedDmsDocument.mockRejectedValueOnce(new Error('发送结果未知')).mockResolvedValueOnce(undefined);
|
mocks.finishDmsPrintAttempt.mockResolvedValue(undefined);
|
onlineUtils.openOfficeDocument({
|
dmsPrint: { printTaskId: 'task', printQuantity: { total: 10, printed: 1 }, fileVersionId: 'version-1' },
|
file: { fileId: 'version-1', name: '质量手册.docx' },
|
fileType: 'dms',
|
mode: 'view',
|
});
|
await flush();
|
await wrapper.find('[aria-label="打印"]').trigger('click');
|
await flush(5);
|
expect((wrapper.find('[aria-label="本次打印份数"]').element as HTMLInputElement).value).toBe('2');
|
expect(mocks.finishDmsPrintAttempt).not.toHaveBeenCalled();
|
await wrapper.find('[aria-label="恢复打印任务"]').trigger('click');
|
await flush(5);
|
expect(mocks.prepareDmsPrintAttempt).toHaveBeenCalledTimes(2);
|
expect(mocks.printManagedDmsDocument.mock.calls[1]![2].id).toBe('original');
|
expect(mocks.finishDmsPrintAttempt).toHaveBeenCalledTimes(1);
|
expect(mocks.printDmsDocument).not.toHaveBeenCalled();
|
});
|
|
it('异步确认未完成前不刷新、不关闭且保持忙碌', async () => {
|
let resolveConfirmation!: () => void;
|
const confirmPrint = vi.fn(
|
() =>
|
new Promise<void>((resolve) => {
|
resolveConfirmation = resolve;
|
}),
|
);
|
const refresh = vi.fn();
|
const ready = vi.spyOn(onlineUtils, 'isHiprintClientReady').mockResolvedValue(true);
|
mocks.printDmsDocument.mockImplementation(async (_name, options) => {
|
await options.onPrintSuccess();
|
});
|
onlineUtils.openOfficeDocument({
|
dmsPrint: { confirmPrint, onPrintSuccess: refresh, closeOnPrintSuccess: true, copies: 1, fileVersionId: 'version-1' },
|
file: { fileId: 'version-1', name: '质量手册.docx' },
|
fileType: 'dms',
|
mode: 'view',
|
});
|
await flush();
|
await wrapper.find('[aria-label="打印"]').trigger('click');
|
await flush();
|
expect(confirmPrint).toHaveBeenCalledTimes(1);
|
expect(refresh).not.toHaveBeenCalled();
|
expect(mocks.closeModal).not.toHaveBeenCalled();
|
expect(wrapper.find('[aria-label="正在打印"]').exists()).toBe(true);
|
resolveConfirmation();
|
await flush(5);
|
expect(refresh).toHaveBeenCalledTimes(1);
|
expect(mocks.closeModal).toHaveBeenCalledTimes(1);
|
ready.mockRestore();
|
});
|
|
it('打印失败时不关闭预览且不执行成功回调', async () => {
|
const onPrintSuccess = vi.fn();
|
mocks.printDmsDocument.mockRejectedValue(new Error('打印失败'));
|
const ready = vi.spyOn(onlineUtils, 'isHiprintClientReady').mockResolvedValue(true);
|
onlineUtils.openOfficeDocument({
|
dmsPrint: {
|
closeOnPrintSuccess: true,
|
copies: 1,
|
fileVersionId: 'version-1',
|
onPrintSuccess,
|
pageVariables: [],
|
},
|
file: { fileId: 'version-1', name: '质量手册.docx' },
|
fileType: 'dms',
|
mode: 'view',
|
});
|
await flush();
|
|
await wrapper.find('[aria-label="打印"]').trigger('click');
|
await flush();
|
|
expect(mocks.closeModal).not.toHaveBeenCalled();
|
expect(onPrintSuccess).not.toHaveBeenCalled();
|
expect(wrapper.find('[data-test="dms-print-loading-icon"]').exists()).toBe(false);
|
expect(wrapper.find('[data-test="dms-print-icon"]').exists()).toBe(true);
|
expect(mocks.messageError).toHaveBeenCalledWith('打印失败');
|
ready.mockRestore();
|
});
|
|
it('批注预览不查询保存状态,审批模式沿用保存基线', async () => {
|
onlineUtils.openOfficeDocument({ file: FILE, mode: 'viewComments' });
|
await flush();
|
expect(mocks.getOnlyOfficeSaveStatus).not.toHaveBeenCalled();
|
expect(mocks.getOnlyOfficeConfig).toHaveBeenCalledWith(expect.objectContaining({ mode: 'viewComments' }));
|
onlineUtils.openOfficeDocument({ file: FILE, mode: 'review' });
|
await flush();
|
expect(mocks.getOnlyOfficeSaveStatus).toHaveBeenCalledWith('file_001', 'annex');
|
expect(mocks.getOnlyOfficeConfig).toHaveBeenCalledWith(expect.objectContaining({ mode: 'review' }));
|
});
|
|
it('签批页模式按可编辑模式获取配置与保存状态', async () => {
|
onlineUtils.openOfficeDocument({ file: FILE, mode: 'qianpiye' });
|
await flush();
|
|
expect(mocks.getOnlyOfficeSaveStatus).toHaveBeenCalledWith('file_001', 'annex');
|
expect(mocks.getOnlyOfficeConfig).toHaveBeenCalledWith(
|
expect.objectContaining({ bizModule: 'dms', bizScene: 'dms.qianpiye', fileId: 'file_001', mode: 'qianpiye' }),
|
);
|
});
|
|
it('config 和状态查询使用同一文件存储类型', async () => {
|
onlineUtils.openOfficeDocument({ file: FILE, fileType: 'temporary' });
|
await flush();
|
|
expect(mocks.getOnlyOfficeConfig).toHaveBeenCalledWith(expect.objectContaining({ fileType: 'temporary' }));
|
expect(mocks.getOnlyOfficeSaveStatus).toHaveBeenCalledWith('file_001', 'temporary');
|
});
|
|
it('编辑器挂载前必须取得保存基线,避免首次 status 6 被误当成基线', async () => {
|
mocks.getOnlyOfficeSaveStatus.mockRejectedValueOnce(new Error('save-status unavailable'));
|
|
onlineUtils.openOfficeDocument({ file: FILE });
|
await flush();
|
|
expect(mocks.getOnlyOfficeConfig).not.toHaveBeenCalled();
|
expect(wrapper.find('[data-test="onlyoffice-editor"]').exists()).toBe(false);
|
expect(wrapper.find('[data-test="modal-content"]').text()).toContain('save-status unavailable');
|
});
|
|
it('普通标注不发送 repairBindingId,修复时准确透传绑定 ID', async () => {
|
onlineUtils.openOfficeDocument({ file: FILE, bizScene: 'eln.template.annotate' });
|
await flush();
|
|
expect(mocks.getOnlyOfficeConfig).toHaveBeenLastCalledWith(expect.not.objectContaining({ repairBindingId: expect.anything() }));
|
|
onlineUtils.openOfficeDocument({ file: FILE, bizScene: 'eln.template.annotate', repairBindingId: 'binding-1' });
|
await flush();
|
|
expect(mocks.getOnlyOfficeConfig).toHaveBeenLastCalledWith(expect.objectContaining({ repairBindingId: 'binding-1' }));
|
});
|
|
it('普通打开不发送 locateControlTag,体检定位时准确透传控件 Tag', async () => {
|
onlineUtils.openOfficeDocument({ file: FILE, bizScene: 'eln.template.annotate' });
|
await flush();
|
expect(mocks.getOnlyOfficeConfig).toHaveBeenLastCalledWith(expect.not.objectContaining({ locateControlTag: expect.anything() }));
|
|
onlineUtils.openOfficeDocument({ file: FILE, bizScene: 'eln.template.annotate', locateControlTag: 'eln.field.orphan', mode: 'view' });
|
await flush();
|
expect(mocks.getOnlyOfficeConfig).toHaveBeenLastCalledWith(expect.objectContaining({ locateControlTag: 'eln.field.orphan', mode: 'view' }));
|
});
|
|
it('后端把 edit 降级为 view 时给出只读提示', async () => {
|
mocks.getOnlyOfficeConfig.mockResolvedValue({
|
data: { ...CONFIG_RESPONSE.data, editorConfig: { ...CONFIG_RESPONSE.data.editorConfig, mode: 'view' } },
|
});
|
|
onlineUtils.openOfficeDocument({ file: FILE, mode: 'edit' });
|
await flush();
|
|
// 标签紧跟标题,不在右侧按钮区——放右边太容易被当成装饰略过
|
expect(wrapper.find('.header-main [data-test="tag"]').exists()).toBe(true);
|
});
|
|
it('config 缺 token 时不渲染编辑器——没有签名 DS 一定会拒,早报错好过让编辑器空转', async () => {
|
mocks.getOnlyOfficeConfig.mockResolvedValue({ data: { document: { key: 'key_v1' } } });
|
|
onlineUtils.openOfficeDocument({ file: FILE });
|
await flush();
|
|
expect(wrapper.find('[data-test="onlyoffice-editor"]').exists()).toBe(false);
|
expect(wrapper.text()).toContain('编辑器配置不完整');
|
});
|
|
it('改动过再关闭才触发 onSave,onClose 则总会触发', async () => {
|
const onSave = vi.fn();
|
const onClose = vi.fn();
|
|
onlineUtils.openOfficeDocument({ file: FILE, onClose, onSave });
|
await flush();
|
|
await wrapper.find('[data-test="onlyoffice-editor"]').trigger('click'); // 桩组件的 click = emit modified
|
await wrapper.find('button').trigger('click'); // 头部关闭按钮
|
await flush();
|
|
expect(mocks.confirm).toHaveBeenCalledWith(
|
expect.objectContaining({
|
cancelText: '继续编辑',
|
content: '关闭后,当前未保存的文档更改将丢失。',
|
okText: '放弃并关闭',
|
okType: 'danger',
|
title: '放弃未保存的更改?',
|
}),
|
);
|
expect(mocks.requestEditorClose).not.toHaveBeenCalled();
|
mocks.confirm.mock.calls[0]?.[0].onOk();
|
await flush();
|
|
expect(mocks.requestEditorClose).not.toHaveBeenCalled();
|
expect(onSave).toHaveBeenCalledWith({ file: FILE, modified: true });
|
expect(onClose).toHaveBeenCalledTimes(1);
|
expect(mocks.closeModal).toHaveBeenCalled();
|
});
|
|
it('提交收尾委托编辑器关闭并等待服务端释放,不走放弃修改分支', async () => {
|
const registerSubmitPreparation = vi.fn();
|
const onClose = vi.fn();
|
onlineUtils.openOfficeDocument({ file: FILE, onClose, registerSubmitPreparation });
|
await flush();
|
await wrapper.find('[data-test="onlyoffice-editor"]').trigger('click');
|
let release!: (value: unknown) => void;
|
mocks.getOnlyOfficeSaveStatus.mockImplementationOnce(() => new Promise((resolve) => { release = resolve; }));
|
const prepare = registerSubmitPreparation.mock.calls[0]![0];
|
let finished = false;
|
const pending = prepare().then(() => { finished = true; });
|
await flush();
|
expect(mocks.requestEditorClose).toHaveBeenCalledOnce();
|
expect(mocks.confirm).not.toHaveBeenCalled();
|
expect(finished).toBe(false);
|
release({ data: { sessionStatus: 'IDLE', lastSaveTime: 200 } });
|
await pending;
|
expect(onClose).toHaveBeenCalledOnce();
|
expect(registerSubmitPreparation).toHaveBeenLastCalledWith(undefined);
|
});
|
|
it('没改动就关闭不触发 onSave', async () => {
|
const onSave = vi.fn();
|
|
onlineUtils.openOfficeDocument({ file: FILE, onSave });
|
await flush();
|
await wrapper.find('button').trigger('click');
|
await flush();
|
|
expect(onSave).not.toHaveBeenCalled();
|
});
|
|
it('编辑器关闭后等待后端会话空闲才完成外层关闭', async () => {
|
const onClose = vi.fn();
|
let resolveReleaseStatus: (value: any) => void = () => {};
|
mocks.getOnlyOfficeSaveStatus
|
.mockResolvedValueOnce({ data: { lastSaveTime: null, sessionStatus: 'IDLE', version: 1 } })
|
.mockReturnValueOnce(new Promise((resolve) => (resolveReleaseStatus = resolve)));
|
|
onlineUtils.openOfficeDocument({ file: FILE, onClose });
|
await flush();
|
await wrapper.find('button').trigger('click');
|
await flush();
|
|
expect(mocks.requestEditorClose).toHaveBeenCalledTimes(1);
|
expect(wrapper.find('[data-test="onlyoffice-editor"]').exists()).toBe(false);
|
expect(wrapper.find('[data-test="document-saving-mask"]').text()).toContain('正在关闭文档');
|
expect(mocks.closeModal).not.toHaveBeenCalled();
|
expect(onClose).not.toHaveBeenCalled();
|
|
resolveReleaseStatus({ data: { lastSaveTime: null, sessionStatus: 'IDLE', version: 2 } });
|
await flush();
|
|
expect(mocks.closeModal).toHaveBeenCalledTimes(1);
|
expect(onClose).toHaveBeenCalledTimes(1);
|
});
|
|
it('等待会话释放期间路由切换也不能提前完成关闭', async () => {
|
let resolveReleaseStatus: (value: any) => void = () => {};
|
mocks.getOnlyOfficeSaveStatus
|
.mockResolvedValueOnce({ data: { lastSaveTime: null, sessionStatus: 'IDLE', version: 1 } })
|
.mockReturnValueOnce(new Promise((resolve) => (resolveReleaseStatus = resolve)));
|
|
onlineUtils.openOfficeDocument({ file: FILE });
|
await flush();
|
await wrapper.find('button').trigger('click');
|
await flush();
|
|
routeHook({ fullPath: '/b' }, { fullPath: '/a' });
|
await flush();
|
|
expect(mocks.closeModal).not.toHaveBeenCalled();
|
|
resolveReleaseStatus({ data: { lastSaveTime: null, sessionStatus: 'IDLE', version: 2 } });
|
await flush();
|
|
expect(mocks.closeModal).toHaveBeenCalledTimes(1);
|
});
|
|
it('会话释放超时不强制关闭并允许用户重试', async () => {
|
vi.useFakeTimers();
|
mocks.getOnlyOfficeSaveStatus
|
.mockResolvedValueOnce({ data: { lastSaveTime: null, sessionStatus: 'IDLE', version: 1 } })
|
.mockResolvedValue({ data: { lastSaveTime: null, sessionStatus: 'EDITING', version: 1 } });
|
|
onlineUtils.openOfficeDocument({ file: FILE });
|
await flush();
|
await wrapper.find('button').trigger('click');
|
await flush();
|
await vi.advanceTimersByTimeAsync(60_000);
|
await flush();
|
|
expect(mocks.closeModal).not.toHaveBeenCalled();
|
expect(mocks.messageError).toHaveBeenCalledWith('文档会话尚未结束,请稍后重试关闭');
|
|
mocks.getOnlyOfficeSaveStatus.mockResolvedValueOnce({ data: { lastSaveTime: null, sessionStatus: 'IDLE', version: 2 } });
|
await wrapper.find('button').trigger('click');
|
await flush();
|
|
expect(mocks.closeModal).toHaveBeenCalledTimes(1);
|
vi.useRealTimers();
|
});
|
|
it('选择继续编辑后等确认框退场再恢复文档焦点,保留原编辑器', async () => {
|
const onClose = vi.fn();
|
|
onlineUtils.openOfficeDocument({ file: FILE, onClose });
|
await flush();
|
await wrapper.find('[data-test="onlyoffice-editor"]').trigger('click');
|
await wrapper.find('button').trigger('click');
|
await flush();
|
|
expect(mocks.confirm).toHaveBeenCalledTimes(1);
|
expect(mocks.requestEditorClose).not.toHaveBeenCalled();
|
const confirm = mocks.confirm.mock.calls[0]?.[0];
|
expect(confirm.focusTriggerAfterClose).toBe(false);
|
confirm.onCancel();
|
await flush();
|
|
expect(mocks.grabEditorFocus).not.toHaveBeenCalled();
|
await confirm.afterClose();
|
expect(mocks.grabEditorFocus).toHaveBeenCalledTimes(1);
|
|
expect(mocks.closeModal).not.toHaveBeenCalled();
|
expect(onClose).not.toHaveBeenCalled();
|
expect(wrapper.find('[data-test="onlyoffice-editor"]').exists()).toBe(true);
|
});
|
|
it('确认放弃并关闭时不恢复文档焦点', async () => {
|
onlineUtils.openOfficeDocument({ file: FILE });
|
await flush();
|
await wrapper.find('[data-test="onlyoffice-editor"]').trigger('click');
|
await wrapper.find('button').trigger('click');
|
const confirm = mocks.confirm.mock.calls[0]?.[0];
|
|
await confirm.onOk();
|
await confirm.afterClose();
|
|
expect(mocks.grabEditorFocus).not.toHaveBeenCalled();
|
expect(mocks.closeModal).toHaveBeenCalledTimes(1);
|
});
|
|
it('继续编辑后打开另一文档,旧确认框退场不能抢新文档焦点', async () => {
|
onlineUtils.openOfficeDocument({ file: FILE });
|
await flush();
|
await wrapper.find('[data-test="onlyoffice-editor"]').trigger('click');
|
await wrapper.find('button').trigger('click');
|
const confirm = mocks.confirm.mock.calls[0]?.[0];
|
confirm.onCancel();
|
|
onlineUtils.openOfficeDocument({ file: { fileId: 'file_002', name: '另一文档.docx' } });
|
await flush();
|
await confirm.afterClose();
|
|
expect(mocks.grabEditorFocus).not.toHaveBeenCalled();
|
});
|
|
it('连续弹出关闭确认时,旧确认框退场不能抢当前确认框焦点', async () => {
|
onlineUtils.openOfficeDocument({ file: FILE });
|
await flush();
|
await wrapper.find('[data-test="onlyoffice-editor"]').trigger('click');
|
await wrapper.find('button').trigger('click');
|
const firstConfirm = mocks.confirm.mock.calls[0]?.[0];
|
firstConfirm.onCancel();
|
await wrapper.find('button').trigger('click');
|
await firstConfirm.afterClose();
|
expect(mocks.grabEditorFocus).not.toHaveBeenCalled();
|
|
const secondConfirm = mocks.confirm.mock.calls[1]?.[0];
|
secondConfirm.onCancel();
|
await secondConfirm.afterClose();
|
expect(mocks.grabEditorFocus).toHaveBeenCalledTimes(1);
|
});
|
|
it('未保存确认框打开期间忽略重复关闭请求', async () => {
|
onlineUtils.openOfficeDocument({ file: FILE });
|
await flush();
|
await wrapper.find('[data-test="onlyoffice-editor"]').trigger('click');
|
|
await wrapper.find('button').trigger('click');
|
await wrapper.find('button').trigger('click');
|
await flush();
|
|
expect(mocks.confirm).toHaveBeenCalledTimes(1);
|
expect(mocks.closeModal).not.toHaveBeenCalled();
|
});
|
|
it('确认框打开后路由跳转会关闭确认框和编辑器', async () => {
|
onlineUtils.openOfficeDocument({ file: FILE });
|
await flush();
|
await wrapper.find('[data-test="onlyoffice-editor"]').trigger('click');
|
await wrapper.find('button').trigger('click');
|
await flush();
|
|
routeHook({ fullPath: '/b' }, { fullPath: '/a' });
|
await flush();
|
|
expect(mocks.confirmDestroy).toHaveBeenCalledTimes(1);
|
expect(mocks.closeModal).toHaveBeenCalledTimes(1);
|
expect(wrapper.find('[data-test="onlyoffice-editor"]').exists()).toBe(false);
|
});
|
|
it('onlyoffice 关闭请求也进入应用未保存确认流程', async () => {
|
onlineUtils.openOfficeDocument({ file: FILE });
|
await flush();
|
const editor = wrapper.getComponent({ name: 'OnlyOfficeEditor' });
|
|
editor.vm.$emit('modified', true);
|
editor.vm.$emit('close');
|
await flush();
|
|
expect(mocks.confirm).toHaveBeenCalledWith(expect.objectContaining({ title: '放弃未保存的更改?' }));
|
expect(mocks.closeModal).not.toHaveBeenCalled();
|
});
|
|
it('关闭前业务校验失败时使用确认弹窗,由用户确认后继续关闭', async () => {
|
const beforeClose = vi.fn().mockRejectedValue(new Error('标注未完成:缺少检验结果'));
|
|
onlineUtils.openOfficeDocument({ beforeClose, file: FILE });
|
await flush();
|
await wrapper.find('button').trigger('click');
|
await flush();
|
|
expect(beforeClose).toHaveBeenCalledTimes(1);
|
expect(mocks.confirm).toHaveBeenCalledWith(
|
expect.objectContaining({
|
cancelText: '返回标注',
|
content: '标注未完成:缺少检验结果',
|
iconType: 'warning',
|
okText: '仍然关闭',
|
title: '标注未完成',
|
}),
|
);
|
expect(mocks.messageError).not.toHaveBeenCalled();
|
expect(mocks.requestEditorClose).not.toHaveBeenCalled();
|
|
mocks.shouldEditorClose = false;
|
mocks.confirm.mock.calls[0]?.[0].onOk();
|
await flush();
|
|
expect(mocks.requestEditorClose).toHaveBeenCalledTimes(1);
|
expect(mocks.closeModal).not.toHaveBeenCalled();
|
wrapper.getComponent({ name: 'OnlyOfficeEditor' }).vm.$emit('close');
|
await flush();
|
|
expect(mocks.closeModal).toHaveBeenCalledTimes(1);
|
expect(wrapper.find('[data-test="onlyoffice-editor"]').exists()).toBe(false);
|
});
|
|
it('关闭校验确认弹窗选择返回标注时保留编辑器', async () => {
|
const beforeClose = vi.fn().mockRejectedValue(new Error('标注未完成:缺少检验结果'));
|
|
onlineUtils.openOfficeDocument({ beforeClose, file: FILE });
|
await flush();
|
await wrapper.find('button').trigger('click');
|
await flush();
|
|
mocks.confirm.mock.calls[0]?.[0].onCancel();
|
await flush();
|
|
expect(mocks.requestEditorClose).not.toHaveBeenCalled();
|
expect(mocks.closeModal).not.toHaveBeenCalled();
|
expect(wrapper.find('[data-test="onlyoffice-editor"]').exists()).toBe(true);
|
});
|
|
it('校验失败且有未保存更改时合并提示并由应用直接关闭', async () => {
|
const beforeClose = vi.fn().mockRejectedValue(new Error('标注未完成:缺少检验结果'));
|
|
onlineUtils.openOfficeDocument({ beforeClose, file: FILE });
|
await flush();
|
await wrapper.find('[data-test="onlyoffice-editor"]').trigger('click');
|
await wrapper.find('button').trigger('click');
|
await flush();
|
|
expect(beforeClose).toHaveBeenCalledTimes(1);
|
expect(mocks.confirm).toHaveBeenCalledWith(
|
expect.objectContaining({
|
content: '标注未完成:缺少检验结果\n关闭后,当前未保存的文档更改也将丢失。',
|
okText: '仍然关闭',
|
title: '标注未完成',
|
}),
|
);
|
mocks.confirm.mock.calls[0]?.[0].onOk();
|
await flush();
|
|
expect(mocks.requestEditorClose).not.toHaveBeenCalled();
|
expect(mocks.closeModal).toHaveBeenCalled();
|
expect(wrapper.find('[data-test="onlyoffice-editor"]').exists()).toBe(false);
|
});
|
|
it('未改动文档时关闭仍校验提示但不阻止关闭', async () => {
|
const beforeClose = vi.fn().mockRejectedValue(new Error('标注未完成:缺少检验结论'));
|
|
onlineUtils.openOfficeDocument({ beforeClose, file: FILE });
|
await flush();
|
await wrapper.find('button').trigger('click');
|
await flush();
|
|
expect(beforeClose).toHaveBeenCalledTimes(1);
|
expect(mocks.confirm).toHaveBeenCalledWith(expect.objectContaining({ content: '标注未完成:缺少检验结论' }));
|
mocks.confirm.mock.calls[0]?.[0].onOk();
|
await flush();
|
|
expect(mocks.requestEditorClose).toHaveBeenCalledTimes(1);
|
expect(mocks.closeModal).toHaveBeenCalled();
|
});
|
|
it('编辑状态回落后关闭不误报未保存,但仍由 onlyoffice 完成关闭', async () => {
|
onlineUtils.openOfficeDocument({ file: FILE });
|
await flush();
|
const editor = wrapper.getComponent({ name: 'OnlyOfficeEditor' });
|
|
editor.vm.$emit('modified', true);
|
editor.vm.$emit('modified', false);
|
await flush();
|
await wrapper.find('button').trigger('click');
|
await flush();
|
|
expect(mocks.confirm).not.toHaveBeenCalled();
|
expect(mocks.requestEditorClose).toHaveBeenCalledTimes(1);
|
expect(mocks.closeModal).toHaveBeenCalled();
|
});
|
|
it('关闭前模板校验期间显示全屏 loading', async () => {
|
let resolveValidation: () => void = () => {};
|
const beforeClose = vi.fn().mockReturnValue(new Promise<void>((resolve) => (resolveValidation = resolve)));
|
|
onlineUtils.openOfficeDocument({ beforeClose, file: FILE });
|
await flush();
|
await wrapper.find('[data-test="onlyoffice-editor"]').trigger('click');
|
await wrapper.find('button').trigger('click');
|
await flush();
|
|
const mask = wrapper.find('[data-test="document-saving-mask"]');
|
expect(mask.exists()).toBe(true);
|
expect(mask.attributes('data-phase')).toBe('validating-close');
|
expect(mask.text()).toContain('模板校验中');
|
|
resolveValidation();
|
await flush();
|
});
|
|
it('编辑器配置加载中关闭也执行必要校验', async () => {
|
let resolveConfig: (value: any) => void = () => {};
|
const beforeClose = vi.fn().mockRejectedValue(new Error('标注未完成:缺少检验结果'));
|
mocks.getOnlyOfficeConfig.mockReturnValue(new Promise((resolve) => (resolveConfig = resolve)));
|
|
onlineUtils.openOfficeDocument({ beforeClose, file: FILE });
|
await flush();
|
await wrapper.find('button').trigger('click');
|
await flush();
|
|
expect(beforeClose).toHaveBeenCalledTimes(1);
|
expect(mocks.confirm).toHaveBeenCalledWith(expect.objectContaining({ content: '标注未完成:缺少检验结果' }));
|
expect(mocks.closeModal).not.toHaveBeenCalled();
|
|
mocks.confirm.mock.calls[0]?.[0].onCancel();
|
resolveConfig(CONFIG_RESPONSE);
|
await flush();
|
});
|
|
it('前一会话只读时不影响下一编辑会话加载期间的关闭校验', async () => {
|
mocks.getOnlyOfficeConfig.mockResolvedValueOnce({
|
data: { ...CONFIG_RESPONSE.data, editorConfig: { ...CONFIG_RESPONSE.data.editorConfig, mode: 'view' } },
|
});
|
onlineUtils.openOfficeDocument({ file: FILE, mode: 'edit' });
|
await flush();
|
|
let resolveConfig: (value: any) => void = () => {};
|
const beforeClose = vi.fn().mockRejectedValue(new Error('标注未完成:缺少检验结果'));
|
mocks.getOnlyOfficeConfig.mockReturnValueOnce(new Promise((resolve) => (resolveConfig = resolve)));
|
onlineUtils.openOfficeDocument({ beforeClose, file: FILE, mode: 'edit' });
|
await flush();
|
await wrapper.find('button').trigger('click');
|
await flush();
|
|
expect(beforeClose).toHaveBeenCalledTimes(1);
|
expect(mocks.confirm).toHaveBeenCalledWith(expect.objectContaining({ content: '标注未完成:缺少检验结果' }));
|
|
mocks.confirm.mock.calls[0]?.[0].onCancel();
|
resolveConfig(CONFIG_RESPONSE);
|
await flush();
|
});
|
|
it('关闭校验期间路由跳转后不再操作已卸载的编辑器', async () => {
|
let resolveValidation: () => void = () => {};
|
const beforeClose = vi.fn().mockReturnValue(new Promise<void>((resolve) => (resolveValidation = resolve)));
|
|
onlineUtils.openOfficeDocument({ beforeClose, file: FILE });
|
await flush();
|
await wrapper.find('button').trigger('click');
|
await flush();
|
|
routeHook({ fullPath: '/b' }, { fullPath: '/a' });
|
resolveValidation();
|
await flush();
|
|
expect(mocks.closeModal).toHaveBeenCalledTimes(1);
|
expect(mocks.requestEditorClose).not.toHaveBeenCalled();
|
expect(wrapper.find('[data-test="onlyoffice-editor"]').exists()).toBe(false);
|
});
|
|
it('关闭校验期间路由跳转后校验失败也不弹出过期确认框', async () => {
|
let rejectValidation: (error: Error) => void = () => {};
|
const beforeClose = vi.fn().mockReturnValue(new Promise<void>((_, reject) => (rejectValidation = reject)));
|
|
onlineUtils.openOfficeDocument({ beforeClose, file: FILE });
|
await flush();
|
await wrapper.find('button').trigger('click');
|
await flush();
|
|
routeHook({ fullPath: '/b' }, { fullPath: '/a' });
|
rejectValidation(new Error('标注未完成:缺少检验结果'));
|
await flush();
|
|
expect(mocks.closeModal).toHaveBeenCalledTimes(1);
|
expect(mocks.confirm).not.toHaveBeenCalled();
|
expect(mocks.requestEditorClose).not.toHaveBeenCalled();
|
});
|
|
it('后端确认新版文件写回后提示保存成功', async () => {
|
mocks.getOnlyOfficeSaveStatus
|
.mockResolvedValueOnce({ data: { lastSaveTime: 1000, version: 1 } })
|
.mockResolvedValueOnce({ data: { lastSaveTime: 2000, version: 1 } });
|
|
onlineUtils.openOfficeDocument({ file: FILE });
|
await flush();
|
await wrapper.find('[data-test="onlyoffice-editor"]').trigger('dblclick');
|
await flush();
|
|
expect(mocks.getOnlyOfficeSaveStatus).toHaveBeenCalledTimes(2);
|
expect(mocks.messageSuccess).toHaveBeenCalledWith('文档保存成功');
|
expect(wrapper.find('[data-test="document-saving-mask"]').exists()).toBe(false);
|
});
|
|
it('签批页保存完成后向 onSave 回传另存为的上传文件对象', async () => {
|
const savedFile = {
|
fileExtension: 'docx',
|
fileId: 'saved-copy.docx',
|
fileSize: 321,
|
name: '检验报告.docx',
|
url: '/api/file/Image/annex/saved-copy.docx',
|
};
|
const onSave = vi.fn();
|
mocks.getOnlyOfficeSaveStatus
|
.mockResolvedValueOnce({ data: { lastSaveTime: 1000, sessionStatus: 'IDLE', version: 1 } })
|
.mockResolvedValueOnce({ data: { file: savedFile, lastSaveTime: 2000, sessionStatus: 'EDITING', version: 1 } })
|
.mockResolvedValueOnce({ data: { file: savedFile, lastSaveTime: 2000, sessionStatus: 'IDLE', version: 2 } });
|
|
onlineUtils.openOfficeDocument({ file: FILE, mode: 'qianpiye', onSave });
|
await flush();
|
const editor = wrapper.find('[data-test="onlyoffice-editor"]');
|
await editor.trigger('contextmenu');
|
await flush();
|
await wrapper.find('button').trigger('click');
|
await flush();
|
|
expect(onSave).toHaveBeenCalledWith({ file: savedFile, modified: true });
|
});
|
|
it('撤销恢复为未修改状态时不显示保存遮罩', async () => {
|
vi.useFakeTimers();
|
mocks.getOnlyOfficeSaveStatus.mockResolvedValue({ data: { lastSaveTime: 1000, version: 1 } });
|
|
onlineUtils.openOfficeDocument({ file: FILE });
|
await flush();
|
const editor = wrapper.find('[data-test="onlyoffice-editor"]');
|
await editor.trigger('click');
|
await editor.trigger('contextmenu');
|
await flush();
|
|
expect(wrapper.find('[data-test="document-saving-mask"]').exists()).toBe(false);
|
await vi.advanceTimersByTimeAsync(60_000);
|
await flush();
|
expect(mocks.messageError).not.toHaveBeenCalled();
|
vi.useRealTimers();
|
});
|
|
it('status 6 延迟三十秒到达时仍能确认保存成功', async () => {
|
vi.useFakeTimers();
|
const callbackAt = Date.now() + 30_000;
|
mocks.getOnlyOfficeSaveStatus.mockImplementation(() => Promise.resolve({ data: { lastSaveTime: Date.now() >= callbackAt ? 2000 : 1000, version: 1 } }));
|
|
onlineUtils.openOfficeDocument({ file: FILE });
|
await flush();
|
await wrapper.find('[data-test="onlyoffice-editor"]').trigger('dblclick');
|
await flush();
|
await vi.advanceTimersByTimeAsync(30_000);
|
await flush();
|
|
expect(mocks.messageSuccess).toHaveBeenCalledWith('文档保存成功');
|
expect(mocks.messageError).not.toHaveBeenCalled();
|
expect(wrapper.find('[data-test="document-saving-mask"]').exists()).toBe(false);
|
vi.useRealTimers();
|
});
|
|
it('保存状态查询在途时路由跳转会取消后续轮询和提示', async () => {
|
let resolveSaveStatus: (value: any) => void = () => {};
|
mocks.getOnlyOfficeSaveStatus
|
.mockResolvedValueOnce({ data: { lastSaveTime: 1000, version: 1 } })
|
.mockReturnValueOnce(new Promise((resolve) => (resolveSaveStatus = resolve)));
|
|
onlineUtils.openOfficeDocument({ file: FILE });
|
await flush();
|
await wrapper.find('[data-test="onlyoffice-editor"]').trigger('dblclick');
|
await flush();
|
|
routeHook({ fullPath: '/b' }, { fullPath: '/a' });
|
resolveSaveStatus({ data: { lastSaveTime: 2000, version: 1 } });
|
await flush();
|
|
expect(mocks.closeModal).toHaveBeenCalledTimes(1);
|
expect(mocks.getOnlyOfficeSaveStatus).toHaveBeenCalledTimes(3);
|
expect(mocks.messageSuccess).not.toHaveBeenCalled();
|
expect(mocks.messageError).not.toHaveBeenCalled();
|
});
|
|
it('手动保存后执行业务校验并以警告提示标注未完成', async () => {
|
const onManualSave = vi.fn().mockRejectedValue(new Error('标注未完成:缺少检验结论'));
|
mocks.getOnlyOfficeSaveStatus
|
.mockResolvedValueOnce({ data: { lastSaveTime: 1000, version: 1 } })
|
.mockResolvedValueOnce({ data: { lastSaveTime: 2000, version: 1 } });
|
|
onlineUtils.openOfficeDocument({ file: FILE, onManualSave });
|
await flush();
|
await wrapper.find('[data-test="onlyoffice-editor"]').trigger('dblclick');
|
await flush();
|
|
expect(onManualSave).toHaveBeenCalledTimes(1);
|
expect(mocks.messageWarning).toHaveBeenCalledWith('标注未完成:缺少检验结论');
|
expect(mocks.messageError).not.toHaveBeenCalled();
|
expect(mocks.messageSuccess).toHaveBeenCalledWith('文档保存成功');
|
expect(wrapper.find('[data-test="onlyoffice-editor"]').exists()).toBe(true);
|
});
|
|
it('业务校验悬挂时不超过保存确认窗口,仍按 status 6 确认结果', async () => {
|
vi.useFakeTimers();
|
const onManualSave = vi.fn().mockReturnValue(new Promise<void>(() => {}));
|
mocks.getOnlyOfficeSaveStatus
|
.mockResolvedValueOnce({ data: { lastSaveTime: 1000, version: 1 } })
|
.mockResolvedValueOnce({ data: { lastSaveTime: 2000, version: 1 } });
|
|
onlineUtils.openOfficeDocument({ file: FILE, onManualSave });
|
await flush();
|
await wrapper.find('[data-test="onlyoffice-editor"]').trigger('dblclick');
|
await flush();
|
await vi.advanceTimersByTimeAsync(60_000);
|
await flush();
|
|
expect(mocks.messageWarning).toHaveBeenCalledWith('模板校验超时,请检查');
|
expect(mocks.messageSuccess).toHaveBeenCalledWith('文档保存成功');
|
expect(wrapper.find('[data-test="document-saving-mask"]').exists()).toBe(false);
|
vi.useRealTimers();
|
});
|
|
it('status 6 到达前不显示保存遮罩,确认写回后才执行模板校验', async () => {
|
let resolveValidation: () => void = () => {};
|
let resolveSaveStatus: (value: any) => void = () => {};
|
const onManualSave = vi.fn().mockReturnValue(new Promise<void>((resolve) => (resolveValidation = resolve)));
|
mocks.getOnlyOfficeSaveStatus
|
.mockResolvedValueOnce({ data: { lastSaveTime: 1000, version: 1 } })
|
.mockReturnValueOnce(new Promise((resolve) => (resolveSaveStatus = resolve)));
|
|
onlineUtils.openOfficeDocument({ file: FILE, onManualSave });
|
await flush();
|
await wrapper.find('[data-test="onlyoffice-editor"]').trigger('dblclick');
|
await flush();
|
|
expect(wrapper.find('[data-test="document-saving-mask"]').exists()).toBe(false);
|
expect(onManualSave).not.toHaveBeenCalled();
|
|
resolveSaveStatus({ data: { lastSaveTime: 2000, version: 1 } });
|
await flush();
|
const mask = wrapper.find('[data-test="document-saving-mask"]');
|
expect(mask.attributes('data-phase')).toBe('validating');
|
expect(mask.text()).toContain('模板校验中');
|
|
resolveValidation();
|
await flush();
|
expect(wrapper.find('[data-test="document-saving-mask"]').exists()).toBe(false);
|
expect(mocks.messageSuccess).toHaveBeenCalledWith('文档保存成功');
|
});
|
|
it('等待 status 6 时不显示保存遮罩且不阻止关闭', async () => {
|
let resolveSaveStatus: (value: any) => void = () => {};
|
mocks.getOnlyOfficeSaveStatus
|
.mockResolvedValueOnce({ data: { lastSaveTime: 1000, version: 1 } })
|
.mockReturnValueOnce(new Promise((resolve) => (resolveSaveStatus = resolve)));
|
|
onlineUtils.openOfficeDocument({ file: FILE });
|
await flush();
|
await wrapper.find('[data-test="onlyoffice-editor"]').trigger('dblclick');
|
await flush();
|
|
expect(wrapper.find('[data-test="document-saving-mask"]').exists()).toBe(false);
|
await wrapper.find('button').trigger('click');
|
expect(mocks.requestEditorClose).toHaveBeenCalledTimes(1);
|
expect(mocks.closeModal).toHaveBeenCalledTimes(1);
|
|
resolveSaveStatus({ data: { lastSaveTime: 2000, version: 1 } });
|
await flush();
|
expect(mocks.messageSuccess).not.toHaveBeenCalled();
|
});
|
|
it('未检测到 status 6 时静默结束观察,不误报保存超时', async () => {
|
vi.useFakeTimers();
|
mocks.getOnlyOfficeSaveStatus.mockResolvedValue({ data: { lastSaveTime: 1000, version: 1 } });
|
|
onlineUtils.openOfficeDocument({ file: FILE });
|
await flush();
|
await wrapper.find('[data-test="onlyoffice-editor"]').trigger('click');
|
await wrapper.find('[data-test="onlyoffice-editor"]').trigger('dblclick');
|
await vi.runAllTimersAsync();
|
await flush();
|
|
expect(wrapper.find('[data-test="document-saving-mask"]').exists()).toBe(false);
|
expect(mocks.messageError).not.toHaveBeenCalled();
|
vi.useRealTimers();
|
});
|
|
it('路由跳走时自动关闭——编辑器占着 DS 编辑锁,不能留在新页面上', async () => {
|
const onClose = vi.fn();
|
onlineUtils.openOfficeDocument({ file: FILE, onClose });
|
await flush();
|
expect(wrapper.find('[data-test="onlyoffice-editor"]').exists()).toBe(true);
|
|
routeHook({ fullPath: '/b' }, { fullPath: '/a' });
|
await flush();
|
|
expect(mocks.closeModal).toHaveBeenCalled();
|
expect(onClose).toHaveBeenCalledTimes(1);
|
expect(wrapper.find('[data-test="onlyoffice-editor"]').exists()).toBe(false);
|
});
|
|
it('导航失败或地址没变时不关——页面根本没动,跟着关是误伤', async () => {
|
onlineUtils.openOfficeDocument({ file: FILE });
|
await flush();
|
|
routeHook({ fullPath: '/b' }, { fullPath: '/a' }, new Error('aborted'));
|
routeHook({ fullPath: '/a' }, { fullPath: '/a' });
|
await flush();
|
|
expect(mocks.closeModal).not.toHaveBeenCalled();
|
expect(wrapper.find('[data-test="onlyoffice-editor"]').exists()).toBe(true);
|
});
|
|
it('组件卸载时摘掉路由钩子', () => {
|
wrapper.unmount();
|
expect(mocks.removeRouteHook).toHaveBeenCalledTimes(1);
|
});
|
|
it('关闭后到达的 config 响应不再打开编辑器(防竞态)', async () => {
|
let resolveConfig: (value: any) => void = () => {};
|
mocks.getOnlyOfficeConfig.mockReturnValue(new Promise((resolve) => (resolveConfig = resolve)));
|
|
onlineUtils.openOfficeDocument({ file: FILE });
|
await flush();
|
await wrapper.find('button').trigger('click'); // 请求还在途中就关闭
|
resolveConfig(CONFIG_RESPONSE);
|
await flush();
|
|
expect(wrapper.find('[data-test="onlyoffice-editor"]').exists()).toBe(false);
|
});
|
});
|