/* @vitest-environment happy-dom */
|
/* eslint-disable vue/one-component-per-file */
|
|
import type { DmsFileCenterItem, DmsFileCenterMode } from '#/api/x/dms/fileCenter';
|
|
import { flushPromises, shallowMount } from '@vue/test-utils';
|
import { defineComponent, h } from 'vue';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import FileCenter from '../FileCenter.vue';
|
|
const apiMocks = vi.hoisted(() => ({
|
createFolder: vi.fn(),
|
evaluateFolderCreate: vi.fn(),
|
getChildFolders: vi.fn(),
|
getDocumentVersions: vi.fn(),
|
getFileOperationLogs: vi.fn(),
|
getFolderFiles: vi.fn(),
|
getRootFolders: vi.fn(),
|
}));
|
|
const modalMocks = vi.hoisted(() => ({
|
index: 0,
|
openCompare: vi.fn(),
|
openCreateFolder: vi.fn(),
|
openOperationLog: vi.fn(),
|
openProperties: vi.fn(),
|
}));
|
|
const messageMocks = vi.hoisted(() => ({ error: vi.fn(), success: vi.fn(), warning: vi.fn() }));
|
const previewMocks = vi.hoisted(() => ({ open: vi.fn() }));
|
const flowMocks = vi.hoisted(() => ({ openFlowForm: vi.fn() }));
|
vi.mock('#/utils/jnpf', () => ({ onlineUtils: flowMocks, getJnpfAppEnCode: () => '', getRealJnpfAppEnCode: () => '' }));
|
|
vi.mock('#/api/x/dms/fileCenter', () => ({
|
createFolder: apiMocks.createFolder,
|
evaluateFolderCreate: apiMocks.evaluateFolderCreate,
|
getChildFolders: apiMocks.getChildFolders,
|
getDocumentVersions: apiMocks.getDocumentVersions,
|
getFileOperationLogs: apiMocks.getFileOperationLogs,
|
getFolderFiles: apiMocks.getFolderFiles,
|
getRootFolders: apiMocks.getRootFolders,
|
}));
|
|
vi.mock('#/api/x/dms/personalFolder', () => ({ addFavoriteFile: vi.fn() }));
|
vi.mock('@jnpf/hooks', () => ({ useMessage: () => ({ createMessage: messageMocks }) }));
|
vi.mock('@jnpf/ui', () => ({ BasicTree: { name: 'BasicTree', template: '<div />' } }));
|
vi.mock('@jnpf/ui/form', () => ({
|
BasicForm: { name: 'BasicForm', template: '<div />' },
|
useForm: () => [vi.fn(), { resetFields: vi.fn(), setFieldsValue: vi.fn(), validate: vi.fn() }],
|
}));
|
vi.mock('@jnpf/ui/vxeTable', () => ({
|
BasicVxeTable: { name: 'BasicVxeTable', template: '<div />' },
|
TableAction: { name: 'TableAction', template: '<div />' },
|
useVxeTable: () => [vi.fn(), {}],
|
}));
|
vi.mock('@jnpf/utils', async (importOriginal) => ({
|
...(await importOriginal<typeof import('@jnpf/utils')>()),
|
formatToDateTime: (value: string) => value,
|
}));
|
vi.mock('../../shared/dmsFilePreview', () => ({
|
dmsPreviewErrorMessage: () => '文件预览失败',
|
openDmsFilePreview: previewMocks.open,
|
}));
|
|
vi.mock('@jnpf/ui/modal', async () => {
|
const { defineComponent, h } = await import('vue');
|
const openModals = [modalMocks.openCompare, modalMocks.openCreateFolder, modalMocks.openOperationLog, modalMocks.openProperties];
|
|
return {
|
BasicModal: defineComponent({
|
name: 'BasicModal',
|
setup:
|
(_, { slots }) =>
|
() =>
|
h('section', slots.default?.()),
|
}),
|
useModal: () => {
|
const openModal = openModals[modalMocks.index % openModals.length];
|
modalMocks.index++;
|
return [vi.fn(), { openModal }];
|
},
|
useModalInner: () => [vi.fn(), { changeOkLoading: vi.fn(), closeModal: vi.fn() }],
|
};
|
});
|
|
const ButtonStub = defineComponent({
|
name: 'AButton',
|
inheritAttrs: false,
|
props: {
|
disabled: Boolean,
|
icon: { default: undefined, type: null },
|
loading: Boolean,
|
},
|
emits: ['click', 'dblclick'],
|
setup(props, { attrs, emit, slots }) {
|
return () =>
|
h(
|
'button',
|
{
|
...attrs,
|
disabled: props.disabled,
|
type: 'button',
|
onClick: (event: MouseEvent) => emit('click', event),
|
onDblclick: (event: MouseEvent) => emit('dblclick', event),
|
},
|
slots.default?.(),
|
);
|
},
|
});
|
|
const SlotStub = defineComponent({
|
setup:
|
(_, { slots }) =>
|
() =>
|
h('div', slots.default?.()),
|
});
|
|
const TreeStub = defineComponent({
|
name: 'BasicTree',
|
inheritAttrs: false,
|
props: {
|
selectedKeys: { default: () => [], type: Array },
|
treeData: { default: () => [], type: Array },
|
},
|
emits: ['select'],
|
setup: () => () => h('div'),
|
});
|
|
const BasicVxeTableStub = defineComponent({
|
name: 'BasicVxeTable',
|
inheritAttrs: false,
|
props: {
|
dataSource: { default: () => [], type: Array },
|
},
|
emits: ['cell-click'],
|
setup(props, { emit, slots }) {
|
return () =>
|
h(
|
'div',
|
{ class: 'basic-vxe-table' },
|
props.dataSource.map((record: any) =>
|
h(
|
'div',
|
{
|
class: 'detail-table-row',
|
onClick: () => emit('cell-click', { column: { field: 'name' }, row: record }),
|
},
|
[
|
slots.entryName?.({ record }),
|
slots.lifecycleStatus?.({ record }),
|
slots.invalidatedAt?.({ record }),
|
slots.revokedAt?.({ record }),
|
slots.action?.({ record }),
|
],
|
),
|
),
|
);
|
},
|
});
|
|
const TableActionStub = defineComponent({
|
name: 'TableAction',
|
inheritAttrs: false,
|
props: {
|
actions: { default: () => [], type: Array },
|
stopButtonPropagation: Boolean,
|
},
|
setup(props) {
|
return () =>
|
h(
|
'div',
|
{ class: 'table-action' },
|
props.actions.map((action: any) =>
|
h(
|
'button',
|
{
|
'data-test': action['data-test'],
|
disabled: action.disabled,
|
onClick: (event: MouseEvent) => {
|
if (props.stopButtonPropagation) event.stopPropagation();
|
action.onClick?.();
|
},
|
type: 'button',
|
},
|
action.label,
|
),
|
),
|
);
|
},
|
});
|
|
const rootFolder: DmsFileCenterItem = {
|
createdAt: '2026-09-01',
|
id: 'folder-1',
|
name: '质量文件',
|
resourceType: 'FOLDER',
|
};
|
|
const documentItem: DmsFileCenterItem = {
|
createdAt: '2026-09-02',
|
id: 'document-1',
|
name: '质量手册.docx',
|
resourceType: 'DOCUMENT',
|
};
|
|
const docxVersion: DmsFileCenterItem = {
|
createdAt: '2026-09-03',
|
documentId: 'document-2',
|
fileName: '检验规程.docx',
|
id: 'version-docx',
|
name: '检验规程.docx',
|
resourceType: 'FILE_VERSION',
|
versionNo: 3,
|
};
|
|
const pdfVersion: DmsFileCenterItem = {
|
createdAt: '2026-09-04',
|
documentId: 'document-3',
|
fileName: '设备说明.pdf',
|
id: 'version-pdf',
|
name: '设备说明.pdf',
|
resourceType: 'FILE_VERSION',
|
versionNo: 1,
|
};
|
|
const resolvedDocumentVersion: DmsFileCenterItem = {
|
...documentItem,
|
documentId: documentItem.id,
|
fileName: '质量手册.docx',
|
id: 'version-document-current',
|
resourceType: 'FILE_VERSION',
|
versionNo: 5,
|
};
|
|
function page(list: DmsFileCenterItem[]) {
|
return { data: { hasMore: false, list } };
|
}
|
|
async function mountFileCenter(mode: DmsFileCenterMode) {
|
const wrapper = shallowMount(FileCenter, {
|
props: { mode },
|
global: {
|
directives: { loading: {} },
|
stubs: {
|
'a-button': ButtonStub,
|
'a-button-group': SlotStub,
|
'a-tooltip': SlotStub,
|
BasicTree: TreeStub,
|
BasicVxeTable: BasicVxeTableStub,
|
'jnpf-input': true,
|
'jnpf-empty': true,
|
TableAction: TableActionStub,
|
},
|
},
|
});
|
await flushPromises();
|
return wrapper;
|
}
|
|
beforeEach(() => {
|
vi.clearAllMocks();
|
modalMocks.index = 0;
|
apiMocks.getRootFolders.mockResolvedValue(page([rootFolder]));
|
apiMocks.getChildFolders.mockResolvedValue(page([]));
|
apiMocks.getFolderFiles.mockResolvedValue(page([documentItem, docxVersion, pdfVersion]));
|
apiMocks.getDocumentVersions.mockResolvedValue(page([resolvedDocumentVersion]));
|
apiMocks.evaluateFolderCreate.mockResolvedValue({ data: { allowed: false } });
|
});
|
|
describe('current file center row actions', () => {
|
it('moves properties and comparison actions out of the CURRENT toolbar and shows only applicable row actions', async () => {
|
const wrapper = await mountFileCenter('CURRENT');
|
|
expect(wrapper.get('.workspace-actions').text()).not.toContain('属性');
|
expect(wrapper.get('.workspace-actions').text()).not.toContain('版本比较');
|
|
const cells = wrapper.findAll('.operation-cell');
|
expect(cells).toHaveLength(3);
|
expect(cells[0]!.findAll('button').map((button) => button.text().trim())).toEqual(['属性']);
|
expect(cells[0]!.find('[data-test="file-compare-action"]').exists()).toBe(false);
|
expect(cells[1]!.get('[data-test="file-properties-action"]').text()).toContain('属性');
|
expect(cells[1]!.get('[data-test="file-compare-action"]').text()).toContain('版本比较');
|
expect(cells[2]!.get('[data-test="file-properties-action"]').text()).toContain('属性');
|
expect(cells[2]!.find('[data-test="file-compare-action"]').exists()).toBe(false);
|
|
wrapper.unmount();
|
});
|
|
it('resolves a DOCUMENT version for properties and sends the selected DOCX version to comparison', async () => {
|
const wrapper = await mountFileCenter('CURRENT');
|
const cells = wrapper.findAll('.operation-cell');
|
|
await cells[0]!.get('[data-test="file-properties-action"]').trigger('click');
|
await flushPromises();
|
|
expect(apiMocks.getDocumentVersions).toHaveBeenCalledWith('CURRENT', documentItem.id, { pageSize: 1 });
|
expect(modalMocks.openProperties).toHaveBeenCalledWith(true, { item: resolvedDocumentVersion });
|
|
await cells[1]!.get('[data-test="file-compare-action"]').trigger('click');
|
expect(modalMocks.openCompare).toHaveBeenCalledWith(true, { item: docxVersion });
|
|
wrapper.unmount();
|
});
|
|
it('keeps TableAction clicks inside the action cell', async () => {
|
const wrapper = await mountFileCenter('CURRENT');
|
|
await wrapper.get('[data-test="file-compare-action"]').trigger('click');
|
|
expect(modalMocks.openCompare).toHaveBeenCalledWith(true, { item: docxVersion });
|
expect(previewMocks.open).not.toHaveBeenCalled();
|
|
wrapper.unmount();
|
});
|
});
|
|
describe('historical file center actions', () => {
|
it('shows invalidated directory suffix and actual execution date without changing the source name', async () => {
|
const invalidated = { ...docxVersion, lifecycleStatus: 'INVALIDATED' as const, invalidatedAt: '2026-09-25T10:16:00+08:00' };
|
const directory = { ...invalidated, id: 'disabled-directory', resourceType: 'DOCUMENT' as const, name: '操作规程.docx', fileName: '操作规程.docx' };
|
apiMocks.getFolderFiles.mockResolvedValue(page([directory, invalidated]));
|
const wrapper = await mountFileCenter('HISTORICAL');
|
|
expect(wrapper.text()).toContain('操作规程(已失效)');
|
expect(wrapper.text()).toContain('已失效');
|
expect(wrapper.text()).toContain('2026-09-25');
|
expect(directory.name).toBe('操作规程.docx');
|
expect(wrapper.find('[data-test="file-compare-action"]').exists()).toBe(false);
|
wrapper.unmount();
|
});
|
|
it('keeps properties in the HISTORICAL toolbar without adding the new row actions', async () => {
|
apiMocks.getFolderFiles.mockResolvedValue(page([docxVersion]));
|
const wrapper = await mountFileCenter('HISTORICAL');
|
|
expect(wrapper.get('.workspace-actions').text()).toContain('属性');
|
expect(wrapper.get('.workspace-actions').text()).not.toContain('版本比较');
|
expect(wrapper.find('[data-test="file-properties-action"]').exists()).toBe(false);
|
expect(wrapper.find('[data-test="file-compare-action"]').exists()).toBe(false);
|
expect(wrapper.get('.operation-cell').text()).toContain('日志');
|
|
wrapper.unmount();
|
});
|
});
|
|
describe('file center tree pagination', () => {
|
it('restores the current folder selection after loading a synthetic more node', async () => {
|
const secondFolder = { ...rootFolder, id: 'folder-2', name: '第二个目录' };
|
apiMocks.getRootFolders
|
.mockResolvedValueOnce({ data: { hasMore: true, list: [rootFolder], nextCursor: 'root-next' } })
|
.mockResolvedValueOnce(page([secondFolder]));
|
const wrapper = await mountFileCenter('CURRENT');
|
const tree = wrapper.findComponent(TreeStub);
|
const initialSelection = tree.props('selectedKeys');
|
const moreNode = (tree.props('treeData') as any[]).find((node) => String(node.key).startsWith('__more__:folder:root:'));
|
|
tree.vm.$emit('select', [moreNode.key]);
|
await flushPromises();
|
|
expect(apiMocks.getRootFolders).toHaveBeenLastCalledWith({ cursor: 'root-next', pageSize: 50 });
|
expect(tree.props('selectedKeys')).toEqual([rootFolder.id]);
|
expect(tree.props('selectedKeys')).not.toBe(initialSelection);
|
wrapper.unmount();
|
});
|
});
|
|
describe('revocation integration with the native flow', () => {
|
it('opens the existing revocation application only from the current center', async () => {
|
const current = await mountFileCenter('CURRENT');
|
await current.get('[data-testid="revocation-application"]').trigger('click');
|
expect(flowMocks.openFlowForm).toHaveBeenCalledWith(expect.objectContaining({ flowId: '859064410424006213' }));
|
current.unmount();
|
const historical = await mountFileCenter('HISTORICAL');
|
expect(historical.find('[data-testid="revocation-application"]').exists()).toBe(false);
|
historical.unmount();
|
});
|
it('shows the revoked state and actual date independently of invalidation', async () => {
|
const directory = { ...documentItem, lifecycleStatus: 'REVOKED' as const, revokedAt: '2026-09-25T15:30:00+08:00' };
|
apiMocks.getFolderFiles.mockResolvedValue(page([directory]));
|
const wrapper = await mountFileCenter('HISTORICAL');
|
expect(wrapper.text()).toContain('已撤销');
|
expect(wrapper.text()).toContain('2026-09-25');
|
expect(wrapper.text()).not.toContain('已失效');
|
wrapper.unmount();
|
});
|
});
|