Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/DOCS_INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,8 @@ npm run build # Build for production
- ✅ ANSI color support (16 colors)
- ✅ Image rendering via `cat` (HEAD-only fetch, inline `<img>` 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)

Expand Down Expand Up @@ -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)
Expand Down
47 changes: 46 additions & 1 deletion docs/TEST_COVERAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
83 changes: 83 additions & 0 deletions e2e/terminal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('/');
Expand Down
Binary file not shown.
Binary file not shown.
Binary file added src/lib/assets/fonts/hack/fonts/hack-bold.woff
Binary file not shown.
Binary file added src/lib/assets/fonts/hack/fonts/hack-bold.woff2
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added src/lib/assets/fonts/hack/fonts/hack-italic.woff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
34 changes: 34 additions & 0 deletions src/lib/assets/fonts/hack/hack-subset.css
Original file line number Diff line number Diff line change
@@ -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;
}

34 changes: 34 additions & 0 deletions src/lib/assets/fonts/hack/hack-subset.css.in
Original file line number Diff line number Diff line change
@@ -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;
}

34 changes: 34 additions & 0 deletions src/lib/assets/fonts/hack/hack.css
Original file line number Diff line number Diff line change
@@ -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;
}

34 changes: 34 additions & 0 deletions src/lib/assets/fonts/hack/hack.css.in
Original file line number Diff line number Diff line change
@@ -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;
}

5 changes: 5 additions & 0 deletions src/routes/layout.css
Original file line number Diff line number Diff line change
@@ -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 *));

Expand Down