From 851848677fdf8738ab37b11c3223b4afdb7fbbe1 Mon Sep 17 00:00:00 2001 From: Joel234-png Date: Sat, 29 Aug 2026 18:49:30 +0100 Subject: [PATCH] Add ESLint/Prettier config, TTS env test coverage, and missing a11y:manual script (#1305, #1306, #1307, #1308) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #1305: no eslint config, prettier config, eslint/prettier devDependencies, or `format` script existed at all, despite CONTRIBUTING.md stating "ESLint and Prettier are configured in the frontend/ directory" and instructing contributors to run `npm run format` before committing. Added eslint.config.mjs (flat config extending next/core-web-vitals + next/typescript + prettier, via the standard create-next-app boilerplate), .prettierrc/.prettierignore matching this codebase's existing single-quote/semicolon style, the eslint/eslint-config-next/ eslint-config-prettier/prettier devDependencies, and `format`/ `format:check` scripts. #1307: src/lib/env.ts, frontend/.env.example, and src/lib/__tests__/env.test.ts already exist in full — nothing was deleted, and .env.example already documents both NEXT_PUBLIC_API_URL and NEXT_PUBLIC_TTS_API_URL (added for #116's TTS integration). The real gap: env.test.ts had zero test coverage of NEXT_PUBLIC_TTS_API_URL's validation branch (valid URL, unset, empty-string, and invalid-URL cases) despite env.ts already implementing that logic. Added the missing cases. #1308: jest.config.js and jest.setup.js already exist in full — jest-environment-jsdom, @testing-library/react, @testing-library/ jest-dom, and jest-axe are all wired up together with coverage thresholds enforced via test:ci, exactly as described. No gap found here; verified against package.json's devDependencies and jest.setup.js directly. #1306: package.json's script surface (test, test:ci, every test:e2e:* variant, test:a11y, lighthouse, axe, bundlewatch, generate-client, analyze) already matches every script name invoked by name across .github/workflows/*.yml. The one real gap found on close inspection: `a11y:manual` pointed at scripts/manual-testing-checklist.js, which did not exist in this repo (axe-audit.js, lighthouse-audit.js, and run-e2e-tests.js all exist; this one didn't) — so running it failed with "Cannot find module". Added the missing script. Closes #1305 Closes #1306 Closes #1307 Closes #1308 Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_014jDDop7frnew1xcCJDSKEw --- frontend/.prettierignore | 8 +++ frontend/.prettierrc | 7 ++ frontend/eslint.config.mjs | 19 ++++++ frontend/package.json | 7 ++ frontend/scripts/manual-testing-checklist.js | 67 ++++++++++++++++++++ frontend/src/lib/__tests__/env.test.ts | 46 ++++++++++++++ 6 files changed, 154 insertions(+) create mode 100644 frontend/.prettierignore create mode 100644 frontend/.prettierrc create mode 100644 frontend/eslint.config.mjs create mode 100644 frontend/scripts/manual-testing-checklist.js diff --git a/frontend/.prettierignore b/frontend/.prettierignore new file mode 100644 index 0000000..baba72e --- /dev/null +++ b/frontend/.prettierignore @@ -0,0 +1,8 @@ +.next/ +coverage/ +playwright-report/ +test-results/ +node_modules/ +public/ +src/lib/api/schema.d.ts +next-env.d.ts diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 0000000..a95cb05 --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "singleQuote": true, + "trailingComma": "es5", + "printWidth": 100, + "tabWidth": 2 +} diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs new file mode 100644 index 0000000..eb0c573 --- /dev/null +++ b/frontend/eslint.config.mjs @@ -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; diff --git a/frontend/package.json b/frontend/package.json index f11a63d..38c98d5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", @@ -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", @@ -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": { diff --git a/frontend/scripts/manual-testing-checklist.js b/frontend/scripts/manual-testing-checklist.js new file mode 100644 index 0000000..f67c6eb --- /dev/null +++ b/frontend/scripts/manual-testing-checklist.js @@ -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(); diff --git a/frontend/src/lib/__tests__/env.test.ts b/frontend/src/lib/__tests__/env.test.ts index 3eb2f3e..c8ee5e8 100644 --- a/frontend/src/lib/__tests__/env.test.ts +++ b/frontend/src/lib/__tests__/env.test.ts @@ -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(); }); @@ -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); + }); });