diff --git a/CHANGELOG.md b/CHANGELOG.md index 981a7803a..96850ce94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog +- Separate navigation, share restoration, visual settings and panel state into focused UI owners with explicit dependencies and terminal cleanup. + - Separate layer lifecycle transactions from panel construction and render/detection reactions; retain existing transition and refresh behavior. - Let CLI tools, development launchers and the setup doctor use an explicit project directory while retaining their existing default paths. diff --git a/docs/CODE-BOUNDARIES.md b/docs/CODE-BOUNDARIES.md index 62611d8af..f3a3105d3 100644 --- a/docs/CODE-BOUNDARIES.md +++ b/docs/CODE-BOUNDARIES.md @@ -461,3 +461,19 @@ render requests and detection invalidation. It owns hidden-panel refresh and listener cleanup. Application data assembly constructs both owners explicitly; `data/manager` is the compatibility facade for direct callers. The ordinary state subscriptions retain their existing event contract. + +## UI state owners + +- `ui/navigation` owns navigation generations, pending search presentation and + tracking handoff. It reads Cockpit admission and uses supplied tracking operations. +- `ui/share-restoration` owns the initial restore transaction, layer coordinator, + status notices and gesture/timer cleanup. +- `ui/visual-settings` owns style preferences, detection overrides, display inputs + and IR cleanup. Its engine services and panel operations are explicit. +- `ui/panel-chrome` owns disclosure, docking, collapse preferences and temporary + Cockpit panel snapshots, composing the existing positioning/layout controllers. + +These package groups contain their own dependencies and exclude application +assembly. `applicationShell` retains composition and compatibility methods; +those methods delegate to the state owner. Source-based regression checks inspect +that implementation owner, and browser acceptance exercises the assembled UI. diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index daccbb96c..842f22ac3 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1,5 +1,14 @@ # God's Eye View Current State +The UI facade composes separate owners for navigation authority, share restoration, +visual settings and panel chrome. Navigation owns request generations and follow +handoff; restoration owns its coordinator, deferred notices, gestures and timer; +visual settings own style/detection overrides and sensor cleanup; panel chrome +owns disclosure, docking and Cockpit panel snapshots. Each takes explicit +operations and reads instead of the complete shell. Existing controls, share +formats and one-application-per-page behavior are preserved. + + Layer lifecycle transactions are owned by `data/lifecycle`, which has no panel, render-governor or detection imports. Application composition mounts a separate layer presentation owner that reacts to status, accepted visibility/parameter diff --git a/package.json b/package.json index d7adaae42..0bf4821af 100644 --- a/package.json +++ b/package.json @@ -196,6 +196,10 @@ "./standalone/catalog": "./src/standalone/catalog.js", "./application/layers": "./src/app/constructCatalog.js", "./application/operations": "./src/app/operations.js", - "./data/lifecycle": "./src/data/lifecycle.js" + "./data/lifecycle": "./src/data/lifecycle.js", + "./ui/navigation": "./src/ui/navigationController.js", + "./ui/share-restoration": "./src/ui/shareRestoration.js", + "./ui/visual-settings": "./src/ui/visualSettings.js", + "./ui/panel-chrome": "./src/ui/panelChrome.js" } } diff --git a/scripts/format-scope.json b/scripts/format-scope.json index 3f37f1b28..8eb4517f8 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -589,5 +589,11 @@ "src/app/operations.test.mjs", "src/data/lifecycle.js", "src/data/manager.js", - "src/app/layerPresentation.js" + "src/app/layerPresentation.js", + "src/ui/navigationController.js", + "src/ui/shareRestoration.js", + "src/ui/visualSettings.js", + "src/ui/panelChrome.js", + "src/testSupport/readShellSource.mjs", + "src/ui/stateOwners.test.mjs" ] diff --git a/scripts/package-boundaries.json b/scripts/package-boundaries.json index 9a2b7040a..130179411 100644 --- a/scripts/package-boundaries.json +++ b/scripts/package-boundaries.json @@ -439,6 +439,7 @@ "ui-shell": { "exports": ["./ui/shell"], "modules": [ + "src/app/stateChannel.js", "src/bloom.js", "src/cctvFocusPolicy.js", "src/cctvFocusRequest.js", @@ -498,6 +499,7 @@ "src/ui/contextTransactions.js", "src/ui/displayControls.js", "src/ui/effects.js", + "src/ui/frameRateMonitor.js", "src/ui/layerPanel.js", "src/ui/layers.js", "src/ui/leftPanelRail.js", @@ -506,6 +508,8 @@ "src/ui/locationSearch.js", "src/ui/mapSource.js", "src/ui/mapSourceControls.js", + "src/ui/navigationController.js", + "src/ui/panelChrome.js", "src/ui/panelDisclosure.js", "src/ui/panelLayoutController.js", "src/ui/panelMeasurement.js", @@ -522,17 +526,17 @@ "src/ui/sceneControls.js", "src/ui/scenePresentation.js", "src/ui/scenes.js", + "src/ui/shareRestoration.js", "src/ui/shellElements.js", "src/ui/shellFeedback.js", "src/ui/styleParameters.js", "src/ui/surfaceKeyboard.js", + "src/ui/uiLifetime.js", "src/ui/visualEffects.js", "src/ui/visualInput.js", "src/ui/visualPresets.js", - "src/worldFocus.js", - "src/ui/uiLifetime.js", - "src/app/stateChannel.js", - "src/ui/frameRateMonitor.js" + "src/ui/visualSettings.js", + "src/worldFocus.js" ], "external": ["cesium"] }, @@ -1498,6 +1502,8 @@ "src/ui/locationSearch.js", "src/ui/mapSource.js", "src/ui/mapSourceControls.js", + "src/ui/navigationController.js", + "src/ui/panelChrome.js", "src/ui/panelDisclosure.js", "src/ui/panelLayoutController.js", "src/ui/panelMeasurement.js", @@ -1514,6 +1520,7 @@ "src/ui/sceneControls.js", "src/ui/scenePresentation.js", "src/ui/scenes.js", + "src/ui/shareRestoration.js", "src/ui/shellElements.js", "src/ui/shellFeedback.js", "src/ui/styleParameters.js", @@ -1522,6 +1529,7 @@ "src/ui/visualEffects.js", "src/ui/visualInput.js", "src/ui/visualPresets.js", + "src/ui/visualSettings.js", "src/voice/commands.js", "src/voice/control.js", "src/voice/gevActions.js", @@ -1911,5 +1919,63 @@ "exports": ["./data/lifecycle"], "modules": ["src/data/lifecycle.js"], "external": [] + }, + "ui-navigation": { + "exports": ["./ui/navigation"], + "modules": ["src/navigationPolicy.js", "src/ui/navigationController.js"], + "external": ["cesium"] + }, + "ui-share-restoration": { + "exports": ["./ui/share-restoration"], + "modules": [ + "src/data/installationFeedback.js", + "src/data/layerState.js", + "src/loadingFeedback.js", + "src/navigationPolicy.js", + "src/ui/shareRestoration.js", + "src/ui/uiLifetime.js" + ], + "external": [] + }, + "ui-visual-settings": { + "exports": ["./ui/visual-settings"], + "modules": [ + "src/bloom.js", + "src/cockpitVisionPolicy.js", + "src/contactsDetectionPolicy.js", + "src/data/detectionPolicy.js", + "src/styles/anime.js", + "src/styles/noir.js", + "src/styles/retro.js", + "src/styles/snow.js", + "src/styles/surveillance.js", + "src/styles/thermal.js", + "src/ui/applicationShortcuts.js", + "src/ui/effects.js", + "src/ui/styleParameters.js", + "src/ui/uiLifetime.js", + "src/ui/visualEffects.js", + "src/ui/visualInput.js", + "src/ui/visualPresets.js", + "src/ui/visualSettings.js" + ], + "external": ["cesium"] + }, + "ui-panel-chrome": { + "exports": ["./ui/panel-chrome"], + "modules": [ + "src/panelStackLayout.js", + "src/ui/leftPanelRail.js", + "src/ui/panelChrome.js", + "src/ui/panelDisclosure.js", + "src/ui/panelLayoutController.js", + "src/ui/panelMeasurement.js", + "src/ui/panelPositionControls.js", + "src/ui/panelRailGeometry.js", + "src/ui/panelRails.js", + "src/ui/rightPanelRail.js", + "src/ui/uiLifetime.js" + ], + "external": [] } } diff --git a/scripts/qa-map-source-tray.mjs b/scripts/qa-map-source-tray.mjs index 8449097a0..17b19f4c9 100644 --- a/scripts/qa-map-source-tray.mjs +++ b/scripts/qa-map-source-tray.mjs @@ -1265,9 +1265,10 @@ try { manager._setCommandDockPanelPinState(id, false, { persist: false, syncShare: false }); manager.setPanelCollapsed(id, true, { persist: false, syncShare: false }); } - const original = manager.setPanelCollapsed; - const probe = { transitions: [], restore: () => { manager.setPanelCollapsed = original; } }; - manager.setPanelCollapsed = function (id, ...args) { + const chrome = manager._panelChrome; + const original = chrome.setPanelCollapsed; + const probe = { transitions: [], restore: () => { chrome.setPanelCollapsed = original; } }; + chrome.setPanelCollapsed = function (id, ...args) { const before = document.getElementById(id)?.classList.contains('collapsed'); const result = original.call(this, id, ...args); const after = document.getElementById(id)?.classList.contains('collapsed'); @@ -1474,7 +1475,8 @@ try { getStats: () => ({ count: 1, lastUpdate: Date.now(), source: 'Local focus fixture' }), }); state.ids.push(transitionId); - manager._renderToggles(); + const { application } = await import('/src/main.js'); + application.getComponents().data.presentation.panel._renderToggles(); const offId = manager.getAll().find((layer) => !state.ids.includes(layer.id) && layer.showInTogglePanel && !layer.enabled)?.id; return { ready: Boolean(offId), offId, fixtureIds: state.ids.slice(0, 2), transitionId, @@ -1514,12 +1516,13 @@ try { JSON.stringify({ setup: enabled ? 'dev QA fixture' : 'real production OFF row; not activated', navigation, state })); await page.screenshot({ path: path.join(shotsDir, `${width}-data-${label.toLowerCase()}-focus.png`) }); if (label === 'OFF') { - const passive = await page.evaluate((layerId) => { + const passive = await page.evaluate(async (layerId) => { const probe = window.__qaDataFocus; const focusBefore = document.activeElement; const productionBefore = JSON.stringify(probe.production()); const durableBefore = localStorage.getItem('gev:layer-state:v2'); - window.__godsEyeView.dataManager._refreshTogglePanel(); + const { application } = await import('/src/main.js'); + application.getComponents().data.presentation.refresh(); return { focusRetained: document.activeElement === focusBefore && focusBefore.matches('.data-toggle-btn') @@ -1625,7 +1628,8 @@ try { state.transition?.releaseDisable?.(); const removed = []; for (const id of state.ids) removed.push(await window.__gevQaUnregisterLayer(manager, id)); - manager._renderToggles(); + const { application } = await import('/src/main.js'); + application.getComponents().data.presentation.panel._renderToggles(); window.__godsEyeView.styleManager.setPanelCollapsed('data-panel', state.collapsed, { persist: false, syncShare: false }); // A native user-origin toggle legitimately asks the production state // coordinator to persist. Restore the exact pre-fixture value so this diff --git a/src/cameraHandoff.test.mjs b/src/cameraHandoff.test.mjs index 2d2580120..9ba486f55 100644 --- a/src/cameraHandoff.test.mjs +++ b/src/cameraHandoff.test.mjs @@ -1,3 +1,4 @@ +import { readShellSource, shellMethod } from './testSupport/readShellSource.mjs'; import { readLayerSource } from './testSupport/readLayerSource.mjs'; import { StyleManager } from './ui/applicationShell.js'; import { enter as cockpitEnter, navigateContext } from './ui/cockpitTrackingController.js'; @@ -9,7 +10,7 @@ import test from 'node:test'; import { fileURLToPath } from 'node:url'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); -const ui = fs.readFileSync(path.join(ROOT, 'src', 'ui', 'applicationShell.js'), 'utf8'); +const ui = readShellSource(); const firms = readLayerSource(path.join(ROOT, 'src', 'data', 'firmsHeatmap.js')); const vessels = readLayerSource(path.join(ROOT, 'src', 'data', 'aisLiveVessels.js')); const voice = fs.readFileSync(path.join(ROOT, 'src', 'voice', 'gevActions.js'), 'utf8'); @@ -18,8 +19,8 @@ const cockpitTracking = fs.readFileSync(path.join(ROOT, 'src', 'cockpitTracking. 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(); + if (source === ui && typeof shellMethod(name) === 'function') { + const method = shellMethod(name).toString(); return method.slice(method.indexOf(') {') + 3, -1); } const match = source.match(pattern); @@ -174,7 +175,7 @@ test('accepted navigation releases through PR15-aware ownership before flight', 'explicit navigation', ); ordered(run, [ - 'cockpitActive: !!this.cockpitView?.active', + 'cockpitActive: this.isCockpitActive()', 'stamp: () => this._stampNavigation()', 'release: () => this._releaseFollowCamera(releaseOptions)', 'navigate,', @@ -266,7 +267,7 @@ test('deferred search releases only after its final authority check', () => { test('a direct globe gesture retires delayed camera and selection restore only', () => { assert.match( ui, - /this\._initialShareGestureHandler = \(\) => \{[\s\S]*?!this\._resolveInitialShareRestore[\s\S]*?stampInitialShareGesture\(\(options\) => this\._stampNavigation\(options\)\);/, + /this\._initialShareGestureHandler = \(\) => \{[\s\S]*?!this\._resolveInitialShareRestore[\s\S]*?stampInitialShareGesture\(\(options\) =>\s*this\.navigation\._stampNavigation\(options\),?\s*\);/, ); assert.match( ui, @@ -300,10 +301,11 @@ test('newer navigation, reset, Cockpit, and teardown share one generation', () = const dispose = body(ui, /async dispose\(\) \{([\s\S]*?)\n \}/, 'dispose'); ordered(dispose, [ 'this._disposed = true;', - 'this._stampNavigation();', + 'this._navigation.destroy();', 'this._removeWorldRequestFocusListener?.();', 'await this._contextControls.restoreForDisposal();', ], 'dispose invalidation'); + assert.match(shellMethod('destroy').toString(), /this\._stampNavigation\(\)/); }); test('teardown synchronously closes immediate camera entry points', () => { diff --git a/src/cctvFocusPolicy.test.mjs b/src/cctvFocusPolicy.test.mjs index faf70f9f6..13fbff552 100644 --- a/src/cctvFocusPolicy.test.mjs +++ b/src/cctvFocusPolicy.test.mjs @@ -1,3 +1,4 @@ +import { readShellSource } from './testSupport/readShellSource.mjs'; import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; @@ -73,7 +74,7 @@ test('CCTV enable retains a pre-await tracking snapshot when tracking clears dur assert.match(diagnostics[0][0], /before setEnabled await/); assert.match(diagnostics[1][0], /after setEnabled await/); - const uiSource = readFileSync(new URL('./ui/applicationShell.js', import.meta.url), 'utf8'); + const uiSource = readShellSource(); assert.match(uiSource, /await runCctvLayerEnableTransition\(\{/); }); @@ -114,6 +115,6 @@ test('a disposed UI cannot activate a camera when enabling finishes late', async release(); await pending; assert.equal(activations, 0); - const uiSource = readFileSync(new URL('./ui/applicationShell.js', import.meta.url), 'utf8'); + const uiSource = readShellSource(); assert.match(uiSource, /shouldFocus: \(\) =>\s*!this\._disposed\s*&&\s*this\._dataManager\.isEnabled\('cctv'\)/); }); diff --git a/src/cctvFocusRequest.test.mjs b/src/cctvFocusRequest.test.mjs index 64b4c20de..be05ef7cb 100644 --- a/src/cctvFocusRequest.test.mjs +++ b/src/cctvFocusRequest.test.mjs @@ -1,3 +1,4 @@ +import { readShellSource } from './testSupport/readShellSource.mjs'; import test from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; @@ -57,7 +58,7 @@ test('UI CCTV focus listener registration disposes the exact added callback once assert.strictEqual(added[0].callback, listener); assert.strictEqual(removed[0].callback, added[0].callback); - const uiSource = readFileSync(new URL('./ui/applicationShell.js', import.meta.url), 'utf8'); + const uiSource = readShellSource(); assert.match( uiSource, /_removeCctvRequestFocusListener = registerCctvFocusRequestListener\([\s\S]+this\._cctvRequestFocusHandler/, diff --git a/src/cockpitMarkup.test.mjs b/src/cockpitMarkup.test.mjs index 05803fa34..8acf28d16 100644 --- a/src/cockpitMarkup.test.mjs +++ b/src/cockpitMarkup.test.mjs @@ -1,3 +1,4 @@ +import { readShellSource, shellMethod } from './testSupport/readShellSource.mjs'; import { expandApplicationHtml } from '../build/application-html.js'; import { readLayerSource } from './testSupport/readLayerSource.mjs'; import { PanelLayoutController } from './ui/panelLayoutController.js'; @@ -28,7 +29,7 @@ import { fileURLToPath } from 'node:url'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const html = expandApplicationHtml(fs.readFileSync(path.join(ROOT, 'index.html'), 'utf8')); -const ui = fs.readFileSync(path.join(ROOT, 'src', 'ui', 'applicationShell.js'), 'utf8'); +const ui = readShellSource(); const css = readStylesheet(path.join(ROOT, 'style.css')); const sceneDirector = fs.readFileSync(path.join(ROOT, 'src', 'scenes', 'director.js'), 'utf8'); const manager = fs.readFileSync(path.join(ROOT, 'src', 'data', 'lifecycle.js'), 'utf8'); @@ -203,7 +204,8 @@ test('programmatic Context layer changes cannot bypass explicit expansion policy }); test('share startup isolates panel defaults from recipient-local collapse preferences', () => { - const parseIndex = ui.indexOf('this._initialShareState = this.shareLinkManager.parseInitialHash();'); + const parseIndex = ui.indexOf('this._shareRestoration.attachLinks(this.shareLinkManager);'); + assert.match(shellMethod('attachLinks').toString(), /this\._initialShareState = shareLinkManager\.parseInitialHash\(\)/); const panelChromeIndex = ui.indexOf('this._initPanelChrome();'); assert.ok(parseIndex >= 0, 'initial share state must be parsed during UI construction'); assert.ok( @@ -211,11 +213,11 @@ test('share startup isolates panel defaults from recipient-local collapse prefer 'share state must be known before panel chrome can read recipient-local preferences', ); assert.equal( - (ui.match(/this\.shareLinkManager\.parseInitialHash\(\)/g) || []).length, + (ui.match(/shareLinkManager\.parseInitialHash\(\)/g) || []).length, 1, 'startup must parse the incoming share exactly once', ); - const panelChrome = ui.match(/_initPanelChrome\(\) \{([\s\S]*?)\n \}\n\n \/\*\*/); + const panelChrome = { 1: shellMethod('_initPanelChrome').toString() }; assert.ok(panelChrome, 'panel chrome initializer is missing'); assert.match( panelChrome[1], @@ -397,7 +399,8 @@ test('fresh Cockpit entry temporarily collapses map panels and exit restores the assert.match(entryPanels[1], new RegExp(`'${panelId}'`), `${panelId} must collapse on entry`); } - const callback = ui.match(/onEntered: \(\) => \{([\s\S]*?)\n \},\n onExited:/); + assert.match(ui, /onEntered: \(\) => this\._panelChrome\.enterCockpit\(\)/); + const callback = { 1: shellMethod('enterCockpit').toString() }; assert.ok(callback, 'Cockpit onEntered callback is missing'); assert.match( callback[1], @@ -426,7 +429,8 @@ test('fresh Cockpit entry temporarily collapses map panels and exit restores the 'normal Context must not reopen over Cockpit', ); - const exitCallback = ui.match(/onExited: \(\) => \{([\s\S]*?)\n \},\n restoreTrackingFrame:/); + assert.match(ui, /onExited: \(\) => this\._panelChrome\.exitCockpit\(\)/); + const exitCallback = { 1: shellMethod('exitCockpit').toString() }; assert.ok(exitCallback, 'Cockpit onExited callback is missing'); assert.match( exitCallback[1], diff --git a/src/contactsDetectionPolicy.test.mjs b/src/contactsDetectionPolicy.test.mjs index 331151f8e..862d055ce 100644 --- a/src/contactsDetectionPolicy.test.mjs +++ b/src/contactsDetectionPolicy.test.mjs @@ -1,3 +1,4 @@ +import { readShellSource, shellMethod } from './testSupport/readShellSource.mjs'; import { _syncContextModeButtons } from './ui/contextPresentation.js'; // Contacts-scoped detection (owner playtest 2026-08-18: "when you click on // Contacts, detections should just turn on, and they should stay on in Cockpit @@ -28,7 +29,7 @@ import { import { canonicalizeDensity } from './data/detectionPolicy.js'; // Follow the UI wiring and its extracted preset definitions. -const uiSource = fs.readFileSync(new URL('./ui/applicationShell.js', import.meta.url), 'utf8') +const uiSource = readShellSource() + '\n' + fs.readFileSync(new URL('./ui/visualPresets.js', import.meta.url), 'utf8'); /** @@ -140,10 +141,7 @@ test('the Contacts preset is the very object the military styles apply', () => { }); test('the shared preset applier ignores the style override flag — the caller owns it', () => { - const applier = uiSource.slice( - uiSource.indexOf('_applyDetectionPreset(det) {'), - uiSource.indexOf('Applies the global post-processing baseline'), - ); + const applier = shellMethod('_applyDetectionPreset').toString(); assert.ok(applier.length > 0); assert.doesNotMatch(applier, /_detectionUserOverridden/); // The STYLE path still gates on it; Contacts deliberately does not. diff --git a/src/contextSessionOrdering.test.mjs b/src/contextSessionOrdering.test.mjs index af836c793..6b1c90904 100644 --- a/src/contextSessionOrdering.test.mjs +++ b/src/contextSessionOrdering.test.mjs @@ -1,3 +1,4 @@ +import { readShellSource } from './testSupport/readShellSource.mjs'; import { _handleContextLayerChange } from './ui/contextLayerChanges.js'; import { connectContextManager } from './ui/contextSubscriptions.js'; import { _restoreContextSession, _restoreContextSessionAfterLayerSettles } from './ui/contextSession.js'; @@ -23,7 +24,7 @@ import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; -const src = readFileSync(fileURLToPath(new URL('./ui/applicationShell.js', import.meta.url)), 'utf8'); +const src = readShellSource(); const handler = _handleContextLayerChange.toString(); diff --git a/src/contextTabKeyboard.test.mjs b/src/contextTabKeyboard.test.mjs index e71daabff..55ff429a2 100644 --- a/src/contextTabKeyboard.test.mjs +++ b/src/contextTabKeyboard.test.mjs @@ -1,3 +1,4 @@ +import { readShellSource } from './testSupport/readShellSource.mjs'; import { expandApplicationHtml } from '../build/application-html.js'; import { readStylesheet } from './testSupport/readStylesheet.mjs'; import { _selectContextMode } from './ui/contextTransactions.js'; @@ -13,7 +14,7 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; const html = expandApplicationHtml(readFileSync(new URL('../index.html', import.meta.url), 'utf8')); -const ui = readFileSync(new URL('./ui/applicationShell.js', import.meta.url), 'utf8'); +const ui = readShellSource(); const css = readStylesheet(new URL('../style.css', import.meta.url)); test('Contacts and Space Missions both participate in the ordinary Tab sequence', () => { diff --git a/src/creditAttribution.test.mjs b/src/creditAttribution.test.mjs index 593fb5395..cdbdf8aaa 100644 --- a/src/creditAttribution.test.mjs +++ b/src/creditAttribution.test.mjs @@ -1,3 +1,4 @@ +import { readShellSource } from './testSupport/readShellSource.mjs'; import { readStylesheet } from './testSupport/readStylesheet.mjs'; import assert from 'node:assert/strict'; import fs from 'node:fs'; @@ -7,7 +8,7 @@ import { fileURLToPath } from 'node:url'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const css = readStylesheet(path.join(ROOT, 'style.css')); -const ui = fs.readFileSync(path.join(ROOT, 'src', 'ui', 'applicationShell.js'), 'utf8'); +const ui = readShellSource(); /* * Required-attribution keep-out pin. diff --git a/src/data/detectionHost.test.mjs b/src/data/detectionHost.test.mjs index e76f6a75f..7ad94c43b 100644 --- a/src/data/detectionHost.test.mjs +++ b/src/data/detectionHost.test.mjs @@ -1,3 +1,4 @@ +import { readShellSource } from '../testSupport/readShellSource.mjs'; import test from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; @@ -671,7 +672,7 @@ test('pathological detection paint holds alternate frames without freezing share test('detection cannot resurrect a private canvas, listener, matrix, resize, clear, or UI inventory', () => { const source = readFileSync(new URL('./detection.js', import.meta.url), 'utf8'); - const uiSource = readFileSync(new URL('../ui/applicationShell.js', import.meta.url), 'utf8') + const uiSource = readShellSource() + '\n' + readFileSync(new URL('../ui/visualPresets.js', import.meta.url), 'utf8'); assert.doesNotMatch(source, /createElement\(\s*['"]canvas['"]\s*\)/); assert.doesNotMatch(source, /postRender\.addEventListener/); diff --git a/src/data/layerState.test.mjs b/src/data/layerState.test.mjs index fe964d19f..4fada4dcb 100644 --- a/src/data/layerState.test.mjs +++ b/src/data/layerState.test.mjs @@ -1,3 +1,4 @@ +import { readShellSource } from '../testSupport/readShellSource.mjs'; import { expandApplicationHtml } from '../../build/application-html.js'; import { readLayerSource } from '../testSupport/readLayerSource.mjs'; import test from 'node:test'; @@ -373,7 +374,7 @@ test('a fresh boot starts 3D aircraft ON in proximity — codec, both layers, an assert.match(source, /^\s*(?:let |flightState\.)_models3dMode = 'proximity';/m, `${name}: and starts in proximity, matching the codec default`); } - const ui = await readFile(new URL('../ui/applicationShell.js', import.meta.url), 'utf8'); + const ui = await readShellSource(); assert.match(ui, /^\s*this\.(?:flightState\.)?_models3dEnabled = true;$/m, 'ui.js: the DISPLAY rail believes 3D is on before any layer-state sync arrives'); assert.match(ui, /this\.(?:flightState\.)?_models3dMode = 'proximity';/, diff --git a/src/loadingFeedback.test.mjs b/src/loadingFeedback.test.mjs index b57c92eb1..01d00b1d6 100644 --- a/src/loadingFeedback.test.mjs +++ b/src/loadingFeedback.test.mjs @@ -1,3 +1,5 @@ +import { ShareRestoration } from './ui/shareRestoration.js'; +import { readShellSource, shellMethod } from './testSupport/readShellSource.mjs'; import { expandApplicationHtml } from '../build/application-html.js'; import { readStylesheet } from './testSupport/readStylesheet.mjs'; import test from 'node:test'; @@ -91,11 +93,9 @@ test('deferred terminal notices lose ownership to newer acquisition epochs and d }); test('share-follow failures use the universal top-center status instead of the bottom toast', () => { - const ui = readFileSync(new URL('./ui/applicationShell.js', import.meta.url), 'utf8'); - const start = ui.indexOf(' _handleShareTrackingRestoreStatus(result) {'); - const end = ui.indexOf('\n _initGlobalContextPanel() {', start); - const handler = ui.slice(start, end); - assert.match(handler, /this\._showGlobalStatusNotice\(message\)/); + const ui = readShellSource(); + const handler = shellMethod('_handleShareTrackingRestoreStatus').toString(); + assert.match(handler, /this\.showStatus\(message\)/); assert.match(handler, /this\.initialRestorePromise\.then\(showAfterStartupCover\)/); assert.match(handler, /this\._lifetime\.frame\(\(\) => \{/); assert.match(handler, /this\._lifetime\.listen\(\s*startupCover,\s*'transitionend',\s*showOnce,\s*\{ once: true \},?\s*\)/); @@ -197,14 +197,15 @@ test('replacement, repetition, and hidden-tab elapsed time use the newest fixed }); test('universal notice lifecycle clears on dispose and uses the one top-center live region', () => { - const ui = readFileSync(new URL('./ui/applicationShell.js', import.meta.url), 'utf8'); + const ui = readShellSource(); const html = expandApplicationHtml(readFileSync(new URL('../index.html', import.meta.url), 'utf8')); const disposeStart = ui.indexOf(' async dispose() {'); const disposeEnd = ui.indexOf('\n }\n', disposeStart); const dispose = ui.slice(disposeStart, disposeEnd); assert.match(dispose, /this\._feedback\._globalStatusNotice = null;/); - assert.match(dispose, /this\._shareTrackingNoticeGeneration \+= 1;/); + assert.match(dispose, /this\._shareRestoration\.destroy\(\)/); + assert.match(ShareRestoration.prototype.destroy.toString(), /this\._shareTrackingNoticeGeneration \+= 1;/); assert.match(html, /