// A small page for area tests (tests/area-.test.ts): the X plugin API and the feature // registry with every area's modules defined, over a fake document that records styles, listeners, // storage and live observers. No DOM library: a module that only adds CSS, listeners, commands and // storage runs as it is; one that reads the DOM gets its elements through `matches`. // // const env = areaPage('/home'); // env.registry.setFlags(flagsWith({ 'reader.readMarker': true })); // assert.deepEqual(env.registry.active(), ['reader.readMarker']); // env.go('/someone/status/1'); // a route change // env.registry.dispose(); // always: the route poll is a timer import en from '../../locales/en.json' with { type: 'json' }; import { AREA_MESSAGES } from '../../src/areas/messages.ts'; import { ALL_MODULES } from '../../src/areas/modules.ts'; import { surfacesOf } from '../../src/areas/surfaces.ts'; import { X_TABLE } from '../../src/areas/table.ts'; import { createPluginApi, type PluginApi } from '../../src/inject/pluginApi.ts'; import { createFeatureRegistry } from '../../../../waybetter/packages/runtime/src/featureModules.ts'; import { effectiveFlags, tableDefaults } from '../../../../waybetter/packages/runtime/src/featureTable.ts'; /** English as the X runtime and the panel have it: the catalog plus the areas' new words. */ export const messages: Record = { ...AREA_MESSAGES, ...(en.messages as Record) }; type Listener = (event: unknown) => void; export class FakeTarget { listeners = new Map>(); addEventListener(type: string, fn: Listener) { (this.listeners.get(type) ?? this.listeners.set(type, new Set()).get(type)!).add(fn); } removeEventListener(type: string, fn: Listener) { this.listeners.get(type)?.delete(fn); } emit(type: string, event: unknown = {}) { for (const fn of [...(this.listeners.get(type) ?? [])]) fn(event); } /** Every listener on this target, of any type. */ count() { return [...this.listeners.values()].reduce((sum, set) => sum + set.size, 0); } } /** The flags as the runtime applies them: the defaults, `patch` over them, ownership resolved. */ export const flagsWith = (patch: Record = {}) => effectiveFlags(X_TABLE, { ...tableDefaults(X_TABLE), ...patch }); /** Only `patch` runs: every module-backed row is forced off first (some are on by default). */ export const flagsOnly = (patch: Record = {}) => flagsWith({ ...Object.fromEntries(ALL_MODULES.map(module => [module.id, false])), ...patch }); export function areaPage(path = '/home') { const styles: { dataset: Record; textContent: string; isConnected: boolean; remove(): void }[] = []; const store = new Map(); /** Elements the fake document returns per selector (`querySelectorAll` / `querySelector`). */ const matches = new Map(); const observers = { live: 0, fire: [] as Array<() => void> }; const toasts: string[] = []; const doc = new FakeTarget() as FakeTarget & Record; Object.assign(doc, { activeElement: null, documentElement: { getAttribute: () => 'en' }, body: { firstElementChild: {} }, head: { appendChild(node: { isConnected: boolean }) { node.isConnected = true; } }, querySelectorAll: (selector: string) => matches.get(selector) ?? [], querySelector: (selector: string) => (matches.get(selector) ?? [])[0] ?? null, createElement: () => { const node = { dataset: {} as Record, textContent: '', isConnected: false, remove() { node.isConnected = false; } }; styles.push(node); return node; }, }); const win = new FakeTarget() as FakeTarget & Record; const location = { href: `https://x.com${path}`, pathname: path }; Object.assign(win, { document: doc, location, localStorage: { get length() { return store.size; }, key: (i: number) => [...store.keys()][i] ?? null, getItem: (k: string) => store.get(k) ?? null, setItem: (k: string, v: string) => { store.set(k, v); }, removeItem: (k: string) => { store.delete(k); } }, requestAnimationFrame: (fn: () => void) => { fn(); return 1; }, cancelAnimationFrame: () => {}, setTimeout: () => 0, clearTimeout: () => {}, MutationObserver: class { on = false; constructor(fn: () => void) { observers.fire.push(() => { if (this.on) fn(); }); } observe() { if (!this.on) { this.on = true; observers.live += 1; } } disconnect() { if (this.on) { this.on = false; observers.live -= 1; } } }, }); const english = { locale: () => 'en', message: (key: string) => messages[key], onChange: () => () => {} }; const base = createPluginApi(win as unknown as Window & typeof globalThis, undefined, undefined, english); // The kit's toast needs a real DOM; the tests read what was said. const api = Object.freeze({ ...base, toast: (message: string) => { toasts.push(message); } }) as PluginApi; const registry = createFeatureRegistry({ win: win as unknown as Window & typeof globalThis, table: X_TABLE, api, label: 'wayBetter for X', surfaces: () => surfacesOf(location.pathname) }); for (const module of ALL_MODULES) registry.define(module); return { registry, api, doc, win, store, matches, observers, toasts, /** X navigated (pushState): the registry re-checks surfaces. */ go(next: string) { location.pathname = next; location.href = `https://x.com${next}`; win.emit('popstate'); }, /** X rendered something: every live observer fires (one frame). */ mutate() { for (const fire of observers.fire) fire(); }, /** Ids of the stylesheets that are in the page right now (`wbf-`). */ live: () => styles.filter(style => style.isConnected && style.textContent).map(style => style.dataset.betterTwttrPlugin).sort(), /** Listeners on the document and the window together. */ listeners: () => doc.count() + win.count(), }; }