Public: anyone can read this; the crew can change it.

A test: 390x844 and 1280x800, no page is covered by a closed overlay (#11831 item 3) #11929

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

torque/forge:torque/11854-a-test-390x844-and-1280x800-no-page-is-c → main 5 commits fork 3 threads · 3 unresolved

  • Merged. torque/11854-a-test-390x844-and-1280x800-no-page-is-c was merged into main as 582d6a4.

Review

Sign in to review this change.
1 file changed +135 −0
test/no-closed-overlay.test.ts added +135
@@ -0,0 +1,135 @@
1 /**
2 * No page is covered by a closed overlay (#11831 item 3, #11854).
3 *
4 * Release 0.7.549 drew a closed palette `<dialog>` over every page on a
5 * phone. This drives a real headless Chromium at 390×844 and 1280×800, signed
6 * out and signed in, over the forge's own pages: with nothing opened, the
7 * element at the centre of the screen is the page's content, never a dialog,
8 * popover or large fixed layer. Then the palette is opened and closed with
9 * its close button, and the page is back.
10 *
11 * Only dialogs, popovers and `position: fixed` layers are looked for: an absolute or sticky
12 * full-page layer is not detected. The detector is proved by a positive control: with the
13 * palette open it must report a dialog, so an empty list is not blindness.
14 *
15 * Not held here: Escape and tap-outside (a native dialog takes those from a
16 * real key and pointer, which this CDP helper does not send; test/palette-dismiss
17 * holds them in the client script), and the release-proof half (#11831 item 4).
18 */
19
20 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
21 import { mkdtemp, rm } from "node:fs/promises";
22 import { tmpdir } from "node:os";
23 import { join } from "node:path";
24 import { createApp, type App } from "../src/app.ts";
25 import { serve, type Servers } from "../src/http/server.ts";
26 import { findChromium } from "../src/looks/capture.ts";
27 import { quietLogs } from "./helpers/globals.ts";
28 import { chromiumRuns, evaluateLoaded } from "./helpers/chrome.ts";
29
30 quietLogs();
31
32 const found = findChromium();
33 const chromium = found !== undefined && chromiumRuns(found) ? found : undefined;
34 if (chromium === undefined)
35 console.log("SKIPPING: no chromium that starts on this machine, so the covered-page test cannot run");
36
37 /** `/search` is left out: it opens the palette as a sheet on purpose (data-palette-auto). */
38 const PAGES = ["/", "/login", "/explore", "/apps", "/changelog"];
39 const VIEWPORTS = [
40 { width: 390, height: 844 },
41 { width: 1280, height: 800 },
42 ];
43
44 /** What is on top at the centre of the screen, and whether it is an overlay. */
45 const COVER = `(() => {
46 const vw = document.documentElement.clientWidth, vh = innerHeight;
47 const top = document.elementFromPoint(vw / 2, vh / 2);
48 const covers = [];
49 for (let el = top; el; el = el.parentElement) {
50 const cs = getComputedStyle(el), r = el.getBoundingClientRect();
51 const big = r.width * r.height > (vw * vh) / 4;
52 const kind = el.tagName === "DIALOG" ? "dialog" : el.hasAttribute("popover") ? "popover"
53 : cs.position === "fixed" && big && cs.visibility !== "hidden" ? "fixed layer" : "";
54 if (kind) covers.push(kind + ":" + el.tagName.toLowerCase() + "." + String(el.className));
55 }
56 return { signedIn: !!document.querySelector('form[action="/logout"]'),
57 top: top ? top.tagName.toLowerCase() + "." + String(top.className) : "none", covers };
58 })()`;
59
60 describe.skipIf(chromium === undefined)("no page is covered by a closed overlay", () => {
61 let dataDir: string;
62 let app: App;
63 let servers: Servers;
64 let base: string;
65 let cookie = { name: "", value: "" };
66
67 beforeAll(async () => {
68 dataDir = await mkdtemp(join(tmpdir(), "forge-cover-"));
69 app = createApp({ dataDir, port: 0 });
70 servers = await serve(app);
71 base = servers.url;
72 const signup = await fetch(`${base}/signup`, {
73 method: "POST",
outdated

split("=") truncates a cookie value that contains '='. Split on the first '=' (indexOf/slice), and assert name and value are non-empty so a failed signup fails loudly.

74 body: new URLSearchParams({ username: "ada", email: "[email protected]", password: "a-long-web-passphrase" }),
75 redirect: "manual",
76 });
77 // Split on the first "=" only: a value may carry "=" padding of its own.
78 const pair = (signup.headers.get("set-cookie") ?? "").split(";")[0]!;
79 const eq = pair.indexOf("=");
80 cookie = { name: pair.slice(0, eq), value: pair.slice(eq + 1) };
81 expect(cookie.name).not.toBe("");
82 expect(cookie.value).not.toBe("");
83 }, 20_000);
84
outdated

Nothing proves signedIn is really signed in. Add an assertion that the page shows the signed-in state, otherwise half the matrix may be the signed-out run twice.

85 afterAll(async () => {
86 servers.stop();
87 app.close();
88 await rm(dataDir, { recursive: true, force: true });
89 });
90
91 for (const viewport of VIEWPORTS)
92 for (const signedIn of [false, true])
93 for (const path of PAGES)
outdated

Add a positive control: with the palette open, COVER must report a dialog. Without it, an empty covers list could just mean the detector is blind.

94 test(`${path} at ${viewport.width}×${viewport.height}, ${signedIn ? "signed in" : "signed out"}: nothing closed is on top`, async () => {
95 const out = JSON.parse(
96 await evaluateLoaded(chromium!, `${base}${path}`, viewport, signedIn ? [cookie] : [],
97 `JSON.stringify(${COVER})`),
98 ) as { signedIn: boolean; top: string; covers: string[] };
99 // The cookie must really sign the page in (or out),
100 // else half the matrix is one run twice.
101 expect(out.signedIn).toBe(signedIn);
102 expect(out.covers).toEqual([]);
103 }, 20_000);
104
105 for (const viewport of VIEWPORTS)
106 test(`the palette at ${viewport.width}×${viewport.height}: opens, its close button closes it, the page is back`, async () => {
107 const out = JSON.parse(
108 await evaluateLoaded(chromium!, `${base}/`, viewport, [cookie], `(async () => {
109 const dlg = document.querySelector("dialog.palette, dialog#palette");
110 if (!dlg) return JSON.stringify({ missing: true });
111 const boxBefore = getComputedStyle(dlg).display;
112 const opener = document.querySelector("[data-palette]");
113 opener.click();
114 const opened = dlg.open;
115 const whileOpen = ${COVER};
116 const close = dlg.querySelector("[data-palette-close]");
117 // Wide screens hide the close button (display:none): close through the dialog itself.
118 if (close && getComputedStyle(close).display !== "none") close.click(); else dlg.close();
119 return JSON.stringify({ opened, whileOpen, closed: !dlg.open, boxBefore, boxAfter: getComputedStyle(dlg).display, cover: ${COVER} });
120 })()`),
121 ) as {
122 missing?: boolean; opened: boolean; closed: boolean; boxBefore: string; boxAfter: string;
123 whileOpen: { covers: string[] }; cover: { covers: string[] };
124 };
125 expect(out.missing).toBeUndefined();
126 // A closed dialog has no layout box: it is not drawn at all.
127 expect(out.boxBefore).toBe("none");
128 expect(out.opened).toBe(true);
129 // Positive control: open, the detector sees a dialog on top.
130 expect(out.whileOpen.covers.some((c) => c.startsWith("dialog:"))).toBe(true);
131 expect(out.boxAfter).toBe("none");
132 expect(out.closed).toBe(true);
133 expect(out.cover.covers).toEqual([]);
134 }, 20_000);
135 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs