mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
Merge origin/main into fix/hal-722-radio-next-no-autoplay
main moved the Radio markup into Vue views and rebuilt the UI tests on
stores. Resolved:
- src/ui/templates/context.html: deleted in main; the new privacy note now
lives in src/ui/views/panels/RadioPanel.vue.
- src/ui/radioControls.test.mjs: main's store fixture; the Next/Previous
autoplay test drives store.act('cycleRadio').
- src/voice/gevRealtime.test.mjs: main's createTestVoiceUi; the stop and
select handoff cases stay one table-driven test.
- src/data/radioNextState.test.mjs reads the published gev-radio view.
- src/i18n/locales/ar.json: the new note translated for the Arabic locale.
This commit is contained in:
commit
a2be947aba
679 files changed
+45369
-19625
No files matched your search
@@ -0,0 +1,40 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { createPinia } from 'pinia';
|
||||
import { createSSRApp } from 'vue';
|
||||
import { renderToString } from 'vue/server-renderer';
|
||||
import { APPLICATION_VIEWS } from '../ui/views/index.js';
|
||||
|
||||
const HOST = /<div class="gev-view" data-gev-view="([^"]+)"><\/div>/g;
|
||||
|
||||
/**
|
||||
* 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 = 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, empty-attribute values and escaped apostrophes
|
||||
// are SSR detail; the DOM is the same as the template's.
|
||||
.replace(/<!--\[-->|<!--\]-->|<!---->/g, '')
|
||||
.replace(/ ([\w:.-]+)=""/g, ' $1')
|
||||
.replaceAll(''', "'");
|
||||
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'),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
const describe = (node) =>
|
||||
node?.nodeType === 1
|
||||
? `<${node.tagName.toLowerCase()}${node.id ? `#${node.id}` : ''}>`
|
||||
: String(node);
|
||||
|
||||
/**
|
||||
* Assert that two DOM nodes (or a node and null) are the same. Never hand
|
||||
* happy-dom nodes to assert.equal or deepEqual: a failure prints both, and a
|
||||
* node reaches the whole page and window, which runs the process out of
|
||||
* memory.
|
||||
*/
|
||||
export function assertSameNode(actual, expected, message = '') {
|
||||
assert.ok(
|
||||
actual === expected,
|
||||
`${message ? `${message}: ` : ''}${describe(actual)} is not ${describe(expected)}`,
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
|
||||
import { createVoiceSession } from '../voice/session.js';
|
||||
import { createGevActionRunner } from '../voice/gevActions.js';
|
||||
import { createGeminiSession } from '../voice/geminiSession.js';
|
||||
import { createTestVoiceUi } from './voiceUi.mjs';
|
||||
import { createNarrationScheduler } from '../voice/narration.js';
|
||||
|
||||
export const turnOfEventLoop = () =>
|
||||
@@ -158,7 +159,7 @@ export function nativeIntegrationFixture(t) {
|
||||
createAdapter: (hooks) =>
|
||||
createGeminiSession({
|
||||
...hooks,
|
||||
ui: { root: { remove() {} } },
|
||||
ui: createTestVoiceUi(),
|
||||
dataManager,
|
||||
now: clock.now,
|
||||
setTimer: clock.setTimer,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { GeminiSessionController } from '../voice/geminiSession.js';
|
||||
import { createTestVoiceUi } from './voiceUi.mjs';
|
||||
import { createNarrationScheduler } from '../voice/narration.js';
|
||||
|
||||
const endpoint =
|
||||
@@ -121,7 +122,7 @@ function harness(overrides = {}) {
|
||||
calls.push({ name, args, options });
|
||||
return { ok: true, action: name };
|
||||
},
|
||||
ui: { root: { remove() {} } },
|
||||
ui: createTestVoiceUi(),
|
||||
radioLayer: radio,
|
||||
now: () => clock,
|
||||
setTimer: (fn, ms) => {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
// src/testSupport/hudReadouts.mjs — the Intel HUD's published readouts by the
|
||||
// element ids its view renders them in, for tests that drive IntelHUD alone.
|
||||
|
||||
const FIELDS = Object.freeze({
|
||||
'hud-alt': 'alt',
|
||||
'hud-summary': 'summary',
|
||||
'hud-mgrs': 'mgrs',
|
||||
'hud-latlon': 'latlon',
|
||||
'hud-bottom-line': 'bottom',
|
||||
'hud-gsd': 'gsd',
|
||||
'hud-coll': 'coll',
|
||||
'hud-ona': 'ona',
|
||||
'hud-mode': 'mode',
|
||||
'hud-timestamp': 'timestamp',
|
||||
});
|
||||
|
||||
/**
|
||||
* A `publish` to hand IntelHUD and each readout's latest text.
|
||||
* @returns {{publish: (view: object) => void, view: () => ?object,
|
||||
* elements: Map<string, {readonly textContent: string}>}}
|
||||
*/
|
||||
export function hudReadouts() {
|
||||
let view = null;
|
||||
const elements = new Map(
|
||||
Object.entries(FIELDS).map(([id, field]) => [
|
||||
id,
|
||||
{
|
||||
get textContent() {
|
||||
return view?.[field] ?? '';
|
||||
},
|
||||
},
|
||||
]),
|
||||
);
|
||||
return { publish: (next) => (view = next), view: () => view, elements };
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// src/testSupport/panelState.mjs — present a panel's state the way the shell
|
||||
// does (PanelChrome._presentPanel), drawn at once as its root's v-panel-state
|
||||
// would, for fixtures whose panels are plain objects with a classList.
|
||||
import { applyPanelState, readPanelState } from '../ui/rail/panelState.js';
|
||||
|
||||
/**
|
||||
* @param {object|string} panelOrId A panel, or its id in `documentRef`.
|
||||
* @param {object} [patch] The state that changes.
|
||||
* @param {Document} [documentRef]
|
||||
*/
|
||||
export function presentPanelLocally(
|
||||
panelOrId,
|
||||
patch = {},
|
||||
documentRef = globalThis.document,
|
||||
) {
|
||||
const panel =
|
||||
typeof panelOrId === 'string'
|
||||
? documentRef?.getElementById(panelOrId)
|
||||
: panelOrId;
|
||||
if (panel) applyPanelState(panel, { ...readPanelState(panel), ...patch });
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { GlobalRegistrator } from '@happy-dom/global-registrator';
|
||||
|
||||
// Import first in a test file whose modules load Vue at import time: Vue binds
|
||||
// the document when it loads. Each test file runs in its own process.
|
||||
GlobalRegistrator.register({ url: 'http://localhost/' });
|
||||
@@ -0,0 +1,16 @@
|
||||
import { createPinia } from 'pinia';
|
||||
import { useVoiceStore } from '../ui/stores/voice.js';
|
||||
import { createVoiceControl } from '../voice/control.js';
|
||||
|
||||
/**
|
||||
* A real voice control presenter on its own store, for owner tests that run
|
||||
* without a page. Read what it shows from `ui.state`; report interactions
|
||||
* with `ui.emit(name, ...args)`.
|
||||
* @param {{panel?: boolean, pinia?: import('pinia').Pinia}} [options]
|
||||
*/
|
||||
export function createTestVoiceUi({
|
||||
panel = false,
|
||||
pinia = createPinia(),
|
||||
} = {}) {
|
||||
return createVoiceControl({ panel, store: useVoiceStore(pinia) });
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { createApp, h, nextTick } from 'vue';
|
||||
import { createPinia, storeToRefs } from 'pinia';
|
||||
import { useVoiceStore } from '../ui/stores/voice.js';
|
||||
import VoiceControl from '../ui/views/panels/VoiceControl.vue';
|
||||
|
||||
/**
|
||||
* Render whichever voice control the store presents, as the command dock
|
||||
* does, into the current document. Import registerDom.mjs first.
|
||||
* @param {import('node:test').TestContext} t
|
||||
* @returns {{pinia: import('pinia').Pinia, settle: () => Promise<void>}}
|
||||
*/
|
||||
export function mountVoiceView(t) {
|
||||
const doc = globalThis.document;
|
||||
// happy-dom lacks the browser's `[hidden]` rule, which visibility checks
|
||||
// and focus moves rely on.
|
||||
if (!doc.getElementById('test-ua-hidden')) {
|
||||
const style = doc.head.appendChild(doc.createElement('style'));
|
||||
style.id = 'test-ua-hidden';
|
||||
style.textContent = '[hidden] { display: none; }';
|
||||
}
|
||||
doc.body.replaceChildren();
|
||||
const host = doc.body.appendChild(doc.createElement('div'));
|
||||
const pinia = createPinia();
|
||||
const { view } = storeToRefs(useVoiceStore(pinia));
|
||||
const app = createApp({
|
||||
render: () =>
|
||||
view.value
|
||||
? h(VoiceControl, { key: view.value.id, view: view.value })
|
||||
: null,
|
||||
}).use(pinia);
|
||||
app.mount(host);
|
||||
t.after(() => app.unmount());
|
||||
// Renders, then any focus the owners asked for after it.
|
||||
const settle = async () => {
|
||||
await nextTick();
|
||||
await nextTick();
|
||||
};
|
||||
return { pinia, settle };
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { GlobalRegistrator } from '@happy-dom/global-registrator';
|
||||
|
||||
/**
|
||||
* Give a component test a browser document; returns the teardown.
|
||||
* Call it from `before`/`after`, since other suites assert there is no DOM.
|
||||
*/
|
||||
export function installVueDom() {
|
||||
GlobalRegistrator.register({ url: 'http://localhost/' });
|
||||
return () => GlobalRegistrator.unregister();
|
||||
}
|
||||
Reference in new issue
Block a user