-
-
Notifications
You must be signed in to change notification settings - Fork 15
Expand file tree
/
Copy pathsite.test.mjs
More file actions
220 lines (196 loc) · 9.65 KB
/
Copy pathsite.test.mjs
File metadata and controls
220 lines (196 loc) · 9.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
import assert from "node:assert/strict";
import { access, readFile, readdir } from "node:fs/promises";
import { join, relative, resolve } from "node:path";
import { test } from "node:test";
const siteRoot = resolve(new URL("..", import.meta.url).pathname);
const outputRoot = join(siteRoot, "public");
const index = await readFile(join(outputRoot, "index.html"), "utf8");
const css = await readFile(join(outputRoot, "style.css"), "utf8");
const script = await readFile(join(outputRoot, "script.js"), "utf8");
const product = await readFile(join(siteRoot, "data/product.toml"), "utf8");
async function htmlFiles(directory) {
const entries = await readdir(directory, { withFileTypes: true });
const files = [];
for (const entry of entries) {
const path = join(directory, entry.name);
if (entry.isDirectory()) files.push(...await htmlFiles(path));
else if (entry.name.endsWith(".html")) files.push(path);
}
return files;
}
const pages = await htmlFiles(outputRoot);
function attrValues(html, attribute) {
return [...html.matchAll(new RegExp(`${attribute}="([^"]+)"`, "g"))].map((match) => match[1]);
}
function pngDimensions(buffer) {
assert.equal(buffer.toString("ascii", 1, 4), "PNG", "asset must be a PNG");
return {
width: buffer.readUInt32BE(16),
height: buffer.readUInt32BE(20),
};
}
test("renders a complete VocaMac homepage", () => {
assert.match(index, /<title>VocaMac: native voice typing for macOS<\/title>/);
assert.equal((index.match(/<h1\b/g) ?? []).length, 1);
assert.match(index, /<main id="main">/);
assert.match(index, /class="skip-link" href="#main"/);
assert.match(index, /<header[^>]+data-header/);
assert.match(index, /<footer class="site-footer">/);
});
test("keeps navigation and anchors accessible", () => {
assert.match(index, /data-nav-toggle[^>]+aria-expanded="false"[^>]+aria-controls="mobile-nav"/);
assert.match(index, /<nav[^>]+id="mobile-nav"[^>]+aria-label="Site"/);
const ids = [...index.matchAll(/\bid="([^"]+)"/g)].map((match) => match[1]);
assert.equal(new Set(ids).size, ids.length, "homepage IDs must be unique");
for (const target of attrValues(index, "href").filter((href) => href.startsWith("#"))) {
assert.ok(ids.includes(target.slice(1)), `missing in-page target ${target}`);
}
});
test("keeps the PRODUCT.md product boundary explicit", () => {
assert.match(index, /v0\.9\.0/);
assert.match(index, /macOS 14\+|macOS 14 Sonoma/);
assert.match(index, /Apple Silicon/);
assert.match(index, /WhisperKit/);
assert.match(index, /model downloads/i);
assert.match(index, /Beta/);
assert.match(index, /includes Parakeet/i);
assert.match(index, /Additional engines and models available in v0\.9\.0/i);
assert.match(index, /Parakeet/);
assert.match(index, /sherpa-onnx/);
assert.doesNotMatch(index, /Stable release/i);
assert.doesNotMatch(index, /macOS 13/);
assert.doesNotMatch(index, /Zero Network Calls/i);
assert.doesNotMatch(index, /100% Offline/i);
assert.doesNotMatch(index, /Works in All Apps/i);
assert.doesNotMatch(index, /99\+ Languages/i);
assert.doesNotMatch(index, /remove local models/i);
assert.match(product, /status = "Beta"/);
assert.match(product, /osShort = "macOS 14\+"/);
});
test("uses local assets and accurate social metadata", async () => {
for (const asset of ["/style.css", "/script.js", "/brand/voca-logo.svg", "/brand/paper-dots.svg", "/og-image.png", "/CNAME"]) {
await access(join(outputRoot, asset));
}
assert.equal((await readFile(join(outputRoot, "CNAME"), "utf8")).trim(), "vocamac.com");
assert.match(index, /rel="canonical" href="https:\/\/vocamac\.com\/"/);
assert.match(index, /property="og:image:secure_url"/);
assert.match(index, /property="og:image:type" content="image\/png"/);
assert.match(index, /property="og:image:width" content="1200"/);
assert.match(index, /property="og:image:height" content="630"/);
assert.deepEqual(pngDimensions(await readFile(join(outputRoot, "og-image.png"))), { width: 1200, height: 630 });
});
test("emits valid structured metadata", () => {
const jsonLd = index.match(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/)?.[1];
assert.ok(jsonLd, "homepage JSON-LD is present");
const structured = JSON.parse(jsonLd);
assert.equal(structured["@type"], "SoftwareApplication");
assert.equal(structured.softwareVersion, "0.9.0");
assert.equal(structured.processorRequirements, "Apple Silicon");
});
test("keeps the visual and privacy boundaries flat", () => {
const banned = ["linear-gradient", "radial-gradient", "conic-gradient", "backdrop-filter"];
for (const token of banned) assert.doesNotMatch(css, new RegExp(token, "i"), `unexpected ${token}`);
assert.match(css, /prefers-reduced-motion:\s*reduce/);
assert.match(css, /overflow-x:\s*hidden/);
assert.doesNotMatch(index, /googletagmanager|google-analytics|gtag\(/i);
assert.doesNotMatch(script, /api\.github\.com|fetch\(/i);
});
test("keeps Windows Chrome headlines on a real sans stack", () => {
assert.match(
css,
/--display:\s*"Avenir Next",\s*"Helvetica Neue",\s*ui-sans-serif,\s*system-ui,\s*"Segoe UI",\s*Arial,\s*sans-serif/,
);
assert.doesNotMatch(css, /SF Pro Display/);
assert.doesNotMatch(css, /@font-face/);
assert.doesNotMatch(css, /fonts\.google/);
assert.doesNotMatch(index, /fonts\.google|fonts\.gstatic|typekit|use\.typekit/i);
assert.match(css, /h1,\s*h2,\s*h3,\s*h4\s*\{[^}]*font-family:\s*var\(--display\)/);
assert.match(css, /h1,\s*h2,\s*h3,\s*h4\s*\{[^}]*font-weight:\s*760/);
assert.match(css, /h1,\s*h2,\s*h3,\s*h4\s*\{[^}]*letter-spacing:\s*-0?\.065em/);
assert.match(css, /h1 em,\s*h2 em,\s*h3 em,\s*h4 em\s*\{[^}]*font-family:\s*var\(--display\)/);
assert.match(css, /\.hero h1 em\s*\{[^}]*color:\s*var\(--teal\)/);
assert.match(css, /\.hero h1 em\s*\{[^}]*font-family:\s*var\(--display\)/);
assert.match(css, /\.hero h1 em\s*\{[^}]*font-style:\s*normal/);
assert.match(css, /\.hero h1\s*\{[^}]*font-size:\s*clamp\(3\.7rem,\s*10vw,\s*7\.4rem\)/);
assert.match(css, /\.hero h1\s*\{[^}]*font-weight:\s*780/);
assert.match(css, /\.hero h1\s*\{[^}]*letter-spacing:\s*-0?\.075em/);
assert.match(css, /\.hero h1\s*\{[^}]*line-height:\s*0?\.92/);
assert.match(css, /\.section-title\s*\{[^}]*font-size:\s*clamp\(2\.2rem,\s*6vw,\s*4\.7rem\)/);
assert.match(css, /\.section-title\s*\{[^}]*font-weight:\s*760/);
assert.match(css, /\.section-title\s*\{[^}]*letter-spacing:\s*-0?\.065em/);
assert.match(css, /\.section-title\s*\{[^}]*line-height:\s*1\.06/);
assert.match(index, /<h1 id="hero-title">Say it once\.<br><em>Your Mac types\.<\/em><\/h1>/);
});
test("keeps content available without javascript", () => {
assert.match(index, /<details[^>]+open/);
assert.match(index, /<summary>Does my voice leave my Mac\?<\/summary>/);
assert.match(index, /brew install --cask vocamac/);
assert.match(index, /Download v0\.9\.0 DMG/);
assert.match(script, /IntersectionObserver/);
assert.match(script, /setTimeout\(function \(\) \{ revealItems\.forEach\(reveal\); \}, 800\)/);
assert.match(script, /event\.key === "Escape"/);
});
test("every rendered page has one heading and image alternatives", async () => {
for (const page of pages) {
const html = page === join(outputRoot, "index.html") ? index : await readFile(page, "utf8");
assert.equal((html.match(/<h1\b/g) ?? []).length, 1, `${relative(outputRoot, page)} must have one h1`);
for (const image of html.matchAll(/<img\b[^>]*>/g)) {
// Hugo's minifier may serialize an empty decorative alt as a boolean
// attribute (`alt`); informative images retain their text value.
assert.match(image[0], /\balt(?:="[^"]*"|\b)/);
if (!image[0].includes('/brand/')) {
assert.match(image[0], /\bwidth="\d+"/);
assert.match(image[0], /\bheight="\d+"/);
assert.match(image[0], /\bloading="lazy"/);
}
}
}
});
test("keeps the site-audit copy and a11y fixes", async () => {
assert.match(index, /Selected on-device engine/);
assert.match(index, /WhisperKit\/CoreML, Parakeet, Apple Speech, ONNX/);
assert.doesNotMatch(
index,
/<span class="route-name">WhisperKit \/ CoreML<\/span>/,
);
const clipboard = await readFile(
join(outputRoot, "features/clipboard-preservation/index.html"),
"utf8",
);
assert.match(clipboard, /On-device transcription after model download/);
assert.doesNotMatch(clipboard, /Works offline/);
for (const page of pages) {
const rel = relative(outputRoot, page);
if (!rel.startsWith("features/") || rel === "features/index.html") continue;
const html = await readFile(page, "utf8");
assert.doesNotMatch(html, /aria-labelledby="feature-content-title"/);
assert.doesNotMatch(html, /id="feature-content-title"/);
}
assert.match(script, /aria-live", "polite"/);
assert.match(script, /announceCopy\("Copied"\)/);
assert.match(script, /copyFeedbackToken/);
assert.match(script, /clearTimeout\(copyFeedbackTimer\)/);
assert.match(script, /showCopyFeedback/);
assert.match(script, /announceCopy\("Press ⌘C"\)/);
const ogSvg = await readFile(join(siteRoot, "static/og-image.svg"), "utf8");
assert.match(ogSvg, /v0\.9\.0/);
assert.doesNotMatch(ogSvg, /v0\.8\.0/);
});
test("all rendered local references resolve", async () => {
const references = new Set();
for (const page of pages) {
const html = await readFile(page, "utf8");
for (const value of [...attrValues(html, "src"), ...attrValues(html, "href")]) {
if (!value.startsWith("/") || value.startsWith("/#") || value === "/") continue;
references.add(value.split("#")[0].split("?")[0]);
}
}
for (const reference of references) {
const file = join(outputRoot, reference);
try {
await access(file);
} catch {
await access(join(file, "index.html"));
}
}
});