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
270 lines
10 KiB
TypeScript
270 lines
10 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { defineComponent, h, nextTick } from 'vue';
|
|
import { flushPromises, mount } from '@vue/test-utils';
|
|
import { useVideoDialog } from '../src/views/device/components/useVideoDialog';
|
|
import VideoChannelPanel from '../src/views/device/components/video-channel-panel.vue';
|
|
|
|
const api = vi.hoisted(() => ({ list: vi.fn(), getObj: vi.fn(), getByCode: vi.fn(), error: vi.fn() }));
|
|
vi.mock('/@/api/device/deviceChannel', () => ({ list: api.list }));
|
|
vi.mock('/@/api/device/deviceManage', () => ({ getObj: api.getObj, getByCode: api.getByCode }));
|
|
vi.mock('/@/hooks/message', () => ({ useMessage: () => ({ error: api.error }) }));
|
|
|
|
const makeChannels = (count = 3) =>
|
|
Array.from({ length: count }, (_, index) => ({
|
|
channelCode: `camera-${index}`,
|
|
channelName: `Camera ${index}`,
|
|
channelSeq: index,
|
|
deviceName: '测试设备',
|
|
}));
|
|
const makePlayer = () => ({
|
|
startReal: vi.fn().mockResolvedValue(undefined),
|
|
startPlayback: vi.fn().mockResolvedValue(undefined),
|
|
closeVideo: vi.fn().mockResolvedValue(undefined),
|
|
destroy: vi.fn(),
|
|
updateVisible: vi.fn(),
|
|
});
|
|
const mounted: ReturnType<typeof mount>[] = [];
|
|
const slotStub = defineComponent({
|
|
setup(_, { slots }) {
|
|
return () => h('div', slots.default?.());
|
|
},
|
|
});
|
|
const panelStubs = {
|
|
ElInput: defineComponent({
|
|
props: ['modelValue'],
|
|
emits: ['update:modelValue'],
|
|
setup(props, { emit }) {
|
|
return () =>
|
|
h('input', {
|
|
class: 'el-input__inner',
|
|
value: props.modelValue,
|
|
onInput: (event: Event) => emit('update:modelValue', (event.target as HTMLInputElement).value),
|
|
});
|
|
},
|
|
}),
|
|
ElButton: defineComponent({
|
|
props: { disabled: Boolean },
|
|
setup(props, { slots }) {
|
|
return () => h('button', { disabled: props.disabled }, slots.default?.());
|
|
},
|
|
}),
|
|
ElIcon: slotStub,
|
|
ElRadioGroup: slotStub,
|
|
ElRadioButton: slotStub,
|
|
};
|
|
|
|
function setupDialog(mode: 'preview' | 'replay' = 'preview', getPlayOptions = () => ({ streamType: 2 } as Record<string, string | number> | null)) {
|
|
let dialog!: ReturnType<typeof useVideoDialog>;
|
|
mounted.push(
|
|
mount(
|
|
defineComponent({
|
|
setup() {
|
|
dialog = useVideoDialog({ mode, getPlayOptions });
|
|
return () => h('div');
|
|
},
|
|
})
|
|
)
|
|
);
|
|
const player = makePlayer();
|
|
dialog.playerRef.value = player;
|
|
return { dialog, player };
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.resetAllMocks();
|
|
api.list.mockResolvedValue({ code: 0, data: makeChannels() });
|
|
api.getObj.mockResolvedValue({ code: 0, data: { deviceCode: 'device-code', deviceName: '测试设备' } });
|
|
api.getByCode.mockResolvedValue({ code: 0, data: { deviceCode: 'device-code', deviceName: '测试设备' } });
|
|
});
|
|
afterEach(() => {
|
|
mounted.splice(0).forEach((wrapper) => wrapper.unmount());
|
|
});
|
|
|
|
describe('视频弹窗通道播放', () => {
|
|
it('预览按设备 ID 查询,默认仅播放第一个通道', async () => {
|
|
const { dialog, player } = setupDialog();
|
|
await dialog.openDialog('device', 'device-id');
|
|
await flushPromises();
|
|
expect(api.getObj).toHaveBeenCalledWith('device-id');
|
|
expect(api.list).toHaveBeenCalledWith({ deviceCode: 'device-code' });
|
|
expect(player.startReal).toHaveBeenLastCalledWith([{ channelId: 'camera-0', channelName: 'Camera 0', streamType: 2, snum: 0 }]);
|
|
expect(dialog.division.value).toBe(1);
|
|
});
|
|
|
|
it('回放保留设备编码入口和回放分组入口', async () => {
|
|
const { dialog } = setupDialog('replay');
|
|
await dialog.openDialog('device', 'device-code');
|
|
expect(api.getByCode).toHaveBeenCalledWith({ code: 'device-code' });
|
|
expect(api.getObj).not.toHaveBeenCalled();
|
|
await dialog.openDialog('playbackGroup', 'group-1');
|
|
expect(api.list).toHaveBeenLastCalledWith({ playbackGroupChannelCode: 'group-1' });
|
|
});
|
|
|
|
it('单选切换前清空旧画面,新通道从窗口 0 播放', async () => {
|
|
const { dialog, player } = setupDialog();
|
|
await dialog.openDialog('channel', 'camera-0');
|
|
await flushPromises();
|
|
player.startReal.mockClear();
|
|
player.closeVideo.mockClear();
|
|
dialog.changeSelection(['camera-2']);
|
|
await flushPromises();
|
|
expect(api.list).toHaveBeenCalledWith({ channelCode: 'camera-0' });
|
|
expect(player.closeVideo).toHaveBeenCalledOnce();
|
|
expect(player.startReal).toHaveBeenLastCalledWith([expect.objectContaining({ channelId: 'camera-2', snum: 0 })]);
|
|
expect(player.closeVideo.mock.invocationCallOrder[0]).toBeLessThan(player.startReal.mock.invocationCallOrder[0]);
|
|
});
|
|
|
|
it('多选暂存后统一播放,切回单选恢复单画面', async () => {
|
|
const { dialog, player } = setupDialog();
|
|
await dialog.openDialog('channel', 'camera-0');
|
|
await flushPromises();
|
|
player.startReal.mockClear();
|
|
dialog.changeSelectionMode('multiple');
|
|
dialog.changeSelection(['camera-0', 'camera-1', 'camera-2', 'camera-2', 'missing']);
|
|
await flushPromises();
|
|
expect(player.startReal).not.toHaveBeenCalled();
|
|
expect(dialog.playingCodes.value).toEqual(['camera-0']);
|
|
await dialog.playSelected();
|
|
expect(player.startReal.mock.lastCall?.[0]).toHaveLength(3);
|
|
expect(dialog.division.value).toBe(4);
|
|
dialog.changeSelectionMode('single');
|
|
await flushPromises();
|
|
expect(player.startReal.mock.lastCall?.[0]).toHaveLength(1);
|
|
expect(dialog.division.value).toBe(1);
|
|
});
|
|
|
|
it('超出 16 路按所选通道分页,窗口号每页从零开始', async () => {
|
|
api.list.mockResolvedValue({ code: 0, data: makeChannels(19) });
|
|
const { dialog, player } = setupDialog();
|
|
await dialog.openDialog('device', 'device-id');
|
|
await flushPromises();
|
|
dialog.changeSelectionMode('multiple');
|
|
dialog.changeSelection(makeChannels(19).map((channel) => channel.channelCode));
|
|
await dialog.playSelected();
|
|
expect(player.startReal.mock.lastCall?.[0]).toHaveLength(16);
|
|
await dialog.changePage(2);
|
|
expect(player.startReal.mock.lastCall?.[0].map((channel: any) => [channel.channelId, channel.snum])).toEqual([
|
|
['camera-16', 0],
|
|
['camera-17', 1],
|
|
['camera-18', 2],
|
|
]);
|
|
expect(dialog.division.value).toBe(4);
|
|
dialog.changeSelection(['camera-18']);
|
|
await dialog.playSelected();
|
|
expect(dialog.currentPage.value).toBe(1);
|
|
expect(player.startReal.mock.lastCall?.[0]).toEqual([expect.objectContaining({ channelId: 'camera-18', snum: 0 })]);
|
|
});
|
|
|
|
it('回放翻页沿用已提交的时间、码流、来源', async () => {
|
|
api.list.mockResolvedValue({ code: 0, data: makeChannels(18) });
|
|
const filters = { startTime: '2026-10-25 00:00:00', endTime: '2026-10-27 23:59:59', streamType: 0, recordSource: 2 };
|
|
const snapshot = { ...filters };
|
|
const { dialog, player } = setupDialog('replay', () => filters);
|
|
await dialog.openDialog('device', 'device-code');
|
|
await flushPromises();
|
|
dialog.changeSelectionMode('multiple');
|
|
dialog.changeSelection(makeChannels(18).map((channel) => channel.channelCode));
|
|
await dialog.playSelected();
|
|
filters.startTime = '2026-10-26 00:00:00';
|
|
filters.streamType = 1;
|
|
filters.recordSource = 3;
|
|
await dialog.changePage(2);
|
|
expect(player.startPlayback.mock.lastCall?.[0][0]).toMatchObject(snapshot);
|
|
await dialog.playSelected();
|
|
expect(player.startPlayback.mock.lastCall?.[0][0]).toMatchObject(filters);
|
|
});
|
|
|
|
it('关闭弹窗后丢弃晚到的通道响应', async () => {
|
|
let resolve!: (value: unknown) => void;
|
|
api.list.mockReturnValue(
|
|
new Promise((done) => {
|
|
resolve = done;
|
|
})
|
|
);
|
|
const { dialog, player } = setupDialog();
|
|
const opening = dialog.openDialog('channel', 'camera-0');
|
|
dialog.closeDialog();
|
|
resolve({ code: 0, data: makeChannels() });
|
|
await opening;
|
|
await flushPromises();
|
|
expect(dialog.channels.value).toEqual([]);
|
|
expect(player.startReal).not.toHaveBeenCalled();
|
|
expect(player.destroy).toHaveBeenCalled();
|
|
});
|
|
|
|
it('连续打开不同来源时,旧响应不能覆盖新来源', async () => {
|
|
let resolve!: (value: unknown) => void;
|
|
api.list
|
|
.mockReturnValueOnce(
|
|
new Promise((done) => {
|
|
resolve = done;
|
|
})
|
|
)
|
|
.mockResolvedValueOnce({ code: 0, data: [makeChannels()[2]] });
|
|
const { dialog } = setupDialog();
|
|
const older = dialog.openDialog('channel', 'older');
|
|
await dialog.openDialog('channel', 'newer');
|
|
resolve({ code: 0, data: makeChannels() });
|
|
await older;
|
|
await flushPromises();
|
|
expect(dialog.channels.value.map((channel) => channel.channelCode)).toEqual(['camera-2']);
|
|
});
|
|
|
|
it('播放切换等待期间关闭弹窗,不再启动旧播放', async () => {
|
|
const { dialog, player } = setupDialog();
|
|
await dialog.openDialog('channel', 'camera-0');
|
|
await flushPromises();
|
|
player.startReal.mockClear();
|
|
let resolve!: () => void;
|
|
player.closeVideo.mockReturnValue(
|
|
new Promise<void>((done) => {
|
|
resolve = done;
|
|
})
|
|
);
|
|
dialog.changeSelection(['camera-1']);
|
|
await nextTick();
|
|
dialog.closeDialog();
|
|
resolve();
|
|
await flushPromises();
|
|
expect(player.startReal).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('设备详情失败不会发起无范围的通道查询', async () => {
|
|
api.getObj.mockResolvedValue({ code: 1, data: null });
|
|
const { dialog, player } = setupDialog();
|
|
await dialog.openDialog('device', 'missing');
|
|
await flushPromises();
|
|
expect(api.list).not.toHaveBeenCalled();
|
|
expect(player.startReal).not.toHaveBeenCalled();
|
|
expect(dialog.loadError.value).toBe('获取设备信息失败');
|
|
});
|
|
});
|
|
|
|
describe('通道选择面板', () => {
|
|
it('搜索全选只添加匹配项,保留已有选择,清空后禁用播放', async () => {
|
|
const wrapper = mount(VideoChannelPanel, {
|
|
props: { channels: makeChannels(), modelValue: ['camera-0'], mode: 'multiple', loading: false, playingCodes: ['camera-0'] },
|
|
global: { stubs: panelStubs, directives: { loading: () => undefined } },
|
|
});
|
|
mounted.push(wrapper);
|
|
await wrapper.get('input.el-input__inner').setValue('Camera 2');
|
|
expect(wrapper.findAll('.video-channel-row')).toHaveLength(1);
|
|
const selectAll = wrapper.findAll('button').find((button) => button.text() === '全选结果')!;
|
|
await selectAll.trigger('click');
|
|
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([['camera-0', 'camera-2']]);
|
|
await wrapper
|
|
.findAll('button')
|
|
.find((button) => button.text() === '清空')!
|
|
.trigger('click');
|
|
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([[]]);
|
|
await wrapper.setProps({ modelValue: [] });
|
|
expect(
|
|
wrapper
|
|
.findAll('button')
|
|
.find((button) => button.text() === '播放所选')!
|
|
.attributes('disabled')
|
|
).toBeDefined();
|
|
});
|
|
});
|