mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
refactor(build): place the views in the page instead of expanding templates
index.html now lists each view's host and the loading screen itself, so the gev:template expansion plugin, its view list, the loading-screen template and the ./build/html export are gone. createApplication takes omitViews for views an application leaves out; their hosts are removed, and a view an earlier call rendered is unmounted.
This commit is contained in:
1 parent
f279200af5
commit
5bf797ef71
18 files changed
+176
-174
No files matched your search
@@ -14,6 +14,12 @@
|
||||
picker, and Node tests that import application modules register the
|
||||
component loader with `--import=<core>/build/register-vue` (samehkhamis).
|
||||
|
||||
- Pages place each view with a `<div class="gev-view" data-gev-view="NAME">`
|
||||
host and write their own loading screen; the `build/html` template
|
||||
expansion and its `gev:template` markers are gone. `createApplication`
|
||||
takes `omitViews` (for example `['provider-settings']`) for views an
|
||||
application leaves out (samehkhamis).
|
||||
|
||||
- `./ui/display`'s `bindDisplayControls` takes the keyhole fade and outside
|
||||
opacity sliders as `fadeSlider` and `outsideOpacitySlider`, and their
|
||||
actions receive whole percentages; `fadeSliders` still works.
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { APPLICATION_VIEW_NAMES } from './applicationViews.js';
|
||||
|
||||
export const APPLICATION_TEMPLATES = Object.freeze([
|
||||
'scene-chrome',
|
||||
'cockpit',
|
||||
'display-controls',
|
||||
'command-dock',
|
||||
'layer-panels',
|
||||
'context',
|
||||
'welcome',
|
||||
'provider-settings',
|
||||
'hud-loading',
|
||||
]);
|
||||
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. */
|
||||
export function expandApplicationHtml(html) {
|
||||
return html.replace(
|
||||
/^[ \t]*<!-- gev:template ([^\s]+) -->\r?\n?/gm,
|
||||
(_, name) => {
|
||||
if (!allowed.has(name))
|
||||
throw new Error(`Unknown application template: ${name}`);
|
||||
if (views.has(name)) return applicationViewHost(name);
|
||||
return readFileSync(
|
||||
new URL(`../src/ui/templates/${name}.html`, import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** Assemble static application markup before Vite processes scripts and assets. */
|
||||
export function applicationHtmlPlugin() {
|
||||
return {
|
||||
name: 'application-component-templates',
|
||||
transformIndexHtml: {
|
||||
order: 'pre',
|
||||
handler: expandApplicationHtml,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
/**
|
||||
* 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([
|
||||
'provider-settings',
|
||||
'scene-chrome',
|
||||
'cockpit',
|
||||
'display-controls',
|
||||
'command-dock',
|
||||
'layer-panels',
|
||||
'context',
|
||||
'welcome',
|
||||
]);
|
||||
@@ -1,5 +1,4 @@
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { applicationHtmlPlugin } from './application-html.js';
|
||||
import cesium from 'vite-plugin-cesium';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import { VUE_PLUGIN_OPTIONS } from './vueOptions.js';
|
||||
@@ -157,7 +156,6 @@ export function createBrowserViteConfig({
|
||||
// Next, so no provider route answers a Host the server does not allow.
|
||||
hostCheckPlugin(),
|
||||
cesium(),
|
||||
applicationHtmlPlugin(),
|
||||
// Application views are Vue single-file components.
|
||||
vue(VUE_PLUGIN_OPTIONS),
|
||||
...plugins,
|
||||
|
||||
+7
-5
@@ -200,11 +200,13 @@ its pending results. Source disposal does not silently reinstate a default.
|
||||
`ui/composition` supplies the default engines to the shell. Applications can
|
||||
supply a HUD implementation or request policy through control services. The
|
||||
shared chrome owns the welcome/loading transition; standalone composition adds
|
||||
Provider Settings. `build/html` expands an allowlist of component markers:
|
||||
most become hosts for the Vue views in `src/ui/views`, which
|
||||
`createApplication()` renders before anything reads the page, and the loading
|
||||
screen is inlined from `src/ui/templates`. Unknown names cannot read arbitrary
|
||||
filesystem paths. A host that does not initialize Core's locale passes
|
||||
Provider Settings. A page places each Vue view in `src/ui/views` with a host,
|
||||
`<div class="gev-view" data-gev-view="NAME"></div>`, and writes its own loading
|
||||
screen (`#loading-screen` with a `.loader-status` line) as static markup so it
|
||||
paints before any script runs. `createApplication()` renders the views before
|
||||
anything reads the page; `omitViews` names views the application leaves out,
|
||||
such as `provider-settings`, and their hosts are removed. An unknown view name
|
||||
is an error. A host that does not initialize Core's locale passes
|
||||
`localePicker: false` to `createApplicationControls` instead of editing the
|
||||
markup. Node tests that import application modules register the component
|
||||
loader with `node --import=<core package>/build/register-vue`.
|
||||
@@ -63,13 +63,13 @@ a feature supplying only its accent.
|
||||
2. **Rail.** `PanelChrome` enforces one panel per side; the panel store
|
||||
mirrors each side's panels and the rail renders from it. Done.
|
||||
3. **Panels.** Each template and its wiring become a component, keeping ids,
|
||||
classes, `data-i18n` text and CSS. A converted template is listed in
|
||||
`build/applicationViews.js`; its `gev:template` marker then expands to a
|
||||
classes, `data-i18n` text and CSS. The page places each view with 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
|
||||
translated and wired. `createApplication()` calls it too, removing the
|
||||
hosts of any views its `omitViews` names. Views belong to the page and are
|
||||
unmounted only when a later call omits them. The loading screen is static markup in the page so it
|
||||
paints before any script runs. Markup
|
||||
tests read `renderApplicationHtml()` from `src/testSupport`, which renders
|
||||
the views on the server. Templates compile with Vue's `preserve`
|
||||
whitespace, which keeps the whitespace between elements as a single
|
||||
@@ -168,8 +168,9 @@ a feature supplying only its accent.
|
||||
Display popover's slots stay static for the Cockpit display portal).
|
||||
Layers that write into the right rail directly gain a slot instead.
|
||||
|
||||
4. **Cleanup.** The build-time template expansion and the attribute
|
||||
translation pass are retired once nothing uses them. Hosts register panels
|
||||
4. **Cleanup.** The build-time template expansion is retired: `index.html`
|
||||
lists the view hosts and the loading screen itself. The attribute
|
||||
translation pass goes with the move to vue-i18n. Hosts register panels
|
||||
and rail items through an explicit API instead of editing markup.
|
||||
Markup-string tests are replaced by component tests.
|
||||
|
||||
|
||||
+18
-16
@@ -29,23 +29,25 @@
|
||||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20,400,0,0&icon_names=adjust,arrow_drop_down,arrow_forward,arrow_left,arrow_right,bolt,chevron_left,chevron_right,close,close_fullscreen,dark_mode,draw,east,flare,flight,layers,layers_clear,light_mode,local_fire_department,more_horiz,movie,my_location,navigation,normal,on,open_in_full,partly_cloudy_day,public,radar,radio,right_panel_close,right_panel_open,rocket_launch,satellite_alt,search,settings,skip_next,skip_previous,streetview,tune,upload_file,videocam,view_in_ar,visibility,visibility_off" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- gev:template scene-chrome -->
|
||||
<div class="gev-view" data-gev-view="scene-chrome"></div>
|
||||
<div class="gev-view" data-gev-view="cockpit"></div>
|
||||
<div class="gev-view" data-gev-view="display-controls"></div>
|
||||
<div class="gev-view" data-gev-view="command-dock"></div>
|
||||
<div class="gev-view" data-gev-view="layer-panels"></div>
|
||||
<div class="gev-view" data-gev-view="context"></div>
|
||||
<div class="gev-view" data-gev-view="welcome"></div>
|
||||
<div class="gev-view" data-gev-view="provider-settings"></div>
|
||||
<!-- Intelligence HUD Overlay -->
|
||||
<div id="intel-hud"></div>
|
||||
|
||||
<!-- gev:template cockpit -->
|
||||
|
||||
<!-- gev:template display-controls -->
|
||||
|
||||
<!-- gev:template command-dock -->
|
||||
|
||||
<!-- gev:template layer-panels -->
|
||||
|
||||
<!-- gev:template context -->
|
||||
|
||||
<!-- gev:template welcome -->
|
||||
|
||||
<!-- gev:template provider-settings -->
|
||||
|
||||
<!-- gev:template hud-loading -->
|
||||
<!-- Loading screen -->
|
||||
<div id="loading-screen">
|
||||
<div class="loader-content">
|
||||
<span class="loader-logo brand-logo" data-logo-gaze data-logo-src="/logo.svg" aria-hidden="true"><img src="/logo.svg" alt="" /></span>
|
||||
<h2 translate="no">GOD'S EYE <span class="title-accent">VIEW</span></h2>
|
||||
<p class="loader-status" data-i18n>Initializing photorealistic world...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
|
||||
@@ -219,9 +219,6 @@
|
||||
"./voice/actions": "./src/voice/gevActions.js",
|
||||
"./annotations": "./src/annotations/index.js",
|
||||
"./context": "./src/data/contextStore.js",
|
||||
"./build/html": {
|
||||
"node": "./build/application-html.js"
|
||||
},
|
||||
"./build/register-vue": {
|
||||
"node": "./build/registerVue.js"
|
||||
},
|
||||
|
||||
@@ -650,7 +650,6 @@
|
||||
"src/voice/realtimeBackend.test.mjs",
|
||||
"src/agent/worldPacket.test.mjs",
|
||||
"src/voice/layerSummary.js",
|
||||
"build/application-html.js",
|
||||
"src/app/sources.js",
|
||||
"src/ui/composition.js",
|
||||
"src/services/application.js",
|
||||
|
||||
+3
-8
@@ -8,8 +8,7 @@
|
||||
// lists it.
|
||||
//
|
||||
// Messages are the English literals passed to t(), tn() and msg() in src/,
|
||||
// plus the markup opted in with data-i18n* in index.html, src/ui/templates and
|
||||
// the Vue views.
|
||||
// plus the markup opted in with data-i18n* in index.html and the Vue views.
|
||||
// It also lists UI text that is not marked at all (see findUnmarked).
|
||||
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
@@ -422,13 +421,9 @@ export function findUnmarked() {
|
||||
});
|
||||
}
|
||||
|
||||
/** Static markup opted in with data-i18n*: the page, its templates and views. */
|
||||
/** Static markup opted in with data-i18n*: the page and its views. */
|
||||
function markupFiles() {
|
||||
return trackedFiles(
|
||||
'index.html',
|
||||
'src/ui/templates/*.html',
|
||||
'src/ui/views/*.vue',
|
||||
);
|
||||
return trackedFiles('index.html', 'src/ui/views/*.vue');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -98,14 +98,12 @@
|
||||
},
|
||||
"vite-build": {
|
||||
"runtime": "node",
|
||||
"exports": ["./build/vite", "./build/html", "./build/register-vue"],
|
||||
"exports": ["./build/vite", "./build/register-vue"],
|
||||
"modules": [
|
||||
"build/vite.js",
|
||||
"build/application-html.js",
|
||||
"build/allowedHosts.js",
|
||||
"build/embed-framing.js",
|
||||
"build/panel.js",
|
||||
"build/applicationViews.js",
|
||||
"build/vueOptions.js",
|
||||
"build/registerVue.js",
|
||||
"build/vueLoader.js"
|
||||
|
||||
@@ -7,7 +7,9 @@ const STOP_ORDER = ['tools', 'controls', 'data', 'scene'];
|
||||
* Own application startup and teardown without importing an application entry.
|
||||
* Constructors receive earlier components, an AbortSignal and defer(cleanup).
|
||||
* Register cleanup immediately after acquiring each resource, before any await.
|
||||
* @param {{createScene:Function,createControls:Function,createData:Function,createTools:Function}} constructors
|
||||
* The page's views render first; `omitViews` names the ones this application
|
||||
* leaves out (see mountApplicationViews).
|
||||
* @param {{createScene:Function,createControls:Function,createData:Function,createTools:Function,omitViews?:string[]}} constructors
|
||||
* @returns {{start:Function,destroy:Function,subscribe:Function,getState:Function,getComponents:Function}}
|
||||
*/
|
||||
export function createApplication({
|
||||
@@ -15,9 +17,10 @@ export function createApplication({
|
||||
createControls,
|
||||
createData,
|
||||
createTools,
|
||||
omitViews = [],
|
||||
}) {
|
||||
// A host's page needs its views before any phase reads the markup.
|
||||
mountApplicationViews();
|
||||
mountApplicationViews(globalThis.document, { omit: omitViews });
|
||||
const factories = {
|
||||
scene: createScene,
|
||||
controls: createControls,
|
||||
|
||||
@@ -2,20 +2,19 @@ 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.
|
||||
* Application markup as the browser shows it once views are mounted: each
|
||||
* view rendered into its host. Markup tests read this instead of the page
|
||||
* source.
|
||||
* @param {string} html Document with `.gev-view` hosts.
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
export async function renderApplicationHtml(html) {
|
||||
let output = expandApplicationHtml(html);
|
||||
let output = html;
|
||||
for (const [host, name] of [...output.matchAll(HOST)]) {
|
||||
const view = APPLICATION_VIEWS[name];
|
||||
if (!view) throw new Error(`Unknown application view: ${name}`);
|
||||
|
||||
@@ -1,41 +1,27 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
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', async () => {
|
||||
const source = readFileSync(new URL('../../index.html', import.meta.url), 'utf8');
|
||||
const source = readFileSync(new URL('../../index.html', import.meta.url), 'utf8');
|
||||
|
||||
test('the standalone document hosts every view once and keeps element ids unique', async () => {
|
||||
const hosts = [...source.matchAll(/data-gev-view="([^"]+)"/g)].map((match) => match[1]);
|
||||
// Order is stacking order for fixed elements that share a z-index.
|
||||
assert.deepEqual(hosts, ['scene-chrome', 'cockpit', 'display-controls', 'command-dock', 'layer-panels', 'context', 'welcome', 'provider-settings']);
|
||||
assert.deepEqual([...hosts].sort(), Object.keys(APPLICATION_VIEWS).sort());
|
||||
const html = await readApplicationHtml();
|
||||
assert.equal([...source.matchAll(/gev:template /g)].length, APPLICATION_TEMPLATES.length);
|
||||
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]);
|
||||
assert.equal(new Set(ids).size, ids.length);
|
||||
assert.match(html, /id="cesiumContainer"/);
|
||||
assert.match(html, /type="module" src="\/src\/main.js"/);
|
||||
});
|
||||
|
||||
test('component selection includes only requested markup and refuses filesystem traversal', () => {
|
||||
const html = expandApplicationHtml('<!-- gev:template hud-loading -->\n');
|
||||
assert.match(html, /id="loading-screen"/);
|
||||
assert.doesNotMatch(html, /id="cesiumContainer"/);
|
||||
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'));
|
||||
test('the loading screen is static markup, so it paints before any script runs', () => {
|
||||
const loading = source.indexOf('id="loading-screen"');
|
||||
assert.ok(loading > 0);
|
||||
assert.ok(loading < source.indexOf('type="module" src="/src/main.js"'));
|
||||
assert.match(source, /class="loader-status" data-i18n>Initializing photorealistic world\.\.\.</);
|
||||
assert.doesNotMatch(source, /gev:template/);
|
||||
});
|
||||
@@ -29,8 +29,8 @@ test('explicit build inputs preserve browser-only defines, plugin order and loop
|
||||
assert.equal(config.plugins[0].enforce, 'pre');
|
||||
assert.equal(config.plugins[1].name, 'host-check');
|
||||
assert.equal(config.plugins[1].enforce, 'pre');
|
||||
assert.equal(config.plugins[4].name, 'vite:vue');
|
||||
assert.equal(config.plugins[5], plugin);
|
||||
assert.equal(config.plugins[3].name, 'vite:vue');
|
||||
assert.equal(config.plugins[4], plugin);
|
||||
assert.equal(config.server.host, 'localhost');
|
||||
assert.equal(config.server.port, 4173);
|
||||
assert.deepEqual(config.server.allowedHosts, ['localhost', '127.0.0.1']);
|
||||
@@ -104,7 +104,7 @@ test('build helper does not discover environment values or construct local provi
|
||||
undefined,
|
||||
);
|
||||
assert.deepEqual(
|
||||
config.plugins.slice(5).map((plugin) => plugin.name),
|
||||
config.plugins.slice(4).map((plugin) => plugin.name),
|
||||
['embed-framing', 'panel-build'],
|
||||
);
|
||||
} finally {
|
||||
@@ -118,7 +118,7 @@ test('root config retains existing named exports and standalone provider order',
|
||||
assert.equal(compatibility[name], value, name);
|
||||
const config = standaloneConfig({ mode: 'test' });
|
||||
const standaloneProviders = config.plugins
|
||||
.slice(5, -6)
|
||||
.slice(4, -6)
|
||||
.map((plugin) => plugin.name);
|
||||
assert.equal(
|
||||
standaloneProviders.filter((name) => name === 'gemini-live-proxy').length,
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
<!-- Intelligence HUD Overlay -->
|
||||
<div id="intel-hud"></div>
|
||||
|
||||
<!-- Loading screen -->
|
||||
<div id="loading-screen">
|
||||
<div class="loader-content">
|
||||
<span class="loader-logo brand-logo" data-logo-gaze data-logo-src="/logo.svg" aria-hidden="true"><img src="/logo.svg" alt="" /></span>
|
||||
<h2 translate="no">GOD'S EYE <span class="title-accent">VIEW</span></h2>
|
||||
<p class="loader-status" data-i18n>Initializing photorealistic world...</p>
|
||||
</div>
|
||||
</div>
|
||||
+28
-10
@@ -10,8 +10,9 @@ import SceneChromeView from './SceneChromeView.vue';
|
||||
import ProviderSettingsView from './ProviderSettingsView.vue';
|
||||
|
||||
/**
|
||||
* Application views by template name (build/applicationViews.js lists the
|
||||
* same names). Each renders the markup its static template held.
|
||||
* Application views by name. A page places each one with a host element,
|
||||
* `<div class="gev-view" data-gev-view="NAME"></div>` (`display: contents`),
|
||||
* where the view's markup belongs.
|
||||
*/
|
||||
export const APPLICATION_VIEWS = Object.freeze({
|
||||
'provider-settings': ProviderSettingsView,
|
||||
@@ -24,21 +25,38 @@ export const APPLICATION_VIEWS = Object.freeze({
|
||||
welcome: WelcomeView,
|
||||
});
|
||||
|
||||
const mounted = new WeakSet();
|
||||
const mounted = new WeakMap();
|
||||
|
||||
/**
|
||||
* 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
|
||||
* their static markup did, so they are unmounted only when a later call omits
|
||||
* them; omit views before any control code is wired to them. Call before anything
|
||||
* reads or translates the page; repeated calls are harmless.
|
||||
* @param {Document} [doc] Page document.
|
||||
* @param {{omit?: string[]}} [options] Views this page leaves out; their
|
||||
* hosts are removed, unmounting a view an earlier call rendered.
|
||||
*/
|
||||
export function mountApplicationViews(doc = globalThis.document) {
|
||||
export function mountApplicationViews(
|
||||
doc = globalThis.document,
|
||||
{ omit = [] } = {},
|
||||
) {
|
||||
for (const name of omit) {
|
||||
if (!Object.hasOwn(APPLICATION_VIEWS, name))
|
||||
throw new Error(`Unknown application view: ${name}`);
|
||||
}
|
||||
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);
|
||||
if (omit.includes(name)) {
|
||||
mounted.get(host)?.unmount();
|
||||
mounted.delete(host);
|
||||
host.remove();
|
||||
continue;
|
||||
}
|
||||
if (mounted.has(host)) continue;
|
||||
if (!Object.hasOwn(APPLICATION_VIEWS, name))
|
||||
throw new Error(`Unknown application view: ${name}`);
|
||||
const app = createApp(APPLICATION_VIEWS[name]).use(applicationPinia());
|
||||
app.mount(host);
|
||||
mounted.set(host, app);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// src/ui/views/mountApplicationViews.test.mjs — a page's view hosts render
|
||||
// their views, and an application can leave views out, over happy-dom.
|
||||
import '../../testSupport/registerDom.mjs';
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createApplication } from '../../app/application.js';
|
||||
import { mountApplicationViews } from './index.js';
|
||||
|
||||
function page(names) {
|
||||
document.body.replaceChildren();
|
||||
for (const name of names) {
|
||||
const host = document.createElement('div');
|
||||
host.className = 'gev-view';
|
||||
host.setAttribute('data-gev-view', name);
|
||||
document.body.append(host);
|
||||
}
|
||||
}
|
||||
|
||||
const hostNames = () =>
|
||||
[...document.querySelectorAll('[data-gev-view]')].map((host) =>
|
||||
host.getAttribute('data-gev-view'),
|
||||
);
|
||||
|
||||
test('omitted views lose their hosts and the rest render', () => {
|
||||
page(['welcome', 'provider-settings']);
|
||||
mountApplicationViews(document, { omit: ['provider-settings'] });
|
||||
assert.deepEqual(hostNames(), ['welcome']);
|
||||
assert.equal(document.getElementById('key-setup-chip'), null);
|
||||
assert.ok(
|
||||
document.querySelector('[data-gev-view="welcome"]').children.length,
|
||||
);
|
||||
});
|
||||
|
||||
test('a view an earlier call rendered is unmounted when a later call omits it', () => {
|
||||
page(['welcome', 'provider-settings']);
|
||||
mountApplicationViews(document);
|
||||
assert.ok(document.getElementById('key-setup-chip'));
|
||||
mountApplicationViews(document, { omit: ['provider-settings'] });
|
||||
assert.deepEqual(hostNames(), ['welcome']);
|
||||
assert.equal(document.getElementById('key-setup-chip'), null);
|
||||
});
|
||||
|
||||
test('unknown view names are refused, in the page or in the omit list', () => {
|
||||
page([]);
|
||||
for (const name of ['toString', 'constructor'])
|
||||
assert.throws(
|
||||
() => mountApplicationViews(document, { omit: [name] }),
|
||||
/Unknown application view/,
|
||||
);
|
||||
page(['toString']);
|
||||
assert.throws(
|
||||
() => mountApplicationViews(document),
|
||||
/Unknown application view/,
|
||||
);
|
||||
assert.throws(
|
||||
() => mountApplicationViews(document, { omit: ['nope'] }),
|
||||
/Unknown application view: nope/,
|
||||
);
|
||||
page(['nope']);
|
||||
assert.throws(
|
||||
() => mountApplicationViews(document),
|
||||
/Unknown application view: nope/,
|
||||
);
|
||||
});
|
||||
|
||||
test('createApplication leaves out the views its omitViews names', () => {
|
||||
page(['welcome', 'provider-settings']);
|
||||
const noop = () => ({});
|
||||
createApplication({
|
||||
createScene: noop,
|
||||
createControls: noop,
|
||||
createData: noop,
|
||||
createTools: noop,
|
||||
omitViews: ['provider-settings'],
|
||||
});
|
||||
assert.deepEqual(hostNames(), ['welcome']);
|
||||
});
|
||||
Reference in new issue
Block a user