web: walk a roll layer by layer, and let the strip drop the branch
This commit is contained in:
@@ -251,6 +251,22 @@ await page.waitForFunction(() => document.querySelectorAll('[data-key^="lib-node
|
||||
});
|
||||
step('the same click draws it open again', true, '3 rows');
|
||||
|
||||
// Show subfolders or not: the strip follows the open folder alone, or the whole
|
||||
// branch under it. The switch is the reader's and the tree does not change with
|
||||
// it — folding is about rows, this is about frames.
|
||||
await page.click('[data-key="lib-subs"]');
|
||||
await page.waitForFunction((sel) => document.querySelectorAll(sel).length === 1, thumbs, { timeout: 10_000, polling: 50 });
|
||||
const shallow = await page.$$eval('[data-key^="lib-node-"]', (els) => els.length);
|
||||
const shallowStage = await page.locator('[data-key="lib-stage-meta"]').innerText();
|
||||
step(
|
||||
'the switch leaves the open folder its own frames',
|
||||
shallow === 3 && /P1010256\.JPG/.test(shallowStage),
|
||||
`${shallow} rows, stage ${shallowStage.split('\n')[0]}`
|
||||
);
|
||||
await page.click('[data-key="lib-subs"]');
|
||||
await page.waitForFunction((sel) => document.querySelectorAll(sel).length === 3, thumbs, { timeout: 10_000, polling: 50 });
|
||||
step('and puts the branch back', true, '3 tiles from the roll below');
|
||||
|
||||
// Opening the subfolder redraws the strip and the frame above it.
|
||||
await page.click('[data-key="lib-node-CheckRoll/2026"]');
|
||||
await page.waitForFunction((sel) => document.querySelectorAll(sel).length === 2, thumbs, { timeout: 30_000 });
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
// The walk that reads a picked folder (src/engine/rollWalk.ts) is the part of a
|
||||
// scan that can run for minutes, and its order is what the reader sees: parents
|
||||
// before children, shallower layers before deeper ones, and the folder they just
|
||||
// clicked in front of everything. None of that can be watched in a browser, so
|
||||
// it is walked here over a stand-in tree.
|
||||
//
|
||||
// node scripts/roll-walk-check.mjs
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import ts from 'typescript';
|
||||
|
||||
const src = readFileSync(new URL('../src/engine/rollWalk.ts', import.meta.url), 'utf8');
|
||||
const js = ts.transpileModule(src, {
|
||||
compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
|
||||
}).outputText;
|
||||
const { DIRS_PER_PASS, jumpQueue, walkPass } = await import(`data:text/javascript,${encodeURIComponent(js)}`);
|
||||
|
||||
// A directory handle is only ever read for `values()`, so a stand-in tree needs
|
||||
// nothing else: `{ name: { … } }` for folders and `name: null` for a file.
|
||||
// `@eaDir` and `.thumbnails` are the folders a real roll drags along.
|
||||
const dir = (name, children) => ({
|
||||
kind: 'directory',
|
||||
name,
|
||||
values: async function* () {
|
||||
for (const [child, kids] of Object.entries(children)) {
|
||||
yield kids === null ? { kind: 'file', name: child } : dir(child, kids);
|
||||
}
|
||||
},
|
||||
});
|
||||
const keep = (name) => /\.(jpe?g|rw2)$/i.test(name);
|
||||
|
||||
const roll = dir('CheckRoll', {
|
||||
'.thumbnails': { hidden: null },
|
||||
'@eaDir': { junk: null },
|
||||
'P1.JPG': null,
|
||||
'2026': { 'P2.JPG': null, '04': { 'P3.RW2': null, 'notes.txt': null } },
|
||||
'2025': { 'P4.JPG': null },
|
||||
});
|
||||
|
||||
// Walks to the end, one click per pass, which is what the browser does between
|
||||
// passes: whatever the reader clicked last is what `jump` names.
|
||||
const walk = async (jumps = []) => {
|
||||
const pending = [{ dir: roll, rel: '' }];
|
||||
const out = [];
|
||||
let pass = 0;
|
||||
let walked = false;
|
||||
while (!walked) walked = await walkPass(pending, out, keep, () => jumps[pass++] ?? null);
|
||||
return { out, passes: pass };
|
||||
};
|
||||
|
||||
// Nothing clicked: the picked folder's own frames, then a layer down, then the
|
||||
// deepest — a folder is never walked before the folder that holds it.
|
||||
const { out, passes } = await walk();
|
||||
const rels = out.map((f) => f.rel);
|
||||
assert.deepEqual(rels, ['P1.JPG', '2025/P4.JPG', '2026/P2.JPG', '2026/04/P3.RW2'], 'layer by layer from the parent down');
|
||||
assert.ok(
|
||||
rels.every((rel, i) => i === 0 || rel.split('/').length >= rels[i - 1].split('/').length),
|
||||
'no deeper frame is read before a shallower one'
|
||||
);
|
||||
assert.ok(!rels.some((rel) => /\.thumbnails|@eaDir|notes\.txt/.test(rel)), 'hidden and unsupported names are skipped');
|
||||
assert.equal(passes, 4, 'one folder a pass — the thread goes back to the page between them');
|
||||
|
||||
// A click on one of the two folders a layer down while its layer is still to be
|
||||
// walked puts that folder first: the frames the reader is looking at are read
|
||||
// before the branch beside them.
|
||||
const clicked = await walk([null, '2026/']);
|
||||
assert.deepEqual(
|
||||
clicked.out.map((f) => f.rel),
|
||||
['P1.JPG', '2026/P2.JPG', '2025/P4.JPG', '2026/04/P3.RW2'],
|
||||
'the clicked folder jumps the queue'
|
||||
);
|
||||
|
||||
// And the same rule without a browser around it: a pass reads the queue in its
|
||||
// own order unless the click says otherwise, and a name the queue does not hold
|
||||
// changes nothing.
|
||||
const wide = dir('Wide', { a: { 'A.JPG': null }, b: { 'B.JPG': null } });
|
||||
const before = [];
|
||||
await walkPass([{ dir: wide, rel: '' }], before, keep, () => null);
|
||||
assert.deepEqual(before.map((f) => f.rel), ['a/A.JPG', 'b/B.JPG'], 'untouched, the folders are read in the order they were found');
|
||||
const after = [];
|
||||
await walkPass([{ dir: wide, rel: '' }], after, keep, () => 'b/');
|
||||
assert.deepEqual(
|
||||
after.map((f) => f.rel),
|
||||
['b/B.JPG', 'a/A.JPG'],
|
||||
'the folder the reader clicked is read first'
|
||||
);
|
||||
assert.equal(jumpQueue([{ rel: 'a/' }], 'nowhere/'), undefined, 'a folder already walked, or never seen, is left alone');
|
||||
|
||||
// A pass is bounded: a library wider than the budget is read over several passes,
|
||||
// and the rest waits in the queue rather than in a loop that never yields.
|
||||
const children = {};
|
||||
for (let i = 0; i < DIRS_PER_PASS + 5; i++) children[`d${String(i).padStart(3, '0')}`] = { 'x.jpg': null };
|
||||
const many = [{ dir: dir('Many', children), rel: '' }];
|
||||
const found = [];
|
||||
assert.equal(await walkPass(many, found, keep, () => null), false, 'the queue is not empty after one pass');
|
||||
assert.equal(found.length, DIRS_PER_PASS - 1, `one pass walked the picked folder and ${DIRS_PER_PASS - 1} below it`);
|
||||
assert.equal(many.length, 6, 'the rest is left in the queue, for the next pass');
|
||||
|
||||
console.log(`ok roll walk — ${rels.length} frames layer by layer, a click jumps the queue, ${DIRS_PER_PASS} folders a pass`);
|
||||
Reference in New Issue
Block a user