Files
jh-military-admin/tests/device-list-actions.test.ts
T
dzc 7670ea22b2
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-11-04 12:31:49 +08:00

213 lines
8.4 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { defineComponent, h } from 'vue';
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils';
import ElementPlus from 'element-plus';
import DeviceRowActions from '../src/views/device/components/device-row-actions.vue';
import type { DeviceRowAction } from '../src/views/device/components/device-row-actions';
import CameraList from '../src/views/device/components/camera-list.vue';
import PlatformCompareButton from '../src/views/collection/video/components/platform-compare-button.vue';
const spies = vi.hoisted(() => ({
permissions: new Set<string>(),
preview: vi.fn(),
replay: vi.fn(),
edit: vi.fn(),
push: vi.fn(),
deviceQuery: vi.fn(),
channelQuery: vi.fn(),
}));
vi.mock('/@/utils/authFunction', () => ({ auth: (permission: string) => spies.permissions.has(permission) }));
vi.mock('vue-router', () => ({ useRouter: () => ({ push: spies.push }) }));
vi.mock('/@/api/device/deviceManage', () => ({ cameraPage: vi.fn(), delObjs: vi.fn() }));
vi.mock('/@/api/device/deviceChannel', () => ({ cameraChannelPage: vi.fn(), delObjs: vi.fn() }));
vi.mock('/@/hooks/dict', () => ({
useDict: () => ({ device_category: [], is_sync: [], is_nline: [], dh_device_manufacturer: [], camera_type: [] }),
}));
vi.mock('/@/hooks/table', () => ({
useTable: (state: any) => {
const channel = state.createdIsNeed === false;
state.dataList = channel
? [{ id: 'channel-id', channelName: '测试通道', channelCode: 'channel-code', deviceCode: 'device-code' }]
: [{ id: 'device-id', deviceName: '测试设备', deviceCode: 'device-code', deviceIp: '192.0.2.10', devicePort: 80 }];
state.pagination = { total: 1, current: 1, size: 20 };
return { getDataList: channel ? spies.channelQuery : spies.deviceQuery };
},
}));
vi.mock('/@/hooks/message', () => ({ useMessage: () => ({}), useMessageBox: () => ({}) }));
vi.mock('/@/views/collection/video/components/platform-compare-button.vue', () => ({
default: { name: 'PlatformCompareButton', props: ['resourceType'], emits: ['changed'], render: () => null },
}));
const actions: DeviceRowAction[] = [
{ key: 'preview', label: '预览' },
{ key: 'replay', label: '回放' },
{ key: 'edit', label: '编辑', permission: 'edit' },
{ key: 'delete', label: '删除', permission: 'delete', danger: true },
];
const mounted: VueWrapper[] = [];
const slotStub = defineComponent({
setup:
(_, { slots }) =>
() =>
h('div', slots.default?.()),
});
const dialogStub = (openDialog: (...args: any[]) => any) =>
defineComponent({
setup: (_, { expose }) => {
expose({ openDialog });
return () => null;
},
});
function renderActions(value = actions) {
const wrapper = mount(DeviceRowActions, { props: { actions: value }, global: { plugins: [ElementPlus] } });
document.body.appendChild(wrapper.element);
mounted.push(wrapper);
return wrapper;
}
function renderCamera(props = {}) {
const wrapper = mount(CameraList, {
props,
global: {
plugins: [ElementPlus],
mocks: { $t: (key: string) => key },
directives: {
auth: {
mounted: (el, binding) => {
if (!spies.permissions.has(binding.value)) el.remove();
},
},
},
stubs: {
DraggableOrgTree: slotStub,
DictSelect: slotStub,
DictTag: slotStub,
RightToolbar: slotStub,
Pagination: slotStub,
UploadExcel: slotStub,
FormDialog: dialogStub(spies.edit),
DeviceChannerForm: dialogStub(spies.edit),
PreviewVideoDialog: dialogStub(spies.preview),
ReplayVideoDialog: dialogStub(spies.replay),
PlatformCompareButton: true,
},
},
});
document.body.appendChild(wrapper.element);
mounted.push(wrapper);
return wrapper;
}
function button(wrapper: VueWrapper, label: string) {
const match = wrapper.findAll('button').find((item) => item.text() === label);
if (!match) throw new Error(`找不到按钮:${label}`);
return match;
}
async function menuItem(label: string) {
await flushPromises();
const match = [...document.querySelectorAll<HTMLElement>('[role="menuitem"]')].find((item) => item.textContent?.trim() === label);
if (!match) throw new Error(`找不到菜单项:${label}`);
return match;
}
beforeEach(() => {
vi.clearAllMocks();
spies.permissions.clear();
});
afterEach(() => {
mounted.splice(0).forEach((wrapper) => wrapper.unmount());
document.body.innerHTML = '';
});
describe('设备列表行操作', () => {
it('先过滤权限,常用按钮不留空位或空的更多菜单', () => {
const wrapper = renderActions([actions[2], actions[0], actions[3]]);
expect(wrapper.text()).toBe('预览');
expect(wrapper.findAll('button')).toHaveLength(1);
});
it('更多菜单保留有权限的操作,点击前不触发操作', async () => {
spies.permissions.add('edit');
spies.permissions.add('delete');
const wrapper = renderActions();
await button(wrapper, '预览').trigger('click');
expect(wrapper.emitted('select')).toEqual([['preview']]);
await button(wrapper, '更多').trigger('click');
expect((await menuItem('删除')).textContent).toContain('删除');
expect(wrapper.emitted('select')).toHaveLength(1);
(await menuItem('编辑')).click();
await flushPromises();
expect(wrapper.emitted('select')).toEqual([['preview'], ['edit']]);
});
it('没有任何可用操作时不渲染菜单,加载中操作无法重复触发', async () => {
const empty = renderActions([actions[2], actions[3]]);
expect(empty.findAll('button')).toHaveLength(0);
const wrapper = renderActions([{ key: 'sync', label: '同步 ICC', loading: true }]);
await button(wrapper, '同步 ICC').trigger('click');
expect(wrapper.emitted('select')).toBeUndefined();
});
it('权限变化后,已经打开的菜单也不能发出无权限操作', async () => {
spies.permissions.add('delete');
const wrapper = renderActions();
await button(wrapper, '更多').trigger('click');
const item = await menuItem('删除');
spies.permissions.delete('delete');
item.click();
await flushPromises();
expect(wrapper.emitted('select')).toBeUndefined();
});
});
describe('摄像头列表操作入口', () => {
it('设备预览传设备 ID,回放传设备编码,详情导航仍可用', async () => {
const wrapper = renderCamera();
await flushPromises();
await button(wrapper, '预览').trigger('click');
await button(wrapper, '回放').trigger('click');
expect(spies.preview).toHaveBeenCalledWith('device', 'device-id');
expect(spies.replay).toHaveBeenCalledWith('device', 'device-code');
await button(wrapper, '更多').trigger('click');
(await menuItem('详情')).click();
await flushPromises();
expect(spies.push).toHaveBeenCalledWith({ path: '/device/detail/index', query: { id: 'device-id' } });
});
it('首次进入通道才查询,预览及回放使用通道编码', async () => {
const wrapper = renderCamera({ enableCrud: false, actions: ['preview', 'replay'] });
await flushPromises();
expect(spies.channelQuery).not.toHaveBeenCalled();
const channelTab = wrapper.findAll('[role="tab"]').find((item) => item.text() === '通道')!;
await channelTab.trigger('click');
await flushPromises();
expect(spies.channelQuery).toHaveBeenCalledOnce();
const pane = wrapper.findAll('.device-list-surface > div').find((item) => item.text().includes('测试通道'))!;
await button(pane, '预览').trigger('click');
await button(pane, '回放').trigger('click');
expect(spies.preview).toHaveBeenCalledWith('channel', 'channel-code');
expect(spies.replay).toHaveBeenCalledWith('channel', 'channel-code');
});
it('设备和通道工具栏打开各自类型的对比,修复后刷新已加载的两张表', async () => {
const wrapper = renderCamera();
await flushPromises();
let entries = wrapper.findAllComponents(PlatformCompareButton);
expect(entries.map(entry => entry.props('resourceType'))).toEqual(['DEVICE']);
entries[0].vm.$emit('changed');
await flushPromises();
expect(spies.deviceQuery).toHaveBeenCalledOnce();
expect(spies.channelQuery).not.toHaveBeenCalled();
await wrapper.findAll('[role="tab"]').find(item => item.text() === '通道')!.trigger('click');
await flushPromises();
entries = wrapper.findAllComponents(PlatformCompareButton);
expect(entries.map(entry => entry.props('resourceType'))).toEqual(['DEVICE', 'CHANNEL']);
spies.deviceQuery.mockClear();
spies.channelQuery.mockClear();
entries[1].vm.$emit('changed');
await flushPromises();
expect(spies.deviceQuery).toHaveBeenCalledOnce();
expect(spies.channelQuery).toHaveBeenCalledOnce();
});
});