Files
pj 67879fd6c3 Keep the hour it is now on the axis
The visible range is drawn from the events in view, and in the evening the
events are behind you: the bounds end at six, everything after that is in the
trailing strip, and the now line has nowhere to land. The app stops saying
where in the day you are, which is most of what it is open for. Same at seven
in the morning, from the other end.

So when today is one of the columns, the axis takes that one hour in, and
everything the widening reached over folds behind it. At half eleven at night
the fixture week reads 7am to 8pm as usual, then an "8pm to 11pm" strip, then
one 11pm row with the line in it. A row and a strip, not four rows of empty
evening. Only the hour itself is opened, so how far the clock has drifted past
the last event costs nothing.

The pin sits on top of the hysteresis rather than inside it. previous.current
still holds what the events and the user asked for, so an hour pinned open
tonight cannot accumulate into the bounds the grid remembers tomorrow.

One consequence, and it is the only place the axis is allowed to move under
you: page to a week that does not contain today and the row goes again, along
with the strip it was sitting under.

The tick that drives it is an hour long, not a minute, because that is how
often the answer changes. It lives in a new useClock alongside the minute tick
the now line already had, which moves there out of GridNowLine. Both schedule
off the clock rather than off an interval, so a machine that was asleep
catches up on the next tick instead of drifting further out every hour.

The browser suite has to say what time it is now. The shape of the axis
depends on the hour a run happens at, so anything measuring a row height or
counting strips pins the clock to the middle of the working day, the same way
it already pins the theme and the week start. The three tests that are about
the clock ask for half eleven at night, which the fixture leaves empty.
2026-08-13 22:15:08 +05:30

93 lines
3.9 KiB
TypeScript

// The smallest window the app can be put in. `src-tauri/tauri.conf.json` sets minWidth 880 and
// minHeight 560, so this is not a hypothetical size: it is the corner of the range the product
// ships with, and it is the one nobody looks at.
import { expect, test } from "@playwright/test";
import { MIDDAY, gridFit, openApp, settle } from "./app";
const MIN = { width: 880, height: 560 };
test.use({ viewport: MIN });
// FAILING in agenda view, and it is the app, not the test. `.titlebar` is a `1fr auto 1fr` grid
// and `.header-range` is a nowrap button with nothing to trim it, so the agenda's long range
// ("11 August to 9 September 2026") runs out of its column and the view switcher paints over it.
test("the header does not collide with itself in any view", async ({ page }) => {
await openApp(page);
for (const key of ["w", "d", "a"] as const) {
await page.keyboard.press(key);
await settle(page);
const measured = await page.evaluate(() => {
const range = document.querySelector(".header-range")?.getBoundingClientRect();
const views = document.querySelector(".view-switch")?.getBoundingClientRect();
const trail = document.querySelector(".trail")?.getBoundingClientRect();
if (!range || !views || !trail) throw new Error("the header is not on screen");
return {
rangeRight: range.right,
viewsLeft: views.left,
viewsRight: views.right,
trailLeft: trail.left,
};
});
// The date range is a nowrap button in a 1fr column, so when it outgrows its share it runs
// under the view switcher rather than being trimmed.
expect(measured.rangeRight, `date range overlaps the view switcher in ${key}`).toBeLessThanOrEqual(
measured.viewsLeft,
);
expect(measured.viewsRight).toBeLessThanOrEqual(measured.trailLeft);
}
});
test("the grid still fits, and still fills the window", async ({ page }) => {
await openApp(page);
const fit = await gridFit(page);
expect(fit.bodyBottom).toBeCloseTo(fit.windowHeight, 0);
expect(fit.canvasHeight).toBeCloseTo(fit.bodyHeight, 0);
expect(fit.overflow).toBe(false);
await expect(page.locator(".grid-head-day")).toHaveCount(7);
await expect(page.locator(".grid-allday")).toBeVisible();
});
test("no block is painted over by the one after it", async ({ page }) => {
// The row height is the subject here, so the axis has to be the one the events drew: an hour
// pinned open for the clock is fewer pixels for every block on it.
await openApp(page, { now: MIDDAY() });
// A block shorter than the minimum block height is drawn taller than its own time, which is
// right: it has to stay hittable. It must not be drawn *under* the next block, though, or the
// title it was given that height for is the thing that gets covered.
const covered = await page.evaluate(() =>
[...document.querySelectorAll<HTMLElement>(".grid-event")]
.map((el) => {
const title = el.querySelector<HTMLElement>(".grid-event-title");
if (!title) return null;
const rect = title.getBoundingClientRect();
const hit = document.elementFromPoint(
Math.round(rect.left + 4),
Math.round(rect.top + rect.height / 2),
);
return hit && el.contains(hit)
? null
: `${(title.textContent ?? "").trim()} (${Math.round(el.getBoundingClientRect().height)}px)`;
})
.filter((entry): entry is string => entry !== null),
);
expect(covered).toEqual([]);
});
test("the agenda is readable at the smallest size", async ({ page }) => {
await openApp(page, { view: "agenda" });
const rows = page.locator(".agenda-row");
expect(await rows.count()).toBeGreaterThan(5);
// Nothing spills sideways out of the window.
const spill = await page.evaluate(
() => document.documentElement.scrollWidth - document.documentElement.clientWidth,
);
expect(spill).toBeLessThanOrEqual(0);
});