import { parseHTML } from 'linkedom' import { build } from 'esbuild' import { parse, compileScript } from '@vue/compiler-sfc' import { readFile, mkdir } from 'node:fs/promises' import { resolve, relative } from 'node:path' import { execFileSync } from 'node:child_process' import { pathToFileURL } from 'node:url' // A DOM-like test host, not a browser or a media decoder. Install it before importing Vue. // Every exercised network seam must be explicitly injected; never fall through to a live endpoint. globalThis.fetch = async () => { throw new Error('Uninjected network request in offline video test') } const { window } = parseHTML('
') for (const key of ['window', 'document', 'HTMLElement', 'Element', 'Node', 'SVGElement', 'Event', 'CustomEvent', 'MutationObserver']) { Object.defineProperty(globalThis, key, { configurable: true, value: key === 'window' ? window : window[key] }) } Object.defineProperty(globalThis, 'navigator', { configurable: true, value: { userAgent: 'offline-video-test' } }) globalThis.location = new URL('http://fixture.invalid/') window.location = globalThis.location document.location = globalThis.location window.requestAnimationFrame = globalThis.requestAnimationFrame = (callback) => setTimeout(callback, 0) window.cancelAnimationFrame = globalThis.cancelAnimationFrame = clearTimeout // Native DOM nodes are not plain objects; preserve that contract for Vue refs in LinkeDOM. Object.defineProperty(window.HTMLElement.prototype, Symbol.toStringTag, { configurable: true, value: 'HTMLElement' }) globalThis.getComputedStyle = window.getComputedStyle = (element) => ({ getPropertyValue: (name) => element.style?.getPropertyValue(name) || '' }) if (process.argv.includes('--hosts')) { window.HTMLElement.prototype.getBoundingClientRect = function () { const detail = this.classList?.contains('cockpit-shell__detail') const width = detail ? 400 : 1600, height = detail ? 420 : 900 return { x: 0, y: 0, left: 0, top: 0, width, height, right: width, bottom: height } } } // LinkeDOM exposes a read-only select.value; Vue expects the browser's writable contract. const selectPrototype = Object.getPrototypeOf(document.createElement('select')) const selectValue = Object.getOwnPropertyDescriptor(selectPrototype, 'value') Object.defineProperty(selectPrototype, 'value', { configurable: true, get: selectValue.get, set(value) { for (const option of this.options) option.selected = option.value === String(value) } }) const videoPrototype = Object.getPrototypeOf(document.createElement('video')) videoPrototype.play = () => Promise.resolve() videoPrototype.pause = () => {} videoPrototype.load = () => {} const baselineOnly = process.argv.includes('--baseline-only') const suite = process.argv.includes('--hosts') ? 'hosts' : 'journey' const base = '668bdfc0e6313b9e75ae67524dc8ac7771326774' const sourceRef = process.argv.find((value) => value.startsWith('--source-ref='))?.slice('--source-ref='.length) || (baselineOnly ? base : '') if (sourceRef && !/^[a-f0-9]{7,40}$/.test(sourceRef)) throw new Error('Expected a fixed Git source revision') // Read a fixed baseline plus explicit owned files; never write baseline bytes over WIP. const workingSources = new Set(process.argv.filter((value) => value.startsWith('--working-source=')) .map((value) => value.slice('--working-source='.length))) if (workingSources.size && !sourceRef) throw new Error('--working-source requires --source-ref') for (const name of workingSources) { if (!/^src\/[\w/.-]+$/.test(name) || name.includes('..')) throw new Error('Expected a repository source path') } if (sourceRef) console.log('Source snapshot:', sourceRef, 'working sources:', [...workingSources]) async function sourceAt(path) { const name = relative(process.cwd(), path).replaceAll('\\', '/') if (!sourceRef || !name.startsWith('src/') || workingSources.has(name)) return readFile(path, 'utf8') return execFileSync('git', ['show', `${sourceRef}:${name}`], { encoding: 'utf8' }) } const output = resolve(`.scratch/cockpit-video-monitoring/test-output/${suite}${baselineOnly ? '-baseline' : ''}.mjs`) await mkdir(resolve('.scratch/cockpit-video-monitoring/test-output'), { recursive: true }) await build({ entryPoints: [resolve(`tests/cockpit-video/${suite}.mjs`)], outfile: output, alias: { '@': resolve('src') }, bundle: true, platform: 'node', format: 'esm', packages: 'external', define: { 'import.meta.env': '{}', '__VUE_OPTIONS_API__': 'true', '__VUE_PROD_DEVTOOLS__': 'false' }, plugins: [{ name: 'offline-vue-sfc', setup(builder) { builder.onResolve({ filter: /\.css$/ }, ({ path }) => ({ path, namespace: 'offline-style' })) builder.onLoad({ filter: /.*/, namespace: 'offline-style' }, () => ({ contents: '', loader: 'js' })) if (suite === 'hosts') { // Only physical rendering adapters vary. mapService/Shell/selection stay production code. const engineMethods = ['createViewer', 'destroyViewer', 'resizeViewer', 'applyOverlays', 'sampleTerrainHeight', 'sampleTerrainHeights', 'createCameraEngine', 'groundHeightAt', 'markerGroundHeight', 'applyViewModeLayers', 'createOverlayReadiness', 'applyMotionResolution', 'hasFogStage', 'syncPointMarkers', 'syncAssociatedDevices', 'clearAssociatedDevices', 'clearPointMarkers', 'hasPointMarker', 'hasDeviceMarker', 'projectPointMarker', 'projectDeviceMarker', 'projectDeviceMarkers', 'projectOverlapSymbols', 'readPointMarkers', 'readAssociatedDevices', 'readDeviceEffects', 'applyDeviceEffects', 'applyDeviceReactions', 'clearDeviceLead', 'syncDeviceLead', 'setOverlapPresentation', 'readOverlapTargets', 'clearOverlapNudge', 'highlightEntities', 'syncPipeLayer', 'pickPipeObjectAt', 'noteFrameInterval', 'tickAdaptiveResolution'] builder.onResolve({ filter: /(?:^|\/)cesiumEngine\.js$/ }, ({ path }) => ({ path, namespace: 'offline-gpu' })) builder.onLoad({ filter: /.*/, namespace: 'offline-gpu' }, () => ({ loader: 'js', contents: engineMethods.map((name) => `export const ${name} = (...args) => globalThis.__VIDEO_GPU__.${name}(...args);`).join('\n') })) builder.onResolve({ filter: /(?:^|\/)threePipeEnhancement\.js$/ }, ({ path, resolveDir, namespace }) => namespace === 'offline-three-real' ? null : ({ path: resolve(resolveDir, path), namespace: 'offline-three-wrapper' })) builder.onLoad({ filter: /.*/, namespace: 'offline-three-wrapper' }, ({ path }) => ({ loader: 'js', resolveDir: resolve(path, '..'), contents: `import { mountPipeEnhancement as mount } from 'offline-three-real'; export function mountPipeEnhancement(host, plan, options) { return mount(host, plan, { ...globalThis.__VIDEO_THREE_OPTIONS__, ...options }); }` })) builder.onResolve({ filter: /^offline-three-real$/ }, () => ({ path: resolve('src/modules/cockpit/services/threePipeEnhancement.js'), namespace: 'offline-three-real' })) builder.onLoad({ filter: /.*/, namespace: 'offline-three-real' }, async ({ path }) => ({ contents: await sourceAt(path), loader: 'js', resolveDir: resolve(path, '..') })) } builder.onLoad({ filter: /\.vue$/ }, async ({ path }) => { const source = await sourceAt(path) const { descriptor, errors } = parse(source, { filename: path }) if (errors.length) throw errors[0] const script = compileScript(descriptor, { id: path, inlineTemplate: true }) return { contents: script.content, loader: 'js', resolveDir: resolve(path, '..') } }) builder.onLoad({ filter: /\.js$/ }, async ({ path }) => { if (!sourceRef || !relative(process.cwd(), path).replaceAll('\\', '/').startsWith('src/')) return null return { contents: await sourceAt(path), loader: 'js', resolveDir: resolve(path, '..') } }) } }], }) await import(pathToFileURL(output).href)