Files
jh-military-admin/tests/video-plugin.lifecycle.test.ts
T
2026-10-27 15:06:03 +08:00

267 lines
10 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 = {
attachTo: document.body,
global: { stubs: { Yt: true, 'el-pagination': true, 'el-icon': true }, directives: { loading: () => undefined } },
};
function createPlayer(props = {}) {
const wrapper = mount(VideoPlugin, { ...mountOptions, props });
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) => wrapper.unmount());
vi.clearAllTimers();
vi.useRealTimers();
document.body.innerHTML = '';
});
describe('原生播放器组件的关闭时序', () => {
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('未 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('同时关闭多个缓存实例,单个 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('两个组件的容器 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('弹窗 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);
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());
});
});