// 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> = []; const errors: unknown[] = []; const original = console.error; console.error = (...args: unknown[]) => { errors.push(args[0]); }; try { const flags = createFeatureFlags({ 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> = []; const flags = createFeatureFlags({ 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 . const listeners: Array<() => void> = []; const doc = { documentElement: null as unknown, addEventListener: (_: string, listener: () => void) => { listeners.push(listener); } }; const late: Array> = []; const waiting = createFeatureFlags({ 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> = []; const gated = createFeatureFlags({ 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 = {}) { return { navigator: { webdriver: false }, ...queued } as unknown as Window & typeof globalThis & Record; } test('the runtime is published under its names, takes what native code queued and reports commands', async () => { const seen: Record = {}; 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); });