/* The frame around the writing: the dashboard, the sidebar on a phone, and the * paper itself. * * These are the things that are only ever wrong on a screen — a heading in the * wrong order, a page you cannot see the edge of, a tap that does nothing — so * they are checked in a browser at the two sizes they are used at. */ import { BASE, check, createNote, createNotebook, launch, report, setTitle, typeInEditor, withFreshUser } from "./lib.mjs"; const browser = await launch(); let session; try { session = await withFreshUser(browser, "shell"); const p = session.page; await createNotebook(p, "Rahmen"); await createNote(p); await setTitle(p, "Zuerst"); await typeInEditor(p, "Ein kurzer Satz."); await p.waitForTimeout(1600); await createNote(p); await setTitle(p, "Zuletzt"); await typeInEditor(p, "Der jüngere."); await p.waitForTimeout(1800); /* ── the dashboard ── * What you were last writing is what you usually came back for; the shelf of * notebooks is for choosing where to go next. */ await p.locator("button").filter({ hasText: "Dashboard" }).first().click(); await p.waitForTimeout(1500); const headings = await p.evaluate(() => [...document.querySelectorAll("h1,h2")].map((h) => h.innerText.trim())); check( headings.indexOf("Recent Notes") < headings.indexOf("My Notebooks"), "the recent notes come before the notebooks", headings.join(" | ") ); /* "Recent" is a question about time, and it has to answer it whatever order the notebook it came from happens to be in — which is a hand-made one here. */ const recent = await p.evaluate(() => { const head = [...document.querySelectorAll("h2")].find((h) => h.innerText.trim() === "Recent Notes"); return [...head.nextElementSibling.querySelectorAll("button")].map((b) => b.innerText.split("\n")[0].trim()); }); check(recent[0] === "Zuletzt", "and the most recently edited is first", recent.join(" | ")); /* ── tapping the blank part of the page ── * The text is only as tall as what has been written, so most of a fresh page * is beside the editable area rather than in it. A tap there used to land on * nothing: no caret, and on a phone no keyboard. */ await p.locator("button").filter({ hasText: "Rahmen" }).first().click(); await p.waitForTimeout(1200); await p.locator("[data-note-row]").first().click(); await p.waitForTimeout(2200); const before = await p.locator(".ProseMirror").innerText(); const sheet = await p.locator("[data-page-sheet]").first().boundingBox(); // Below the writing and inside the window — the sheet is taller than both. await p.mouse.click(sheet.x + sheet.width / 2, Math.min(sheet.y + sheet.height * 0.8, 900)); await p.waitForTimeout(500); check( (await p.locator(".ProseMirror-focused").count()) === 1, "tapping the blank part of the page starts writing there" ); await p.keyboard.type("ANGEHÄNGT"); await p.waitForTimeout(900); const after = await p.locator(".ProseMirror").innerText(); check(after.includes("ANGEHÄNGT") && after.startsWith(before.slice(0, 10)), "and what is typed lands in the note", after.slice(0, 60)); /* ── the phone ── */ const phone = await browser.newContext({ viewport: { width: 390, height: 844 }, hasTouch: true }); const m = await phone.newPage(); await m.goto(BASE, { waitUntil: "networkidle" }); await m.getByPlaceholder("Username").fill(session.user); await m.getByPlaceholder("Password").fill(session.pass); await m.getByRole("button", { name: "Sign In" }).click(); await m.waitForTimeout(4000); /* Dragging in from the left edge shows the sidebar. The button that opens it sits in the top corner, which is the one part of a tall screen a thumb cannot reach. */ const swipe = (from, to, y = 500) => m.evaluate( ([x1, x2, yy]) => { // Real Touch objects: a plain object is refused by the constructor. const touch = (x) => [new Touch({ identifier: 1, target: document.body, clientX: x, clientY: yy })]; const fire = (type, x) => document.dispatchEvent( new TouchEvent(type, { touches: type === "touchend" ? [] : touch(x), changedTouches: touch(x), bubbles: true }) ); fire("touchstart", x1); fire("touchmove", (x1 + x2) / 2); fire("touchmove", x2); fire("touchend", x2); }, [from, to, y] ); /* Measured on the panel itself: closed, it is a strip of zero width with the contents clipped inside it, so the buttons in there still have a size and a place — asking one of them proves nothing. */ const sidebarShown = async () => (await m.locator("[data-sidebar]").evaluate((el) => el.getBoundingClientRect().width)) > 100; check(!(await sidebarShown()), "on a phone the sidebar starts out of the way"); await swipe(4, 200); await m.waitForTimeout(900); check(await sidebarShown(), "a drag in from the left edge brings it out"); await swipe(200, 10); await m.waitForTimeout(900); check(!(await sidebarShown()), "and pushing it back sends it away again"); // A drag that starts in the middle of the page is not that gesture — it is a // selection, or a table being scrolled. await swipe(220, 340); await m.waitForTimeout(700); check(!(await sidebarShown()), "a drag that does not start at the edge leaves it alone"); /* The paper on a phone runs to the edges, but it has an edge — without one there was nothing to see where one page ended and the next began. Opened from the dashboard's own shelf: the sidebar holds a row of the same name, and while it is closed that row is clipped to nothing. */ await m.locator("button").filter({ hasText: "Rahmen" }).last().click(); await m.waitForTimeout(1800); await m.locator("[data-note-row]").first().click(); await m.waitForTimeout(2500); const edge = await m.locator("[data-page-sheet]").first().evaluate((el) => { const cs = getComputedStyle(el); return { width: cs.borderTopWidth, style: cs.borderTopStyle }; }); check( parseFloat(edge.width) >= 1 && edge.style === "solid", "the page has a visible edge on a phone too", JSON.stringify(edge) ); await phone.close(); } catch (e) { check(false, "suite ran to the end", e.stack || e.message); } finally { await session?.cleanup?.(); await browser.close(); process.exit(report()); }