Files
jh-military-admin/tests/platform-resource-dialog.test.ts
T
dzc 3554d0cc80
build / task (16.x, npm, macOS-latest) (push) Canceled after 0s
build / task (16.x, npm, ubuntu-latest) (push) Canceled after 0s
build / task (16.x, npm, windows-latest) (push) Canceled after 0s
build / task (16.x, pnpm, macOS-latest) (push) Canceled after 0s
build / task (16.x, pnpm, ubuntu-latest) (push) Canceled after 0s
build / task (16.x, pnpm, windows-latest) (push) Canceled after 0s
build / task (16.x, yarn, macOS-latest) (push) Canceled after 0s
build / task (16.x, yarn, ubuntu-latest) (push) Canceled after 0s
build / task (16.x, yarn, windows-latest) (push) Canceled after 0s
build / task (18.x, npm, macOS-latest) (push) Canceled after 0s
build / task (18.x, npm, ubuntu-latest) (push) Canceled after 0s
build / task (18.x, npm, windows-latest) (push) Canceled after 0s
build / task (18.x, pnpm, macOS-latest) (push) Canceled after 0s
build / task (18.x, pnpm, ubuntu-latest) (push) Canceled after 0s
build / task (18.x, pnpm, windows-latest) (push) Canceled after 0s
build / task (18.x, yarn, macOS-latest) (push) Canceled after 0s
build / task (18.x, yarn, ubuntu-latest) (push) Canceled after 0s
build / task (18.x, yarn, windows-latest) (push) Canceled after 0s
build / task (20.x, npm, macOS-latest) (push) Canceled after 0s
build / task (20.x, npm, ubuntu-latest) (push) Canceled after 0s
build / task (20.x, npm, windows-latest) (push) Canceled after 0s
build / task (20.x, pnpm, macOS-latest) (push) Canceled after 0s
build / task (20.x, pnpm, ubuntu-latest) (push) Canceled after 0s
build / task (20.x, pnpm, windows-latest) (push) Canceled after 0s
build / task (20.x, yarn, macOS-latest) (push) Canceled after 0s
build / task (20.x, yarn, ubuntu-latest) (push) Canceled after 0s
build / task (20.x, yarn, windows-latest) (push) Canceled after 0s
11
2026-10-27 16:38:51 +08:00

