The text is laid out on a page of one fixed size and the sheet scaled to the screen, so a phone breaks it exactly where a desktop does — twenty-one lines to the page on both. The type size is computed from the page and the line count rather than chosen. A PDF's full screen is the browser window, not the screen, and its button sits beside the zoom. "Recent Notes" is stamped by every write, not only by a rename. And a PDF that will not open says which of the four reasons it is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
535 lines
23 KiB
JavaScript
535 lines
23 KiB
JavaScript
/* Editor: tables, task lists, links between notes, and whether any of it
|
|
* survives a reload — the place where a stale server response used to eat
|
|
* freshly typed content. */
|
|
import {
|
|
OUT,
|
|
api,
|
|
check,
|
|
createNote,
|
|
createNotebook,
|
|
insertTable,
|
|
launch,
|
|
reopenNote,
|
|
report,
|
|
setTitle,
|
|
typeInEditor,
|
|
withFreshUser,
|
|
} from "./lib.mjs";
|
|
|
|
const browser = await launch();
|
|
let session;
|
|
try {
|
|
session = await withFreshUser(browser, "editor");
|
|
const p = session.page;
|
|
|
|
await createNotebook(p, "Editor Book");
|
|
await createNote(p);
|
|
await typeInEditor(p, "Grundtext");
|
|
|
|
/* ── tables ── */
|
|
await insertTable(p);
|
|
check((await p.locator(".ProseMirror table").count()) > 0, "table inserted");
|
|
check((await p.locator(".ProseMirror table th").count()) >= 3, "table has a header row");
|
|
|
|
/* ── task list ── */
|
|
await p.locator(".ProseMirror").click();
|
|
await p.keyboard.press("Control+End");
|
|
await p.keyboard.press("Enter");
|
|
await p.getByTitle("Task list").click();
|
|
await p.waitForTimeout(400);
|
|
await p.keyboard.type("Milch kaufen");
|
|
await p.waitForTimeout(1200);
|
|
check((await p.locator('.ProseMirror input[type="checkbox"]').count()) > 0, "task list inserted");
|
|
await p.locator('.ProseMirror input[type="checkbox"]').first().click();
|
|
await p.waitForTimeout(1500);
|
|
|
|
await p.screenshot({ path: `${OUT}/editor-content.png` });
|
|
|
|
/* ── right-click menu on the table ── */
|
|
const cellCount = () => p.locator(".ProseMirror table td, .ProseMirror table th").count();
|
|
const before = await cellCount();
|
|
await p.locator(".ProseMirror table td").first().click({ button: "right" });
|
|
await p.waitForTimeout(600);
|
|
check((await p.getByText("Insert row below").count()) > 0, "right-click in a table opens the table menu");
|
|
check((await p.getByText("Delete table").count()) > 0, "and offers deleting the table");
|
|
await p.screenshot({ path: `${OUT}/editor-table-menu.png` });
|
|
|
|
await p.getByText("Insert row below").click();
|
|
await p.waitForTimeout(1000);
|
|
check((await cellCount()) > before, "the menu really added a row", `${before} → ${await cellCount()}`);
|
|
|
|
const withRow = await cellCount();
|
|
await p.locator(".ProseMirror table td").first().click({ button: "right" });
|
|
await p.waitForTimeout(600);
|
|
await p.getByText("Delete row").click();
|
|
await p.waitForTimeout(1000);
|
|
check((await cellCount()) < withRow, "and removing one works too", `${withRow} → ${await cellCount()}`);
|
|
|
|
// Outside a table the browser's own menu must stay available.
|
|
await p.locator(".ProseMirror p").first().click({ button: "right" });
|
|
await p.waitForTimeout(500);
|
|
check(
|
|
(await p.getByText("Insert row below").count()) === 0,
|
|
"right-click outside a table shows no table menu"
|
|
);
|
|
await p.keyboard.press("Escape");
|
|
await p.waitForTimeout(400);
|
|
|
|
/* ── a second note to link to ── */
|
|
await setTitle(p, "Quelle");
|
|
await createNote(p);
|
|
await setTitle(p, "Ziel");
|
|
await typeInEditor(p, "Ich bin das Ziel.");
|
|
|
|
await p.locator("button").filter({ hasText: "Quelle" }).first().click();
|
|
await p.waitForSelector(".ProseMirror", { timeout: 15000 });
|
|
await p.waitForTimeout(1200);
|
|
|
|
/* ── note link via the @ menu ── */
|
|
// Control+End lands inside the task list, where a plain Enter would only make
|
|
// another task. A second Enter on the empty item leaves the list, so the link
|
|
// goes into a paragraph of its own and the checked task above stays untouched.
|
|
await p.locator(".ProseMirror").click();
|
|
await p.keyboard.press("Control+End");
|
|
await p.keyboard.press("Enter");
|
|
await p.keyboard.press("Enter");
|
|
await p.waitForTimeout(300);
|
|
await p.keyboard.type("@Ziel");
|
|
await p.waitForTimeout(1400);
|
|
const menuOpen = (await p.locator("text=Ziel").count()) > 0;
|
|
check(menuOpen, "@ opens the link menu");
|
|
await p.keyboard.press("Enter");
|
|
await p.waitForTimeout(1800);
|
|
|
|
const linkCount = await p.locator('.ProseMirror [data-note-link], .ProseMirror .note-link').count();
|
|
check(linkCount > 0, "note link inserted into the document", `${linkCount} found`);
|
|
await p.screenshot({ path: `${OUT}/editor-link.png` });
|
|
|
|
/* ── the server learned about it ── */
|
|
await p.waitForTimeout(1500);
|
|
await p.locator("button").filter({ hasText: "Ziel" }).first().click();
|
|
await p.waitForSelector(".ProseMirror", { timeout: 15000 });
|
|
await p.waitForTimeout(2000);
|
|
const backlink = await p.locator("text=Quelle").count();
|
|
check(backlink > 0, "the target shows a backlink to the source");
|
|
|
|
/* ── everything survives a cold reload ── */
|
|
await reopenNote(p, "Quelle");
|
|
check((await p.locator(".ProseMirror table").count()) > 0, "table survived a reload");
|
|
check(
|
|
(await p.locator('.ProseMirror [data-note-link], .ProseMirror .note-link').count()) > 0,
|
|
"note link survived a reload"
|
|
);
|
|
check((await p.locator('.ProseMirror input[type="checkbox"]').count()) > 0, "task list survived a reload");
|
|
check(
|
|
(await p.locator('.ProseMirror input[type="checkbox"]:checked').count()) === 1,
|
|
"the ticked task stayed ticked, and only that one"
|
|
);
|
|
check(
|
|
(await p.locator(".ProseMirror li").filter({ hasText: "Ziel" }).count()) === 0,
|
|
"the note link went into a paragraph, not into the task list"
|
|
);
|
|
check((await p.locator(".ProseMirror").innerText()).includes("Grundtext"), "plain text survived a reload");
|
|
await p.screenshot({ path: `${OUT}/editor-after-reload.png` });
|
|
|
|
/* ── the sheet is the right size from the first frame ──
|
|
* It was not. The stage starts at the canonical page, 1600px tall, and the
|
|
* measurement that pulls it back to the pane ran an ordinary effect — a frame
|
|
* too late. Every note opened by flashing a long narrow sheet and then
|
|
* righting itself. Sampled every frame, because one frame is all it was. */
|
|
const settled = await p.locator("[data-page-sheet]").first().evaluate((el) => el.getBoundingClientRect().height);
|
|
await p.evaluate(() => {
|
|
window.__tall = 0;
|
|
const tick = () => {
|
|
const el = document.querySelector("[data-page-sheet]");
|
|
if (el) window.__tall = Math.max(window.__tall, el.getBoundingClientRect().height);
|
|
window.__raf = requestAnimationFrame(tick);
|
|
};
|
|
tick();
|
|
});
|
|
await p.locator("button").filter({ hasText: "Ziel" }).first().click();
|
|
await p.waitForSelector(".ProseMirror", { timeout: 15000 });
|
|
await p.waitForTimeout(1500);
|
|
const tallest = await p.evaluate(() => {
|
|
cancelAnimationFrame(window.__raf);
|
|
return window.__tall;
|
|
});
|
|
check(
|
|
tallest <= settled * 1.15,
|
|
"switching notes never draws the sheet at the wrong size, not even for a frame",
|
|
`settled ${Math.round(settled)}, tallest ${Math.round(tallest)}`
|
|
);
|
|
await reopenNote(p, "Quelle");
|
|
/* ── how many pages the writing fills ── */
|
|
// Pages are a reading aid, not a change to the document: the note stays one
|
|
// body, and Markdown has no page. The height of one is the same 3:4 the canvas
|
|
// uses, so "page 2" means the same thing writing or drawing.
|
|
await createNote(p);
|
|
await setTitle(p, "Langer Text");
|
|
check(
|
|
(await p.locator("[data-note-position]").innerText()).includes("of 1"),
|
|
"a fresh note is one page",
|
|
await p.locator("[data-note-position]").innerText()
|
|
);
|
|
|
|
await p.locator(".ProseMirror").click();
|
|
/* Enough for several pages. With only two, the middle of the window never
|
|
leaves the first one — a window is shorter than a page — so the check would
|
|
be testing arithmetic rather than behaviour.
|
|
Pasted rather than typed: at the speed a robot types, a long note loses
|
|
keystrokes between the editor and the pagination measuring after each one,
|
|
and the page count then measures typing throughput instead of pagination.
|
|
(At any human speed nothing is lost — checked at 50 characters a second.) */
|
|
await p.keyboard.type("Absatz 1: Dichtungen altern durch Wärme.");
|
|
await p.keyboard.press("Enter");
|
|
await p.evaluate((text) => {
|
|
const dt = new DataTransfer();
|
|
dt.setData("text/plain", text);
|
|
document
|
|
.querySelector(".ProseMirror")
|
|
.dispatchEvent(new ClipboardEvent("paste", { clipboardData: dt, bubbles: true, cancelable: true }));
|
|
}, Array.from({ length: 200 }, (_, i) => `Absatz ${i + 2}: Dichtungen altern durch Wärme.`).join("\n"));
|
|
await p.waitForTimeout(3500);
|
|
|
|
const position = () => p.locator("[data-note-position]").innerText();
|
|
const scrollTo = async (fraction) => {
|
|
await p.evaluate((f) => {
|
|
const pm = document.querySelector(".ProseMirror");
|
|
let el = pm.parentElement;
|
|
while (el && getComputedStyle(el).overflowY !== "auto") el = el.parentElement;
|
|
el.scrollTop = (el.scrollHeight - el.clientHeight) * f;
|
|
}, fraction);
|
|
await p.waitForTimeout(900);
|
|
return position();
|
|
};
|
|
|
|
const total = Number((await position()).match(/of (\d+)/)?.[1] || 0);
|
|
check(total > 2, "writing past the end of a page makes more of them", await position());
|
|
check(
|
|
(await p.locator("[data-note-position]").count()) === 1,
|
|
"and the count is shown the way the PDF reader shows it"
|
|
);
|
|
|
|
check((await scrollTo(0)).startsWith("Page 1"), "at the top it says page one", await position());
|
|
const nearEnd = await scrollTo(0.9);
|
|
check(
|
|
Number(nearEnd.match(/Page (\d+)/)[1]) > 1,
|
|
"scrolling down moves the count on",
|
|
nearEnd
|
|
);
|
|
|
|
/* ── the paper ──
|
|
* Each page is a sheet of its own with a gap after it, the way the PDF reader
|
|
* draws its pages. The gap is the part that has to be got right: the browser
|
|
* knows nothing about our sheets and would lay a line of text straight across
|
|
* it, so the text flow holds it open — see editor/pagination.js. */
|
|
check(
|
|
(await p.locator("[data-page-sheet]").count()) === Number((await position()).match(/of (\d+)/)[1]),
|
|
"there is a sheet of paper for every page",
|
|
`${await p.locator("[data-page-sheet]").count()} sheets, ${await position()}`
|
|
);
|
|
const straddling = await p.evaluate(() => {
|
|
const pm = document.querySelector(".ProseMirror");
|
|
const top = pm.getBoundingClientRect().top;
|
|
const sheets = [...document.querySelectorAll("[data-page-sheet]")].map((el) => el.getBoundingClientRect());
|
|
const gaps = sheets.slice(0, -1).map((r, i) => ({ from: r.bottom - top, to: sheets[i + 1].top - top }));
|
|
const bad = [];
|
|
for (const el of pm.children) {
|
|
if (el.classList.contains("ne-page-spacer")) continue;
|
|
const r = el.getBoundingClientRect();
|
|
if (r.height === 0) continue;
|
|
for (const g of gaps) {
|
|
if (r.bottom - top > g.from + 0.5 && r.top - top < g.to - 0.5) bad.push((el.textContent || "").slice(0, 30));
|
|
}
|
|
}
|
|
return bad;
|
|
});
|
|
check(straddling.length === 0, "and no line of text is cut in half by a gap", straddling.join(" | "));
|
|
check(
|
|
(await p.locator(".ne-page-spacer").count()) > 0,
|
|
"which takes holding the gap open inside the text itself"
|
|
);
|
|
/* The placeholder extension styles `[contenteditable]:empty::before`, and a
|
|
* spacer is exactly that. "Start writing..." was printed at the foot of every
|
|
* page that had a break after it. */
|
|
check(
|
|
await p.evaluate(() =>
|
|
[...document.querySelectorAll(".ne-page-spacer")].every(
|
|
(el) => getComputedStyle(el, "::before").content === "none"
|
|
)
|
|
),
|
|
"and the gap stays blank — no placeholder printed at the foot of the page"
|
|
);
|
|
|
|
/* ── twenty-one lines, and the same twenty-one everywhere ──
|
|
*
|
|
* Two claims. First, a page holds the number of lines a ruled page holds —
|
|
* it held eighteen on a phone, which is what started this. Second, and the
|
|
* one that matters: the phone breaks the text in exactly the same places the
|
|
* desktop does. It did not, because the text used to be set in screen pixels:
|
|
* a 340-pixel sheet fitted nothing like what a 760-pixel one did, so the same
|
|
* note had different lines on different pages depending on what you read it
|
|
* on. The text is now laid out on a page of a fixed size and the whole sheet
|
|
* scaled, so the only thing a narrow screen changes is how big it looks.
|
|
*
|
|
* Measured in page units — every length divided by the sheet's own scale —
|
|
* because screen pixels are precisely what must not agree. */
|
|
const pageUnits = () =>
|
|
p.evaluate(() => {
|
|
const scale = Number(document.querySelector("[data-text-page-scale]").dataset.textPageScale);
|
|
const pm = document.querySelector(".ProseMirror");
|
|
const cs = getComputedStyle(pm);
|
|
const sheet = document.querySelector("[data-page-sheet]").getBoundingClientRect();
|
|
const top = pm.getBoundingClientRect().top;
|
|
const blocks = [...pm.children]
|
|
.filter((el) => !el.classList.contains("ne-page-spacer"))
|
|
.slice(0, 40)
|
|
.map((el) => {
|
|
const r = el.getBoundingClientRect();
|
|
return { y: Math.round((r.top - top) / scale), h: Math.round(r.height / scale) };
|
|
});
|
|
return {
|
|
lines:
|
|
(sheet.height / scale - parseFloat(cs.paddingTop) - parseFloat(cs.paddingBottom)) /
|
|
parseFloat(cs.lineHeight),
|
|
blocks,
|
|
};
|
|
});
|
|
|
|
const onDesktop = await pageUnits();
|
|
check(
|
|
Math.floor(onDesktop.lines) === 21,
|
|
"a page holds twenty-one lines",
|
|
`${onDesktop.lines.toFixed(2)} fit`
|
|
);
|
|
|
|
const phone = { width: 390, height: 844 };
|
|
const desktop = p.viewportSize();
|
|
await p.setViewportSize(phone);
|
|
await p.waitForTimeout(2000);
|
|
/* A phone shows one pane at a time, so the note may have been left behind in
|
|
the list when the window narrowed. Open it again before measuring. */
|
|
if ((await p.locator("[data-text-page-scale]").count()) === 0) await reopenNote(p, "Langer Text");
|
|
await p.waitForSelector("[data-text-page-scale]", { timeout: 15000 });
|
|
await p.waitForTimeout(3000);
|
|
const onPhone = await pageUnits();
|
|
check(
|
|
Math.floor(onPhone.lines) === 21,
|
|
"on a phone as well — the same page, not a smaller one",
|
|
`${onPhone.lines.toFixed(2)} fit`
|
|
);
|
|
const moved = onDesktop.blocks
|
|
.map((b, i) => ({ i, b, o: onPhone.blocks[i] }))
|
|
.filter(({ b, o }) => !o || Math.abs(b.y - o.y) > 1 || Math.abs(b.h - o.h) > 1)
|
|
.map(({ i, b, o }) => `#${i}: ${b.y}/${b.h} vs ${o ? `${o.y}/${o.h}` : "missing"}`);
|
|
check(
|
|
onPhone.blocks.length === onDesktop.blocks.length && moved.length === 0,
|
|
"and every paragraph falls in the same place on both, to the page unit",
|
|
moved.slice(0, 4).join(" | ")
|
|
);
|
|
await p.setViewportSize(desktop);
|
|
await p.waitForTimeout(2000);
|
|
if ((await p.locator(".ProseMirror").count()) === 0) await reopenNote(p, "Langer Text");
|
|
|
|
/* The note is still one document — pages are drawn, not stored. */
|
|
const stored = (await api(p, "GET", "/notes")).body[0];
|
|
const body = (await api(p, "GET", `/notes/${stored.id}`)).body.content;
|
|
check(!/"type"\s*:\s*"page"/.test(body), "no page ever reaches the stored document");
|
|
check(!/page-spacer/.test(body), "and neither does anything holding a gap open");
|
|
|
|
/* ── alignment ── */
|
|
// Left is the default and is never written into the document, so an untouched
|
|
// paragraph reads as left without carrying an attribute for it.
|
|
await createNote(p);
|
|
await setTitle(p, "Ausrichtung");
|
|
await typeInEditor(p, "Diese Zeile wandert.");
|
|
check(
|
|
(await p.locator('[data-align="left"]').count()) === 1,
|
|
"the toolbar offers alignment"
|
|
);
|
|
|
|
await p.locator('[data-align="center"]').click();
|
|
await p.waitForTimeout(1800);
|
|
check(
|
|
(await p.locator(".ProseMirror p").first().evaluate((e) => getComputedStyle(e).textAlign)) === "center",
|
|
"choosing centre centres the paragraph"
|
|
);
|
|
|
|
await reopenNote(p, "Ausrichtung");
|
|
check(
|
|
(await p.locator(".ProseMirror p").first().evaluate((e) => getComputedStyle(e).textAlign)) === "center",
|
|
"and it survives a reload, so it is in the document and not on the screen"
|
|
);
|
|
|
|
const alignedBody = (await api(p, "GET", "/notes")).body;
|
|
const alignedNote = alignedBody.find((n) => n.title === "Ausrichtung");
|
|
const alignedContent = (await api(p, "GET", `/notes/${alignedNote.id}`)).body.content;
|
|
check(/"textAlign"\s*:\s*"center"/.test(alignedContent), "stored on the paragraph itself");
|
|
|
|
await p.locator('[data-align="left"]').click();
|
|
await p.waitForTimeout(1800);
|
|
check(
|
|
(await p.locator(".ProseMirror p").first().evaluate((e) => getComputedStyle(e).textAlign)) === "start",
|
|
"and going back to left leaves no alignment behind",
|
|
await p.locator(".ProseMirror p").first().evaluate((e) => getComputedStyle(e).textAlign)
|
|
);
|
|
|
|
/* ── the marker pen and the ink ──
|
|
* Two marks the schema had no room for until now, so the thing worth checking
|
|
* is that they reach the document and come back out of it: a colour that only
|
|
* exists on screen is a colour that is lost on the next reload. */
|
|
await createNote(p);
|
|
await setTitle(p, "Farbe");
|
|
await typeInEditor(p, "Wichtig und bunt");
|
|
await p.waitForTimeout(1200);
|
|
|
|
// Select the whole line, the way somebody marks a sentence they just wrote.
|
|
await p.locator(".ProseMirror").click();
|
|
await p.keyboard.press("Control+a");
|
|
await p.locator("[data-toolbar-colour]").click();
|
|
await p.waitForTimeout(400);
|
|
check((await p.locator("[data-colour-panel]").count()) === 1, "the toolbar offers colour");
|
|
const swatches = await p.locator("[data-colour-swatch]").count();
|
|
check(swatches >= 8, "with a palette for both kinds", `${swatches} swatches`);
|
|
|
|
await p.locator('[data-colour-swatch="#fff3a3"]').click();
|
|
await p.waitForTimeout(1600);
|
|
check((await p.locator(".ProseMirror mark").count()) === 1, "a highlight lands on the selected words");
|
|
|
|
await p.keyboard.press("Control+a");
|
|
await p.locator('[data-colour-swatch="#d1242f"]').click();
|
|
await p.waitForTimeout(1800);
|
|
const inked = await p.locator(".ProseMirror mark span").first().evaluate((el) => getComputedStyle(el).color);
|
|
check(inked === "rgb(209, 36, 47)", "and a text colour on top of it", inked);
|
|
|
|
await reopenNote(p, "Farbe");
|
|
check((await p.locator(".ProseMirror mark").count()) === 1, "the highlight is still there after a reload");
|
|
check(
|
|
(await p.locator(".ProseMirror [style*=color]").count()) >= 1,
|
|
"and so is the colour of the letters"
|
|
);
|
|
|
|
/* Pressing the same colour again takes it off — the button that put it there
|
|
is the one people reach for to remove it. */
|
|
await p.locator(".ProseMirror").click();
|
|
await p.keyboard.press("Control+a");
|
|
await p.locator("[data-toolbar-colour]").click();
|
|
await p.waitForTimeout(400);
|
|
await p.locator('[data-colour-swatch="#fff3a3"]').click();
|
|
await p.waitForTimeout(1500);
|
|
check((await p.locator(".ProseMirror mark").count()) === 0, "and pressing the same colour again removes it");
|
|
|
|
/* ── a quote that holds more than a sentence ──
|
|
* Markdown lets a quote hold whole blocks, and so does the editor. What it did
|
|
* not do was give them any room: inside a quote nothing had a margin, so a
|
|
* heading, a list and a closing line stood jammed together while the items of
|
|
* the list were further apart than the blocks around them. */
|
|
await createNote(p);
|
|
await setTitle(p, "Zitat");
|
|
await p.locator(".ProseMirror").click();
|
|
await p.keyboard.type("> ### Quartalszahlen");
|
|
await p.keyboard.press("Enter");
|
|
await p.keyboard.type("- Umsatz");
|
|
await p.keyboard.press("Enter");
|
|
await p.keyboard.type("Gewinn");
|
|
await p.keyboard.press("Enter");
|
|
await p.keyboard.press("Enter");
|
|
await p.keyboard.type("Alles nach Plan.");
|
|
await p.waitForTimeout(1500);
|
|
|
|
const quoted = await p.evaluate(() =>
|
|
[...(document.querySelector(".ProseMirror blockquote")?.children || [])].map((el) => {
|
|
const r = el.getBoundingClientRect();
|
|
return { tag: el.tagName, top: r.top, bottom: r.bottom };
|
|
})
|
|
);
|
|
check(
|
|
quoted.map((c) => c.tag).join(",") === "H3,UL,P",
|
|
"a quote holds a heading, a list and a paragraph",
|
|
quoted.map((c) => c.tag).join(",")
|
|
);
|
|
const gaps = quoted.slice(1).map((c, i) => Math.round(c.top - quoted[i].bottom));
|
|
check(gaps.every((g) => g >= 6), "with room between them, as outside a quote", gaps.join(" | "));
|
|
|
|
/* And the same divide when it leaves as Markdown: written on consecutive
|
|
lines, every other reader folds that closing line into the last bullet. */
|
|
const quoteNote = (await api(p, "GET", "/notes")).body.find((n) => n.title === "Zitat");
|
|
const quoteDoc = (await api(p, "GET", `/notes/${quoteNote.id}`)).body.content;
|
|
check(/blockquote/.test(quoteDoc), "and it is stored as a quote");
|
|
|
|
/* ── the typeface follows the account ── */
|
|
await p.getByTitle("Account").click();
|
|
await p.waitForTimeout(800);
|
|
await p.locator('[data-note-font="mono"]').click();
|
|
await p.waitForTimeout(1500);
|
|
await p.keyboard.press("Escape");
|
|
await p.waitForTimeout(1200);
|
|
check(
|
|
/mono/i.test(await p.locator(".ProseMirror").evaluate((e) => getComputedStyle(e).fontFamily)),
|
|
"choosing a typeface changes the note text",
|
|
await p.locator(".ProseMirror").evaluate((e) => getComputedStyle(e).fontFamily)
|
|
);
|
|
|
|
// On the account, not in this browser — so it is there on the next device.
|
|
const me = await api(p, "GET", "/auth/me");
|
|
check(me.body?.preferences?.noteFont === "mono", "and is stored on the account", JSON.stringify(me.body?.preferences));
|
|
|
|
await p.reload({ waitUntil: "networkidle" });
|
|
await p.waitForTimeout(3000);
|
|
// A reload lands on the dashboard, so the notebook has to be opened again.
|
|
await p.locator("button").filter({ hasText: "Editor Book" }).first().click();
|
|
await p.waitForTimeout(1500);
|
|
await p.locator("[data-note-row]").first().click();
|
|
await p.waitForTimeout(2500);
|
|
check(
|
|
/mono/i.test(await p.locator(".ProseMirror").evaluate((e) => getComputedStyle(e).fontFamily)),
|
|
"and survives a reload"
|
|
);
|
|
|
|
/* ── light or dark is a setting, and it belongs to the account ──
|
|
* It used to be a sun in the corner of the sidebar: reached for by accident,
|
|
* forgotten by the next device, and gone on every reload. */
|
|
const shade = () =>
|
|
p.evaluate(() => getComputedStyle(document.querySelector("#root > div") || document.body).backgroundColor);
|
|
const wasDark = await shade();
|
|
|
|
await p.getByTitle("Account").click();
|
|
await p.waitForTimeout(900);
|
|
check(
|
|
(await p.locator("[data-theme-option]").count()) === 2,
|
|
"the account panel offers light and dark"
|
|
);
|
|
await p.locator('[data-theme-option="light"]').click();
|
|
await p.waitForTimeout(1500);
|
|
await p.keyboard.press("Escape");
|
|
await p.waitForTimeout(900);
|
|
check((await shade()) !== wasDark, "choosing one repaints the app", `${wasDark} → ${await shade()}`);
|
|
|
|
const themed = await api(p, "GET", "/auth/me");
|
|
check(
|
|
themed.body?.preferences?.theme === "light",
|
|
"and it is stored on the account, not in this browser",
|
|
JSON.stringify(themed.body?.preferences)
|
|
);
|
|
|
|
await p.reload({ waitUntil: "networkidle" });
|
|
await p.waitForTimeout(3000);
|
|
check((await shade()) !== wasDark, "and it is still there after a reload", await shade());
|
|
|
|
/* The sun in the sidebar is gone with it — two switches for one setting is one
|
|
too many, and that corner is for places to go. Counted where it stood: the
|
|
account button's own row, which used to hold two. */
|
|
const cornerButtons = await p
|
|
.locator('[title="Account"]')
|
|
.evaluate((el) => el.parentElement.querySelectorAll("button").length);
|
|
check(cornerButtons === 1, "and the sidebar corner holds nothing but the account", `${cornerButtons} buttons`);
|
|
|
|
} catch (e) {
|
|
check(false, "suite ran to the end", e.message);
|
|
} finally {
|
|
await session?.cleanup();
|
|
const code = report();
|
|
await browser.close();
|
|
process.exit(code);
|
|
}
|