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
248 lines
12 KiB
TypeScript
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 }));
|
|
});
|
|
});
|