248 lines
12 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { computed, defineComponent, h, inject, provide, type ComputedRef } from 'vue';
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils';
import PlatformResourceDialog from '../src/views/collection/video/components/platform-resource-dialog.vue';
import type { PlatformDetailItem, PlatformDetailQuery, PlatformResourceType } from '../src/api/collection/videoPlatform';
const api = vi.hoisted(() => ({
getPlatformDetail: vi.fn(),
syncMissingDevices: vi.fn(), deleteLocalExtraDevices: vi.fn(),
syncMissingChannels: vi.fn(), deleteLocalExtraChannels: vi.fn(),
syncMissingPersons: vi.fn(), deleteLocalExtraPersons: vi.fn(),
syncMissingCars: vi.fn(), deleteLocalExtraCars: vi.fn(),
confirm: vi.fn(), success: vi.fn(), error: vi.fn(), request: vi.fn(),
}));
vi.mock('/@/api/collection/videoPlatform', () => api);
vi.mock('/@/hooks/message', () => ({
useMessage: () => ({ success: api.success, error: api.error }),
useMessageBox: () => ({ confirm: api.confirm }),
}));
vi.mock('/@/utils/request', () => ({ default: api.request }));
const slotStub = defineComponent({ setup: (_, { slots }) => () => h('div', slots.default?.()) });
const tableStub = defineComponent({
props: ['data'],
setup(props, { slots }) {
provide('rows', computed(() => props.data));
return () => h('div', slots.default?.());
},
});
const columnStub = defineComponent({
props: ['label', 'prop'],
setup(props, { slots }) {
const rows = inject<ComputedRef<PlatformDetailItem[]>>('rows')!;
return () => h('div', { 'data-column': props.label }, [
props.label,
...rows.value.map(row => h('div', slots.default?.({ row }) || String(row[props.prop as keyof PlatformDetailItem] || '-'))),
]);
},
});
const permissions = new Set<string>();
const mounted: VueWrapper[] = [];
const mutationNames = [
'syncMissingDevices', 'deleteLocalExtraDevices', 'syncMissingChannels', 'deleteLocalExtraChannels',
'syncMissingPersons', 'deleteLocalExtraPersons', 'syncMissingCars', 'deleteLocalExtraCars',
] as const;
function detail(query: PlatformDetailQuery) {
return { data: {
...query, total: 2, remoteTotal: 2, localTotal: 2, remoteOnlyCount: 1, localOnlyCount: 1, matchedCount: 1,
records: [
{ code: '9007199254740993', name: 'ICC 档案', businessCode: 'P-1', source: 'ICC', differenceType: 'REMOTE_ONLY' },
{ code: '2', name: '本地档案', source: 'LOCAL', differenceType: 'LOCAL_ONLY' },
],
} };
}
function setupDialog() {
const wrapper = mount(PlatformResourceDialog, {
global: {
directives: {
loading: () => {},
auth: { mounted: (el, binding) => { if (!permissions.has(binding.value)) el.remove(); } },
},
stubs: {
ElDialog: defineComponent({
props: ['modelValue', 'title', 'beforeClose'],
setup: (props, { slots }) => () => props.modelValue
? h('div', [h('h2', props.title), slots.default?.(), slots.footer?.()]) : null,
}),
ElButton: defineComponent({
props: { disabled: Boolean, loading: Boolean },
setup: (props, { slots }) => () => h('button', { disabled: props.disabled || props.loading }, slots.default?.()),
}),
ElAlert: defineComponent({
props: ['title'],
setup: (props, { slots }) => () => h('div', { role: 'alert' }, [props.title, slots.default?.()]),
}),
ElTable: tableStub, ElTableColumn: columnStub,
ElTag: slotStub, ElDivider: slotStub, ElPagination: slotStub,
},
},
});
mounted.push(wrapper);
return wrapper;
}
async function open(wrapper: VueWrapper, type: PlatformResourceType, view = 'DIFFERENCE') {
(wrapper.vm as any).open(type, view, `${type} 明细`);
await flushPromises();
}
function button(wrapper: VueWrapper, label: string) {
const result = wrapper.findAll('button').find(item => item.text().includes(label));
if (!result) throw new Error(`找不到按钮:${label}`);
return result;
}
beforeEach(() => {
vi.resetAllMocks();
permissions.clear();
['admin_deviceManage', 'admin_sysPerson', 'car_carManage'].forEach(prefix => {
permissions.add(`${prefix}_edit`);
permissions.add(`${prefix}_del`);
});
api.getPlatformDetail.mockImplementation(async query => detail(query));
api.confirm.mockResolvedValue(undefined);
mutationNames.forEach(name => api[name].mockResolvedValue({ data: 1 }));
});
afterEach(() => mounted.splice(0).forEach(wrapper => wrapper.unmount()));
describe('ICC 人员车辆差异弹窗', () => {
it.each([
['PERSON', '人员', 'ICC 人员 ID', '人员姓名', '手机号'],
['CAR', '车辆', 'ICC 车辆 ID', '车牌号码', '车主'],
] as const)('展示 %s 的双向差异和对应档案字段', async (type, label, idLabel, nameLabel, extraLabel) => {
const wrapper = setupDialog();
await open(wrapper, type);
expect(wrapper.text()).toContain('本地缺少 1 条');
expect(wrapper.text()).toContain('ICC 缺少 1 条');
expect(wrapper.text()).toContain(`按 ICC ${label} ID 匹配`);
[idLabel, nameLabel, extraLabel].forEach(column => expect(wrapper.find(`[data-column="${column}"]`).exists()).toBe(true));
expect(wrapper.find('[data-column="所属设备编码"]').exists()).toBe(false);
expect(wrapper.find('[data-column="在线状态"]').exists()).toBe(false);
expect(button(wrapper, `同步本地缺少${label}`).attributes('disabled')).toBeUndefined();
expect(button(wrapper, `删除本地多余${label}`).attributes('disabled')).toBeUndefined();
});
it.each([
['PERSON', '人员', 'syncMissingPersons', 'deleteLocalExtraPersons'],
['CAR', '车辆', 'syncMissingCars', 'deleteLocalExtraCars'],
] as const)('批量处理 %s 后重新加载并通知页面刷新', async (type, label, sync, remove) => {
const wrapper = setupDialog();
await open(wrapper, type);
await button(wrapper, `同步本地缺少${label}`).trigger('click');
await flushPromises();
expect(api[sync]).toHaveBeenCalledWith({ resourceIds: [] });
expect(wrapper.emitted('changed')).toHaveLength(1);
await button(wrapper, `删除本地多余${label}`).trigger('click');
await flushPromises();
expect(api[remove]).toHaveBeenCalledWith({ resourceIds: [] });
expect(api.confirm.mock.lastCall?.[0]).toContain('保留历史进出记录');
expect(wrapper.emitted('changed')).toHaveLength(2);
expect(api.getPlatformDetail).toHaveBeenCalledTimes(3);
});
it('逐条操作保留 ICC 大整数 ID 精度', async () => {
const wrapper = setupDialog();
await open(wrapper, 'PERSON');
const sync = wrapper.findAll('button').find(item => item.text() === '同步')!;
await sync.trigger('click');
await flushPromises();
expect(api.syncMissingPersons).toHaveBeenCalledWith({ resourceIds: ['9007199254740993'] });
await wrapper.findAll('button').find(item => item.text() === '删除本地')!.trigger('click');
await flushPromises();
expect(api.deleteLocalExtraPersons).toHaveBeenCalledWith({ resourceIds: ['2'] });
});
it('取消确认后不发起修改请求', async () => {
api.confirm.mockRejectedValue('cancel');
const wrapper = setupDialog();
await open(wrapper, 'CAR');
await button(wrapper, '删除本地多余车辆').trigger('click');
await flushPromises();
expect(api.deleteLocalExtraCars).not.toHaveBeenCalled();
expect(button(wrapper, '关闭').attributes('disabled')).toBeUndefined();
});
it('请求失败保留错误并隐藏差异处理按钮', async () => {
api.getPlatformDetail.mockRejectedValue({ msg: 'ICC 档案未完整读取' });
const wrapper = setupDialog();
await open(wrapper, 'PERSON');
expect(wrapper.text()).toContain('ICC 档案未完整读取');
expect(wrapper.text()).not.toContain('无缺少或多余数据');
expect(wrapper.text()).not.toContain('同步本地缺少人员');
expect(button(wrapper, '重新加载').exists()).toBe(true);
});
it('使用人员车辆各自权限,并在切换资源时重新检查', async () => {
permissions.delete('car_carManage_del');
permissions.delete('admin_sysPerson_edit');
const wrapper = setupDialog();
await open(wrapper, 'PERSON');
expect(wrapper.text()).not.toContain('同步本地缺少人员');
expect(wrapper.text()).toContain('删除本地多余人员');
await open(wrapper, 'CAR');
expect(wrapper.text()).toContain('同步本地缺少车辆');
expect(wrapper.text()).not.toContain('删除本地多余车辆');
});
it('确认中禁用重复提交和关闭', async () => {
let resolveConfirm!: () => void;
api.confirm.mockImplementation(() => new Promise<void>(resolve => { resolveConfirm = resolve; }));
const wrapper = setupDialog();
await open(wrapper, 'CAR');
await button(wrapper, '同步本地缺少车辆').trigger('click');
expect(button(wrapper, '关闭').attributes('disabled')).toBeDefined();
await button(wrapper, '同步本地缺少车辆').trigger('click');
expect(api.confirm).toHaveBeenCalledOnce();
resolveConfirm();
await flushPromises();
expect(api.syncMissingCars).toHaveBeenCalledOnce();
});
it('切换资源后忽略旧查询的迟到响应', async () => {
let resolveOld!: (value: unknown) => void;
api.getPlatformDetail.mockImplementationOnce(() => new Promise(resolve => { resolveOld = resolve; }));
const wrapper = setupDialog();
await open(wrapper, 'PERSON');
await open(wrapper, 'CAR');
resolveOld({ data: { ...detail({ resourceType: 'PERSON', viewType: 'DIFFERENCE', current: 1, size: 20 }).data, records: [] } });
await flushPromises();
expect(wrapper.text()).toContain('ICC 档案');
expect(wrapper.text()).toContain('车牌号码');
});
it('无差异时禁用批量操作', async () => {
api.getPlatformDetail.mockImplementation(async query => ({ data: {
...detail(query).data, total: 0, remoteOnlyCount: 0, localOnlyCount: 0, records: [],
} }));
const wrapper = setupDialog();
await open(wrapper, 'PERSON');
expect(button(wrapper, '同步本地缺少人员').attributes('disabled')).toBeDefined();
expect(button(wrapper, '删除本地多余人员').attributes('disabled')).toBeDefined();
});
it.each([
['DEVICE', '设备', 'syncMissingDevices', { deviceCodes: [] }],
['CHANNEL', '通道', 'syncMissingChannels', { channelCodes: [] }],
] as const)('保留 %s 原有同步入口', async (type, label, method, payload) => {
const wrapper = setupDialog();
await open(wrapper, type);
await button(wrapper, `同步本地缺少${label}`).trigger('click');
await flushPromises();
expect(api[method]).toHaveBeenCalledWith(payload);
});
});
describe('ICC 档案 API 路由', () => {
it.each(['PERSON', 'CAR', 'DEVICE', 'CHANNEL'] as const)('%s 明细使用对应查询权限接口', async resourceType => {
const actual = await vi.importActual<typeof import('../src/api/collection/videoPlatform')>('../src/api/collection/videoPlatform');
const query: PlatformDetailQuery = { resourceType, viewType: 'DIFFERENCE', current: 1, size: 20 };
actual.getPlatformDetail(query);
const prefix = resourceType === 'PERSON' ? '/person' : resourceType === 'CAR' ? '/car' : '';
expect(api.request).toHaveBeenCalledWith(expect.objectContaining({ url: `/admin/third/platform${prefix}/detail`, params: query }));
});
});