waybetter/packages/runtime/tests/siteRuntime.test.ts
CRT f31b00eec7 Feature modules: a feature table (areas, flags, modes), a registry that runs a module only on its surfaces, one shared observer for every feature and plugin, settings from a schema, selector health with self-pause
- featureTable.ts: areas, per-feature flags, core flags, modes as presets; sparse storage, ownership (an area that is off turns off what it owns), mode switching that keeps or drops manual changes, table validation
- featureModules.ts: createFeatureRegistry (flag + surface gating, scoped context, palette commands under the area with a budget of 5, settings dialog, probes, hotfix selector overrides and kill flags)
- settingsForm.ts: a settings schema normalizes stored values and draws the controls from the UI kit
- domHub observeSelector; api.observe goes through the shared hub (a root of the caller's own keeps its observer)
- createFeatureFlags: a delivery is the whole state (a flag it does not name is at its default), resolve() for ownership, current()
2026-09-30 18:11:42 +07:00

195 lines
12 KiB
TypeScript

// The runtime composition helpers: the IPC bridge read at call time, flag-controlled parts with a
// deck column's overrides, the runtime object under its window names with what native code queued,
// the page strings native code delivers, the palette command report and the panel button's placement.
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { devErrorReporter, installCommandReport } from '../src/commandReport.ts';
import { createPageStrings, PAGE_STRING_LIMITS } from '../src/pageStrings.ts';
import { BUTTON_GAP, buttonBottom, buttonHidden } from '../src/panelButton.ts';
import { createPluginContext } from '../src/permissions.ts';
import { createFeatureFlags, installSiteRuntime, pageInvoke } from '../src/siteRuntime.ts';
const tick = (ms = 15) => new Promise(resolve => setTimeout(resolve, ms));
const SITE = { product: 'wayBetter for Test', runtimeGlobal: 'Demo' };
test('pageInvoke: the bridge is read at call time; a test browser without one has no invoke', async () => {
const win = { navigator: { webdriver: false } } as unknown as Window & { __TAURI_INTERNALS__?: unknown };
const invoke = pageInvoke(win, 'Demo')!;
await assert.rejects(invoke('ping', {}), /Demo: IPC bridge not ready/);
win.__TAURI_INTERNALS__ = { invoke: async (command: string, args: unknown) => ({ command, args }) };
assert.deepEqual(await invoke('ping', { a: 1 }), { command: 'ping', args: { a: 1 } });
assert.equal(pageInvoke({ navigator: { webdriver: true } } as unknown as Window, 'Demo'), undefined);
});
type Flags = { gate: boolean; pill: boolean; css: boolean };
test('feature flags: parts install on enable and dispose on disable, in order, and a failing part is contained', () => {
const log: string[] = [];
const part = (name: string) => () => { log.push(`+${name}`); return { dispose: () => { log.push(`-${name}`); } }; };
const applied: Array<Partial<Flags>> = [];
const errors: unknown[] = [];
const original = console.error;
console.error = (...args: unknown[]) => { errors.push(args[0]); };
try {
const flags = createFeatureFlags<Flags>({
defaults: { gate: true, pill: false, css: true },
label: 'Demo',
installers: [['gate', part('gate')], ['pill', () => { throw new Error('no pill'); }]],
apply: config => applied.push(config),
});
assert.deepEqual(log, ['+gate'], 'default-on parts install at once');
assert.ok(flags.installed('gate') && !flags.installed('pill'));
flags.apply({ gate: false, css: false });
assert.deepEqual(log, ['+gate', '-gate']);
assert.deepEqual(applied, [{ gate: false, pill: false, css: false }], 'the full map: a flag the delivery does not name is at its default');
flags.apply({ gate: true, pill: true });
assert.deepEqual(log, ['+gate', '-gate', '+gate']);
assert.equal(flags.installed('pill'), false);
assert.match(String(errors[0]), /\[Demo\] pill failed/);
flags.set('gate', false);
assert.equal(flags.installed('gate'), false, 'a kill switch');
flags.set('css', true);
} finally { console.error = original; }
});
test('feature flags: a deck column overrides global flags; dropped overrides go back to the global value or the default', () => {
const applied: Array<Partial<Flags>> = [];
const flags = createFeatureFlags<Flags>({ defaults: { gate: true, pill: false, css: true }, label: 'Demo', installers: [], apply: config => applied.push(config) });
flags.apply({ pill: true });
flags.setColumnFlags({ pill: false, css: false, unknown: true, gate: 'yes' });
assert.deepEqual(applied.at(-1), { gate: true, pill: false, css: false }, 'only known booleans; the column wins');
flags.apply({ pill: true, css: true });
assert.deepEqual(applied.at(-1), { gate: true, pill: false, css: false }, 'a global change does not undo the column');
flags.setColumnFlags({ pill: false });
assert.deepEqual(applied.at(-1), { gate: true, pill: false, css: true }, 'css is the global value again');
flags.setColumnFlags(null);
assert.deepEqual(applied.at(-1), { gate: true, pill: true, css: true }, 'an ordinary tab again: the global flags');
flags.apply({});
assert.deepEqual(applied.at(-1), { gate: true, pill: false, css: true }, 'a delivery is the whole state: what it does not name is at its default');
assert.deepEqual(flags.current(), { gate: true, pill: false, css: true });
const before = applied.length;
flags.setColumnFlags(undefined);
assert.equal(applied.length, before, 'nothing to change');
// Delivered at document start, a column's flags wait for <html>.
const listeners: Array<() => void> = [];
const doc = { documentElement: null as unknown, addEventListener: (_: string, listener: () => void) => { listeners.push(listener); } };
const late: Array<Partial<Flags>> = [];
const waiting = createFeatureFlags<Flags>({ defaults: { gate: true, pill: false, css: true }, label: 'Demo', installers: [], apply: config => late.push(config), document: doc as unknown as Document });
waiting.setColumnFlags({ css: false });
assert.deepEqual(late, []);
waiting.apply({ pill: true });
assert.deepEqual(late.at(-1), { gate: true, pill: true, css: false }, 'recorded at once: a global delivery already respects it');
listeners[0]!();
assert.deepEqual(late.at(-1), { gate: true, pill: true, css: false });
// Ownership: `resolve` decides what runs; a column's own choice still wins.
const owned: Array<Partial<Flags>> = [];
const gated = createFeatureFlags<Flags>({ defaults: { gate: true, pill: false, css: true }, label: 'Demo', installers: [], apply: config => owned.push(config), resolve: all => (all.gate ? all : { ...all, css: false }) });
gated.apply({ gate: false });
assert.deepEqual(owned.at(-1), { gate: false, pill: false, css: false }, 'the owner is off, so what it owns is off');
gated.setColumnFlags({ css: true });
assert.deepEqual(owned.at(-1), { gate: false, pill: false, css: true });
});
function runtimeWindow(queued: Record<string, unknown> = {}) {
return { navigator: { webdriver: false }, ...queued } as unknown as Window & typeof globalThis & Record<string, unknown>;
}
test('the runtime is published under its names, takes what native code queued and reports commands', async () => {
const seen: Record<string, unknown> = {};
const sent: unknown[] = [];
const commandListeners: Array<(list: unknown[]) => void> = [];
let list = [{ id: 'a.plugin.js:go', title: 'Go', keywords: '' }];
const toasts: string[] = [];
const api = { ui: { toast: (message: string) => { toasts.push(message); } }, commands: { list: () => list, onChange: (callback: (next: unknown[]) => void) => { commandListeners.push(callback); return () => {}; } }, i18n: {} };
const strings = createPageStrings({ english: { 'demo.hello': 'Hello {name}' }, prefixes: ['demo.'], label: 'Demo' });
const win = runtimeWindow({
__wbI18n: { locale: 'tr', messages: { 'demo.hello': 'Merhaba {name}', 'other.key': 'dropped' } },
__btFeatures: { gate: false },
__btLinks: { site: 'tab' },
__btColumn: { source: 'home' },
__demoExtra: ['x'],
});
const runtime = installSiteRuntime({
win, site: SITE, version: '1.2.3', aliases: ['OldName'],
invoke: async (command, args) => { sent.push([command, args]); },
plugins: createPluginContext(api),
strings,
pluginFailed: error => `Plugin failed: ${error}`,
features: { apply: (config: unknown) => { seen.features = config; } },
links: { configure: value => { seen.links = value; } },
column: { set: config => { seen.column = config; }, markRead() {}, jumpToUnread() {}, unread: () => 3 },
extra: { demo: { set: (value: unknown) => { seen.extra = value; } } },
queues: [['__demoExtra', value => { seen.extra = value; }]],
});
for (const name of ['wayBetter', 'Demo', 'OldName']) {
assert.equal(win[name], runtime, name);
assert.deepEqual([Object.getOwnPropertyDescriptor(win, name)!.writable, Object.getOwnPropertyDescriptor(win, name)!.configurable], [false, false]);
}
assert.equal(runtime.version, '1.2.3');
assert.deepEqual(seen, { features: { gate: false }, links: { site: 'tab' }, column: { source: 'home' }, extra: ['x'] });
assert.equal(runtime.i18n.locale(), 'tr');
assert.equal(runtime.i18n.t('demo.hello', { name: 'Ada' }), 'Merhaba Ada');
assert.equal(strings.message('other.key'), undefined, 'only the runtime\'s prefixes are taken');
assert.equal(runtime.column.unread(), 3);
assert.equal(runtime.api, api, 'outside a plugin: the full API');
assert.equal(runtime.withPlugin('a.plugin.js', null, () => runtime.api), api, 'a legacy plugin: the full API');
// A member that throws never breaks the others.
const errors: unknown[] = [];
const original = console.error;
console.error = (...args: unknown[]) => { errors.push(args[0]); };
try {
const broken = installSiteRuntime({ win: runtimeWindow(), site: { product: 'Demo', runtimeGlobal: 'Broken' }, version: '1', plugins: createPluginContext(api), strings, pluginFailed: String, fonts: () => { throw new Error('bad fonts'); } });
broken.fonts.provide([]);
assert.match(String(errors[0]), /\[Demo\] fonts failed/);
} finally { console.error = original; }
// Developer mode: a plugin that failed to start shows file:line in a toast.
runtime.devError('a.plugin.js', Object.assign(new Error('boom'), { stack: 'Error: boom\n at start (wb-dev/a.plugin.js:12:5)' }));
assert.match(toasts[0] ?? '', /^Plugin failed: a\.plugin\.js:\d+/);
// The palette hears about commands only when they change.
list = [...list, { id: 'a.plugin.js:stop', title: 'Stop', keywords: 'halt' }];
commandListeners.forEach(listener => listener(list));
await tick(120);
assert.deepEqual(sent, [['tab_report', { report: { commands: list } }]]);
});
test('page strings: deliveries are data (prefix, type and length checked), English is the fallback', () => {
const strings = createPageStrings({ english: { 'x.a': 'A', 'kit.ok': 'OK', 'plugin.clean-links.name': 'Clean links' }, prefixes: ['x.', 'kit.', 'plugin.'], label: 'Demo', bundledPrefix: 'bt-' });
let changes = 0;
const off = strings.onChange(() => { changes += 1; });
strings.set(null);
strings.set({ locale: 'not a tag!', messages: {} });
strings.set({ locale: 'de' });
assert.equal(strings.locale(), 'en');
strings.set({ locale: 'de', messages: { 'x.a': 'Ä', 'x.long': 'l'.repeat(PAGE_STRING_LIMITS.value + 1), 'x.n': 5, 'native.secret': 'no' } });
assert.equal(changes, 1);
assert.deepEqual([strings.locale(), strings.t('x.a'), strings.t('kit.ok'), strings.message('x.long'), strings.message('native.secret'), strings.t('x.unknown')], ['de', 'Ä', 'OK', undefined, undefined, 'x.unknown']);
strings.set({ locale: 'de', messages: { 'x.a': 'Ä' } });
assert.equal(changes, 1, 'the same delivery changes nothing');
assert.equal(strings.source.message('x.a'), 'Ä');
// Bundled plugins are installed with a prefix; their strings are keyed by the source name.
assert.equal(strings.pluginNamespace('bt-clean-links.plugin.js'), 'clean-links');
assert.equal(strings.pluginNamespace('bt-mine.plugin.js'), 'bt-mine', 'an owner\'s file that only looks bundled keeps its name');
assert.equal(strings.pluginNamespace('other.plugin.js'), 'other');
off();
strings.set({ locale: 'fr', messages: {} });
assert.equal(changes, 1);
});
test('command report and developer errors without a bridge or a toast do nothing', () => {
assert.doesNotThrow(() => installCommandReport({ list: () => [], onChange: () => () => {} }, undefined)());
assert.doesNotThrow(() => devErrorReporter(() => undefined, String)('a.plugin.js', new Error('x')));
assert.doesNotThrow(() => devErrorReporter(() => () => { throw new Error('toast broke'); }, String)('a.plugin.js', new Error('x')));
assert.doesNotThrow(() => devErrorReporter(() => () => {}, String)(42, new Error('x')));
});
test('the panel button sits above a bottom bar, never over a viewer or under an overlay', () => {
assert.equal(buttonBottom(null, 800), BUTTON_GAP);
assert.equal(buttonBottom(747, 800), 53 + BUTTON_GAP, 'above a 53px bottom bar');
assert.equal(buttonBottom(800, 800), BUTTON_GAP, 'an empty bar at the bottom edge adds nothing');
assert.equal(buttonBottom(100, 800), BUTTON_GAP, 'an implausibly tall bar adds nothing');
const site = { hiddenOn: (pathname: string) => /\/photo\/\d+$/.test(pathname) };
assert.equal(buttonHidden(site, '/a/status/1/photo/1', false), true);
assert.equal(buttonHidden(site, '/home', true), true);
assert.equal(buttonHidden(site, '/home', false), false);
assert.equal(buttonHidden({}, '/a/status/1/photo/1', false), false);
});