| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202 |
- /**
- * T04 真机走查:配置三态、四层来源与「凭据不进界面」在浏览器里确实成立(票 04)。
- *
- * 逻辑脚本(t04-config.mjs)用假 fetch 把语义测透了,这里只补浏览器里才成立的部分:
- * · 默认(仓库里没有 cockpit/config.json)= 缺失态,用内置 mock;
- * · 宿主注入 `window.__COCKPIT_CONFIG__` = 成功态,且**令牌不出现在 DOM 里**;
- * · 运行时配置文件(走 HTTP 真拉一份 JSON)= 成功态,source 记成 file;
- * · 配置文件坏掉 = 失败态,顶栏说「数据不可用」,且**不发生任何登录跳转**;
- * · `map.cesium.enabled=false` 确实让中央一张图退回临时影像。
- *
- * 运行(需先起 `yarn dev`,服务在 http://localhost:8000/;改了引擎/懒加载模块先重启服务):
- * node .scratch/cockpit-shell-panel-layout/verify/t04-config-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'
- /** 走查用的假令牌:它只要出现在 DOM 里就说明凭据泄漏到了界面层。 */
- const SECRET = 'walkthrough-readonly-token'
- 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-t04-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()}`)
- /** 按注入脚本开一页,等外壳与配置标签就绪。 */
- 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()}`)
- })
- if (inject) await page.evaluateOnNewDocument(inject)
- await page.goto(ENTRY_URL, { waitUntil: 'networkidle2', timeout: 60000 })
- await page.waitForSelector('.cockpit-shell', { timeout: 20000 })
- await page
- .waitForFunction(() => !!document.querySelector('.cockpit-shell__config'), { timeout: 20000 })
- .catch(() => {})
- await sleep(300)
- return { page, errors }
- }
- const readState = (page) =>
- page.evaluate(() => {
- const indicator = document.querySelector('.cockpit-shell__config')
- const tag = indicator ? indicator.querySelector('.status-tag') : null
- const host = document.querySelector('.cockpit-shell__map .map-canvas')
- return {
- state: (indicator && indicator.dataset.configState) || '',
- dataSource: (indicator && indicator.dataset.dataSource) || '',
- source: (indicator && indicator.dataset.source) || '',
- tone: (tag && tag.dataset.tone) || '',
- label: indicator ? indicator.textContent.trim() : '',
- engine: (host && host.dataset.mapEngine) || '',
- canvases: host ? host.querySelectorAll('canvas').length : -1,
- photoLayers: host ? host.querySelectorAll('.map-canvas__photo').length : -1,
- bodyText: document.body.innerText,
- htmlHasSecret: document.documentElement.outerHTML.includes('walkthrough-readonly-token'),
- href: location.href,
- }
- })
- try {
- // ---------- A1 默认:没有配置文件 = 缺失态,内置 mock ----------
- {
- // 本机 .env.local 可能已打开配置文件层(指向票 21 的本地 GIS);本条测的是「没有运行时配置」
- // 这个用例,所以显式给空串关掉文件层(宿主键优先于构建期环境变量)。
- const { page, errors } = await openPage(() => {
- window.__COCKPIT_CONFIG_URL__ = ''
- })
- const state = await readState(page)
- check(
- 'A1-config-missing-default',
- '仓库里没有 cockpit/config.json 时:缺失态 + 内置 mock,外壳照常渲染',
- state.state === 'missing' &&
- state.dataSource === 'mock' &&
- state.source === 'defaults' &&
- state.label.includes('配置缺失') &&
- state.engine === 'ready',
- JSON.stringify({ ...state, bodyText: undefined, htmlHasSecret: undefined }),
- )
- check('A1-no-page-errors', '默认态无页面级报错(配置缺失不该刷错误)', errors.length === 0, JSON.stringify(errors.slice(0, 3)))
- await page.close()
- }
- // ---------- A2 宿主注入 = 成功态;令牌不得进界面 ----------
- {
- const { page, errors } = await openPage(() => {
- window.__COCKPIT_CONFIG__ = {
- dataSource: 'real',
- api: { baseUrl: '/api/cockpit/v1', token: 'walkthrough-readonly-token' },
- ws: { url: 'ws://localhost:8000/ws/deviceStatus' },
- }
- })
- const state = await readState(page)
- check(
- 'A2-config-injected-ready',
- '宿主注入 window.__COCKPIT_CONFIG__:成功态、数据源 real、来源记成 window',
- state.state === 'ready' && state.dataSource === 'real' && state.source === 'window' && state.label.includes('real'),
- JSON.stringify({ state: state.state, source: state.source, label: state.label }),
- )
- check(
- 'A2-credential-not-in-dom',
- '配置里的令牌不出现在 DOM/顶栏(页面与 store 不碰凭据)',
- state.htmlHasSecret === false && !state.bodyText.includes(SECRET),
- `htmlHasSecret=${state.htmlHasSecret}`,
- )
- check('A2-no-page-errors', '注入成功态无页面级报错', errors.length === 0, JSON.stringify(errors.slice(0, 3)))
- await page.close()
- }
- // ---------- A3 运行时配置文件(真 HTTP 拉一份 JSON)= 成功态 ----------
- {
- const { page } = await openPage(() => {
- window.__COCKPIT_CONFIG_URL__ = 'cockpit/config.example.json'
- })
- const state = await readState(page)
- check(
- 'A3-config-file-ready',
- '部署配置文件走 HTTP 真正加载:成功态、来源记成 file、地址可观测',
- state.state === 'ready' && state.source === 'file' && state.dataSource === 'mock',
- JSON.stringify({ state: state.state, source: state.source, label: state.label }),
- )
- await page.close()
- }
- // ---------- A4 配置文件坏掉 = 失败态,且绝不跳登录页 ----------
- {
- const { page } = await openPage(() => {
- // aerial.webp 是真文件但不是 JSON:模拟「部署放了错东西」。
- window.__COCKPIT_CONFIG_URL__ = 'cockpit/aerial.webp'
- })
- const state = await readState(page)
- check(
- 'A4-config-failed',
- '配置文件不可用:失败态、顶栏红色「数据不可用」、仍给出可跑的内置 mock',
- state.state === 'failed' &&
- state.tone === 'lv1' &&
- state.label.includes('数据不可用') &&
- state.photoLayers === 1,
- JSON.stringify({ state: state.state, tone: state.tone, label: state.label }),
- )
- check(
- 'A4-no-login-redirect',
- '失败时不跳登录页:URL 不变、页面里没有登录入口(ADR 0005)',
- state.href === ENTRY_URL && !/登录|login/i.test(state.bodyText),
- JSON.stringify({ href: state.href }),
- )
- await page.close()
- }
- // ---------- A5 Cesium 确实可由配置关闭 ----------
- {
- const { page } = await openPage(() => {
- window.__COCKPIT_CONFIG__ = { map: { cesium: { enabled: false } } }
- })
- await page
- .waitForFunction(
- () => document.querySelector('.cockpit-shell__map .map-canvas')?.dataset.mapEngine === 'disabled',
- { timeout: 20000 },
- )
- .catch(() => {})
- const state = await readState(page)
- check(
- 'A5-cesium-disabled-by-config',
- 'map.cesium.enabled=false:引擎不启动,中央区域保留临时影像兜底',
- state.state === 'ready' && state.engine === 'disabled' && state.canvases === 0 && state.photoLayers === 1,
- JSON.stringify({ state: state.state, engine: state.engine, canvases: state.canvases, photoLayers: state.photoLayers }),
- )
- await page.close()
- }
- } finally {
- await browser.close()
- }
- const failed = results.filter((r) => !r.pass)
- console.log('')
- console.log(`T04 走查:${results.length - failed.length}/${results.length} 项通过。`)
- if (failed.length) {
- console.log('未通过:' + failed.map((f) => f.id).join(', '))
- process.exit(1)
- }
- console.log('T04 走查全部通过。')
|