// Verify "reopen a closed tab" (Phase 1 — manual reopen-from-node): // 1. build a chain Istanbul → Turkish_language → Turkey in a tab, // 2. CLOSE the tab (its visits survive; live per-tab state is dropped), // 3. reopen Turkey in a NEW tab + send hg:attach for the Turkey node (as the // viewer's "Reopen here" affordance does) → the create's commit must NOT // duplicate Turkey, // 4. navigate onward (→ Ankara) → must branch from Turkey (the new tab // continues the closed tab's subtree, not a fresh root). // // node scripts/reopen-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 clickLink = (page, href) => page.evaluate((h) => { const a = document.createElement("a"); a.href = h; document.body.prepend(a); a.click(); }, href); const context = await chromium.launchPersistentContext(mkdtempSync(resolve(tmpdir(), "hg-")), { headless: false, args: [`--disable-extensions-except=${DIST}`, `--load-extension=${DIST}`], }); const readVisits = (pg) => pg.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); }); }); try { let [sw] = context.serviceWorkers(); if (!sw) sw = await context.waitForEvent("serviceworker", { timeout: 10_000 }); const extId = new URL(sw.url()).host; sw.on("console", (m) => { const t = m.text(); if (t.includes("history-graph")) console.log(" [SW]", t.split(" {")[0]); }); // 1. Build a chain in a tab we will then close. const page = await context.newPage(); await page.goto("https://en.wikipedia.org/wiki/Istanbul", { waitUntil: "load" }); await sleep(1500); await clickLink(page, "/wiki/Turkish_language"); await page.waitForLoadState("load"); await sleep(1500); await clickLink(page, "/wiki/Turkey"); await page.waitForLoadState("load"); await sleep(1500); // Grab the closed-tab leaf (Turkey) before closing. const viewer = await context.newPage(); await viewer.goto(`chrome-extension://${extId}/viewer.html`, { waitUntil: "load" }); const before = await readVisits(viewer); const turkey = before.find((v) => v.url.endsWith("/Turkey")); if (!turkey) throw new Error("setup failed: no Turkey node captured"); // 2. Close the tab → tabs.onRemoved drops its live current/nav-stack state. await page.close(); await sleep(800); // 3. Reopen Turkey in a NEW tab and register the attach (what reopenFrom does): // create the tab BLANK, register the attach, THEN navigate — so the url's // commit can't race ahead of the seed and fork a duplicate root. const sourceGroup = turkey.tabKey ?? String(turkey.tabId); const newTabId = await viewer.evaluate( async ({ id, url, tabKey }) => { const tab = await chrome.tabs.create({}); await chrome.runtime.sendMessage({ type: "hg:attach", tabId: tab.id, id, url, tabKey }); await chrome.tabs.update(tab.id, { url }); return tab.id; }, { id: turkey.id, url: turkey.url, tabKey: sourceGroup }, ); // The page object Playwright opened is a blank tab; the real reopened tab is // the one chrome.tabs.create made. Find it and drive it onward. await sleep(2000); const pages = context.pages(); const turkeyPage = pages.find((p) => p.url() === turkey.url) ?? pages[pages.length - 1]; await turkeyPage.bringToFront(); await clickLink(turkeyPage, "/wiki/Ankara"); await turkeyPage.waitForLoadState("load"); await sleep(1800); // 4. Assertions. const after = await readVisits(viewer); const byId = new Map(after.map((v) => [v.id, v])); const short = (u) => (u ? u.replace("https://en.wikipedia.org/wiki/", "") : "(root)"); const turkeyCount = after.filter((v) => v.url.endsWith("/Turkey")).length; const ankara = after.find((v) => v.url.endsWith("/Ankara")); const ankaraGroup = ankara && (ankara.tabKey ?? String(ankara.tabId)); console.log(`\nreopened new tab id: ${newTabId}`); console.log(`Turkey nodes: ${turkeyCount} (want 1 — reopen didn't duplicate)`); console.log(`Ankara parent: ${short(byId.get(ankara?.parentId)?.url)} (want Turkey)`); console.log(`Ankara group: ${ankaraGroup} (want ${sourceGroup} — rejoined original tab)`); const ok = turkeyCount === 1 && ankara && ankara.parentId === turkey.id && ankaraGroup === sourceGroup; console.log(`\n${ok ? "✓" : "✗"} reopened tab continued the closed tab's subtree in its group`); if (!ok && ankara) { console.log(` Ankara.parentId=${ankara.parentId} expected ${turkey.id}`); console.log(` Ankara.group=${ankaraGroup} expected ${sourceGroup}`); } } finally { await context.close(); }