run.mjs 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. import { parseHTML } from 'linkedom'
  2. import { build } from 'esbuild'
  3. import { parse, compileScript } from '@vue/compiler-sfc'
  4. import { readFile, mkdir } from 'node:fs/promises'
  5. import { resolve, relative } from 'node:path'
  6. import { execFileSync } from 'node:child_process'
  7. import { pathToFileURL } from 'node:url'
  8. // A DOM-like test host, not a browser or a media decoder. Install it before importing Vue.
  9. // Every exercised network seam must be explicitly injected; never fall through to a live endpoint.
  10. globalThis.fetch = async () => { throw new Error('Uninjected network request in offline video test') }
  11. const { window } = parseHTML('<html><body></body></html>')
  12. for (const key of ['window', 'document', 'HTMLElement', 'Element', 'Node', 'SVGElement', 'Event', 'CustomEvent', 'MutationObserver']) {
  13. Object.defineProperty(globalThis, key, { configurable: true, value: key === 'window' ? window : window[key] })
  14. }
  15. Object.defineProperty(globalThis, 'navigator', { configurable: true, value: { userAgent: 'offline-video-test' } })
  16. globalThis.location = new URL('http://fixture.invalid/')
  17. window.location = globalThis.location
  18. document.location = globalThis.location
  19. window.requestAnimationFrame = globalThis.requestAnimationFrame = (callback) => setTimeout(callback, 0)
  20. window.cancelAnimationFrame = globalThis.cancelAnimationFrame = clearTimeout
  21. // Native DOM nodes are not plain objects; preserve that contract for Vue refs in LinkeDOM.
  22. Object.defineProperty(window.HTMLElement.prototype, Symbol.toStringTag, { configurable: true, value: 'HTMLElement' })
  23. globalThis.getComputedStyle = window.getComputedStyle = (element) => ({ getPropertyValue: (name) => element.style?.getPropertyValue(name) || '' })
  24. if (process.argv.includes('--hosts')) {
  25. window.HTMLElement.prototype.getBoundingClientRect = function () {
  26. const detail = this.classList?.contains('cockpit-shell__detail')
  27. const width = detail ? 400 : 1600, height = detail ? 420 : 900
  28. return { x: 0, y: 0, left: 0, top: 0, width, height, right: width, bottom: height }
  29. }
  30. }
  31. const videoPrototype = Object.getPrototypeOf(document.createElement('video'))
  32. videoPrototype.play = () => Promise.resolve()
  33. videoPrototype.pause = () => {}
  34. videoPrototype.load = () => {}
  35. const baselineOnly = process.argv.includes('--baseline-only')
  36. const suite = process.argv.includes('--hosts') ? 'hosts' : 'journey'
  37. const base = '668bdfc0e6313b9e75ae67524dc8ac7771326774'
  38. const sourceRef = process.argv.find((value) => value.startsWith('--source-ref='))?.slice('--source-ref='.length)
  39. || (baselineOnly ? base : '')
  40. if (sourceRef && !/^[a-f0-9]{7,40}$/.test(sourceRef)) throw new Error('Expected a fixed Git source revision')
  41. async function sourceAt(path) {
  42. const name = relative(process.cwd(), path).replaceAll('\\', '/')
  43. if (!sourceRef || !name.startsWith('src/')) return readFile(path, 'utf8')
  44. return execFileSync('git', ['show', `${sourceRef}:${name}`], { encoding: 'utf8' })
  45. }
  46. const output = resolve(`.scratch/cockpit-video-monitoring/test-output/${suite}${baselineOnly ? '-baseline' : ''}.mjs`)
  47. await mkdir(resolve('.scratch/cockpit-video-monitoring/test-output'), { recursive: true })
  48. await build({
  49. entryPoints: [resolve(`tests/cockpit-video/${suite}.mjs`)], outfile: output,
  50. alias: { '@': resolve('src') },
  51. bundle: true, platform: 'node', format: 'esm', packages: 'external',
  52. define: { 'import.meta.env': '{}', '__VUE_OPTIONS_API__': 'true', '__VUE_PROD_DEVTOOLS__': 'false' },
  53. plugins: [{ name: 'offline-vue-sfc', setup(builder) {
  54. builder.onResolve({ filter: /\.css$/ }, ({ path }) => ({ path, namespace: 'offline-style' }))
  55. builder.onLoad({ filter: /.*/, namespace: 'offline-style' }, () => ({ contents: '', loader: 'js' }))
  56. if (suite === 'hosts') {
  57. // Only physical rendering adapters vary. mapService/Shell/selection stay production code.
  58. const engineMethods = ['createViewer', 'destroyViewer', 'resizeViewer', 'applyOverlays', 'sampleTerrainHeight', 'sampleTerrainHeights',
  59. 'createCameraEngine', 'groundHeightAt', 'markerGroundHeight', 'applyViewModeLayers', 'createOverlayReadiness',
  60. 'applyMotionResolution', 'hasFogStage', 'syncPointMarkers', 'syncAssociatedDevices', 'clearAssociatedDevices',
  61. 'clearPointMarkers', 'hasPointMarker', 'hasDeviceMarker', 'projectPointMarker', 'projectDeviceMarker',
  62. 'projectDeviceMarkers', 'projectOverlapSymbols', 'readPointMarkers', 'readAssociatedDevices', 'readDeviceEffects',
  63. 'applyDeviceEffects', 'applyDeviceReactions', 'clearDeviceLead', 'syncDeviceLead', 'setOverlapPresentation',
  64. 'readOverlapTargets', 'clearOverlapNudge', 'highlightEntities', 'syncPipeLayer', 'pickPipeObjectAt', 'noteFrameInterval', 'tickAdaptiveResolution']
  65. builder.onResolve({ filter: /(?:^|\/)cesiumEngine\.js$/ }, ({ path }) => ({ path, namespace: 'offline-gpu' }))
  66. builder.onLoad({ filter: /.*/, namespace: 'offline-gpu' }, () => ({ loader: 'js',
  67. contents: engineMethods.map((name) => `export const ${name} = (...args) => globalThis.__VIDEO_GPU__.${name}(...args);`).join('\n') }))
  68. builder.onResolve({ filter: /(?:^|\/)threePipeEnhancement\.js$/ }, ({ path, resolveDir, namespace }) => namespace === 'offline-three-real'
  69. ? null : ({ path: resolve(resolveDir, path), namespace: 'offline-three-wrapper' }))
  70. builder.onLoad({ filter: /.*/, namespace: 'offline-three-wrapper' }, ({ path }) => ({ loader: 'js', resolveDir: resolve(path, '..'),
  71. contents: `import { mountPipeEnhancement as mount } from 'offline-three-real'; export function mountPipeEnhancement(host, plan, options) { return mount(host, plan, { ...globalThis.__VIDEO_THREE_OPTIONS__, ...options }); }` }))
  72. builder.onResolve({ filter: /^offline-three-real$/ }, () => ({ path: resolve('src/modules/cockpit/services/threePipeEnhancement.js'), namespace: 'offline-three-real' }))
  73. builder.onLoad({ filter: /.*/, namespace: 'offline-three-real' }, async ({ path }) => ({ contents: await sourceAt(path), loader: 'js', resolveDir: resolve(path, '..') }))
  74. }
  75. builder.onLoad({ filter: /\.vue$/ }, async ({ path }) => {
  76. const source = await sourceAt(path)
  77. const { descriptor, errors } = parse(source, { filename: path })
  78. if (errors.length) throw errors[0]
  79. const script = compileScript(descriptor, { id: path, inlineTemplate: true })
  80. return { contents: script.content, loader: 'js', resolveDir: resolve(path, '..') }
  81. })
  82. builder.onLoad({ filter: /\.js$/ }, async ({ path }) => {
  83. if (!sourceRef || !relative(process.cwd(), path).replaceAll('\\', '/').startsWith('src/')) return null
  84. return { contents: await sourceAt(path), loader: 'js', resolveDir: resolve(path, '..') }
  85. })
  86. } }],
  87. })
  88. await import(pathToFileURL(output).href)