probe-walkthrough.mjs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. /**
  2. * 票 46 浏览器走查探针(真实后端 real,headless Chrome via CDP)。
  3. *
  4. * 为什么用探针而不是交互式会话:地图标记是 Cesium 实体(交互式会话点不到),
  5. * 且并行会话写 src 会触发 vite 整页重载打断多步操作。本仓票 26/33/36 遇同题用同一手法。
  6. * 针对"重载打断"的两层防御:
  7. * ① 进入路径可重入(ensurePointLevel / ensureDeviceTab:状态丢了就重新走进去);
  8. * ② 交互+读数合并进**单个页内 evaluate**(C/D/E、G 各一次往返),
  9. * 把被重载打断的窗口从几十秒压到几秒;被打断则整块重试(幂等:全是视图操作)。
  10. *
  11. * 进入路径:顶部状态栏「报警」筛选(决策 02 冻结的接缝,把唯一报警点孤立出来)
  12. * → setCameraFocus 飞近 → CDP 真鼠标点覆盖层标记的内部 SVG(宿主节点是 0×0 定位锚;
  13. * 诊断 3 实测全球视角下邻近点位实体抢先拾取,按坐标重拾不认传入 code)。
  14. * 目标:CBWS202610150001(设备 21119740:真实后端 5 预警/66 报警/7 工单)。
  15. *
  16. * 覆盖票面验收:
  17. * A 点位摘要卡片化(状态卡 + KPI 计数条 + t41 钉住的 dd/b 形状)
  18. * B 设备详情标签页(五页、默认档案、图片并入档案页、页签带条数)
  19. * C 报警分页(每页 10、第 1/7 页、末页余数 6、到边禁用、默认全收起)
  20. * D 分类查询(chips 计数、先筛后翻页码回 1、生效回显、再点取消)
  21. * E 历史折叠展开(数据时间/状态/来源/关联设备四件事、值非空白)
  22. * F 图例对齐(详情状态标签带图标、色键与一张图顶部状态栏同源)
  23. * G 返回再进(不重复取数、视图回默认页签)
  24. * H 窄面板 480px(页签/卡体不横向溢出)
  25. * Z 应用侧零 pageerror / 零 console error
  26. *
  27. * 运行(dev server 需已在 :8000):
  28. * node .scratch/cockpit-real-data-wayfinding/verify/artifacts/t46/probe-walkthrough.mjs
  29. */
  30. import { mkdir, writeFile } from 'node:fs/promises'
  31. import path from 'node:path'
  32. import { cdpLaunch } from '../../../../cockpit-shell-panel-layout/verify/cdp-driver.mjs'
  33. const CHROME = process.env.CHROME_PATH || 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'
  34. const URL = 'http://127.0.0.1:8000/#/cockpit'
  35. const OUT = path.resolve('.scratch/cockpit-real-data-wayfinding/verify/artifacts/t46')
  36. const POINT = 'CBWS202610150001'
  37. const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
  38. const results = []
  39. const shots = []
  40. function check(name, pass, evidence) {
  41. results.push({ name, pass })
  42. console.log(`${pass ? 'PASS' : 'FAIL'} ${name}${evidence ? ` — ${evidence}` : ''}`)
  43. }
  44. async function main() {
  45. await mkdir(OUT, { recursive: true })
  46. const browser = await cdpLaunch({ executablePath: CHROME, headless: true, width: 1680, height: 950 })
  47. const page = await browser.newPage()
  48. const pageErrors = []
  49. const consoleErrors = []
  50. page.on('pageerror', (e) => pageErrors.push(String(e)))
  51. page.on('console', (m) => consoleErrors.push(String(m.text || '')))
  52. await page.goto(URL, { timeout: 60000 })
  53. await sleep(2500)
  54. await page.click('[data-cockpit-intro]').catch(() => {})
  55. await page.waitForFunction(
  56. () => ['ready', 'failed', 'disabled'].includes(document.querySelector('.cockpit-shell__map .map-canvas')?.dataset.mapEngine || ''),
  57. { timeout: 60000 },
  58. )
  59. const shot = async (tag) => {
  60. const b64 = await page.screenshot()
  61. await writeFile(path.join(OUT, `${tag}.png`), Buffer.from(b64, 'base64'))
  62. shots.push(`${tag}.png`)
  63. }
  64. /** 真鼠标点覆盖层标记的内部 SVG 中心;返回点后的详情层级/点位。 */
  65. async function clickOverlay(code) {
  66. const box = await page.evaluate((target) => {
  67. const node = document.querySelector(`[data-marker-overlay-code="${target}"]`)
  68. if (!node) return null
  69. const inner = node.querySelector('svg') || node.firstElementChild || node
  70. const r = inner.getBoundingClientRect()
  71. return { cx: r.x + r.width / 2, cy: r.y + r.height / 2, w: r.width, h: r.height }
  72. }, code)
  73. if (!box || !(box.w > 0)) return null
  74. await page._dispatchMouse('mouseMoved', box.cx, box.cy)
  75. await sleep(120)
  76. await page._dispatchMouse('mousePressed', box.cx, box.cy, { button: 'left', buttons: 1, clickCount: 1 })
  77. await sleep(60)
  78. await page._dispatchMouse('mouseReleased', box.cx, box.cy, { button: 'left', buttons: 0, clickCount: 1 })
  79. await sleep(2500)
  80. return page.evaluate(() => {
  81. const c = document.querySelector('[data-point-detail]')
  82. return c ? { level: c.dataset.detailLevel, point: c.dataset.pointCode } : null
  83. })
  84. }
  85. let coordCache = null
  86. /** 可重入:确保详情卡停在目标点位的点位级。 */
  87. async function ensurePointLevel() {
  88. for (let round = 0; round < 4; round += 1) {
  89. const current = await page.evaluate(() => {
  90. const c = document.querySelector('[data-point-detail]')
  91. return c ? { level: c.dataset.detailLevel, point: c.dataset.pointCode } : null
  92. })
  93. if (current?.point === POINT && current?.level === 'point') return current
  94. if (!coordCache) {
  95. coordCache = await page.evaluate(async (code) => {
  96. const r = await fetch('http://192.168.110.96:8301/api/cockpit/v1/map-snapshot?page=1&pageSize=200&pointStatus=ALARM', { cache: 'no-store' })
  97. const j = await r.json()
  98. const d = j.data || j
  99. const found = (d.monitoringPoints || []).find((p) => (p.pointCode || p.code) === code)
  100. return found ? { lon: found.longitude, lat: found.latitude } : null
  101. }, POINT)
  102. }
  103. const selected = await page.evaluate(() => document.querySelector('[data-legend-key="ALARM"]')?.dataset.statusSelected === 'yes')
  104. if (!selected) {
  105. await page.click('[data-legend-key="ALARM"]').catch(() => {})
  106. await sleep(4000)
  107. }
  108. if (coordCache) {
  109. await page.evaluate((c) => globalThis.__cockpitMap?.setCameraFocus({ lon: c.lon, lat: c.lat, height: 900 }) === true, coordCache)
  110. await sleep(4000)
  111. }
  112. const hit = await clickOverlay(POINT)
  113. if (hit?.point === POINT && hit?.level === 'point') return hit
  114. }
  115. return null
  116. }
  117. /**
  118. * 可重入:确保在设备级并停在指定页签。
  119. * 已在目标点位设备级时**只点页签按钮**——不重走标记拾取(详情浮层盖住标记,
  120. * 真鼠标事件到不了覆盖层;实测 3 轮全被打断就是这个原因)。
  121. */
  122. async function ensureDeviceTab(tab) {
  123. const read = () => page.evaluate(
  124. (name) => ({
  125. level: document.querySelector('[data-point-detail]')?.dataset.detailLevel || '',
  126. point: document.querySelector('[data-point-detail]')?.dataset.pointCode || '',
  127. active: [...document.querySelectorAll('[data-equipment-tab]')].find((b) => b.getAttribute('aria-selected') === 'true')?.dataset.equipmentTab || '',
  128. hasSection: !!document.querySelector(`[data-equipment-section="${name}"]`),
  129. }),
  130. tab,
  131. )
  132. let state = await read()
  133. if (state.level === 'equipment' && state.point === POINT) {
  134. if (state.active !== tab) {
  135. await page.evaluate((name) => document.querySelector(`[data-equipment-tab="${name}"]`)?.click(), tab)
  136. await sleep(700)
  137. }
  138. state = await read()
  139. if (state.active === tab && state.hasSection) return state
  140. }
  141. const opened = await ensurePointLevel()
  142. if (opened?.point !== POINT) return { broken: true }
  143. await page.evaluate(() => document.querySelector('[data-device-card]')?.click())
  144. await page.waitForFunction(() => document.querySelector('[data-point-detail]')?.dataset.detailLevel === 'equipment', { timeout: 25000 }).catch(() => {})
  145. await sleep(2000)
  146. await page.evaluate((name) => document.querySelector(`[data-equipment-tab="${name}"]`)?.click(), tab)
  147. await sleep(800)
  148. return read()
  149. }
  150. /** 整块重试:ensure 状态 → 一次 evaluate 内完成全部交互+读数(重载打断则重来)。 */
  151. async function atomicOnAlarms(body, rounds = 3) {
  152. for (let round = 0; round < rounds; round += 1) {
  153. try {
  154. await ensureDeviceTab('alarms')
  155. const out = await page.evaluate(body)
  156. if (out && !out.__broken) return out
  157. } catch {
  158. /* 页被重载:整块重试 */
  159. }
  160. await sleep(800)
  161. }
  162. return { __broken: true }
  163. }
  164. // ---------------- A 点位摘要卡片化 ----------------
  165. const opened = await ensurePointLevel()
  166. check('A0-opened-target-point', opened?.point === POINT && opened?.level === 'point', `命中=${opened ? `${opened.point}/${opened.level}` : 'null'}`)
  167. const level1 = await page.evaluate(() => {
  168. const card = document.querySelector('[data-point-detail]')
  169. const kpis = [...card.querySelectorAll('[data-point-kpi]')]
  170. return {
  171. hasSummary: !!card.querySelector('[data-point-summary]'),
  172. codeShape: card.querySelector('[data-point-code-value]')?.tagName || '',
  173. statusCard: !!card.querySelector('[data-point-status-card]'),
  174. kpiCount: kpis.length,
  175. kpi: kpis.map((n) => `${n.dataset.pointKpi}=${n.querySelector('b')?.textContent?.trim()}:${n.querySelector('b')?.dataset.pointKpiTone || ''}`),
  176. deviceCount: card.querySelector('[data-point-device-count]')?.textContent?.trim(),
  177. deviceCountTag: card.querySelector('[data-point-device-count]')?.tagName || '',
  178. deviceCards: card.querySelectorAll('[data-device-card]').length,
  179. }
  180. })
  181. check('A1-point-summary-card-ified', level1.hasSummary && level1.statusCard, JSON.stringify({ summary: level1.hasSummary, statusCard: level1.statusCard }))
  182. check('A1b-kpi-strip-rendered', level1.kpiCount >= 5, level1.kpi.join(' '))
  183. check('A1c-pinned-shapes-kept', level1.codeShape === 'DD' && level1.deviceCountTag === 'B', `code=<${level1.codeShape}> count=<${level1.deviceCountTag}>`)
  184. await shot('01-point-level')
  185. // ---------------- F 图例对齐(点位级:状态卡 + 设备卡片 >=2 个标签)----------------
  186. // 顶部图例的图标类是 .map-holo__legend-icon(CockpitIcon),详情标签内是 .status-tag__icon。
  187. const legend = await page.evaluate(() => {
  188. 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') }))
  189. 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') }))
  190. return { top, card }
  191. })
  192. const alarmRow = legend.top.find((r) => r.key === 'ALARM')
  193. check('F1-top-legend-has-icon-rows', legend.top.length >= 8 && alarmRow?.icon === true, `顶部状态栏 ${legend.top.length} 行,报警行带图标=${alarmRow?.icon}`)
  194. 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)}`)
  195. check('F3-tone-shared', legend.card.some((c) => c.tone === alarmRow?.tone), `详情标签色键 ${legend.card.map((c) => c.tone).join('/')} 与顶部报警行 ${alarmRow?.tone} 同源`)
  196. // ---------------- B 进设备详情:标签页 ----------------
  197. await ensureDeviceTab('archive')
  198. const tabs = await page.evaluate(() => {
  199. const btns = [...document.querySelectorAll('[data-equipment-tab]')]
  200. const body = document.querySelector('[data-equipment-tab-body]')
  201. return {
  202. tabs: btns.map((b) => `${b.dataset.equipmentTab}:${b.getAttribute('aria-selected') === 'true' ? 'sel' : ''}:${b.querySelector('.equipment-tabs__count')?.textContent || ''}`),
  203. activeTab: btns.find((b) => b.getAttribute('aria-selected') === 'true')?.dataset.equipmentTab || '',
  204. sections: [...body.querySelectorAll('[data-equipment-section]')].map((s) => s.dataset.equipmentSection),
  205. deviceStatusIcon: !!document.querySelector('[data-device-status] .status-tag__icon'),
  206. }
  207. })
  208. check('B1-five-tabs-default-archive', tabs.tabs.length === 5 && tabs.activeTab === 'archive', tabs.tabs.join(' | '))
  209. check('B2-image-merged-into-archive-tab', tabs.sections.join(',') === 'archive,latestImage', `档案页含分区:${tabs.sections.join(',')}`)
  210. check('B3-device-status-icon', tabs.deviceStatusIcon, '设备详情表头状态标签带图标')
  211. await shot('02-device-archive-tab')
  212. // ---------------- C+D+E 报警页:分页 / 分类 / 折叠展开(单 evaluate 原子块)----------------
  213. const cde = await atomicOnAlarms(async () => {
  214. const nap = (ms) => new Promise((r) => setTimeout(r, ms))
  215. const sec = () => document.querySelector('[data-equipment-section="alarms"]')
  216. if (!sec()) return { __broken: true }
  217. const out = {}
  218. out.tabCount = document.querySelector('[data-equipment-tab="alarms"]')?.querySelector('.equipment-tabs__count')?.textContent || ''
  219. // C:第 1 页
  220. const pager1 = sec().querySelector('[data-pager]')
  221. out.page1 = {
  222. rows: sec().querySelectorAll('[data-event]').length,
  223. collapsedClosed: [...sec().querySelectorAll('[data-history-item]')].every((d) => !d.hasAttribute('open')),
  224. pagerText: pager1 ? pager1.querySelector('[data-page-state]')?.textContent?.trim() : '(无控件)',
  225. prevDisabled: pager1?.querySelector('[data-page-prev]')?.disabled,
  226. nextDisabled: pager1?.querySelector('[data-page-next]')?.disabled,
  227. }
  228. // 翻到末页
  229. for (let i = 0; i < 6; i += 1) { sec().querySelector('[data-page-next]')?.click(); await nap(120) }
  230. const last = sec().querySelector('[data-pager]')
  231. out.lastPage = {
  232. text: last?.querySelector('[data-page-state]')?.textContent?.trim(),
  233. rows: sec().querySelectorAll('[data-event]').length,
  234. nextDisabled: last?.querySelector('[data-page-next]')?.disabled,
  235. }
  236. // 回第 1 页
  237. for (let i = 0; i < 6; i += 1) { sec().querySelector('[data-page-prev]')?.click(); await nap(120) }
  238. // D:点一个计数 >10 的分类 chip(此刻停在末页也验证"换分类回第 1 页")
  239. for (let i = 0; i < 6; i += 1) { sec().querySelector('[data-page-next]')?.click(); await nap(100) }
  240. const chips = [...sec().querySelectorAll('[data-category-chip]')]
  241. out.chips = chips.map((c) => `${c.dataset.categoryChip.split(':').pop()}=${c.querySelector('[data-category-count]')?.textContent}`)
  242. const pick = chips.find((c) => !c.dataset.categoryChip.endsWith(':all') && Number(c.querySelector('[data-category-count]')?.textContent) > 10)
  243. out.chipKey = pick?.dataset.categoryChip || ''
  244. pick?.click()
  245. await nap(300)
  246. out.afterChip = {
  247. rows: sec().querySelectorAll('[data-event]').length,
  248. pager: sec().querySelector('[data-page-state]')?.textContent?.trim() || '(无控件)',
  249. note: sec().querySelector('[data-category-note]')?.textContent?.trim() || '',
  250. active: sec().querySelector('[data-category-chip][aria-pressed="true"]')?.dataset.categoryChip || '',
  251. }
  252. // 再点同一 chip = 取消
  253. sec().querySelector(`[data-category-chip="${out.chipKey}"]`)?.click()
  254. await nap(300)
  255. out.afterCancel = {
  256. rows: sec().querySelectorAll('[data-event]').length,
  257. note: sec().querySelector('[data-category-note]')?.textContent || '',
  258. active: sec().querySelector('[data-category-chip][aria-pressed="true"]')?.dataset.categoryChip || '',
  259. }
  260. // E:展开第一条
  261. const item = sec().querySelector('[data-history-item]')
  262. item?.querySelector('summary')?.click()
  263. await nap(250)
  264. out.expanded = item?.hasAttribute('open') === true
  265. const d = sec().querySelector('[data-history-item][open] [data-history-detail]')
  266. out.detail = {
  267. labels: [...(d?.querySelectorAll('[data-detail-row]') || [])].map((n) => n.dataset.detailRow),
  268. values: [...(d?.querySelectorAll('dd') || [])].map((n) => n.textContent.trim()),
  269. }
  270. return out
  271. })
  272. check('C0-atomic-block-completed', !cde.__broken, cde.__broken ? '整块 3 次都被重载打断(并行会话在写 src)' : '一次 evaluate 完成 C/D/E 全部交互与读数')
  273. check('C1-tab-count-66-and-page10', cde.tabCount === '66' && cde.page1?.rows === 10, `页签计数=${cde.tabCount} 第1页行数=${cde.page1?.rows}`)
  274. check('C2-pager-reads-page-and-total', /第\s*1\/7\s*页/.test(cde.page1?.pagerText || '') && /66/.test(cde.page1?.pagerText || ''), cde.page1?.pagerText)
  275. // 空集合的 every 恒真 —— 必须连"确实有行"一起判(本仓"守卫比它要守的东西宽"的同一课)。
  276. check('C3-default-collapsed', (cde.page1?.rows || 0) > 0 && cde.page1?.collapsedClosed === true, `第1页 ${cde.page1?.rows} 行全部默认收起=${cde.page1?.collapsedClosed}`)
  277. check('C4-edge-disabled-prev', cde.page1?.prevDisabled === true && cde.page1?.nextDisabled === false, `上一页禁用=${cde.page1?.prevDisabled} 下一页禁用=${cde.page1?.nextDisabled}`)
  278. 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}`)
  279. check('D0-chips-counts', (cde.chips || []).length >= 2 && cde.chips.some((c) => c.startsWith('all=66')), (cde.chips || []).join(' '))
  280. 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}`)
  281. check('D2-active-note-visible', /再点同一项取消/.test(cde.afterChip?.note || ''), cde.afterChip?.note)
  282. 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}`)
  283. const four = ['数据时间', '状态', '来源', '关联设备']
  284. check('E1-expand-shows-four-facts', cde.expanded === true && four.every((l) => (cde.detail?.labels || []).includes(l)), `展开=${cde.expanded} 详情行:${(cde.detail?.labels || []).join('/')}`)
  285. 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)}`)
  286. await shot('06-alarms-expanded')
  287. // ---------------- G 返回再进:不重复取数、视图回默认(原子块)----------------
  288. const g = await (async () => {
  289. for (let round = 0; round < 3; round += 1) {
  290. await ensureDeviceTab('alarms')
  291. try {
  292. const out = await page.evaluate(async () => {
  293. const nap = (ms) => new Promise((r) => setTimeout(r, ms))
  294. const before = performance.getEntriesByType('resource').filter((r) => r.name.includes('/detail-aggregate')).length
  295. document.querySelector('[data-detail-back]')?.click()
  296. await nap(1200)
  297. document.querySelector('[data-device-card]')?.click()
  298. for (let i = 0; i < 30; i += 1) {
  299. await nap(200)
  300. if (document.querySelector('[data-point-detail]')?.dataset.detailLevel === 'equipment') break
  301. }
  302. return {
  303. before,
  304. after: performance.getEntriesByType('resource').filter((r) => r.name.includes('/detail-aggregate')).length,
  305. activeTab: [...document.querySelectorAll('[data-equipment-tab]')].find((b) => b.getAttribute('aria-selected') === 'true')?.dataset.equipmentTab || '',
  306. }
  307. })
  308. if (out && out.after >= out.before) return out
  309. } catch {
  310. /* 重载打断:重试 */
  311. }
  312. await sleep(600)
  313. }
  314. return { __broken: true }
  315. })()
  316. check('G1-no-refetch-on-return', !g.__broken && g.after === g.before, `detail-aggregate 请求数 ${g.before} → ${g.after}`)
  317. check('G2-view-state-back-to-default', g.activeTab === 'archive', `重进后停在默认页签 ${g.activeTab}(分类/页码也随组件重建归零)`)
  318. await shot('07-back-and-reenter')
  319. // ---------------- H 窄面板 480px ----------------
  320. await ensureDeviceTab('alarms')
  321. await page.conn.send('Emulation.setDeviceMetricsOverride', { width: 480, height: 820, deviceScaleFactor: 1, mobile: false }, page.sessionId)
  322. await sleep(1500)
  323. const narrow = await page.evaluate(() => {
  324. const overflow = (sel) => { const el = document.querySelector(sel); return el ? { sw: el.scrollWidth, cw: el.clientWidth, over: el.scrollWidth - el.clientWidth } : null }
  325. return { tabs: overflow('[data-equipment-tabs]'), aside: overflow('[data-point-detail]'), body: overflow('.equipment-sections'), chips: overflow('[data-category-bar]') }
  326. })
  327. check('H1-narrow-no-overflow', [narrow.tabs, narrow.aside, narrow.body, narrow.chips].every((n) => n && n.over <= 1), JSON.stringify(narrow))
  328. await shot('08-narrow-480')
  329. await page.conn.send('Emulation.clearDeviceMetricsOverride', {}, page.sessionId).catch(() => {})
  330. // ---------------- Z 应用侧报错 ----------------
  331. const appErrors = pageErrors.filter((e) => !/chrome-extension|baidu|Permissions policy/i.test(e))
  332. const appConsole = consoleErrors.filter((e) => /error/i.test(e) && !/chrome-extension|baidu|Permissions policy|Video overlay|favicon/i.test(e))
  333. check('Z1-no-pageerror', appErrors.length === 0, appErrors.slice(0, 3).join(' | '))
  334. check('Z2-no-console-error', appConsole.length === 0, appConsole.slice(0, 3).join(' | '))
  335. await writeFile(path.join(OUT, 'walkthrough-result.json'), JSON.stringify({ results, shots, level1, legend, tabs, cde, g, narrow }, null, 2))
  336. const failed = results.filter((r) => !r.pass)
  337. console.log('')
  338. console.log(`T46 浏览器走查:${results.length - failed.length}/${results.length} 项通过。截图:${shots.join(', ')}`)
  339. if (failed.length) console.log('未通过:' + failed.map((f) => f.name).join(', '))
  340. await browser.close()
  341. process.exitCode = failed.length ? 1 : 0
  342. }
  343. main().catch((err) => {
  344. console.error('PROBE ERROR:', err)
  345. process.exit(1)
  346. })