Files
jh-military-admin/tests/video-plugin.lifecycle.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

405 lines
17 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { mount, type VueWrapper } from '@vue/test-utils';
import { KeepAlive, defineComponent, h, nextTick, ref } from 'vue';
import VideoPlugin from '../src/components/rtsp-plugin-video/video-plugin.vue';
const mocks = vi.hoisted(() => ({ getSetting: vi.fn() }));
vi.mock('../src/components/rtsp-plugin-video/lib/videoPlayer', () => ({}));
vi.mock('../src/api/com/comDhSetting', () => ({ getObj: mocks.getSetting }));
vi.mock('element-plus', () => ({ ElMessage: { error: vi.fn(), success: vi.fn(), warning: vi.fn() } }));
function deferred<T = void>() {
let resolve!: (value: T) => void;
let reject!: (error: Error) => void;
const promise = new Promise<T>((res, rej) => { resolve = res; reject = rej; });
return { promise, resolve, reject };
}
const instances: MockPlayer[] = [];
class MockPlayer {
setting: any;
disposed = false;
ready = false;
closeVideo = vi.fn(() => Promise.resolve());
dispose = vi.fn(() => { this.disposed = true; });
hide = vi.fn();
show = vi.fn();
startReal = vi.fn((items) => { this.setting.channelList = items; });
startPlayback = vi.fn((items) => { this.setting.channelList = items; });
controlPlayback = vi.fn();
setTabControlBtn = vi.fn();
changeDivision = vi.fn((division) => { this.setting.division = division; });
constructor(public options: any) {
this.setting = { ...options, hwnd: instances.length + 10, channelList: [] };
instances.push(this);
}
}
const wrappers: VueWrapper[] = [];
const mountOptions = {
global: { stubs: { Yt: true, 'el-pagination': true, 'el-icon': true }, directives: { loading: () => undefined } },
};
function createPlayer(props = {}) {
const wrapper = mount(VideoPlugin, { ...mountOptions, props });
document.body.appendChild(wrapper.element);
wrappers.push(wrapper);
return wrapper;
}
const channels = (name = 'camera-a') => [{ channelId: name, snum: 0, streamType: 2 }];
const playback = () => [{ ...channels()[0], startTime: '2026-10-27 08:00:00', endTime: '2026-10-27 09:00:00' }];
async function ready(player: MockPlayer) {
player.ready = true;
player.options.createSuccess({});
await vi.advanceTimersByTimeAsync(500);
}
async function play(wrapper: VueWrapper, mode = 'real') {
const task = mode === 'real' ? wrapper.vm.startReal(channels()) : wrapper.vm.startPlayback(playback());
await vi.advanceTimersByTimeAsync(0);
const player = instances.at(-1)!;
if (!player.ready) await ready(player);
await task;
return player;
}
beforeEach(() => {
vi.useFakeTimers();
vi.spyOn(console, 'log').mockImplementation(() => undefined);
Object.defineProperty(document, 'hidden', { configurable: true, value: false });
(window as any).VideoPlayer = MockPlayer;
instances.length = 0;
mocks.getSetting.mockReset().mockResolvedValue({ code: 0, data: { dhIccSetting: {} } });
});
afterEach(() => {
wrappers.splice(0).forEach((wrapper) => { if (wrapper.exists()) wrapper.unmount(); });
vi.clearAllTimers();
vi.useRealTimers();
document.body.innerHTML = '';
});
describe('原生播放器组件的关闭时序', () => {
it('九宫格编号在未安装插件时仍然显示,原生播放器为编号保留遮挡区域', async () => {
const labels = Array.from({ length: 9 }, (_, snum) => ({
snum, label: `${Math.floor(snum / 3) + 1}-${snum % 3 + 1}`,
...(snum === 0 ? { channelId: 'camera-a', channelName: '南门球机' } : {}),
}));
const wrapper = createPlayer({ division: 9, divisionSettingList: [9], windowLabels: labels });
const task = wrapper.vm.startReal(channels());
await vi.advanceTimersByTimeAsync(0);
expect(instances[0].options.division).toBe(9);
expect(instances[0].options.shieldClass).toContain('video-window-label');
instances[0].options.createError({ code: 1001, message: '插件未安装' });
await task;
await nextTick();
expect(wrapper.findAll('.video-window-label').map(item => item.text())).toEqual(labels.map(item => item.label));
expect(wrapper.text()).toContain('南门球机');
expect(wrapper.text()).toContain('未能连接本机视频服务');
expect(wrapper.find('.loading-mask').exists()).toBe(false);
});
it('插件就绪后只保留角标,双击放大只显示对应窗口编号,恢复后还原九格', async () => {
const labels = Array.from({ length: 9 }, (_, snum) => ({
snum, label: `${Math.floor(snum / 3) + 1}-${snum % 3 + 1}`, channelId: `camera-${snum}`,
}));
const wrapper = createPlayer({ division: 9, divisionSettingList: [9], windowLabels: labels });
const player = await play(wrapper);
await nextTick();
expect(wrapper.findAll('.video-window-placeholder')).toHaveLength(0);
expect(wrapper.findAll('.video-window-label')).toHaveLength(9);
player.options.dbClickWindow(4);
await nextTick();
expect(wrapper.findAll('.video-window-label').map(item => item.text())).toEqual(['2-2']);
player.options.dbClickWindow(4);
await nextTick();
expect(wrapper.findAll('.video-window-label')).toHaveLength(9);
player.options.dbClickWindow(4);
player.options.changeDivision(9);
await nextTick();
expect(wrapper.findAll('.video-window-label')).toHaveLength(9);
});
it('仅在超过每页窗口数时显示分页,切换选择能重置到第一页', async () => {
const wrapper = createPlayer({ division: 9, divisionSettingList: [9] });
wrapper.vm.setTotal(9, 1);
await nextTick();
expect(wrapper.find('.video-container-page').exists()).toBe(false);
wrapper.vm.setTotal(11, 2);
await nextTick();
expect(wrapper.find('.video-container-page').exists()).toBe(true);
expect(wrapper.findComponent({ name: 'el-pagination' }).attributes('current-page')).toBe('2');
wrapper.vm.setTotal(11, 1);
await nextTick();
expect(wrapper.findComponent({ name: 'el-pagination' }).attributes('current-page')).toBe('1');
});
it('正常实时预览和回放仍调用对应 SDK 接口', async () => {
const wrapper = createPlayer();
const real = await play(wrapper);
expect(real.startReal).toHaveBeenCalledWith(channels());
const replay = await play(wrapper, 'playback');
expect(replay.startPlayback).toHaveBeenCalledWith(playback());
wrapper.vm.destroy();
expect(real.dispose).toHaveBeenCalledTimes(1);
expect(replay.dispose).toHaveBeenCalledTimes(1);
});
it('同类型并发初始化只查询和构造一次,只执行最后一次播放请求', async () => {
const wrapper = createPlayer();
const first = wrapper.vm.startReal(channels('first'));
const last = wrapper.vm.startReal(channels('last'));
await vi.advanceTimersByTimeAsync(0);
expect(mocks.getSetting).toHaveBeenCalledTimes(1);
expect(instances).toHaveLength(1);
await ready(instances[0]);
await Promise.all([first, last]);
expect(instances[0].startReal).toHaveBeenCalledTimes(1);
expect(instances[0].startReal).toHaveBeenCalledWith(channels('last'));
});
it('配置请求返回之前卸载,不再构造播放器', async () => {
const config = deferred<any>();
mocks.getSetting.mockReturnValue(config.promise);
const wrapper = createPlayer();
const task = wrapper.vm.startReal(channels());
wrapper.unmount();
config.resolve({ code: 0, data: { dhIccSetting: {} } });
await vi.advanceTimersByTimeAsync(0);
await task;
expect(instances).toHaveLength(0);
});
it('原生创建回包晚于卸载,不恢复已取消的播放', async () => {
const wrapper = createPlayer();
const task = wrapper.vm.startReal(channels());
await vi.advanceTimersByTimeAsync(0);
const player = instances[0];
wrapper.unmount();
await ready(player);
await task;
expect(player.dispose).toHaveBeenCalledTimes(1);
expect(player.startReal).not.toHaveBeenCalled();
});
it('创建成功后的 500ms 延迟也能被卸载取消', async () => {
const wrapper = createPlayer();
const task = wrapper.vm.startReal(channels());
await vi.advanceTimersByTimeAsync(0);
const player = instances[0];
player.options.createSuccess({});
wrapper.unmount();
await vi.advanceTimersByTimeAsync(1000);
await task;
expect(player.startReal).not.toHaveBeenCalled();
});
it('软关闭会取消等待中的播放,切回浏览器也不会自行恢复', async () => {
const wrapper = createPlayer();
const task = wrapper.vm.startReal(channels());
await vi.advanceTimersByTimeAsync(0);
const player = instances[0];
await wrapper.vm.closeVideo();
await ready(player);
await task;
document.dispatchEvent(new Event('visibilitychange'));
await vi.advanceTimersByTimeAsync(0);
expect(player.startReal).not.toHaveBeenCalled();
});
it('原生工具栏关闭的通道,不会在浏览器重新可见时复播', async () => {
const wrapper = createPlayer();
const player = await play(wrapper);
player.setting.channelList = [];
player.options.closeWindowSuccess({ isAll: true, channelList: [] });
Object.defineProperty(document, 'hidden', { configurable: true, value: true });
document.dispatchEvent(new Event('visibilitychange'));
Object.defineProperty(document, 'hidden', { configurable: true, value: false });
document.dispatchEvent(new Event('visibilitychange'));
await vi.advanceTimersByTimeAsync(0);
expect(player.startReal).toHaveBeenCalledTimes(1);
await wrapper.vm.startReal(channels());
expect(player.startReal).toHaveBeenCalledTimes(2);
});
it('未 await 的关闭也会在新播放前等待旧 SDK 关闭确认', async () => {
const wrapper = createPlayer();
const player = await play(wrapper);
const closed = deferred();
player.closeVideo.mockReturnValue(closed.promise);
const closeTask = wrapper.vm.closeVideo();
const playTask = wrapper.vm.startReal(channels('next'));
await vi.advanceTimersByTimeAsync(0);
expect(player.startReal).toHaveBeenCalledTimes(1);
closed.resolve();
await Promise.all([closeTask, playTask]);
expect(player.startReal).toHaveBeenLastCalledWith(channels('next'));
});
it('快速切出再切回浏览器,仍恢复正在观看的预览,不会被防重入间隔拦掉', async () => {
const wrapper = createPlayer();
const player = await play(wrapper);
Object.defineProperty(document, 'hidden', { configurable: true, value: true });
document.dispatchEvent(new Event('visibilitychange'));
Object.defineProperty(document, 'hidden', { configurable: true, value: false });
document.dispatchEvent(new Event('visibilitychange'));
await vi.advanceTimersByTimeAsync(0);
expect(player.closeVideo).toHaveBeenCalledTimes(1);
expect(player.startReal).toHaveBeenCalledTimes(2);
});
it('同时关闭多个缓存实例,单个 SDK 不回包也不妨碍销毁其他窗口', async () => {
const wrapper = createPlayer();
const real = await play(wrapper);
const replay = await play(wrapper, 'playback');
const closed = deferred();
real.closeVideo.mockReturnValue(closed.promise);
const closing = wrapper.vm.closeVideo();
expect(real.closeVideo).toHaveBeenCalledTimes(1);
expect(replay.closeVideo).toHaveBeenCalledTimes(1);
wrapper.vm.destroy();
expect(real.dispose).toHaveBeenCalledTimes(1);
expect(replay.dispose).toHaveBeenCalledTimes(1);
closed.resolve();
await closing;
});
it('重叠关闭不会重复给同一实例发送关闭指令', async () => {
const wrapper = createPlayer();
const player = await play(wrapper);
const closed = deferred();
player.closeVideo.mockReturnValue(closed.promise);
const first = wrapper.vm.closeVideo();
const second = wrapper.vm.closeVideo();
expect(player.closeVideo).toHaveBeenCalledTimes(1);
closed.resolve();
await Promise.all([first, second]);
});
it('销毁会中断对旧关闭回包的等待,旧 SDK 永不回包也能重新打开', async () => {
const wrapper = createPlayer();
const old = await play(wrapper);
old.closeVideo.mockReturnValue(new Promise(() => undefined));
void wrapper.vm.closeVideo();
const waiting = wrapper.vm.startReal(channels('waiting'));
await vi.advanceTimersByTimeAsync(0);
wrapper.vm.destroy();
await waiting;
expect(old.startReal).toHaveBeenCalledTimes(1);
const replacement = await play(wrapper);
expect(instances).toHaveLength(2);
expect(replacement.startReal).toHaveBeenCalledWith(channels());
});
it('两个组件的容器 ID 不重叠,关闭其中一个不会销毁另一个', async () => {
const first = createPlayer();
const second = createPlayer();
const a = await play(first);
const b = await play(second);
expect(a.options.videoId).not.toEqual(b.options.videoId);
first.unmount();
expect(a.dispose).toHaveBeenCalledTimes(1);
expect(b.dispose).not.toHaveBeenCalled();
expect(document.getElementById(b.options.videoId)).not.toBeNull();
});
it('配置请求失败时播放 Promise 正常结束,之后可重新初始化', async () => {
mocks.getSetting.mockRejectedValueOnce(new Error('configuration unavailable'));
const wrapper = createPlayer();
await wrapper.vm.startReal(channels());
expect(instances).toHaveLength(0);
const player = await play(wrapper);
expect(player.startReal).toHaveBeenCalledTimes(1);
});
it('SDK 创建失败会清掉失败实例,允许重新打开', async () => {
const wrapper = createPlayer();
const task = wrapper.vm.startReal(channels());
await vi.advanceTimersByTimeAsync(0);
instances[0].options.createError({ code: 1001, message: 'not installed' });
await task;
expect(instances[0].dispose).toHaveBeenCalledTimes(1);
const replacement = await play(wrapper);
expect(instances).toHaveLength(2);
expect(replacement.startReal).toHaveBeenCalledTimes(1);
});
it('SDK 在构造函数内同步报告失败时,同样销毁失败实例并结束等待', async () => {
(window as any).VideoPlayer = class extends MockPlayer {
constructor(options: any) {
super(options);
options.createError({ code: 1002, message: 'unsupported system' });
}
};
const wrapper = createPlayer();
await wrapper.vm.startReal(channels());
expect(instances[0].dispose).toHaveBeenCalledTimes(1);
expect(instances[0].startReal).not.toHaveBeenCalled();
});
it('正常播放仍保留辅码流失败后只重试一次主码流的行为', async () => {
const wrapper = createPlayer();
const player = await play(wrapper);
player.options.realError(channels()[0], { code: 408 });
player.options.realError(channels()[0], { code: 408 });
expect(player.startReal).toHaveBeenCalledTimes(2);
expect(player.startReal).toHaveBeenLastCalledWith([{ ...channels()[0], streamType: 1 }]);
});
it('弹窗 active=false 会立即销毁,离场期间的晚到播放调用被忽略', async () => {
const wrapper = createPlayer();
const player = await play(wrapper);
await wrapper.setProps({ active: false });
expect(player.dispose).toHaveBeenCalledTimes(1);
await wrapper.vm.startReal(channels('late-response'));
expect(instances).toHaveLength(1);
await wrapper.setProps({ active: true });
const replacement = await play(wrapper);
expect(instances).toHaveLength(2);
expect(replacement.startReal).toHaveBeenCalledTimes(1);
});
it('销毁后晚到的错误回调不会触发主码流重试', async () => {
const wrapper = createPlayer();
const player = await play(wrapper);
wrapper.vm.destroy();
player.options.realError(channels()[0], { code: 408 });
expect(player.startReal).toHaveBeenCalledTimes(1);
});
it('KeepAlive 停用时销毁窗口,重新激活时只重建当前播放', async () => {
const visible = ref(true);
const host = mount(defineComponent({ render: () => h(KeepAlive, {}, () => visible.value ? h(VideoPlugin) : null) }), mountOptions);
document.body.appendChild(host.element);
wrappers.push(host);
const child = host.findComponent(VideoPlugin);
const oldPlayer = await play(child);
visible.value = false;
await nextTick();
expect(oldPlayer.dispose).toHaveBeenCalledTimes(1);
await child.vm.startReal(channels('inactive'));
expect(instances).toHaveLength(1);
visible.value = true;
await nextTick();
await vi.advanceTimersByTimeAsync(0);
await ready(instances[1]);
expect(instances[1].startReal).toHaveBeenCalledWith(channels());
});
it('当前预览已手动关闭时,重新激活不能误恢复此前缓存的回放', async () => {
const visible = ref(true);
const host = mount(defineComponent({ render: () => h(KeepAlive, {}, () => visible.value ? h(VideoPlugin) : null) }), mountOptions);
document.body.appendChild(host.element);
wrappers.push(host);
const child = host.findComponent(VideoPlugin);
const real = await play(child);
await play(child, 'playback');
await child.vm.startReal(channels());
real.setting.channelList = [];
real.options.closeWindowSuccess({ isAll: true, channelList: [] });
visible.value = false;
await nextTick();
visible.value = true;
await nextTick();
await vi.advanceTimersByTimeAsync(0);
expect(instances).toHaveLength(2);
});
});