import assert from 'node:assert/strict'; import { test } from 'node:test'; import { addGate, addRewrite, addTap } from '../src/inject/netHub.ts'; import { watchDom } from '../src/inject/domHub.ts'; import { installResponseObserver, type PageObservation } from '../src/observer/network.ts'; 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; } }; } // ---------- shared request hook ---------- 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://abs.twimg.com/responsive-web/client-web/x.wasm', 'https://pbs.twimg.com/media/a.jpg', 'https://video.twimg.com/a.mp4', '/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('HomeTimeline'), maxBytes: 1e6, onJson: value => got.push(value) }); const b = addTap(target, { match: url => url.includes('Timeline'), 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://x.com/i/api/graphql/q/HomeTimeline?variables=%7B%7D'); assert.deepEqual(await response.json(), body, 'X 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: 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; }); await target.fetch('/i/api/A'); assert.deepEqual(order, ['rewrite', 'gate:/i/api/B']); assert.deepEqual(calls, ['/i/api/B']); offRewrite(); assert.notEqual(target.fetch, inner, 'still hooked while the gate is registered'); offGate(); assert.equal(target.fetch, inner); }); // ---------- shared DOM watcher ---------- 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: chat pages skip features marked offOnChat', () => { const { win, flush, mutate } = fakeDomWindow('/i/chat/123-456'); const runs: string[] = []; const media = watchDom(win, () => runs.push('media'), { offOnChat: 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(); }); // ---------- response observer on chat pages ---------- test('response observer reads nothing on chat and DM pages', async () => { const clones = countClones(); try { const body = { data: { user: { result: { __typename: 'User', rest_id: '1', legacy: { following: true } } } } }; for (const pathname of ['/i/chat/1-2', '/messages', '/home']) { const page = Object.assign(new EventTarget(), { fetch: (async () => json(body)) as typeof fetch, CustomEvent, location: { origin: 'https://x.com', href: `https://x.com${pathname}`, pathname }, top: null as unknown, }); page.top = page; const events: PageObservation[] = []; const handle = installResponseObserver(page as unknown as Window & typeof globalThis, { onObservation: event => events.push(event) }); await (await page.fetch('https://x.com/i/api/graphql/q/UserByScreenName')).text(); await tick(); assert.equal(events.length, pathname === '/home' ? 1 : 0, pathname); handle.dispose(); } assert.equal(clones.count, 1, 'only the /home response was cloned'); } finally { clones.restore(); } });