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
8 changes: 8 additions & 0 deletions frontend/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.next/
coverage/
playwright-report/
test-results/
node_modules/
public/
src/lib/api/schema.d.ts
next-env.d.ts
7 changes: 7 additions & 0 deletions frontend/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 100,
"tabWidth": 2
}
19 changes: 19 additions & 0 deletions frontend/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { FlatCompat } from '@eslint/eslintrc';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const compat = new FlatCompat({
baseDirectory: __dirname,
});

const eslintConfig = [
...compat.extends('next/core-web-vitals', 'next/typescript', 'prettier'),
{
ignores: ['.next/**', 'coverage/**', 'playwright-report/**', 'test-results/**'],
},
];

export default eslintConfig;
7 changes: 7 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
"build": "npm run generate-client && next build",
"start": "next start",
"lint": "next lint",
"format": "prettier --write .",
"format:check": "prettier --check .",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
Expand Down Expand Up @@ -41,6 +43,7 @@
},
"devDependencies": {
"@axe-core/react": "^4.12.1",
"@eslint/eslintrc": "^3.2.0",
"@next/bundle-analyzer": "16.2.11",
"@playwright/test": "^1.61.1",
"@testing-library/dom": "^10.4.1",
Expand All @@ -53,12 +56,16 @@
"@types/react-dom": "^19.2.3",
"axe-core": "^4.12.1",
"bundlewatch": "^0.4.2",
"eslint": "^9.20.0",
"eslint-config-next": "16.2.11",
"eslint-config-prettier": "^9.1.0",
"jest": "^30.4.2",
"jest-axe": "^10.0.0",
"jest-environment-jsdom": "^30.4.1",
"lighthouse": "^13.4.1",
"openapi-typescript": "^7.0.0",
"pa11y": "^9.1.1",
"prettier": "^3.4.2",
"typescript": "^7.0.2"
},
"engines": {
Expand Down
67 changes: 67 additions & 0 deletions frontend/scripts/manual-testing-checklist.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/**
* Manual Accessibility Testing Checklist
*
* Automated tools (axe-core, Lighthouse, jest-axe) catch most WCAG 2.1 AA
* violations, but several categories require a human to actually try the
* page. This prints that checklist so `npm run a11y:manual` has something
* to run — it previously pointed at a script that didn't exist in this repo.
*/

const CHECKLIST = [
{
category: 'Keyboard navigation',
items: [
'Tab through the entire page — every interactive element is reachable and in a logical order',
'Focus is always visible (no invisible focus outlines)',
'No keyboard trap: focus can always move forward and backward out of any widget',
'Modals/dialogs trap focus while open and return it to the trigger on close',
'Skip-to-content link is the first focusable element and actually works',
],
},
{
category: 'Screen reader (VoiceOver / NVDA / JAWS)',
items: [
'Page landmarks (banner, main, navigation, contentinfo) are announced correctly',
'Headings form a logical, non-skipping hierarchy',
'Form fields announce their label, required state, and any validation error',
'Live regions (toasts, status updates) are announced without moving focus',
'Images convey their alt text (or are correctly marked decorative)',
],
},
{
category: 'Color & contrast',
items: [
'Text meets 4.5:1 contrast (3:1 for large text) in both light and dark mode',
'Information is never conveyed by color alone (e.g. status badges also use text/icons)',
'Focus indicators meet 3:1 contrast against their background',
],
},
{
category: 'Zoom & reflow',
items: [
'Page is usable at 200% browser zoom with no horizontal scroll or clipped content',
'Page is usable at 400% zoom in a 1280px viewport (WCAG 1.4.10 reflow)',
],
},
{
category: 'Motion & animation',
items: [
'prefers-reduced-motion is respected for any animated transitions',
'No content flashes more than 3 times per second',
],
},
];

function printChecklist() {
console.log('Manual Accessibility Testing Checklist (WCAG 2.1 AA)\n');
for (const { category, items } of CHECKLIST) {
console.log(category);
for (const item of items) {
console.log(` [ ] ${item}`);
}
console.log('');
}
console.log('Run alongside `npm run test:a11y`, `npm run lighthouse`, and `npm run axe`.');
}

printChecklist();
46 changes: 46 additions & 0 deletions frontend/src/lib/__tests__/env.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
describe('env validation', () => {
const original = process.env.NEXT_PUBLIC_API_URL;
const originalTts = process.env.NEXT_PUBLIC_TTS_API_URL;

afterEach(() => {
process.env.NEXT_PUBLIC_API_URL = original;
process.env.NEXT_PUBLIC_TTS_API_URL = originalTts;
jest.resetModules();
});

Expand Down Expand Up @@ -33,4 +35,48 @@ describe('env validation', () => {
});
}).toThrow(/must be a valid URL/);
});

// Issue #1307: TTS integration (#116) is now in scope, but no test
// exercised NEXT_PUBLIC_TTS_API_URL's validation branch at all.
it('parses a valid NEXT_PUBLIC_TTS_API_URL', () => {
process.env.NEXT_PUBLIC_API_URL = 'http://localhost:3001';
process.env.NEXT_PUBLIC_TTS_API_URL = 'http://localhost:3002';
let mod: typeof import('../env');
jest.isolateModules(() => {
mod = require('../env');
});
expect(mod!.getEnvConfig().NEXT_PUBLIC_TTS_API_URL).toBe('http://localhost:3002');
});

it('allows NEXT_PUBLIC_TTS_API_URL to be unset — TTS features are optional', () => {
process.env.NEXT_PUBLIC_API_URL = 'http://localhost:3001';
delete process.env.NEXT_PUBLIC_TTS_API_URL;
let mod: typeof import('../env');
expect(() => {
jest.isolateModules(() => {
mod = require('../env');
});
}).not.toThrow();
expect(mod!.getEnvConfig().NEXT_PUBLIC_TTS_API_URL).toBeUndefined();
});

it('allows NEXT_PUBLIC_TTS_API_URL to be an empty string', () => {
process.env.NEXT_PUBLIC_API_URL = 'http://localhost:3001';
process.env.NEXT_PUBLIC_TTS_API_URL = '';
expect(() => {
jest.isolateModules(() => {
require('../env');
});
}).not.toThrow();
});

it('throws when NEXT_PUBLIC_TTS_API_URL is set but not a valid URL', () => {
process.env.NEXT_PUBLIC_API_URL = 'http://localhost:3001';
process.env.NEXT_PUBLIC_TTS_API_URL = 'not-a-url';
expect(() => {
jest.isolateModules(() => {
require('../env');
});
}).toThrow(/NEXT_PUBLIC_TTS_API_URL.*must be a valid URL/s);
});
});