import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
import { createHash } from 'node:crypto';
import { chromium } from 'playwright';
import { describePage } from './browser-worker.mjs';
// Compare against an explicitly saved pre-change evaluate function, never a
// production/RPC switch. All fixtures are synthetic and network is blocked.
const baselinePath = process.argv[2];
assert.ok(baselinePath, 'Pass the saved baseline module path');
const { describeBaseline } = await import(pathToFileURL(path.resolve(baselinePath)).href);
const runDirectory = path.resolve('.tmp', `observation-cache-${Date.now()}`);
await fs.mkdir(runDirectory, { recursive: true });
const report = { browser: process.platform === 'win32' ? 'msedge' : 'chromium', real_platform_requests: 0,
baseline_sha256: createHash('sha256').update(await fs.readFile(path.resolve(baselinePath))).digest('hex'), comparisons: [], performance: [] };
const browser = await chromium.launch({ headless: true, ...(process.platform === 'win32' ? { channel: 'msedge' } : {}) });
try {
const page = await browser.newPage({ viewport: { width: 1120, height: 760 } });
await page.route('**/*', route => route.abort());
const cards = Array.from({ length: 230 }, (_, index) => ``).join('');
await page.setContent(`
Clipped ordinary text
Clipped away content
Ordinary heading
Fresh text
Contact 13800138000
Hidden content
Transparent content
Concealed content
Offscreen retained loaded text
Visible overlay
`);
await page.evaluate(() => {
const costs = () => ({ computed_style: 0, bounding_rect: 0, query_selector: 0, query_selector_all: 0, element_from_point: 0 });
window.observationCosts = costs();
window.resetObservationCosts = () => { window.observationCosts = costs(); };
const wrap = (owner, name, cost) => {
const original = owner[name];
owner[name] = function (...args) { window.observationCosts[cost] += 1; return original.apply(this, args); };
};
wrap(window, 'getComputedStyle', 'computed_style');
wrap(Element.prototype, 'getBoundingClientRect', 'bounding_rect');
for (const owner of [Document.prototype, Element.prototype]) {
wrap(owner, 'querySelector', 'query_selector');
wrap(owner, 'querySelectorAll', 'query_selector_all');
}
wrap(Document.prototype, 'elementFromPoint', 'element_from_point');
});
async function sample(read, request) {
await page.evaluate(() => window.resetObservationCosts());
const started = performance.now();
const value = await read(page, request);
const duration_ms = performance.now() - started;
return { value, duration_ms, costs: await page.evaluate(() => window.observationCosts) };
}
async function compare(name, mutate, request = null) {
if (mutate) await page.evaluate(mutate);
const baseline = await sample(describeBaseline, request), optimized = await sample(describePage, request);
assert.deepEqual(optimized.value, baseline.value, name);
report.comparisons.push({ name, exact: true, elements: optimized.value.elements.length, ...(optimized.value.masks ? { masks: optimized.value.masks.length } : {}) });
return { baseline, optimized };
}
const initial = await compare('initial full observation with overlay, clipping, sensitive subtrees and output caps');
assert.ok(!initial.optimized.value.text.includes('SENSITIVE_ANCESTOR'));
assert.ok(!initial.optimized.value.text.includes('SENSITIVE_FORM'));
assert.ok(!initial.optimized.value.text.includes('13800138000'));
assert.ok(initial.optimized.value.masks.includes('#phone-leaf'));
assert.ok(initial.optimized.value.elements.length <= 120);
assert.equal(initial.optimized.value.viewport.scroll_y, 0);
await compare('canonical metadata', null, { selector: '#search-field', requireCanonical: true });
await compare('collection metadata', null, { selector: '#scroller a.name', collection: true });
await compare('noncanonical metadata rejected', null, { selector: 'input[type=search]', requireCanonical: true });
await compare('sensitive metadata rejected', null, { selector: '#sensitive-child' });
await compare('bounded metadata rejects more than 500 nodes', () => {
const root = document.createElement('div'); root.id = 'many';
root.innerHTML = '