| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328 |
- /**
- * T08 真机走查:地图摘要分页与快照在浏览器里确实成立(票 08)。
- *
- * 逻辑脚本(t08-map-summary.mjs)已经把合并、装载器状态机与分区映射测透了;这里只补
- * **只有在真机才成立**的部分 —— 票 08 验收第 1 条原句:
- * 「地图先显示框架,再按分页批次增加摘要点位」。
- * 纯逻辑断言证明不了「框架先于点位出现」:那要求页面真的渲染出来、地图宿主与 Cesium 引擎先就绪。
- *
- * 运行(需先起 `yarn dev`,服务在 http://localhost:8000/;改了懒加载模块先重启服务):
- * node .scratch/cockpit-shell-panel-layout/verify/t08-map-summary-walkthrough.mjs
- *
- * 演示用的开关(都由宿主全局键注入,不改代码重新构建):
- * `window.__COCKPIT_MOCK_SCENARIO__` / `window.__COCKPIT_MOCK_MAP__`
- * 人工演示同一件事:控制台里 `__cockpitMap.mapSummary.advance()` 一页页推进。
- */
- 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 = []
- /**
- * `check(id, pass, evidence)` —— 注意参数顺序:第二参是**断言结果**,不是描述。
- * (踩过一次:签名多了一个 `desc`,结果每个断言都把证据字符串当成了"通过",走查假绿 11/11。)
- */
- function check(id, pass, evidence) {
- results.push({ id, pass })
- const rest = evidence === undefined ? '' : String(evidence)
- console.log(`${pass ? 'PASS' : 'FAIL'} ${id} :: ${rest}`)
- }
- 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-t08-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()}`)
- /** 读地图宿主上的摘要状态(断言不读中文文案,读 dataset)。 */
- const readSummary = (page) =>
- page.evaluate(() => {
- const host = document.querySelector('.cockpit-shell__map .map-canvas')
- const action = document.querySelector('[data-map-summary-action]')
- return {
- phase: (host && host.dataset.mapSummary) || '',
- loaded: Number((host && host.dataset.mapSummaryLoaded) || 0),
- total: Number((host && host.dataset.mapSummaryTotal) || 0),
- snapshot: (host && host.dataset.mapSummarySnapshot) || '',
- page: Number((host && host.dataset.mapSummaryPage) || 0),
- engine: (host && host.dataset.mapEngine) || '',
- actionLabel: action ? action.textContent.trim() : '',
- actionDisabled: action ? action.disabled : null,
- label: (document.querySelector('[data-map-summary-label]') || {}).textContent || '',
- board: Array.from(document.querySelectorAll('[data-section-board] [data-section-state]')).map((t) => ({
- name: t.dataset.sectionName,
- state: t.dataset.sectionState,
- retained: t.dataset.sectionRetained,
- })),
- hosts: document.querySelectorAll('.cockpit-shell__map .map-canvas').length,
- }
- })
- 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
- .waitForFunction(
- () => ['ready', 'failed', 'disabled'].includes(document.querySelector('.cockpit-shell__map .map-canvas')?.dataset.mapEngine),
- { timeout: 30000 },
- )
- .catch(() => {})
- // 外壳挂载后会自动取第 1 批(票 08 的"再按分页批次增加"):等它落定再断言,
- // 否则会把"正在取第 1 批"误判成"框架就绪但一个点都没有"。
- await waitForFirstBatch(page)
- await sleep(300)
- return { page, errors }
- }
- /** 点「加载下一页」并等**已上图对象数真的变了**(不知增量,所以等"变化"而不是等某个值)。 */
- async function clickAndWaitChange(page, previousLoaded, timeout = 8000) {
- const clicked = await page.evaluate(() => {
- const button = document.querySelector('[data-map-summary-action]')
- if (!button || button.disabled) return false
- button.click()
- return true
- })
- if (!clicked) return false
- await page
- .waitForFunction(
- (before) => {
- const host = document.querySelector('.cockpit-shell__map .map-canvas')
- const loaded = Number((host && host.dataset.mapSummaryLoaded) || 0)
- const phase = (host && host.dataset.mapSummary) || ''
- return loaded !== before || phase === 'failed' || phase === 'expired'
- },
- { timeout },
- previousLoaded,
- )
- .catch(() => {})
- return true
- }
- /** 等外壳自动取完第 1 批(票 08:地图挂载后自己先取一批,不等用户点)。 */
- async function waitForFirstBatch(page, timeout = 15000) {
- await page
- .waitForFunction(
- () => {
- const host = document.querySelector('.cockpit-shell__map .map-canvas')
- const loaded = Number((host && host.dataset.mapSummaryLoaded) || 0)
- const phase = (host && host.dataset.mapSummary) || ''
- return loaded > 0 || phase === 'empty' || phase === 'failed' || phase === 'expired'
- },
- { timeout },
- )
- .catch(() => {})
- }
- /** 一批一批点,直到不再有下一页(完成/空/失败/失效)或点不动了。返回每一步的状态。 */
- async function advanceUntilSettled(page, maxSteps = 10) {
- const series = [await readSummary(page)]
- for (let step = 0; step < maxSteps; step += 1) {
- const current = series[series.length - 1]
- if (['complete', 'empty', 'failed', 'expired'].includes(current.phase)) break
- if (!(await clickAndWaitChange(page, current.loaded))) break
- series.push(await readSummary(page))
- }
- return series
- }
- try {
- // ---------- A 「地图先显示框架,再按分页批次增加摘要点位」----------
- {
- const { page, errors } = await openPage(() => {
- window.__COCKPIT_MOCK_MAP__ = { pageSize: 2 }
- })
- const first = await readSummary(page)
- // 外壳在挂载后自动取第 1 批(票 08 原句的"再按分页批次增加"):首屏就应看到框架 + 第 1 批,
- // 而不是等全部取完。`loaded < total` 正是"渐进"的判据。
- check(
- 'A1-frame-before-points',
- ['ready', 'disabled'].includes(first.engine) && first.phase !== 'failed' && first.loaded > 0 && first.loaded < first.total,
- JSON.stringify({ engine: first.engine, phase: first.phase, loaded: first.loaded, total: first.total }),
- )
- check('A1-host-alive', first.hosts === 1, `地图宿主 ${first.hosts} 个`)
- const series = await advanceUntilSettled(page)
- const loadedSeries = series.map((item) => item.loaded)
- const snapshots = new Set(series.map((item) => item.snapshot).filter(Boolean))
- const last = series[series.length - 1]
- check(
- 'A2-progressive-batches',
- loadedSeries.join(',') === '2,4,5' && last.phase === 'complete' && last.total === 5,
- `已上图序列 ${loadedSeries.join(' → ')} · 总数 ${last.total} · 终态 ${last.phase}`,
- )
- check(
- 'A3-one-snapshot-per-load',
- snapshots.size === 1 && [...snapshots][0] !== '',
- `快照 ${[...snapshots].join(', ')}`,
- )
- check(
- 'A4-batch-count-consistent',
- series.length === 3 && loadedSeries[0] === 2 && loadedSeries[1] === 4,
- `批次数 ${series.length} · 序列 ${loadedSeries.join(' → ')}`,
- )
- // 取完之后按钮从「加载下一页」变成「刷新快照」:完成态是终态,再点就是换新快照。
- check(
- 'A5-complete-switches-action',
- last.actionLabel.includes('刷新') && first.actionLabel.includes('加载'),
- `按钮:${first.actionLabel} → ${last.actionLabel}`,
- )
- check('A6-no-page-errors', errors.length === 0, JSON.stringify(errors.slice(0, 3)))
- await page.close()
- }
- // ---------- B 刷新切换到新快照,旧快照的点不留在图上 ----------
- {
- const { page } = await openPage(() => {
- window.__COCKPIT_MOCK_MAP__ = { pageSize: 2 }
- })
- const series = await advanceUntilSettled(page)
- const completed = series[series.length - 1]
- // 完成态下点这个按钮 = `mapStore.refresh()`:丢掉当前快照,从第 1 页重来。
- await clickAndWaitChange(page, completed.loaded)
- await sleep(400)
- const refreshed = await readSummary(page)
- check(
- 'B1-refresh-back-to-page-1',
- refreshed.page === 1 && refreshed.loaded === 2 && refreshed.phase !== 'expired' && refreshed.phase !== 'failed',
- JSON.stringify({ page: refreshed.page, loaded: refreshed.loaded, phase: refreshed.phase, snapshot: refreshed.snapshot }),
- )
- check(
- 'B2-old-snapshot-not-accumulated',
- refreshed.loaded < completed.loaded,
- `完成时 ${completed.loaded} → 刷新后 ${refreshed.loaded}(旧快照的点没有留下来)`,
- )
- await page.close()
- }
- // ---------- C 失败:分区标失败,地图框架与已上图点位保留 ----------
- {
- const { page } = await openPage(() => {
- window.__COCKPIT_MOCK_SCENARIO__ = 'failed'
- window.__COCKPIT_MOCK_MAP__ = { pageSize: 2 }
- })
- const failed = await readSummary(page)
- const mapSection = failed.board.find((item) => item.name === 'mapSnapshot')
- check(
- 'C1-failed-shows-on-board',
- failed.phase === 'failed' && !!mapSection && mapSection.state === 'failed' && failed.engine !== '',
- JSON.stringify({ phase: failed.phase, section: mapSection }),
- )
- check(
- 'C2-failed-keeps-frame',
- failed.hosts === 1 && failed.label.includes('失败'),
- JSON.stringify({ hosts: failed.hosts, label: failed.label }),
- )
- await page.close()
- }
- // ---------- D 快照过期:取完第 1 页后后端换快照,界面说「已失效」并保留已加载的点 ----------
- {
- const { page } = await openPage(() => {
- window.__COCKPIT_MOCK_SCENARIO__ = 'stale'
- window.__COCKPIT_MOCK_MAP__ = { pageSize: 2, expireAfterPage: 1 }
- })
- // 首屏自动取到的就是第 1 页(旧快照);再点一次,后端已经换快照 → 失效。
- const before = await readSummary(page)
- await clickAndWaitChange(page, before.loaded)
- await sleep(500)
- const expired = await readSummary(page)
- check(
- 'D1-expired-after-first-page',
- expired.phase === 'expired' && expired.loaded === before.loaded && expired.label.includes('失效'),
- JSON.stringify({ before: `${before.phase}/${before.loaded}`, phase: expired.phase, loaded: expired.loaded, label: expired.label }),
- )
- check(
- 'D2-expired-not-merged',
- expired.snapshot !== '' && expired.total === before.total,
- JSON.stringify({ snapshot: expired.snapshot, total: expired.total }),
- )
- await page.close()
- }
- // ---------- E 空快照:不是失败,也不是「加载完成」的假象 ----------
- {
- const { page } = await openPage(() => {
- window.__COCKPIT_MOCK_SCENARIO__ = 'empty'
- })
- const empty = await readSummary(page)
- const mapSection = empty.board.find((item) => item.name === 'mapSnapshot')
- check(
- 'E1-empty-is-neutral',
- empty.phase === 'empty' && empty.loaded === 0 && (empty.label.includes('没有可上图对象') || empty.label.includes('无数据')),
- JSON.stringify({ phase: empty.phase, label: empty.label, section: mapSection && mapSection.state }),
- )
- await page.close()
- }
- // ---------- F 布局:新增的「地图摘要」控件不得把筛选条撑破(票 08 改到了顶栏)----------
- {
- const { page } = await openPage(() => {
- window.__COCKPIT_MOCK_MAP__ = { pageSize: 2 }
- })
- const probe = () =>
- page.evaluate(() => {
- const scope = document.querySelector('.cockpit-shell__scope')
- const summary = document.querySelector('.cockpit-shell__map-summary')
- const doc = document.documentElement
- return {
- scopeOverflow: scope ? scope.scrollWidth - scope.clientWidth : -1,
- summaryVisible: !!summary && summary.getBoundingClientRect().width > 0,
- summaryInside: summary && scope ? summary.getBoundingClientRect().right <= scope.getBoundingClientRect().right + 1 : false,
- docOverflow: doc.scrollWidth - doc.clientWidth,
- }
- })
- const at1680 = await probe()
- await page.setViewport({ width: 1280, height: 720 })
- await sleep(500)
- const at1280 = await probe()
- check(
- 'F1-filter-bar-not-overflowing',
- at1680.scopeOverflow <= 0 && at1680.docOverflow <= 0 && at1280.scopeOverflow <= 0 && at1280.docOverflow <= 0,
- JSON.stringify({ at1680, at1280 }),
- )
- check(
- 'F2-summary-widget-visible-and-inside',
- at1680.summaryVisible && at1680.summaryInside && at1280.summaryVisible,
- JSON.stringify({ at1680: { visible: at1680.summaryVisible, inside: at1680.summaryInside }, at1280: { visible: at1280.summaryVisible } }),
- )
- await page.close()
- }
- } finally {
- await browser.close()
- }
- const failed = results.filter((r) => !r.pass)
- console.log('')
- console.log(`T08 走查:${results.length - failed.length}/${results.length} 项通过。`)
- if (failed.length) {
- console.log('未通过:' + failed.map((f) => f.id).join(', '))
- process.exit(1)
- }
- console.log('T08 走查全部通过。')
|