tests/breakpoints.test.ts
Rama main · réplica de lectura del repositorio original.
import assert from "node:assert/strict";
import { readdirSync, readFileSync, statSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { describe, it } from "node:test";
// BREAKPOINT PARITY — "a phone" is one width everywhere.
//
// The shell decides phone-or-desktop with PHONE_SHELL_QUERY (client/
// shellQuery.ts): ≤700px, or a coarse pointer that cannot hover. Stylesheets
// that restyle something "for phones" used to pick their own width — 720 in
// settings, attachments and presets, 760 in the composer, 640 in the
// hovercard, the theme picker and the palette — so a 701–760px window was a
// phone to one sheet and a desktop to the next. This test holds every
// stylesheet under client/ (and the Android shell's own) to the shell's
// width: a `max-width` in the phone band (601–799px) is 700, unless the
// three lines above it say why not with the words "not the shell's 700"
// (the public blog's 640 does, for its own grids). And no source file
// re-spells a phone-band query in a string: it imports the constant.
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const SHELL_WIDTH = 700;
const BAND = { lo: 601, hi: 799 };
const REASON = "not the shell's 700";
function walk(dir: string, keep: (name: string) => boolean): string[] {
const out: string[] = [];
for (const name of readdirSync(dir)) {
if (name === "node_modules" || name === "dist") continue;
const p = path.join(dir, name);
if (statSync(p).isDirectory()) out.push(...walk(p, keep));
else if (keep(name)) out.push(p);
}
return out;
}
const stylesheets = [
...walk(path.join(root, "client"), (n) => n.endsWith(".css")),
...walk(path.join(root, "mobile/src"), (n) => n.endsWith(".css")),
];
const sources = [
...walk(path.join(root, "client"), (n) => /\.tsx?$/.test(n)),
...walk(path.join(root, "shared"), (n) => /\.tsx?$/.test(n)),
...walk(path.join(root, "mobile/src"), (n) => /\.tsx?$/.test(n)),
];
/** Every `max-width: Npx` inside an `@media` condition, with its line. */
export function phoneBandWidths(css: string): { line: number; px: number; justified: boolean }[] {
const lines = css.split("\n");
const out: { line: number; px: number; justified: boolean }[] = [];
const re = /@media\s+([^{]+)\{/g;
for (let m = re.exec(css); m; m = re.exec(css)) {
const line = css.slice(0, m.index).split("\n").length;
for (const w of m[1].matchAll(/max-width:\s*(\d+)px/g)) {
const px = Number(w[1]);
if (px < BAND.lo || px > BAND.hi) continue;
const above = lines.slice(Math.max(0, line - 4), line - 1).join("\n");
out.push({ line, px, justified: above.includes(REASON) });
}
}
return out;
}
describe("breakpoint parity", () => {
it("the shell's width is the constant this test enforces", () => {
const q = readFileSync(path.join(root, "client/shellQuery.ts"), "utf8");
assert.match(q, new RegExp(`PHONE_SHELL_QUERY = "\\(max-width: ${SHELL_WIDTH}px\\)`));
});
it("scans a real set of stylesheets", () => {
assert.ok(stylesheets.length > 40, `only ${stylesheets.length} stylesheets found`);
assert.ok(stylesheets.some((f) => f.endsWith("client/phone/phone.css")));
assert.ok(stylesheets.some((f) => f.endsWith("client/reading/query.css")));
});
it("every phone-band max-width in every stylesheet is the shell's 700, or says why not", () => {
const drift: string[] = [];
for (const file of stylesheets) {
for (const { line, px, justified } of phoneBandWidths(readFileSync(file, "utf8"))) {
if (px !== SHELL_WIDTH && !justified) drift.push(`${path.relative(root, file)}:${line} max-width: ${px}px`);
}
}
assert.deepEqual(drift, [], `a stylesheet calls a different width "phone":\n ${drift.join("\n ")}`);
});
it("a justified exception is rare, and the reason is written next to it", () => {
const justified: string[] = [];
for (const file of stylesheets) {
for (const w of phoneBandWidths(readFileSync(file, "utf8"))) {
if (w.px !== SHELL_WIDTH) justified.push(path.relative(root, file));
}
}
// Only the public blog's own layout grids today. A new file here is a
// decision worth a second look, not a mechanical addition.
assert.deepEqual([...new Set(justified)], ["client/styles/blog.css"]);
});
it("the detector catches the widths that drifted before, and honours a reason", () => {
assert.deepEqual(
phoneBandWidths("@media (max-width: 720px) {\n}\n@media (max-width: 760px), (pointer: coarse) {\n}").map((w) => [w.px, w.justified]),
[[720, false], [760, false]],
);
assert.deepEqual(
phoneBandWidths(`/* ${REASON}: a grid */\n@media (max-width: 640px) {\n}`).map((w) => w.justified),
[true],
);
// Outside the band: a 560 stack, a 900 desk, a 1100 composer — not phones.
assert.deepEqual(phoneBandWidths("@media (max-width: 560px) {}\n@media (max-width: 900px) {}"), []);
});
it("no source file re-spells a phone-band query; it imports PHONE_SHELL_QUERY", () => {
const copies: string[] = [];
for (const file of sources) {
if (file.endsWith("client/shellQuery.ts")) continue;
readFileSync(file, "utf8").split("\n").forEach((text, i) => {
const code = text.replace(/\/\/.*$/, "");
if (/^\s*\*/.test(code)) return; // a doc comment's body
for (const m of code.matchAll(/["'`][^"'`]*\(max-width:\s*(\d+)px\)/g)) {
const px = Number(m[1]);
if (px >= BAND.lo && px <= BAND.hi) copies.push(`${path.relative(root, file)}:${i + 1} ${text.trim()}`);
}
if (/["'`]\(pointer: coarse\) and \(hover: none\)/.test(code)) copies.push(`${path.relative(root, file)}:${i + 1} ${text.trim()}`);
});
}
assert.deepEqual(copies, [], `use PHONE_SHELL_QUERY from client/shellQuery.ts:\n ${copies.join("\n ")}`);
});
});