t34-status-summary.mjs 4.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
  1. import assert from 'node:assert/strict'
  2. import { readFileSync } from 'node:fs'
  3. import { parse } from '@vue/compiler-sfc'
  4. import { baseParse } from '@vue/compiler-dom'
  5. // Focused static/logic checks for the toolbar request; no server or browser.
  6. const source = readFileSync(new URL('../../../src/modules/cockpit/CockpitShell.vue', import.meta.url), 'utf8')
  7. const { descriptor, errors } = parse(source)
  8. assert.deepEqual(errors, [], 'Vue SFC must parse')
  9. const ast = baseParse(descriptor.template.content)
  10. const elements = []
  11. function visit(node, parent = null) {
  12. if (node.type === 1) elements.push({ node, parent })
  13. for (const child of node.children || []) visit(child, node.type === 1 ? node : parent)
  14. }
  15. visit(ast)
  16. const has = (node, name) => node.props.some((p) => (p.type === 6 && p.name === name) || (p.type === 7 && p.name === 'bind' && p.arg?.content === name))
  17. const cls = (node, name) => node.props.some((p) => p.type === 6 && p.name === 'class' && p.value.content.split(/\s+/).includes(name))
  18. const find = (predicate) => elements.find(({ node }) => predicate(node))?.node
  19. function within(node, ancestor) {
  20. while (node) {
  21. if (node === ancestor) return true
  22. node = elements.find((entry) => entry.node === node)?.parent
  23. }
  24. return false
  25. }
  26. const scope = find((n) => cls(n, 'cockpit-shell__scope'))
  27. const summary = find((n) => cls(n, 'cockpit-shell__status-summary'))
  28. const details = find((n) => has(n, 'data-status-details'))
  29. const refresh = find((n) => has(n, 'data-map-refresh'))
  30. assert.equal(elements.filter(({ node }) => has(node, 'data-status-toggle')).length, 1)
  31. assert.ok(within(details, summary), 'details belong to summary')
  32. for (const marker of ['data-map-summary-phase', 'data-section-board', 'data-ws-state', 'data-freshness']) {
  33. assert.ok(within(find((n) => has(n, marker)), details), `${marker} belongs to details`)
  34. }
  35. assert.equal(elements.filter(({ node }) => has(node, 'data-map-refresh')).length, 1)
  36. assert.ok(!within(refresh, details), 'refresh is outside details')
  37. assert.equal(scope.children.filter((n) => n.type === 1).at(-1), refresh, 'refresh is final toolbar control')
  38. assert.ok(!descriptor.template.content.includes('区内限位'))
  39. assert.ok(!descriptor.template.content.includes('{{ fogText }}'))
  40. assert.ok(!descriptor.template.content.includes('推送 · {{ runtimeStore.statusText }}'))
  41. // Execute the actual computed getters against controlled state (not token-presence checks).
  42. function getter(name) {
  43. const body = source.match(new RegExp(`const ${name} = computed\\(\\(\\) => \\{([\\s\\S]*?)\\n\\}\\)`))?.[1]
  44. assert.ok(body, `${name} getter exists`)
  45. return new Function('runtimeStore', 'overallFreshnessState', 'overallFreshnessLabel', 'mapSummaryView', body)
  46. }
  47. const text = getter('statusSummaryText')
  48. const tone = getter('statusSummaryTone')
  49. function checkState(view, phase, overall, expected, expectedTone) {
  50. const runtime = { freshness: { view, text: `fresh:${view}`, lastSuccessClock: '12:00:00' }, status: { state: 'open' }, statusText: 'ws:open' }
  51. const args = [runtime, { value: overall }, { value: 'partial warning' }, { value: { phase, label: `map:${phase}` } }]
  52. assert.equal(text(...args), expected)
  53. assert.equal(tone(...args), expectedTone)
  54. }
  55. checkState('current', 'complete', 'partial', 'partial warning', 'is-warning')
  56. for (const view of ['empty', 'stale', 'unavailable']) checkState(view, 'complete', 'current', `fresh:${view}`, 'is-warning')
  57. for (const phase of ['idle', 'loading', 'empty', 'failed', 'expired']) checkState('current', phase, 'current', `map:${phase}`, 'is-warning')
  58. checkState('current', 'complete', 'current', '更新 12:00:00', 'is-ok')
  59. const style = descriptor.styles.map((s) => s.content).join('\n')
  60. const rule = (selector) => style.slice(style.indexOf(`${selector} {`)).split('}')[0]
  61. assert.match(rule('.cockpit-shell__scope-left'), /overflow-x:\s*auto/)
  62. assert.match(rule('.cockpit-shell__status-details'), /max-height:/)
  63. assert.match(rule('.cockpit-shell__status-details'), /overflow:\s*auto/)
  64. assert.match(rule('.cockpit-shell__scope'), /overflow:\s*visible/)
  65. assert.match(rule('.cockpit-shell__scope'), /z-index:\s*3/)
  66. console.log('PASS toolbar AST nesting, rightmost refresh, removed diagnostics, summary state cases, and overflow guards')