← back to Homesonspec
apps/mobile/lib/tracker-block.test.mjs
138 lines
/**
* Behavioural tests for TRACKER_BLOCK — the script actually injected into every WebView.
*
* `tracker-policy.test.mjs` only covers isTrackerUrl(), which is the NATIVE-side URL
* filter. That left the thing doing most of the work — the injected page script that
* neuters gtag/fbq and intercepts fetch/XHR/Image/sendBeacon — with no test at all.
* Since the App Store "Data Not Collected" claim rests on this script actually working,
* "it looks right" was not good enough.
*
* These tests run the real script text in a sandbox with a fake window/document and
* assert observable behaviour: tracker requests are dropped, first-party requests are
* untouched.
*/
import assert from 'node:assert/strict';
import test from 'node:test';
import vm from 'node:vm';
import { buildTrackerBlock } from './tracker-block.ts';
import { TRACKER_HOSTS } from './tracker-policy.ts';
// Test the script exactly as shipped: built from the REAL host list.
const TRACKER_BLOCK = buildTrackerBlock(TRACKER_HOSTS);
const TRACKER = 'https://www.googletagmanager.com/gtag/js?id=G-ZGFNZ3RQ6S';
const PIXEL = 'https://connect.facebook.net/en_US/fbevents.js';
const FIRST_PARTY = 'https://homesonspec.com/api/listings';
/** Build a minimal browser-ish sandbox, run TRACKER_BLOCK in it, return the context. */
function runBlock() {
const calls = { fetch: [], beacon: [], xhrSend: [], imgSrc: [] };
class FakeXHR {
open(method, url) { this.__url = url; }
send() { calls.xhrSend.push(this.__url); }
}
class FakeImage {
constructor() { this._src = ''; }
set src(v) { this._src = v; calls.imgSrc.push(v); }
get src() { return this._src; }
}
const el = () => ({ _attrs: {}, setAttribute(k, v) { this._attrs[k] = v; }, getAttribute(k) { return this._attrs[k]; } });
const sandbox = {
calls,
Response: class { constructor(body, init) { this.body = body; this.status = init?.status ?? 200; } },
Promise,
URL,
XMLHttpRequest: FakeXHR,
MutationObserver: class { observe() {} },
console,
document: {
documentElement: {},
createElement: (tag) => ({ tagName: String(tag).toUpperCase(), ...el() }),
},
};
sandbox.window = sandbox;
sandbox.Image = FakeImage;
sandbox.HTMLImageElement = FakeImage;
sandbox.HTMLElement = function () {};
sandbox.location = { href: 'https://homesonspec.com/' };
sandbox.navigator = { sendBeacon: (url) => { calls.beacon.push(url); return true; } };
sandbox.fetch = (input) => { calls.fetch.push(input?.url ?? input); return Promise.resolve('REAL'); };
vm.createContext(sandbox);
vm.runInContext(TRACKER_BLOCK, sandbox);
return sandbox;
}
test('gtag / fbq / dataLayer are neutered into no-ops', () => {
const w = runBlock();
assert.equal(typeof w.gtag, 'function');
assert.equal(typeof w.fbq, 'function');
assert.doesNotThrow(() => w.gtag('event', 'page_view', { send_to: 'G-XXXX' }));
assert.doesNotThrow(() => w.fbq('track', 'PageView'));
assert.doesNotThrow(() => w.dataLayer.push({ event: 'x' }));
// dataLayer.push must not actually accumulate — a real GTM would drain it.
assert.equal(w.dataLayer.length, 0, 'dataLayer.push should be a no-op, not a real push');
});
test('fetch to a tracker host is dropped; first-party fetch passes through', async () => {
const w = runBlock();
const blocked = await w.fetch(TRACKER);
assert.equal(blocked.status, 204, 'tracker fetch should be short-circuited with an empty 204');
assert.deepEqual(w.calls.fetch, [], 'the real fetch must never see a tracker URL');
const real = await w.fetch(FIRST_PARTY);
assert.equal(real, 'REAL', 'first-party fetch must reach the real implementation');
assert.deepEqual(w.calls.fetch, [FIRST_PARTY]);
});
test('sendBeacon (GA4 default transport) is dropped for trackers only', () => {
const w = runBlock();
assert.equal(w.navigator.sendBeacon('https://region1.google-analytics.com/g/collect'), false);
assert.deepEqual(w.calls.beacon, [], 'no tracker beacon may reach the real sendBeacon');
assert.equal(w.navigator.sendBeacon(FIRST_PARTY), true);
assert.deepEqual(w.calls.beacon, [FIRST_PARTY]);
});
test('XHR to a tracker never sends; first-party XHR does', () => {
const w = runBlock();
const bad = new w.XMLHttpRequest();
bad.open('POST', PIXEL);
bad.send();
assert.deepEqual(w.calls.xhrSend, [], 'tracker XHR must not send');
const good = new w.XMLHttpRequest();
good.open('GET', FIRST_PARTY);
good.send();
assert.deepEqual(w.calls.xhrSend, [FIRST_PARTY]);
});
test('Image() pixel to a tracker is dropped; a first-party image still loads', () => {
const w = runBlock();
const px = new w.Image();
px.src = 'https://www.facebook.com/tr?id=123&ev=PageView';
assert.deepEqual(w.calls.imgSrc, [], 'the Facebook tr pixel must never get a real src');
const img = new w.Image();
img.src = 'https://homesonspec.com/hero.jpg';
assert.deepEqual(w.calls.imgSrc, ['https://homesonspec.com/hero.jpg']);
});
test('a subdomain of a tracker host is blocked too (not just the bare host)', async () => {
const w = runBlock();
const r = await w.fetch('https://analytics.google-analytics.com/g/collect?v=2');
assert.equal(r.status, 204);
assert.deepEqual(w.calls.fetch, []);
});
test('a lookalike domain is NOT blocked (the filter must not over-reach)', async () => {
const w = runBlock();
const r = await w.fetch('https://notgoogletagmanager.com/thing.js');
assert.equal(r, 'REAL', 'a lookalike host must pass through — over-blocking breaks real sites');
});