feat(library): the all-thumbs wall is cut into the days its frames were shot
This commit is contained in:
@@ -31,8 +31,20 @@ const SAMPLE_PORT = Number(process.env.SAMPLE_PORT ?? 5034);
|
||||
const FRAMES = Number(process.env.FRAMES ?? 300);
|
||||
const FOLDER = 'WallRoll';
|
||||
// Every frame is the same bytes under a different name: what is being checked is
|
||||
// how many cards the wall will draw, not what is on them.
|
||||
// how many cards the wall will draw, not what is on them. The bytes carry no
|
||||
// EXIF, so a frame's date is its own `lastModified` — spread over the days below,
|
||||
// because a wall of one day is a wall with one header in it, and the headers are
|
||||
// half of what this check is for.
|
||||
const names = Array.from({ length: FRAMES }, (_, i) => `frame-${String(i + 1).padStart(4, '0')}.jpg`);
|
||||
const DAY_MS = 86400000;
|
||||
const PER_DAY = Number(process.env.PER_DAY ?? 10);
|
||||
const DAY0 = Date.UTC(2026, 0, 1);
|
||||
// The day a frame is filed under, off its own name — the same rule on both sides
|
||||
// of the wire, so the check reads the wall the way the wall was told to sort it.
|
||||
const dayOfName = (name) => {
|
||||
const n = Number((name.match(/\d+/) ?? ['1'])[0]);
|
||||
return DAY0 + Math.floor(Math.max(0, n - 1) / PER_DAY) * DAY_MS;
|
||||
};
|
||||
|
||||
let failures = 0;
|
||||
function step(name, ok, detail = '') {
|
||||
@@ -125,14 +137,17 @@ await context.route('**/api/**', (route) => {
|
||||
// they are put back where the catalogue reads them, because a cloned handle is a
|
||||
// plain object with no `getFile`.
|
||||
await context.addInitScript(
|
||||
({ origin, names, folder }) => {
|
||||
({ origin, names, folder, days }) => {
|
||||
// `days` is the day of each frame, index-aligned with `names`: the init
|
||||
// script is cloned into the page and cannot close over the check's own scope.
|
||||
const dayOfNameIn = (name) => days[names.indexOf(name)] ?? Date.UTC(2026, 0, 1);
|
||||
const fileHandle = (name) => {
|
||||
const handle = { kind: 'file', name, __fake: true };
|
||||
Object.defineProperty(handle, 'getFile', {
|
||||
value: async () =>
|
||||
new File([await (await fetch(`${origin}/${name}`)).arrayBuffer()], name, {
|
||||
type: 'image/jpeg',
|
||||
lastModified: Date.UTC(2026, 0, 1),
|
||||
lastModified: dayOfNameIn(name),
|
||||
}),
|
||||
});
|
||||
return handle;
|
||||
@@ -155,6 +170,9 @@ await context.addInitScript(
|
||||
};
|
||||
const root = () => dirHandle(folder, { files: names });
|
||||
window.showDirectoryPicker = async () => root();
|
||||
// The day each frame was given, for the header audit below: the page's own
|
||||
// clock reads the label, so the day is read back here the same way.
|
||||
window.__dayOf = (name) => dayOfNameIn(name);
|
||||
|
||||
const revive = (row) => {
|
||||
if (row?.handle?.__fake && !Object.getOwnPropertyDescriptor(row.handle, 'getFile')) {
|
||||
@@ -178,7 +196,7 @@ await context.addInitScript(
|
||||
};
|
||||
}
|
||||
},
|
||||
{ origin: `http://127.0.0.1:${SAMPLE_PORT}`, names, folder: FOLDER }
|
||||
{ origin: `http://127.0.0.1:${SAMPLE_PORT}`, names, folder: FOLDER, days: names.map(dayOfName) }
|
||||
);
|
||||
|
||||
const page = await context.newPage();
|
||||
@@ -208,6 +226,33 @@ await page
|
||||
const counted = await page.$eval('[data-key="lib-count"]', (e) => e.textContent?.trim() ?? '');
|
||||
step('the roll is read whole', counted.includes(String(FRAMES)), counted);
|
||||
|
||||
// The day the header of a card stands under, read off the sections themselves:
|
||||
// every header must name the day of every card filed under it. A day whose first
|
||||
// rows are above the window still gets its header where the eye lands (see
|
||||
// `dayRuns`), so the rule is "a header names its own cards" and not "one header
|
||||
// per day in the shelf".
|
||||
const dayAudit = () =>
|
||||
page.evaluate(() => {
|
||||
const key = (ts) => {
|
||||
const d = new Date(ts);
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||
};
|
||||
const out = { sections: 0, cards: 0, labelled: 0, bad: [] };
|
||||
for (const section of document.querySelectorAll('.adm-day')) {
|
||||
out.sections++;
|
||||
const own = (section.getAttribute('data-key') ?? '').replace('lib-day-', '');
|
||||
const head = section.querySelector('.adm-day-head')?.textContent?.trim() ?? '';
|
||||
if (head) out.labelled++;
|
||||
const names = [...section.querySelectorAll('[data-key^="lib-photo-"]')].map(
|
||||
(el) => (el.getAttribute('data-key') ?? '').split('/').pop()
|
||||
);
|
||||
const days = new Set(names.map((name) => key(window.__dayOf(name))));
|
||||
if (!names.length || days.size !== 1 || !days.has(own)) out.bad.push({ own, head, days: [...days], names: names.slice(0, 2) });
|
||||
}
|
||||
out.cards = document.querySelectorAll('.adm-card').length;
|
||||
return out;
|
||||
});
|
||||
|
||||
await page.click('[data-key="lib-view-grid"]');
|
||||
await page.waitForSelector('.adm-grid', { timeout: 15_000 });
|
||||
await page.waitForFunction(() => document.querySelectorAll('.adm-card').length > 0, null, { timeout: 30_000 });
|
||||
@@ -216,6 +261,22 @@ const wall = await read();
|
||||
step('the wall draws a screenful of the shelf', wall.cards > 0 && wall.cards < FRAMES, `${wall.cards} of ${FRAMES} cards drawn`);
|
||||
step('and stands in for the rows it is not drawing', wall.wall, 'the shelf scrolls');
|
||||
|
||||
const days1 = await dayAudit();
|
||||
step('the wall is cut into the days its frames were shot', days1.sections > 1, `${days1.sections} headers over ${days1.cards} cards`);
|
||||
step('every header names the day of the cards under it', days1.bad.length === 0, JSON.stringify(days1.bad.slice(0, 2)));
|
||||
step('and every header carries its day in words', days1.labelled === days1.sections, `${days1.labelled} of ${days1.sections}`);
|
||||
|
||||
// The same wall, scrolled to where no day starts at the fold: the headers have to
|
||||
// move with it, which is the whole risk of cutting a virtualised wall into days.
|
||||
await page.evaluate(() => {
|
||||
const el = document.querySelector('.adm-grid');
|
||||
el.scrollTop = Math.round(el.scrollHeight * 0.7);
|
||||
el.dispatchEvent(new Event('scroll'));
|
||||
});
|
||||
await page.waitForTimeout(1500);
|
||||
const days2 = await dayAudit();
|
||||
step('a scrolled wall still files every card under its own day', days2.sections > 0 && days2.bad.length === 0, JSON.stringify(days2.bad.slice(0, 2)));
|
||||
|
||||
// The class of trigger, made to happen: one input the wall reads back off itself,
|
||||
// moving on every pass.
|
||||
await page.evaluate(() => {
|
||||
|
||||
Reference in New Issue
Block a user