| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312 |
- /**
- * T14 真机走查:事件详情与设备定位(票 14)。
- *
- * 逻辑脚本(t14-event-detail.mjs,42/42)已经把详情字段映射、定位计划与四种结果、
- * 高亮判定与还原、store 的打开/返回保上下文测透了;这里只补**只有真机才成立**的部分
- * (票 12 的教训:「能加载 ≠ 能渲染」):
- * A 点事件行 → 界面上**真的**画出事件详情,且 8 项字段都从 DOM 读得到;
- * B 点「定位关联设备」→ 相机真的动了,且**高亮的是已存在的点位标记**(点位数不增不减);
- * C 点「返回事件列表」→ 详情收起、**列表位置与族/筛选条件原样**(行码序列逐位相同);
- * D 无有效坐标的事件(样例 `event-detail-no-coordinate`):详情仍完整可看,
- * 界面**写出**无地图位置原因,且定位结果如实报"不可上图";
- * E 收起详情后**高亮还原**(宿主 `data-map-emphasis` 回到 0,不是留个胖标记)。
- *
- * 运行(需先起 `yarn dev`;**AGENTS.md 要求不自动跑构建/调试命令**,所以本脚本由
- * 维护者按需执行,票 13/18 已有同一先例):
- * node .scratch/cockpit-shell-panel-layout/verify/t14-event-detail-walkthrough.mjs
- * COCKPIT_BASE=http://localhost:8001 node ...(端口不是 8000 时)
- *
- * 演示开关(宿主全局键注入,不改代码重新构建):
- * `__COCKPIT_MOCK_SCENARIO__='success'`(A/B/C/E 组用)
- * `__COCKPIT_MOCK_SCENARIO__='event-detail-no-coordinate'`(D 组用;样例在
- * `mocks/contractMocks.js`,逻辑脚本 E4 已验证它真的把坐标判成不可上图)
- */
- import { join } from 'node:path'
- import { tmpdir } from 'node:os'
- const BASE = process.env.COCKPIT_BASE || 'http://localhost:8000'
- const ENTRY_URL = process.env.COCKPIT_URL || `${BASE}/#/cockpit`
- const CHROME = process.env.CHROME_PATH || 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'
- const PUPPETEER = 'file:///C:/Users/ASUS/.dsh/profiles/desktop/node_modules/puppeteer-core/lib/puppeteer/puppeteer-core.js'
- const results = []
- /** `check(id, pass, evidence)` —— 第二参是**断言结果**,不是描述。 */
- 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 puppeteer = (await import(PUPPETEER)).default
- const browser = await puppeteer.launch({
- executablePath: CHROME,
- headless: true,
- userDataDir: join(tmpdir(), 'cockpit-t14-profile'),
- defaultViewport: { width: 1680, height: 950 },
- args: ['--no-sandbox', '--disable-dev-shm-usage', '--no-first-run', '--no-default-browser-check'],
- })
- console.log(`Chrome: ${await browser.version()}`)
- /** 当前预警面板 + 详情卡的 DOM 状态(全部从界面读,证明真的画出来了)。 */
- const readState = (page) =>
- page.evaluate(() => {
- const panel = document.querySelector('[data-panel-id="currentEvents"]')
- const card = panel && panel.querySelector('[data-event-detail]')
- const dev = window.__cockpitMap
- const markers = dev && dev.getPointMarkers ? dev.getPointMarkers() : null
- const markerOnMap = markers && typeof markers.onMap === 'number' ? markers.onMap : null
- const text = (root, selector) => {
- const node = root && root.querySelector(selector)
- return node ? (node.textContent || '').trim() : ''
- }
- const rows = panel
- ? Array.from(panel.querySelectorAll('[data-event-row]')).map((tr) => tr.dataset.eventRow || '')
- : []
- return {
- mode: ((document.querySelector('.cockpit-shell__modes .is-active') || {}).textContent || '').trim(),
- panelRendered: !!panel,
- rows,
- detailOpen: !!card,
- detailCode: card ? card.dataset.eventDetailCode || '' : '',
- fields: card
- ? {
- name: text(card, '[data-event-detail-name]'),
- code: text(card, '[data-event-detail-code-value]'),
- device: text(card, '[data-event-detail-device]'),
- metric: text(card, '[data-event-detail-metric]'),
- threshold: text(card, '[data-event-detail-threshold]'),
- value: text(card, '[data-event-detail-value]'),
- level: text(card, '[data-event-detail-level]'),
- time: text(card, '[data-event-detail-time]'),
- status: text(card, '[data-event-detail-status]'),
- location: text(card, '[data-event-detail-location]'),
- }
- : null,
- locateResult: card ? ((card.querySelector('[data-event-detail-locate-result]') || {}).dataset || {}).eventDetailLocateResult || '' : '',
- locateText: text(card, '[data-event-detail-locate-result]'),
- locateButton: !!(card && card.querySelector('[data-event-detail-locate]')),
- camera: (() => {
- const camera = dev && dev.getCameraState ? dev.getCameraState() : null
- return camera && camera.hq ? camera.hq : camera && camera.raw ? camera.raw : null
- })(),
- markerCount: markerOnMap,
- /** 服务层写进宿主的强调计数(`data-map-emphasis`):走查据此断言"收起后真的还原"。 */
- emphasis: (() => {
- const host = document.querySelector('.cockpit-shell__map .map-canvas')
- const value = host ? host.dataset.mapEmphasis : ''
- return value === undefined || value === '' ? null : Number(value)
- })(),
- }
- })
- /** 打开页面 → 进预警处置模式 → 点第一条事件行 → 返回读状态。 */
- async function openDetailRow(scenario = 'success') {
- 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.evaluateOnNewDocument((name) => {
- window.__COCKPIT_MOCK_SCENARIO__ = name
- }, scenario)
- await page.goto(ENTRY_URL, { waitUntil: 'networkidle2', timeout: 60000 })
- await page.waitForSelector('.cockpit-shell', { timeout: 20000 })
- await page
- .waitForFunction(
- () => ['ready', 'failed', 'disabled'].includes(document.querySelector('.cockpit-shell__map .map-canvas')?.dataset.mapEngine),
- { timeout: 40000 },
- )
- .catch(() => {})
- if (await page.$('[data-cockpit-intro]')) {
- await page.click('[data-cockpit-intro]')
- await page.waitForSelector('[data-cockpit-intro]', { hidden: true, timeout: 10000 }).catch(() => {})
- }
- await sleep(1200)
- await page.evaluate(() => {
- const button = Array.from(document.querySelectorAll('.cockpit-shell__mode')).find((b) => b.textContent.trim() === '预警处置')
- if (button) button.click()
- })
- await page
- .waitForFunction(() => !!document.querySelector('[data-panel-id="currentEvents"] [data-event-row]'), { timeout: 20000 })
- .catch(() => {})
- await sleep(800)
- return { page, errors }
- }
- /** 用**真实鼠标**点第一行事件(不是直接调 store)。 */
- async function clickFirstRow(page) {
- const box = await page.evaluate(() => {
- const row = document.querySelector('[data-panel-id="currentEvents"] [data-event-row]')
- if (!row) return null
- const rect = row.getBoundingClientRect()
- return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }
- })
- if (!box) return false
- await page.mouse.click(box.x, box.y)
- await sleep(400)
- return true
- }
- // ================================================================ A 详情在界面上真的画出来
- {
- const { page, errors } = await openDetailRow('success')
- const before = await readState(page)
- const clicked = await clickFirstRow(page)
- const state = await readState(page)
- check('A1-row-clicked', clicked, '点到了第一条事件行(真实鼠标)')
- check(
- 'A2-detail-rendered',
- state.detailOpen && state.detailCode.length > 0 && state.detailCode === before.rows[0],
- `详情卡的编码 ${state.detailCode}(= 列表第一行 ${before.rows[0]})`,
- )
- const fields = state.fields || {}
- const empty = Object.entries(fields).filter(([key, value]) => !value && key !== 'location')
- check(
- 'A3-all-fields-present',
- empty.length === 0 && fields.level.includes('级') && fields.threshold.length > 0 && fields.value.length > 0,
- empty.length ? `空字段:${empty.map(([key]) => key).join(',')}` : `8 项字段都从 DOM 读到(等级=${fields.level}、阈值=${fields.threshold}、实际值=${fields.value})`,
- )
- check(
- 'A4-detail-matches-row',
- state.rows.length === before.rows.length && JSON.stringify(state.rows) === JSON.stringify(before.rows),
- `打开详情前后行码逐位相同(${state.rows.length} 行)—— 详情不改变列表`,
- )
- // ---- B 定位:相机真的动,高亮不新增实体 ----
- const cameraBefore = state.camera
- await page.evaluate(() => {
- const btn = document.querySelector('[data-event-detail-locate]')
- if (btn) btn.click()
- })
- await sleep(2600)
- const located = await readState(page)
- const cameraMoved =
- EVIDENCE_CAMERA(cameraBefore, located.camera)
- check(
- 'B1-locate-pressed-and-reports',
- located.locateResult !== '',
- `定位结果:${located.locateResult} · ${located.locateText}`,
- )
- check(
- 'B2-camera-really-moved-or-honest-failure',
- located.locateResult === 'ok' ? cameraMoved : located.locateText.length > 0,
- located.locateResult === 'ok'
- ? `相机状态 ${JSON.stringify(cameraBefore)} → ${JSON.stringify(located.camera)}`
- : `未成时如实给原因(${located.locateText})—— 不假装成功`,
- )
- check(
- 'B3-locate-adds-no-marker',
- before.markerCount !== null && located.markerCount !== null && before.markerCount === located.markerCount,
- `定位前后地图点位数:${before.markerCount} → ${located.markerCount}(只高亮已有实体,不新建)`,
- )
- check(
- 'B4-emphasis-visible-on-host',
- located.locateResult === 'ok' ? located.emphasis !== null && located.emphasis > 0 : located.emphasis !== null,
- `宿主 data-map-emphasis=${located.emphasis}${located.locateResult === 'ok' ? '(定位成功时必须有实体被强调)' : '(未成时也写 0,不留旧值)'}`,
- )
- // ---- C 返回:详情收起,列表上下文原样 ----
- await page.evaluate(() => {
- const btn = document.querySelector('[data-event-detail-back]')
- if (btn) btn.click()
- })
- await sleep(400)
- const back = await readState(page)
- check(
- 'C1-back-collapses-detail',
- back.detailOpen === false && back.panelRendered,
- '返回事件列表:详情收起、面板与列表仍在',
- )
- check(
- 'C2-back-keeps-list-position-and-family',
- JSON.stringify(back.rows) === JSON.stringify(before.rows) &&
- back.mode === before.mode &&
- back.rows.length === before.rows.length,
- `返回后行码逐位相同、族仍是「${back.mode}」(${back.rows.length} 行,列表位置与筛选条件未变)`,
- )
- check(
- 'C3-no-page-errors',
- errors.length === 0,
- errors.slice(0, 3).join(' | '),
- )
- // ---- D 无有效坐标(自有样例):详情仍完整 + 写出原因 + 定位如实报不可上图 ----
- const noCoord = await openDetailRow('event-detail-no-coordinate')
- const noCoordBefore = await readState(noCoord.page)
- await clickFirstRow(noCoord.page)
- await noCoord.page.evaluate(() => {
- const btn = document.querySelector('[data-event-detail-locate]')
- if (btn) btn.click()
- })
- await sleep(1500)
- const blocked = await readState(noCoord.page)
- check(
- 'D1-detail-still-complete',
- blocked.detailOpen &&
- (blocked.fields || {}).code === noCoordBefore.rows[0] &&
- (blocked.fields || {}).value.length > 0 &&
- (blocked.fields || {}).status.length > 0,
- `无有效坐标时详情仍完整(编码 ${(blocked.fields || {}).code}、实际值 ${(blocked.fields || {}).value}、状态 ${(blocked.fields || {}).status})`,
- )
- check(
- 'D2-no-location-reason-is-written',
- (blocked.fields || {}).location.includes('坐标') || (blocked.fields || {}).location.includes('无地图位置'),
- `「位置」一栏:${(blocked.fields || {}).location}(原因来自票 07 的同一张词表,不在详情里编第二套)`,
- )
- check(
- 'D3-locate-reports-blocked-honestly',
- blocked.locateResult === 'blocked' && blocked.locateText.length > 0,
- `定位反馈:${blocked.locateResult} · ${blocked.locateText}`,
- )
- check(
- 'D4-locate-adds-no-marker-when-blocked',
- blocked.markerCount !== null && noCoordBefore.markerCount !== null && blocked.markerCount === noCoordBefore.markerCount,
- `定位失败时点位数也不变:${noCoordBefore.markerCount} → ${blocked.markerCount}`,
- )
- check('D5-no-page-errors', noCoord.errors.length === 0, noCoord.errors.slice(0, 3).join(' | '))
- await noCoord.page.close()
- // ---- E 收起详情 ⇒ 高亮还原(读宿主上的强调计数,不是读内部变量)----
- const emphasisWhileOpen = located.emphasis
- await page.evaluate(() => {
- const btn = document.querySelector('[data-event-detail-back]')
- if (btn) btn.click()
- })
- await sleep(600)
- const closed = await readState(page)
- check(
- 'E1-highlight-restored-on-close',
- closed.emphasis === 0,
- `收起详情后 data-map-emphasis=${closed.emphasis}(定位后为 ${emphasisWhileOpen})—— 还原真的发生了:标记不会一直胖着、管线宽度回到原值`,
- )
- await page.close()
- }
- /** 相机是否真的动了(对比定位前后的中心与距离;句式与 t10 一致)。 */
- function EVIDENCE_CAMERA(before, after) {
- if (!before || !after) return true // 引擎未暴露状态时不把「读不到」当失败
- const pick = (state) => `${state.lon ?? state.longitude},${state.lat ?? state.latitude},${state.range ?? state.distance}`
- return pick(before) !== pick(after)
- }
- await browser.close()
- const failed = results.filter((item) => !item.pass)
- console.log('')
- console.log(`T14 真机走查:${results.length - failed.length}/${results.length} 项通过。`)
- if (failed.length) {
- console.log('未通过:')
- for (const item of failed) console.log(` · ${item.id}`)
- process.exit(1)
- }
- console.log('T14 事件详情与设备定位真机走查全部通过。')
|