t10-point-detail-walkthrough.mjs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. /**
  2. * T10 真机走查:点地图上的监测点 → 点位摘要 + 设备卡片 → 选卡片进设备详情(票 10)。
  3. *
  4. * 逻辑脚本(t10-point-detail.mjs)已经把两级状态机、摘要/卡片视图、store 取数与竞态、
  5. * 「关联设备不重复上图」的守卫测透了;这里只补**只有在真机才成立**的部分:
  6. * 1. 地图上的点位标记**真的能被鼠标拾到**(pointerdown/up → Cesium 拾取 → 打开浮层整条路);
  7. * 2. 拖拽地图**不会**误触发点选(票 03 的左键平移手感不能被新功能打坏);
  8. * 3. 点位摘要五要素真的渲染出来,且状态标签与地图标记同源(同形状同色键同文字);
  9. * 4. 设备卡片顺序 = 后端返回顺序(样例刻意逆状态优先级,界面一排序就露馅);
  10. * 5. 选卡片进第二级、返回第一级、关闭浮层;打开详情不改动地图上的标记数量。
  11. *
  12. * 运行(需先起 `yarn dev`;本机 dev 端口见启动输出,默认 8000/8001):
  13. * node .scratch/cockpit-shell-panel-layout/verify/t10-point-detail-walkthrough.mjs
  14. * COCKPIT_BASE=http://localhost:8001 node ...(端口不是 8000 时)
  15. *
  16. * 两个只有真机才踩得到的坑(都写进了下面的实现,替换成"直接点屏幕中心"就会假失败):
  17. * · 相机落位是**阻尼飞行**(跨县要 1~2 秒),飞行途中中心还在半路上 —— 必须等 `shownPivot`
  18. * 贴到目标再点,固定 sleep 会点在飞行路径上;
  19. * · 贴地标记与"高度 0 的中心"之间有地形高程差 —— 所以先用引擎拾取在中心附近
  20. * **找到标记的真实屏幕坐标**,再用真实鼠标点那个坐标(点选链路本身仍是 DOM 事件)。
  21. *
  22. * 演示开关(宿主全局键注入,不改代码重新构建):
  23. * `window.__COCKPIT_MOCK_SCENARIO__ = 'point-detail-order'` —— 设备卡片逆状态优先级排列
  24. * `window.__COCKPIT_MOCK_SCENARIO__ = 'point-statuses'` —— 六态点位 + 一个可上图的未关联设备
  25. * 人工演示同一件事:控制台里 `__cockpitMap.getPointMarkers()` 看标记计划、
  26. * `__cockpitMap.pickPoint(x, y)` 用屏幕坐标走同一条点选链路。
  27. */
  28. import { join } from 'node:path'
  29. import { tmpdir } from 'node:os'
  30. const BASE = process.env.COCKPIT_BASE || 'http://localhost:8000'
  31. const ENTRY_URL = process.env.COCKPIT_URL || `${BASE}/#/cockpit`
  32. const CHROME = process.env.CHROME_PATH || 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe'
  33. const PUPPETEER = 'file:///C:/Users/ASUS/.dsh/profiles/desktop/node_modules/puppeteer-core/lib/puppeteer/puppeteer-core.js'
  34. const results = []
  35. /** `check(id, pass, evidence)` —— 第二参是**断言结果**,不是描述(票 08 踩过这个假绿)。 */
  36. function check(id, pass, evidence) {
  37. results.push({ id, pass })
  38. const rest = evidence === undefined ? '' : String(evidence)
  39. console.log(`${pass ? 'PASS' : 'FAIL'} ${id} :: ${rest}`)
  40. }
  41. const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
  42. const puppeteer = (await import(PUPPETEER)).default
  43. const browser = await puppeteer.launch({
  44. executablePath: CHROME,
  45. headless: true,
  46. userDataDir: join(tmpdir(), 'cockpit-t10-profile'),
  47. defaultViewport: { width: 1680, height: 950 },
  48. args: ['--no-sandbox', '--disable-dev-shm-usage', '--no-first-run', '--no-default-browser-check'],
  49. })
  50. console.log(`Chrome: ${await browser.version()}`)
  51. /** 一次读全:详情浮层 DOM(层级/摘要五要素/设备卡片)+ 地图侧口径(标记计划、宿主 dataset)。 */
  52. const readDetail = (page) =>
  53. page.evaluate(() => {
  54. const host = document.querySelector('.cockpit-shell__map .map-canvas')
  55. const card = document.querySelector('[data-point-detail]')
  56. const text = (selector) => {
  57. const el = card && card.querySelector(selector)
  58. return el ? el.textContent.trim() : ''
  59. }
  60. const tag = (el) =>
  61. el
  62. ? {
  63. label: ((el.querySelector('.status-tag__label') || {}).textContent || '').trim(),
  64. shape: el.dataset.shape || '',
  65. tone: el.dataset.tone || '',
  66. hasShapeSvg: !!el.querySelector('.status-tag__shape'),
  67. }
  68. : null
  69. const dev = window.__cockpitMap
  70. const markers = dev && dev.getPointMarkers ? dev.getPointMarkers() : null
  71. return {
  72. hasCard: !!card,
  73. level: card ? card.dataset.detailLevel : '',
  74. pointCode: card ? card.dataset.pointCode : '',
  75. deviceCode: card ? card.dataset.deviceCode : '',
  76. state: card ? card.dataset.pointDetailState : '',
  77. name: text('[data-point-name]'),
  78. code: text('[data-point-code-value]'),
  79. reason: text('[data-point-reason]'),
  80. count: text('[data-point-device-count]'),
  81. status: tag(card && card.querySelector('[data-point-status]')),
  82. cards: card
  83. ? Array.from(card.querySelectorAll('[data-device-card]')).map((el) => ({
  84. code: el.dataset.deviceCode || '',
  85. status: tag(el.querySelector('.status-tag')),
  86. reason: ((el.querySelector('.point-detail__card-reason') || {}).textContent || '').trim(),
  87. }))
  88. : [],
  89. deviceName: text('[data-device-name]'),
  90. deviceCodeShown: text('[data-device-code-value]'),
  91. hasBack: !!(card && card.querySelector('[data-detail-back]')),
  92. unassociatedNote: text('[data-device-unassociated]'),
  93. deviceBody: text('[data-device-detail-body]'),
  94. // ---- 地图侧 ----
  95. engine: (host && host.dataset.mapEngine) || '',
  96. summaryLoaded: Number((host && host.dataset.mapSummaryLoaded) || 0),
  97. summaryPhase: (host && host.dataset.mapSummary) || '',
  98. dsMarkers: Number((host && host.dataset.mapMarkers) || 0),
  99. dsPlan: Number((host && host.dataset.mapMarkersPlan) || 0),
  100. picked: (host && host.dataset.mapPicked) || '',
  101. entityCount: markers ? markers.onMap : -1,
  102. planMarkers: markers
  103. ? markers.markers.map((item) => ({ code: item.code, kind: item.kind, status: item.status, shape: item.shape, tone: item.tone, label: item.label }))
  104. : [],
  105. cardHosts: document.querySelectorAll('[data-point-detail]').length,
  106. mapHosts: document.querySelectorAll('.cockpit-shell__map .map-canvas').length,
  107. }
  108. })
  109. /**
  110. * 打开页面并**走真实的进入引导**(点一下「点击进入系统」→ 飞到沅陵预设)。
  111. * 不注入 `__COCKPIT_CONFIG__`:注入了就不再拉 `public/cockpit/config.json`,
  112. * 家视角会退化成 (0,0)(配置层把未配置的 home 归一化成 null,`readCoord(null)` 得到 0)。
  113. */
  114. async function openPage(inject) {
  115. const page = await browser.newPage()
  116. const errors = []
  117. page.on('pageerror', (e) => errors.push(String(e && e.message ? e.message : e)))
  118. page.on('console', (m) => {
  119. if (m.type() === 'error') errors.push(`console.error: ${m.text()}`)
  120. })
  121. if (inject) await page.evaluateOnNewDocument(inject)
  122. await page.goto(ENTRY_URL, { waitUntil: 'networkidle2', timeout: 60000 })
  123. await page.waitForSelector('.cockpit-shell', { timeout: 20000 })
  124. await page
  125. .waitForFunction(
  126. () => ['ready', 'failed', 'disabled'].includes(document.querySelector('.cockpit-shell__map .map-canvas')?.dataset.mapEngine),
  127. { timeout: 40000 },
  128. )
  129. .catch(() => {})
  130. // 进入引导:真实用户路径(点一下进入系统)。
  131. if (await page.$('[data-cockpit-intro]')) {
  132. await page.click('[data-cockpit-intro]')
  133. await page.waitForSelector('[data-cockpit-intro]', { hidden: true, timeout: 10000 }).catch(() => {})
  134. }
  135. await sleep(1500)
  136. return { page, errors }
  137. }
  138. /** 点「加载下一页」直到翻完。 */
  139. async function advanceUntilSettled(page, maxSteps = 12) {
  140. for (let step = 0; step < maxSteps; step += 1) {
  141. const state = await readDetail(page)
  142. if (['complete', 'empty', 'failed', 'expired'].includes(state.summaryPhase)) break
  143. const clicked = await page.evaluate(() => {
  144. const button = document.querySelector('[data-map-summary-action]')
  145. if (!button || button.disabled) return false
  146. button.click()
  147. return true
  148. })
  149. if (!clicked) break
  150. const before = state.summaryLoaded
  151. await page
  152. .waitForFunction(
  153. (previous) => {
  154. const host = document.querySelector('.cockpit-shell__map .map-canvas')
  155. const loaded = Number((host && host.dataset.mapSummaryLoaded) || 0)
  156. const phase = (host && host.dataset.mapSummary) || ''
  157. return loaded !== previous || ['failed', 'expired', 'empty', 'complete'].includes(phase)
  158. },
  159. { timeout: 10000 },
  160. before,
  161. )
  162. .catch(() => {})
  163. await sleep(350)
  164. }
  165. return readDetail(page)
  166. }
  167. /** 等相机**真的落在**目标上:阻尼飞行途中中心还在半路,这时点屏幕中心必然点空。 */
  168. async function waitForCameraNear(page, target, timeout = 25000) {
  169. await page
  170. .waitForFunction(
  171. ({ lon, lat }) => {
  172. const dev = window.__cockpitMap
  173. const state = dev && dev.getCameraState ? dev.getCameraState() : null
  174. const shown = state && state.shownPivot
  175. return !!shown && Math.abs(shown.lon - lon) < 2e-4 && Math.abs(shown.lat - lat) < 2e-4
  176. },
  177. { timeout },
  178. { lon: target.lon, lat: target.lat },
  179. )
  180. .catch(() => {})
  181. }
  182. /** 相机中心对准某类标记里的第一个(返回它的标记信息,供断言对照)。 */
  183. async function focusOnMarker(page, kind) {
  184. const target = await page.evaluate((wantedKind) => {
  185. const dev = window.__cockpitMap
  186. const marker = dev.getPointMarkers().markers.find((item) => item.kind === wantedKind)
  187. if (!marker) return null
  188. dev.setCameraFocus({ lon: marker.longitude, lat: marker.latitude, height: 0 })
  189. return { lon: marker.longitude, lat: marker.latitude, code: marker.code, status: marker.status, shape: marker.shape, tone: marker.tone, label: marker.label }
  190. }, kind)
  191. if (target) await waitForCameraNear(page, target)
  192. return target
  193. }
  194. /**
  195. * 在相机中心附近找到一个**真实标记**的屏幕坐标(先挑落点,再真点)。
  196. *
  197. * 用引擎拾取来找:它和真实点击走的是同一个 `scene.pick`,所以找到的坐标点下去必然命中;
  198. * 「地形高程差」与「阻尼滞后」都因此不再是问题。每个候选点先确认鼠标落点确实是画布
  199. * (浮层/图例盖住的地方不点),命中即返回。
  200. */
  201. async function locateMarker(page, kind, radius = 300, stride = 10) {
  202. return page.evaluate(
  203. ([wantedKind, max, step]) => {
  204. const dev = window.__cockpitMap
  205. const canvas = document.querySelector('.cockpit-shell__map canvas')
  206. const center = dev.projectCameraPivot()
  207. if (!center || !canvas) return { x: 0, y: 0, code: '', kind: '', center }
  208. for (let dy = -max; dy <= max; dy += step) {
  209. for (let dx = -max; dx <= max; dx += step) {
  210. const x = Math.round(center.x + dx)
  211. const y = Math.round(center.y + dy)
  212. const hitElement = document.elementFromPoint(x, y)
  213. if (!hitElement || (hitElement !== canvas && !canvas.contains(hitElement))) continue
  214. const hit = dev.pickPoint(x, y)
  215. if (hit && hit.kind === wantedKind) return { x, y, dx, dy, code: hit.code, kind: hit.kind, center }
  216. }
  217. }
  218. return { x: 0, y: 0, dx: 0, dy: 0, code: '', kind: '', center }
  219. },
  220. [kind, radius, stride],
  221. )
  222. }
  223. /** 关掉可能已经被「找点」打开的浮层,好让真实点击成为唯一证据。 */
  224. async function closeCard(page) {
  225. if (await page.$('[data-detail-close]')) {
  226. await page.click('[data-detail-close]')
  227. await sleep(250)
  228. }
  229. }
  230. try {
  231. // ================================================================ A-F 顺序样例
  232. {
  233. const { page, errors } = await openPage(() => {
  234. window.__COCKPIT_MOCK_SCENARIO__ = 'point-detail-order'
  235. window.__COCKPIT_MOCK_MAP__ = { pageSize: 2 }
  236. })
  237. const settled = await advanceUntilSettled(page)
  238. check('A1-engine-ready', ['ready', 'disabled'].includes(settled.engine), `engine=${settled.engine}`)
  239. check(
  240. 'A2-markers-on-map',
  241. settled.dsPlan === 2 && settled.dsMarkers === 2 && settled.entityCount === 2,
  242. `计划 ${settled.dsPlan} · dataset ${settled.dsMarkers} · 实体 ${settled.entityCount}`,
  243. )
  244. check('A3-no-card-before-click', settled.cardHosts === 0, `点之前浮层 ${settled.cardHosts} 个`)
  245. // ---------- F 拖拽不误触发(票 03 的左键平移不能被新功能打坏)----------
  246. const pivot = await page.evaluate(() => {
  247. const dev = window.__cockpitMap
  248. return dev && dev.projectCameraPivot ? dev.projectCameraPivot() : null
  249. })
  250. if (pivot) {
  251. await page.mouse.move(pivot.x - 70, pivot.y)
  252. await page.mouse.down()
  253. await page.mouse.move(pivot.x + 70, pivot.y, { steps: 8 })
  254. await page.mouse.up()
  255. }
  256. await sleep(500)
  257. const afterDrag = await readDetail(page)
  258. check(
  259. 'F1-drag-does-not-open-detail',
  260. afterDrag.cardHosts === 0 && afterDrag.picked === '',
  261. `拖拽 140px 后浮层 ${afterDrag.cardHosts} 个 · data-map-picked「${afterDrag.picked}」`,
  262. )
  263. // ---------- B 真实鼠标点中一个监测点标记 → 点位摘要 ----------
  264. const target = await focusOnMarker(page, 'monitoring-point')
  265. const located = await locateMarker(page, 'monitoring-point')
  266. await closeCard(page)
  267. const beforeClick = await readDetail(page)
  268. if (located.code) {
  269. await page.mouse.click(located.x, located.y)
  270. await sleep(600)
  271. }
  272. const detail = await readDetail(page)
  273. check(
  274. 'B0-marker-locatable-by-engine-pick',
  275. !!target && !!located.code && detail.planMarkers.some((item) => item.code === located.code && item.kind === 'monitoring-point'),
  276. `相机对准 ${target && target.code},引擎在中心偏移 ${located.dx},${located.dy}px 处拾到 ${located.code}(两个点位只隔约 7px,最上面那个先被拾到;偏移量就是贴地标记与中心的高程差)`,
  277. )
  278. check(
  279. 'B1-real-click-opens-summary',
  280. !!located.code && beforeClick.cardHosts === 0 && detail.hasCard && detail.level === 'point' && detail.state === 'success',
  281. located.code
  282. ? `真实鼠标点 (${located.x},${located.y}) → 层级 ${detail.level} · 分区 ${detail.state}`
  283. : '没找到可点的标记',
  284. )
  285. check(
  286. 'B2-card-is-the-picked-point',
  287. !!located.code && detail.picked === `monitoring-point|${located.code}` && detail.pointCode === located.code,
  288. `dataset 拾到「${detail.picked}」· 浮层显示 ${detail.pointCode}`,
  289. )
  290. // 摘要五要素:名称 / 编码 / 状态 / 原因 / 关联设备数量。
  291. check(
  292. 'B3-summary-five-fields',
  293. !!detail.name && detail.code === detail.pointCode && !!detail.status && !!detail.status.label && detail.reason === '流量超高' && detail.count === '3',
  294. `名称「${detail.name}」编码「${detail.code}」状态「${detail.status && detail.status.label}」原因「${detail.reason}」关联设备 ${detail.count}`,
  295. )
  296. check(
  297. 'B4-status-tag-has-shape-and-text',
  298. !!detail.status && detail.status.hasShapeSvg && !!detail.status.shape && !!detail.status.tone && !!detail.status.label,
  299. `状态标签 ${detail.status && detail.status.tone}/${detail.status.shape}/${detail.status.label}(不只靠颜色)`,
  300. )
  301. // 状态标签必须与**地图上那个标记**同源:形状、色键、文字逐个对齐。
  302. const marker = detail.planMarkers.find((item) => item.code === detail.pointCode) || null
  303. check(
  304. 'B5-card-status-matches-map-marker',
  305. !!marker && !!detail.status && marker.shape === detail.status.shape && marker.tone === detail.status.tone && marker.label === detail.status.label,
  306. marker && detail.status
  307. ? `标记 ${marker.tone}/${marker.shape}/${marker.label} vs 浮层 ${detail.status.tone}/${detail.status.shape}/${detail.status.label}`
  308. : '浮层里的点位在地图标记计划里',
  309. )
  310. // ---------- C 设备卡片:顺序 = 后端返回顺序(样例刻意逆状态优先级)----------
  311. const ORDER = ['EQ-ORDER-NORMAL', 'EQ-ORDER-ALARM', 'EQ-ORDER-WARNING']
  312. check(
  313. 'C1-device-card-order-is-backend-order',
  314. detail.cards.map((card) => card.code).join(',') === ORDER.join(','),
  315. `卡片顺序 ${detail.cards.map((card) => card.code).join(' → ')}(后端给的就是这个顺序:正常 → 报警 → 预警)`,
  316. )
  317. check(
  318. 'C2-every-card-has-status-tag',
  319. detail.cards.length === 3 && detail.cards.every((card) => card.status && card.status.hasShapeSvg && !!card.status.label),
  320. detail.cards.map((card) => `${card.code}:${card.status && card.status.label}`).join(' · '),
  321. )
  322. check('C3-card-count-equals-summary-count', detail.cards.length === Number(detail.count), `摘要 ${detail.count} 台 = 卡片 ${detail.cards.length} 张`)
  323. // ---------- E 关联设备不重复上图 ----------
  324. const deviceCodes = detail.cards.map((card) => card.code)
  325. check(
  326. 'E1-markers-unchanged-after-detail',
  327. detail.dsMarkers === settled.dsMarkers && detail.entityCount === settled.entityCount && detail.planMarkers.length === settled.planMarkers.length,
  328. `打开详情前后:dataset ${settled.dsMarkers} → ${detail.dsMarkers} · 实体 ${settled.entityCount} → ${detail.entityCount}`,
  329. )
  330. check(
  331. 'E2-associated-device-not-a-marker',
  332. deviceCodes.length === 3 && deviceCodes.every((code) => !detail.planMarkers.some((item) => item.code === code)),
  333. `设备 ${deviceCodes.join(', ')} 都不在标记计划里(地图上只有 ${detail.planMarkers.map((item) => item.code).join(', ')})`,
  334. )
  335. check('E3-picked-recorded-on-host', detail.picked === `monitoring-point|${detail.pointCode}`, `data-map-picked=「${detail.picked}」`)
  336. check('E4-single-card-host', detail.cardHosts === 1 && detail.mapHosts === 1, `浮层 ${detail.cardHosts} 个 · 地图宿主 ${detail.mapHosts} 个`)
  337. // ---------- D 选卡片进第二级 → 返回 → 关闭 ----------
  338. await page.click('[data-device-card="0"]')
  339. await sleep(300)
  340. const second = await readDetail(page)
  341. check(
  342. 'D1-select-card-enters-device-level',
  343. second.level === 'equipment' && second.deviceCode === ORDER[0] && second.deviceCodeShown === ORDER[0] && !!second.deviceName,
  344. `层级 ${second.level} · 设备 ${second.deviceCodeShown}「${second.deviceName}」`,
  345. )
  346. check('D2-back-button-present', second.hasBack === true && !!second.deviceBody, `第二级有「返回点位」· 分区区「${second.deviceBody}」`)
  347. await page.click('[data-detail-back]')
  348. await sleep(300)
  349. const back = await readDetail(page)
  350. check(
  351. 'D3-back-returns-to-point-level',
  352. back.level === 'point' && back.pointCode === detail.pointCode && back.cards.length === 3,
  353. `返回后层级 ${back.level} · 点位 ${back.pointCode} · 卡片 ${back.cards.length} 张`,
  354. )
  355. await page.click('[data-detail-close]')
  356. await sleep(300)
  357. const closed = await readDetail(page)
  358. check(
  359. 'D4-close-hides-card-without-touching-map',
  360. closed.cardHosts === 0 && closed.dsMarkers === detail.dsMarkers && closed.entityCount === detail.entityCount,
  361. `浮层 ${closed.cardHosts} 个 · 标记仍 ${closed.dsMarkers} 个`,
  362. )
  363. check('D5-no-page-errors', errors.length === 0, JSON.stringify(errors.slice(0, 3)))
  364. await page.close()
  365. }
  366. // ================================================================ G 未关联设备直接进设备详情
  367. {
  368. const { page, errors } = await openPage(() => {
  369. window.__COCKPIT_MOCK_SCENARIO__ = 'point-statuses'
  370. window.__COCKPIT_MOCK_MAP__ = { pageSize: 10 }
  371. })
  372. await advanceUntilSettled(page)
  373. const target = await focusOnMarker(page, 'unassociated-equipment')
  374. const located = await locateMarker(page, 'unassociated-equipment')
  375. await closeCard(page)
  376. const beforeClick = await readDetail(page)
  377. if (located.code) {
  378. await page.mouse.click(located.x, located.y)
  379. await sleep(600)
  380. }
  381. const detail = await readDetail(page)
  382. check(
  383. 'G1-unassociated-device-enters-device-level',
  384. !!located.code && beforeClick.cardHosts === 0 && detail.hasCard && detail.level === 'equipment' && detail.pointCode === '' && detail.deviceCode === located.code,
  385. located.code ? `点中未关联设备 ${detail.deviceCode} → 层级 ${detail.level}(没有点位可摘要)` : '没找到可点的未关联设备标记',
  386. )
  387. check('G2-unassociated-note-visible', !!detail.unassociatedNote && !!detail.deviceBody, `提示「${detail.unassociatedNote}」· 分区区「${detail.deviceBody}」`)
  388. check('G3-no-back-button', detail.hasBack === false, '没有点位可回时不显示「返回点位」')
  389. check('G4-no-page-errors', errors.length === 0, JSON.stringify(errors.slice(0, 3)))
  390. await page.close()
  391. }
  392. // ================================================================ H 默认样例不受影响
  393. {
  394. const { page, errors } = await openPage(() => {
  395. window.__COCKPIT_MOCK_MAP__ = { pageSize: 2 }
  396. })
  397. const settled = await advanceUntilSettled(page)
  398. // 默认样例:3 点 + 2 未关联设备 = 5 个对象、其中 2 个可上图(票 07/08 的数)。
  399. check(
  400. 'H1-default-sample-unchanged',
  401. settled.summaryLoaded === 5 && settled.dsPlan === 2 && settled.entityCount === 2,
  402. `已加载 ${settled.summaryLoaded} · 计划 ${settled.dsPlan} · 实体 ${settled.entityCount}(票 07/08 的数)`,
  403. )
  404. check('H2-no-page-errors', errors.length === 0, JSON.stringify(errors.slice(0, 3)))
  405. await page.close()
  406. }
  407. } finally {
  408. await browser.close()
  409. }
  410. const failed = results.filter((item) => !item.pass)
  411. console.log(`\nT10 走查:${results.length - failed.length}/${results.length} 项通过${failed.length ? `,未通过:${failed.map((r) => r.id).join(', ')}` : ''}`)
  412. process.exit(failed.length ? 1 : 0)