fix(library): measure the wall's shelf in two steps so the scroll reaches its end
This commit is contained in:
@@ -37,14 +37,36 @@ const FOLDER = 'WallRoll';
|
||||
// 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 days the roll was shot on, and they are not equal: a roll is an afternoon
|
||||
// of three frames and a wedding of four hundred, and the wall's own model of its
|
||||
// rows is the thing that has to survive that. A shelf of equal days is a shelf a
|
||||
// flat count is right about — `count / cols` rows of `cols` cards, with the step
|
||||
// between them read off the run in hand standing for the rest — and a check of
|
||||
// equal days says nothing about the shelf that is not. And the order matters as
|
||||
// much as the sizes: the head of the roll is what a step read off the drawn rows
|
||||
// comes from, so the roll is sixty days of two under four days of forty — and the
|
||||
// head of it is a screen of bands with a frame or two under each, which is what
|
||||
// puts a step taken as one average, over a band's row and a row of frames both,
|
||||
// furthest out of true. (One small day among runs of a hundred and twenty was
|
||||
// tried and does not show it; a first cut of this check missed the shape and had
|
||||
// to be walked straight instead of jumped.)
|
||||
const SMALL_DAY = 2;
|
||||
const BIG_DAY = 40;
|
||||
const DAY_PATTERN = [...Array(60).fill(SMALL_DAY), ...Array(4).fill(BIG_DAY)];
|
||||
// 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;
|
||||
const dayIndex = (n) => {
|
||||
let left = Math.max(0, n);
|
||||
for (let cycle = 0; cycle < 1000; cycle++) {
|
||||
for (let i = 0; i < DAY_PATTERN.length; i++) {
|
||||
if (left < DAY_PATTERN[i]) return cycle * DAY_PATTERN.length + i;
|
||||
left -= DAY_PATTERN[i];
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
const dayOfName = (name) => DAY0 + dayIndex(Number((name.match(/\d+/) ?? ['1'])[0]) - 1) * DAY_MS;
|
||||
|
||||
let failures = 0;
|
||||
function step(name, ok, detail = '') {
|
||||
@@ -262,20 +284,80 @@ step('the wall draws a screenful of the shelf', wall.cards > 0 && wall.cards < F
|
||||
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`);
|
||||
// The first screenful is one day of a mixed roll often as not — a day big enough
|
||||
// to fill it is a day at all — so what is read here is that a band stands over
|
||||
// the cards and names its day, and the cut into several days is read below, on a
|
||||
// wall scrolled off the fold.
|
||||
step('the wall draws its days over the cards', days1.sections >= 1 && days1.labelled === days1.sections, `${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(() => {
|
||||
// Read down the shelf rather than at one stop: a stop of any one fraction lands
|
||||
// inside a day big enough to fill the screen as often as it lands on a fold, so
|
||||
// what is read is that no band ever files a card under another day's header, and
|
||||
// that the shelf's own cuts move through the window as it travels.
|
||||
const stops = [0.15, 0.35, 0.55, 0.75, 0.95];
|
||||
const seenDays = new Set();
|
||||
let scrolledBad = [];
|
||||
for (const at of stops) {
|
||||
await page.evaluate((f) => {
|
||||
const el = document.querySelector('.adm-grid');
|
||||
el.scrollTop = Math.round((el.scrollHeight - el.clientHeight) * f);
|
||||
el.dispatchEvent(new Event('scroll'));
|
||||
}, at);
|
||||
await page.waitForTimeout(900);
|
||||
const audit = await dayAudit();
|
||||
for (const section of await page.$$eval('.adm-day', (els) => els.map((e) => e.getAttribute('data-key')))) seenDays.add(section);
|
||||
scrolledBad = scrolledBad.concat(audit.bad);
|
||||
}
|
||||
step(
|
||||
'a scrolled wall still files every card under its own day',
|
||||
scrolledBad.length === 0 && seenDays.size > 1,
|
||||
`${seenDays.size} days through the window, ${JSON.stringify(scrolledBad.slice(0, 2))}`
|
||||
);
|
||||
|
||||
// The shelf walked to its own end, a screen at a time — a jump straight to the
|
||||
// bottom is a different question, and the one this check exists for: the wall
|
||||
// stands in for the frames it is not drawing, and a spacer standing in for more
|
||||
// rows than those frames take leaves the reader at the end of the scroll with a
|
||||
// screen of nothing under the last card drawn and the rest of the roll never
|
||||
// drawn at all. What the walk is read for is that it *gets* there, standing on
|
||||
// the last frame, and that every frame of the roll was drawn on the way.
|
||||
const walked = await page.evaluate(async () => {
|
||||
const el = document.querySelector('.adm-grid');
|
||||
el.scrollTop = Math.round(el.scrollHeight * 0.7);
|
||||
el.dispatchEvent(new Event('scroll'));
|
||||
const seen = new Set();
|
||||
const collect = () => {
|
||||
for (const card of el.querySelectorAll('.adm-card')) seen.add(card.getAttribute('data-key'));
|
||||
};
|
||||
const breathe = () => new Promise((r) => setTimeout(r, 150));
|
||||
el.scrollTop = 0;
|
||||
await breathe();
|
||||
let guard = 0;
|
||||
while (guard++ < 400) {
|
||||
collect();
|
||||
if (el.scrollTop + el.clientHeight >= el.scrollHeight - 2) break;
|
||||
el.scrollTop = el.scrollTop + Math.round(el.clientHeight * 0.8);
|
||||
await breathe();
|
||||
}
|
||||
collect();
|
||||
const cards = [...el.querySelectorAll('.adm-card')];
|
||||
const last = cards[cards.length - 1];
|
||||
const box = el.getBoundingClientRect();
|
||||
return {
|
||||
top: Math.round(el.scrollTop),
|
||||
reached: el.scrollTop + el.clientHeight >= el.scrollHeight - 2,
|
||||
room: Math.round(el.scrollHeight - el.clientHeight),
|
||||
// What is left under the last card drawn: the spacer that has nothing left
|
||||
// to stand in for. A few pixels is the grid's own gap.
|
||||
gap: last ? Math.round(el.scrollHeight - (last.getBoundingClientRect().bottom - box.top + el.scrollTop)) : null,
|
||||
seen: [...seen],
|
||||
};
|
||||
});
|
||||
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)));
|
||||
step('the shelf is walked to its own end', walked.reached, `stopped at ${walked.top} of ${walked.room}`);
|
||||
step('and stands on nothing there', walked.gap !== null && walked.gap <= 16, `${walked.gap}px of nothing under the last card`);
|
||||
const missed = names.filter((name) => !walked.seen.includes(`lib-photo-${FOLDER}/${name}`));
|
||||
step('every frame of the roll is drawn on the way', missed.length === 0, `${walked.seen.length} of ${names.length} drawn, first missed ${missed[0] ?? 'none'}`);
|
||||
|
||||
// The class of trigger, made to happen: one input the wall reads back off itself,
|
||||
// moving on every pass.
|
||||
|
||||
Reference in New Issue
Block a user