| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219 |
- /**
- * T05 真机走查:领域适配层在浏览器里确实成立(票 05)。
- *
- * 逻辑脚本(t05-adapter.mjs)已经把转换语义、mock 五类样例与分区隔离测透了;
- * 这里只补**只有在真机才成立**的部分 —— 也就是票 05 验收第 6 条的渲染侧原句:
- * 「401 或凭据类失败只把受影响分区标为数据不可用,保留已成功分区与地图骨架」。
- * 纯逻辑断言证明不了「地图骨架还在」:那要求页面真的渲染出来、且 Cesium 宿主没被拆掉。
- *
- * 运行(需先起 `yarn dev`,服务在 http://localhost:8000/;改了懒加载模块先重启服务):
- * node .scratch/cockpit-shell-panel-layout/verify/t05-adapter-walkthrough.mjs
- */
- 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 = []
- function check(id, desc, pass, evidence) {
- results.push({ id, desc, pass })
- console.log(`${pass ? 'PASS' : 'FAIL'} ${id} ${desc}${evidence ? ` — ${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-t05-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()}`)
- /**
- * 开一页并等分区状态板就绪。
- * `scenario` / `onlyPath` 通过宿主全局键注入(mocks/index.js 读它们),
- * 因此走查不必改代码或重新构建就能演示五类样例。
- */
- async function openPage(inject) {
- 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(() => {
- // 引导层会盖住页面,走查不需要它(票 30 的开关)。
- window.__COCKPIT_CONFIG__ = { dataSource: 'mock', map: { intro: { enabled: false } } }
- })
- if (inject) await page.evaluateOnNewDocument(inject)
- await page.goto(ENTRY_URL, { waitUntil: 'networkidle2', timeout: 60000 })
- await page.waitForSelector('.cockpit-shell', { timeout: 20000 })
- await page.waitForSelector('[data-section-board]', { timeout: 20000 }).catch(() => {})
- // 等分区板从「未加载」变成有内容(请求 + 转换 + 渲染)。
- await page
- .waitForFunction(() => document.querySelectorAll('[data-section-board] [data-section-state]').length >= 3, { timeout: 20000 })
- .catch(() => {})
- // 引擎要拉静态资源/地形,比分区取数慢:等它落到终态再断言,否则会把 booting 误判成失败。
- // (t04/t29 的走查同样先等 dataset.mapEngine 变成终态。)
- await page
- .waitForFunction(
- () => ['ready', 'failed', 'disabled'].includes(document.querySelector('.cockpit-shell__map .map-canvas')?.dataset.mapEngine),
- { timeout: 30000 },
- )
- .catch(() => {})
- await sleep(400)
- return { page, errors }
- }
- const readBoard = (page) =>
- page.evaluate(() => {
- const tags = Array.from(document.querySelectorAll('[data-section-board] [data-section-state]'))
- const host = document.querySelector('.cockpit-shell__map .map-canvas')
- return {
- sections: tags.map((t) => ({
- name: t.dataset.sectionName,
- state: t.dataset.sectionState,
- tone: t.dataset.sectionTone,
- retained: t.dataset.sectionRetained,
- text: t.textContent.trim(),
- })),
- engine: (host && host.dataset.mapEngine) || '',
- mapHostCount: document.querySelectorAll('.cockpit-shell__map .map-canvas').length,
- shellPresent: !!document.querySelector('.cockpit-shell'),
- panels: document.querySelectorAll('.cockpit-panel').length,
- bodyText: document.body.innerText,
- href: location.href,
- }
- })
- try {
- // ---------- A1 默认(成功样例):三条共享分区都就绪 ----------
- {
- const { page, errors } = await openPage()
- const board = await readBoard(page)
- check(
- 'A1-success-sections',
- '默认 mock:地图摘要/设备详情/数据新鲜度三条共享分区都取到数据并标为已就绪',
- board.sections.length === 3 && board.sections.every((s) => s.state === 'success' && s.tone === 'lv4'),
- JSON.stringify(board.sections),
- )
- check(
- 'A1-map-skeleton-alive',
- '地图骨架常驻:Cesium 引擎就绪、宿主只有一个(分区取数不影响地图)',
- board.engine === 'ready' && board.mapHostCount === 1 && board.panels === 4,
- `engine=${board.engine} hosts=${board.mapHostCount} panels=${board.panels}`,
- )
- check('A1-no-page-errors', '默认态无页面级报错', errors.length === 0, JSON.stringify(errors.slice(0, 3)))
- await page.close()
- }
- // ---------- A2 空数据:分区显示「无数据」,不是「失败」 ----------
- {
- const { page } = await openPage(() => {
- window.__COCKPIT_MOCK_SCENARIO__ = 'empty'
- })
- const board = await readBoard(page)
- check(
- 'A2-empty-sections',
- '空数据样例:设备详情与新鲜度显示「无数据」(中性色),不误报成失败',
- board.sections.length === 3 &&
- board.sections.filter((s) => s.state === 'empty').length >= 2 &&
- board.sections.every((s) => s.state !== 'failed' && s.state !== 'unavailable'),
- JSON.stringify(board.sections),
- )
- await page.close()
- }
- // ---------- A3 过期:保留旧值但带过期标识 ----------
- {
- const { page } = await openPage(() => {
- window.__COCKPIT_MOCK_SCENARIO__ = 'stale'
- })
- const board = await readBoard(page)
- const fresh = board.sections.find((s) => s.name === 'equipmentFreshness')
- check(
- 'A3-stale-flagged',
- '过期样例:新鲜度分区显示「数据已过期」并带 retained 标识(不得伪装成实时)',
- !!fresh && fresh.state === 'stale' && fresh.tone === 'lv2' && fresh.retained === 'true' && fresh.text.includes('过期'),
- JSON.stringify(fresh),
- )
- await page.close()
- }
- // ---------- A4 失败:分区失败但地图与其它分区照常 ----------
- {
- const { page } = await openPage(() => {
- window.__COCKPIT_MOCK_SCENARIO__ = 'failed'
- })
- const board = await readBoard(page)
- check(
- 'A4-failed-keeps-skeleton',
- '失败样例:分区标为加载失败,外壳、四个面板与地图骨架全部保留',
- board.sections.every((s) => s.state === 'failed') &&
- board.shellPresent &&
- board.panels === 4 &&
- board.mapHostCount === 1 &&
- board.engine === 'ready',
- JSON.stringify({ sections: board.sections, engine: board.engine, panels: board.panels }),
- )
- await page.close()
- }
- // ---------- A5 票 05 验收第 6 条:401 只标受影响分区 ----------
- {
- const { page, errors } = await openPage(() => {
- window.__COCKPIT_MOCK_SCENARIO__ = 'unauthorized'
- // 只让「设备详情聚合」这一个分区凭据失败。必须细到该端点:
- // 若用 `/equipments/` 前缀,新鲜度分区会被一起打掉,就证不了「只标受影响分区」。
- window.__COCKPIT_MOCK_ONLY__ = '/equipments/*/detail-aggregate'
- })
- const board = await readBoard(page)
- const affected = board.sections.find((s) => s.name === 'equipmentAggregate')
- const healthy = board.sections.filter((s) => s.name !== 'equipmentAggregate')
- check(
- 'A5-401-marks-affected-only',
- '401/凭据类失败:只有受影响分区标为「数据不可用」(红色),其余分区保持已就绪',
- !!affected &&
- affected.state === 'unavailable' &&
- affected.tone === 'lv1' &&
- affected.text.includes('数据不可用') &&
- healthy.length === 2 &&
- healthy.every((s) => s.state === 'success'),
- JSON.stringify({ affected, healthy }),
- )
- check(
- 'A5-map-skeleton-preserved',
- '同一次 401:地图骨架与外壳未被牵连(引擎就绪、宿主与面板都在)',
- board.engine === 'ready' && board.mapHostCount === 1 && board.panels === 4 && board.shellPresent,
- `engine=${board.engine} hosts=${board.mapHostCount} panels=${board.panels}`,
- )
- check(
- 'A5-no-login-redirect',
- '凭据类失败不跳登录页:URL 不变、页面里没有登录入口(ADR 0005)',
- board.href === ENTRY_URL && !/登录|login/i.test(board.bodyText),
- JSON.stringify({ href: board.href }),
- )
- check('A5-no-page-errors', '401 场景无页面级报错(失败被收敛成分区状态,没有抛出)', errors.length === 0, JSON.stringify(errors.slice(0, 3)))
- await page.close()
- }
- } finally {
- await browser.close()
- }
- const failed = results.filter((r) => !r.pass)
- console.log('')
- console.log(`T05 走查:${results.length - failed.length}/${results.length} 项通过。`)
- if (failed.length) {
- console.log('未通过:' + failed.map((f) => f.id).join(', '))
- process.exit(1)
- }
- console.log('T05 走查全部通过。')
|