// Verify the orphan-rescue fallback: if the current-visit pointer is lost // (storage.session cleared, simulating SW eviction / restart with a stable tab), // a follow-on click should still attach to the tab's last page via IndexedDB. // // node scripts/orphan-probe.mjs (after `npm run build`) import { chromium } from "playwright"; import { mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; import { fileURLToPath } from "node:url"; import { dirname, resolve } from "node:path"; const DIST = resolve(dirname(fileURLToPath(import.meta.url)), "../dist"); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); const context = await chromium.launchPersistentContext(mkdtempSync(resolve(tmpdir(), "hg-")), { headless: false, args: [`--disable-extensions-except=${DIST}`, `--load-extension=${DIST}`], }); try { let [sw] = context.serviceWorkers(); if (!sw) sw = await context.waitForEvent("serviceworker", { timeout: 10_000 }); const extId = new URL(sw.url()).host; const page = await context.newPage(); await page.goto("https://en.wikipedia.org/wiki/Main_Page", { waitUntil: "load" }); await sleep(1500); await page.goto("https://en.wikipedia.org/wiki/Istanbul", { waitUntil: "load" }); await sleep(1500); // Simulate a lost current-visit pointer (eviction / browser restart). const wiper = await context.newPage(); await wiper.goto(`chrome-extension://${extId}/viewer.html`, { waitUntil: "load" }); await wiper.evaluate(() => chrome.storage.session.clear()); await wiper.close(); // Follow-on click — pointer is gone, so this would orphan without the fallback. await page.bringToFront(); await page.evaluate(() => { const a = document.createElement("a"); a.href = "/wiki/Turkish_language"; document.body.prepend(a); a.click(); }); await page.waitForLoadState("load"); await sleep(1800); const viewer = await context.newPage(); await viewer.goto(`chrome-extension://${extId}/viewer.html`, { waitUntil: "load" }); const visits = await viewer.evaluate(async () => { const db = await new Promise((res, rej) => { const q = indexedDB.open("history-graph"); q.onsuccess = () => res(q.result); q.onerror = () => rej(q.error); }); return await new Promise((res, rej) => { const q = db.transaction("visits").objectStore("visits").getAll(); q.onsuccess = () => res(q.result); q.onerror = () => rej(q.error); }); }); const byId = new Map(visits.map((v) => [v.id, v])); const short = (u) => u.replace("https://en.wikipedia.org/wiki/", ""); console.log("\nafter clearing the pointer, the next click:"); for (const v of visits.sort((a, b) => a.ts - b.ts)) { const parent = v.parentId ? short(byId.get(v.parentId)?.url ?? "?") : "(root)"; console.log(` ${short(v.url).padEnd(20)} ← ${parent}`); } } finally { await context.close(); }