feat: download and self-host Google Fonts woff2 files
- Created scripts/download-fonts.py: downloads Google Fonts woff2 files (latin subset only) using urllib.request with Chrome User-Agent header
- Parses CSS response for latin @font-face blocks, extracts family/weight/style/URL metadata
- Downloads to src/fonts/ with naming convention {family}-{weight}{-italic}.woff2
- Created src/styles/fonts.css: 8 @font-face declarations for:
* Source Serif 4 (400, 600, 400-italic) — prose voice
* Inter (400, 500, 600, 700) — structure voice
* JetBrains Mono (400) — evidence voice
- All fonts use format('woff2'), font-display: swap, and relative URLs
- Fonts placed in src/fonts/ for Vite processing at build time
Generated with Amplifier
Co-Authored-By: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* Verification test for Self-Hosted Fonts (Task 3)
|
||||
*
|
||||
* Checks that:
|
||||
* 1. 8 woff2 font files exist in src/fonts/
|
||||
* 2. src/styles/fonts.css exists with 8 @font-face declarations
|
||||
* 3. Each @font-face has correct font-family, weight, style, src, font-display
|
||||
* 4. src URLs use relative paths to ../fonts/
|
||||
*/
|
||||
import { readFileSync, existsSync, statSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const root = join(__dirname, '..');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function assert(condition, message) {
|
||||
if (condition) {
|
||||
console.log(` ✓ ${message}`);
|
||||
passed++;
|
||||
} else {
|
||||
console.error(` ✗ ${message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
// === 1. Font files exist in src/fonts/ ===
|
||||
console.log('\n1. Font files exist in src/fonts/');
|
||||
const expectedFonts = [
|
||||
'source-serif-4-400.woff2',
|
||||
'source-serif-4-600.woff2',
|
||||
'source-serif-4-400-italic.woff2',
|
||||
'inter-400.woff2',
|
||||
'inter-500.woff2',
|
||||
'inter-600.woff2',
|
||||
'inter-700.woff2',
|
||||
'jetbrains-mono-400.woff2',
|
||||
];
|
||||
|
||||
for (const font of expectedFonts) {
|
||||
const fontPath = join(root, 'src/fonts', font);
|
||||
const exists = existsSync(fontPath);
|
||||
assert(exists, `src/fonts/${font} exists`);
|
||||
if (exists) {
|
||||
const stat = statSync(fontPath);
|
||||
assert(stat.size > 1000, `src/fonts/${font} is a real font file (${stat.size} bytes)`);
|
||||
}
|
||||
}
|
||||
|
||||
// === 2. fonts.css exists ===
|
||||
console.log('\n2. fonts.css exists');
|
||||
const fontsCssPath = join(root, 'src/styles/fonts.css');
|
||||
assert(existsSync(fontsCssPath), 'src/styles/fonts.css exists');
|
||||
|
||||
if (!existsSync(fontsCssPath)) {
|
||||
console.log('\nfonts.css not found, skipping remaining tests.');
|
||||
console.log(`\n${'='.repeat(40)}`);
|
||||
console.log(`Results: ${passed} passed, ${failed} failed out of ${passed + failed}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const css = readFileSync(fontsCssPath, 'utf8');
|
||||
|
||||
// === 3. Has 8 @font-face declarations ===
|
||||
console.log('\n3. @font-face count');
|
||||
const fontFaceBlocks = css.match(/@font-face\s*\{[^}]+\}/g);
|
||||
const fontFaceCount = fontFaceBlocks ? fontFaceBlocks.length : 0;
|
||||
assert(fontFaceCount === 8, `has exactly 8 @font-face declarations (found ${fontFaceCount})`);
|
||||
|
||||
// === 4. Source Serif 4 declarations (prose voice) ===
|
||||
console.log('\n4. Source Serif 4 declarations (prose voice)');
|
||||
// Regular 400
|
||||
assert(
|
||||
css.includes("font-family: 'Source Serif 4'") || css.includes('font-family: "Source Serif 4"'),
|
||||
"has font-family: 'Source Serif 4'"
|
||||
);
|
||||
// Check for 400 normal
|
||||
const ss4_400_block = fontFaceBlocks?.find(
|
||||
(b) =>
|
||||
(b.includes("'Source Serif 4'") || b.includes('"Source Serif 4"')) &&
|
||||
b.includes('font-weight: 400') &&
|
||||
b.includes('font-style: normal')
|
||||
);
|
||||
assert(!!ss4_400_block, 'Source Serif 4 400 normal declaration exists');
|
||||
// Check for 600 normal
|
||||
const ss4_600_block = fontFaceBlocks?.find(
|
||||
(b) =>
|
||||
(b.includes("'Source Serif 4'") || b.includes('"Source Serif 4"')) &&
|
||||
b.includes('font-weight: 600') &&
|
||||
b.includes('font-style: normal')
|
||||
);
|
||||
assert(!!ss4_600_block, 'Source Serif 4 600 normal declaration exists');
|
||||
// Check for 400 italic
|
||||
const ss4_400i_block = fontFaceBlocks?.find(
|
||||
(b) =>
|
||||
(b.includes("'Source Serif 4'") || b.includes('"Source Serif 4"')) &&
|
||||
b.includes('font-weight: 400') &&
|
||||
b.includes('font-style: italic')
|
||||
);
|
||||
assert(!!ss4_400i_block, 'Source Serif 4 400 italic declaration exists');
|
||||
|
||||
// === 5. Inter declarations (structure voice) ===
|
||||
console.log('\n5. Inter declarations (structure voice)');
|
||||
const interWeights = [400, 500, 600, 700];
|
||||
for (const weight of interWeights) {
|
||||
const interBlock = fontFaceBlocks?.find(
|
||||
(b) =>
|
||||
(b.includes("'Inter'") || b.includes('"Inter"')) &&
|
||||
b.includes(`font-weight: ${weight}`) &&
|
||||
b.includes('font-style: normal')
|
||||
);
|
||||
assert(!!interBlock, `Inter ${weight} normal declaration exists`);
|
||||
}
|
||||
|
||||
// === 6. JetBrains Mono declaration (evidence voice) ===
|
||||
console.log('\n6. JetBrains Mono declaration (evidence voice)');
|
||||
const jbBlock = fontFaceBlocks?.find(
|
||||
(b) =>
|
||||
(b.includes("'JetBrains Mono'") || b.includes('"JetBrains Mono"')) &&
|
||||
b.includes('font-weight: 400') &&
|
||||
b.includes('font-style: normal')
|
||||
);
|
||||
assert(!!jbBlock, 'JetBrains Mono 400 normal declaration exists');
|
||||
|
||||
// === 7. All declarations use format('woff2') ===
|
||||
console.log("\n7. All use format('woff2')");
|
||||
if (fontFaceBlocks) {
|
||||
for (const block of fontFaceBlocks) {
|
||||
assert(block.includes("format('woff2')"), `@font-face block uses format('woff2')`);
|
||||
}
|
||||
}
|
||||
|
||||
// === 8. All declarations use font-display: swap ===
|
||||
console.log('\n8. All use font-display: swap');
|
||||
if (fontFaceBlocks) {
|
||||
for (const block of fontFaceBlocks) {
|
||||
assert(block.includes('font-display: swap'), '@font-face block uses font-display: swap');
|
||||
}
|
||||
}
|
||||
|
||||
// === 9. All src URLs use relative paths to ../fonts/ ===
|
||||
console.log('\n9. src URLs use relative ../fonts/ paths');
|
||||
if (fontFaceBlocks) {
|
||||
for (const block of fontFaceBlocks) {
|
||||
assert(block.includes("url('../fonts/"), "@font-face src uses url('../fonts/...')");
|
||||
}
|
||||
}
|
||||
|
||||
// === 10. download-fonts.py script exists ===
|
||||
console.log('\n10. download-fonts.py script exists');
|
||||
const scriptPath = join(root, 'scripts/download-fonts.py');
|
||||
assert(existsSync(scriptPath), 'scripts/download-fonts.py exists');
|
||||
|
||||
if (existsSync(scriptPath)) {
|
||||
const script = readFileSync(scriptPath, 'utf8');
|
||||
assert(script.includes('urllib.request'), 'script uses urllib.request');
|
||||
assert(script.includes('User-Agent') || script.includes('user-agent'), 'script sets User-Agent header');
|
||||
assert(script.includes('woff2'), 'script references woff2 format');
|
||||
}
|
||||
|
||||
// === Summary ===
|
||||
console.log(`\n${'='.repeat(40)}`);
|
||||
console.log(`Results: ${passed} passed, ${failed} failed out of ${passed + failed}`);
|
||||
if (failed > 0) {
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('All font verification tests passed!');
|
||||
}
|
||||
Reference in New Issue
Block a user