返回 DeepSeek-Reasonix
mobile-nav-contract.test.mjs
根目录 / site / src / scripts / mobile-nav-contract.test.mjs
1 import assert from "node:assert/strict";
2 import { readFile } from "node:fs/promises";
3 import test from "node:test";
4
5 const source = (path) => readFile(new URL(path, import.meta.url), "utf8");
6
7 /* Extract the body of a `@media (max-width: Npx)` block (up to the next
8 at-rule or EOF) so assertions stay scoped to small viewports. */
9 const mediaBlock = (css, px) => {
10 const marker = `@media (max-width: ${px}px)`;
11 const start = css.indexOf(marker);
12 assert.notEqual(start, -1, `missing ${marker}`);
13 const rest = css.slice(start + marker.length);
14 const next = rest.search(/@media|@keyframes/);
15 return next === -1 ? rest : rest.slice(0, next);
16 };
17
18 /* The header must fit narrow viewports while keeping a clear menu trigger. */
19 test("≤640px: marketing nav contracts to fit 390px viewports", async () => {
20 const [css, header] = await Promise.all([
21 source("../styles/global.css"),
22 source("../components/SiteHeader.astro"),
23 ]);
24 const block = mediaBlock(css, 640);
25 assert.match(block, /\.nav-sign-in \{ display: none/);
26 assert.match(block, /\.nav \.brand span \{ display: none/);
27 assert.match(block, /\.nav \.brand img \{ width: 64px; \}/);
28 assert.match(header, /<source media=\"\(max-width: 400px\)\" srcset=\{`\$\{base\}\/favicon\.svg`\} \/>/);
29 assert.match(block, /\.theme-switch button \{ padding: 6px 9px/);
30 });
31
32 test("≤1100px: marketing navigation collapses before controls overlap", async () => {
33 const css = await source("../styles/global.css");
34 const tabletBlock = mediaBlock(css, 1100);
35 const mobileBlock = mediaBlock(css, 900);
36
37 assert.match(tabletBlock, /\.nav-links \{ display: none/);
38 assert.match(tabletBlock, /\.nav-github \{ margin-left: auto; padding-inline: 8px/);
39 assert.match(tabletBlock, /\.nav-github-label \{ display: none/);
40 assert.match(tabletBlock, /\.nav-menu-toggle \{ display: inline-flex/);
41 assert.match(mobileBlock, /\.nav-github \{ display: none/);
42 });
43
44 test("≤360px: marketing nav keeps the menu trigger within 320px", async () => {
45 const css = await source("../styles/global.css");
46 const block = mediaBlock(css, 360);
47 assert.match(block, /\.nav-inner \{ padding: 0 12px; gap: 6px/);
48 assert.match(block, /\.lang-switch button \{ padding: 6px 8px/);
49 assert.match(block, /\.theme-switch button \{ padding: 6px 7px/);
50 assert.match(block, /\.nav-install \{ display: none/);
51 assert.match(block, /\.nav-menu-toggle \{ width: 36px; height: 36px/);
52 });
53
54 test("≤400px: marketing nav uses the compact brand mark", async () => {
55 const css = await source("../styles/global.css");
56 const block = mediaBlock(css, 400);
57 assert.match(block, /\.nav \.brand img \{ width: 28px; height: 28px; border-radius: 8px; \}/);
58 });
59
60 test("≤360px: hero install command keeps its copy control inside the bar", async () => {
61 const [css, page] = await Promise.all([
62 source("../styles/global.css"),
63 source("../pages/index.astro"),
64 ]);
65 const block = mediaBlock(css, 360);
66 assert.match(page, /class=\"install-command\">npm i -g reasonix<\/span>/);
67 assert.match(css, /\.hero-install \.install-command\s*\{[\s\S]*?min-width:\s*0;/);
68 assert.match(block, /\.hero-install \.install \{ gap: 6px; padding-inline: 6px; \}/);
69 assert.match(block, /\.hero-install \.install button \{ padding-inline: 10px; \}/);
70 });
71
72 test("≤440px: community nav budgets for the async account control", async () => {
73 const css = await source("../styles/community.css");
74 const layout = await source("../layouts/Community.astro");
75 const block = mediaBlock(css, 440);
76 assert.match(layout, /class="brand-name">Reasonix/);
77 assert.match(layout, /id="nav-account"/);
78 assert.match(block, /\.nav \.brand-name \{ display: none/);
79 assert.match(block, /\.nav-right \{ gap: 8px/);
80 assert.match(block, /\.lang-switch button \{ padding: 6px 8px/);
81 assert.match(block, /\.theme-switch button \{ padding: 6px 7px/);
82 assert.match(block, /#nav-account \.btn \{ padding: 7px 10px/);
83 });
84
85 test("shared headers expose an accessible mobile navigation", async () => {
86 const [header, community] = await Promise.all([
87 source("../components/SiteHeader.astro"),
88 source("../layouts/Community.astro"),
89 ]);
90 for (const sourceText of [header, community]) {
91 assert.match(sourceText, /data-mobile-nav-toggle/);
92 assert.match(sourceText, /data-mobile-nav/);
93 assert.match(sourceText, /data-mobile-nav-close/);
94 assert.match(sourceText, /aria-expanded="false"/);
95 }
96 });
97
98 test("mobile navigation behavior supports close, Escape, and focus cycling", async () => {
99 const js = await source("./mobile-nav.js");
100 assert.match(js, /event\.key === "Escape"/);
101 assert.match(js, /event\.key !== "Tab"/);
102 assert.match(js, /document\.body\.classList\.add\("mobile-nav-open"\)/);
103 assert.match(js, /document\.body\.classList\.remove\("mobile-nav-open"\)/);
104 });
105
105 lines Plain Text