diff --git a/docs/DOCS_INDEX.md b/docs/DOCS_INDEX.md index 1203102..863d9e8 100644 --- a/docs/DOCS_INDEX.md +++ b/docs/DOCS_INDEX.md @@ -161,7 +161,8 @@ npm run build # Build for production - ✅ ANSI color support (16 colors) - ✅ Image rendering via `cat` (HEAD-only fetch, inline `` output) - ✅ Full SEO: title, description, Open Graph, Twitter Card, JSON-LD, sitemap -- ✅ 83 comprehensive tests +- ✅ Custom Hack monospace font with fallbacks for privacy browsers +- ✅ 86 comprehensive tests (64 unit + 22 E2E) - ✅ TypeScript strict mode - ✅ Accessible (WCAG compliant) @@ -194,7 +195,8 @@ npm run build # Build for production - **Persist action** (async history saving in JS mode) - **API endpoints** (/api/files, form actions) - **Storage architecture** (KV vs cookies, size limits, fallback logic) -- Testing approach (83 tests, Vitest + Playwright) +- Testing approach (86 tests: 64 unit + 22 E2E, Vitest + Playwright) +- Font styling (Hack font with system fallbacks for privacy browsers) - Deployment process (Cloudflare Pages + KV setup) - File system operations (build-time file listing) - Security measures (sanitization, httpOnly cookies, CSRF protection) diff --git a/docs/TEST_COVERAGE.md b/docs/TEST_COVERAGE.md index 87260c3..8ec726c 100644 --- a/docs/TEST_COVERAGE.md +++ b/docs/TEST_COVERAGE.md @@ -117,7 +117,52 @@ Tests API endpoint for file listing: ### 6. terminal.test.ts (E2E) -End-to-end tests for full terminal workflows using Playwright. +End-to-end tests for full terminal workflows using Playwright: + +**Core Functionality**: + +- ✅ Terminal loading and greeting display +- ✅ Command execution (ping, help, echo, clear) +- ✅ Input focus management + +**File Operations**: + +- ✅ File listing (ls) +- ✅ File reading (cat) +- ✅ Usage errors and file not found +- ✅ Markdown link rendering with security attributes + +**UI Features**: + +- ✅ Theme toggle (dark/light mode) +- ✅ History persistence across reloads +- ✅ Clear history persistence +- ✅ Greeting link buttons remain clickable after overflow + +**Progressive Enhancement**: + +- ✅ No-JS functionality + +**ANSI Colors**: + +- ✅ Color command output with proper styling + +**Font Styling** (NEW): + +- ✅ Hack font with fallback stack +- ✅ Font loading verification +- ✅ Graceful fallback when fonts blocked + +**Error Handling**: + +- ✅ Unknown command errors +- ✅ Empty command handling + +**Edge cases covered:** + +- Font blocking (privacy browsers like Tor) +- Content overflow edge cases +- Form-based no-JS submissions ## Coverage Analysis diff --git a/e2e/terminal.test.ts b/e2e/terminal.test.ts index 4699970..2c3974e 100644 --- a/e2e/terminal.test.ts +++ b/e2e/terminal.test.ts @@ -269,6 +269,89 @@ test.describe('Terminal E2E Tests', () => { }); }); + test.describe('Font Styling', () => { + test('should use Hack font with fallbacks for terminal', async ({ page }) => { + await page.goto('/'); + await page.waitForLoadState('networkidle'); + + // Check input element has correct font family + const input = page.locator('input[type="text"]').first(); + const inputFont = await input.evaluate((el) => + window.getComputedStyle(el).getPropertyValue('font-family') + ); + + // Font family should include Hack as the primary font + expect(inputFont).toContain('Hack'); + + // Check fallback fonts are present + expect(inputFont).toContain('Consolas'); + expect(inputFont).toContain('Monaco'); + expect(inputFont).toContain('monospace'); + }); + + test('should load Hack font files', async ({ page }) => { + await page.goto('/'); + await page.waitForLoadState('networkidle'); + + // Wait for fonts to be loaded using the Font Loading API + const fontsLoaded = await page.evaluate(async () => { + // Wait for fonts to load + await document.fonts.ready; + + // Check if Hack font is available + const hackFontLoaded = document.fonts.check('16px Hack'); + return hackFontLoaded; + }); + + // Hack font should be loaded + expect(fontsLoaded).toBeTruthy(); + + // Verify the font is actually being used + const input = page.locator('input[type="text"]').first(); + const computedFont = await input.evaluate((el) => { + return window.getComputedStyle(el).getPropertyValue('font-family'); + }); + + expect(computedFont).toContain('Hack'); + }); + + test('should gracefully fallback when custom fonts blocked', async ({ browser }) => { + // Create context that blocks font loading + const context = await browser.newContext({ + permissions: [] + }); + const page = await context.newPage(); + + // Block all font file requests + await page.route('**/*.{woff,woff2}', (route) => route.abort()); + + await page.goto('/'); + await page.waitForLoadState('networkidle'); + + // Terminal should still be visible and functional + await expect(page.getByText("Type 'help' for commands")).toBeVisible(); + + const input = page.locator('input[type="text"]').first(); + await input.fill('ping'); + await input.press('Enter'); + await expect(page.getByText('pong')).toBeVisible(); + + // Font family should still have fallbacks + const inputFont = await input.evaluate((el) => + window.getComputedStyle(el).getPropertyValue('font-family') + ); + + // Should use fallback fonts (Consolas, Monaco, or monospace) + const hasFallback = + inputFont.includes('Consolas') || + inputFont.includes('Monaco') || + inputFont.includes('monospace'); + expect(hasFallback).toBeTruthy(); + + await context.close(); + }); + }); + test.describe('Input Focus', () => { test('should auto-focus input after command execution', async ({ page }) => { await page.goto('/'); diff --git a/src/lib/assets/fonts/hack/fonts/hack-bold-subset.woff b/src/lib/assets/fonts/hack/fonts/hack-bold-subset.woff new file mode 100644 index 0000000..a47c8aa Binary files /dev/null and b/src/lib/assets/fonts/hack/fonts/hack-bold-subset.woff differ diff --git a/src/lib/assets/fonts/hack/fonts/hack-bold-subset.woff2 b/src/lib/assets/fonts/hack/fonts/hack-bold-subset.woff2 new file mode 100644 index 0000000..93d425e Binary files /dev/null and b/src/lib/assets/fonts/hack/fonts/hack-bold-subset.woff2 differ diff --git a/src/lib/assets/fonts/hack/fonts/hack-bold.woff b/src/lib/assets/fonts/hack/fonts/hack-bold.woff new file mode 100644 index 0000000..368b913 Binary files /dev/null and b/src/lib/assets/fonts/hack/fonts/hack-bold.woff differ diff --git a/src/lib/assets/fonts/hack/fonts/hack-bold.woff2 b/src/lib/assets/fonts/hack/fonts/hack-bold.woff2 new file mode 100644 index 0000000..1155477 Binary files /dev/null and b/src/lib/assets/fonts/hack/fonts/hack-bold.woff2 differ diff --git a/src/lib/assets/fonts/hack/fonts/hack-bolditalic-subset.woff b/src/lib/assets/fonts/hack/fonts/hack-bolditalic-subset.woff new file mode 100644 index 0000000..0da4750 Binary files /dev/null and b/src/lib/assets/fonts/hack/fonts/hack-bolditalic-subset.woff differ diff --git a/src/lib/assets/fonts/hack/fonts/hack-bolditalic-subset.woff2 b/src/lib/assets/fonts/hack/fonts/hack-bolditalic-subset.woff2 new file mode 100644 index 0000000..236b7de Binary files /dev/null and b/src/lib/assets/fonts/hack/fonts/hack-bolditalic-subset.woff2 differ diff --git a/src/lib/assets/fonts/hack/fonts/hack-bolditalic.woff b/src/lib/assets/fonts/hack/fonts/hack-bolditalic.woff new file mode 100644 index 0000000..ce87fe2 Binary files /dev/null and b/src/lib/assets/fonts/hack/fonts/hack-bolditalic.woff differ diff --git a/src/lib/assets/fonts/hack/fonts/hack-bolditalic.woff2 b/src/lib/assets/fonts/hack/fonts/hack-bolditalic.woff2 new file mode 100644 index 0000000..46ff1c4 Binary files /dev/null and b/src/lib/assets/fonts/hack/fonts/hack-bolditalic.woff2 differ diff --git a/src/lib/assets/fonts/hack/fonts/hack-italic-subset.woff b/src/lib/assets/fonts/hack/fonts/hack-italic-subset.woff new file mode 100644 index 0000000..1d1f511 Binary files /dev/null and b/src/lib/assets/fonts/hack/fonts/hack-italic-subset.woff differ diff --git a/src/lib/assets/fonts/hack/fonts/hack-italic-subset.woff2 b/src/lib/assets/fonts/hack/fonts/hack-italic-subset.woff2 new file mode 100644 index 0000000..b6f5fc9 Binary files /dev/null and b/src/lib/assets/fonts/hack/fonts/hack-italic-subset.woff2 differ diff --git a/src/lib/assets/fonts/hack/fonts/hack-italic.woff b/src/lib/assets/fonts/hack/fonts/hack-italic.woff new file mode 100644 index 0000000..bd545e4 Binary files /dev/null and b/src/lib/assets/fonts/hack/fonts/hack-italic.woff differ diff --git a/src/lib/assets/fonts/hack/fonts/hack-italic.woff2 b/src/lib/assets/fonts/hack/fonts/hack-italic.woff2 new file mode 100644 index 0000000..1e7630c Binary files /dev/null and b/src/lib/assets/fonts/hack/fonts/hack-italic.woff2 differ diff --git a/src/lib/assets/fonts/hack/fonts/hack-regular-subset.woff b/src/lib/assets/fonts/hack/fonts/hack-regular-subset.woff new file mode 100644 index 0000000..85583a5 Binary files /dev/null and b/src/lib/assets/fonts/hack/fonts/hack-regular-subset.woff differ diff --git a/src/lib/assets/fonts/hack/fonts/hack-regular-subset.woff2 b/src/lib/assets/fonts/hack/fonts/hack-regular-subset.woff2 new file mode 100644 index 0000000..1e3abb9 Binary files /dev/null and b/src/lib/assets/fonts/hack/fonts/hack-regular-subset.woff2 differ diff --git a/src/lib/assets/fonts/hack/fonts/hack-regular.woff b/src/lib/assets/fonts/hack/fonts/hack-regular.woff new file mode 100644 index 0000000..e835381 Binary files /dev/null and b/src/lib/assets/fonts/hack/fonts/hack-regular.woff differ diff --git a/src/lib/assets/fonts/hack/fonts/hack-regular.woff2 b/src/lib/assets/fonts/hack/fonts/hack-regular.woff2 new file mode 100644 index 0000000..524465c Binary files /dev/null and b/src/lib/assets/fonts/hack/fonts/hack-regular.woff2 differ diff --git a/src/lib/assets/fonts/hack/hack-subset.css b/src/lib/assets/fonts/hack/hack-subset.css new file mode 100644 index 0000000..20ba245 --- /dev/null +++ b/src/lib/assets/fonts/hack/hack-subset.css @@ -0,0 +1,34 @@ +/*! + * Hack typeface https://github.com/source-foundry/Hack + * License: https://github.com/source-foundry/Hack/blob/master/LICENSE.md + */ +/* FONT PATHS + * -------------------------- */ +@font-face { + font-family: 'Hack'; + src: url('fonts/hack-regular-subset.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-regular-subset.woff?sha=3114f1256') format('woff'); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: 'Hack'; + src: url('fonts/hack-bold-subset.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-bold-subset.woff?sha=3114f1256') format('woff'); + font-weight: 700; + font-style: normal; +} + +@font-face { + font-family: 'Hack'; + src: url('fonts/hack-italic-subset.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-italic-webfont.woff?sha=3114f1256') format('woff'); + font-weight: 400; + font-style: italic; +} + +@font-face { + font-family: 'Hack'; + src: url('fonts/hack-bolditalic-subset.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-bolditalic-subset.woff?sha=3114f1256') format('woff'); + font-weight: 700; + font-style: italic; +} + diff --git a/src/lib/assets/fonts/hack/hack-subset.css.in b/src/lib/assets/fonts/hack/hack-subset.css.in new file mode 100644 index 0000000..b8f3885 --- /dev/null +++ b/src/lib/assets/fonts/hack/hack-subset.css.in @@ -0,0 +1,34 @@ +/*! + * Hack typeface https://github.com/source-foundry/Hack + * License: https://github.com/source-foundry/Hack/blob/master/LICENSE.md + */ +/* FONT PATHS + * -------------------------- */ +@font-face { + font-family: 'Hack'; + src: url('fonts/hack-regular-subset.woff2?sha={{ ink }}') format('woff2'), url('fonts/hack-regular-subset.woff?sha={{ ink }}') format('woff'); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: 'Hack'; + src: url('fonts/hack-bold-subset.woff2?sha={{ ink }}') format('woff2'), url('fonts/hack-bold-subset.woff?sha={{ ink }}') format('woff'); + font-weight: 700; + font-style: normal; +} + +@font-face { + font-family: 'Hack'; + src: url('fonts/hack-italic-subset.woff2?sha={{ ink }}') format('woff2'), url('fonts/hack-italic-webfont.woff?sha={{ ink }}') format('woff'); + font-weight: 400; + font-style: italic; +} + +@font-face { + font-family: 'Hack'; + src: url('fonts/hack-bolditalic-subset.woff2?sha={{ ink }}') format('woff2'), url('fonts/hack-bolditalic-subset.woff?sha={{ ink }}') format('woff'); + font-weight: 700; + font-style: italic; +} + diff --git a/src/lib/assets/fonts/hack/hack.css b/src/lib/assets/fonts/hack/hack.css new file mode 100644 index 0000000..df17027 --- /dev/null +++ b/src/lib/assets/fonts/hack/hack.css @@ -0,0 +1,34 @@ +/*! + * Hack typeface https://github.com/source-foundry/Hack + * License: https://github.com/source-foundry/Hack/blob/master/LICENSE.md + */ +/* FONT PATHS + * -------------------------- */ +@font-face { + font-family: 'Hack'; + src: url('fonts/hack-regular.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-regular.woff?sha=3114f1256') format('woff'); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: 'Hack'; + src: url('fonts/hack-bold.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-bold.woff?sha=3114f1256') format('woff'); + font-weight: 700; + font-style: normal; +} + +@font-face { + font-family: 'Hack'; + src: url('fonts/hack-italic.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-italic.woff?sha=3114f1256') format('woff'); + font-weight: 400; + font-style: italic; +} + +@font-face { + font-family: 'Hack'; + src: url('fonts/hack-bolditalic.woff2?sha=3114f1256') format('woff2'), url('fonts/hack-bolditalic.woff?sha=3114f1256') format('woff'); + font-weight: 700; + font-style: italic; +} + diff --git a/src/lib/assets/fonts/hack/hack.css.in b/src/lib/assets/fonts/hack/hack.css.in new file mode 100644 index 0000000..8b9f2dd --- /dev/null +++ b/src/lib/assets/fonts/hack/hack.css.in @@ -0,0 +1,34 @@ +/*! + * Hack typeface https://github.com/source-foundry/Hack + * License: https://github.com/source-foundry/Hack/blob/master/LICENSE.md + */ +/* FONT PATHS + * -------------------------- */ +@font-face { + font-family: 'Hack'; + src: url('fonts/hack-regular.woff2?sha={{ ink }}') format('woff2'), url('fonts/hack-regular.woff?sha={{ ink }}') format('woff'); + font-weight: 400; + font-style: normal; +} + +@font-face { + font-family: 'Hack'; + src: url('fonts/hack-bold.woff2?sha={{ ink }}') format('woff2'), url('fonts/hack-bold.woff?sha={{ ink }}') format('woff'); + font-weight: 700; + font-style: normal; +} + +@font-face { + font-family: 'Hack'; + src: url('fonts/hack-italic.woff2?sha={{ ink }}') format('woff2'), url('fonts/hack-italic.woff?sha={{ ink }}') format('woff'); + font-weight: 400; + font-style: italic; +} + +@font-face { + font-family: 'Hack'; + src: url('fonts/hack-bolditalic.woff2?sha={{ ink }}') format('woff2'), url('fonts/hack-bolditalic.woff?sha={{ ink }}') format('woff'); + font-weight: 700; + font-style: italic; +} + diff --git a/src/routes/layout.css b/src/routes/layout.css index 9056904..51d3720 100644 --- a/src/routes/layout.css +++ b/src/routes/layout.css @@ -1,4 +1,9 @@ @import 'tailwindcss'; +@import '../lib/assets/fonts/hack/hack.css'; + +@theme { + --font-mono: 'Hack', 'Consolas', 'Monaco', 'Courier New', 'Courier', monospace; +} @custom-variant dark (&:where(.dark, .dark *));