| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269 |
- /**
- * T47 设备状态动效走查(票 47;决策票 07 问题 7 的**第 2、3 层证据**)。
- *
- * 三层证据缺一不可(第 1 层是 `t47-device-effects.mjs` 的纯逻辑):
- * 2. **场景图跨帧快照**(关键层):对同一设备隔帧读两次,断言
- * 报警/预警的 `scale`/`alpha` 与工单的 `rotation` **确实变了**,
- * 而正常/离线**完全不变**。这才是"真在跑"而不是"已配置"的证据 ——
- * 正面避开"拿两份 JS 数据互比"的恒真断言。
- * 3. **真实帧差**:同一视角隔一小段时间截两帧,断言**字节不同**(画面确实在变)。
- * 4. 人工走查:观感由维护者目视裁定(**未获人工确认不关闭本票**)。
- *
- * 场景走 URL 通道 `?scenario=device-effects`(受控可上图样例;真实环境仍是清单-only,
- * 决策票 07 明写"不得从纯逻辑断言推断现场特效已生效")。
- */
- import { cdpLaunch } from './cdp-driver.mjs'
- const BASE = process.env.COCKPIT_BASE || 'http://localhost:8000'
- const CHROME = process.env.CHROME_PATH || 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'
- const SCENARIO = process.env.REPRO_SCENARIO || 'device-effects'
- const ENTRY_URL = process.env.COCKPIT_URL || `${BASE}/?scenario=${SCENARIO}#/cockpit`
- const results = []
- function check(id, pass, evidence) {
- results.push({ id, pass })
- console.log(`${pass ? 'PASS' : 'FAIL'} ${id} :: ${evidence === undefined ? '' : String(evidence)}`)
- }
- const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
- const browser = await cdpLaunch({ executablePath: CHROME, width: 1680, height: 950 })
- console.log(`Chrome: ${await browser.version()}`)
- const page = await browser.newPage()
- const errors = []
- page.on('pageerror', (e) => errors.push(String(e && e.message ? e.message : e)))
- page.on('console', (m) => { if (m.type() === 'error') errors.push(`console.error: ${m.text()}`) })
- await page.goto(ENTRY_URL, { timeout: 60000 })
- await page.waitForSelector('.cockpit-shell', { timeout: 20000 })
- if (await page.$('[data-cockpit-intro]')) {
- await page.click('[data-cockpit-intro]')
- await page.waitForSelector('[data-cockpit-intro]', { hidden: true, timeout: 10000 }).catch(() => {})
- }
- await page.waitForFunction(
- () => ['ready', 'failed', 'disabled'].includes(document.querySelector('.cockpit-shell__map .map-canvas')?.dataset.mapEngine),
- { timeout: 40000 },
- ).catch(() => {})
- await page.evaluate(() => {
- const dev = window.__cockpitMap
- if (!dev || !dev.mapSummary) return
- let steps = 0
- const step = () => {
- const state = dev.mapSummary.getState()
- if (!state || ['complete', 'empty', 'failed', 'expired'].includes(state.phase) || steps >= 20) return
- dev.mapSummary.advance(); steps += 1; window.setTimeout(step, 220)
- }
- step()
- })
- await page.waitForFunction(() => {
- const dev = window.__cockpitMap
- const s = dev && dev.mapSummary ? dev.mapSummary.getState() : null
- return !s || ['complete', 'empty', 'failed', 'expired'].includes(s.phase)
- }, { timeout: 20000 }).catch(() => {})
- await page.waitForFunction(() => {
- const dev = window.__cockpitMap
- const s = dev && dev.getCameraState ? dev.getCameraState() : null
- return !s || s.settled === true
- }, { timeout: 15000 }).catch(() => {})
- await sleep(1500)
- const locate = () => page.evaluate(() => {
- const dev = window.__cockpitMap
- const all = dev && dev.getPointMarkerTargets ? dev.getPointMarkerTargets() : []
- return all.find((item) => item.visible && item.code === 'MP-001') || null
- })
- const readDetail = () => page.evaluate(() => ({
- level: (document.querySelector('[data-point-detail]') || { dataset: {} }).dataset.detailLevel || '',
- pointCode: (document.querySelector('[data-point-detail]') || { dataset: {} }).dataset.pointCode || '',
- }))
- const OFFSETS = [[0, 0], [0, -6], [0, 6], [-6, 0], [6, 0], [0, -12], [0, 12], [-10, -6], [10, 6]]
- // 真实鼠标点开 MP-001(贴地形标记会抖几像素,做小网格扫描点击)。
- for (let round = 0; round < 4; round += 1) {
- const now = await readDetail()
- if (now.level === 'point' && now.pointCode === 'MP-001') break
- const marker = await locate()
- if (marker) {
- for (const [dx, dy] of OFFSETS) {
- await page.mouse.move(1200, 300)
- await sleep(60)
- await page.mouse.move(marker.x + dx, marker.y + dy)
- await sleep(130)
- await page.mouse.click(marker.x + dx, marker.y + dy)
- await sleep(700)
- const after = await readDetail()
- if (after.level === 'point' && after.pointCode === 'MP-001') break
- }
- }
- await sleep(300)
- }
- const opened = await readDetail()
- check('E1-controlled-sample-opens',
- opened.level === 'point' && opened.pointCode === 'MP-001',
- `真实点开受控样例点位:level=${opened.level} pointCode=${opened.pointCode}`)
- const readEffects = () => page.evaluate(() => {
- const host = document.querySelector('.cockpit-shell__map .map-canvas')
- const dev = window.__cockpitMap
- const state = dev && dev.getDeviceEffects ? dev.getDeviceEffects() : null
- return {
- host: host ? {
- effects: host.dataset.mapDeviceEffects,
- abnormal: host.dataset.mapDeviceEffectsAbnormal,
- animated: host.dataset.mapDeviceEffectsAnimated,
- } : {},
- enabled: state ? state.enabled : null,
- degraded: state ? state.degraded : null,
- running: state ? state.running : null,
- tick: state ? state.tick : -1,
- ringLayers: state ? state.ringLayers : 0,
- devices: state && state.devices ? state.devices : [],
- fx: state && state.fx ? state.fx : [],
- }
- })
- // 多帧采样:闪烁(周期 900ms)等动效在两次采样间可能正好落在同一段,
- // 所以按 ~140ms 采 8 帧,用"取值集合的势"判定 —— 比两点比较更稳健,判据也更强
- // (正常/离线要求**全程只有一个取值**)。
- const SAMPLES = []
- for (let index = 0; index < 8; index += 1) {
- SAMPLES.push(await readEffects())
- await sleep(140)
- }
- const frameA = SAMPLES[0]
- const frameB = SAMPLES[SAMPLES.length - 1]
- const valuesOf = (code, key) => SAMPLES
- .map((frame) => (frame.devices || []).find((item) => item.code === code))
- .map((item) => (item && Number.isFinite(item[key]) ? Number(item[key].toFixed(5)) : null))
- .filter((value) => value !== null)
- const distinct = (code, key) => new Set(valuesOf(code, key)).size
- const alarmA = (frameA.devices || []).find((item) => item.code === 'EQ-EF-ALARM')
- const warnA = (frameA.devices || []).find((item) => item.code === 'EQ-EF-WARNING')
- const workA = (frameA.devices || []).find((item) => item.code === 'EQ-EF-WORK')
- const normalA = (frameA.devices || []).find((item) => item.code === 'EQ-EF-NORMAL')
- const offlineA = (frameA.devices || []).find((item) => item.code === 'EQ-EF-OFFLINE')
- console.log(`SAMPLES: ${SAMPLES.length} 帧;tick ${frameA.tick}→${frameB.tick};host=${JSON.stringify(frameA.host)}`)
- console.log(`ALARM scale 取值 ${valuesOf('EQ-EF-ALARM', 'scale').join(',')}`)
- console.log(`WARNING alpha 取值 ${valuesOf('EQ-EF-WARNING', 'alpha').join(',')}`)
- console.log(`WORK rotation 取值 ${valuesOf('EQ-EF-WORK', 'rotation').join(',')}`)
- console.log(`NORMAL scale/alpha 取值 ${valuesOf('EQ-EF-NORMAL', 'scale').join(',')} / ${valuesOf('EQ-EF-NORMAL', 'alpha').join(',')}`)
- check('E2-effects-enabled-and-running',
- SCENARIO === 'device-effects-degraded'
- ? (frameA.enabled === true && frameA.host.effects === 'degraded' && frameA.host.animated === '0' && frameA.running === false)
- : (frameA.enabled === true && frameA.running === true && frameA.host.effects === 'on' && frameB.tick > frameA.tick),
- SCENARIO === 'device-effects-degraded'
- ? `降级场景:enabled=${frameA.enabled} host=${frameA.host.effects} 动效台数=${frameA.host.animated} 异常台数=${frameA.host.abnormal}`
- : `enabled=${frameA.enabled} running=${frameA.running} host=${frameA.host.effects} 动效台数=${frameA.host.animated} 异常台数=${frameA.host.abnormal}(tick ${frameA.tick}→${frameB.tick})`)
- if (SCENARIO === 'device-effects') {
- const fxSeries = (code, key) => SAMPLES.map((frame) =>
- (frame.fx || []).find((item) => item.for === code && item.kind === 'ring' && item.layer === 0)?.[key])
- const fxDistinct = (code, key) => new Set(fxSeries(code, key).filter(Number.isFinite).map((value) => value.toFixed(5))).size
- check('E3-alarm-pulses-across-frames', fxDistinct('EQ-EF-ALARM', 'scale') >= 2,
- '报警外围环跨帧扩张,中心符号保持清晰')
- check('E4-warning-blinks-across-frames', fxDistinct('EQ-EF-WARNING', 'alpha') >= 2,
- '预警外围环闪烁,中心不参与透明度动画')
- check('E5-work-order-spins-across-frames', fxDistinct('EQ-EF-WORK', 'rotation') >= 2,
- '工单外围环旋转,中心状态形状不转动')
- check('E17-center-icons-remain-opaque',
- ['EQ-EF-ALARM', 'EQ-EF-WARNING', 'EQ-EF-WORK'].every((code) =>
- valuesOf(code, 'alpha').length === SAMPLES.length && valuesOf(code, 'alpha').every((value) => value === 1)),
- '三台动态状态设备的中心图标在全部采样帧中保持 alpha=1')
- check('E6-normal-stays-static',
- distinct('EQ-EF-NORMAL', 'scale') === 1 && distinct('EQ-EF-NORMAL', 'alpha') === 1 && distinct('EQ-EF-NORMAL', 'rotation') === 1,
- `正常设备**全程静止**:scale ${normalA && normalA.scale}、alpha ${normalA && normalA.alpha}、rotation ${normalA && normalA.rotation}`)
- check('E7-offline-stays-static',
- distinct('EQ-EF-OFFLINE', 'scale') === 1 && distinct('EQ-EF-OFFLINE', 'alpha') === 1 && distinct('EQ-EF-OFFLINE', 'rotation') === 1,
- `离线设备**全程静止**:scale ${offlineA && offlineA.scale}、alpha ${offlineA && offlineA.alpha}、rotation ${offlineA && offlineA.rotation}`)
- // ── 科技感光环(决策票 07 新增方向):多层同心扩散环 + 中心辉光 ──
- const fxValues = (code, kind, key) => SAMPLES
- .flatMap((frame) => (frame.fx || []).filter((item) => item.for === code && item.kind === kind))
- .map((item) => (Number.isFinite(item[key]) ? Number(item[key].toFixed(5)) : null))
- .filter((value) => value !== null)
- const ringLayersOf = (code) => new Set((SAMPLES[0].fx || [])
- .filter((item) => item.for === code && item.kind === 'ring')
- .map((item) => item.layer)).size
- check('E13-alarm-has-multi-layer-rings',
- frameA.ringLayers >= 3 && ringLayersOf('EQ-EF-ALARM') === frameA.ringLayers,
- `报警设备有 ${ringLayersOf('EQ-EF-ALARM')} 层同心环(期望 ${frameA.ringLayers} 层)`)
- check('E14-rings-expand-and-fade-across-frames',
- new Set(fxValues('EQ-EF-ALARM', 'ring', 'scale')).size >= 3 &&
- new Set(fxValues('EQ-EF-ALARM', 'ring', 'alpha')).size >= 3,
- `环跨帧扩张渐隐:scale 取值数 ${new Set(fxValues('EQ-EF-ALARM', 'ring', 'scale')).size}、alpha 取值数 ${new Set(fxValues('EQ-EF-ALARM', 'ring', 'alpha')).size}`)
- check('E15-glow-only-for-alarm',
- fxValues('EQ-EF-ALARM', 'glow', 'alpha').length > 0 &&
- new Set(fxValues('EQ-EF-ALARM', 'glow', 'alpha')).size >= 2 &&
- fxValues('EQ-EF-WARNING', 'glow', 'alpha').length === 0 &&
- fxValues('EQ-EF-NORMAL', 'ring', 'scale').length === 0,
- `中心辉光只给报警(报警辉光取值数 ${new Set(fxValues('EQ-EF-ALARM', 'glow', 'alpha')).size};预警/正常无辉光/无环)`)
- check('E16-static-devices-have-no-rings',
- ringLayersOf('EQ-EF-NORMAL') === 0 && ringLayersOf('EQ-EF-OFFLINE') === 0,
- `正常/离线**没有任何光环**(决策票 07:静态状态不参与动效)`)
- }
- // ── 第 3 层:真实帧差(同一视角两帧,字节不同 ⇒ 画面确实在变)──
- if (SCENARIO === 'device-effects') {
- const shotA = await page.screenshot()
- await sleep(500)
- const shotB = await page.screenshot()
- check('E8-real-frame-diff-changes',
- Boolean(shotA) && Boolean(shotB) && shotA !== shotB,
- `两帧截图字节${shotA === shotB ? '相同(画面没变 → 不合格)' : '不同'}(长度 ${shotA.length} vs ${shotB.length})`)
- }
- // ── 降级场景(>30 台异常)另有判据:动画全停 + 异常计数可见 ──
- if (SCENARIO === 'device-effects-degraded') {
- const degradedNote = await page.evaluate(() => {
- const note = document.querySelector('[data-device-effects-degraded]')
- return note ? { text: (note.textContent || '').trim(), abnormal: note.getAttribute('data-device-effects-abnormal') } : null
- })
- const allStatic = SAMPLES.every((frame) => (frame.devices || []).every((item) => !item.animated))
- check('E10-degraded-stops-animation',
- frameA.host.effects === 'degraded' && frameA.enabled === true && frameA.degraded === true &&
- frameA.host.animated === '0' && frameA.running === false && allStatic,
- `降级:host=${frameA.host.effects} running=${frameA.running} 动效台数=${frameA.host.animated}(${SAMPLES.length} 帧全部无动效=${allStatic})`)
- check('E11-degraded-count-is-visible',
- Boolean(degradedNote) && degradedNote.abnormal === '31' && frameA.host.abnormal === '31',
- `异常计数可见:面板「${degradedNote ? degradedNote.text : '(缺失)'}」、data-abnormal=${degradedNote && degradedNote.abnormal}`)
- check('E12-degraded-keeps-devices-drawn',
- frameA.devices.length === 31,
- `降级后**仍画 31 台静态异常标记**(不静默删点):实际 ${frameA.devices.length} 台`)
- }
- // 不改变面板 pointer-events,以真实卡片点击核验设备详情换锚。
- if (SCENARIO === 'device-effects') {
- const panelState = () => page.evaluate(() => {
- const panel = document.querySelector('.cockpit-shell__detail')
- const box = panel?.getBoundingClientRect()
- return { code: panel?.dataset.anchorCode, state: panel?.dataset.anchorState,
- lead: Boolean(document.querySelector('[data-anchor-lead]')),
- box: box ? { left: box.left, right: box.right, top: box.top, bottom: box.bottom } : null }
- })
- for (const code of ['EQ-EF-ALARM', 'EQ-EF-WARNING']) {
- await page.click(`[data-device-layer-code="${code}"]`)
- await page.waitForFunction((wanted) => document.querySelector('.cockpit-shell__detail')?.dataset.anchorCode === wanted,
- { timeout: 10000 }, code)
- await sleep(300)
- const state = await panelState()
- check(`E18-device-anchor-${code}`, state.code === code && state.state === 'visible' && state.lead,
- `设备详情锚点=${state.code}、面板=${state.state}、连接线=${state.lead}`)
- await page.click('[data-detail-back]')
- await page.waitForFunction(() => document.querySelector('.cockpit-shell__detail')?.dataset.anchorCode === 'MP-001',
- { timeout: 10000 })
- check(`E19-return-anchor-${code}`, (await panelState()).code === 'MP-001', '返回摘要恢复监测点锚点')
- }
- }
- check('E9-no-page-errors', errors.length === 0, errors.length ? errors.slice(0, 3).join(' | ') : '无 pageerror / console.error')
- await page.close()
- await browser.close()
- const failed = results.filter((r) => !r.pass)
- console.log('')
- console.log(`T47 动效走查(${SCENARIO}):${results.length - failed.length}/${results.length} 项通过。`)
- if (failed.length) { for (const item of failed) console.log(` · ${item.id}`); process.exit(1) }
- console.log('T47 动效走查全部通过。')
|