mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
feat(ui): render application templates as Vue views mounted before wiring
Templates listed in build/applicationViews.js expand to mount hosts, and mountApplicationViews() renders them before translation and wiring. Markup tests read the page through a server-side render helper, the i18n scanner reads view templates, and Vue preserves whitespace so views keep the exact DOM. No template is converted yet.
This commit is contained in:
1 parent
b7e7058a5c
commit
9d24945143
32 files changed
+243
-63
No files matched your search
@@ -1,4 +1,5 @@
|
|||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { APPLICATION_VIEW_NAMES } from './applicationViews.js';
|
||||||
|
|
||||||
export const APPLICATION_TEMPLATES = Object.freeze([
|
export const APPLICATION_TEMPLATES = Object.freeze([
|
||||||
'scene-chrome',
|
'scene-chrome',
|
||||||
@@ -12,6 +13,16 @@ export const APPLICATION_TEMPLATES = Object.freeze([
|
|||||||
'hud-loading',
|
'hud-loading',
|
||||||
]);
|
]);
|
||||||
const allowed = new Set(APPLICATION_TEMPLATES);
|
const allowed = new Set(APPLICATION_TEMPLATES);
|
||||||
|
const views = new Set(APPLICATION_VIEW_NAMES);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The element a Vue view renders into. `display: contents` keeps the view's
|
||||||
|
* own elements laid out as children of the page.
|
||||||
|
* @param {string} name Application template name.
|
||||||
|
*/
|
||||||
|
export function applicationViewHost(name) {
|
||||||
|
return `<div class="gev-view" data-gev-view="${name}"></div>\n`;
|
||||||
|
}
|
||||||
|
|
||||||
/** Expand only known component templates; markers cannot name filesystem paths. */
|
/** Expand only known component templates; markers cannot name filesystem paths. */
|
||||||
export function expandApplicationHtml(html) {
|
export function expandApplicationHtml(html) {
|
||||||
@@ -20,6 +31,7 @@ export function expandApplicationHtml(html) {
|
|||||||
(_, name) => {
|
(_, name) => {
|
||||||
if (!allowed.has(name))
|
if (!allowed.has(name))
|
||||||
throw new Error(`Unknown application template: ${name}`);
|
throw new Error(`Unknown application template: ${name}`);
|
||||||
|
if (views.has(name)) return applicationViewHost(name);
|
||||||
return readFileSync(
|
return readFileSync(
|
||||||
new URL(`../src/ui/templates/${name}.html`, import.meta.url),
|
new URL(`../src/ui/templates/${name}.html`, import.meta.url),
|
||||||
'utf8',
|
'utf8',
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* Application templates that are now Vue views (src/ui/views). Their markers
|
||||||
|
* expand to a mount host instead of static markup, and
|
||||||
|
* `mountApplicationViews()` renders them before the page is wired. The
|
||||||
|
* loading screen stays static so it paints before any script runs.
|
||||||
|
*/
|
||||||
|
export const APPLICATION_VIEW_NAMES = Object.freeze([]);
|
||||||
+2
-1
@@ -2,6 +2,7 @@ import { fileURLToPath } from 'node:url';
|
|||||||
import { applicationHtmlPlugin } from './application-html.js';
|
import { applicationHtmlPlugin } from './application-html.js';
|
||||||
import cesium from 'vite-plugin-cesium';
|
import cesium from 'vite-plugin-cesium';
|
||||||
import vue from '@vitejs/plugin-vue';
|
import vue from '@vitejs/plugin-vue';
|
||||||
|
import { VUE_PLUGIN_OPTIONS } from './vueOptions.js';
|
||||||
import { DEFAULT_ALLOWED_HOSTS, hostCheckPlugin } from './allowedHosts.js';
|
import { DEFAULT_ALLOWED_HOSTS, hostCheckPlugin } from './allowedHosts.js';
|
||||||
import { embedFramingPlugin } from './embed-framing.js';
|
import { embedFramingPlugin } from './embed-framing.js';
|
||||||
import { panelBuildPlugin } from './panel.js';
|
import { panelBuildPlugin } from './panel.js';
|
||||||
@@ -158,7 +159,7 @@ export function createBrowserViteConfig({
|
|||||||
cesium(),
|
cesium(),
|
||||||
applicationHtmlPlugin(),
|
applicationHtmlPlugin(),
|
||||||
// Application views are Vue single-file components.
|
// Application views are Vue single-file components.
|
||||||
vue(),
|
vue(VUE_PLUGIN_OPTIONS),
|
||||||
...plugins,
|
...plugins,
|
||||||
embedFramingPlugin(),
|
embedFramingPlugin(),
|
||||||
panelBuildPlugin(),
|
panelBuildPlugin(),
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* Template compiler options shared by the browser build, the boundary check
|
||||||
|
* and the unit-test loader. Whitespace is preserved so a view renders exactly
|
||||||
|
* the DOM its static markup did: inline layouts depend on text between nodes.
|
||||||
|
*/
|
||||||
|
export const VUE_COMPILER_OPTIONS = Object.freeze({ whitespace: 'preserve' });
|
||||||
|
|
||||||
|
/** `@vitejs/plugin-vue` options for application builds. */
|
||||||
|
export const VUE_PLUGIN_OPTIONS = Object.freeze({
|
||||||
|
template: { compilerOptions: VUE_COMPILER_OPTIONS },
|
||||||
|
});
|
||||||
+11
-1
@@ -53,7 +53,17 @@ They contain no application state.
|
|||||||
2. **Rail.** `PanelChrome` enforces one panel per side; the panel store
|
2. **Rail.** `PanelChrome` enforces one panel per side; the panel store
|
||||||
mirrors each side's panels and the rail renders from it. Done.
|
mirrors each side's panels and the rail renders from it. Done.
|
||||||
3. **Panels.** Each template and its wiring become a component, keeping ids,
|
3. **Panels.** Each template and its wiring become a component, keeping ids,
|
||||||
classes, `data-i18n` text and CSS. Order: Scenes, Weather, Recent imagery,
|
classes, `data-i18n` text and CSS. A converted template is listed in
|
||||||
|
`build/applicationViews.js`; its `gev:template` marker then expands to a
|
||||||
|
`.gev-view` host (`display: contents`), and `mountApplicationViews()`
|
||||||
|
(`src/ui/views/index.js`) renders the view into it before the page is
|
||||||
|
translated and wired. `createApplication()` calls it too, so host pages
|
||||||
|
need no change. Views belong to the page and are never unmounted. The
|
||||||
|
loading screen stays static so it paints before any script runs. Markup
|
||||||
|
tests read `renderApplicationHtml()` from `src/testSupport`, which renders
|
||||||
|
the views on the server. Vue preserves template whitespace so a view's DOM
|
||||||
|
matches its template exactly. First the markup moves, unchanged;
|
||||||
|
then each view takes over its wiring. Order: Scenes, Weather, Recent imagery,
|
||||||
Street level, CCTV, Layers, Display, Context and Radio, the command dock,
|
Street level, CCTV, Layers, Display, Context and Radio, the command dock,
|
||||||
the phone shell, then Cockpit. Layers that write into the right rail
|
the phone shell, then Cockpit. Layers that write into the right rail
|
||||||
directly gain a slot instead.
|
directly gain a slot instead.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { readFile, realpath } from 'node:fs/promises';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||||
import vue from '@vitejs/plugin-vue';
|
import vue from '@vitejs/plugin-vue';
|
||||||
|
import { VUE_PLUGIN_OPTIONS } from '../build/vueOptions.js';
|
||||||
import { build, normalizePath } from 'vite';
|
import { build, normalizePath } from 'vite';
|
||||||
|
|
||||||
/** Build each browser or Node export group and reject imports outside its declared ownership. */
|
/** Build each browser or Node export group and reject imports outside its declared ownership. */
|
||||||
@@ -100,7 +101,7 @@ export async function checkPackageBoundaries(root) {
|
|||||||
ssr: node ? { noExternal: true } : undefined,
|
ssr: node ? { noExternal: true } : undefined,
|
||||||
logLevel: 'silent',
|
logLevel: 'silent',
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(VUE_PLUGIN_OPTIONS),
|
||||||
{
|
{
|
||||||
name: 'check-package-ownership',
|
name: 'check-package-ownership',
|
||||||
moduleParsed(info) {
|
moduleParsed(info) {
|
||||||
|
|||||||
+22
-6
@@ -119,10 +119,10 @@ export function extractMessages() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const markup = trackedFiles('index.html', 'src/ui/templates/*.html');
|
const markup = markupFiles();
|
||||||
const TAG = /<([a-zA-Z][\w-]*)(\s[^>]*?)\/?>([^<]*)/gs;
|
const TAG = /<([a-zA-Z][\w-]*)(\s[^>]*?)\/?>([^<]*)/gs;
|
||||||
for (const file of markup) {
|
for (const file of markup) {
|
||||||
const html = readFileSync(path.join(ROOT, file), 'utf8');
|
const html = readMarkup(file);
|
||||||
for (const [, tag, attrs, text] of html.matchAll(TAG)) {
|
for (const [, tag, attrs, text] of html.matchAll(TAG)) {
|
||||||
const where = `${file} <${tag}>`;
|
const where = `${file} <${tag}>`;
|
||||||
if (/\sdata-i18n(\s|=|$)/.test(attrs)) {
|
if (/\sdata-i18n(\s|=|$)/.test(attrs)) {
|
||||||
@@ -404,10 +404,8 @@ export function unmarkedScript(file, original) {
|
|||||||
*/
|
*/
|
||||||
export function findUnmarked() {
|
export function findUnmarked() {
|
||||||
const found = [];
|
const found = [];
|
||||||
for (const file of trackedFiles('index.html', 'src/ui/templates/*.html'))
|
for (const file of markupFiles())
|
||||||
found.push(
|
found.push(...unmarkedMarkup(file, readMarkup(file)));
|
||||||
...unmarkedMarkup(file, readFileSync(path.join(ROOT, file), 'utf8')),
|
|
||||||
);
|
|
||||||
for (const file of sourceFiles().filter(
|
for (const file of sourceFiles().filter(
|
||||||
(name) => !NOT_UI.some((pattern) => pattern.test(name)),
|
(name) => !NOT_UI.some((pattern) => pattern.test(name)),
|
||||||
))
|
))
|
||||||
@@ -423,6 +421,24 @@ export function findUnmarked() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Static markup opted in with data-i18n*: the page, its templates and views. */
|
||||||
|
function markupFiles() {
|
||||||
|
return trackedFiles(
|
||||||
|
'index.html',
|
||||||
|
'src/ui/templates/*.html',
|
||||||
|
'src/ui/views/*.vue',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A file's markup; for a single-file component, its template block. */
|
||||||
|
function readMarkup(file) {
|
||||||
|
const source = readFileSync(path.join(ROOT, file), 'utf8');
|
||||||
|
if (!file.endsWith('.vue')) return source;
|
||||||
|
const start = source.indexOf('<template>');
|
||||||
|
const end = source.lastIndexOf('</template>');
|
||||||
|
return start === -1 ? '' : source.slice(start + '<template>'.length, end);
|
||||||
|
}
|
||||||
|
|
||||||
// Text that never reaches the page as UI: tool results and descriptions for
|
// Text that never reaches the page as UI: tool results and descriptions for
|
||||||
// AI clients (MCP and the voice model).
|
// AI clients (MCP and the voice model).
|
||||||
const NOT_UI = [/^src\/tools\//];
|
const NOT_UI = [/^src\/tools\//];
|
||||||
|
|||||||
@@ -17,8 +17,12 @@
|
|||||||
},
|
},
|
||||||
"application": {
|
"application": {
|
||||||
"exports": ["./application"],
|
"exports": ["./application"],
|
||||||
"modules": ["src/app/application.js"],
|
"modules": [
|
||||||
"external": []
|
"src/app/application.js",
|
||||||
|
"src/ui/vueRuntime.js",
|
||||||
|
"src/ui/views/index.js"
|
||||||
|
],
|
||||||
|
"external": ["vue", "pinia"]
|
||||||
},
|
},
|
||||||
"viewer": {
|
"viewer": {
|
||||||
"exports": ["./application/viewer"],
|
"exports": ["./application/viewer"],
|
||||||
@@ -40,7 +44,9 @@
|
|||||||
"build/application-html.js",
|
"build/application-html.js",
|
||||||
"build/allowedHosts.js",
|
"build/allowedHosts.js",
|
||||||
"build/embed-framing.js",
|
"build/embed-framing.js",
|
||||||
"build/panel.js"
|
"build/panel.js",
|
||||||
|
"build/applicationViews.js",
|
||||||
|
"build/vueOptions.js"
|
||||||
],
|
],
|
||||||
"external": ["vite-plugin-cesium", "@vitejs/plugin-vue"]
|
"external": ["vite-plugin-cesium", "@vitejs/plugin-vue"]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { mountApplicationViews } from '../ui/views/index.js';
|
||||||
const START_ORDER = ['scene', 'controls', 'data', 'tools'];
|
const START_ORDER = ['scene', 'controls', 'data', 'tools'];
|
||||||
// Controls cancel restoration while the data manager and viewer still exist.
|
// Controls cancel restoration while the data manager and viewer still exist.
|
||||||
const STOP_ORDER = ['tools', 'controls', 'data', 'scene'];
|
const STOP_ORDER = ['tools', 'controls', 'data', 'scene'];
|
||||||
@@ -15,6 +16,8 @@ export function createApplication({
|
|||||||
createData,
|
createData,
|
||||||
createTools,
|
createTools,
|
||||||
}) {
|
}) {
|
||||||
|
// A host's page needs its views before any phase reads the markup.
|
||||||
|
mountApplicationViews();
|
||||||
const factories = {
|
const factories = {
|
||||||
scene: createScene,
|
scene: createScene,
|
||||||
controls: createControls,
|
controls: createControls,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {
|
|||||||
readShellSource,
|
readShellSource,
|
||||||
shellMethod,
|
shellMethod,
|
||||||
} from './testSupport/readShellSource.mjs';
|
} from './testSupport/readShellSource.mjs';
|
||||||
import { expandApplicationHtml } from '../build/application-html.js';
|
import { renderApplicationHtml } from './testSupport/applicationHtml.mjs';
|
||||||
import { readLayerSource } from './testSupport/readLayerSource.mjs';
|
import { readLayerSource } from './testSupport/readLayerSource.mjs';
|
||||||
import { PanelLayoutController } from './ui/panelLayoutController.js';
|
import { PanelLayoutController } from './ui/panelLayoutController.js';
|
||||||
import { readShellElements } from './ui/shellElements.js';
|
import { readShellElements } from './ui/shellElements.js';
|
||||||
@@ -46,7 +46,7 @@ import { fileURLToPath } from 'node:url';
|
|||||||
|
|
||||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
|
|
||||||
const html = expandApplicationHtml(
|
const html = await renderApplicationHtml(
|
||||||
fs.readFileSync(path.join(ROOT, 'index.html'), 'utf8'),
|
fs.readFileSync(path.join(ROOT, 'index.html'), 'utf8'),
|
||||||
);
|
);
|
||||||
const ui = readShellSource();
|
const ui = readShellSource();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { expandApplicationHtml } from '../build/application-html.js';
|
import { renderApplicationHtml } from './testSupport/applicationHtml.mjs';
|
||||||
import { readStylesheet } from './testSupport/readStylesheet.mjs';
|
import { readStylesheet } from './testSupport/readStylesheet.mjs';
|
||||||
import { _selectContextMode } from './ui/contextTransactions.js';
|
import { _selectContextMode } from './ui/contextTransactions.js';
|
||||||
import { _syncContextModeButtons } from './ui/contextPresentation.js';
|
import { _syncContextModeButtons } from './ui/contextPresentation.js';
|
||||||
@@ -11,7 +11,7 @@ import { readFileSync } from 'node:fs';
|
|||||||
import { test } from 'node:test';
|
import { test } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
|
|
||||||
const html = expandApplicationHtml(readFileSync(new URL('../index.html', import.meta.url), 'utf8'));
|
const html = await renderApplicationHtml(readFileSync(new URL('../index.html', import.meta.url), 'utf8'));
|
||||||
const css = readStylesheet(new URL('../style.css', import.meta.url));
|
const css = readStylesheet(new URL('../style.css', import.meta.url));
|
||||||
|
|
||||||
test('Contacts and Space Missions both participate in the ordinary Tab sequence', () => {
|
test('Contacts and Space Missions both participate in the ordinary Tab sequence', () => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { expandApplicationHtml } from '../../build/application-html.js';
|
import { renderApplicationHtml } from '../testSupport/applicationHtml.mjs';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
} from './detectionPolicy.js';
|
} from './detectionPolicy.js';
|
||||||
import { KEYHOLE_OUTSIDE_OPACITY_DEFAULT } from '../celestialRing.js';
|
import { KEYHOLE_OUTSIDE_OPACITY_DEFAULT } from '../celestialRing.js';
|
||||||
|
|
||||||
const indexHtml = expandApplicationHtml(fs.readFileSync(new URL('../../index.html', import.meta.url), 'utf8'));
|
const indexHtml = await renderApplicationHtml(fs.readFileSync(new URL('../../index.html', import.meta.url), 'utf8'));
|
||||||
|
|
||||||
test('side aircraft brackets stay readable without changing zero-opacity intent', () => {
|
test('side aircraft brackets stay readable without changing zero-opacity intent', () => {
|
||||||
assert.equal(detectionBracketAlpha('AIR', 0), 0);
|
assert.equal(detectionBracketAlpha('AIR', 0), 0);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { encodeParams, decodeParams } from '../layers/streetLevel/params.js';
|
import { encodeParams, decodeParams } from '../layers/streetLevel/params.js';
|
||||||
import { readShellSource } from '../testSupport/readShellSource.mjs';
|
import { readShellSource } from '../testSupport/readShellSource.mjs';
|
||||||
import { expandApplicationHtml } from '../../build/application-html.js';
|
import { renderApplicationHtml } from '../testSupport/applicationHtml.mjs';
|
||||||
import { readLayerSource } from '../testSupport/readLayerSource.mjs';
|
import { readLayerSource } from '../testSupport/readLayerSource.mjs';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
@@ -734,7 +734,7 @@ test('a fresh boot starts 3D aircraft ON in proximity — codec, both layers, an
|
|||||||
/this\.(?:flightState\.)?_models3dMode = 'proximity';/,
|
/this\.(?:flightState\.)?_models3dMode = 'proximity';/,
|
||||||
'ui.js: and believes the mode is proximity',
|
'ui.js: and believes the mode is proximity',
|
||||||
);
|
);
|
||||||
const html = expandApplicationHtml(
|
const html = await renderApplicationHtml(
|
||||||
await readFile(new URL('../../index.html', import.meta.url), 'utf8'),
|
await readFile(new URL('../../index.html', import.meta.url), 'utf8'),
|
||||||
);
|
);
|
||||||
assert.match(
|
assert.match(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { expandApplicationHtml } from '../build/application-html.js';
|
import { renderApplicationHtml } from './testSupport/applicationHtml.mjs';
|
||||||
import { readStylesheet } from './testSupport/readStylesheet.mjs';
|
import { readStylesheet } from './testSupport/readStylesheet.mjs';
|
||||||
import { GEV_REALTIME_TOOLS } from '../server/providers/openai/tools.js';
|
import { GEV_REALTIME_TOOLS } from '../server/providers/openai/tools.js';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
@@ -797,8 +797,8 @@ test('the decision table is written down where the next editor will read it', ()
|
|||||||
|
|
||||||
// ── Markup, startup ordering, accessibility ─────────────────────────────────
|
// ── Markup, startup ordering, accessibility ─────────────────────────────────
|
||||||
|
|
||||||
test('markup, startup ordering and accessibility remain pinned', () => {
|
test('markup, startup ordering and accessibility remain pinned', async () => {
|
||||||
const html = expandApplicationHtml(
|
const html = await renderApplicationHtml(
|
||||||
fs.readFileSync(new URL('../index.html', import.meta.url), 'utf8'),
|
fs.readFileSync(new URL('../index.html', import.meta.url), 'utf8'),
|
||||||
);
|
);
|
||||||
const startup = fs.readFileSync(
|
const startup = fs.readFileSync(
|
||||||
@@ -915,8 +915,8 @@ test('markup, startup ordering and accessibility remain pinned', () => {
|
|||||||
assert.match(choicesBlock, /overflow-y: auto;/);
|
assert.match(choicesBlock, /overflow-y: auto;/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the card closes without a keyboard: the ESC hint is a button, kept on phones', () => {
|
test('the card closes without a keyboard: the ESC hint is a button, kept on phones', async () => {
|
||||||
const html = expandApplicationHtml(
|
const html = await renderApplicationHtml(
|
||||||
fs.readFileSync(new URL('../index.html', import.meta.url), 'utf8'),
|
fs.readFileSync(new URL('../index.html', import.meta.url), 'utf8'),
|
||||||
);
|
);
|
||||||
const module = fs.readFileSync(
|
const module = fs.readFileSync(
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { expandApplicationHtml } from '../build/application-html.js';
|
import { renderApplicationHtml } from './testSupport/applicationHtml.mjs';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
|
|
||||||
const html = expandApplicationHtml(readFileSync(new URL('../index.html', import.meta.url), 'utf8'));
|
const html = await renderApplicationHtml(readFileSync(new URL('../index.html', import.meta.url), 'utf8'));
|
||||||
const parameters = readFileSync(new URL('./ui/styleParameters.js', import.meta.url), 'utf8');
|
const parameters = readFileSync(new URL('./ui/styleParameters.js', import.meta.url), 'utf8');
|
||||||
|
|
||||||
// Focused markup guards; actual computed names are checked in Chromium.
|
// Focused markup guards; actual computed names are checked in Chromium.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ShareRestoration } from './ui/shareRestoration.js';
|
import { ShareRestoration } from './ui/shareRestoration.js';
|
||||||
import { readShellSource, shellMethod } from './testSupport/readShellSource.mjs';
|
import { readShellSource, shellMethod } from './testSupport/readShellSource.mjs';
|
||||||
import { expandApplicationHtml } from '../build/application-html.js';
|
import { renderApplicationHtml } from './testSupport/applicationHtml.mjs';
|
||||||
import { readStylesheet } from './testSupport/readStylesheet.mjs';
|
import { readStylesheet } from './testSupport/readStylesheet.mjs';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
@@ -202,9 +202,9 @@ test('replacement, repetition, and hidden-tab elapsed time use the newest fixed
|
|||||||
'elapsed wall time while hidden expires the notice instead of replaying it');
|
'elapsed wall time while hidden expires the notice instead of replaying it');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('universal notice lifecycle clears on dispose and uses the one top-center live region', () => {
|
test('universal notice lifecycle clears on dispose and uses the one top-center live region', async () => {
|
||||||
const ui = readShellSource();
|
const ui = readShellSource();
|
||||||
const html = expandApplicationHtml(readFileSync(new URL('../index.html', import.meta.url), 'utf8'));
|
const html = await renderApplicationHtml(readFileSync(new URL('../index.html', import.meta.url), 'utf8'));
|
||||||
const disposeStart = ui.indexOf(' async dispose() {');
|
const disposeStart = ui.indexOf(' async dispose() {');
|
||||||
const disposeEnd = ui.indexOf('\n }\n', disposeStart);
|
const disposeEnd = ui.indexOf('\n }\n', disposeStart);
|
||||||
const dispose = ui.slice(disposeStart, disposeEnd);
|
const dispose = ui.slice(disposeStart, disposeEnd);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { shellMethod } from './testSupport/readShellSource.mjs';
|
import { shellMethod } from './testSupport/readShellSource.mjs';
|
||||||
import { expandApplicationHtml } from '../build/application-html.js';
|
import { renderApplicationHtml } from './testSupport/applicationHtml.mjs';
|
||||||
import { createHoverDisclosure, collapsePanelOnEscape } from './ui/panelDisclosure.js';
|
import { createHoverDisclosure, collapsePanelOnEscape } from './ui/panelDisclosure.js';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
@@ -7,7 +7,7 @@ import { readFileSync } from 'node:fs';
|
|||||||
import { msg, t } from './i18n/index.js';
|
import { msg, t } from './i18n/index.js';
|
||||||
|
|
||||||
// Exercise the installed event routes and central close method, without WebGL.
|
// Exercise the installed event routes and central close method, without WebGL.
|
||||||
const markup = expandApplicationHtml(readFileSync(new URL('../index.html', import.meta.url), 'utf8'));
|
const markup = await renderApplicationHtml(readFileSync(new URL('../index.html', import.meta.url), 'utf8'));
|
||||||
const locationMarkup = markup.slice(markup.indexOf('<div id="location-bar"'), markup.indexOf('<div id="left-panel-stack"'));
|
const locationMarkup = markup.slice(markup.indexOf('<div id="location-bar"'), markup.indexOf('<div id="left-panel-stack"'));
|
||||||
const locationToggleMarkup = locationMarkup.match(/<button\b([^>]*\bid="location-bar-toggle"[^>]*)>([\s\S]*?)<\/button>/);
|
const locationToggleMarkup = locationMarkup.match(/<button\b([^>]*\bid="location-bar-toggle"[^>]*)>([\s\S]*?)<\/button>/);
|
||||||
const locationToggleAttributes = Object.fromEntries(
|
const locationToggleAttributes = Object.fromEntries(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { readShellSource } from './testSupport/readShellSource.mjs';
|
import { readShellSource } from './testSupport/readShellSource.mjs';
|
||||||
import { expandApplicationHtml } from '../build/application-html.js';
|
import { renderApplicationHtml } from './testSupport/applicationHtml.mjs';
|
||||||
import { readStylesheet } from './testSupport/readStylesheet.mjs';
|
import { readStylesheet } from './testSupport/readStylesheet.mjs';
|
||||||
// MAP STACK chip row — the dropdown's replacement control surface.
|
// MAP STACK chip row — the dropdown's replacement control surface.
|
||||||
//
|
//
|
||||||
@@ -482,8 +482,8 @@ test('the keyboard focus ring survives on the ACTIVE chip', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the Visual Presets tray owns Map Source and the retired left panel is absent', () => {
|
test('the Visual Presets tray owns Map Source and the retired left panel is absent', async () => {
|
||||||
const html = expandApplicationHtml(
|
const html = await renderApplicationHtml(
|
||||||
readFileSync(new URL('../index.html', import.meta.url), 'utf8'),
|
readFileSync(new URL('../index.html', import.meta.url), 'utf8'),
|
||||||
);
|
);
|
||||||
const ui = readShellSource();
|
const ui = readShellSource();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createNamedMarkers } from '../layers/installations/namedMarkers.js';
|
import { createNamedMarkers } from '../layers/installations/namedMarkers.js';
|
||||||
import { expandApplicationHtml } from '../../build/application-html.js';
|
import { renderApplicationHtml } from '../testSupport/applicationHtml.mjs';
|
||||||
import { readStylesheet } from '../testSupport/readStylesheet.mjs';
|
import { readStylesheet } from '../testSupport/readStylesheet.mjs';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
@@ -447,7 +447,7 @@ test('destroying before the first init leaves pre-init buffering intact', () =>
|
|||||||
env.cleanup();
|
env.cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('lifecycle is idempotent and teardown removes listeners, observers, and DOM', () => {
|
test('lifecycle is idempotent and teardown removes listeners, observers, and DOM', async () => {
|
||||||
const env = installMockEnvironment();
|
const env = installMockEnvironment();
|
||||||
assert.equal(env.document.getElementById('world-overlay-detection-surface'), null);
|
assert.equal(env.document.getElementById('world-overlay-detection-surface'), null);
|
||||||
initWorldOverlay(env.viewer);
|
initWorldOverlay(env.viewer);
|
||||||
@@ -465,7 +465,7 @@ test('lifecycle is idempotent and teardown removes listeners, observers, and DOM
|
|||||||
assert.deepEqual(root.children, [canvas],
|
assert.deepEqual(root.children, [canvas],
|
||||||
'the overlay root carries only the shared card canvas');
|
'the overlay root carries only the shared card canvas');
|
||||||
assert.doesNotMatch(
|
assert.doesNotMatch(
|
||||||
expandApplicationHtml(readFileSync(new URL('../../index.html', import.meta.url), 'utf8')),
|
await renderApplicationHtml(readFileSync(new URL('../../index.html', import.meta.url), 'utf8')),
|
||||||
/world-overlay-detection-surface/,
|
/world-overlay-detection-surface/,
|
||||||
'the surface is runtime host-owned, not static markup',
|
'the surface is runtime host-owned, not static markup',
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { readShellSource } from './testSupport/readShellSource.mjs';
|
import { readShellSource } from './testSupport/readShellSource.mjs';
|
||||||
import { expandApplicationHtml } from '../build/application-html.js';
|
import { renderApplicationHtml } from './testSupport/applicationHtml.mjs';
|
||||||
import { readStylesheet } from './testSupport/readStylesheet.mjs';
|
import { readStylesheet } from './testSupport/readStylesheet.mjs';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
@@ -264,8 +264,8 @@ test('expanded left panels integrate their headers with the container shell', ()
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Map Source uses five compact tiles in the bottom Visual Presets tray', () => {
|
test('Map Source uses five compact tiles in the bottom Visual Presets tray', async () => {
|
||||||
const html = expandApplicationHtml(readFileSync(new URL('../index.html', import.meta.url), 'utf8'));
|
const html = await renderApplicationHtml(readFileSync(new URL('../index.html', import.meta.url), 'utf8'));
|
||||||
const css = readStylesheet(new URL('../style.css', import.meta.url));
|
const css = readStylesheet(new URL('../style.css', import.meta.url));
|
||||||
|
|
||||||
assert.doesNotMatch(html, /id="stack-panel"/);
|
assert.doesNotMatch(html, /id="stack-panel"/);
|
||||||
@@ -277,8 +277,8 @@ test('Map Source uses five compact tiles in the bottom Visual Presets tray', ()
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('expanded right panels highlight the title divider without changing collapsed launchers', () => {
|
test('expanded right panels highlight the title divider without changing collapsed launchers', async () => {
|
||||||
const html = expandApplicationHtml(readFileSync(new URL('../index.html', import.meta.url), 'utf8'));
|
const html = await renderApplicationHtml(readFileSync(new URL('../index.html', import.meta.url), 'utf8'));
|
||||||
const css = readStylesheet(new URL('../style.css', import.meta.url));
|
const css = readStylesheet(new URL('../style.css', import.meta.url));
|
||||||
|
|
||||||
assert.match(
|
assert.match(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { readRealtimeSource } from './testSupport/readRealtimeSource.mjs';
|
|||||||
import { GEV_REALTIME_TOOLS } from '../server/providers/openai/tools.js';
|
import { GEV_REALTIME_TOOLS } from '../server/providers/openai/tools.js';
|
||||||
import { realtimeInstructions } from '../server/providers/openai/instructions.js';
|
import { realtimeInstructions } from '../server/providers/openai/instructions.js';
|
||||||
import { readShellSource } from './testSupport/readShellSource.mjs';
|
import { readShellSource } from './testSupport/readShellSource.mjs';
|
||||||
import { expandApplicationHtml } from '../build/application-html.js';
|
import { renderApplicationHtml } from './testSupport/applicationHtml.mjs';
|
||||||
import { readLayerSource } from './testSupport/readLayerSource.mjs';
|
import { readLayerSource } from './testSupport/readLayerSource.mjs';
|
||||||
import { readStylesheet } from './testSupport/readStylesheet.mjs';
|
import { readStylesheet } from './testSupport/readStylesheet.mjs';
|
||||||
import { readFileSync as readRadioSource } from 'node:fs';
|
import { readFileSync as readRadioSource } from 'node:fs';
|
||||||
@@ -14,7 +14,7 @@ import { createHash } from 'node:crypto';
|
|||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
|
|
||||||
const html = expandApplicationHtml(readFileSync(new URL('../index.html', import.meta.url), 'utf8'));
|
const html = await renderApplicationHtml(readFileSync(new URL('../index.html', import.meta.url), 'utf8'));
|
||||||
const ui = readShellSource();
|
const ui = readShellSource();
|
||||||
const radio = ['playback', 'interaction'].map(name =>
|
const radio = ['playback', 'interaction'].map(name =>
|
||||||
readFileSync(new URL(`./layers/radio/${name}.js`, import.meta.url), 'utf8')
|
readFileSync(new URL(`./layers/radio/${name}.js`, import.meta.url), 'utf8')
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { readShellSource } from './testSupport/readShellSource.mjs';
|
import { readShellSource } from './testSupport/readShellSource.mjs';
|
||||||
import { expandApplicationHtml } from '../build/application-html.js';
|
import { renderApplicationHtml } from './testSupport/applicationHtml.mjs';
|
||||||
// src/reasonableDefaults.test.mjs
|
// src/reasonableDefaults.test.mjs
|
||||||
//
|
//
|
||||||
// What the console looks like the FIRST time it opens — before any share link,
|
// What the console looks like the FIRST time it opens — before any share link,
|
||||||
@@ -46,7 +46,7 @@ import { ShareLinkManager } from './sharelink.js';
|
|||||||
// Follow the UI wiring and its extracted preset definitions.
|
// Follow the UI wiring and its extracted preset definitions.
|
||||||
const uiSource = readShellSource()
|
const uiSource = readShellSource()
|
||||||
+ '\n' + fs.readFileSync(new URL('./ui/visualPresets.js', import.meta.url), 'utf8');
|
+ '\n' + fs.readFileSync(new URL('./ui/visualPresets.js', import.meta.url), 'utf8');
|
||||||
const indexHtml = expandApplicationHtml(fs.readFileSync(new URL('../index.html', import.meta.url), 'utf8'));
|
const indexHtml = await renderApplicationHtml(fs.readFileSync(new URL('../index.html', import.meta.url), 'utf8'));
|
||||||
const shareSource = fs.readFileSync(new URL('./sharelink.js', import.meta.url), 'utf8');
|
const shareSource = fs.readFileSync(new URL('./sharelink.js', import.meta.url), 'utf8');
|
||||||
|
|
||||||
/** Slice ui.js between two literal anchors, so a pin reads one method, not the file. */
|
/** Slice ui.js between two literal anchors, so a pin reads one method, not the file. */
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { createStandaloneData } from './data.js';
|
|||||||
import { createStandaloneTools } from './tools.js';
|
import { createStandaloneTools } from './tools.js';
|
||||||
import { resolveLocale, applyDocumentLanguage } from '../i18n/locale.js';
|
import { resolveLocale, applyDocumentLanguage } from '../i18n/locale.js';
|
||||||
import { applyDocumentTranslations, setLocale } from '../i18n/index.js';
|
import { applyDocumentTranslations, setLocale } from '../i18n/index.js';
|
||||||
|
import { mountApplicationViews } from '../ui/views/index.js';
|
||||||
import { createGoogleTokenSource } from '../maps/googleTokens.js';
|
import { createGoogleTokenSource } from '../maps/googleTokens.js';
|
||||||
|
|
||||||
// The existing controls and layer catalog contain page-scoped state.
|
// The existing controls and layer catalog contain page-scoped state.
|
||||||
@@ -33,6 +34,8 @@ export function createStandaloneApplication({
|
|||||||
const activeLocale = resolveLocale();
|
const activeLocale = resolveLocale();
|
||||||
setLocale(activeLocale);
|
setLocale(activeLocale);
|
||||||
applyDocumentLanguage(document, activeLocale);
|
applyDocumentLanguage(document, activeLocale);
|
||||||
|
// Views render the page's markup; translation and wiring read it.
|
||||||
|
mountApplicationViews(document);
|
||||||
applyDocumentTranslations(document);
|
applyDocumentTranslations(document);
|
||||||
const loadingScreen = document.getElementById('loading-screen');
|
const loadingScreen = document.getElementById('loading-screen');
|
||||||
const loaderStatus = loadingScreen.querySelector('.loader-status');
|
const loaderStatus = loadingScreen.querySelector('.loader-status');
|
||||||
|
|||||||
@@ -3,13 +3,13 @@ import fs from 'node:fs';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { expandApplicationHtml } from '../build/application-html.js';
|
import { renderApplicationHtml } from './testSupport/applicationHtml.mjs';
|
||||||
import { readStylesheet } from './testSupport/readStylesheet.mjs';
|
import { readStylesheet } from './testSupport/readStylesheet.mjs';
|
||||||
|
|
||||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
const read = (file) => fs.readFileSync(path.join(ROOT, file), 'utf8');
|
const read = (file) => fs.readFileSync(path.join(ROOT, file), 'utf8');
|
||||||
|
|
||||||
const html = expandApplicationHtml(read('index.html'));
|
const html = await renderApplicationHtml(read('index.html'));
|
||||||
const css = readStylesheet(path.join(ROOT, 'style.css'));
|
const css = readStylesheet(path.join(ROOT, 'style.css'));
|
||||||
const panelCss = read('src/ui/styles/street-level.css');
|
const panelCss = read('src/ui/styles/street-level.css');
|
||||||
const panelChrome = read('src/ui/panelChrome.js');
|
const panelChrome = read('src/ui/panelChrome.js');
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { createPinia } from 'pinia';
|
||||||
|
import { createSSRApp } from 'vue';
|
||||||
|
import { renderToString } from 'vue/server-renderer';
|
||||||
|
import { expandApplicationHtml } from '../../build/application-html.js';
|
||||||
|
import { APPLICATION_VIEWS } from '../ui/views/index.js';
|
||||||
|
|
||||||
|
const HOST = /<div class="gev-view" data-gev-view="([^"]+)"><\/div>/g;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expand application markup as the browser shows it once views are mounted:
|
||||||
|
* static templates inline, and each view rendered into its host. Markup tests
|
||||||
|
* read this instead of the raw template expansion.
|
||||||
|
* @param {string} html Document with `gev:template` markers.
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
export async function renderApplicationHtml(html) {
|
||||||
|
let output = expandApplicationHtml(html);
|
||||||
|
for (const [host, name] of [...output.matchAll(HOST)]) {
|
||||||
|
const view = APPLICATION_VIEWS[name];
|
||||||
|
if (!view) throw new Error(`Unknown application view: ${name}`);
|
||||||
|
const markup = (await renderToString(createSSRApp(view).use(createPinia())))
|
||||||
|
// Hydration markers and empty-attribute values are SSR detail; the
|
||||||
|
// DOM is the same as the template's.
|
||||||
|
.replace(/<!--\[-->|<!--\]-->|<!---->/g, '')
|
||||||
|
.replace(/ ([\w:.-]+)=""/g, ' $1');
|
||||||
|
output = output.replace(
|
||||||
|
host,
|
||||||
|
`<div class="gev-view" data-gev-view="${name}">${markup}</div>`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The standalone page's markup with every view rendered. */
|
||||||
|
export function readApplicationHtml() {
|
||||||
|
return renderApplicationHtml(
|
||||||
|
readFileSync(new URL('../../index.html', import.meta.url), 'utf8'),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { readFile } from 'node:fs/promises';
|
import { readFile } from 'node:fs/promises';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { compileScript, compileTemplate, parse } from 'vue/compiler-sfc';
|
import { compileScript, compileTemplate, parse } from 'vue/compiler-sfc';
|
||||||
|
import { VUE_COMPILER_OPTIONS } from '../../build/vueOptions.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compile a single-file component to an ES module for Node's test runner.
|
* Compile a single-file component to an ES module for Node's test runner.
|
||||||
@@ -11,7 +12,11 @@ export function compileVueModule(source, filename) {
|
|||||||
if (errors.length) throw errors[0];
|
if (errors.length) throw errors[0];
|
||||||
const id = filename;
|
const id = filename;
|
||||||
if (descriptor.scriptSetup) {
|
if (descriptor.scriptSetup) {
|
||||||
return compileScript(descriptor, { id, inlineTemplate: true }).content;
|
return compileScript(descriptor, {
|
||||||
|
id,
|
||||||
|
inlineTemplate: true,
|
||||||
|
templateOptions: { compilerOptions: VUE_COMPILER_OPTIONS },
|
||||||
|
}).content;
|
||||||
}
|
}
|
||||||
const script = descriptor.script
|
const script = descriptor.script
|
||||||
? compileScript(descriptor, { id, genDefaultAs: '__sfc__' }).content
|
? compileScript(descriptor, { id, genDefaultAs: '__sfc__' }).content
|
||||||
@@ -22,6 +27,7 @@ export function compileVueModule(source, filename) {
|
|||||||
source: descriptor.template.content,
|
source: descriptor.template.content,
|
||||||
filename,
|
filename,
|
||||||
id,
|
id,
|
||||||
|
compilerOptions: VUE_COMPILER_OPTIONS,
|
||||||
});
|
});
|
||||||
if (compiled.errors.length) throw compiled.errors[0];
|
if (compiled.errors.length) throw compiled.errors[0];
|
||||||
template = compiled.code;
|
template = compiled.code;
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
import { expandApplicationHtml, APPLICATION_TEMPLATES } from '../../build/application-html.js';
|
import {
|
||||||
|
expandApplicationHtml,
|
||||||
|
APPLICATION_TEMPLATES,
|
||||||
|
} from '../../build/application-html.js';
|
||||||
|
import { APPLICATION_VIEW_NAMES } from '../../build/applicationViews.js';
|
||||||
|
import { APPLICATION_VIEWS } from '../ui/views/index.js';
|
||||||
|
import { readApplicationHtml } from '../testSupport/applicationHtml.mjs';
|
||||||
|
|
||||||
test('the standalone document expands every component once and preserves unique element ids', () => {
|
test('the standalone document expands every component once and preserves unique element ids', async () => {
|
||||||
const source = readFileSync(new URL('../../index.html', import.meta.url), 'utf8');
|
const source = readFileSync(new URL('../../index.html', import.meta.url), 'utf8');
|
||||||
const html = expandApplicationHtml(source);
|
const html = await readApplicationHtml();
|
||||||
assert.equal([...source.matchAll(/gev:template /g)].length, APPLICATION_TEMPLATES.length);
|
assert.equal([...source.matchAll(/gev:template /g)].length, APPLICATION_TEMPLATES.length);
|
||||||
assert.doesNotMatch(html, /gev:template/);
|
assert.doesNotMatch(html, /gev:template/);
|
||||||
const ids = [...html.matchAll(/\sid="([^"]+)"/g)].map(match => match[1]);
|
const ids = [...html.matchAll(/\sid="([^"]+)"/g)].map(match => match[1]);
|
||||||
@@ -15,8 +21,21 @@ test('the standalone document expands every component once and preserves unique
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('component selection includes only requested markup and refuses filesystem traversal', () => {
|
test('component selection includes only requested markup and refuses filesystem traversal', () => {
|
||||||
const html = expandApplicationHtml('<!-- gev:template welcome -->\n');
|
const html = expandApplicationHtml('<!-- gev:template hud-loading -->\n');
|
||||||
assert.match(html, /id="first-run-launcher"/);
|
assert.match(html, /id="loading-screen"/);
|
||||||
assert.doesNotMatch(html, /id="cesiumContainer"/);
|
assert.doesNotMatch(html, /id="cesiumContainer"/);
|
||||||
assert.throws(() => expandApplicationHtml('<!-- gev:template ../../.env -->'), /Unknown application template/);
|
assert.throws(() => expandApplicationHtml('<!-- gev:template ../../.env -->'), /Unknown application template/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('views expand to mount hosts, the loading screen stays static, and every view is registered', () => {
|
||||||
|
assert.deepEqual(Object.keys(APPLICATION_VIEWS).sort(), [...APPLICATION_VIEW_NAMES].sort());
|
||||||
|
for (const name of APPLICATION_VIEW_NAMES) {
|
||||||
|
assert.ok(APPLICATION_TEMPLATES.includes(name), name);
|
||||||
|
assert.equal(
|
||||||
|
expandApplicationHtml(`<!-- gev:template ${name} -->\n`),
|
||||||
|
`<div class="gev-view" data-gev-view="${name}"></div>\n`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// It paints before any script runs.
|
||||||
|
assert.ok(!APPLICATION_VIEW_NAMES.includes('hud-loading'));
|
||||||
|
});
|
||||||
@@ -52,7 +52,7 @@ import {
|
|||||||
} from '../i18n/index.js';
|
} from '../i18n/index.js';
|
||||||
import { applyLocaleChoice, mountLocalePicker } from './localePicker.js';
|
import { applyLocaleChoice, mountLocalePicker } from './localePicker.js';
|
||||||
import { PanelNavigation } from './panelNavigation.js';
|
import { PanelNavigation } from './panelNavigation.js';
|
||||||
import { createUiRuntime } from './vueRuntime.js';
|
import { applicationPinia, createUiRuntime } from './vueRuntime.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Central UI orchestrator for the God's Eye View application.
|
* Central UI orchestrator for the God's Eye View application.
|
||||||
@@ -136,7 +136,7 @@ export class StyleManager extends ShellFacade {
|
|||||||
this._mobileShell?.sharedCollapsed(panelId),
|
this._mobileShell?.sharedCollapsed(panelId),
|
||||||
});
|
});
|
||||||
// Vue views share this runtime and its stores; dispose unmounts them.
|
// Vue views share this runtime and its stores; dispose unmounts them.
|
||||||
this._ui = createUiRuntime();
|
this._ui = createUiRuntime({ pinia: applicationPinia() });
|
||||||
this._feedback = new ShellFeedback({
|
this._feedback = new ShellFeedback({
|
||||||
readLayers: () => this._dataManager?.getAll?.() || [],
|
readLayers: () => this._dataManager?.getAll?.() || [],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import test from 'node:test';
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
|
|
||||||
import { expandApplicationHtml } from '../../build/application-html.js';
|
import { renderApplicationHtml } from '../testSupport/applicationHtml.mjs';
|
||||||
import { createBrowserViteConfig } from '../../build/vite.js';
|
import { createBrowserViteConfig } from '../../build/vite.js';
|
||||||
import { R820T_GAIN_STEPS_DB } from '../sdr/gain.js';
|
import { R820T_GAIN_STEPS_DB } from '../sdr/gain.js';
|
||||||
|
|
||||||
const html = expandApplicationHtml(
|
const html = await renderApplicationHtml(
|
||||||
readFileSync(new URL('../../index.html', import.meta.url), 'utf8'),
|
readFileSync(new URL('../../index.html', import.meta.url), 'utf8'),
|
||||||
);
|
);
|
||||||
const css = readFileSync(
|
const css = readFileSync(
|
||||||
|
|||||||
@@ -98,3 +98,9 @@
|
|||||||
font: 600 9px/14px var(--font-mono);
|
font: 600 9px/14px var(--font-mono);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Application view hosts (src/ui/views): the view's own elements lay out as
|
||||||
|
children of the page, as the static markup they replace did. */
|
||||||
|
.gev-view {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { createApp } from 'vue';
|
||||||
|
import { applicationPinia } from '../vueRuntime.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Application views by template name (build/applicationViews.js lists the
|
||||||
|
* same names). Each renders the markup its static template held.
|
||||||
|
*/
|
||||||
|
export const APPLICATION_VIEWS = Object.freeze({});
|
||||||
|
|
||||||
|
const mounted = new WeakSet();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render every application view into its host. Views belong to the page, as
|
||||||
|
* their static markup did, so they are never unmounted. Call before anything
|
||||||
|
* reads or translates the page; repeated calls are harmless.
|
||||||
|
* @param {Document} [doc] Page document.
|
||||||
|
*/
|
||||||
|
export function mountApplicationViews(doc = globalThis.document) {
|
||||||
|
for (const host of doc?.querySelectorAll?.('[data-gev-view]') ?? []) {
|
||||||
|
if (mounted.has(host)) continue;
|
||||||
|
const name = host.getAttribute('data-gev-view');
|
||||||
|
const view = APPLICATION_VIEWS[name];
|
||||||
|
if (!view) throw new Error(`Unknown application view: ${name}`);
|
||||||
|
createApp(view).use(applicationPinia()).mount(host);
|
||||||
|
mounted.add(host);
|
||||||
|
}
|
||||||
|
}
|
||||||
+16
-4
@@ -1,13 +1,25 @@
|
|||||||
import { createApp } from 'vue';
|
import { createApp } from 'vue';
|
||||||
import { createPinia } from 'pinia';
|
import { createPinia } from 'pinia';
|
||||||
|
|
||||||
|
let pagePinia = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the application's Vue runtime: one Pinia instance shared by every
|
* The page's Pinia instance, shared by the application views and the shell,
|
||||||
* view the shell mounts, and the mounts themselves for teardown.
|
* so a view and a shell control read the same stores.
|
||||||
|
* @returns {import('pinia').Pinia}
|
||||||
|
*/
|
||||||
|
export function applicationPinia() {
|
||||||
|
pagePinia ??= createPinia();
|
||||||
|
return pagePinia;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a Vue runtime: one Pinia instance shared by every view it mounts,
|
||||||
|
* and the mounts themselves for teardown.
|
||||||
|
* @param {{pinia?: import('pinia').Pinia}} [options]
|
||||||
* @returns {{pinia: import('pinia').Pinia, mount: Function, destroy: Function}}
|
* @returns {{pinia: import('pinia').Pinia, mount: Function, destroy: Function}}
|
||||||
*/
|
*/
|
||||||
export function createUiRuntime() {
|
export function createUiRuntime({ pinia = createPinia() } = {}) {
|
||||||
const pinia = createPinia();
|
|
||||||
const apps = new Set();
|
const apps = new Set();
|
||||||
return {
|
return {
|
||||||
pinia,
|
pinia,
|
||||||
|
|||||||
Reference in new issue
Block a user