|
@@ -94,7 +94,8 @@ async function mountRoot({ positioned = false, underground = false } = {}) {
|
|
|
longitude: positioned ? 110.4 : null, latitude: positioned ? 28.4 : null, coordinateValidation: location }
|
|
longitude: positioned ? 110.4 : null, latitude: positioned ? 28.4 : null, coordinateValidation: location }
|
|
|
const selectedCamera = { ...camera, longitude: selectedPoint.longitude, latitude: selectedPoint.latitude, coordinateValidation: location }
|
|
const selectedCamera = { ...camera, longitude: selectedPoint.longitude, latitude: selectedPoint.latitude, coordinateValidation: location }
|
|
|
const requests = [], unexpected = [], errors = []
|
|
const requests = [], unexpected = [], errors = []
|
|
|
- const server = { snapshotGate: null, pointGate: null, pointFails: false, cameraPresent: true, liveFailures: 0 }
|
|
|
|
|
|
|
+ const server = { snapshotGate: null, pointGate: null, pointFails: false, cameraPresent: true, liveFailures: 0,
|
|
|
|
|
+ recordings: [], recordsFail: false, playbackBusy: false }
|
|
|
const fetchImpl = async (address, options = {}) => {
|
|
const fetchImpl = async (address, options = {}) => {
|
|
|
const url = new URL(address), path = url.pathname.replace('/api/cockpit/v1', '')
|
|
const url = new URL(address), path = url.pathname.replace('/api/cockpit/v1', '')
|
|
|
const body = options.body ? JSON.parse(options.body) : null
|
|
const body = options.body ? JSON.parse(options.body) : null
|
|
@@ -131,6 +132,7 @@ async function mountRoot({ positioned = false, underground = false } = {}) {
|
|
|
if (path === '/video/device') { assert.equal(url.searchParams.get('deviceCode'), cameraCode); return envelope(device) }
|
|
if (path === '/video/device') { assert.equal(url.searchParams.get('deviceCode'), cameraCode); return envelope(device) }
|
|
|
if (path === '/video/records') {
|
|
if (path === '/video/records') {
|
|
|
assert.equal(url.searchParams.get('deviceCode'), cameraCode)
|
|
assert.equal(url.searchParams.get('deviceCode'), cameraCode)
|
|
|
|
|
+ if (server.recordsFail) return { ok: false, status: 502, json: async () => ({ code: 502, data: { category: 'VIDEO_RECORDS_FAILED', message: '录像查询失败,请稍后重试' } }) }
|
|
|
const rows = server.recordings ?? []
|
|
const rows = server.recordings ?? []
|
|
|
return envelope({ deviceCode: cameraCode, deviceName: device.deviceName,
|
|
return envelope({ deviceCode: cameraCode, deviceName: device.deviceName,
|
|
|
startTime: url.searchParams.get('startTime'), endTime: url.searchParams.get('endTime'),
|
|
startTime: url.searchParams.get('startTime'), endTime: url.searchParams.get('endTime'),
|
|
@@ -323,6 +325,85 @@ test('历史回放占用时提示占用且不释放他人回放', async () => {
|
|
|
} finally { await page.dispose() }
|
|
} finally { await page.dispose() }
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+test('历史回放控制未确认时全部灰色且带原因,不发任何控制请求', async () => {
|
|
|
|
|
+ const page = await mountRoot()
|
|
|
|
|
+ try {
|
|
|
|
|
+ page.server.recordings = [{ name: 'CAM/03', startTime: '2026-11-10 01:00:00', endTime: '2026-11-10 02:00:00', type: 'time' }]
|
|
|
|
|
+ const detail = await openMainDetail(page)
|
|
|
|
|
+ detail.querySelector('[data-video-open]').click(); await settle()
|
|
|
|
|
+ const dialog = () => document.querySelector('[data-video-monitor]')
|
|
|
|
|
+ const tabs = [...dialog().querySelectorAll('button')].filter((button) => ['实时视频', '历史录像'].includes(button.textContent.trim()))
|
|
|
|
|
+ tabs[1].click(); await settle()
|
|
|
|
|
+ const play = [...dialog().querySelectorAll('button')].find((button) => button.textContent.trim() === '播放')
|
|
|
|
|
+ play.click(); await settle()
|
|
|
|
|
+ // Unconfirmed controls must be present but grey, each with a reason, not silently absent.
|
|
|
|
|
+ const controls = [...dialog().querySelectorAll('[data-playback-control]')]
|
|
|
|
|
+ assert.ok(controls.length >= 6, 'pause/resume and 0.25/0.5/1/2/4x plus seek must all be rendered')
|
|
|
|
|
+ for (const control of controls) {
|
|
|
|
|
+ assert.equal(control.disabled, true, `${control.dataset.playbackControl} 未确认时必须灰色`)
|
|
|
|
|
+ assert.ok((control.getAttribute('title') || '').trim().length > 0, `${control.dataset.playbackControl} 必须说明为何不可用`)
|
|
|
|
|
+ }
|
|
|
|
|
+ // A grey control must never send a request even if activated programmatically.
|
|
|
|
|
+ for (const control of controls) control.click()
|
|
|
|
|
+ await settle()
|
|
|
|
|
+ assert.equal(page.requests.filter((request) => request.path.includes('/control')).length, 0,
|
|
|
|
|
+ '灰色控件不得发出任何控制请求')
|
|
|
|
|
+ assert.deepEqual(page.errors, [])
|
|
|
|
|
+ assert.deepEqual(page.unexpected, [])
|
|
|
|
|
+ } finally { await page.dispose() }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+test('历史页区分无录像、查询失败与非法时间范围三种结果', async () => {
|
|
|
|
|
+ const page = await mountRoot()
|
|
|
|
|
+ try {
|
|
|
|
|
+ const detail = await openMainDetail(page)
|
|
|
|
|
+ detail.querySelector('[data-video-open]').click(); await settle()
|
|
|
|
|
+ const dialog = () => document.querySelector('[data-video-monitor]')
|
|
|
|
|
+ const tabs = [...dialog().querySelectorAll('button')].filter((button) => ['实时视频', '历史录像'].includes(button.textContent.trim()))
|
|
|
|
|
+ page.server.recordings = []
|
|
|
|
|
+ tabs[1].click(); await settle()
|
|
|
|
|
+ // No recordings is a legitimate answer that must not look like a failure.
|
|
|
|
|
+ assert.ok(dialog().textContent.includes('没有录像'), dialog().textContent)
|
|
|
|
|
+ // A failed query is a different visible state and is never auto-retried.
|
|
|
|
|
+ page.server.recordsFail = true
|
|
|
|
|
+ const queryButton = [...dialog().querySelectorAll('button')].find((button) => button.textContent.trim() === '查询录像')
|
|
|
|
|
+ queryButton.click(); await settle()
|
|
|
|
|
+ assert.ok(dialog().textContent.includes('查询失败'), dialog().textContent)
|
|
|
|
|
+ const after = page.requests.filter((request) => request.path.endsWith('/records')).length
|
|
|
|
|
+ await new Promise((resolve) => setTimeout(resolve, 2500))
|
|
|
|
|
+ assert.equal(page.requests.filter((request) => request.path.endsWith('/records')).length, after,
|
|
|
|
|
+ '查询失败不得自动重试')
|
|
|
|
|
+ assert.deepEqual(page.errors, [])
|
|
|
|
|
+ assert.deepEqual(page.unexpected, [])
|
|
|
|
|
+ } finally { await page.dispose() }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+test('历史页非法时间范围在本地拒绝且不发查询请求', async () => {
|
|
|
|
|
+ const page = await mountRoot()
|
|
|
|
|
+ try {
|
|
|
|
|
+ const detail = await openMainDetail(page)
|
|
|
|
|
+ detail.querySelector('[data-video-open]').click(); await settle()
|
|
|
|
|
+ const dialog = () => document.querySelector('[data-video-monitor]')
|
|
|
|
|
+ const tabs = [...dialog().querySelectorAll('button')].filter((button) => ['实时视频', '历史录像'].includes(button.textContent.trim()))
|
|
|
|
|
+ page.server.recordings = []
|
|
|
|
|
+ tabs[1].click(); await settle()
|
|
|
|
|
+ const before = page.requests.filter((request) => request.path.endsWith('/records')).length
|
|
|
|
|
+ const inputs = [...dialog().querySelectorAll('.video-monitor__range input')]
|
|
|
|
|
+ assert.equal(inputs.length, 2)
|
|
|
|
|
+ // A 7-day+ span is refused before any request is made.
|
|
|
|
|
+ inputs[0].value = '2026-11-01 00:00:00'; inputs[0].dispatchEvent(new Event('input'))
|
|
|
|
|
+ inputs[1].value = '2026-11-10 00:00:00'; inputs[1].dispatchEvent(new Event('input'))
|
|
|
|
|
+ await settle()
|
|
|
|
|
+ const queryButton = [...dialog().querySelectorAll('button')].find((button) => button.textContent.trim() === '查询录像')
|
|
|
|
|
+ queryButton.click(); await settle()
|
|
|
|
|
+ assert.equal(page.requests.filter((request) => request.path.endsWith('/records')).length, before,
|
|
|
|
|
+ '超 7 天的范围不得发到服务端')
|
|
|
|
|
+ assert.ok(dialog().textContent.includes('7 天'), dialog().textContent)
|
|
|
|
|
+ assert.deepEqual(page.errors, [])
|
|
|
|
|
+ assert.deepEqual(page.unexpected, [])
|
|
|
|
|
+ } finally { await page.dispose() }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
test('已关闭的设备详情入口同轮迟到点击不得创建观看', async () => {
|
|
test('已关闭的设备详情入口同轮迟到点击不得创建观看', async () => {
|
|
|
const page = await mountRoot()
|
|
const page = await mountRoot()
|
|
|
try {
|
|
try {
|