Files
Steffen Becker 554f0f6674 feat: each tab edits its own document
Every document-scoped key now goes through docKey(), which hangs it off the
tab's document id; the first document keeps the empty id, so existing browser
copies read back unchanged. A fresh tab takes up the most recently used
document no live tab holds, else starts a new one — one signed timestamp per
document (edentext-live@id) says which, written only by the tab holding it.
Snapshots and embedded fonts get a database per document. The picture store
stays shared and content-addressed, so its sweep now drops only keys no stored
document names any more, instead of the open document's own.
2026-09-07 23:50:11 +02:00

72 lines
2.9 KiB
JavaScript

// Several tabs, several documents: the one thing jsdom cannot show, since it has no
// second tab. Two pages in one browser context share localStorage and have their own
// sessionStorage each — exactly what two tabs of the same browser are.
import { checker, previewServer, openApp } from '../browser.mjs';
const PORT = +(process.env.TABS_PORT ?? 4183);
const URL = `http://localhost:${PORT}/`;
const { check, failures } = checker();
const server = await previewServer(PORT);
const { browser, page, pageErrors } = await openApp(PORT);
// openApp's page has a context of its own; the tabs need to share one.
await page.close();
const ctx = await browser.newContext({ viewport: { width: 1400, height: 1000 }, locale: 'en-US' });
const settle = (p) => p.waitForTimeout(1500);
const text = (p) => p.textContent('.tiptap');
async function openTab() {
const p = await ctx.newPage();
p.on('dialog', (d) => d.accept());
p.on('pageerror', (err) => pageErrors.push(String(err)));
await p.goto(URL, { waitUntil: 'load' });
await p.waitForSelector('.tiptap', { timeout: 15_000 });
return p;
}
async function type(p, s) {
await p.click('.tiptap p');
await p.keyboard.type(s);
await settle(p);
}
try {
const a = await openTab();
await a.evaluate(() => localStorage.clear());
await a.reload({ waitUntil: 'load' });
await a.waitForSelector('.tiptap');
await type(a, 'Alpha');
const b = await openTab();
check(!(await text(b)).includes('Alpha'), 'a second tab starts on its own empty document');
await type(b, 'Beta');
check((await text(a)).includes('Alpha') && !(await text(a)).includes('Beta'), 'the first tab keeps its own text');
const keys = await a.evaluate(() => Object.fromEntries(Object.keys(localStorage)
.filter((k) => k.startsWith('edentext-doc'))
.map((k) => [k, localStorage.getItem(k) ?? ''])));
const scoped = Object.keys(keys).find((k) => k.startsWith('edentext-doc@'));
check(keys['edentext-doc']?.includes('Alpha'), 'the first document keeps the unsuffixed key');
check(!!scoped && keys[scoped].includes('Beta'), `the second document has its own key (${scoped})`);
for (const p of [a, b]) {
await p.reload({ waitUntil: 'load' });
await p.waitForSelector('.tiptap');
await settle(p);
}
check((await text(a)).includes('Alpha') && (await text(b)).includes('Beta'), 'both tabs keep their document across a reload');
// A closed tab signs its document off, so the next fresh tab takes it up again.
await b.close();
const c = await openTab();
await settle(c);
check((await text(c)).includes('Beta'), 'a new tab takes up the document the closed tab held');
} catch (err) {
check(false, `tabs run threw: ${err.message ?? err}`);
} finally {
check(pageErrors.length === 0, pageErrors.length ? `no uncaught page errors — got: ${pageErrors.join(' | ')}` : 'no uncaught page errors');
await browser.close();
if (server) process.kill(-server.pid);
}
process.exit(failures.length ? 1 : 0);