033c9ec315
- vitest: dictionary parity, slug-map bijection, content-schema validation, JSON-LD (23) - playwright: both locales, language-switch mapping (incl. no-JS), CV downloads, form happy-path + honeypot, axe a11y on 5 templates x 2 themes (23) - fix: raise ink-faint + light accent to meet WCAG AA 4.5:1 (axe-verified) - fix: gate reveal animations behind html.js so content is visible without JS (progressive enhancement - was hidden at opacity 0); scan reduced-motion path - fix: validate contact form before the anti-bot time-trap (no false success) - chore: ESLint node globals, typed diagram props, resvg fontFiles, prettier pass Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
56 lines
2.3 KiB
TypeScript
56 lines
2.3 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
|
|
test.describe('CV downloads (J1)', () => {
|
|
test('both CV PDFs resolve with the right filenames', async ({ request }) => {
|
|
const en = await request.get('/cv/connor-babbington-cv-en.pdf');
|
|
const no = await request.get('/cv/connor-babbington-cv-no.pdf');
|
|
expect(en.status()).toBe(200);
|
|
expect(no.status()).toBe(200);
|
|
expect(en.headers()['content-type']).toContain('pdf');
|
|
});
|
|
|
|
test('header CV button defaults to the current locale', async ({ page }) => {
|
|
await page.goto('/no/');
|
|
const primary = page.locator('.cv-split a[download]').first();
|
|
await expect(primary).toHaveAttribute('href', /cv-no\.pdf$/);
|
|
});
|
|
});
|
|
|
|
test.describe('contact form (A9)', () => {
|
|
test('client validation blocks an empty submit', async ({ page }) => {
|
|
await page.goto('/contact/');
|
|
await page.locator('#contact-form button[type="submit"]').click();
|
|
await expect(page.locator('[data-error-for="name"]')).toBeVisible();
|
|
await expect(page.locator('[data-error-for="email"]')).toBeVisible();
|
|
});
|
|
|
|
test('happy path shows the success panel', async ({ page }) => {
|
|
await page.route('**/api/contact', (route) =>
|
|
route.fulfill({ status: 200, contentType: 'application/json', body: '{"ok":true}' }),
|
|
);
|
|
await page.goto('/contact/');
|
|
await page.fill('#cf-name', 'Martin');
|
|
await page.fill('#cf-email', 'martin@example.com');
|
|
await page.fill('#cf-message', 'We would like to talk to you about a role.');
|
|
await page.waitForTimeout(2100); // pass the time-trap
|
|
await page.locator('#contact-form button[type="submit"]').click();
|
|
await expect(page.locator('#form-success')).toBeVisible();
|
|
});
|
|
|
|
test('honeypot submission never calls the relay', async ({ page }) => {
|
|
let called = false;
|
|
await page.route('**/api/contact', (route) => {
|
|
called = true;
|
|
return route.fulfill({ status: 200, body: '{"ok":true}' });
|
|
});
|
|
await page.goto('/contact/');
|
|
await page.fill('#cf-name', 'Bot');
|
|
await page.fill('#cf-email', 'bot@spam.com');
|
|
await page.fill('#cf-message', 'spam');
|
|
await page.fill('#cf-company', 'ACME'); // honeypot
|
|
await page.locator('#contact-form button[type="submit"]').click();
|
|
await expect(page.locator('#form-success')).toBeVisible();
|
|
expect(called).toBe(false);
|
|
});
|
|
});
|