web: count a roll as it is read, and open a frame without losing the count
Two holes left by the last change. A frame is opened with `window.location.href` rather than a link, so the studio was still handed a fresh page and the scan with it — every route out of the catalogue now goes through `go()`, which pushes the address instead of reloading while a scan is in flight. And a row counted what was filed away rather than what had been read, so it sat at zero for the whole of a first scan: the catalogue's frames only reach IndexedDB in one batch at the end. `ScanProgress.counts` carries the frames the scan has reached, per row, off the scan's own bookkeeping, and the column reads it while the scan runs — the row under the reader's eye moves as the roll is read, and the toolbar's line stays the whole picture. A re-read counts from the start, which is what it is doing. The regression check grows the two: a frame opened mid-scan from the catalogue (of a roll it already holds) has to stay in the page, and the row has to count.
This commit is contained in:
@@ -90,8 +90,8 @@ const browser = await playwright.chromium.launch({
|
||||
});
|
||||
const context = await browser.newContext();
|
||||
|
||||
// Documents this tab has loaded, across navigations: 1 means the click stayed in
|
||||
// the page, 2 means the page was thrown away and reloaded.
|
||||
// Documents this tab has loaded, across navigations: the count only moves when a
|
||||
// click threw the page away and started a new one.
|
||||
await context.addInitScript(() => {
|
||||
const n = Number(sessionStorage.getItem('docLoads') ?? '0') + 1;
|
||||
sessionStorage.setItem('docLoads', String(n));
|
||||
@@ -104,6 +104,10 @@ await context.addInitScript(
|
||||
if (!bytes.has(name)) bytes.set(name, fetch(`${origin}/${name}`).then((r) => r.arrayBuffer()));
|
||||
return bytes.get(name);
|
||||
};
|
||||
// A second scan of the same roll reads the same names again; dropping what the
|
||||
// stand-in already holds makes it go back to the server for each of them, so
|
||||
// a re-reading roll has the same minutes in it a first one does.
|
||||
window.__forget = () => bytes.clear();
|
||||
const fileHandle = (name) => {
|
||||
const handle = { kind: 'file', name, __fake: true };
|
||||
Object.defineProperty(handle, 'getFile', {
|
||||
@@ -112,7 +116,11 @@ await context.addInitScript(
|
||||
return handle;
|
||||
};
|
||||
const dirHandle = (label, node) => {
|
||||
const handle = { kind: 'directory', name: label };
|
||||
// `__fake` on a folder too: the catalogue reads its folders back out of the
|
||||
// store after every remount, and a handle that does not say what it is comes
|
||||
// back as a plain object with no `queryPermission` — a folder it cannot ask
|
||||
// about is a folder it offers to reconnect.
|
||||
const handle = { kind: 'directory', name: label, __fake: true };
|
||||
Object.defineProperties(handle, {
|
||||
values: {
|
||||
value: () =>
|
||||
@@ -186,6 +194,11 @@ const progressText = (timeout) =>
|
||||
|
||||
const docs = () => page.evaluate(() => Number(sessionStorage.getItem('docLoads') ?? '0'));
|
||||
const tiles = () => page.$$eval('[data-key^="lib-thumb-"]', (els) => els.length);
|
||||
// The count of documents standing when the page was last settled: every step that
|
||||
// follows a link or a frame asserts against this, because a link followed with no
|
||||
// scan in flight is *meant* to load the page again.
|
||||
let seen = await docs();
|
||||
const noReload = async () => (await docs()) === seen;
|
||||
|
||||
// Hand over to the studio while the roll is still being read, which is the whole
|
||||
// point: part of it in, the rest still to come.
|
||||
@@ -194,16 +207,17 @@ step('the roll is half read when the studio is opened', !!midway, String(midway)
|
||||
|
||||
await page.click('[data-key="lib-studio"]');
|
||||
await page.waitForSelector('[data-key="nav-library"]', { timeout: 15_000 });
|
||||
step('the handover stays in the page, it does not reload it', (await docs()) === 1, `${await docs()} document load(s)`);
|
||||
step('the handover stays in the page, it does not reload it', await noReload(), `${await docs()} document load(s)`);
|
||||
step('and it lands in the studio', new URL(page.url()).pathname === '/app', page.url());
|
||||
|
||||
// Back to the catalogue mid-scan: the screen that started the scan is gone and
|
||||
// back, and the roll is still being read — the reading belongs to the tab.
|
||||
await page.click('[data-key="nav-library"]');
|
||||
await page.waitForSelector('[data-key="lib-frame"]', { timeout: 15_000 });
|
||||
step('coming back stays in the page too', (await docs()) === 1, `${await docs()} document load(s)`);
|
||||
step('coming back stays in the page too', await noReload(), `${await docs()} document load(s)`);
|
||||
const progress = await progressText(15_000);
|
||||
step('the catalogue comes back showing the scan it missed', !!progress, String(progress));
|
||||
await page.waitForSelector('[data-key^="lib-node-"]', { timeout: 15_000 });
|
||||
const tree = await page.$$eval('[data-key^="lib-node-"]', (els) => els.map((e) => e.dataset.key));
|
||||
step('the tree is there, from the scan that outlived its screen', tree.includes('lib-node-SlowRoll'), JSON.stringify(tree));
|
||||
|
||||
@@ -217,7 +231,7 @@ for (let i = 0; i < 300 && indexed_to < TOTAL; i++) {
|
||||
if (indexed_to < TOTAL) await sleep(200);
|
||||
}
|
||||
step('the whole roll is read with the studio up', indexed_to === TOTAL, `${indexed_to}/${TOTAL} indexed`);
|
||||
step('and the studio is still the page it was', (await docs()) === 1, `${await docs()} document load(s)`);
|
||||
step('and the studio is still the page it was', await noReload(), `${await docs()} document load(s)`);
|
||||
|
||||
await page.click('[data-key="nav-library"]');
|
||||
await page.waitForFunction(
|
||||
@@ -228,6 +242,42 @@ await page.waitForFunction(
|
||||
step('every frame ends up as a thumbnail', (await tiles()) === TOTAL, `${await tiles()} tiles`);
|
||||
const grown = await page.$$eval('[data-key^="lib-node-"]', (els) => els.map((e) => e.dataset.key));
|
||||
step('the tree ends up holding the deepest folder of the roll', grown.includes('lib-node-SlowRoll/2026/04'), JSON.stringify(grown));
|
||||
// Nothing was scanning at that last click, so that one did load the page again —
|
||||
// which is the whole point of the guard. Everything below counts from here.
|
||||
seen = await docs();
|
||||
|
||||
// Now the reader's own way into the studio, and the noisiest one for this page: a
|
||||
// frame opened from a roll that is being read again. The rows are drawn from the
|
||||
// catalogue it already has, so this is the one route out of here that the browser
|
||||
// could take with no link anywhere in sight.
|
||||
const rows = () => page.$eval('[data-key="lib-node-SlowRoll"] .adm-album-count', (el) => Number(el.textContent));
|
||||
await page.evaluate(() => window.__forget());
|
||||
await page.click('[data-key="lib-node-SlowRoll"]', { button: 'right' });
|
||||
await page.waitForSelector('[data-key="lib-menu"]', { timeout: 10_000 });
|
||||
const menu = await page.$$eval('[data-key="lib-menu"] button', (els) => els.map((b) => b.dataset.key));
|
||||
const rescan = menu.find((k) => /^lib-(rescan|reconnect)-/.test(k));
|
||||
step('the roll can be read again from its own menu', !!rescan, JSON.stringify(menu));
|
||||
await page.click(`[data-key="${rescan}"]`);
|
||||
await page.waitForSelector('[data-key="lib-stop"]', { timeout: 15_000 });
|
||||
await sleep(DELAY);
|
||||
const early = await rows();
|
||||
await sleep(DELAY * 2);
|
||||
const later = await rows();
|
||||
step('the row counts the reading as it comes, not the catalogue standing still', later !== early, `${early} → ${later} of ${TOTAL}`);
|
||||
step('the strip is still the catalogue the roll left behind', (await tiles()) === TOTAL, `${await tiles()} tiles`);
|
||||
|
||||
await page.click('[data-key="lib-open"]');
|
||||
await page.waitForSelector('[data-key="nav-library"]', { timeout: 30_000 });
|
||||
step('opening a frame stays in the page, it does not reload it', await noReload(), `${await docs()} document load(s)`);
|
||||
// The studio takes the frame out of the address as soon as it has it, so the
|
||||
// route is what is left to say the handover arrived.
|
||||
step('and it lands in the studio', new URL(page.url()).pathname === '/app', page.url());
|
||||
|
||||
await page.click('[data-key="nav-library"]');
|
||||
await page.waitForSelector('[data-key="lib-frame"]', { timeout: 15_000 });
|
||||
step('and the scan it interrupted is still the scan in hand', await noReload(), `${await docs()} document load(s)`);
|
||||
const steady = await progressText(15_000);
|
||||
step('the catalogue is still reading the roll it was reading', !!steady, String(steady));
|
||||
|
||||
await browser.close();
|
||||
app.kill('SIGTERM');
|
||||
|
||||
Reference in New Issue
Block a user