diff --git a/scripts/qa-scene-controls.mjs b/scripts/qa-scene-controls.mjs index e34bb03c7..81882bcee 100644 --- a/scripts/qa-scene-controls.mjs +++ b/scripts/qa-scene-controls.mjs @@ -192,6 +192,13 @@ try { window.__godsEyeView.styleManager.getControlState().recording, ), ); + // A focused panel owns Escape to collapse one level. Release panel focus + // before testing the global playback shortcut; the canvas is not tabbable. + await page.evaluate(() => document.activeElement?.blur()); + check( + 'global playback shortcut has document focus', + await page.evaluate(() => document.activeElement === document.body), + ); await page.keyboard.press('Escape'); await page.waitForFunction( () => diff --git a/src/cameraHandoff.test.mjs b/src/cameraHandoff.test.mjs index eecd53bbc..f66f13ed8 100644 --- a/src/cameraHandoff.test.mjs +++ b/src/cameraHandoff.test.mjs @@ -1,3 +1,4 @@ +import { StyleManager } from './ui/applicationShell.js'; import { enter as cockpitEnter, navigateContext } from './ui/cockpitTrackingController.js'; import { CockpitViewController } from './ui/cockpitController.js'; import assert from 'node:assert/strict'; @@ -15,6 +16,11 @@ const cameraVerbs = fs.readFileSync(path.join(ROOT, 'src', 'cameraVerbs.js'), 'u const cockpitTracking = fs.readFileSync(path.join(ROOT, 'src', 'cockpitTracking.js'), 'utf8'); function body(source, pattern, label) { + const name = pattern.source.match(/^(\w+)/)?.[1]; + if (source === ui && typeof StyleManager.prototype[name] === 'function') { + const method = StyleManager.prototype[name].toString(); + return method.slice(method.indexOf(') {') + 3, -1); + } const match = source.match(pattern); assert.ok(match, `${label} is missing`); return match[1]; @@ -40,7 +46,7 @@ test('Cockpit takeover invalidates deferred work before camera cancellation', () ], 'Cockpit takeover'); assert.match( ui, - /onCameraTakeover: \(\) => this\._stampNavigation\(\{ cancelPendingSelection: false \}\),/, + /onCameraTakeover: \(\) =>\s*this\._stampNavigation\(\{\s*cancelPendingSelection: false,?\s*\}\),/, 'Cockpit retires stale camera work without clearing the aircraft selection it adopts', ); }); @@ -51,14 +57,14 @@ test('one explicit tracking selection clears sibling IDs before publishing its d /_persistAwarenessSelection\(event, cleared = false\) \{([\s\S]*?)\n \}/, 'tracking persistence', ); - assert.match(persist, /adoptLayerParams\?\.\(layerId,/); + assert.match(persist, /adoptLayerParams\?\.\(\s*layerId,/); assert.match(persist, /\['flights', 'selectedFlightsTrackingId'\]/); assert.match(persist, /\['military', 'selectedMilitaryTrackingId'\]/); assert.match(persist, /\['satellites', 'selectedSatTrackingId'\]/); assert.match(persist, /if \(otherLayerId === layerId\) continue;/); assert.match( persist, - /for \(const \[otherLayerId, otherKey\][\s\S]*?setLayerParams\(otherLayerId,[\s\S]*?adoptLayerParams\?\.\(layerId,/, + /for \(const \[otherLayerId, otherKey\][\s\S]*?setLayerParams\(\s*otherLayerId,[\s\S]*?adoptLayerParams\?\.\(\s*layerId,/, 'the previous family clears before Flight/Military/Satellite publishes the new durable ID', ); }); @@ -73,9 +79,9 @@ test('navigation clears dormant tracker IDs without aborting unrelated layer res assert.match(stamp, /flightsLayer\.cancelPendingTrackingRestore\?\.\(\)/); assert.match(stamp, /militaryFlightsLayer\.cancelPendingTrackingRestore\?\.\(\)/); assert.match(stamp, /satellitesLayer\.cancelPendingTrackingRestore\?\.\(\)/); - assert.match(stamp, /if \(!passivelyClearedShareSelection && !flightsLayer\.getTrackedInfo\?\.\(\)\)[\s\S]*?selectedFlightsTrackingId: null/); - assert.match(stamp, /if \(!passivelyClearedShareSelection && !militaryFlightsLayer\.getTrackedInfo\?\.\(\)\)[\s\S]*?selectedMilitaryTrackingId: null/); - assert.match(stamp, /if \(!passivelyClearedShareSelection && !satellitesLayer\.getTrackedInfo\?\.\(\)\)[\s\S]*?selectedSatTrackingId: null/); + assert.match(stamp, /if \(\s*!passivelyClearedShareSelection\s*&&\s*!flightsLayer\.getTrackedInfo\?\.\(\)\s*\)[\s\S]*?selectedFlightsTrackingId: null/); + assert.match(stamp, /if \(\s*!passivelyClearedShareSelection\s*&&\s*!militaryFlightsLayer\.getTrackedInfo\?\.\(\)\s*\)[\s\S]*?selectedMilitaryTrackingId: null/); + assert.match(stamp, /if \(\s*!passivelyClearedShareSelection\s*&&\s*!satellitesLayer\.getTrackedInfo\?\.\(\)\s*\)[\s\S]*?selectedSatTrackingId: null/); }); test('voice Cockpit entry reaches the camera only through stamping seams', () => { @@ -108,7 +114,7 @@ test('voice Cockpit entry reaches the camera only through stamping seams', () => // Seam 1: any tracker handoff stamps, so the adoption step is covered. assert.match( ui, - /viewer\.trackedEntityChanged\.addEventListener\(\(entity\) => \{\s*if \(entity && !this\._disposed\) this\._stampNavigation\(\{ cancelPendingSelection: false \}\);/, + /viewer\.trackedEntityChanged\.addEventListener\(\s*\(entity\) => \{\s*if \(entity && !this\._disposed\)\s*this\._stampNavigation\(\{\s*cancelPendingSelection: false,?\s*\}\);/, 'tracker handoff must stamp', ); // Seam 2 is pinned by "Cockpit takeover invalidates deferred work" above. @@ -251,7 +257,7 @@ test('deferred search releases only after its final authority check', () => { ], 'deferred search component'); assert.match(ui, /begin: \(\) => this\._beginDeferredNavigation\('location'\)/); assert.match(ui, /beforeFly: \(generation\) => this\._reassertNavigationHandoff\(generation\)/); - assert.match(ui, /isCurrent: \(generation\) => !this\._disposed && generation === this\._navigationGeneration/); + assert.match(ui, /isCurrent: \(generation\) =>\s*!this\._disposed && generation === this\._navigationGeneration/); assert.match(ui, /onSettled: \(generation\) => this\._settleLocationSearchUi\(generation\)/); assert.doesNotMatch(search.slice(0, search.indexOf('await this.search')), /_releaseFollowCamera/); }); @@ -263,15 +269,15 @@ test('a direct globe gesture retires delayed camera and selection restore only', ); assert.match( ui, - /viewer\?\.canvas\?\.addEventListener\('pointerdown', this\._initialShareGestureHandler/, + /viewer\?\.canvas\?\.addEventListener\(\s*'pointerdown',\s*this\._initialShareGestureHandler/, ); assert.match( ui, - /viewer\?\.canvas\?\.addEventListener\('wheel', this\._initialShareGestureHandler/, + /viewer\?\.canvas\?\.addEventListener\(\s*'wheel',\s*this\._initialShareGestureHandler/, ); assert.match( ui, - /removeEventListener\('pointerdown', this\._initialShareGestureHandler\)[\s\S]*?removeEventListener\('wheel', this\._initialShareGestureHandler\)/, + /removeEventListener\(\s*'pointerdown',\s*this\._initialShareGestureHandler,?\s*\)[\s\S]*?removeEventListener\(\s*'wheel',\s*this\._initialShareGestureHandler,?\s*\)/, ); const stamp = body( ui, diff --git a/src/cctvFocusPolicy.test.mjs b/src/cctvFocusPolicy.test.mjs index 3574bdf78..faf70f9f6 100644 --- a/src/cctvFocusPolicy.test.mjs +++ b/src/cctvFocusPolicy.test.mjs @@ -115,5 +115,5 @@ test('a disposed UI cannot activate a camera when enabling finishes late', async await pending; assert.equal(activations, 0); const uiSource = readFileSync(new URL('./ui/applicationShell.js', import.meta.url), 'utf8'); - assert.match(uiSource, /shouldFocus: \(\) => !this\._disposed && this\._dataManager\.isEnabled\('cctv'\)/); + assert.match(uiSource, /shouldFocus: \(\) =>\s*!this\._disposed\s*&&\s*this\._dataManager\.isEnabled\('cctv'\)/); }); diff --git a/src/cockpitMarkup.test.mjs b/src/cockpitMarkup.test.mjs index aa4b83792..4b370f439 100644 --- a/src/cockpitMarkup.test.mjs +++ b/src/cockpitMarkup.test.mjs @@ -87,13 +87,13 @@ test('Cockpit vision cycle exposes exactly five real visual styles without NONE' assert.match(cycleVisionMode.toString(), /const modes = COCKPIT_VISION_MODES;/); assert.match(setVisionMode.toString(), /const labels = \{\s*optical: inherited,\s*crt: 'CRT',\s*nvg: 'NVG',\s*thermal: 'FLIR',\s*noir: 'NOIR',?\s*\};/); assert.doesNotMatch(setVisionMode.toString(), /none: 'NONE'/); - assert.match(ui, /getInheritedVisionLabel: \(\) => \([\s\S]*?STYLE_STATUS_LABELS\[this\.activeStyle\]/); + assert.match(ui, /getInheritedVisionLabel: \(\) =>\s*\(?[\s\S]*?STYLE_STATUS_LABELS\[this\.activeStyle\]/); assert.match(html, /id="cockpit-vision-current-label"[^>]*>NORMAL<\/strong>/); - assert.match(ui, /const target = applyCockpitVisionStageIntensities\(this\.stages, next, this\._cockpitVisionRestore\);/); - assert.match(ui, /this\._cockpitVisionRestore = captureCockpitVisionBaseline\(this\.stages, this\.transitions\);/); + assert.match(ui, /const target = applyCockpitVisionStageIntensities\(\s*this\.stages,\s*next,\s*this\._cockpitVisionRestore,?\s*\);/); + assert.match(ui, /this\._cockpitVisionRestore = captureCockpitVisionBaseline\(\s*this\.stages,\s*this\.transitions,?\s*\);/); assert.match( ui, - /if \(next === 'optical'\) \{[\s\S]*?applyCockpitVisionStageIntensities\(this\.stages, next, this\._cockpitVisionRestore\);[\s\S]*?return;[\s\S]*?const target = applyCockpitVisionStageIntensities/, + /if \(next === 'optical'\) \{[\s\S]*?applyCockpitVisionStageIntensities\(\s*this\.stages,\s*next,\s*this\._cockpitVisionRestore,?\s*\);[\s\S]*?return;[\s\S]*?const target = applyCockpitVisionStageIntensities/, 'the inherited entry must restore the map shader while CRT, NVG, FLIR, and NOIR remain temporary Cockpit overrides', ); assert.match( @@ -181,7 +181,7 @@ test('the Contact panel never hides itself out from under its own NEXT button', ); assert.match( css, - /\.cockpit-context-window\[data-state="lost"\]/, + /\.cockpit-context-window\[data-state='lost'\]/, 'CONTACT LOST reuses the panel-level data-state cue that "uncertain" already uses', ); }); @@ -255,15 +255,15 @@ test('Cockpit owns a focused shared Display portal and compact Radio controls', const cockpitRadio = html.match(/id="cockpit-radio-panel"[\s\S]*?<\/div>\s*<\/div>\s*<\/aside>/); assert.ok(cockpitRadio, 'Cockpit compact Radio controls are missing'); assert.doesNotMatch(cockpitRadio[0], /context-radio-details-btn|radio-filter|radio-tuner/); - assert.match(css, /\.cockpit-utility-controls[\s\S]*?top:\s*var\(--cockpit-utility-top/); - assert.match(css, /\[data-cockpit-launcher="display"\]\s*\{\s*width:\s*var\(--left-collapsed-width, 176px\)/); + assert.match(css, /\.cockpit-utility-controls[\s\S]*?top:\s*var\(\s*--cockpit-utility-top/); + assert.match(css, /\[data-cockpit-launcher='display'\]\s*\{\s*width:\s*var\(\s*--left-collapsed-width, 176px\)/); assert.match(css, /--display-panel-expanded-width:\s*272px/); - assert.match(css, /#pp-toggles\s*\{[\s\S]*?--pp-expanded-width:\s*var\(--display-panel-expanded-width\)/); - assert.match(css, /is-expanded:has\(\[data-cockpit-launcher="display"\]\)\s*\{[\s\S]*?width:\s*var\(--display-panel-expanded-width\)/); + assert.match(css, /#pp-toggles\s*\{[\s\S]*?--pp-expanded-width:\s*var\(\s*--display-panel-expanded-width\)/); + assert.match(css, /is-expanded:has\(\[data-cockpit-launcher='display'\]\)\s*\{[\s\S]*?width:\s*var\(\s*--display-panel-expanded-width\)/); assert.doesNotMatch(ui, /--cockpit-display-expanded-width|dataPanelWidth/); - assert.match(css, /is-expanded:has\(\[data-cockpit-launcher="display"\]\)[\s\S]*?box-shadow:\s*0 8px 32px rgba\(0, 0, 0, \.42\)/); + assert.match(css, /is-expanded:has\(\[data-cockpit-launcher='display'\]\)[\s\S]*?box-shadow:\s*0 8px 32px rgba\(0, 0, 0, 0\.42\)/); assert.match(css, /\.cockpit-utility-control\.is-expanded \.cockpit-utility-launcher\s*\{[\s\S]*?border:\s*0;[\s\S]*?background:\s*transparent;[\s\S]*?box-shadow:\s*none;/); - assert.match(css, /\.cockpit-utility-control\.is-expanded \.cockpit-utility-divider\s*\{[\s\S]*?linear-gradient\(90deg, rgb\(0 212 255 \/ 28%\), rgba\(0, 212, 255, 0\.18\) 58%, transparent\)[\s\S]*?box-shadow:\s*0 0 7px rgba\(0, 212, 255, \.22\);/); + assert.match(css, /\.cockpit-utility-control\.is-expanded \.cockpit-utility-divider\s*\{[\s\S]*?linear-gradient\(\s*90deg,\s*rgb\(0 212 255 \/ 28%\),\s*rgba\(0, 212, 255, 0\.18\) 58%,\s*transparent\s*\)[\s\S]*?box-shadow:\s*0 0 7px rgba\(0, 212, 255, 0\.22\);/); assert.match(radioBindings, /this\._cockpitDisplayToggleBtn\.textContent = displayOpen \? '▶' : '◀';/); assert.match(radioBindings, /this\._cockpitRadioToggleBtn\.textContent = radioOpen \? '▶' : '◀';/); assert.match(css, /#cockpit-display-panel\s*\{[\s\S]*?display:\s*flex;[\s\S]*?gap:\s*7px;[\s\S]*?padding:\s*0;[\s\S]*?border-top:\s*0;/); @@ -275,8 +275,8 @@ test('Cockpit owns a focused shared Display portal and compact Radio controls', assert.match(css, /--cockpit-radio-expanded-width:\s*232px/); assert.doesNotMatch(css, /--map-stack-(?:collapsed|expanded)-width/); assert.doesNotMatch(html, /id="stack-panel"/, 'the retired Map Stack panel must not remain in Cockpit layout'); - assert.match(css, /\[data-cockpit-launcher="radio"\]\s*\{\s*width:\s*var\(--cockpit-radio-collapsed-width, 148px\)/); - assert.match(css, /is-expanded:has\(\[data-cockpit-launcher="radio"\]\)[\s\S]*?--cockpit-radio-expanded-width/); + assert.match(css, /\[data-cockpit-launcher='radio'\]\s*\{\s*width:\s*var\(\s*--cockpit-radio-collapsed-width, 148px\)/); + assert.match(css, /is-expanded:has\(\[data-cockpit-launcher='radio'\]\)[\s\S]*?--cockpit-radio-expanded-width/); assert.doesNotMatch(ui, /--cockpit-radio-tab-width/); assert.doesNotMatch(css, /\.cockpit-utility-launcher:hover/); const desktopUtilityCss = css.slice( @@ -348,11 +348,11 @@ test('Cockpit side surfaces behave as two single-expanded accordions', () => { ); assert.match( ui, - /!nextCollapsed && this\.cockpitView\?\.active && panelId === 'data-panel'[\s\S]*?_cockpitContextCollapsedForDataPanel = !this\.cockpitView\.contextCollapsed[\s\S]*?this\.cockpitView\.setContextCollapsed\(true\);/, + /!nextCollapsed\s*&&\s*this\.cockpitView\?\.active\s*&&\s*panelId === 'data-panel'[\s\S]*?_cockpitContextCollapsedForDataPanel =\s*!this\.cockpitView\.contextCollapsed[\s\S]*?this\.cockpitView\.setContextCollapsed\(true\);/, ); assert.match( ui, - /nextCollapsed && this\.cockpitView\?\.active && panelId === 'data-panel'[\s\S]*?_cockpitContextCollapsedForDataPanel[\s\S]*?this\.cockpitView\.setContextCollapsed\(false\);/, + /nextCollapsed\s*&&\s*this\.cockpitView\?\.active\s*&&\s*panelId === 'data-panel'[\s\S]*?_cockpitContextCollapsedForDataPanel[\s\S]*?this\.cockpitView\.setContextCollapsed\(false\);/, 'closing Data Layers must restore Contact only after an automatic collapse', ); assert.match(setContextCollapsed.toString(), @@ -404,7 +404,7 @@ test('fresh Cockpit entry temporarily collapses map panels and exit restores the ); assert.match( callback[1], - /_cockpitPanelRestore\.set\(panelId, panel\.classList\.contains\('collapsed'\)\)/, + /_cockpitPanelRestore\.set\(\s*panelId,\s*panel\.classList\.contains\('collapsed'\),?\s*\)/, 'entry must remember each panel\'s exact collapsed state before hiding it', ); assert.match( @@ -449,12 +449,12 @@ test('fresh Cockpit entry temporarily collapses map panels and exit restores the }); test('real disclosure changes reconsider only their own temporary panel lane', () => { - const collapseStart = ui.indexOf('setPanelCollapsed(panelId, collapsed, {'); + const collapseStart = ui.indexOf(' setPanelCollapsed('); const collapse = ui.slice( collapseStart, ui.indexOf('toggleCleanView(forceEnabled)', collapseStart), ); - assert.match(collapse, /classList\.contains\('collapsed'\) === nextCollapsed && !wasAutoCollapsed[\s\S]*?return;/); + assert.match(collapse, /classList\.contains\('collapsed'\) === nextCollapsed\s*&&\s*!wasAutoCollapsed[\s\S]*?return;/); assert.match(collapse, /_rightPanelStack\?\.contains\(panelEl\)[\s\S]*?_scheduleRightPanelLayout\(\{ reconsiderAutoCollapse: true \}\)/); assert.match(collapse, /_scheduleLeftPanelLayout\(\{[\s\S]*?reconsiderAutoCollapse: this\._leftPanelStack\?\.contains\(panelEl\) === true/); }); @@ -462,7 +462,7 @@ test('real disclosure changes reconsider only their own temporary panel lane', ( test('Cockpit hides the complete top-center globe action group', () => { assert.match( css, - /body\.cockpit-mode :is\([\s\S]*?#top-center-actions[\s\S]*?\) \{ display: none !important; \}/, + /body\.cockpit-mode :is\([\s\S]*?#top-center-actions[\s\S]*?\)\s*\{\s*display: none !important;\s*\}/, ); assert.match( css, @@ -587,7 +587,7 @@ test('Cockpit panel corridors reserve the owned topline readouts', () => { assert.match(signalLayout, /cockpit-utility-controls[\s\S]*?--cockpit-utility-max-height/); assert.match( css, - /\.cockpit-utility-controls\s*\{[\s\S]*?top:\s*var\(--cockpit-utility-top, var\(--left-stack-safe-top, var\(--left-stack-top\)\)\)/, + /\.cockpit-utility-controls\s*\{[\s\S]*?top:\s*var\(\s*--cockpit-utility-top,\s*var\(\s*--left-stack-safe-top,\s*var\(\s*--left-stack-top\)\s*\)\s*\)/, 'the first frame before Cockpit publishes an anchor still needs the fallback chain', ); assert.match(css, /body\.cockpit-mode #left-panel-stack\s*\{[\s\S]*?transition:\s*none;/); @@ -859,7 +859,7 @@ test('cockpit state cannot report entryAllowed while already active', () => { ); assert.match( state, - /const entryAllowed = !active && Boolean\(/, + /const entryAllowed =\s*!active &&\s*Boolean\(/, 'entry is impossible while already inside, unconditionally', ); assert.match(state, /entryBlockedReason:/, 'and a refusal can be explained'); diff --git a/src/creditAttribution.test.mjs b/src/creditAttribution.test.mjs index 7dd7d0b70..593fb5395 100644 --- a/src/creditAttribution.test.mjs +++ b/src/creditAttribution.test.mjs @@ -166,7 +166,7 @@ function flattenRules(source) { rules.push({ order: rules.length, media: [...mediaStack], - parts: splitTopLevel(head, ',').map((part) => part.trim()).filter(Boolean), + parts: splitTopLevel(head, ',').map((part) => part.trim().replace(/\(\s+/g, '(').replace(/\s+\)/g, ')')).filter(Boolean), decls, }); index = close + 1; @@ -440,18 +440,18 @@ test('custom properties inside modelled offsets are provably non-negative', () = const layout = fs.readFileSync(new URL('./ui/panelLayoutController.js', import.meta.url), 'utf8'); const start = layout.indexOf('_updateCommandDockTrayStack() {'); assert.ok(start > 0, '_updateCommandDockTrayStack is missing'); - const writer = layout.slice(start, start + 1800); + const writer = layout.slice(start, layout.indexOf(' _scheduleAdaptivePanelLayout(', start)); // Every value traces back to a rect height, floored at 0 and rounded up. - assert.match(writer, /const locationHeight = [\s\S]{0,120}?getBoundingClientRect\(\)\.height \|\| 0;/); - assert.match(writer, /const presetsHeight = [\s\S]{0,120}?getBoundingClientRect\(\)\.height \|\| 0;/); - assert.match(writer, /const lowerPinnedHeight = [\s\S]{0,160}?getBoundingClientRect\(\)\.height \|\| 0;/); + assert.match(writer, /const locationHeight =\s*[\s\S]{0,180}?getBoundingClientRect\(\)\.height\s*\|\| 0;/); + assert.match(writer, /const presetsHeight =\s*[\s\S]{0,180}?getBoundingClientRect\(\)\.height\s*\|\| 0;/); + assert.match(writer, /const lowerPinnedHeight =\s*[\s\S]{0,220}?getBoundingClientRect\(\)\.height\s*\|\| 0;/); assert.match(writer, /const locationHeightPx = Math\.ceil\(locationHeight\);/); assert.match(writer, /const presetsHeightPx = Math\.ceil\(presetsHeight\);/); - assert.match(writer, /'--dock-location-pinned-height', `\$\{locationHeightPx\}px`/); - assert.match(writer, /'--dock-presets-pinned-height', `\$\{presetsHeightPx\}px`/); - assert.match(writer, /'--dock-lower-pinned-height', `\$\{Math\.ceil\(lowerPinnedHeight\)\}px`/); + assert.match(writer, /'--dock-location-pinned-height',\s*`\$\{locationHeightPx\}px`/); + assert.match(writer, /'--dock-presets-pinned-height',\s*`\$\{presetsHeightPx\}px`/); + assert.match(writer, /'--dock-lower-pinned-height',\s*`\$\{Math\.ceil\(lowerPinnedHeight\)\}px`/); assert.match(writer, /'--dock-pinned-stack-height', stackHeight/); - assert.match(writer, /const stackHeight = pinnedCount > 1[\s\S]{0,160}?`calc\(\$\{locationHeightPx\}px \+ \$\{presetsHeightPx\}px \+ 1\.2rem\)`/); + assert.match(writer, /const stackHeight =\s*pinnedCount > 1[\s\S]{0,160}?`calc\(\$\{locationHeightPx\}px \+ \$\{presetsHeightPx\}px \+ 1\.2rem\)`/); }); test('the inputs behind the measured constants are unchanged', () => { diff --git a/src/data/layerState.test.mjs b/src/data/layerState.test.mjs index 0080ef9ab..b9c83a72a 100644 --- a/src/data/layerState.test.mjs +++ b/src/data/layerState.test.mjs @@ -379,7 +379,7 @@ test('a fresh boot starts 3D aircraft ON in proximity — codec, both layers, an const html = await readFile(new URL('../../index.html', import.meta.url), 'utf8'); assert.match(html, /class="pp-toggle-btn active" id="models3d-toggle" aria-pressed="true"/, 'index.html: the 3D button paints lit on first paint, before ui.js runs — and says so'); - assert.match(ui, /this\._models3dBtn\?\.setAttribute\('aria-pressed', String\(this\._models3dEnabled\)\)/, + assert.match(ui, /this\._models3dBtn\?\.setAttribute\(\s*'aria-pressed',\s*String\(this\._models3dEnabled\),?\s*\)/, 'ui.js: and keeps aria-pressed synchronized, so the lit state is not colour-only'); assert.match(html, /class="pp-slider-row visible" id="models3d-mode-row"/, 'index.html: and the Proximity/All row paints open with it'); diff --git a/src/loadingFeedback.test.mjs b/src/loadingFeedback.test.mjs index cb26795e6..78e6b6bbd 100644 --- a/src/loadingFeedback.test.mjs +++ b/src/loadingFeedback.test.mjs @@ -97,7 +97,7 @@ test('share-follow failures use the universal top-center status instead of the b assert.match(handler, /this\._showGlobalStatusNotice\(message\)/); assert.match(handler, /this\.initialRestorePromise\.then\(showAfterStartupCover\)/); assert.match(handler, /this\._lifetime\.frame\(\(\) => \{/); - assert.match(handler, /this\._lifetime\.listen\(startupCover, 'transitionend', showOnce, \{ once: true \}\)/); + assert.match(handler, /this\._lifetime\.listen\(\s*startupCover,\s*'transitionend',\s*showOnce,\s*\{ once: true \},?\s*\)/); assert.match(handler, /fallbackTimer = this\._lifetime\.timeout\(showOnce, 1000\)/); assert.doesNotMatch(handler, /this\._showToast\(message\)/); assert.doesNotMatch(handler, /pushCockpitSignal/); @@ -107,7 +107,7 @@ test('share-follow failures use the universal top-center status instead of the b assert.match(handler, /this\._shareTrackingNoticeGeneration \+= 1/); assert.match(handler, /canPresentDeferredStatusNotice\(/); assert.match(handler, /if \(this\._shareTrackingAcquiringKey\) return/); - assert.match(handler, /result\.classification === 'followed' \|\| result\.classification === 'cancelled'/); + assert.match(handler, /result\.classification === 'followed'\s*\|\|\s*result\.classification === 'cancelled'/); }); test('universal notice masks active loading only for its own fixed dwell', () => { @@ -239,11 +239,11 @@ test('terminal loading feedback centers its label without an empty detail slot', const css = readStylesheet(new URL('../style.css', import.meta.url)); assert.match( css, - /#global-loading-status:is\(\[data-state='complete'\], \[data-state='cancelled'\], \[data-state='error'\]\)\s*\{[\s\S]*?justify-content:\s*center;[\s\S]*?text-align:\s*center;/, + /#global-loading-status:is\(\s*\[data-state='complete'\],\s*\[data-state='cancelled'\],\s*\[data-state='error'\]\s*\)\s*\{[\s\S]*?justify-content:\s*center;[\s\S]*?text-align:\s*center;/, ); assert.match( css, - /#global-loading-status:is\(\[data-state='complete'\], \[data-state='cancelled'\], \[data-state='error'\]\) #global-loading-detail\s*\{\s*display:\s*none;/, + /#global-loading-status:is\(\s*\[data-state='complete'\],\s*\[data-state='cancelled'\],\s*\[data-state='error'\]\s*\)\s*#global-loading-detail\s*\{\s*display:\s*none;/, ); }); @@ -656,7 +656,7 @@ test('the loading ticker never runs hidden and stops after loading and notices s // Persistent ACQUIRING notices remain visible without a 60ms timer. assert.match( arm, - /const noticeNeedsTicker = Number\.isFinite\(this\._globalStatusNotice\?\.hideAt\);[\s\S]*?if \(this\._loadingFeedbackState\?\.phase === 'idle' && !noticeNeedsTicker\) \{\s*this\._stopLoadingFeedbackTicker\(\);\s*\}/, + /const noticeNeedsTicker = Number\.isFinite\(\s*this\._globalStatusNotice\?\.hideAt,?\s*\);[\s\S]*?if \(\s*this\._loadingFeedbackState\?\.phase === 'idle'\s*&&\s*!noticeNeedsTicker\s*\) \{\s*this\._stopLoadingFeedbackTicker\(\);\s*\}/, 'an idle phase with no expiring notice must stop the ticker', ); assert.match( @@ -672,7 +672,7 @@ test('the loading ticker never runs hidden and stops after loading and notices s // throws on every visibilitychange — and must be torn down with the rest. assert.match( ui, - /this\._loadingVisibilityHandler = \(\) => \{\s*if \(!document\.hidden\) this\._updateGlobalLoadingFeedback\(\);\s*else this\._stopLoadingFeedbackTicker\(\);\s*\};\s*document\.addEventListener\('visibilitychange', this\._loadingVisibilityHandler\);/, + /this\._loadingVisibilityHandler = \(\) => \{\s*if \(!document\.hidden\) this\._updateGlobalLoadingFeedback\(\);\s*else this\._stopLoadingFeedbackTicker\(\);\s*\};\s*document\.addEventListener\(\s*'visibilitychange',\s*this\._loadingVisibilityHandler,?\s*\);/, 'visibilitychange must resample the chip on return', ); const styleManager = ui.slice(ui.indexOf('export class ShellFeedback')); @@ -682,7 +682,7 @@ test('the loading ticker never runs hidden and stops after loading and notices s ); assert.match( ui, - /document\.removeEventListener\('visibilitychange', this\._loadingVisibilityHandler\);/, + /document\.removeEventListener\(\s*'visibilitychange',\s*this\._loadingVisibilityHandler,?\s*\);/, 'the resample handler must be removed on teardown', ); }); diff --git a/src/locationReadouts.test.mjs b/src/locationReadouts.test.mjs index cfe87640b..26d078c30 100644 --- a/src/locationReadouts.test.mjs +++ b/src/locationReadouts.test.mjs @@ -1,3 +1,4 @@ +import { StyleManager } from './ui/applicationShell.js'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; @@ -24,7 +25,7 @@ test('the ACTIVE STYLE indicator is written from the style name and nothing else assert.equal(writes.length, 1, 'the style indicator must have exactly one writer'); assert.match( ui.slice(writes[0].index, writes[0].index + 160), - /this\._styleIndicator\.textContent = displayNames\[styleName\] \|\| styleName\.toUpperCase\(\);/, + /this\._styleIndicator\.textContent =\s*displayNames\[styleName\] \|\| styleName\.toUpperCase\(\);/, ); const handler = locationSearchHandler(); @@ -66,15 +67,15 @@ test('any other camera destination clears the search label too', () => { // Voice navigation, the globe reset, camera takeover and entity selection // all funnel through _stampNavigation; without a clear there, a searched // label outlives the place it named. - const start = ui.indexOf(' _stampNavigation({ cancelPendingSelection = true, clearSearchedLocation = true } = {}) {'); + const start = ui.indexOf(' _stampNavigation('); assert.ok(start > 0, '_stampNavigation is missing'); - assert.match(ui.slice(start, start + 700), /if \(clearSearchedLocation\) this\.clearSearchedLocation\(\);/); + assert.match(StyleManager.prototype._stampNavigation.toString(), /if \(clearSearchedLocation\) this\.clearSearchedLocation\(\);/); // The shared funnel is what the reset and voice seams actually reach. - for (const seam of ['resetToGlobeView() {', 'beginLocationNavigation() {', '_runExplicitNavigation(noun, navigate']) { + for (const seam of ['resetToGlobeView() {', 'beginLocationNavigation() {', '_runExplicitNavigation(']) { const at = ui.indexOf(seam); assert.ok(at > 0, `missing navigation seam "${seam}"`); - assert.match(ui.slice(at, at + 900), /_stampNavigation\(/, `"${seam}" must stamp navigation`); + assert.match(StyleManager.prototype[seam.match(/^(\w+)/)[1]].toString(), /_stampNavigation\(/, `"${seam}" must stamp navigation`); } // Public seam, so a camera owner that flies on its own can invalidate it. @@ -86,11 +87,11 @@ test('a deferred lookup that never flies leaves the readout standing', () => { // the stamp blanked a still-true readout whenever the lookup failed, was // superseded, or was refused — no camera ever moved. The deferred begin opts // out; the reassert seam, reached only once the flight is granted, clears. - const begin = ui.indexOf(' _beginDeferredNavigation(noun = '); + const begin = ui.indexOf(' _beginDeferredNavigation('); assert.ok(begin > 0, '_beginDeferredNavigation is missing'); assert.match( - ui.slice(begin, begin + 700), - /stamp: \(\) => this\._stampNavigation\(\{ cancelPendingSelection, clearSearchedLocation: false \}\)/, + StyleManager.prototype._beginDeferredNavigation.toString(), + /stamp: \(\) =>\s*this\._stampNavigation\(\{\s*cancelPendingSelection,\s*clearSearchedLocation: false,?\s*\}\)/, ); const reassert = ui.indexOf(' _reassertNavigationHandoff(generation) {'); diff --git a/src/mapSourceFocus.test.mjs b/src/mapSourceFocus.test.mjs index dd7b43ea2..f4a033442 100644 --- a/src/mapSourceFocus.test.mjs +++ b/src/mapSourceFocus.test.mjs @@ -1,3 +1,4 @@ +import { StyleManager } from './ui/applicationShell.js'; import { createHoverDisclosure, collapsePanelOnEscape } from './ui/panelDisclosure.js'; import test from 'node:test'; import assert from 'node:assert/strict'; @@ -11,17 +12,6 @@ const locationToggleMarkup = locationMarkup.match(/]*\bid="location const locationToggleAttributes = Object.fromEntries( [...(locationToggleMarkup?.[1] || '').matchAll(/([\w-]+)="([^"]*)"/g)].map((match) => [match[1], match[2]]), ); -const initStart = source.indexOf(' _initAutoHoverPanel('); -const initEnd = source.indexOf(' /**\n * Sets up drag-to-reposition', initStart); -const escapeStart = source.indexOf(' _collapsePanelOnEscape('); -const escapeEnd = source.indexOf(' _initCommandDockPins(', escapeStart); -const closeStart = source.indexOf(' setPanelCollapsed(panelId, collapsed, {'); -const closeEnd = source.indexOf(' /**\n * Toggles "clean view"', closeStart); -const syncStart = source.indexOf(' _syncPanelCollapseButton(panelEl) {'); -const syncEnd = source.indexOf(' /**\n * Converts a panel', syncStart); -assert.ok(initStart >= 0 && initEnd > initStart && escapeStart >= 0 && escapeEnd > escapeStart - && closeStart >= 0 && closeEnd > closeStart); -assert.ok(syncStart >= 0 && syncEnd > syncStart); function harness({ hidden = false, selected = true, noChips = false } = {}) { let now = 0; @@ -116,7 +106,7 @@ function harness({ hidden = false, selected = true, noChips = false } = {}) { window.performance = { now: () => now }; document.defaultView = window; const methods = new Function('createHoverDisclosure', 'collapsePanelOnEscape', 'document', 'window', 'clearTimeout', 'performance', 'requestAnimationFrame', - `return ({${source.slice(initStart, initEnd)},\n${source.slice(escapeStart, escapeEnd)},\n${source.slice(closeStart, closeEnd)},\n${source.slice(syncStart, syncEnd)}});`)( + `return ({${StyleManager.prototype._initAutoHoverPanel.toString()},\n${StyleManager.prototype._collapsePanelOnEscape.toString()},\n${StyleManager.prototype.setPanelCollapsed.toString()},\n${StyleManager.prototype._syncPanelCollapseButton.toString()}});`)( createHoverDisclosure, collapsePanelOnEscape, document, window, (id) => timers.delete(id), { now: () => now }, () => {}, ); const saves = []; diff --git a/src/mapStackChips.test.mjs b/src/mapStackChips.test.mjs index 7bb2dd6e7..7106cd2e7 100644 --- a/src/mapStackChips.test.mjs +++ b/src/mapStackChips.test.mjs @@ -346,7 +346,7 @@ test('the Visual Presets tray owns Map Source and the retired left panel is abse ); const panels = readFileSync(new URL('./ui/panelDisclosure.js', import.meta.url), 'utf8'); assert.match(panels, /event\.key !== 'Escape'[\s\S]*?disclosure\?\.focus/); - assert.match(ui, /querySelector\('\.map-stack-chip\.active'\)\s*\|\| panel\.querySelector\('\.map-stack-chip'\)/); + assert.match(ui, /querySelector\(\s*'\.map-stack-chip\.active',?\s*\)\s*\|\|\s*panel\.querySelector\('\.map-stack-chip'\)/); const controls = readFileSync(new URL('./ui/mapSourceControls.js', import.meta.url), 'utf8'); assert.match(ui, /return this\._mapSourceControls\.select\(stackId, \{ syncShare \}\)/, diff --git a/src/panelStackLayout.test.mjs b/src/panelStackLayout.test.mjs index 29fa17599..465f3cf4a 100644 --- a/src/panelStackLayout.test.mjs +++ b/src/panelStackLayout.test.mjs @@ -178,8 +178,8 @@ test('desktop panel lanes use per-panel allocations and presentation-only auto-c /panel !== displayPanel[\s\S]*?removeProperty\('--right-panel-allocated-height'\)[\s\S]*?const naturalHeight/, 'right intrinsic measurement must retain Display allocation while clearing other panel allocations', ); - assert.match(css, /var\(--left-panel-allocated-height/); - assert.match(css, /var\(--right-panel-allocated-height/); + assert.match(css, /var\(\s*--left-panel-allocated-height/); + assert.match(css, /var\(\s*--right-panel-allocated-height/); assert.match( css, /#left-panel-stack\.layout-focus > \[data-panel-id\]\.collapsed\s*\{\s*display:\s*none;/, @@ -217,7 +217,7 @@ test('parameterized Display presets keep one stable scroll owner', () => { const css = readStylesheet(new URL('../style.css', import.meta.url)); assert.match(css, /#pp-toggles:not\(\.collapsed\) > #param-slider-panel\.active\s*\{[\s\S]*?flex:\s*0 0 auto;[\s\S]*?max-height:\s*none;[\s\S]*?overflow-y:\s*visible;/); - assert.match(ui, /readDisplayScrollTop: \(\) => this\._displayPortalScrollRestoreOwner === 'standard'[\s\S]*?this\._standardDisplayScrollTop[\s\S]*?this\._ppToggles\?\.scrollTop \|\| 0/); + assert.match(ui, /readDisplayScrollTop: \(\) =>\s*this\._displayPortalScrollRestoreOwner === 'standard'[\s\S]*?this\._standardDisplayScrollTop[\s\S]*?this\._ppToggles\?\.scrollTop \|\| 0/); assert.match(rightRail, /displayPanel\.scrollTop = Math\.min\(displayScrollTop, maxScrollTop\);/); assert.match( ui, @@ -243,7 +243,7 @@ test('expanded Display uses its container shell instead of a nested header card' ); assert.match( css, - /#pp-toggles\.collapsed \.pp-header-row\s*\{[\s\S]*?width:\s*var\(--right-collapsed-width, 132px\);/, + /#pp-toggles\.collapsed \.pp-header-row\s*\{[\s\S]*?width:\s*var\(\s*--right-collapsed-width, 132px\);/, 'collapsed Display must retain its standalone launcher sizing', ); }); @@ -257,7 +257,7 @@ test('expanded left panels integrate their headers with the container shell', () ); assert.match( css, - /#left-panel-stack > \[data-panel-id\]:not\(\.collapsed\) \.panel-divider\s*\{[\s\S]*?linear-gradient\(90deg, rgb\(0 212 255 \/ 28%\), rgba\(0, 212, 255, 0\.18\) 58%, transparent\)[\s\S]*?box-shadow:\s*0 0 7px rgba\(0, 212, 255, 0\.22\);/, + /#left-panel-stack > \[data-panel-id\]:not\(\.collapsed\) \.panel-divider\s*\{[\s\S]*?linear-gradient\(\s*90deg,\s*rgb\(0 212 255 \/ 28%\),\s*rgba\(0, 212, 255, 0\.18\) 58%,\s*transparent\s*\)[\s\S]*?box-shadow:\s*0 0 7px rgba\(0, 212, 255, 0\.22\);/, ); }); @@ -284,10 +284,10 @@ test('expanded right panels highlight the title divider without changing collaps ); assert.match( css, - /#right-context-rail \[data-panel-id\]:not\(\.collapsed\) \.panel-divider\s*\{[\s\S]*?linear-gradient\(90deg, rgb\(0 212 255 \/ 28%\), rgba\(0, 212, 255, 0\.18\) 58%, transparent\)[\s\S]*?box-shadow:\s*0 0 7px rgba\(0, 212, 255, 0\.22\);/, + /#right-context-rail \[data-panel-id\]:not\(\.collapsed\) \.panel-divider\s*\{[\s\S]*?linear-gradient\(\s*90deg,\s*rgb\(0 212 255 \/ 28%\),\s*rgba\(0, 212, 255, 0\.18\) 58%,\s*transparent\s*\)[\s\S]*?box-shadow:\s*0 0 7px rgba\(0, 212, 255, 0\.22\);/, ); assert.match( css, - /#param-slider-panel:not\(\.collapsed\) \.param-panel-divider\s*\{[\s\S]*?linear-gradient\(90deg, rgb\(0 212 255 \/ 28%\), rgba\(0, 212, 255, 0\.18\) 58%, transparent\);/, + /#param-slider-panel:not\(\.collapsed\) \.param-panel-divider\s*\{[\s\S]*?linear-gradient\(\s*90deg,\s*rgb\(0 212 255 \/ 28%\),\s*rgba\(0, 212, 255, 0\.18\) 58%,\s*transparent\s*\);/, ); }); diff --git a/src/radioMarkup.test.mjs b/src/radioMarkup.test.mjs index 42e722520..d401d51d7 100644 --- a/src/radioMarkup.test.mjs +++ b/src/radioMarkup.test.mjs @@ -207,7 +207,7 @@ test('Radio volume and mission speed share the Sharpen slider visual language', assert.match(css, /\.gev-quantitative-slider::-webkit-slider-runnable-track\s*\{[\s\S]*?height: 3px;[\s\S]*?background: rgba\(255, 255, 255, 0\.08\);/); assert.match(css, /\.gev-quantitative-slider::-webkit-slider-thumb\s*\{[\s\S]*?width: 10px;[\s\S]*?height: 10px;[\s\S]*?border-radius: 50%;[\s\S]*?background: var\(--accent\);/); assert.match(css, /\.gev-quantitative-slider:focus-visible\s*\{[\s\S]*?outline: 1px solid/); - assert.match(css, /\.gev-quantitative-slider:disabled\s*\{[\s\S]*?opacity: \.42;[\s\S]*?cursor: not-allowed;/); + assert.match(css, /\.gev-quantitative-slider:disabled\s*\{[\s\S]*?opacity: 0\.42;[\s\S]*?cursor: not-allowed;/); assert.match(css, /\.gev-slider-value\s*\{[\s\S]*?color: var\(--accent\);[\s\S]*?font-size: 9px;/); assert.doesNotMatch(css, /#space-mission-panel \[data-mission-replay-speed\]::-webkit-slider-thumb/); }); @@ -245,13 +245,13 @@ test('Radio is nested inside Context with separate disclosure and power controls assert.match(css, /\.radio-tuner\.is-static/); assert.match(css, /\.radio-tuner-tick\s*\{/); assert.doesNotMatch(css, /radio-tuner-scale-(?:left|right)/); - assert.match(css, /\.radio-tuner-needle\s*\{[\s\S]*?transition: left \.18s ease-out;/); + assert.match(css, /\.radio-tuner-needle\s*\{[\s\S]*?transition: left 0\.18s ease-out;/); assert.match(css, /\.radio-tuner\.is-dragging \.radio-tuner-needle,[\s\S]*?\.radio-tuner\.is-dragging \.radio-tuner-tick\s*\{\s*transition: none;/); assert.match(css, /\.radio-tuner\s*\{[\s\S]*?max-width: 100%;[\s\S]*?overflow: hidden;/); assert.match(css, /#radio-tuner-slider\s*\{[\s\S]*?max-width: 100%;[\s\S]*?touch-action: none;/); assert.match(css, /#title-bar\.radio-broadcasting \.title-logo::before/); assert.match(css, /#title-bar\.radio-broadcasting \.title-logo::after/); - assert.match(css, /--radio-broadcast-opacity: \.17/); + assert.match(css, /--radio-broadcast-opacity: 0\.17/); assert.match(css, /@media \(prefers-reduced-motion: reduce\)[\s\S]*?\.radio-tuner-needle,[\s\S]*?\.radio-tuner-tick\s*\{\s*transition: none;/); assert.doesNotMatch(ui, /_radioTunerCameraRemove = this\.viewer\?\.camera\?\.changed/); assert.match(radioPresentation, /classList\.toggle\('radio-broadcasting', state\.audioState === 'playing'\)/); diff --git a/src/sharelink.celestial.test.mjs b/src/sharelink.celestial.test.mjs index 0a2a3cefa..5defb11a6 100644 --- a/src/sharelink.celestial.test.mjs +++ b/src/sharelink.celestial.test.mjs @@ -1,3 +1,4 @@ +import { StyleManager } from './ui/applicationShell.js'; import { _claimContextVisualAuthority, setContextMode } from './ui/contextActions.js'; import { _initGlobalContextPanel } from './ui/contextBindings.js'; import { test } from 'node:test'; @@ -9,6 +10,8 @@ import { createDefaultLayerState } from './data/layerState.js'; const uiSource = fs.readFileSync(new URL('./ui/applicationShell.js', import.meta.url), 'utf8'); function sourceBlock(start, end) { + const name = start.trim().match(/^(?:async )?(\w+)\(/)?.[1]; + if (typeof StyleManager.prototype[name] === 'function') return StyleManager.prototype[name].toString(); const startIndex = uiSource.indexOf(start); const endIndex = uiSource.indexOf(end, startIndex + start.length); assert.ok(startIndex >= 0, `missing source block start: ${start}`); @@ -194,7 +197,7 @@ test('a shared view reserves its own camera without cancelling its saved Follow' ); // The shared view's own `cancelPendingSelection` must reach the stamp; other // stamp options may ride alongside it. - assert.match(deferred, /_stampNavigation\(\{ cancelPendingSelection[^)]*\}\)/); + assert.match(deferred, /_stampNavigation\(\{\s*cancelPendingSelection[^)]*\}\)/); }); test('copy timestamp parsing is strict and rejects malformed or future values', () => { @@ -472,7 +475,7 @@ test('explicit Context transitions claim the visual restore lane before transiti 'the Context authority helper must claim the visual lane', ); - assert.match(uiSource, /claimVisualAuthority: \(\) => this\.shareLinkManager\?\.claimRestoreLane\?\.\('visual'\)/); + assert.match(uiSource, /claimVisualAuthority: \(\) =>\s*this\.shareLinkManager\?\.claimRestoreLane\?\.\('visual'\)/); const contextPanel = _initGlobalContextPanel.toString(); for (const [start, end, label] of [ ["this.listen(this._globalContextFlightsBtn, 'click'", "this.listen(this._globalContextMissionsBtn, 'click'", 'Contacts tab'],