Grey letters ran down the bar while dragging a column edge. The editor marks the column under the pointer by putting a small div in each of its cells; drawn along the cell's right edge, that absolutely positioned child sent Chromium down another painting path and the cell's own text came out a second time, squeezed into the two pixels under the bar, one letter to a line. It appeared with the bar and went with it, which named the culprit -- measured, there was no text there at all, only three empty markers, and a forced repaint did not clear it. The marker is display:none now and only says which cells the column is made of; the line is an inset shadow on those cells, matched with :has(). Two suspects were tried and reverted first: the table as its own paint region, and separate borders. Neither changed anything, so neither stayed. Opening a note flashed a long narrow page. The drawing stage starts at the canonical page, 1600px tall, and the measurement that fits it to the pane ran as an ordinary effect -- a frame too late. useLayoutEffect runs between the DOM being built and the browser painting it, which is exactly the gap that flash lived in. The test samples the sheet's height every frame across a note switch, because one frame is all it was. The mark above a note did not stand in the same column as the tools below it. Vertically it was already exact -- measured, symbol centre 20, cap centre 20 -- but it began six pixels right of the first tool, and the title thirty. The mark's box is only as wide as the mark now, so the mark lands on that column, and the title follows nine pixels closer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
163 lines
7.1 KiB
JavaScript
163 lines
7.1 KiB
JavaScript
/* Tables, and what happens to one at the edge of a sheet.
|
||
*
|
||
* The writing surface is drawn as separate sheets with a gap between them. Text
|
||
* is pushed past that gap a block at a time, which is right for a paragraph and
|
||
* wrong for a table: moved whole, a table either jumps to the next sheet leaving
|
||
* the rest of this one blank, or — being longer than a sheet — has nowhere to
|
||
* jump to and lies straight across the gap with a row cut in half.
|
||
*
|
||
* So the claim under test is that a long table is broken *between its rows*, and
|
||
* that no row is left straddling a gap.
|
||
*/
|
||
import { OUT, check, createNote, createNotebook, launch, report, setTitle, withFreshUser } from "./lib.mjs";
|
||
|
||
/** Where each sheet sits, and therefore where the gaps are. */
|
||
const sheetBands = (page) =>
|
||
page.evaluate(() =>
|
||
[...document.querySelectorAll("[data-page-sheet]")].map((el) => {
|
||
const r = el.getBoundingClientRect();
|
||
return { top: r.top, bottom: r.bottom };
|
||
})
|
||
);
|
||
|
||
/** Every table row on screen, with the band it occupies. */
|
||
const rowBands = (page) =>
|
||
page.evaluate(() =>
|
||
[...document.querySelectorAll(".ProseMirror table tr:not(.ne-page-spacer-row)")].map((el) => {
|
||
const r = el.getBoundingClientRect();
|
||
return { top: r.top, bottom: r.bottom, text: el.textContent.trim().slice(0, 12) };
|
||
})
|
||
);
|
||
|
||
const browser = await launch();
|
||
let session;
|
||
try {
|
||
session = await withFreshUser(browser, "tables");
|
||
const p = session.page;
|
||
|
||
await createNotebook(p, "Tabellen");
|
||
await p.locator("button").filter({ hasText: "Tabellen" }).first().click();
|
||
await p.waitForTimeout(700);
|
||
await createNote(p);
|
||
await setTitle(p, "Lange Tabelle");
|
||
|
||
/* Enough text to push the table down the sheet, then a table long enough to
|
||
run past the bottom of it — the two cases that used to go wrong. */
|
||
await p.locator(".ProseMirror").click();
|
||
for (let i = 0; i < 12; i++) {
|
||
await p.keyboard.type(`Zeile ${i} vor der Tabelle. `);
|
||
await p.keyboard.press("Enter");
|
||
}
|
||
await p.waitForTimeout(500);
|
||
|
||
await p.locator("[data-table-menu] button").first().click();
|
||
await p.waitForTimeout(400);
|
||
await p.getByText("Insert 3 × 3 table", { exact: true }).click();
|
||
await p.waitForTimeout(1200);
|
||
check((await p.locator(".ProseMirror table").count()) === 1, "a table can be inserted");
|
||
|
||
/* Rows are added from the menu on the table itself, which is also the thing
|
||
Marc uses — and the labels there are English like the rest of the app. */
|
||
await p.locator(".ProseMirror table td").first().click();
|
||
await p.waitForTimeout(300);
|
||
for (let i = 0; i < 26; i++) {
|
||
await p.keyboard.type(`R${i}`);
|
||
await p.locator(".ProseMirror table").click({ button: "right" });
|
||
await p.waitForTimeout(220);
|
||
await p.getByText("Insert row below", { exact: true }).click();
|
||
await p.waitForTimeout(220);
|
||
}
|
||
await p.waitForTimeout(2500);
|
||
|
||
check(
|
||
(await p.locator("[data-submenu-toggle], [role=menu]").count()) >= 0 &&
|
||
!/Kopfzeile|Zellen verbinden|Zelle teilen/.test(await p.locator("body").innerText()),
|
||
"the table's own menu speaks the same language as the rest of the app"
|
||
);
|
||
|
||
const bands = await sheetBands(p);
|
||
check(bands.length >= 2, "the table runs past the first sheet", `${bands.length} sheets`);
|
||
|
||
const spacers = await p.locator("tr.ne-page-spacer-row").count();
|
||
check(spacers >= 1, "the table is broken between its rows rather than moved whole", String(spacers));
|
||
|
||
/* The point of it all: nothing is left lying across a gap. A row may end
|
||
exactly on a sheet's edge — that is not a straddle — so a pixel of slack. */
|
||
const rows = await rowBands(p);
|
||
const gaps = bands.slice(0, -1).map((b, i) => ({ from: b.bottom, to: bands[i + 1].top }));
|
||
const straddling = rows.filter((r) => gaps.some((g) => r.bottom > g.from + 1 && r.top < g.to - 1));
|
||
check(
|
||
straddling.length === 0,
|
||
"and no row is left lying across the gap between two sheets",
|
||
straddling.map((r) => r.text).join(", ")
|
||
);
|
||
|
||
await p.screenshot({ path: `${OUT}/tables-split.png`, fullPage: true });
|
||
|
||
/* The header has to be legible on the dark theme, where the fill it had was a
|
||
difference you could only find by looking for it. */
|
||
// The table was inserted with a header row, so there is one to measure — the
|
||
// menu entry toggles it *off*, which would leave nothing to look at.
|
||
const header = await p.locator(".ProseMirror table th").first().evaluate((el) => {
|
||
const cs = getComputedStyle(el);
|
||
const cell = getComputedStyle(el.closest("table").querySelector("td") || el);
|
||
return { head: cs.backgroundColor, body: cell.backgroundColor, rule: cs.borderBottomWidth };
|
||
});
|
||
check(
|
||
header.head !== header.body && parseFloat(header.rule) >= 2,
|
||
"a header row is filled differently from the body and underlined",
|
||
JSON.stringify(header)
|
||
);
|
||
|
||
/* Written, nothing is lost: the spacers are decorations, so the body the
|
||
server keeps has the table and no page furniture in it. */
|
||
await p.waitForTimeout(2000);
|
||
const body = await p.evaluate(async () => {
|
||
const auth = { Authorization: "Bearer " + localStorage.getItem("inknote_token") };
|
||
const notes = await (await fetch("/api/notes", { headers: auth })).json();
|
||
const mine = notes.find((n) => n.title === "Lange Tabelle");
|
||
return (await (await fetch(`/api/notes/${mine.id}`, { headers: auth })).json()).content;
|
||
});
|
||
check(!/ne-page-spacer/.test(body || ""), "and no page furniture reaches the stored note");
|
||
check(/table/.test(body || ""), "which does hold the table itself", String(body || "").slice(0, 60));
|
||
|
||
/* ── the edge being dragged is drawn on the cell, not in it ──
|
||
* The editor marks the column under the pointer with a small div in each of
|
||
* its cells. Positioned along the cell's right edge it painted the cell's own
|
||
* text a second time, squeezed into the two pixels under the bar, one letter
|
||
* to a line — an absolutely positioned child inside a collapsed-border table
|
||
* cell sends Chromium down another painting path. The div is never drawn now;
|
||
* it only says which cells the column is made of. */
|
||
const cell = await p.locator(".ProseMirror table th").first().boundingBox();
|
||
await p.mouse.move(cell.x + cell.width - 1, cell.y + cell.height / 2);
|
||
await p.waitForTimeout(500);
|
||
const edge = await p.evaluate(() => {
|
||
const marks = [...document.querySelectorAll(".column-resize-handle")];
|
||
const host = marks[0]?.parentElement;
|
||
return {
|
||
marks: marks.length,
|
||
drawn: marks.map((m) => getComputedStyle(m).display),
|
||
onCell: host ? getComputedStyle(host).boxShadow : "",
|
||
painted: marks.every((m) => m.getBoundingClientRect().width === 0),
|
||
};
|
||
});
|
||
check(edge.marks > 0, "holding over a column edge marks that column", JSON.stringify(edge.marks));
|
||
check(
|
||
edge.drawn.every((d) => d === "none") && edge.painted,
|
||
"the marker itself is never painted",
|
||
JSON.stringify(edge.drawn)
|
||
);
|
||
check(
|
||
/inset/.test(edge.onCell) && edge.onCell !== "none",
|
||
"and the line is drawn on the cell that carries it",
|
||
edge.onCell
|
||
);
|
||
} catch (e) {
|
||
check(false, "suite ran to the end", e.message);
|
||
} finally {
|
||
await session?.cleanup();
|
||
const code = report();
|
||
await browser.close();
|
||
process.exit(code);
|
||
}
|