X renders its chat inside an open shadow root since 2026-10-01 (the XChatEmbed host [data-testid=xchatEmbedRoute]); the hub's document observer saw nothing of it, selector observers found nothing, feature sheets in <head> did not reach it and probes could not see it. A site adapter may name shadowHosts. The hub's one MutationObserver then also observes each such root (picked up on the next page mutation, or a few timed retries while a host has no root yet; dropped with the observer), selector observers search the roots too, domRoots(win) gives features the roots, a feature's ctx.css sheet is also adopted by each root (CSP-proof, updated in place, taken off on stop), and probes count matches inside the roots. Tests: hub observes and searches a root that appears later with one observer; registry adopts and removes the sheet and a probe inside a root is never a miss.
249 lines
12 KiB
TypeScript
249 lines
12 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { test } from 'node:test';
|
|
import { configureRuntime, isSiteUrl, type SiteAdapter } from '../src/site.ts';
|
|
import { addGate, addRewrite, addTap, addXhrGate, fetchHooked, isApiUrl } from '../src/netHub.ts';
|
|
import { configureDomHub, domRoots, isHeavyPath, observeSelector, watchDom } from '../src/domHub.ts';
|
|
|
|
const SITE: SiteAdapter = {
|
|
product: 'wayBetter for Test',
|
|
hosts: ['site.test'],
|
|
runtimeGlobal: 'Demo',
|
|
heavyPath: /^\/chat(?:\/|$)/,
|
|
apiMarks: ['/api/'],
|
|
xhrMarks: ['/upload'],
|
|
plugins: { name: 'Demo', product: 'wayBetter for Test', storagePrefix: 'demo-plugin:', styleDataKey: 'demoPlugin', toastId: 'demo-toast', toastBackground: '#000', itemSelector: 'article', chordKeys: [] },
|
|
};
|
|
configureRuntime(SITE);
|
|
|
|
const tick = () => new Promise(resolve => setTimeout(resolve, 15));
|
|
const json = (body: unknown) => new Response(JSON.stringify(body), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
|
|
function countClones() {
|
|
const original = Response.prototype.clone;
|
|
let clones = 0;
|
|
Response.prototype.clone = function (this: Response) { clones++; return original.call(this); };
|
|
return { get count() { return clones; }, restore() { Response.prototype.clone = original; } };
|
|
}
|
|
|
|
test('site adapter: origin and API marks come from the adapter', () => {
|
|
assert.ok(isSiteUrl(SITE, 'https://site.test/a'));
|
|
for (const url of ['http://site.test/', 'https://site.test.evil/', 'https://u:p@site.test/', 'nope']) assert.equal(isSiteUrl(SITE, url), false, url);
|
|
assert.ok(isApiUrl('https://site.test/api/v1/feed'));
|
|
assert.equal(isApiUrl('https://cdn.site.test/a.png'), false);
|
|
assert.ok(isHeavyPath('/chat/1') && !isHeavyPath('/home'));
|
|
});
|
|
|
|
test('net hub: non-API requests skip every stage and get the inner promise itself', async () => {
|
|
const promise = Promise.resolve(json({}));
|
|
const target = { fetch: (() => promise) as unknown as typeof fetch };
|
|
const seen: string[] = [];
|
|
const offTap = addTap(target, { match: url => { seen.push(url); return true; }, maxBytes: 1e6, onJson() {} });
|
|
const offRewrite = addRewrite(target, () => { seen.push('rewrite'); return null; });
|
|
for (const url of ['https://cdn.site.test/app.wasm', 'https://cdn.site.test/a.jpg', '/sw.js']) {
|
|
assert.equal(target.fetch(url), promise, url);
|
|
}
|
|
assert.deepEqual(seen, [], 'no URL parsing, matching or cloning for non-API traffic');
|
|
offTap();
|
|
offRewrite();
|
|
});
|
|
|
|
test('net hub: one clone and one parse feed every matching tap', async () => {
|
|
const clones = countClones();
|
|
try {
|
|
const body = { data: { ok: true } };
|
|
const target = { fetch: (async () => json(body)) as typeof fetch };
|
|
const got: unknown[] = [];
|
|
const a = addTap(target, { match: url => url.includes('Feed'), maxBytes: 1e6, onJson: value => got.push(value) });
|
|
const b = addTap(target, { match: url => url.includes('HomeFeed'), maxBytes: 1e6, onJson: value => got.push(value) });
|
|
const c = addTap(target, { match: () => false, maxBytes: 1e6, onJson: value => got.push(value) });
|
|
const response = await target.fetch('https://site.test/api/HomeFeed?cursor=1');
|
|
assert.deepEqual(await response.json(), body, 'the page still reads the original body');
|
|
await tick();
|
|
assert.equal(clones.count, 1);
|
|
assert.equal(got.length, 2);
|
|
assert.equal(got[0], got[1], 'both taps get the same parsed object');
|
|
a(); b(); c();
|
|
} finally {
|
|
clones.restore();
|
|
}
|
|
});
|
|
|
|
test('net hub: oversized bodies are never cloned', async () => {
|
|
const clones = countClones();
|
|
try {
|
|
const response = new Response('{}', { status: 200, headers: { 'content-length': '5000000' } });
|
|
const target = { fetch: (async () => response) as typeof fetch };
|
|
const off = addTap(target, { match: () => true, maxBytes: 1000, onJson() { assert.fail('not delivered'); } });
|
|
await target.fetch('/api/big');
|
|
await tick();
|
|
assert.equal(clones.count, 0);
|
|
off();
|
|
} finally {
|
|
clones.restore();
|
|
}
|
|
});
|
|
|
|
test('net hub: rewrite runs before the gate; unregistering the last stage restores fetch', async () => {
|
|
const calls: unknown[] = [];
|
|
const inner = ((input: unknown) => { calls.push(input); return Promise.resolve(json({})); }) as typeof fetch;
|
|
const target = { fetch: inner };
|
|
const order: string[] = [];
|
|
const offRewrite = addRewrite(target, input => { order.push('rewrite'); return String(input).replace('A', 'B'); });
|
|
const offGate = addGate(target, (input, _init, send) => { order.push(`gate:${String(input)}`); return String(input).endsWith('B') ? send() : null; });
|
|
assert.ok(fetchHooked(target));
|
|
await target.fetch('/api/A');
|
|
assert.deepEqual(order, ['rewrite', 'gate:/api/B']);
|
|
assert.deepEqual(calls, ['/api/B']);
|
|
offRewrite();
|
|
assert.notEqual(target.fetch, inner, 'still hooked while the gate is registered');
|
|
offGate();
|
|
assert.equal(target.fetch, inner);
|
|
assert.equal(fetchHooked(target), false);
|
|
});
|
|
|
|
test('net hub: XHR gates see API and XHR-mark requests only, and can rewrite the URL', () => {
|
|
const sent: unknown[] = [];
|
|
class FakeXhr {
|
|
url = '';
|
|
open(_method: string, url: string) { this.url = url; }
|
|
send(body?: unknown) { sent.push([this.url, body]); }
|
|
addEventListener() {}
|
|
}
|
|
const target = { fetch: (async () => json({})) as typeof fetch, XMLHttpRequest: FakeXhr as unknown as typeof XMLHttpRequest };
|
|
const seen: string[] = [];
|
|
const off = addXhrGate(target, {
|
|
open(_xhr, request) { return request.url.includes('/api/old') ? request.url.replace('old', 'new') : undefined; },
|
|
send(_xhr, request, body, sendNative) { seen.push(request.url); sendNative(`${String(body)}!`); return true; },
|
|
});
|
|
for (const url of ['/api/old', '/upload/chunk', '/static/app.js']) {
|
|
const xhr = new FakeXhr();
|
|
xhr.open('post', url);
|
|
xhr.send('body');
|
|
}
|
|
assert.deepEqual(seen, ['/api/new', '/upload/chunk']);
|
|
assert.deepEqual(sent, [['/api/new', 'body!'], ['/upload/chunk', 'body!'], ['/static/app.js', 'body']]);
|
|
off();
|
|
assert.equal(FakeXhr.prototype.send.name, 'send', 'the last gate restores the prototype');
|
|
});
|
|
|
|
function fakeDomWindow(pathname = '/home') {
|
|
const frames: FrameRequestCallback[] = [];
|
|
const observers: { callback: MutationCallback; options?: MutationObserverInit; connected: boolean }[] = [];
|
|
class FakeObserver {
|
|
entry: { callback: MutationCallback; options?: MutationObserverInit; connected: boolean };
|
|
constructor(callback: MutationCallback) { this.entry = { callback, connected: false }; observers.push(this.entry); }
|
|
observe(_node: Node, options: MutationObserverInit) { this.entry.options = options; this.entry.connected = true; }
|
|
disconnect() { this.entry.connected = false; }
|
|
}
|
|
const win = {
|
|
document: { body: {}, addEventListener() {}, removeEventListener() {} } as unknown as Document,
|
|
location: { pathname },
|
|
MutationObserver: FakeObserver as unknown as typeof MutationObserver,
|
|
requestAnimationFrame(callback: FrameRequestCallback) { frames.push(callback); return frames.length; },
|
|
};
|
|
const flush = () => { for (const frame of frames.splice(0)) frame(0); };
|
|
const mutate = (...types: MutationRecordType[]) => observers[0]!.callback(types.map(type => ({ type, attributeName: type === 'attributes' ? 'aria-selected' : null })) as unknown as MutationRecord[], {} as MutationObserver);
|
|
return { win, frames, observers, flush, mutate };
|
|
}
|
|
|
|
test('dom hub: one observer and one frame for all features; kinds and phases respected', () => {
|
|
const { win, frames, observers, flush, mutate } = fakeDomWindow();
|
|
const runs: string[] = [];
|
|
const writer = watchDom(win, () => runs.push('writer'));
|
|
const text = watchDom(win, () => runs.push('text'), { characterData: true });
|
|
const reader = watchDom(win, () => runs.push('reader'), { phase: 'read' });
|
|
const tabs = watchDom(win, () => runs.push('tabs'), { attributes: ['aria-selected'] });
|
|
assert.equal(observers.length, 1);
|
|
assert.deepEqual(observers[0]!.options, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ['aria-selected'] });
|
|
mutate('childList'); mutate('childList'); mutate('characterData');
|
|
assert.equal(frames.length, 1, 'many mutation callbacks, one frame');
|
|
flush();
|
|
assert.deepEqual(runs, ['reader', 'writer', 'text', 'tabs'], 'layout readers before DOM writers');
|
|
runs.length = 0;
|
|
mutate('characterData');
|
|
flush();
|
|
assert.deepEqual(runs, ['text'], 'text changes only wake features that asked for them');
|
|
runs.length = 0;
|
|
mutate('attributes');
|
|
flush();
|
|
assert.deepEqual(runs, ['tabs']);
|
|
runs.length = 0;
|
|
writer.schedule(); writer.schedule();
|
|
flush();
|
|
assert.deepEqual(runs, ['writer'], 'explicit schedule runs once per frame');
|
|
for (const handle of [writer, text, reader, tabs]) handle.dispose();
|
|
assert.equal(observers[0]!.connected, false, 'last feature off disconnects the observer');
|
|
});
|
|
|
|
test('dom hub: heavy pages skip features marked offOnHeavy', () => {
|
|
const { win, flush, mutate } = fakeDomWindow('/chat/123');
|
|
const runs: string[] = [];
|
|
const media = watchDom(win, () => runs.push('media'), { offOnHeavy: true });
|
|
const nav = watchDom(win, () => runs.push('nav'));
|
|
mutate('childList');
|
|
flush();
|
|
assert.deepEqual(runs, ['nav']);
|
|
win.location.pathname = '/home';
|
|
mutate('childList');
|
|
flush();
|
|
assert.deepEqual(runs, ['nav', 'media', 'nav']);
|
|
media.dispose();
|
|
nav.dispose();
|
|
});
|
|
|
|
test('dom hub: a failing feature does not stop the others', () => {
|
|
const { win, flush, mutate } = fakeDomWindow();
|
|
const runs: string[] = [];
|
|
const errors: unknown[] = [];
|
|
const original = console.error;
|
|
console.error = (...args: unknown[]) => { errors.push(args[0]); };
|
|
try {
|
|
const bad = watchDom(win, () => { throw new Error('boom'); });
|
|
const good = watchDom(win, () => runs.push('good'));
|
|
mutate('childList');
|
|
flush();
|
|
assert.deepEqual(runs, ['good']);
|
|
assert.deepEqual(errors, ['[wayBetter for Test] DOM feature failed']);
|
|
bad.dispose();
|
|
good.dispose();
|
|
} finally {
|
|
console.error = original;
|
|
}
|
|
});
|
|
|
|
test('dom hub: an open shadow root of a configured host is observed and searched like the page (the X chat embed, 2026-10-01)', async () => {
|
|
configureDomHub({ shadowHosts: ['[data-testid="embedRoute"]'] });
|
|
try {
|
|
const { win, observers, flush, mutate } = fakeDomWindow('/chat/1');
|
|
const observed: unknown[] = [];
|
|
const inRoot = [{ id: 'row-1' }, { id: 'row-2' }];
|
|
const root = { querySelectorAll: (selector: string) => (selector === '.row' ? inRoot : []) };
|
|
const host: { shadowRoot: unknown } = { shadowRoot: null };
|
|
const doc = win.document as unknown as Record<string, unknown>;
|
|
doc.querySelectorAll = (selector: string) => (selector === '[data-testid="embedRoute"]' ? [host] : []);
|
|
const seen: string[] = [];
|
|
const stop = observeSelector(win, '.row', element => seen.push((element as unknown as { id: string }).id));
|
|
// Every observe call is recorded (the page body first, then each root).
|
|
const entry = observers[0]!;
|
|
const FakeObserver = win.MutationObserver as unknown as { prototype: { observe(node: unknown, options: MutationObserverInit): void } };
|
|
const original = FakeObserver.prototype.observe;
|
|
FakeObserver.prototype.observe = function (node: unknown, options: MutationObserverInit) { observed.push(node); return original.call(this, node as Node, options); };
|
|
try {
|
|
assert.deepEqual(domRoots(win), [win.document], 'the host has no root yet: only the page');
|
|
mutate('childList'); flush();
|
|
assert.deepEqual(seen, [], 'nothing inside yet');
|
|
// The host attaches its root (no mutation for that); the next page mutation picks it up.
|
|
host.shadowRoot = root;
|
|
mutate('childList'); flush();
|
|
assert.ok(observed.includes(root), 'the root is observed by the one observer of the hub');
|
|
assert.deepEqual(domRoots(win), [win.document, root]);
|
|
assert.deepEqual(seen, ['row-1', 'row-2'], 'the selector observer found the rows inside the root');
|
|
inRoot.push({ id: 'row-3' });
|
|
entry.callback([{ type: 'childList' }] as unknown as MutationRecord[], {} as MutationObserver); flush();
|
|
assert.deepEqual(seen, ['row-1', 'row-2', 'row-3'], 'a mutation in the root wakes the same observer');
|
|
assert.equal(observers.length, 1, 'still one MutationObserver');
|
|
} finally { FakeObserver.prototype.observe = original; }
|
|
stop();
|
|
assert.equal(entry.connected, false, 'the last subscriber off disconnects it, roots included');
|
|
} finally { configureDomHub({ shadowHosts: [] }); }
|
|
});
|