Move the browser suites into the repository
They lived in a scratch directory under /tmp, which was cleared overnight; four of the five were simply gone. Whatever they were worth, they were worth nothing kept there. Rewritten against a shared lib.mjs. The old ones registered their users through the sign-up form, which no longer exists — each suite now creates a throwaway account through the admin API and deletes it afterwards, so a run leaves the database as it found it. The previous generation left nine accounts behind; those are gone too. Three test hooks went into the app, each of which earns its place beyond testing: aria-label on the note title (the sidebar search box is also a text input, and a screen reader could not tell them apart either), data-note-row on list rows, data-user on admin rows. Two suites were flaky in ways worth naming, because both were the test misreading the app rather than the app misbehaving: - ticking a task then inserting a note link put the link inside the task list, since Control+End lands there. Verified against the server that the tick itself persists correctly; the suite now leaves the list first and asserts where the link ended up. - the note-title lookup matched the sidebar's Trash entry, which wears the same emoji as a note row. 56 checks across five suites, all passing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
41ba99c017
commit
311e992496
13 files changed
+915
No files matched your search
+140
@@ -0,0 +1,140 @@
|
||||
/* Drawings must look the same on every screen.
|
||||
*
|
||||
* Strokes are stored as vectors in a fixed 1200x1600 page space, so the same
|
||||
* note rendered at different viewport widths should differ only by rasterising
|
||||
* error. Each rendering is reduced to a coarse ink grid and compared cell by
|
||||
* cell, which ignores antialiasing but catches a stroke that moved or vanished.
|
||||
*
|
||||
* The second thing checked here is the bug that started all of this: the 6-second
|
||||
* poller adopting a server response that was issued before the local save, which
|
||||
* made strokes disappear again seconds after being drawn. */
|
||||
import {
|
||||
OUT,
|
||||
check,
|
||||
createNote,
|
||||
createNotebook,
|
||||
launch,
|
||||
reopenNote,
|
||||
report,
|
||||
setTitle,
|
||||
withFreshUser,
|
||||
} from "./lib.mjs";
|
||||
|
||||
const GRID_W = 48,
|
||||
GRID_H = 64;
|
||||
|
||||
/** Rasterises the drawing surface into a GRID_W x GRID_H map of "has ink". */
|
||||
const inkGrid = (page) =>
|
||||
page.evaluate(
|
||||
([gw, gh]) => {
|
||||
const c = document.querySelector("canvas");
|
||||
if (!c) return null;
|
||||
const off = document.createElement("canvas");
|
||||
off.width = gw;
|
||||
off.height = gh;
|
||||
const ctx = off.getContext("2d", { willReadFrequently: true });
|
||||
ctx.drawImage(c, 0, 0, gw, gh);
|
||||
const d = ctx.getImageData(0, 0, gw, gh).data;
|
||||
const out = [];
|
||||
for (let i = 0; i < gw * gh; i++) out.push(d[i * 4 + 3] > 40 ? 1 : 0);
|
||||
return out;
|
||||
},
|
||||
[GRID_W, GRID_H]
|
||||
);
|
||||
|
||||
const diff = (a, b) => a.reduce((n, v, i) => n + (v === b[i] ? 0 : 1), 0);
|
||||
const ink = (g) => g.reduce((n, v) => n + v, 0);
|
||||
|
||||
async function draw(page) {
|
||||
await page.locator("button").filter({ hasText: "Draw" }).first().click();
|
||||
await page.waitForTimeout(1200);
|
||||
const box = await page.locator("canvas").first().boundingBox();
|
||||
// Three strokes in different parts of the page, so a shifted origin shows up.
|
||||
const strokes = [
|
||||
[0.2, 0.2, 0.7, 0.35],
|
||||
[0.25, 0.55, 0.75, 0.5],
|
||||
[0.5, 0.7, 0.55, 0.85],
|
||||
];
|
||||
for (const [x1, y1, x2, y2] of strokes) {
|
||||
await page.mouse.move(box.x + box.width * x1, box.y + box.height * y1);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(box.x + box.width * x2, box.y + box.height * y2, { steps: 14 });
|
||||
await page.mouse.up();
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
await page.waitForTimeout(2500);
|
||||
return box;
|
||||
}
|
||||
|
||||
const browser = await launch();
|
||||
let session;
|
||||
try {
|
||||
session = await withFreshUser(browser, "draw");
|
||||
const p = session.page;
|
||||
await createNotebook(p, "Zeichen Buch");
|
||||
await createNote(p);
|
||||
await setTitle(p, "Zeichnung");
|
||||
|
||||
await draw(p);
|
||||
const reference = await inkGrid(p);
|
||||
check(reference !== null && ink(reference) > 0, "strokes were drawn", `${ink(reference || [])} ink cells`);
|
||||
await p.screenshot({ path: `${OUT}/drawing-1400.png` });
|
||||
|
||||
/* ── the poller must not undo what was just drawn ── */
|
||||
await p.waitForTimeout(14000);
|
||||
const afterPolling = await inkGrid(p);
|
||||
check(diff(reference, afterPolling) === 0, "strokes survive 14s of background polling", `${diff(reference, afterPolling)} cells changed`);
|
||||
|
||||
/* ── same note, other screen widths ── */
|
||||
const noteId = await p.evaluate(async () => {
|
||||
const r = await fetch("/api/notes", {
|
||||
headers: { Authorization: "Bearer " + localStorage.getItem("inknote_token") },
|
||||
});
|
||||
return (await r.json())[0].id;
|
||||
});
|
||||
|
||||
for (const width of [1000, 700, 420]) {
|
||||
await p.setViewportSize({ width, height: 900 });
|
||||
await p.waitForTimeout(2500);
|
||||
if (width < 768) {
|
||||
// Shrinking past the breakpoint leaves the sidebar open as a full-screen
|
||||
// overlay, and its backdrop swallows every click. Dismiss it by clicking
|
||||
// to the right of the panel, then reopen the note from the list.
|
||||
await p.mouse.click(width - 12, 450);
|
||||
await p.waitForTimeout(800);
|
||||
const row = p.locator(`[data-note-row="${noteId}"]`);
|
||||
if (await row.count()) {
|
||||
await row.click();
|
||||
await p.waitForTimeout(2500);
|
||||
}
|
||||
}
|
||||
const g = await inkGrid(p);
|
||||
if (!g) {
|
||||
check(false, `drawing visible at ${width}px`);
|
||||
continue;
|
||||
}
|
||||
const d = diff(reference, g);
|
||||
const pct = ((d / (GRID_W * GRID_H)) * 100).toFixed(1);
|
||||
check(d / (GRID_W * GRID_H) < 0.02, `drawing matches at ${width}px`, `${d} cells, ${pct}%`);
|
||||
await p.screenshot({ path: `${OUT}/drawing-${width}.png` });
|
||||
}
|
||||
|
||||
/* ── and after a reload ── */
|
||||
// A reload lands on the dashboard, which has no note list — reopen by title.
|
||||
await p.setViewportSize({ width: 1400, height: 950 });
|
||||
await reopenNote(p, "Zeichnung");
|
||||
await p.waitForTimeout(2000);
|
||||
const afterReload = await inkGrid(p);
|
||||
check(afterReload !== null && ink(afterReload) > 0, "the drawing is still there after a reload");
|
||||
if (afterReload) {
|
||||
const d = diff(reference, afterReload);
|
||||
check(d / (GRID_W * GRID_H) < 0.02, "the reloaded drawing matches the original", `${d} cells`);
|
||||
}
|
||||
} catch (e) {
|
||||
check(false, "suite ran to the end", e.message);
|
||||
} finally {
|
||||
await session?.cleanup();
|
||||
const code = report();
|
||||
await browser.close();
|
||||
process.exit(code);
|
||||
}
|
||||
Reference in new issue
Block a user