t14-final-walkthrough.mjs 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
  1. // Ticket 14 browser acceptance. real transport uses controlled HTTP responses,
  2. // not a deployed-backend integration claim (that belongs to ticket 18).
  3. import assert from 'node:assert/strict'
  4. import { registerCockpitMocks } from '../../../src/modules/cockpit/mocks/contractMocks.js'
  5. import { registerMockTransport, findMockTransport } from '../../../src/modules/cockpit/services/cockpitApi.js'
  6. const base = process.env.COCKPIT_BASE || 'http://localhost:8001'
  7. const puppeteer = (await import(process.env.PUPPETEER_CORE_PATH || 'file:///C:/Users/ASUS/.dsh/puppeteer-runner/node_modules/puppeteer-core/lib/puppeteer/puppeteer-core.js')).default
  8. registerCockpitMocks({ handlers: { registerMockTransport } })
  9. const browser = await puppeteer.launch({ executablePath: process.env.CHROME_PATH || 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', headless: true, defaultViewport: { width: 1680, height: 950 }, args: ['--no-sandbox'] })
  10. let passed = 0
  11. function check(name, condition, evidence) { assert.ok(condition, `${name}: ${JSON.stringify(evidence)}`); passed++; console.log(`PASS ${name}: ${JSON.stringify(evidence)}`) }
  12. async function open(config, controlled = false) {
  13. const page = await browser.newPage()
  14. const paths = []
  15. if (controlled) {
  16. await page.setRequestInterception(true)
  17. page.on('request', async request => {
  18. const url = new URL(request.url())
  19. if (!url.pathname.startsWith('/ticket14-api/')) return request.continue()
  20. const path = url.pathname.slice('/ticket14-api'.length) + url.search
  21. paths.push(path)
  22. if (url.pathname.endsWith('/monitoring/overview')) return request.respond({ status: 401, body: '{}' })
  23. const handler = findMockTransport(path)
  24. if (!handler) return request.respond({ status: 404, body: '{}' })
  25. try { const data = await handler({ path }); await request.respond({ status: 200, contentType: 'application/json', body: JSON.stringify(data) }) }
  26. catch { await request.respond({ status: 500, body: '{}' }) }
  27. })
  28. }
  29. await page.evaluateOnNewDocument(config => { window.__COCKPIT_CONFIG__ = config }, config)
  30. await page.goto(`${base}/#/cockpit`, { waitUntil: 'domcontentloaded', timeout: 60000 })
  31. await page.waitForSelector('.cockpit-shell')
  32. return { page, paths }
  33. }
  34. const map = { intro: { enabled: false }, cesium: { enabled: false } }
  35. try {
  36. {
  37. const { page } = await open({ dataSource: 'mock', map })
  38. await page.$eval('.cockpit-shell__mode:nth-child(3)', r => r.click())
  39. await page.waitForSelector('[data-pie-slice]')
  40. const counts = () => page.$$eval('[data-legend-key]', rows => rows.map(r => [r.dataset.legendKey, r.dataset.statusCount]))
  41. const before = await counts()
  42. await page.$eval('[data-legend-key="ALARM"]', r => r.click())
  43. await page.$eval('[data-legend-key="WARNING"]', r => r.click())
  44. await page.waitForFunction(() => document.querySelector('[data-legend-key="ALARM"]')?.dataset.statusSelected === 'yes' && document.querySelector('[data-legend-key="WARNING"]')?.dataset.statusSelected === 'yes')
  45. const selected = await page.$$eval('[data-legend-key][data-status-selected="yes"]', rows => rows.map(r => r.dataset.legendKey).sort())
  46. check('two-status OR selection', selected.join(',') === 'ALARM,WARNING', selected)
  47. check('global counts stable', JSON.stringify(await counts()) === JSON.stringify(before), before)
  48. await page.waitForSelector('[data-pie-slice][data-pie-selected="yes"]').catch(async error => { console.log(await page.$eval('.cockpit-shell', r => r.innerText)); throw error })
  49. const pie = await page.$$eval('[data-pie-slice]', rows => rows.map(r => ({ key: r.dataset.pieSlice, selected: r.dataset.pieSelected, dim: r.classList.contains('is-dim'), path: r.getAttribute('d') })))
  50. check('pie selection and dimming', pie.filter(r => r.selected === 'yes').length === 2 && pie.filter(r => r.selected !== 'yes').every(r => r.dim), pie)
  51. check('pie has finite geometry', pie.every(r => !/NaN|Infinity/.test(r.path)), pie)
  52. await page.$eval('[data-legend-key="ALARM"]', r => r.click())
  53. check('remove one status preserves other', await page.$eval('[data-legend-key="WARNING"]', r => r.dataset.statusSelected === 'yes'), null)
  54. await page.close()
  55. }
  56. {
  57. const { page, paths } = await open({ dataSource: 'real', api: { baseUrl: `${base}/ticket14-api` }, map, runtime: { refreshSeconds: 3600 } }, true)
  58. await page.$eval('.cockpit-shell__mode:nth-child(3)', r => r.click())
  59. await page.waitForSelector('[data-section-name="monitoringOverview"][data-section-state="unavailable"]')
  60. await page.waitForSelector('[data-section-name="monitoringDistribution"][data-section-state="success"]')
  61. check('real transport auto starts and isolates 401', paths.some(p => p.startsWith('/map-snapshot')) && paths.some(p => p.startsWith('/monitoring/situation')) && paths.some(p => p.startsWith('/overview/')) && paths.some(p => p.startsWith('/infrastructure/')), paths)
  62. await page.close()
  63. }
  64. {
  65. const { page } = await open({ dataSource: 'real', api: { baseUrl: '' }, map })
  66. await page.waitForSelector('[data-section-name="equipmentAggregate"][data-section-state="unavailable"]')
  67. await page.waitForSelector('[data-section-name="mapSnapshot"][data-section-state="unavailable"]')
  68. check('NO_BASE_URL unavailable without mock fallback', await page.$eval('[data-section-name="mapSnapshot"]', r => r.textContent.includes('不可用')), null)
  69. await page.close()
  70. }
  71. } finally { await browser.close() }
  72. console.log(`T14 final walkthrough: ${passed} passed`)