wayBetter for X
This commit is contained in:
commit
df5dff619e
720 changed files with 147465 additions and 0 deletions
84
apps/desktop/tests/helpers/areaPage.ts
Normal file
84
apps/desktop/tests/helpers/areaPage.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
// A small page for area tests (tests/area-<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<string, string> = { ...AREA_MESSAGES, ...(en.messages as Record<string, string>) };
|
||||
|
||||
type Listener = (event: unknown) => void;
|
||||
export class FakeTarget {
|
||||
listeners = new Map<string, Set<Listener>>();
|
||||
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<string, boolean> = {}) => 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<string, boolean> = {}) => flagsWith({ ...Object.fromEntries(ALL_MODULES.map(module => [module.id, false])), ...patch });
|
||||
|
||||
export function areaPage(path = '/home') {
|
||||
const styles: { dataset: Record<string, string>; textContent: string; isConnected: boolean; remove(): void }[] = [];
|
||||
const store = new Map<string, string>();
|
||||
/** Elements the fake document returns per selector (`querySelectorAll` / `querySelector`). */
|
||||
const matches = new Map<string, object[]>();
|
||||
const observers = { live: 0, fire: [] as Array<() => void> };
|
||||
const toasts: string[] = [];
|
||||
const doc = new FakeTarget() as FakeTarget & Record<string, unknown>;
|
||||
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<string, string>, textContent: '', isConnected: false, remove() { node.isConnected = false; } }; styles.push(node); return node; },
|
||||
});
|
||||
const win = new FakeTarget() as FakeTarget & Record<string, unknown>;
|
||||
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<PluginApi>({ 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-<flag>`). */
|
||||
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(),
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue