|
|
@@ -0,0 +1,367 @@
|
|
|
+/**
|
|
|
+ * 票 46 浏览器走查探针(真实后端 real,headless Chrome via CDP)。
|
|
|
+ *
|
|
|
+ * 为什么用探针而不是交互式会话:地图标记是 Cesium 实体(交互式会话点不到),
|
|
|
+ * 且并行会话写 src 会触发 vite 整页重载打断多步操作。本仓票 26/33/36 遇同题用同一手法。
|
|
|
+ * 针对"重载打断"的两层防御:
|
|
|
+ * ① 进入路径可重入(ensurePointLevel / ensureDeviceTab:状态丢了就重新走进去);
|
|
|
+ * ② 交互+读数合并进**单个页内 evaluate**(C/D/E、G 各一次往返),
|
|
|
+ * 把被重载打断的窗口从几十秒压到几秒;被打断则整块重试(幂等:全是视图操作)。
|
|
|
+ *
|
|
|
+ * 进入路径:顶部状态栏「报警」筛选(决策 02 冻结的接缝,把唯一报警点孤立出来)
|
|
|
+ * → setCameraFocus 飞近 → CDP 真鼠标点覆盖层标记的内部 SVG(宿主节点是 0×0 定位锚;
|
|
|
+ * 诊断 3 实测全球视角下邻近点位实体抢先拾取,按坐标重拾不认传入 code)。
|
|
|
+ * 目标:CBWS202610150001(设备 21119740:真实后端 5 预警/66 报警/7 工单)。
|
|
|
+ *
|
|
|
+ * 覆盖票面验收:
|
|
|
+ * A 点位摘要卡片化(状态卡 + KPI 计数条 + t41 钉住的 dd/b 形状)
|
|
|
+ * B 设备详情标签页(五页、默认档案、图片并入档案页、页签带条数)
|
|
|
+ * C 报警分页(每页 10、第 1/7 页、末页余数 6、到边禁用、默认全收起)
|
|
|
+ * D 分类查询(chips 计数、先筛后翻页码回 1、生效回显、再点取消)
|
|
|
+ * E 历史折叠展开(数据时间/状态/来源/关联设备四件事、值非空白)
|
|
|
+ * F 图例对齐(详情状态标签带图标、色键与一张图顶部状态栏同源)
|
|
|
+ * G 返回再进(不重复取数、视图回默认页签)
|
|
|
+ * H 窄面板 480px(页签/卡体不横向溢出)
|
|
|
+ * Z 应用侧零 pageerror / 零 console error
|
|
|
+ *
|
|
|
+ * 运行(dev server 需已在 :8000):
|
|
|
+ * node .scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/probe-walkthrough.mjs
|
|
|
+ */
|
|
|
+import { mkdir, writeFile } from 'node:fs/promises'
|
|
|
+import path from 'node:path'
|
|
|
+import { cdpLaunch } from '../../../../cockpit-shell-panel-layout/verify/cdp-driver.mjs'
|
|
|
+
|
|
|
+const CHROME = process.env.CHROME_PATH || 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'
|
|
|
+const URL = 'http://127.0.0.1:8000/#/cockpit'
|
|
|
+const OUT = path.resolve('.scratch/cockpit-real-data-wayfinding/verify/artifacts/t46')
|
|
|
+const POINT = 'CBWS202610150001'
|
|
|
+
|
|
|
+const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
|
+const results = []
|
|
|
+const shots = []
|
|
|
+function check(name, pass, evidence) {
|
|
|
+ results.push({ name, pass })
|
|
|
+ console.log(`${pass ? 'PASS' : 'FAIL'} ${name}${evidence ? ` — ${evidence}` : ''}`)
|
|
|
+}
|
|
|
+
|
|
|
+async function main() {
|
|
|
+ await mkdir(OUT, { recursive: true })
|
|
|
+ const browser = await cdpLaunch({ executablePath: CHROME, headless: true, width: 1680, height: 950 })
|
|
|
+ const page = await browser.newPage()
|
|
|
+ const pageErrors = []
|
|
|
+ const consoleErrors = []
|
|
|
+ page.on('pageerror', (e) => pageErrors.push(String(e)))
|
|
|
+ page.on('console', (m) => consoleErrors.push(String(m.text || '')))
|
|
|
+
|
|
|
+ await page.goto(URL, { timeout: 60000 })
|
|
|
+ await sleep(2500)
|
|
|
+ await page.click('[data-cockpit-intro]').catch(() => {})
|
|
|
+ await page.waitForFunction(
|
|
|
+ () => ['ready', 'failed', 'disabled'].includes(document.querySelector('.cockpit-shell__map .map-canvas')?.dataset.mapEngine || ''),
|
|
|
+ { timeout: 60000 },
|
|
|
+ )
|
|
|
+
|
|
|
+ const shot = async (tag) => {
|
|
|
+ const b64 = await page.screenshot()
|
|
|
+ await writeFile(path.join(OUT, `${tag}.png`), Buffer.from(b64, 'base64'))
|
|
|
+ shots.push(`${tag}.png`)
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 真鼠标点覆盖层标记的内部 SVG 中心;返回点后的详情层级/点位。 */
|
|
|
+ async function clickOverlay(code) {
|
|
|
+ const box = await page.evaluate((target) => {
|
|
|
+ const node = document.querySelector(`[data-marker-overlay-code="${target}"]`)
|
|
|
+ if (!node) return null
|
|
|
+ const inner = node.querySelector('svg') || node.firstElementChild || node
|
|
|
+ const r = inner.getBoundingClientRect()
|
|
|
+ return { cx: r.x + r.width / 2, cy: r.y + r.height / 2, w: r.width, h: r.height }
|
|
|
+ }, code)
|
|
|
+ if (!box || !(box.w > 0)) return null
|
|
|
+ await page._dispatchMouse('mouseMoved', box.cx, box.cy)
|
|
|
+ await sleep(120)
|
|
|
+ await page._dispatchMouse('mousePressed', box.cx, box.cy, { button: 'left', buttons: 1, clickCount: 1 })
|
|
|
+ await sleep(60)
|
|
|
+ await page._dispatchMouse('mouseReleased', box.cx, box.cy, { button: 'left', buttons: 0, clickCount: 1 })
|
|
|
+ await sleep(2500)
|
|
|
+ return page.evaluate(() => {
|
|
|
+ const c = document.querySelector('[data-point-detail]')
|
|
|
+ return c ? { level: c.dataset.detailLevel, point: c.dataset.pointCode } : null
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ let coordCache = null
|
|
|
+ /** 可重入:确保详情卡停在目标点位的点位级。 */
|
|
|
+ async function ensurePointLevel() {
|
|
|
+ for (let round = 0; round < 4; round += 1) {
|
|
|
+ const current = await page.evaluate(() => {
|
|
|
+ const c = document.querySelector('[data-point-detail]')
|
|
|
+ return c ? { level: c.dataset.detailLevel, point: c.dataset.pointCode } : null
|
|
|
+ })
|
|
|
+ if (current?.point === POINT && current?.level === 'point') return current
|
|
|
+ if (!coordCache) {
|
|
|
+ coordCache = await page.evaluate(async (code) => {
|
|
|
+ const r = await fetch('http://192.168.110.96:8301/api/cockpit/v1/map-snapshot?page=1&pageSize=200&pointStatus=ALARM', { cache: 'no-store' })
|
|
|
+ const j = await r.json()
|
|
|
+ const d = j.data || j
|
|
|
+ const found = (d.monitoringPoints || []).find((p) => (p.pointCode || p.code) === code)
|
|
|
+ return found ? { lon: found.longitude, lat: found.latitude } : null
|
|
|
+ }, POINT)
|
|
|
+ }
|
|
|
+ const selected = await page.evaluate(() => document.querySelector('[data-legend-key="ALARM"]')?.dataset.statusSelected === 'yes')
|
|
|
+ if (!selected) {
|
|
|
+ await page.click('[data-legend-key="ALARM"]').catch(() => {})
|
|
|
+ await sleep(4000)
|
|
|
+ }
|
|
|
+ if (coordCache) {
|
|
|
+ await page.evaluate((c) => globalThis.__cockpitMap?.setCameraFocus({ lon: c.lon, lat: c.lat, height: 900 }) === true, coordCache)
|
|
|
+ await sleep(4000)
|
|
|
+ }
|
|
|
+ const hit = await clickOverlay(POINT)
|
|
|
+ if (hit?.point === POINT && hit?.level === 'point') return hit
|
|
|
+ }
|
|
|
+ return null
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 可重入:确保在设备级并停在指定页签。
|
|
|
+ * 已在目标点位设备级时**只点页签按钮**——不重走标记拾取(详情浮层盖住标记,
|
|
|
+ * 真鼠标事件到不了覆盖层;实测 3 轮全被打断就是这个原因)。
|
|
|
+ */
|
|
|
+ async function ensureDeviceTab(tab) {
|
|
|
+ const read = () => page.evaluate(
|
|
|
+ (name) => ({
|
|
|
+ level: document.querySelector('[data-point-detail]')?.dataset.detailLevel || '',
|
|
|
+ point: document.querySelector('[data-point-detail]')?.dataset.pointCode || '',
|
|
|
+ active: [...document.querySelectorAll('[data-equipment-tab]')].find((b) => b.getAttribute('aria-selected') === 'true')?.dataset.equipmentTab || '',
|
|
|
+ hasSection: !!document.querySelector(`[data-equipment-section="${name}"]`),
|
|
|
+ }),
|
|
|
+ tab,
|
|
|
+ )
|
|
|
+ let state = await read()
|
|
|
+ if (state.level === 'equipment' && state.point === POINT) {
|
|
|
+ if (state.active !== tab) {
|
|
|
+ await page.evaluate((name) => document.querySelector(`[data-equipment-tab="${name}"]`)?.click(), tab)
|
|
|
+ await sleep(700)
|
|
|
+ }
|
|
|
+ state = await read()
|
|
|
+ if (state.active === tab && state.hasSection) return state
|
|
|
+ }
|
|
|
+ const opened = await ensurePointLevel()
|
|
|
+ if (opened?.point !== POINT) return { broken: true }
|
|
|
+ await page.evaluate(() => document.querySelector('[data-device-card]')?.click())
|
|
|
+ await page.waitForFunction(() => document.querySelector('[data-point-detail]')?.dataset.detailLevel === 'equipment', { timeout: 25000 }).catch(() => {})
|
|
|
+ await sleep(2000)
|
|
|
+ await page.evaluate((name) => document.querySelector(`[data-equipment-tab="${name}"]`)?.click(), tab)
|
|
|
+ await sleep(800)
|
|
|
+ return read()
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 整块重试:ensure 状态 → 一次 evaluate 内完成全部交互+读数(重载打断则重来)。 */
|
|
|
+ async function atomicOnAlarms(body, rounds = 3) {
|
|
|
+ for (let round = 0; round < rounds; round += 1) {
|
|
|
+ try {
|
|
|
+ await ensureDeviceTab('alarms')
|
|
|
+ const out = await page.evaluate(body)
|
|
|
+ if (out && !out.__broken) return out
|
|
|
+ } catch {
|
|
|
+ /* 页被重载:整块重试 */
|
|
|
+ }
|
|
|
+ await sleep(800)
|
|
|
+ }
|
|
|
+ return { __broken: true }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ---------------- A 点位摘要卡片化 ----------------
|
|
|
+ const opened = await ensurePointLevel()
|
|
|
+ check('A0-opened-target-point', opened?.point === POINT && opened?.level === 'point', `命中=${opened ? `${opened.point}/${opened.level}` : 'null'}`)
|
|
|
+ const level1 = await page.evaluate(() => {
|
|
|
+ const card = document.querySelector('[data-point-detail]')
|
|
|
+ const kpis = [...card.querySelectorAll('[data-point-kpi]')]
|
|
|
+ return {
|
|
|
+ hasSummary: !!card.querySelector('[data-point-summary]'),
|
|
|
+ codeShape: card.querySelector('[data-point-code-value]')?.tagName || '',
|
|
|
+ statusCard: !!card.querySelector('[data-point-status-card]'),
|
|
|
+ kpiCount: kpis.length,
|
|
|
+ kpi: kpis.map((n) => `${n.dataset.pointKpi}=${n.querySelector('b')?.textContent?.trim()}:${n.querySelector('b')?.dataset.pointKpiTone || ''}`),
|
|
|
+ deviceCount: card.querySelector('[data-point-device-count]')?.textContent?.trim(),
|
|
|
+ deviceCountTag: card.querySelector('[data-point-device-count]')?.tagName || '',
|
|
|
+ deviceCards: card.querySelectorAll('[data-device-card]').length,
|
|
|
+ }
|
|
|
+ })
|
|
|
+ check('A1-point-summary-card-ified', level1.hasSummary && level1.statusCard, JSON.stringify({ summary: level1.hasSummary, statusCard: level1.statusCard }))
|
|
|
+ check('A1b-kpi-strip-rendered', level1.kpiCount >= 5, level1.kpi.join(' '))
|
|
|
+ check('A1c-pinned-shapes-kept', level1.codeShape === 'DD' && level1.deviceCountTag === 'B', `code=<${level1.codeShape}> count=<${level1.deviceCountTag}>`)
|
|
|
+ await shot('01-point-level')
|
|
|
+
|
|
|
+ // ---------------- F 图例对齐(点位级:状态卡 + 设备卡片 >=2 个标签)----------------
|
|
|
+ // 顶部图例的图标类是 .map-holo__legend-icon(CockpitIcon),详情标签内是 .status-tag__icon。
|
|
|
+ const legend = await page.evaluate(() => {
|
|
|
+ const top = [...document.querySelectorAll('[data-map-legend] [data-legend-key]')].map((n) => ({ key: n.dataset.legendKey, tone: n.querySelector('.status-tag')?.dataset.tone || '', icon: !!n.querySelector('.map-holo__legend-icon') }))
|
|
|
+ const card = [...document.querySelectorAll('[data-point-detail] .status-tag')].map((n) => ({ tone: n.dataset.tone, shape: n.dataset.shape, icon: !!n.querySelector('.status-tag__icon') }))
|
|
|
+ return { top, card }
|
|
|
+ })
|
|
|
+ const alarmRow = legend.top.find((r) => r.key === 'ALARM')
|
|
|
+ check('F1-top-legend-has-icon-rows', legend.top.length >= 8 && alarmRow?.icon === true, `顶部状态栏 ${legend.top.length} 行,报警行带图标=${alarmRow?.icon}`)
|
|
|
+ check('F2-detail-tags-carry-icon', legend.card.length >= 2 && legend.card.every((c) => c.icon === true), `详情内 ${legend.card.length} 个状态标签全部带图标=${legend.card.every((c) => c.icon)}`)
|
|
|
+ check('F3-tone-shared', legend.card.some((c) => c.tone === alarmRow?.tone), `详情标签色键 ${legend.card.map((c) => c.tone).join('/')} 与顶部报警行 ${alarmRow?.tone} 同源`)
|
|
|
+
|
|
|
+ // ---------------- B 进设备详情:标签页 ----------------
|
|
|
+ await ensureDeviceTab('archive')
|
|
|
+ const tabs = await page.evaluate(() => {
|
|
|
+ const btns = [...document.querySelectorAll('[data-equipment-tab]')]
|
|
|
+ const body = document.querySelector('[data-equipment-tab-body]')
|
|
|
+ return {
|
|
|
+ tabs: btns.map((b) => `${b.dataset.equipmentTab}:${b.getAttribute('aria-selected') === 'true' ? 'sel' : ''}:${b.querySelector('.equipment-tabs__count')?.textContent || ''}`),
|
|
|
+ activeTab: btns.find((b) => b.getAttribute('aria-selected') === 'true')?.dataset.equipmentTab || '',
|
|
|
+ sections: [...body.querySelectorAll('[data-equipment-section]')].map((s) => s.dataset.equipmentSection),
|
|
|
+ deviceStatusIcon: !!document.querySelector('[data-device-status] .status-tag__icon'),
|
|
|
+ }
|
|
|
+ })
|
|
|
+ check('B1-five-tabs-default-archive', tabs.tabs.length === 5 && tabs.activeTab === 'archive', tabs.tabs.join(' | '))
|
|
|
+ check('B2-image-merged-into-archive-tab', tabs.sections.join(',') === 'archive,latestImage', `档案页含分区:${tabs.sections.join(',')}`)
|
|
|
+ check('B3-device-status-icon', tabs.deviceStatusIcon, '设备详情表头状态标签带图标')
|
|
|
+ await shot('02-device-archive-tab')
|
|
|
+
|
|
|
+ // ---------------- C+D+E 报警页:分页 / 分类 / 折叠展开(单 evaluate 原子块)----------------
|
|
|
+ const cde = await atomicOnAlarms(async () => {
|
|
|
+ const nap = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
|
+ const sec = () => document.querySelector('[data-equipment-section="alarms"]')
|
|
|
+ if (!sec()) return { __broken: true }
|
|
|
+ const out = {}
|
|
|
+ out.tabCount = document.querySelector('[data-equipment-tab="alarms"]')?.querySelector('.equipment-tabs__count')?.textContent || ''
|
|
|
+
|
|
|
+ // C:第 1 页
|
|
|
+ const pager1 = sec().querySelector('[data-pager]')
|
|
|
+ out.page1 = {
|
|
|
+ rows: sec().querySelectorAll('[data-event]').length,
|
|
|
+ collapsedClosed: [...sec().querySelectorAll('[data-history-item]')].every((d) => !d.hasAttribute('open')),
|
|
|
+ pagerText: pager1 ? pager1.querySelector('[data-page-state]')?.textContent?.trim() : '(无控件)',
|
|
|
+ prevDisabled: pager1?.querySelector('[data-page-prev]')?.disabled,
|
|
|
+ nextDisabled: pager1?.querySelector('[data-page-next]')?.disabled,
|
|
|
+ }
|
|
|
+ // 翻到末页
|
|
|
+ for (let i = 0; i < 6; i += 1) { sec().querySelector('[data-page-next]')?.click(); await nap(120) }
|
|
|
+ const last = sec().querySelector('[data-pager]')
|
|
|
+ out.lastPage = {
|
|
|
+ text: last?.querySelector('[data-page-state]')?.textContent?.trim(),
|
|
|
+ rows: sec().querySelectorAll('[data-event]').length,
|
|
|
+ nextDisabled: last?.querySelector('[data-page-next]')?.disabled,
|
|
|
+ }
|
|
|
+ // 回第 1 页
|
|
|
+ for (let i = 0; i < 6; i += 1) { sec().querySelector('[data-page-prev]')?.click(); await nap(120) }
|
|
|
+
|
|
|
+ // D:点一个计数 >10 的分类 chip(此刻停在末页也验证"换分类回第 1 页")
|
|
|
+ for (let i = 0; i < 6; i += 1) { sec().querySelector('[data-page-next]')?.click(); await nap(100) }
|
|
|
+ const chips = [...sec().querySelectorAll('[data-category-chip]')]
|
|
|
+ out.chips = chips.map((c) => `${c.dataset.categoryChip.split(':').pop()}=${c.querySelector('[data-category-count]')?.textContent}`)
|
|
|
+ const pick = chips.find((c) => !c.dataset.categoryChip.endsWith(':all') && Number(c.querySelector('[data-category-count]')?.textContent) > 10)
|
|
|
+ out.chipKey = pick?.dataset.categoryChip || ''
|
|
|
+ pick?.click()
|
|
|
+ await nap(300)
|
|
|
+ out.afterChip = {
|
|
|
+ rows: sec().querySelectorAll('[data-event]').length,
|
|
|
+ pager: sec().querySelector('[data-page-state]')?.textContent?.trim() || '(无控件)',
|
|
|
+ note: sec().querySelector('[data-category-note]')?.textContent?.trim() || '',
|
|
|
+ active: sec().querySelector('[data-category-chip][aria-pressed="true"]')?.dataset.categoryChip || '',
|
|
|
+ }
|
|
|
+ // 再点同一 chip = 取消
|
|
|
+ sec().querySelector(`[data-category-chip="${out.chipKey}"]`)?.click()
|
|
|
+ await nap(300)
|
|
|
+ out.afterCancel = {
|
|
|
+ rows: sec().querySelectorAll('[data-event]').length,
|
|
|
+ note: sec().querySelector('[data-category-note]')?.textContent || '',
|
|
|
+ active: sec().querySelector('[data-category-chip][aria-pressed="true"]')?.dataset.categoryChip || '',
|
|
|
+ }
|
|
|
+
|
|
|
+ // E:展开第一条
|
|
|
+ const item = sec().querySelector('[data-history-item]')
|
|
|
+ item?.querySelector('summary')?.click()
|
|
|
+ await nap(250)
|
|
|
+ out.expanded = item?.hasAttribute('open') === true
|
|
|
+ const d = sec().querySelector('[data-history-item][open] [data-history-detail]')
|
|
|
+ out.detail = {
|
|
|
+ labels: [...(d?.querySelectorAll('[data-detail-row]') || [])].map((n) => n.dataset.detailRow),
|
|
|
+ values: [...(d?.querySelectorAll('dd') || [])].map((n) => n.textContent.trim()),
|
|
|
+ }
|
|
|
+ return out
|
|
|
+ })
|
|
|
+ check('C0-atomic-block-completed', !cde.__broken, cde.__broken ? '整块 3 次都被重载打断(并行会话在写 src)' : '一次 evaluate 完成 C/D/E 全部交互与读数')
|
|
|
+ check('C1-tab-count-66-and-page10', cde.tabCount === '66' && cde.page1?.rows === 10, `页签计数=${cde.tabCount} 第1页行数=${cde.page1?.rows}`)
|
|
|
+ check('C2-pager-reads-page-and-total', /第\s*1\/7\s*页/.test(cde.page1?.pagerText || '') && /66/.test(cde.page1?.pagerText || ''), cde.page1?.pagerText)
|
|
|
+ // 空集合的 every 恒真 —— 必须连"确实有行"一起判(本仓"守卫比它要守的东西宽"的同一课)。
|
|
|
+ check('C3-default-collapsed', (cde.page1?.rows || 0) > 0 && cde.page1?.collapsedClosed === true, `第1页 ${cde.page1?.rows} 行全部默认收起=${cde.page1?.collapsedClosed}`)
|
|
|
+ check('C4-edge-disabled-prev', cde.page1?.prevDisabled === true && cde.page1?.nextDisabled === false, `上一页禁用=${cde.page1?.prevDisabled} 下一页禁用=${cde.page1?.nextDisabled}`)
|
|
|
+ check('C5-last-page-remainder', /第\s*7\/7\s*页/.test(cde.lastPage?.text || '') && cde.lastPage?.rows === 6 && cde.lastPage?.nextDisabled === true, `${cde.lastPage?.text} 行数=${cde.lastPage?.rows} 下一页禁用=${cde.lastPage?.nextDisabled}`)
|
|
|
+ check('D0-chips-counts', (cde.chips || []).length >= 2 && cde.chips.some((c) => c.startsWith('all=66')), (cde.chips || []).join(' '))
|
|
|
+ check('D1-chip-filters-and-resets-page', !!cde.chipKey && /第\s*1\//.test(cde.afterChip?.pager || '') && cde.afterChip?.active === cde.chipKey, `选中=${cde.afterChip?.active} 页码:${cde.afterChip?.pager} 行数=${cde.afterChip?.rows}`)
|
|
|
+ check('D2-active-note-visible', /再点同一项取消/.test(cde.afterChip?.note || ''), cde.afterChip?.note)
|
|
|
+ check('D3-reclick-cancels-filter', cde.afterCancel?.rows === 10 && cde.afterCancel?.note === '' && cde.afterCancel?.active?.endsWith(':all'), `取消后行数=${cde.afterCancel?.rows} 回显="${cde.afterCancel?.note}" 选中=${cde.afterCancel?.active}`)
|
|
|
+ const four = ['数据时间', '状态', '来源', '关联设备']
|
|
|
+ check('E1-expand-shows-four-facts', cde.expanded === true && four.every((l) => (cde.detail?.labels || []).includes(l)), `展开=${cde.expanded} 详情行:${(cde.detail?.labels || []).join('/')}`)
|
|
|
+ check('E2-values-not-blank', (cde.detail?.values || []).length > 0 && cde.detail.values.every((v) => v && v.length > 0), `值:${(cde.detail?.values || []).join(' | ').slice(0, 140)}`)
|
|
|
+ await shot('06-alarms-expanded')
|
|
|
+
|
|
|
+ // ---------------- G 返回再进:不重复取数、视图回默认(原子块)----------------
|
|
|
+ const g = await (async () => {
|
|
|
+ for (let round = 0; round < 3; round += 1) {
|
|
|
+ await ensureDeviceTab('alarms')
|
|
|
+ try {
|
|
|
+ const out = await page.evaluate(async () => {
|
|
|
+ const nap = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
|
+ const before = performance.getEntriesByType('resource').filter((r) => r.name.includes('/detail-aggregate')).length
|
|
|
+ document.querySelector('[data-detail-back]')?.click()
|
|
|
+ await nap(1200)
|
|
|
+ document.querySelector('[data-device-card]')?.click()
|
|
|
+ for (let i = 0; i < 30; i += 1) {
|
|
|
+ await nap(200)
|
|
|
+ if (document.querySelector('[data-point-detail]')?.dataset.detailLevel === 'equipment') break
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ before,
|
|
|
+ after: performance.getEntriesByType('resource').filter((r) => r.name.includes('/detail-aggregate')).length,
|
|
|
+ activeTab: [...document.querySelectorAll('[data-equipment-tab]')].find((b) => b.getAttribute('aria-selected') === 'true')?.dataset.equipmentTab || '',
|
|
|
+ }
|
|
|
+ })
|
|
|
+ if (out && out.after >= out.before) return out
|
|
|
+ } catch {
|
|
|
+ /* 重载打断:重试 */
|
|
|
+ }
|
|
|
+ await sleep(600)
|
|
|
+ }
|
|
|
+ return { __broken: true }
|
|
|
+ })()
|
|
|
+ check('G1-no-refetch-on-return', !g.__broken && g.after === g.before, `detail-aggregate 请求数 ${g.before} → ${g.after}`)
|
|
|
+ check('G2-view-state-back-to-default', g.activeTab === 'archive', `重进后停在默认页签 ${g.activeTab}(分类/页码也随组件重建归零)`)
|
|
|
+ await shot('07-back-and-reenter')
|
|
|
+
|
|
|
+ // ---------------- H 窄面板 480px ----------------
|
|
|
+ await ensureDeviceTab('alarms')
|
|
|
+ await page.conn.send('Emulation.setDeviceMetricsOverride', { width: 480, height: 820, deviceScaleFactor: 1, mobile: false }, page.sessionId)
|
|
|
+ await sleep(1500)
|
|
|
+ const narrow = await page.evaluate(() => {
|
|
|
+ const overflow = (sel) => { const el = document.querySelector(sel); return el ? { sw: el.scrollWidth, cw: el.clientWidth, over: el.scrollWidth - el.clientWidth } : null }
|
|
|
+ return { tabs: overflow('[data-equipment-tabs]'), aside: overflow('[data-point-detail]'), body: overflow('.equipment-sections'), chips: overflow('[data-category-bar]') }
|
|
|
+ })
|
|
|
+ check('H1-narrow-no-overflow', [narrow.tabs, narrow.aside, narrow.body, narrow.chips].every((n) => n && n.over <= 1), JSON.stringify(narrow))
|
|
|
+ await shot('08-narrow-480')
|
|
|
+ await page.conn.send('Emulation.clearDeviceMetricsOverride', {}, page.sessionId).catch(() => {})
|
|
|
+
|
|
|
+ // ---------------- Z 应用侧报错 ----------------
|
|
|
+ const appErrors = pageErrors.filter((e) => !/chrome-extension|baidu|Permissions policy/i.test(e))
|
|
|
+ const appConsole = consoleErrors.filter((e) => /error/i.test(e) && !/chrome-extension|baidu|Permissions policy|Video overlay|favicon/i.test(e))
|
|
|
+ check('Z1-no-pageerror', appErrors.length === 0, appErrors.slice(0, 3).join(' | '))
|
|
|
+ check('Z2-no-console-error', appConsole.length === 0, appConsole.slice(0, 3).join(' | '))
|
|
|
+
|
|
|
+ await writeFile(path.join(OUT, 'walkthrough-result.json'), JSON.stringify({ results, shots, level1, legend, tabs, cde, g, narrow }, null, 2))
|
|
|
+ const failed = results.filter((r) => !r.pass)
|
|
|
+ console.log('')
|
|
|
+ console.log(`T46 浏览器走查:${results.length - failed.length}/${results.length} 项通过。截图:${shots.join(', ')}`)
|
|
|
+ if (failed.length) console.log('未通过:' + failed.map((f) => f.name).join(', '))
|
|
|
+ await browser.close()
|
|
|
+ process.exitCode = failed.length ? 1 : 0
|
|
|
+}
|
|
|
+
|
|
|
+main().catch((err) => {
|
|
|
+ console.error('PROBE ERROR:', err)
|
|
|
+ process.exit(1)
|
|
|
+})
|