// 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(),
};
}