feat: 重建 Sense 单路实时监看 (#68)
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import SenseLiveview from '@/views/sense/liveview/index.vue'
|
||||
import { createLiveviewSession, getLiveviewSession, listLiveviewRoutes } from '@/api/sense/liveview'
|
||||
|
||||
jest.mock('@/api/sense/liveview', () => ({
|
||||
listLiveviewRoutes: jest.fn(),
|
||||
createLiveviewSession: jest.fn(),
|
||||
getLiveviewSession: jest.fn()
|
||||
}))
|
||||
|
||||
describe('Sense live-view page lifecycle', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers()
|
||||
listLiveviewRoutes.mockResolvedValue({ data: { list: [], count: 0 }})
|
||||
createLiveviewSession.mockResolvedValue({ data: { id: 'view_test', playerUrl: '/api/v1/liveview/player/view_test', status: 'waiting', detail: '等待播放器连接' }})
|
||||
getLiveviewSession.mockResolvedValue({ data: { id: 'view_test', playerUrl: '/api/v1/liveview/player/view_test', status: 'ready', detail: '上游拉流正常' }})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
jest.useRealTimers()
|
||||
})
|
||||
|
||||
it('opens only the selected route and clears polling when unmounted', async() => {
|
||||
const context = {
|
||||
...SenseLiveview.data(),
|
||||
currentRoute: { id: 'device-1:main', deviceName: '东门摄像机' },
|
||||
playerOpen: true
|
||||
}
|
||||
context.stopTimers = SenseLiveview.methods.stopTimers.bind(context)
|
||||
context.refreshSession = SenseLiveview.methods.refreshSession.bind(context)
|
||||
context.startPolling = SenseLiveview.methods.startPolling.bind(context)
|
||||
await SenseLiveview.methods.openSession.call(context)
|
||||
|
||||
expect(createLiveviewSession).toHaveBeenCalledWith('device-1:main')
|
||||
expect(context.session.playerUrl).toBe('/api/v1/liveview/player/view_test')
|
||||
expect(jest.getTimerCount()).toBe(2)
|
||||
|
||||
SenseLiveview.beforeUnmount.call(context)
|
||||
expect(jest.getTimerCount()).toBe(0)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,32 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import SenseVideoPlayer from '@/components/sense/video-player/index.vue'
|
||||
|
||||
const ElButton = { emits: ['click'], template: '<button @click="$emit(\'click\')"><slot /></button>' }
|
||||
const ElIcon = { template: '<span><slot /></span>' }
|
||||
|
||||
const mountPlayer = props => mount(SenseVideoPlayer, {
|
||||
props,
|
||||
global: { components: { ElButton, ElIcon }}
|
||||
})
|
||||
|
||||
describe('Sense live-view player lifecycle', () => {
|
||||
it('creates an iframe only for a selected playable session', () => {
|
||||
const empty = mountPlayer({ state: 'loading', playerUrl: '' })
|
||||
expect(empty.find('iframe').exists()).toBe(false)
|
||||
empty.unmount()
|
||||
|
||||
const waiting = mountPlayer({ state: 'waiting', playerUrl: '/api/v1/liveview/player/view_test' })
|
||||
expect(waiting.findAll('iframe')).toHaveLength(1)
|
||||
expect(waiting.find('iframe').attributes('src')).toBe('/api/v1/liveview/player/view_test')
|
||||
expect(waiting.attributes('aria-busy')).toBe('true')
|
||||
waiting.unmount()
|
||||
})
|
||||
|
||||
it('removes the iframe on timeout and offers an explicit retry', async() => {
|
||||
const wrapper = mountPlayer({ state: 'timeout', playerUrl: '/api/v1/liveview/player/view_test' })
|
||||
expect(wrapper.find('iframe').exists()).toBe(false)
|
||||
expect(wrapper.text()).toContain('连接超时')
|
||||
await wrapper.find('button').trigger('click')
|
||||
expect(wrapper.emitted('retry')).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
import { playbackStatusDetail, playbackStatusLabel, playbackStatusType, routeStatus } from '@/views/sense/liveview/playbackStatus'
|
||||
|
||||
describe('Sense live-view playback states', () => {
|
||||
it('keeps authentication, missing stream and timeout actionable', () => {
|
||||
expect(playbackStatusLabel('authentication_failed')).toBe('摄像头认证失败')
|
||||
expect(playbackStatusDetail('stream_not_found')).toContain('视频服务')
|
||||
expect(playbackStatusDetail('timeout')).toContain('20 秒')
|
||||
expect(playbackStatusType('offline')).toBe('danger')
|
||||
})
|
||||
|
||||
it('maps route readiness without relying on color alone', () => {
|
||||
expect(routeStatus('ready')).toEqual({ label: '可观看', type: 'success' })
|
||||
expect(routeStatus('waiting')).toEqual({ label: '等待观看', type: 'warning' })
|
||||
expect(routeStatus('apply_failed').label).toBe('需要处理')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user