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

352 lines
14 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { mount, type VueWrapper } from '@vue/test-utils'
import { nextTick } 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() } }))
type Mode = 'real' | 'playback'
const modes: Mode[] = ['real', 'playback']
const connectionError = '未能连接本机视频服务,请启动或重新启动视频插件后重试'
const instances: MockPlayer[] = []
let constructorError: { code: number; message: string } | null = null
class MockPlayer {
setting: any
closeVideo = vi.fn(() => Promise.resolve())
dispose = vi.fn()
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)
if (constructorError) options.createError(constructorError)
}
manualClose(all: boolean) {
this.setting.channelList = all ? [] : this.setting.channelList.slice(1)
this.options.closeWindowSuccess({ isAll: all, snum: 0, channelList: this.setting.channelList })
}
}
function deferred<T = void>() {
let resolve!: (value: T) => void
const promise = new Promise<T>((done) => { resolve = done })
return { promise, resolve }
}
const wrappers: VueWrapper[] = []
function createPlayer() {
const wrapper = mount(VideoPlugin, {
global: { stubs: { Yt: true, 'el-pagination': true, 'el-icon': true }, directives: { loading: () => undefined } },
})
document.body.appendChild(wrapper.element)
wrappers.push(wrapper)
return wrapper
}
function requests(mode: Mode, names = ['camera-a']) {
return names.map((channelId, snum) => ({
channelId, snum, streamType: mode === 'real' ? 2 : 0,
...(mode === 'playback' ? { startTime: '2026-11-04 08:00:00', endTime: '2026-11-04 09:00:00', recordSource: 2 } : {}),
}))
}
function start(wrapper: VueWrapper, mode: Mode, items = requests(mode)) {
return mode === 'real' ? wrapper.vm.startReal(items) : wrapper.vm.startPlayback(items)
}
function startSpy(player: MockPlayer, mode: Mode) {
return mode === 'real' ? player.startReal : player.startPlayback
}
async function ready(player: MockPlayer) {
player.options.createSuccess({})
await vi.advanceTimersByTimeAsync(500)
}
async function play(wrapper: VueWrapper, mode: Mode, items = requests(mode)) {
const task = start(wrapper, mode, items)
await vi.advanceTimersByTimeAsync(0)
const player = instances[instances.length - 1]
await ready(player)
await task
expect(startSpy(player, mode)).toHaveBeenCalledTimes(1)
return player
}
async function failInitial(wrapper: VueWrapper, mode: Mode, code = 1001) {
const task = start(wrapper, mode)
await vi.advanceTimersByTimeAsync(0)
const player = instances[instances.length - 1]
player.options.createError({ code, message: '插件未安装' })
await task
await nextTick()
expect(player.dispose).toHaveBeenCalledTimes(1)
return player
}
function retryButton(wrapper: VueWrapper) {
const button = wrapper.findAll('button').find((item) => item.text() === '重新连接')
expect(button, '连接失败时应提供重新连接按钮').toBeDefined()
return button!
}
function setHidden(hidden: boolean) {
Object.defineProperty(document, 'hidden', { configurable: true, value: hidden })
document.dispatchEvent(new Event('visibilitychange'))
}
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
constructorError = null
mocks.getSetting.mockReset().mockResolvedValue({ code: 0, data: { dhIccSetting: {} } })
})
afterEach(() => {
wrappers.splice(0).forEach((wrapper) => { if (wrapper.exists()) wrapper.unmount() })
vi.clearAllTimers()
vi.useRealTimers()
vi.restoreAllMocks()
document.body.innerHTML = ''
})
describe.each(modes)('%s 视频连接恢复', (mode) => {
it.each([1001, 1003])('终态错误 %s 显示连接故障,点击重试恢复当前请求', async (code) => {
const wrapper = createPlayer()
await failInitial(wrapper, mode, code)
expect(wrapper.text()).toContain(connectionError)
expect(wrapper.text()).not.toContain('插件未安装')
await retryButton(wrapper).trigger('click')
await vi.advanceTimersByTimeAsync(0)
expect(instances).toHaveLength(2)
await ready(instances[1])
expect(startSpy(instances[1], mode)).toHaveBeenCalledTimes(1)
expect(startSpy(instances[1], mode)).toHaveBeenCalledWith(requests(mode))
expect(wrapper.text()).not.toContain(connectionError)
})
it('连接中状态不是终态,不销毁实例或显示未安装提示', async () => {
const wrapper = createPlayer()
const task = start(wrapper, mode)
await vi.advanceTimersByTimeAsync(0)
const player = instances[0]
player.options.connectionStateChange({ state: 'connecting' })
await nextTick()
expect(player.dispose).not.toHaveBeenCalled()
expect(wrapper.text()).not.toContain('插件未安装')
await ready(player)
await task
expect(startSpy(player, mode)).toHaveBeenCalledTimes(1)
})
it('运行中的连接恢复复用实例,并绕过800ms重复播放保护重新开流', async () => {
const wrapper = createPlayer()
const player = await play(wrapper, mode)
const disconnectedAt = Date.now()
player.options.connectionStateChange({ state: 'reconnecting' })
expect(player.dispose).not.toHaveBeenCalled()
await ready(player)
expect(Date.now() - disconnectedAt).toBeLessThan(800)
expect(instances).toHaveLength(1)
expect(startSpy(player, mode)).toHaveBeenCalledTimes(2)
expect(startSpy(player, mode)).toHaveBeenLastCalledWith(requests(mode))
})
it.each([false, true])('重连时不会重新播放手动关闭的通道(全部关闭:%s)', async (all) => {
const wrapper = createPlayer()
const items = requests(mode, ['camera-a', 'camera-b'])
const player = await play(wrapper, mode, items)
player.manualClose(all)
player.options.connectionStateChange({ state: 'reconnecting' })
await ready(player)
expect(player.dispose).not.toHaveBeenCalled()
expect(startSpy(player, mode)).toHaveBeenCalledTimes(all ? 1 : 2)
if (!all) expect(startSpy(player, mode)).toHaveBeenLastCalledWith([items[1]])
})
it('后台重连成功时先不播放,回到前台才恢复有效请求', async () => {
const wrapper = createPlayer()
const player = await play(wrapper, mode)
setHidden(true)
await vi.advanceTimersByTimeAsync(0)
player.options.connectionStateChange({ state: 'reconnecting' })
await ready(player)
expect(startSpy(player, mode)).toHaveBeenCalledTimes(1)
expect(player.setting.visible).toBe(false)
setHidden(false)
await vi.advanceTimersByTimeAsync(500)
expect(startSpy(player, mode)).toHaveBeenCalledTimes(2)
expect(startSpy(player, mode)).toHaveBeenLastCalledWith(requests(mode))
expect(player.setting.visible).toBe(true)
})
})
describe('重试和页面关闭的竞态', () => {
it.each(modes)('切换模式失败后返回正常缓存的 %s 模式会清除错误遮罩和重试按钮', async (mode) => {
const wrapper = createPlayer()
const cached = await play(wrapper, mode)
const otherMode = mode === 'real' ? 'playback' : 'real'
await failInitial(wrapper, otherMode)
expect(wrapper.text()).toContain(connectionError)
retryButton(wrapper)
await start(wrapper, mode)
await nextTick()
expect(instances).toHaveLength(2)
expect(cached.setting.visible).toBe(true)
expect(wrapper.text()).not.toContain(connectionError)
expect(wrapper.findAll('button').some((button) => button.text() === '重新连接')).toBe(false)
expect(wrapper.find('.loading-mask').exists()).toBe(false)
// 配置查询失败也不能把错误状态带回仍可用的缓存播放器。
mocks.getSetting.mockResolvedValueOnce({ code: 1, data: null })
await start(wrapper, otherMode)
await nextTick()
expect(wrapper.text()).toContain('配置')
retryButton(wrapper)
await start(wrapper, mode)
await nextTick()
expect(instances).toHaveLength(2)
expect(cached.setting.visible).toBe(true)
expect(wrapper.text()).not.toContain('配置')
expect(wrapper.findAll('button').some((button) => button.text() === '重新连接')).toBe(false)
expect(wrapper.find('.loading-mask').exists()).toBe(false)
})
it('关闭命令一直不回包时,重连会结束关闭等待供父组件继续执行', async () => {
const wrapper = createPlayer()
const player = await play(wrapper, 'real')
const noResponse = deferred()
player.closeVideo.mockReturnValueOnce(noResponse.promise)
let closed = false
const closing = wrapper.vm.closeVideo().then(() => { closed = true })
await vi.advanceTimersByTimeAsync(0)
expect(closed).toBe(false)
player.options.connectionStateChange({ state: 'reconnecting' })
await vi.advanceTimersByTimeAsync(0)
expect(closed).toBe(true)
await closing
await ready(player)
expect(player.startReal).toHaveBeenCalledTimes(1)
})
it('快速重复点击重试只查询一次配置并创建一个替换实例', async () => {
const wrapper = createPlayer()
await failInitial(wrapper, 'real')
const pending = deferred<any>()
mocks.getSetting.mockReturnValueOnce(pending.promise)
const button = retryButton(wrapper).element
button.dispatchEvent(new MouseEvent('click', { bubbles: true }))
button.dispatchEvent(new MouseEvent('click', { bubbles: true }))
await nextTick()
button.dispatchEvent(new MouseEvent('click', { bubbles: true }))
expect(mocks.getSetting).toHaveBeenCalledTimes(2)
expect(instances).toHaveLength(1)
pending.resolve({ code: 0, data: { dhIccSetting: {} } })
await vi.advanceTimersByTimeAsync(0)
expect(instances).toHaveLength(2)
await ready(instances[1])
expect(instances[1].startReal).toHaveBeenCalledTimes(1)
})
it('非当前缓存模式重连成功不抢占当前回放', async () => {
const wrapper = createPlayer()
const preview = await play(wrapper, 'real')
const replay = await play(wrapper, 'playback')
preview.options.connectionStateChange({ state: 'reconnecting' })
await ready(preview)
expect(preview.dispose).not.toHaveBeenCalled()
expect(preview.startReal).toHaveBeenCalledTimes(1)
expect(preview.setting.visible).toBe(false)
expect(replay.startPlayback).toHaveBeenCalledTimes(1)
expect(replay.setting.visible).toBe(true)
})
it('重连过程中切换模式,只恢复最新回放请求', async () => {
const wrapper = createPlayer()
const preview = await play(wrapper, 'real')
preview.options.connectionStateChange({ state: 'reconnecting' })
const task = start(wrapper, 'playback')
await vi.advanceTimersByTimeAsync(0)
const replay = instances[1]
await ready(replay)
await task
await ready(preview)
expect(preview.startReal).toHaveBeenCalledTimes(1)
expect(replay.startPlayback).toHaveBeenCalledTimes(1)
expect(replay.startPlayback).toHaveBeenCalledWith(requests('playback'))
})
it('重连取消旧关闭等待,旧回包不能清空已恢复的新通道', async () => {
const wrapper = createPlayer()
const player = await play(wrapper, 'real')
const close = deferred()
player.closeVideo.mockReturnValueOnce(close.promise)
const closing = wrapper.vm.closeVideo()
let waitingSettled = false
const items = requests('real', ['new-camera'])
const waiting = start(wrapper, 'real', items).then(() => { waitingSettled = true })
await vi.advanceTimersByTimeAsync(0)
expect(player.startReal).toHaveBeenCalledTimes(1)
player.options.connectionStateChange({ state: 'reconnecting' })
await vi.advanceTimersByTimeAsync(0)
expect(waitingSettled).toBe(true)
await ready(player)
expect(player.startReal).toHaveBeenCalledTimes(2)
expect(player.startReal).toHaveBeenLastCalledWith(items)
close.resolve()
await closing
await waiting
expect(player.setting.channelList).toEqual(items)
})
it.each(['close', 'unmount', 'inactive'])('手动重试后%s,晚到就绪不能重新播放', async (action) => {
const wrapper = createPlayer()
const failed = await failInitial(wrapper, 'real')
await retryButton(wrapper).trigger('click')
await vi.advanceTimersByTimeAsync(0)
const replacement = instances[1]
expect(replacement).toBeDefined()
if (action === 'close') await wrapper.vm.closeVideo()
if (action === 'unmount') wrapper.unmount()
if (action === 'inactive') await wrapper.setProps({ active: false })
await ready(failed)
await ready(replacement)
expect(failed.startReal).not.toHaveBeenCalled()
expect(replacement.startReal).not.toHaveBeenCalled()
expect(instances).toHaveLength(2)
})
it('重试配置仍在加载时关闭,不再构造替换播放器', async () => {
const wrapper = createPlayer()
await failInitial(wrapper, 'real')
const config = deferred<any>()
mocks.getSetting.mockReturnValueOnce(config.promise)
await retryButton(wrapper).trigger('click')
wrapper.unmount()
config.resolve({ code: 0, data: { dhIccSetting: {} } })
await vi.advanceTimersByTimeAsync(1000)
expect(instances).toHaveLength(1)
})
it('SDK构造期间同步报错也会清理实例,后续手动重试可成功', async () => {
constructorError = { code: 1001, message: '插件未安装' }
const wrapper = createPlayer()
let settled = false
const task = start(wrapper, 'real').then(() => { settled = true })
await vi.advanceTimersByTimeAsync(0)
expect(settled).toBe(true)
expect(instances[0].dispose).toHaveBeenCalledTimes(1)
expect(wrapper.text()).toContain(connectionError)
constructorError = null
await retryButton(wrapper).trigger('click')
await vi.advanceTimersByTimeAsync(0)
expect(instances).toHaveLength(2)
await ready(instances[1])
await task
expect(instances[1].startReal).toHaveBeenCalledTimes(1)
})
})