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.
72 lines
2.9 KiB
JavaScript
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);
|