/* eslint-disable vue/one-component-per-file, vue/require-default-prop -- Test doubles intentionally stay local to this component suite. */
|
import type { DmsPersonalFolderFile } from '#/api/x/dms/personalFolder';
|
|
import { mount } from '@vue/test-utils';
|
import { defineComponent, h, nextTick } from 'vue';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import PersonalFolderPage from '../index.vue';
|
import pageSource from '../index.vue?raw';
|
|
const apiMocks = vi.hoisted(() => ({
|
createDmsFileDownloadTicket: vi.fn(),
|
downloadDmsFile: vi.fn(),
|
evaluatePersonalFolderDownloads: vi.fn(),
|
getPersonalFolderFiles: vi.fn(),
|
getPersonalFolders: vi.fn(),
|
}));
|
const messageMocks = vi.hoisted(() => ({ error: vi.fn(), success: vi.fn() }));
|
const previewMocks = vi.hoisted(() => ({ open: vi.fn() }));
|
const utilityMocks = vi.hoisted(() => ({ downloadByData: vi.fn() }));
|
const formState = vi.hoisted(() => ({ config: undefined as any, setFieldsValue: vi.fn() }));
|
const tableState = vi.hoisted(() => ({ config: undefined as any }));
|
const treeState = vi.hoisted(() => ({ setSelectedKeys: vi.fn() }));
|
|
vi.mock('#/api/x/dms/personalFolder', () => ({
|
createDmsFileDownloadTicket: apiMocks.createDmsFileDownloadTicket,
|
downloadDmsFile: apiMocks.downloadDmsFile,
|
evaluatePersonalFolderDownloads: apiMocks.evaluatePersonalFolderDownloads,
|
getPersonalFolderFiles: apiMocks.getPersonalFolderFiles,
|
getPersonalFolders: apiMocks.getPersonalFolders,
|
}));
|
|
vi.mock('#/utils/jnpf', () => ({
|
getJnpfAppEnCode: () => '',
|
onlineUtils: { hideLoading: vi.fn(), showLoading: vi.fn() },
|
}));
|
|
vi.mock('../../shared/dmsFilePreview', () => ({
|
dmsPreviewErrorMessage: (error: unknown) => (error instanceof Error ? error.message : '文件预览失败'),
|
openDmsFilePreview: previewMocks.open,
|
}));
|
|
vi.mock('@jnpf/hooks', () => ({
|
useMessage: () => ({ createMessage: messageMocks }),
|
}));
|
|
vi.mock('@jnpf/utils', () => ({
|
downloadByData: utilityMocks.downloadByData,
|
formatToDateTime: (value: string, format?: string) => `${format || 'datetime'}:${value}`,
|
}));
|
|
vi.mock('@jnpf/ui', async () => {
|
const { defineComponent, h } = await import('vue');
|
return {
|
BasicLeftTree: defineComponent({
|
name: 'BasicLeftTree',
|
props: {
|
fieldNames: { default: () => ({}), type: Object },
|
loading: Boolean,
|
showSearch: Boolean,
|
showToolbar: Boolean,
|
title: String,
|
treeData: { default: () => [], type: Array },
|
},
|
emits: ['reload', 'select'],
|
setup(props, { expose, slots }) {
|
expose({ setSelectedKeys: treeState.setSelectedKeys });
|
return () =>
|
h('aside', { 'data-test': 'folder-tree' }, [JSON.stringify(props.treeData), slots.title?.({ key: '__personal_root__', title: '个人文件夹' })]);
|
},
|
}),
|
ScrollContainer: defineComponent({
|
name: 'ScrollContainer',
|
setup(_, { slots }) {
|
return () => h('div', { 'data-test': 'tile-scroll' }, slots.default?.());
|
},
|
}),
|
};
|
});
|
|
vi.mock('@jnpf/ui/modal', async () => {
|
const { defineComponent, h } = await import('vue');
|
return {
|
BasicModal: defineComponent({
|
name: 'BasicModal',
|
emits: ['register', 'ok'],
|
setup(_, { slots }) {
|
return () => h('div', slots.default?.());
|
},
|
}),
|
useModal: () => [vi.fn(), { openModal: vi.fn(), closeModal: vi.fn(), setModalProps: vi.fn() }],
|
};
|
});
|
|
vi.mock('@jnpf/ui/form', async () => {
|
const { defineComponent, h } = await import('vue');
|
return {
|
BasicForm: defineComponent({
|
name: 'BasicForm',
|
emits: ['register', 'reset', 'submit'],
|
setup() {
|
return () => h('form', { 'data-test': 'search-form' });
|
},
|
}),
|
useForm: (config: any) => {
|
formState.config = config;
|
return [vi.fn(), { setFieldsValue: formState.setFieldsValue }];
|
},
|
};
|
});
|
|
vi.mock('@jnpf/ui/vxeTable', async () => {
|
const { defineComponent, h } = await import('vue');
|
return {
|
BasicVxeTable: defineComponent({
|
name: 'BasicVxeTable',
|
inheritAttrs: false,
|
props: {
|
dataSource: { default: () => [], type: Array },
|
emptyText: String,
|
loading: Boolean,
|
rowConfig: Object,
|
},
|
emits: ['cell-dblclick', 'register', 'row-click'],
|
setup(props, { slots }) {
|
return () =>
|
h(
|
'div',
|
{ 'data-test': 'personal-file-table' },
|
(props.dataSource as DmsPersonalFolderFile[]).flatMap((record) => [
|
slots.fileName?.({ record }),
|
slots.documentNo?.({ record }),
|
slots.versionLabel?.({ record }),
|
slots.createdAt?.({ record }),
|
slots.validTo?.({ record }),
|
slots.sizeBytes?.({ record }),
|
slots.action?.({ record }),
|
]),
|
);
|
},
|
}),
|
TableAction: defineComponent({
|
name: 'TableAction',
|
props: { actions: { default: () => [], type: Array }, stopButtonPropagation: Boolean },
|
setup(props) {
|
return () => h('div', { 'data-actions': (props.actions as any[]).map((action) => action.label).join(','), 'data-test': 'table-action' });
|
},
|
}),
|
useVxeTable: (config: any) => {
|
tableState.config = config;
|
return [vi.fn(), {}];
|
},
|
};
|
});
|
|
vi.mock('ant-design-vue', async () => {
|
const { defineComponent, h } = await import('vue');
|
return {
|
Radio: defineComponent({
|
name: 'AntRadio',
|
setup(_, { slots }) {
|
return () => h('span', slots.default?.());
|
},
|
}),
|
RadioGroup: defineComponent({
|
name: 'RadioGroup',
|
props: { value: String },
|
emits: ['update:value'],
|
setup(_, { slots }) {
|
return () => h('div', slots.default?.());
|
},
|
}),
|
Breadcrumb: defineComponent({
|
name: 'AntBreadcrumb',
|
setup(_, { slots }) {
|
return () => h('nav', slots.default?.());
|
},
|
}),
|
BreadcrumbItem: defineComponent({
|
name: 'BreadcrumbItem',
|
setup(_, { slots }) {
|
return () => h('span', slots.default?.());
|
},
|
}),
|
};
|
});
|
|
const ButtonStub = defineComponent({
|
name: 'AButton',
|
inheritAttrs: false,
|
props: { loading: Boolean, type: String },
|
emits: ['click'],
|
setup(_, { attrs, emit, slots }) {
|
return () => h('button', { ...attrs, onClick: () => emit('click'), type: 'button' }, slots.default?.());
|
},
|
});
|
|
const SlotStub = defineComponent({
|
setup(_, { slots }) {
|
return () => h('div', slots.default?.());
|
},
|
});
|
|
function file(values: Partial<DmsPersonalFolderFile> = {}): DmsPersonalFolderFile {
|
return {
|
createdAt: '2026-09-01T00:00:00Z',
|
documentNo: 'DOC-001',
|
fileName: '质量标准.pdf',
|
id: 'file-1',
|
name: '质量标准',
|
resourceType: 'FILE_VERSION',
|
sizeBytes: 1536,
|
sourceId: 'source-1',
|
sourceType: 'DMS',
|
versionLabel: 'R01',
|
...values,
|
};
|
}
|
|
function mountPage() {
|
return mount(PersonalFolderPage, {
|
global: {
|
directives: { loading: () => {} },
|
stubs: {
|
'a-button': ButtonStub,
|
'a-button-group': SlotStub,
|
'a-tooltip': SlotStub,
|
FileViewModeSwitch: defineComponent({
|
name: 'FileViewModeSwitch',
|
props: { value: String },
|
emits: ['update:value'],
|
setup() {
|
return () => h('div');
|
},
|
}),
|
'jnpf-empty': defineComponent({ name: 'JnpfEmpty', props: { description: String }, template: '<div data-test="empty">{{ description }}</div>' }),
|
},
|
},
|
});
|
}
|
|
describe('dms personal folder page', () => {
|
beforeEach(() => {
|
vi.clearAllMocks();
|
formState.config = undefined;
|
tableState.config = undefined;
|
formState.setFieldsValue.mockResolvedValue(undefined);
|
apiMocks.getPersonalFolders.mockResolvedValue({
|
data: [
|
{ code: 'BORROW', name: '借阅' },
|
{ code: 'DOWNLOAD', name: '下载' },
|
{ code: 'FAVORITE', name: '收藏' },
|
],
|
});
|
apiMocks.getPersonalFolderFiles.mockImplementation(async (folderType: string) => ({
|
data: [file({ fileName: `${folderType}.pdf`, id: `${folderType}-file` })],
|
}));
|
apiMocks.evaluatePersonalFolderDownloads.mockImplementation(async (ids: string[]) => ({
|
data: ids.map((resourceId) => ({ allowed: true, resourceId })),
|
}));
|
apiMocks.createDmsFileDownloadTicket.mockResolvedValue({
|
data: { accessTicket: 'ticket-1', fileName: 'DOWNLOAD.pdf', fileVersionId: 'DOWNLOAD-file', mimeType: 'application/pdf' },
|
});
|
apiMocks.downloadDmsFile.mockResolvedValue(new Blob(['pdf'], { type: 'application/pdf' }));
|
previewMocks.open.mockResolvedValue(true);
|
});
|
|
it('uses the standard JNPF page, tree, form and table without fake pagination', async () => {
|
const wrapper = mountPage();
|
|
await vi.waitFor(() => expect(apiMocks.getPersonalFolderFiles).toHaveBeenCalledWith('BORROW', { keyword: undefined, pageSize: 100 }));
|
|
expect(pageSource).toContain('class="jnpf-content-wrapper personal-folder-page"');
|
expect(pageSource).toContain('<BasicLeftTree');
|
expect(pageSource).toContain('<BasicForm');
|
expect(pageSource).toContain('<BasicVxeTable');
|
expect(pageSource).toContain('<TableAction');
|
expect(pageSource).not.toContain('<ATree');
|
expect(pageSource).not.toContain('class="detail-list"');
|
expect(tableState.config).toMatchObject({ immediate: false, pagination: false, rowKey: 'id', showIndexColumn: false });
|
expect(tableState.config.columns.map((column: any) => column.title)).toEqual(['标题', '文件编号', '版本号', '上传日期', '有效期至', '文件大小']);
|
|
const tree = wrapper.findComponent({ name: 'BasicLeftTree' });
|
expect(tree.props()).toMatchObject({
|
fieldNames: { children: 'children', key: 'key', title: 'title' },
|
showSearch: false,
|
showToolbar: true,
|
title: '个人文件夹',
|
});
|
expect(treeState.setSelectedKeys).toHaveBeenCalledWith(['BORROW']);
|
});
|
|
it('submits trimmed search text and clears it once before changing folders', async () => {
|
const wrapper = mountPage();
|
await vi.waitFor(() => expect(apiMocks.getPersonalFolderFiles).toHaveBeenCalledOnce());
|
apiMocks.getPersonalFolderFiles.mockClear();
|
|
wrapper.findComponent({ name: 'BasicForm' }).vm.$emit('submit', { keyword: ' 标准 ' });
|
await vi.waitFor(() => expect(apiMocks.getPersonalFolderFiles).toHaveBeenCalledWith('BORROW', { keyword: '标准', pageSize: 100 }));
|
|
apiMocks.getPersonalFolderFiles.mockClear();
|
wrapper.findComponent({ name: 'BasicLeftTree' }).vm.$emit('select', 'DOWNLOAD');
|
await vi.waitFor(() => expect(apiMocks.getPersonalFolderFiles).toHaveBeenCalledWith('DOWNLOAD', { keyword: undefined, pageSize: 100 }));
|
expect(apiMocks.getPersonalFolderFiles).toHaveBeenCalledOnce();
|
expect(formState.setFieldsValue).toHaveBeenCalledWith({ keyword: '' });
|
|
await vi.waitFor(() => {
|
const labels = (wrapper.findComponent({ name: 'TableAction' }).props('actions') as any[]).map((action) => action.label);
|
expect(labels).toEqual(['预览', '下载']);
|
});
|
|
apiMocks.getPersonalFolderFiles.mockClear();
|
wrapper.findComponent({ name: 'BasicForm' }).vm.$emit('reset');
|
await vi.waitFor(() => expect(apiMocks.getPersonalFolderFiles).toHaveBeenCalledWith('DOWNLOAD', { keyword: undefined, pageSize: 100 }));
|
expect(apiMocks.getPersonalFolderFiles).toHaveBeenCalledOnce();
|
});
|
|
it('selects rows and previews non-action cells from table events', async () => {
|
const wrapper = mountPage();
|
await vi.waitFor(() => expect(wrapper.find('.file-name-trigger').exists()).toBe(true));
|
|
const table = wrapper.findComponent({ name: 'BasicVxeTable' });
|
const record = file({ fileName: 'BORROW.pdf', id: 'BORROW-file' });
|
table.vm.$emit('row-click', { row: record });
|
await nextTick();
|
expect(wrapper.find('.file-name-trigger').attributes('aria-selected')).toBe('true');
|
|
table.vm.$emit('cell-dblclick', { column: { field: 'fileName' }, row: record });
|
await vi.waitFor(() =>
|
expect(previewMocks.open).toHaveBeenCalledWith(
|
expect.objectContaining({ fileName: 'BORROW.pdf', resourceId: 'BORROW-file', resourceType: 'FILE_VERSION' }),
|
),
|
);
|
|
previewMocks.open.mockClear();
|
table.vm.$emit('cell-dblclick', { column: { field: 'action' }, row: record });
|
await nextTick();
|
expect(previewMocks.open).not.toHaveBeenCalled();
|
});
|
|
it('keeps Enter preview available in details and tile views', async () => {
|
const wrapper = mountPage();
|
await vi.waitFor(() => expect(wrapper.find('.file-name-trigger').exists()).toBe(true));
|
|
await wrapper.find('.file-name-trigger').trigger('keydown.enter');
|
expect(previewMocks.open).toHaveBeenCalledWith(
|
expect.objectContaining({ fileName: 'BORROW.pdf', resourceId: 'BORROW-file', resourceType: 'FILE_VERSION' }),
|
);
|
|
previewMocks.open.mockClear();
|
wrapper.findComponent({ name: 'FileViewModeSwitch' }).vm.$emit('update:value', 'tiles');
|
await nextTick();
|
await wrapper.find('.file-tile').trigger('keydown.enter');
|
expect(previewMocks.open).toHaveBeenCalledWith(
|
expect.objectContaining({ fileName: 'BORROW.pdf', resourceId: 'BORROW-file', resourceType: 'FILE_VERSION' }),
|
);
|
});
|
|
it('disables expired preview actions', async () => {
|
apiMocks.getPersonalFolderFiles.mockResolvedValueOnce({ data: [file({ validTo: '2000-01-01T00:00:00Z' })] });
|
const wrapper = mountPage();
|
await vi.waitFor(() => expect(wrapper.findComponent({ name: 'TableAction' }).exists()).toBe(true));
|
|
const [previewAction] = wrapper.findComponent({ name: 'TableAction' }).props('actions') as any[];
|
expect(previewAction.disabled).toBe(true);
|
await previewAction.onClick();
|
expect(previewMocks.open).not.toHaveBeenCalled();
|
});
|
|
it('downloads through a ticket only from the download folder action', async () => {
|
const wrapper = mountPage();
|
await vi.waitFor(() => expect(apiMocks.getPersonalFolderFiles).toHaveBeenCalledOnce());
|
|
wrapper.findComponent({ name: 'BasicLeftTree' }).vm.$emit('select', 'DOWNLOAD');
|
await vi.waitFor(() => {
|
const actions = wrapper.findComponent({ name: 'TableAction' }).props('actions') as any[];
|
expect(actions.map((action) => action.label)).toEqual(['预览', '下载']);
|
});
|
|
const downloadAction = (wrapper.findComponent({ name: 'TableAction' }).props('actions') as any[])[1];
|
await downloadAction.onClick();
|
expect(apiMocks.createDmsFileDownloadTicket).not.toHaveBeenCalled();
|
wrapper.findComponent({ name: 'BasicModal' }).vm.$emit('ok');
|
await vi.waitFor(() => expect(utilityMocks.downloadByData).toHaveBeenCalled());
|
|
expect(apiMocks.createDmsFileDownloadTicket).toHaveBeenCalledWith('DOWNLOAD-file', { watermarkBusinessTypes: ['gerenWenjianjia'] });
|
expect(apiMocks.evaluatePersonalFolderDownloads).toHaveBeenCalledWith(['DOWNLOAD-file']);
|
expect(apiMocks.downloadDmsFile).toHaveBeenCalledWith('DOWNLOAD-file', 'ticket-1', 'pdf');
|
expect(utilityMocks.downloadByData).toHaveBeenCalledWith(expect.any(Blob), 'DOWNLOAD.pdf', 'application/pdf');
|
});
|
it('downloads Word only after confirming the selected format', async () => {
|
apiMocks.getPersonalFolderFiles.mockResolvedValue({ data: [file({ fileName: 'manual.docx', id: 'word-file' })] });
|
apiMocks.createDmsFileDownloadTicket.mockResolvedValue({ data: { accessTicket: 'word-ticket', fileName: 'manual.docx', fileVersionId: 'word-file' } });
|
const wrapper = mountPage();
|
await vi.waitFor(() => expect(apiMocks.getPersonalFolderFiles).toHaveBeenCalledOnce());
|
wrapper.findComponent({ name: 'BasicLeftTree' }).vm.$emit('select', 'DOWNLOAD');
|
await vi.waitFor(() => expect(wrapper.findComponent({ name: 'TableAction' }).props('actions')).toHaveLength(2));
|
const action = (wrapper.findComponent({ name: 'TableAction' }).props('actions') as any[])[1];
|
await action.onClick();
|
expect(apiMocks.createDmsFileDownloadTicket).not.toHaveBeenCalled();
|
wrapper.findComponent({ name: 'BasicModal' }).vm.$emit('ok');
|
await vi.waitFor(() => expect(apiMocks.downloadDmsFile).toHaveBeenCalledWith('word-file', 'word-ticket', undefined));
|
});
|
it('explains expired download authorization without requesting a ticket', async () => {
|
apiMocks.getPersonalFolderFiles.mockResolvedValue({ data: [file({ validTo: '2000-01-01T00:00:00Z' })] });
|
const wrapper = mountPage();
|
await vi.waitFor(() => expect(apiMocks.getPersonalFolderFiles).toHaveBeenCalledOnce());
|
wrapper.findComponent({ name: 'BasicLeftTree' }).vm.$emit('select', 'DOWNLOAD');
|
await vi.waitFor(() => expect(wrapper.findComponent({ name: 'TableAction' }).props('actions')).toHaveLength(2));
|
const action = (wrapper.findComponent({ name: 'TableAction' }).props('actions') as any[])[1];
|
expect(action.disabled).toBe(true);
|
expect(apiMocks.createDmsFileDownloadTicket).not.toHaveBeenCalled();
|
});
|
|
it('disables downloads for expired file lifecycle and exhausted workflow authorization', async () => {
|
apiMocks.getPersonalFolderFiles.mockImplementation(async (folderType: string) => ({
|
data: folderType === 'DOWNLOAD' ? [file({ id: 'expired', lifecycleStatus: 'EXPIRED' }), file({ id: 'used' })] : [file()],
|
}));
|
apiMocks.evaluatePersonalFolderDownloads.mockResolvedValue({
|
data: [
|
{ allowed: true, resourceId: 'expired' },
|
{ allowed: false, resourceId: 'used' },
|
],
|
});
|
const wrapper = mountPage();
|
await vi.waitFor(() => expect(apiMocks.getPersonalFolderFiles).toHaveBeenCalledOnce());
|
wrapper.findComponent({ name: 'BasicLeftTree' }).vm.$emit('select', 'DOWNLOAD');
|
await vi.waitFor(() => expect(wrapper.findAllComponents({ name: 'TableAction' })).toHaveLength(2));
|
expect(wrapper.findAllComponents({ name: 'TableAction' }).map((item) => (item.props('actions') as any[])[1].disabled)).toEqual([true, true]);
|
expect(apiMocks.evaluatePersonalFolderDownloads).toHaveBeenCalledWith(['expired', 'used']);
|
});
|
|
it('disables download when authorization expires while the page stays open', async () => {
|
const deadline = Date.now() + 1000;
|
apiMocks.getPersonalFolderFiles.mockResolvedValue({ data: [file({ validTo: new Date(deadline).toISOString() })] });
|
const wrapper = mountPage();
|
await vi.waitFor(() => expect(apiMocks.getPersonalFolderFiles).toHaveBeenCalledOnce());
|
wrapper.findComponent({ name: 'BasicLeftTree' }).vm.$emit('select', 'DOWNLOAD');
|
await vi.waitFor(() => expect((wrapper.findComponent({ name: 'TableAction' }).props('actions') as any[])[1].disabled).toBe(false));
|
await vi.waitFor(() => expect((wrapper.findComponent({ name: 'TableAction' }).props('actions') as any[])[1].disabled).toBe(true), { timeout: 2000 });
|
wrapper.unmount();
|
});
|
|
it('disables download as soon as the ticket is issued', async () => {
|
apiMocks.downloadDmsFile.mockReturnValue(new Promise(() => {}));
|
const wrapper = mountPage();
|
await vi.waitFor(() => expect(apiMocks.getPersonalFolderFiles).toHaveBeenCalledOnce());
|
wrapper.findComponent({ name: 'BasicLeftTree' }).vm.$emit('select', 'DOWNLOAD');
|
await vi.waitFor(() => expect((wrapper.findComponent({ name: 'TableAction' }).props('actions') as any[])[1].disabled).toBe(false));
|
(wrapper.findComponent({ name: 'TableAction' }).props('actions') as any[])[1].onClick();
|
wrapper.findComponent({ name: 'BasicModal' }).vm.$emit('ok');
|
await vi.waitFor(() => expect(apiMocks.downloadDmsFile).toHaveBeenCalledOnce());
|
expect((wrapper.findComponent({ name: 'TableAction' }).props('actions') as any[])[1].disabled).toBe(true);
|
});
|
|
it('explains expiry while the format dialog is open', async () => {
|
const deadline = Date.now() + 60_000;
|
apiMocks.getPersonalFolderFiles.mockResolvedValue({ data: [file({ validTo: new Date(deadline).toISOString() })] });
|
const wrapper = mountPage();
|
await vi.waitFor(() => expect(apiMocks.getPersonalFolderFiles).toHaveBeenCalledOnce());
|
wrapper.findComponent({ name: 'BasicLeftTree' }).vm.$emit('select', 'DOWNLOAD');
|
await vi.waitFor(() => expect(wrapper.findComponent({ name: 'TableAction' }).props('actions')).toHaveLength(2));
|
await (wrapper.findComponent({ name: 'TableAction' }).props('actions') as any[])[1].onClick();
|
const clock = vi.spyOn(Date, 'now').mockReturnValue(deadline);
|
try {
|
wrapper.findComponent({ name: 'BasicModal' }).vm.$emit('ok');
|
await nextTick();
|
expect(messageMocks.error).toHaveBeenCalledWith('下载授权已过期,请重新提交下载申请');
|
expect(apiMocks.createDmsFileDownloadTicket).not.toHaveBeenCalled();
|
} finally {
|
clock.mockRestore();
|
}
|
});
|
});
|