From 36bbfa0b168ecc12cd43884848f93c87a19202f4 Mon Sep 17 00:00:00 2001 From: Anettkva Date: Thu, 5 Nov 2026 12:45:50 +0100 Subject: [PATCH 1/3] refactor: move the pages to src/pages - Move the Elements page, the element page, Your gymnasts and the login page with their tests from src/components/ to src/pages/ (git mv) - Move the Calculator, About and not-found placeholders out of App.tsx - Import the pages from @/pages in App.tsx - Keep the page theme test checking the two moved pages - Describe src/pages, src/components, src/components/ui and src/lib in the README, and update the links in the docs Closes #97 Co-Authored-By: Claude Opus 5.5 --- README.md | 52 ++++++++++--------- docs/ACCESSIBILITY.md | 6 +-- docs/AI_USAGE.md | 6 +++ docs/AUTHENTICATION.md | 2 +- docs/SUSTAINABILITY.md | 4 +- src/App.tsx | 34 +++--------- src/pages/AboutPage.tsx | 11 ++++ src/pages/CalculatorPage.tsx | 11 ++++ .../ElementDetailPage.test.tsx | 0 .../elements => pages}/ElementDetailPage.tsx | 0 .../elements => pages}/ElementsPage.test.tsx | 0 .../elements => pages}/ElementsPage.tsx | 0 .../auth => pages}/LoginPage.test.tsx | 2 +- src/{components/auth => pages}/LoginPage.tsx | 2 +- src/pages/NotFoundPage.tsx | 11 ++++ .../YourGymnastsPage.test.tsx | 0 .../gymnasts => pages}/YourGymnastsPage.tsx | 0 src/styles/gymnastTheme.test.ts | 11 ++-- 18 files changed, 91 insertions(+), 61 deletions(-) create mode 100644 src/pages/AboutPage.tsx create mode 100644 src/pages/CalculatorPage.tsx rename src/{components/elements => pages}/ElementDetailPage.test.tsx (100%) rename src/{components/elements => pages}/ElementDetailPage.tsx (100%) rename src/{components/elements => pages}/ElementsPage.test.tsx (100%) rename src/{components/elements => pages}/ElementsPage.tsx (100%) rename src/{components/auth => pages}/LoginPage.test.tsx (99%) rename src/{components/auth => pages}/LoginPage.tsx (99%) create mode 100644 src/pages/NotFoundPage.tsx rename src/{components/gymnasts => pages}/YourGymnastsPage.test.tsx (100%) rename src/{components/gymnasts => pages}/YourGymnastsPage.tsx (100%) diff --git a/README.md b/README.md index a92133c..84b577a 100644 --- a/README.md +++ b/README.md @@ -21,18 +21,18 @@ The calculator and About pages are placeholders. Adding items from the catalog t ## What to review -| Review | What to look at | -| -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | -| [Running application](http://it2810-29.idi.ntnu.no/project2) | Search, load more, element details, and the Your gymnasts test login | -| [ElementsPage tests](src/components/elements/ElementsPage.test.tsx) | Search, result paging, URL state, loading, and error behavior | -| [Element detail tests](src/components/elements/ElementDetailPage.test.tsx) | Catalog detail rendering and error/loading states | -| [Gymnast page tests](src/components/gymnasts/YourGymnastsPage.test.tsx) | Test login, loading, profile creation, and error handling | -| [Backend catalog tests](backend/src/catalog/search.test.ts) | Search, filters, sorting, pagination, and count limits | -| [Backend server tests](backend/src/server/server.test.ts) | GraphQL API behavior; database-backed tests require a migrated and seeded database | -| [docs/API.md](docs/API.md) | API design, schema, search, pagination, authentication, and errors | -| [docs/AI_USAGE.md](docs/AI_USAGE.md) | How AI was used and how the work was reviewed | -| [docs/ACCESSIBILITY.md](docs/ACCESSIBILITY.md) | Accessibility choices, testing, and known open points | -| [docs/SUSTAINABILITY.md](docs/SUSTAINABILITY.md) | Sustainable design choices, measurements, and what remains unmeasured | +| Review | What to look at | +| ------------------------------------------------------------ | ---------------------------------------------------------------------------------- | +| [Running application](http://it2810-29.idi.ntnu.no/project2) | Search, load more, element details, and the Your gymnasts test login | +| [ElementsPage tests](src/pages/ElementsPage.test.tsx) | Search, result paging, URL state, loading, and error behavior | +| [Element detail tests](src/pages/ElementDetailPage.test.tsx) | Catalog detail rendering and error/loading states | +| [Gymnast page tests](src/pages/YourGymnastsPage.test.tsx) | Test login, loading, profile creation, and error handling | +| [Backend catalog tests](backend/src/catalog/search.test.ts) | Search, filters, sorting, pagination, and count limits | +| [Backend server tests](backend/src/server/server.test.ts) | GraphQL API behavior; database-backed tests require a migrated and seeded database | +| [docs/API.md](docs/API.md) | API design, schema, search, pagination, authentication, and errors | +| [docs/AI_USAGE.md](docs/AI_USAGE.md) | How AI was used and how the work was reviewed | +| [docs/ACCESSIBILITY.md](docs/ACCESSIBILITY.md) | Accessibility choices, testing, and known open points | +| [docs/SUSTAINABILITY.md](docs/SUSTAINABILITY.md) | Sustainable design choices, measurements, and what remains unmeasured | ### How to try it @@ -219,12 +219,12 @@ fetchMore({ variables: { after: data.searchCatalog.pageInfo.endCursor } }) - **Load more with `fetchMore`.** The cache keeps all the pages of one search in one list (the key is the search, filter and sort, not `first` or `after`). A query with `after` that is sent with `useQuery` or `client.query` is answered from the cache. `fetchMore` always asks the server, so use it for the next page. - **`totalCount` only with the first page:** the first-page query asks for the count, and later-page queries omit it; the cache keeps the count when those pages are loaded. -- **Authentication.** Session handling (`devLogin`, `logout`, and `restoreSession`) is in `src/lib/apollo/client.ts`. The development test login accepts any email address and is the only login flow planned; do not use real or sensitive data with it. See [Test login on the virtual machine](docs/VM_SETUP.md#6-test-login-on-the-virtual-machine) and [Login for development](docs/API.md#login-for-development). The login page (`src/components/auth/LoginPage.tsx`) offers the demo coach and a test coach of your own. The app reads who is logged in in one place, `useAuth()` (`src/components/auth/useAuth.ts`), which the menu, the protected pages and the gymnasts page share. +- **Authentication.** Session handling (`devLogin`, `logout`, and `restoreSession`) is in `src/lib/apollo/client.ts`. The development test login accepts any email address and is the only login flow planned; do not use real or sensitive data with it. See [Test login on the virtual machine](docs/VM_SETUP.md#6-test-login-on-the-virtual-machine) and [Login for development](docs/API.md#login-for-development). The login page (`src/pages/LoginPage.tsx`) offers the demo coach and a test coach of your own. The app reads who is logged in in one place, `useAuth()` (`src/components/auth/useAuth.ts`), which the menu, the protected pages and the gymnasts page share. - **Errors.** `describeError(error)` (`src/lib/apollo/errors.ts`) turns an error from the API or the network into a message for the user, and says whether trying again can help. ## The Elements page: search, filters and sort -The Elements page (`/elements`, `src/components/elements/ElementsPage.tsx`) is where a coach searches the catalog. It sends one `searchCatalog` request with the search text, the filters and the sort, and shows the result as a table (cards on a phone) with "Load more elements". The search, the filters and the sort are all in the address (`?search=salto&apparatus=UB&group=UB-6&difficulty=A,B&sort=difficulty&dir=desc`), so a result can be shared and the back button works. +The Elements page (`/elements`, `src/pages/ElementsPage.tsx`) is where a coach searches the catalog. It sends one `searchCatalog` request with the search text, the filters and the sort, and shows the result as a table (cards on a phone) with "Load more elements". The search, the filters and the sort are all in the address (`?search=salto&apparatus=UB&group=UB-6&difficulty=A,B&sort=difficulty&dir=desc`), so a result can be shared and the back button works. ### What the user can do @@ -253,7 +253,7 @@ The Elements page (`/elements`, `src/components/elements/ElementsPage.tsx`) is w ### Testing -- `src/components/elements/ElementsPage.test.tsx` runs the page on the real Apollo Client with a stub for `fetch` that answers like the server: each filter, several values, the address in both directions, the disabled filters, clear, the sort list, the filters and the sort together, a wrong address, a structure that fails, and "Load more" with a filter. Each behavior was broken on purpose to check that a test fails without it. +- `src/pages/ElementsPage.test.tsx` runs the page on the real Apollo Client with a stub for `fetch` that answers like the server: each filter, several values, the address in both directions, the disabled filters, clear, the sort list, the filters and the sort together, a wrong address, a structure that fails, and "Load more" with a filter. Each behavior was broken on purpose to check that a test fails without it. - The backend tests (`backend/src/catalog/search.test.ts`, `backend/src/server/server.test.ts`) cover the vault filter, the range of the values and `Apparatus.difficultyValues`, and need the seeded database like the rest. - Not everything can be tested in jsdom (for example the tooltip, which Base UI opens from real pointer events). Those parts were checked in a real browser against the real backend. @@ -585,15 +585,19 @@ Settings, all optional, as environment variables: ## Project structure -| Folder | Contents | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| `backend/` | The backend package: TypeScript, Prisma, the seed script, the catalog search, the GraphQL server and the GraphQL schema | -| `data/` | The catalog data extracted from the Code of Points: JSON files and one image per element | -| `docs/` | Project plans, data model, API, accessibility, sustainability, AI usage, and catalog extraction documentation | -| `e2e/` | The end-to-end tests (Playwright): whole flows in a real browser, accessibility checks and the API over HTTP. See "End-to-end tests" | -| `public/` | Static files served as they are, such as the favicon | -| `scripts/extract-cop/` | The Python script that extracts `data/` from the Code of Points PDF | -| `src/` | The React frontend, shared UI components, API operations, Apollo Client, and frontend tests | +| Folder | Contents | +| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | +| `backend/` | The backend package: TypeScript, Prisma, the seed script, the catalog search, the GraphQL server and the GraphQL schema | +| `data/` | The catalog data extracted from the Code of Points: JSON files and one image per element | +| `docs/` | Project plans, data model, API, accessibility, sustainability, AI usage, and catalog extraction documentation | +| `e2e/` | The end-to-end tests (Playwright): whole flows in a real browser, accessibility checks and the API over HTTP. See "End-to-end tests" | +| `public/` | Static files served as they are, such as the favicon | +| `scripts/extract-cop/` | The Python script that extracts `data/` from the Code of Points PDF | +| `src/` | The React frontend, shared UI components, API operations, Apollo Client, and frontend tests | +| `src/pages/` | One file for each route in `src/App.tsx` (the Elements page, the element page, Your gymnasts, Log in and the placeholder pages), with its tests | +| `src/components/` | The parts of the pages, in one folder for each part of the site (`elements/`, `gymnasts/`, `auth/`, `navbar/`) | +| `src/components/ui/` | The shadcn/ui components (written by the shadcn/ui CLI) and a few small shared components (`DifficultyBadge`, `ElementValueBadge`, `ElementVariants`) | +| `src/lib/` | The GraphQL operations, Apollo Client and the logic that is not a component | ## Working in this repository diff --git a/docs/ACCESSIBILITY.md b/docs/ACCESSIBILITY.md index b4e4ad8..33b8ea6 100644 --- a/docs/ACCESSIBILITY.md +++ b/docs/ACCESSIBILITY.md @@ -23,19 +23,19 @@ The “Skip to main content” link is visually hidden until focused. The app us - The desktop results table has a screen-reader caption, column headers, and `scope="col"` relationships. On narrow screens the results are presented as cards instead. - Disabled actions for removed catalog items reference the explanatory text with `aria-describedby`. -**Where:** [ElementsPage.tsx](../src/components/elements/ElementsPage.tsx), [ResultsTable.tsx](../src/components/elements/ResultsTable.tsx), [ElementActions.tsx](../src/components/elements/ElementActions.tsx). +**Where:** [ElementsPage.tsx](../src/pages/ElementsPage.tsx), [ResultsTable.tsx](../src/components/elements/ResultsTable.tsx), [ElementActions.tsx](../src/components/elements/ElementActions.tsx). ### Status messages and errors Dynamic updates such as loading, result counts, and action feedback use status semantics (`role="status"` and, where needed, `aria-live`). Errors use alert semantics so important feedback is exposed to assistive technology. -**Where:** [ElementsPage.tsx](../src/components/elements/ElementsPage.tsx), [ElementActions.tsx](../src/components/elements/ElementActions.tsx), [ElementDetailPage.tsx](../src/components/elements/ElementDetailPage.tsx), and [YourGymnastsPage.tsx](../src/components/gymnasts/YourGymnastsPage.tsx). +**Where:** [ElementsPage.tsx](../src/pages/ElementsPage.tsx), [ElementActions.tsx](../src/components/elements/ElementActions.tsx), [ElementDetailPage.tsx](../src/pages/ElementDetailPage.tsx), and [YourGymnastsPage.tsx](../src/pages/YourGymnastsPage.tsx). ### Themes, contrast, and reduced motion The interface supports light and dark themes with separate color tokens. Interactive controls and links include visible focus styling. Animations and transitions use reduced-motion variants so they can be suppressed when the user requests reduced motion. -**Where:** theme state in [App.tsx](../src/App.tsx), theme colors in [variables.css](../src/styles/variables.css), and reduced-motion styles in [ElementsPage.tsx](../src/components/elements/ElementsPage.tsx), [ResultsTable.tsx](../src/components/elements/ResultsTable.tsx), and [ElementDetailPage.tsx](../src/components/elements/ElementDetailPage.tsx). +**Where:** theme state in [App.tsx](../src/App.tsx), theme colors in [variables.css](../src/styles/variables.css), and reduced-motion styles in [ElementsPage.tsx](../src/pages/ElementsPage.tsx), [ResultsTable.tsx](../src/components/elements/ResultsTable.tsx), and [ElementDetailPage.tsx](../src/pages/ElementDetailPage.tsx). The color themes have not yet been comprehensively measured against WCAG contrast criteria. Focus, contrast, zoom, and screen-reader behavior need verification in the complete application. diff --git a/docs/AI_USAGE.md b/docs/AI_USAGE.md index b1b9990..38babd8 100644 --- a/docs/AI_USAGE.md +++ b/docs/AI_USAGE.md @@ -526,3 +526,9 @@ - **Tool:** Claude Code - **What the team did:** Found in a review that the element page shows the long description of a vault group under "Element group", and wrote issue #125: show "Group N" for a vault, with the same rule as the Elements list. - **What Claude did:** Added `number` (and `id`, so Apollo's cache can recognise the group across queries) to the `catalogItem` query, and let the element page use `groupLabel` from the Elements list, so the two pages cannot disagree. Wrote one test for an element and one for a vault (with the real group of VT-1.22-a), and checked that each fails when the page shows the name for every item or "Group N" for every item. After a review of its own work, split one test that checked two cases, and replaced test data for the vault that was half an element. + +## The pages in `src/pages/` (`src/pages/`, `src/App.tsx`) + +- **Tool:** Claude Code +- **What the team did:** Wrote issue #97: move the pages out of `src/components/`, so it is clear which files are routes and which are parts of pages, and describe the folders in the README. +- **What Claude did:** Moved the four pages and their tests with `git mv` (so their history follows them), and moved the three placeholder pages out of `App.tsx` into their own files, so every route has a file in `src/pages/`. Updated the imports, the links in the README and the docs, and the "Project structure" table. Found that `src/styles/gymnastTheme.test.ts` reads the files of the gymnasts and login pages by folder, so after the move it would have stopped checking the two pages without failing; added them to the list. The shared components in `components/ui/` were left where they are (see the issue). diff --git a/docs/AUTHENTICATION.md b/docs/AUTHENTICATION.md index 64e514d..8915ef5 100644 --- a/docs/AUTHENTICATION.md +++ b/docs/AUTHENTICATION.md @@ -123,7 +123,7 @@ With https and a real login, in this order of importance: | -------------------------------- | ------------------------------------------------------------------------ | | The question `me` | `src/lib/authOperations.ts` | | The provider and the hook | `src/components/auth/AuthProvider.tsx`, `src/components/auth/useAuth.ts` | -| The login page | `src/components/auth/LoginPage.tsx` | +| The login page | `src/pages/LoginPage.tsx` | | The test login form | `src/components/gymnasts/DevLoginButton.tsx` | | The protected pages | `src/components/ProtectedRoute.tsx`, `src/App.tsx` | | The menu | `src/components/navbar/Navbar.tsx` | diff --git a/docs/SUSTAINABILITY.md b/docs/SUSTAINABILITY.md index 3ea6241..3900b4c 100644 --- a/docs/SUSTAINABILITY.md +++ b/docs/SUSTAINABILITY.md @@ -14,7 +14,7 @@ With GraphQL, each operation selects the fields its page uses instead of downloa The search waits **300 ms after the user stops typing** before updating the address and sending the query. It requests **20 results per page**, with a “Load more” action for additional results, rather than downloading the whole catalog at once. This avoids requests for every keystroke and limits the initial response. -**Where:** `SEARCH_DELAY_MS` and `PAGE_SIZE` in [searchOperations.ts](../src/lib/searchOperations.ts); the debounce, initial query, and “Load more” behavior in [ElementsPage.tsx](../src/components/elements/ElementsPage.tsx). +**Where:** `SEARCH_DELAY_MS` and `PAGE_SIZE` in [searchOperations.ts](../src/lib/searchOperations.ts); the debounce, initial query, and “Load more” behavior in [ElementsPage.tsx](../src/pages/ElementsPage.tsx). ### Bound the cost of counting results @@ -32,7 +32,7 @@ Catalog drawings use WebP. Result-card images are lazy-loaded and decoded asynch The catalog JSON is no longer imported into the frontend to build the element detail view; the page fetches the selected item from the GraphQL API instead. The project history records the JavaScript bundle changing from **665 kB to 522 kB** after this move. On the current version, `npm run build` reports a JavaScript bundle of **548.06 kB (170.79 kB gzip)**. These are build-output sizes, not per-visit measurements; the historical before/after comparison and current build can differ because the code and build tooling have changed. -**Where:** the API-backed detail query in [catalogOperations.ts](../src/lib/catalogOperations.ts) and its use in [ElementDetailPage.tsx](../src/components/elements/ElementDetailPage.tsx); the historical comparison is recorded in [AI_USAGE.md](./AI_USAGE.md). +**Where:** the API-backed detail query in [catalogOperations.ts](../src/lib/catalogOperations.ts) and its use in [ElementDetailPage.tsx](../src/pages/ElementDetailPage.tsx); the historical comparison is recorded in [AI_USAGE.md](./AI_USAGE.md). ### Reuse cached search results diff --git a/src/App.tsx b/src/App.tsx index fdc7b72..5fec1fd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,38 +4,20 @@ import { Navbar } from './components/navbar/Navbar'; import { SkipLink } from './components/skipLink/SkipLink'; import { ProtectedRoute } from './components/ProtectedRoute'; import { AuthProvider } from './components/auth/AuthProvider'; -import LoginPage from './components/auth/LoginPage'; -import PageTitle from './components/PageTitle'; -import ElementDetailPage from './components/elements/ElementDetailPage'; -import ElementsPage from './components/elements/ElementsPage'; -import YourGymnastsPage from './components/gymnasts/YourGymnastsPage'; +// One page for each route (src/pages/). The parts of the pages are in src/components/. +import AboutPage from '@/pages/AboutPage'; +import CalculatorPage from '@/pages/CalculatorPage'; +import ElementDetailPage from '@/pages/ElementDetailPage'; +import ElementsPage from '@/pages/ElementsPage'; +import LoginPage from '@/pages/LoginPage'; +import NotFoundPage from '@/pages/NotFoundPage'; +import YourGymnastsPage from '@/pages/YourGymnastsPage'; import './index.css'; import './styles/variables.css'; import './styles/global.css'; -// Placeholder pages for routes without full implementations yet. Each page -// has its own
, like the real pages. -const CalculatorPage = () => ( -
- Calculator -

Calculator

-
-); -const AboutPage = () => ( -
- About -

About

-
-); -const NotFoundPage = () => ( -
- Page not found -

404 - Page not found

-
-); - export function App() { const [isDarkMode, setIsDarkMode] = useState( () => window.localStorage.getItem('theme') === 'dark', diff --git a/src/pages/AboutPage.tsx b/src/pages/AboutPage.tsx new file mode 100644 index 0000000..6085613 --- /dev/null +++ b/src/pages/AboutPage.tsx @@ -0,0 +1,11 @@ +import PageTitle from '@/components/PageTitle' + +// A placeholder until the page is built. It has its own
, like the real pages. +export default function AboutPage() { + return ( +
+ About +

About

+
+ ) +} diff --git a/src/pages/CalculatorPage.tsx b/src/pages/CalculatorPage.tsx new file mode 100644 index 0000000..61585e3 --- /dev/null +++ b/src/pages/CalculatorPage.tsx @@ -0,0 +1,11 @@ +import PageTitle from '@/components/PageTitle' + +// A placeholder until the page is built. It has its own
, like the real pages. +export default function CalculatorPage() { + return ( +
+ Calculator +

Calculator

+
+ ) +} diff --git a/src/components/elements/ElementDetailPage.test.tsx b/src/pages/ElementDetailPage.test.tsx similarity index 100% rename from src/components/elements/ElementDetailPage.test.tsx rename to src/pages/ElementDetailPage.test.tsx diff --git a/src/components/elements/ElementDetailPage.tsx b/src/pages/ElementDetailPage.tsx similarity index 100% rename from src/components/elements/ElementDetailPage.tsx rename to src/pages/ElementDetailPage.tsx diff --git a/src/components/elements/ElementsPage.test.tsx b/src/pages/ElementsPage.test.tsx similarity index 100% rename from src/components/elements/ElementsPage.test.tsx rename to src/pages/ElementsPage.test.tsx diff --git a/src/components/elements/ElementsPage.tsx b/src/pages/ElementsPage.tsx similarity index 100% rename from src/components/elements/ElementsPage.tsx rename to src/pages/ElementsPage.tsx diff --git a/src/components/auth/LoginPage.test.tsx b/src/pages/LoginPage.test.tsx similarity index 99% rename from src/components/auth/LoginPage.test.tsx rename to src/pages/LoginPage.test.tsx index 31d07da..e1d45e9 100644 --- a/src/components/auth/LoginPage.test.tsx +++ b/src/pages/LoginPage.test.tsx @@ -11,7 +11,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { AuthContext, type AuthState } from '@/components/auth/useAuth' import { devLogin } from '@/lib/apollo/client' import { ApiError } from '@/lib/apollo/api' -import { DEMO_EMAIL } from '../../../shared/demo' +import { DEMO_EMAIL } from '../../shared/demo' import LoginPage from './LoginPage' // The real function would call the server. The page only has to call devLogin. diff --git a/src/components/auth/LoginPage.tsx b/src/pages/LoginPage.tsx similarity index 99% rename from src/components/auth/LoginPage.tsx rename to src/pages/LoginPage.tsx index c60e17d..39bc96a 100644 --- a/src/components/auth/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -10,7 +10,7 @@ import { devLogin } from '@/lib/apollo/client' import { describeError } from '@/lib/apollo/errors' import { isDevLoginEnabled } from '@/lib/devLogin' import { isInsecureConnection } from '@/lib/insecureConnection' -import { DEMO_EMAIL } from '../../../shared/demo' +import { DEMO_EMAIL } from '../../shared/demo' /** Where the visitor goes after logging in when they did not come from a protected page. */ const DEFAULT_TARGET = '/gymnasts' diff --git a/src/pages/NotFoundPage.tsx b/src/pages/NotFoundPage.tsx new file mode 100644 index 0000000..a5f8c8d --- /dev/null +++ b/src/pages/NotFoundPage.tsx @@ -0,0 +1,11 @@ +import PageTitle from '@/components/PageTitle' + +// The page for an address that does not exist. It has its own
, like the real pages. +export default function NotFoundPage() { + return ( +
+ Page not found +

404 - Page not found

+
+ ) +} diff --git a/src/components/gymnasts/YourGymnastsPage.test.tsx b/src/pages/YourGymnastsPage.test.tsx similarity index 100% rename from src/components/gymnasts/YourGymnastsPage.test.tsx rename to src/pages/YourGymnastsPage.test.tsx diff --git a/src/components/gymnasts/YourGymnastsPage.tsx b/src/pages/YourGymnastsPage.tsx similarity index 100% rename from src/components/gymnasts/YourGymnastsPage.tsx rename to src/pages/YourGymnastsPage.tsx diff --git a/src/styles/gymnastTheme.test.ts b/src/styles/gymnastTheme.test.ts index c0ad01e..95f038a 100644 --- a/src/styles/gymnastTheme.test.ts +++ b/src/styles/gymnastTheme.test.ts @@ -203,12 +203,16 @@ describe('the components of the gymnasts and login pages', () => { '--accent-color', ] - const files = ['../components/gymnasts/', '../components/auth/'].flatMap( - (dir) => + // The two pages (src/pages/) and the components they use. + const files = [ + '../pages/YourGymnastsPage.tsx', + '../pages/LoginPage.tsx', + ...['../components/gymnasts/', '../components/auth/'].flatMap((dir) => readdirSync(here(dir)) .filter((name) => name.endsWith('.tsx') && !name.endsWith('.test.tsx')) .map((name) => dir + name), - ) + ), + ] it.each(files)( '%s: a button with the outline variant also sets its colors for dark mode', @@ -231,6 +235,7 @@ describe('the components of the gymnasts and login pages', () => { it('looks at the components of both pages and not at their tests', () => { expect(files.some((f) => f.endsWith('GymnastRow.tsx'))).toBe(true) + expect(files.some((f) => f.endsWith('YourGymnastsPage.tsx'))).toBe(true) expect(files.some((f) => f.endsWith('LoginPage.tsx'))).toBe(true) expect(files.some((f) => f.endsWith('.test.tsx'))).toBe(false) }) From b65ef3e1bb4962217f967c0ac75127807473cf5a Mon Sep 17 00:00:00 2001 From: Anettkva Date: Thu, 5 Nov 2026 13:11:48 +0100 Subject: [PATCH 2/3] refactor: move the frontend tests to src/tests - Move all 29 frontend tests with git mv into src/tests/, in the same folders as the code they test - Import what they test with the @/ alias instead of relative paths - Read the theme files from the new place in gymnastTheme.test.ts - Describe src/tests in the README, and update the links to the tests Part of #97 Co-Authored-By: Claude Opus 5.5 --- README.md | 53 ++++++++++--------- docs/AI_USAGE.md | 2 +- docs/AUTHENTICATION.md | 2 +- src/{ => tests}/App.flow.test.tsx | 2 +- src/{ => tests}/App.test.tsx | 2 +- .../components/ProtectedRoute.test.tsx | 2 +- .../elements/ElementActions.test.tsx | 2 +- .../elements/ElementDetailHeader.test.tsx | 2 +- .../components/elements/ResultsTable.test.tsx | 2 +- .../gymnasts/DeleteGymnastButton.test.tsx | 2 +- .../gymnasts/DevLoginButton.test.tsx | 2 +- .../components/gymnasts/GymnastList.test.tsx | 2 +- .../components/navbar/Navbar.test.tsx | 2 +- .../components/skipLink/SkipLink.test.tsx | 2 +- .../components/ui/DifficultyBadge.test.tsx | 2 +- .../components/ui/ElementVariants.test.tsx | 2 +- src/{ => tests}/components/ui/button.test.tsx | 0 src/{ => tests}/lib/apollo/api.test.ts | 2 +- src/{ => tests}/lib/apollo/authLink.test.ts | 8 +-- src/{ => tests}/lib/apollo/cache.test.ts | 2 +- src/{ => tests}/lib/apollo/client.test.ts | 2 +- src/{ => tests}/lib/apollo/errors.test.ts | 4 +- src/{ => tests}/lib/apollo/session.test.ts | 8 ++- src/{ => tests}/lib/catalog.test.ts | 4 +- src/{ => tests}/lib/gymnasts.test.ts | 2 +- .../lib/insecureConnection.test.ts | 2 +- src/{ => tests}/lib/searchOperations.test.ts | 2 +- .../pages/ElementDetailPage.test.tsx | 2 +- src/{ => tests}/pages/ElementsPage.test.tsx | 2 +- src/{ => tests}/pages/LoginPage.test.tsx | 4 +- .../pages/YourGymnastsPage.test.tsx | 2 +- src/{ => tests}/styles/gymnastTheme.test.ts | 8 +-- 32 files changed, 71 insertions(+), 66 deletions(-) rename src/{ => tests}/App.flow.test.tsx (99%) rename src/{ => tests}/App.test.tsx (99%) rename src/{ => tests}/components/ProtectedRoute.test.tsx (97%) rename src/{ => tests}/components/elements/ElementActions.test.tsx (95%) rename src/{ => tests}/components/elements/ElementDetailHeader.test.tsx (93%) rename src/{ => tests}/components/elements/ResultsTable.test.tsx (99%) rename src/{ => tests}/components/gymnasts/DeleteGymnastButton.test.tsx (99%) rename src/{ => tests}/components/gymnasts/DevLoginButton.test.tsx (98%) rename src/{ => tests}/components/gymnasts/GymnastList.test.tsx (96%) rename src/{ => tests}/components/navbar/Navbar.test.tsx (99%) rename src/{ => tests}/components/skipLink/SkipLink.test.tsx (94%) rename src/{ => tests}/components/ui/DifficultyBadge.test.tsx (95%) rename src/{ => tests}/components/ui/ElementVariants.test.tsx (95%) rename src/{ => tests}/components/ui/button.test.tsx (100%) rename src/{ => tests}/lib/apollo/api.test.ts (98%) rename src/{ => tests}/lib/apollo/authLink.test.ts (96%) rename src/{ => tests}/lib/apollo/cache.test.ts (99%) rename src/{ => tests}/lib/apollo/client.test.ts (96%) rename src/{ => tests}/lib/apollo/errors.test.ts (96%) rename src/{ => tests}/lib/apollo/session.test.ts (98%) rename src/{ => tests}/lib/catalog.test.ts (93%) rename src/{ => tests}/lib/gymnasts.test.ts (98%) rename src/{ => tests}/lib/insecureConnection.test.ts (92%) rename src/{ => tests}/lib/searchOperations.test.ts (98%) rename src/{ => tests}/pages/ElementDetailPage.test.tsx (99%) rename src/{ => tests}/pages/ElementsPage.test.tsx (99%) rename src/{ => tests}/pages/LoginPage.test.tsx (98%) rename src/{ => tests}/pages/YourGymnastsPage.test.tsx (99%) rename src/{ => tests}/styles/gymnastTheme.test.ts (97%) diff --git a/README.md b/README.md index 84b577a..3607120 100644 --- a/README.md +++ b/README.md @@ -21,18 +21,18 @@ The calculator and About pages are placeholders. Adding items from the catalog t ## What to review -| Review | What to look at | -| ------------------------------------------------------------ | ---------------------------------------------------------------------------------- | -| [Running application](http://it2810-29.idi.ntnu.no/project2) | Search, load more, element details, and the Your gymnasts test login | -| [ElementsPage tests](src/pages/ElementsPage.test.tsx) | Search, result paging, URL state, loading, and error behavior | -| [Element detail tests](src/pages/ElementDetailPage.test.tsx) | Catalog detail rendering and error/loading states | -| [Gymnast page tests](src/pages/YourGymnastsPage.test.tsx) | Test login, loading, profile creation, and error handling | -| [Backend catalog tests](backend/src/catalog/search.test.ts) | Search, filters, sorting, pagination, and count limits | -| [Backend server tests](backend/src/server/server.test.ts) | GraphQL API behavior; database-backed tests require a migrated and seeded database | -| [docs/API.md](docs/API.md) | API design, schema, search, pagination, authentication, and errors | -| [docs/AI_USAGE.md](docs/AI_USAGE.md) | How AI was used and how the work was reviewed | -| [docs/ACCESSIBILITY.md](docs/ACCESSIBILITY.md) | Accessibility choices, testing, and known open points | -| [docs/SUSTAINABILITY.md](docs/SUSTAINABILITY.md) | Sustainable design choices, measurements, and what remains unmeasured | +| Review | What to look at | +| ------------------------------------------------------------------ | ---------------------------------------------------------------------------------- | +| [Running application](http://it2810-29.idi.ntnu.no/project2) | Search, load more, element details, and the Your gymnasts test login | +| [ElementsPage tests](src/tests/pages/ElementsPage.test.tsx) | Search, result paging, URL state, loading, and error behavior | +| [Element detail tests](src/tests/pages/ElementDetailPage.test.tsx) | Catalog detail rendering and error/loading states | +| [Gymnast page tests](src/tests/pages/YourGymnastsPage.test.tsx) | Test login, loading, profile creation, and error handling | +| [Backend catalog tests](backend/src/catalog/search.test.ts) | Search, filters, sorting, pagination, and count limits | +| [Backend server tests](backend/src/server/server.test.ts) | GraphQL API behavior; database-backed tests require a migrated and seeded database | +| [docs/API.md](docs/API.md) | API design, schema, search, pagination, authentication, and errors | +| [docs/AI_USAGE.md](docs/AI_USAGE.md) | How AI was used and how the work was reviewed | +| [docs/ACCESSIBILITY.md](docs/ACCESSIBILITY.md) | Accessibility choices, testing, and known open points | +| [docs/SUSTAINABILITY.md](docs/SUSTAINABILITY.md) | Sustainable design choices, measurements, and what remains unmeasured | ### How to try it @@ -253,7 +253,7 @@ The Elements page (`/elements`, `src/pages/ElementsPage.tsx`) is where a coach s ### Testing -- `src/pages/ElementsPage.test.tsx` runs the page on the real Apollo Client with a stub for `fetch` that answers like the server: each filter, several values, the address in both directions, the disabled filters, clear, the sort list, the filters and the sort together, a wrong address, a structure that fails, and "Load more" with a filter. Each behavior was broken on purpose to check that a test fails without it. +- `src/tests/pages/ElementsPage.test.tsx` runs the page on the real Apollo Client with a stub for `fetch` that answers like the server: each filter, several values, the address in both directions, the disabled filters, clear, the sort list, the filters and the sort together, a wrong address, a structure that fails, and "Load more" with a filter. Each behavior was broken on purpose to check that a test fails without it. - The backend tests (`backend/src/catalog/search.test.ts`, `backend/src/server/server.test.ts`) cover the vault filter, the range of the values and `Apparatus.difficultyValues`, and need the seeded database like the rest. - Not everything can be tested in jsdom (for example the tooltip, which Base UI opens from real pointer events). Those parts were checked in a real browser against the real backend. @@ -585,19 +585,20 @@ Settings, all optional, as environment variables: ## Project structure -| Folder | Contents | -| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | -| `backend/` | The backend package: TypeScript, Prisma, the seed script, the catalog search, the GraphQL server and the GraphQL schema | -| `data/` | The catalog data extracted from the Code of Points: JSON files and one image per element | -| `docs/` | Project plans, data model, API, accessibility, sustainability, AI usage, and catalog extraction documentation | -| `e2e/` | The end-to-end tests (Playwright): whole flows in a real browser, accessibility checks and the API over HTTP. See "End-to-end tests" | -| `public/` | Static files served as they are, such as the favicon | -| `scripts/extract-cop/` | The Python script that extracts `data/` from the Code of Points PDF | -| `src/` | The React frontend, shared UI components, API operations, Apollo Client, and frontend tests | -| `src/pages/` | One file for each route in `src/App.tsx` (the Elements page, the element page, Your gymnasts, Log in and the placeholder pages), with its tests | -| `src/components/` | The parts of the pages, in one folder for each part of the site (`elements/`, `gymnasts/`, `auth/`, `navbar/`) | -| `src/components/ui/` | The shadcn/ui components (written by the shadcn/ui CLI) and a few small shared components (`DifficultyBadge`, `ElementValueBadge`, `ElementVariants`) | -| `src/lib/` | The GraphQL operations, Apollo Client and the logic that is not a component | +| Folder | Contents | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `backend/` | The backend package: TypeScript, Prisma, the seed script, the catalog search, the GraphQL server and the GraphQL schema | +| `data/` | The catalog data extracted from the Code of Points: JSON files and one image per element | +| `docs/` | Project plans, data model, API, accessibility, sustainability, AI usage, and catalog extraction documentation | +| `e2e/` | The end-to-end tests (Playwright): whole flows in a real browser, accessibility checks and the API over HTTP. See "End-to-end tests" | +| `public/` | Static files served as they are, such as the favicon | +| `scripts/extract-cop/` | The Python script that extracts `data/` from the Code of Points PDF | +| `src/` | The React frontend, shared UI components, API operations, Apollo Client, and frontend tests | +| `src/pages/` | One file for each route in `src/App.tsx` (the Elements page, the element page, Your gymnasts, Log in and the placeholder pages) | +| `src/components/` | The parts of the pages, in one folder for each part of the site (`elements/`, `gymnasts/`, `auth/`, `navbar/`) | +| `src/components/ui/` | The shadcn/ui components (written by the shadcn/ui CLI) and a few small shared components (`DifficultyBadge`, `ElementValueBadge`, `ElementVariants`) | +| `src/lib/` | The GraphQL operations, Apollo Client and the logic that is not a component | +| `src/tests/` | The frontend tests (Vitest), in the same folders as the code they test (`pages/`, `components/elements/`, `lib/apollo/` ...), and the shared helpers (`apollo.ts` for the Apollo test client, `setupTests.ts`) | ## Working in this repository diff --git a/docs/AI_USAGE.md b/docs/AI_USAGE.md index 38babd8..f278aa0 100644 --- a/docs/AI_USAGE.md +++ b/docs/AI_USAGE.md @@ -531,4 +531,4 @@ - **Tool:** Claude Code - **What the team did:** Wrote issue #97: move the pages out of `src/components/`, so it is clear which files are routes and which are parts of pages, and describe the folders in the README. -- **What Claude did:** Moved the four pages and their tests with `git mv` (so their history follows them), and moved the three placeholder pages out of `App.tsx` into their own files, so every route has a file in `src/pages/`. Updated the imports, the links in the README and the docs, and the "Project structure" table. Found that `src/styles/gymnastTheme.test.ts` reads the files of the gymnasts and login pages by folder, so after the move it would have stopped checking the two pages without failing; added them to the list. The shared components in `components/ui/` were left where they are (see the issue). +- **What Claude did:** Moved the four pages and their tests with `git mv` (so their history follows them), and moved the three placeholder pages out of `App.tsx` into their own files, so every route has a file in `src/pages/`. Updated the imports, the links in the README and the docs, and the "Project structure" table. Found that `src/styles/gymnastTheme.test.ts` reads the files of the gymnasts and login pages by folder, so after the move it would have stopped checking the two pages without failing; added them to the list. The shared components in `components/ui/` were left where they are (see the issue). After that, asked to move the frontend tests to `src/tests/` as well: moved all 29 with `git mv` into the same folders as the code they test, changed their relative imports to the `@/` alias (so a test does not depend on where it is), and updated the paths that `gymnastTheme.test.ts` reads files from. diff --git a/docs/AUTHENTICATION.md b/docs/AUTHENTICATION.md index 8915ef5..64cf69c 100644 --- a/docs/AUTHENTICATION.md +++ b/docs/AUTHENTICATION.md @@ -102,7 +102,7 @@ AuthProvider --asks--> me { id name email } (the server) - A visitor who is not logged in is sent to `/login` with the page they wanted in the state of the navigation. After the login they go there, but only if it is a path on this site (a `from` such as `//evil.example` is ignored). - Logging out goes to `/elements` first, so the page the coach leaves does not send them to the login page. -The whole path (log in, save a gymnast, reload, log out, log in again, see the gymnast, delete it) is tested as one test in `src/App.flow.test.tsx`. +The whole path (log in, save a gymnast, reload, log out, log in again, see the gymnast, delete it) is tested as one test in `src/tests/App.flow.test.tsx`. ## What we would do in a real production system diff --git a/src/App.flow.test.tsx b/src/tests/App.flow.test.tsx similarity index 99% rename from src/App.flow.test.tsx rename to src/tests/App.flow.test.tsx index f834e55..5052ed9 100644 --- a/src/App.flow.test.tsx +++ b/src/tests/App.flow.test.tsx @@ -18,7 +18,7 @@ import { import { BrowserRouter } from 'react-router-dom' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { createTestClient, ok, type Handler } from '@/tests/apollo' -import App from './App' +import App from '@/App' const BASE = import.meta.env.BASE_URL diff --git a/src/App.test.tsx b/src/tests/App.test.tsx similarity index 99% rename from src/App.test.tsx rename to src/tests/App.test.tsx index 74e8b3a..fe11b89 100644 --- a/src/App.test.tsx +++ b/src/tests/App.test.tsx @@ -4,7 +4,7 @@ import { cleanup, fireEvent, render, screen } from '@testing-library/react'; import { BrowserRouter } from 'react-router-dom'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { createTestClient, ok, type Handler } from '@/tests/apollo'; -import App from './App'; +import App from '@/App'; /** * The app with the router and the Apollo client that main.tsx gives it. The real diff --git a/src/components/ProtectedRoute.test.tsx b/src/tests/components/ProtectedRoute.test.tsx similarity index 97% rename from src/components/ProtectedRoute.test.tsx rename to src/tests/components/ProtectedRoute.test.tsx index d45ab01..4cc2749 100644 --- a/src/components/ProtectedRoute.test.tsx +++ b/src/tests/components/ProtectedRoute.test.tsx @@ -3,7 +3,7 @@ import { cleanup, render, screen } from '@testing-library/react' import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom' import { afterEach, describe, expect, it } from 'vitest' import { AuthContext, type AuthState } from '@/components/auth/useAuth' -import { ProtectedRoute } from './ProtectedRoute' +import { ProtectedRoute } from '@/components/ProtectedRoute' afterEach(cleanup) diff --git a/src/components/elements/ElementActions.test.tsx b/src/tests/components/elements/ElementActions.test.tsx similarity index 95% rename from src/components/elements/ElementActions.test.tsx rename to src/tests/components/elements/ElementActions.test.tsx index cf4306f..691bd24 100644 --- a/src/components/elements/ElementActions.test.tsx +++ b/src/tests/components/elements/ElementActions.test.tsx @@ -1,7 +1,7 @@ import { renderToString } from 'react-dom/server' import { describe, expect, it } from 'vitest' import type { ElementDetail } from '@/lib/catalog' -import ElementActions from './ElementActions' +import ElementActions from '@/components/elements/ElementActions' const element: ElementDetail = { id: 'UB-2.305-a', diff --git a/src/components/elements/ElementDetailHeader.test.tsx b/src/tests/components/elements/ElementDetailHeader.test.tsx similarity index 93% rename from src/components/elements/ElementDetailHeader.test.tsx rename to src/tests/components/elements/ElementDetailHeader.test.tsx index 83331e5..3238599 100644 --- a/src/components/elements/ElementDetailHeader.test.tsx +++ b/src/tests/components/elements/ElementDetailHeader.test.tsx @@ -2,7 +2,7 @@ import { renderToString } from 'react-dom/server' import { MemoryRouter } from 'react-router-dom' import { describe, expect, it } from 'vitest' import type { ElementDetail } from '@/lib/catalog' -import ElementDetailHeader from './ElementDetailHeader' +import ElementDetailHeader from '@/components/elements/ElementDetailHeader' const element: ElementDetail = { id: 'UB-2.305-a', diff --git a/src/components/elements/ResultsTable.test.tsx b/src/tests/components/elements/ResultsTable.test.tsx similarity index 99% rename from src/components/elements/ResultsTable.test.tsx rename to src/tests/components/elements/ResultsTable.test.tsx index ecab212..2e71d7a 100644 --- a/src/components/elements/ResultsTable.test.tsx +++ b/src/tests/components/elements/ResultsTable.test.tsx @@ -3,7 +3,7 @@ import { MemoryRouter } from 'react-router-dom' import { describe, expect, it } from 'vitest' import { groupLabel, type ElementListRow } from '@/lib/elementList' import { exampleElementRows } from '@/tests/elementRows' -import ResultsTable from './ResultsTable' +import ResultsTable from '@/components/elements/ResultsTable' const render = (rows: ElementListRow[]) => renderToString( diff --git a/src/components/gymnasts/DeleteGymnastButton.test.tsx b/src/tests/components/gymnasts/DeleteGymnastButton.test.tsx similarity index 99% rename from src/components/gymnasts/DeleteGymnastButton.test.tsx rename to src/tests/components/gymnasts/DeleteGymnastButton.test.tsx index 54e44d3..4240d7f 100644 --- a/src/components/gymnasts/DeleteGymnastButton.test.tsx +++ b/src/tests/components/gymnasts/DeleteGymnastButton.test.tsx @@ -13,7 +13,7 @@ import { } from '@testing-library/react' import { afterEach, describe, expect, it, vi } from 'vitest' import { createTestClient, failure, ok, type Handler } from '@/tests/apollo' -import DeleteGymnastButton from './DeleteGymnastButton' +import DeleteGymnastButton from '@/components/gymnasts/DeleteGymnastButton' afterEach(cleanup) diff --git a/src/components/gymnasts/DevLoginButton.test.tsx b/src/tests/components/gymnasts/DevLoginButton.test.tsx similarity index 98% rename from src/components/gymnasts/DevLoginButton.test.tsx rename to src/tests/components/gymnasts/DevLoginButton.test.tsx index 8a45cb3..b17b341 100644 --- a/src/components/gymnasts/DevLoginButton.test.tsx +++ b/src/tests/components/gymnasts/DevLoginButton.test.tsx @@ -5,7 +5,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { ApiError } from "@/lib/apollo/api"; import { devLogin } from "@/lib/apollo/client"; import { isDevLoginEnabled } from "@/lib/devLogin"; -import DevLoginButton from "./DevLoginButton"; +import DevLoginButton from "@/components/gymnasts/DevLoginButton"; vi.mock("@/lib/apollo/client", () => ({ devLogin: vi.fn() })); diff --git a/src/components/gymnasts/GymnastList.test.tsx b/src/tests/components/gymnasts/GymnastList.test.tsx similarity index 96% rename from src/components/gymnasts/GymnastList.test.tsx rename to src/tests/components/gymnasts/GymnastList.test.tsx index 6d59d1d..c052d5d 100644 --- a/src/components/gymnasts/GymnastList.test.tsx +++ b/src/tests/components/gymnasts/GymnastList.test.tsx @@ -2,7 +2,7 @@ import { ApolloProvider } from "@apollo/client/react"; import { renderToString } from "react-dom/server"; import { describe, expect, it } from "vitest"; import { createTestClient } from "@/tests/apollo"; -import GymnastList from "./GymnastList"; +import GymnastList from "@/components/gymnasts/GymnastList"; // Each row has a delete button, which uses the client for its mutation. const { client } = createTestClient(() => new Promise(() => {})); diff --git a/src/components/navbar/Navbar.test.tsx b/src/tests/components/navbar/Navbar.test.tsx similarity index 99% rename from src/components/navbar/Navbar.test.tsx rename to src/tests/components/navbar/Navbar.test.tsx index 977c92a..d5e9c1b 100644 --- a/src/components/navbar/Navbar.test.tsx +++ b/src/tests/components/navbar/Navbar.test.tsx @@ -4,7 +4,7 @@ import { BrowserRouter, useLocation } from 'react-router-dom'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { AuthContext, type AuthState } from '@/components/auth/useAuth'; import { logout } from '@/lib/apollo/client'; -import { Navbar } from './Navbar'; +import { Navbar } from '@/components/navbar/Navbar'; // The real functions would call the server. The Navbar only has to call logout. vi.mock('@/lib/apollo/client', () => ({ diff --git a/src/components/skipLink/SkipLink.test.tsx b/src/tests/components/skipLink/SkipLink.test.tsx similarity index 94% rename from src/components/skipLink/SkipLink.test.tsx rename to src/tests/components/skipLink/SkipLink.test.tsx index 6fa0275..9f52d96 100644 --- a/src/components/skipLink/SkipLink.test.tsx +++ b/src/tests/components/skipLink/SkipLink.test.tsx @@ -1,7 +1,7 @@ // @vitest-environment jsdom import { render, screen } from '@testing-library/react'; import { describe, expect, it } from 'vitest'; -import { SkipLink } from './SkipLink'; +import { SkipLink } from '@/components/skipLink/SkipLink'; describe('SkipLink', () => { it('links to the main content using the default id', () => { diff --git a/src/components/ui/DifficultyBadge.test.tsx b/src/tests/components/ui/DifficultyBadge.test.tsx similarity index 95% rename from src/components/ui/DifficultyBadge.test.tsx rename to src/tests/components/ui/DifficultyBadge.test.tsx index 978bf15..8b3afe6 100644 --- a/src/components/ui/DifficultyBadge.test.tsx +++ b/src/tests/components/ui/DifficultyBadge.test.tsx @@ -1,6 +1,6 @@ import { renderToString } from 'react-dom/server' import { describe, expect, it } from 'vitest' -import DifficultyBadge from './DifficultyBadge' +import DifficultyBadge from '@/components/ui/DifficultyBadge' /** The classes of the circle (the first span with rounded-full in the badge). */ const circle = (markup: string) => diff --git a/src/components/ui/ElementVariants.test.tsx b/src/tests/components/ui/ElementVariants.test.tsx similarity index 95% rename from src/components/ui/ElementVariants.test.tsx rename to src/tests/components/ui/ElementVariants.test.tsx index 17565aa..59e21c8 100644 --- a/src/components/ui/ElementVariants.test.tsx +++ b/src/tests/components/ui/ElementVariants.test.tsx @@ -1,7 +1,7 @@ import { renderToString } from 'react-dom/server' import { MemoryRouter } from 'react-router-dom' import { describe, expect, it } from 'vitest' -import ElementVariants from './ElementVariants' +import ElementVariants from '@/components/ui/ElementVariants' describe('ElementVariants', () => { it('renders navigable variant cards with their values', () => { diff --git a/src/components/ui/button.test.tsx b/src/tests/components/ui/button.test.tsx similarity index 100% rename from src/components/ui/button.test.tsx rename to src/tests/components/ui/button.test.tsx diff --git a/src/lib/apollo/api.test.ts b/src/tests/lib/apollo/api.test.ts similarity index 98% rename from src/lib/apollo/api.test.ts rename to src/tests/lib/apollo/api.test.ts index 991e87d..e787f0e 100644 --- a/src/lib/apollo/api.test.ts +++ b/src/tests/lib/apollo/api.test.ts @@ -4,7 +4,7 @@ import { REQUEST_TIMEOUT_MS, devLoginRequest, refreshRequest, -} from './api' +} from '@/lib/apollo/api' const answer = (body: unknown, status = 200) => new Response(JSON.stringify(body), { status }) diff --git a/src/lib/apollo/authLink.test.ts b/src/tests/lib/apollo/authLink.test.ts similarity index 96% rename from src/lib/apollo/authLink.test.ts rename to src/tests/lib/apollo/authLink.test.ts index 276871a..23b13ff 100644 --- a/src/lib/apollo/authLink.test.ts +++ b/src/tests/lib/apollo/authLink.test.ts @@ -2,9 +2,9 @@ import { ApolloLink } from '@apollo/client/link' import { gql } from '@apollo/client' import { firstValueFrom, of } from 'rxjs' import { type Mock, describe, expect, it, vi } from 'vitest' -import type { AuthPayload } from './api' -import { createAuthLink } from './authLink' -import { createSession, type TokenStorage } from './session' +import type { AuthPayload } from '@/lib/apollo/api' +import { createAuthLink } from '@/lib/apollo/authLink' +import { createSession, type TokenStorage } from '@/lib/apollo/session' const ME = gql` query Me { @@ -128,7 +128,7 @@ describe('the authorization link', () => { }) it('passes on the error, and forgets the login, when the refresh token is refused', async () => { - const { ApiError } = await import('./api') + const { ApiError } = await import('@/lib/apollo/api') const refreshRequest = vi .fn<(refreshToken: string) => Promise>() .mockRejectedValue(new ApiError('UNAUTHENTICATED', 'Log in again.')) diff --git a/src/lib/apollo/cache.test.ts b/src/tests/lib/apollo/cache.test.ts similarity index 99% rename from src/lib/apollo/cache.test.ts rename to src/tests/lib/apollo/cache.test.ts index b979b79..553267f 100644 --- a/src/lib/apollo/cache.test.ts +++ b/src/tests/lib/apollo/cache.test.ts @@ -1,6 +1,6 @@ import { gql } from '@apollo/client' import { describe, expect, it } from 'vitest' -import { createCache } from './cache' +import { createCache } from '@/lib/apollo/cache' const SEARCH = gql` query Search( diff --git a/src/lib/apollo/client.test.ts b/src/tests/lib/apollo/client.test.ts similarity index 96% rename from src/lib/apollo/client.test.ts rename to src/tests/lib/apollo/client.test.ts index 3397190..ca46d29 100644 --- a/src/lib/apollo/client.test.ts +++ b/src/tests/lib/apollo/client.test.ts @@ -3,7 +3,7 @@ // this is the test that the real ones reset the store. import { afterEach, describe, expect, it, vi } from 'vitest' -import { apolloClient, devLogin, logout, session } from './client' +import { apolloClient, devLogin, logout, session } from '@/lib/apollo/client' const payload = { accessToken: 'access-1', diff --git a/src/lib/apollo/errors.test.ts b/src/tests/lib/apollo/errors.test.ts similarity index 96% rename from src/lib/apollo/errors.test.ts rename to src/tests/lib/apollo/errors.test.ts index 8f234af..e8332e3 100644 --- a/src/lib/apollo/errors.test.ts +++ b/src/tests/lib/apollo/errors.test.ts @@ -1,7 +1,7 @@ import { CombinedGraphQLErrors } from '@apollo/client/errors' import { describe, expect, it } from 'vitest' -import { ApiError } from './api' -import { describeError } from './errors' +import { ApiError } from '@/lib/apollo/api' +import { describeError } from '@/lib/apollo/errors' const fromServer = (code: string, message: string) => new CombinedGraphQLErrors({ diff --git a/src/lib/apollo/session.test.ts b/src/tests/lib/apollo/session.test.ts similarity index 98% rename from src/lib/apollo/session.test.ts rename to src/tests/lib/apollo/session.test.ts index 4b75b84..cd77d40 100644 --- a/src/lib/apollo/session.test.ts +++ b/src/tests/lib/apollo/session.test.ts @@ -1,6 +1,10 @@ import { describe, expect, it, vi } from 'vitest' -import { ApiError, type AuthPayload } from './api' -import { REFRESH_TOKEN_KEY, createSession, type TokenStorage } from './session' +import { ApiError, type AuthPayload } from '@/lib/apollo/api' +import { + REFRESH_TOKEN_KEY, + createSession, + type TokenStorage, +} from '@/lib/apollo/session' const payload = (n: number): AuthPayload => ({ accessToken: `access-${n}`, diff --git a/src/lib/catalog.test.ts b/src/tests/lib/catalog.test.ts similarity index 93% rename from src/lib/catalog.test.ts rename to src/tests/lib/catalog.test.ts index ef24f8e..b689ed6 100644 --- a/src/lib/catalog.test.ts +++ b/src/tests/lib/catalog.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' -import { displayCode, toElementDetail } from './catalog' -import type { CatalogItemResult } from './catalogOperations' +import { displayCode, toElementDetail } from '@/lib/catalog' +import type { CatalogItemResult } from '@/lib/catalogOperations' const shared = { name: null, diff --git a/src/lib/gymnasts.test.ts b/src/tests/lib/gymnasts.test.ts similarity index 98% rename from src/lib/gymnasts.test.ts rename to src/tests/lib/gymnasts.test.ts index 2dacd40..f867d36 100644 --- a/src/lib/gymnasts.test.ts +++ b/src/tests/lib/gymnasts.test.ts @@ -4,7 +4,7 @@ import { getGymnastNameError, limitGymnastNameInput, normalizeGymnastName, -} from "./gymnasts"; +} from "@/lib/gymnasts"; describe("gymnast helpers", () => { it("creates initials from the first and last name", () => { diff --git a/src/lib/insecureConnection.test.ts b/src/tests/lib/insecureConnection.test.ts similarity index 92% rename from src/lib/insecureConnection.test.ts rename to src/tests/lib/insecureConnection.test.ts index 25db9cc..8ed6bdd 100644 --- a/src/lib/insecureConnection.test.ts +++ b/src/tests/lib/insecureConnection.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { isInsecureConnection } from './insecureConnection' +import { isInsecureConnection } from '@/lib/insecureConnection' describe('isInsecureConnection', () => { it.each([ diff --git a/src/lib/searchOperations.test.ts b/src/tests/lib/searchOperations.test.ts similarity index 98% rename from src/lib/searchOperations.test.ts rename to src/tests/lib/searchOperations.test.ts index c83da47..8524a9e 100644 --- a/src/lib/searchOperations.test.ts +++ b/src/tests/lib/searchOperations.test.ts @@ -4,7 +4,7 @@ import { MAX_COUNTED, toListRow, type SearchNode, -} from './searchOperations' +} from '@/lib/searchOperations' const group = { name: 'Mounts', diff --git a/src/pages/ElementDetailPage.test.tsx b/src/tests/pages/ElementDetailPage.test.tsx similarity index 99% rename from src/pages/ElementDetailPage.test.tsx rename to src/tests/pages/ElementDetailPage.test.tsx index f2c48ca..4a02b38 100644 --- a/src/pages/ElementDetailPage.test.tsx +++ b/src/tests/pages/ElementDetailPage.test.tsx @@ -21,7 +21,7 @@ import { type Handler, type Operation, } from '@/tests/apollo' -import ElementDetailPage from './ElementDetailPage' +import ElementDetailPage from '@/pages/ElementDetailPage' const variant = (id: string) => ({ __typename: 'Element', diff --git a/src/pages/ElementsPage.test.tsx b/src/tests/pages/ElementsPage.test.tsx similarity index 99% rename from src/pages/ElementsPage.test.tsx rename to src/tests/pages/ElementsPage.test.tsx index 15ee94f..c75b6df 100644 --- a/src/pages/ElementsPage.test.tsx +++ b/src/tests/pages/ElementsPage.test.tsx @@ -32,7 +32,7 @@ import { type Operation, } from '@/tests/apollo' import { exampleElementRows, exampleTotalCount } from '@/tests/elementRows' -import ElementsPage from './ElementsPage' +import ElementsPage from '@/pages/ElementsPage' afterEach(cleanup) diff --git a/src/pages/LoginPage.test.tsx b/src/tests/pages/LoginPage.test.tsx similarity index 98% rename from src/pages/LoginPage.test.tsx rename to src/tests/pages/LoginPage.test.tsx index e1d45e9..eb066ae 100644 --- a/src/pages/LoginPage.test.tsx +++ b/src/tests/pages/LoginPage.test.tsx @@ -11,8 +11,8 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { AuthContext, type AuthState } from '@/components/auth/useAuth' import { devLogin } from '@/lib/apollo/client' import { ApiError } from '@/lib/apollo/api' -import { DEMO_EMAIL } from '../../shared/demo' -import LoginPage from './LoginPage' +import { DEMO_EMAIL } from '../../../shared/demo' +import LoginPage from '@/pages/LoginPage' // The real function would call the server. The page only has to call devLogin. vi.mock('@/lib/apollo/client', () => ({ devLogin: vi.fn() })) diff --git a/src/pages/YourGymnastsPage.test.tsx b/src/tests/pages/YourGymnastsPage.test.tsx similarity index 99% rename from src/pages/YourGymnastsPage.test.tsx rename to src/tests/pages/YourGymnastsPage.test.tsx index 76f0ef7..ff19938 100644 --- a/src/pages/YourGymnastsPage.test.tsx +++ b/src/tests/pages/YourGymnastsPage.test.tsx @@ -21,7 +21,7 @@ import { type Answer, type Handler, } from '@/tests/apollo' -import YourGymnastsPage from './YourGymnastsPage' +import YourGymnastsPage from '@/pages/YourGymnastsPage' /** What the server says to `YourGymnasts` for a coach with these gymnasts. */ const coachWith = (names: string[]) => diff --git a/src/styles/gymnastTheme.test.ts b/src/tests/styles/gymnastTheme.test.ts similarity index 97% rename from src/styles/gymnastTheme.test.ts rename to src/tests/styles/gymnastTheme.test.ts index 95f038a..983d389 100644 --- a/src/styles/gymnastTheme.test.ts +++ b/src/tests/styles/gymnastTheme.test.ts @@ -14,7 +14,7 @@ const here = (path: string) => fileURLToPath(new URL(path, import.meta.url)) const read = (path: string) => readFileSync(here(path), 'utf8').replace(/\r\n/g, '\n') -const css = read('./variables.css') +const css = read('../../styles/variables.css') const light = css.slice( css.indexOf(':root'), css.indexOf("[data-theme='dark']"), @@ -205,9 +205,9 @@ describe('the components of the gymnasts and login pages', () => { // The two pages (src/pages/) and the components they use. const files = [ - '../pages/YourGymnastsPage.tsx', - '../pages/LoginPage.tsx', - ...['../components/gymnasts/', '../components/auth/'].flatMap((dir) => + '../../pages/YourGymnastsPage.tsx', + '../../pages/LoginPage.tsx', + ...['../../components/gymnasts/', '../../components/auth/'].flatMap((dir) => readdirSync(here(dir)) .filter((name) => name.endsWith('.tsx') && !name.endsWith('.test.tsx')) .map((name) => dir + name), From 25ab14fb769e718ea4936d51f45aec4c365a4dba Mon Sep 17 00:00:00 2001 From: Anettkva Date: Thu, 5 Nov 2026 13:16:54 +0100 Subject: [PATCH 3/3] docs: explain where the frontend, backend and end-to-end tests are Co-Authored-By: Claude Opus 5.5 --- README.md | 39 +++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 3607120..1d463c4 100644 --- a/README.md +++ b/README.md @@ -585,20 +585,31 @@ Settings, all optional, as environment variables: ## Project structure -| Folder | Contents | -| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `backend/` | The backend package: TypeScript, Prisma, the seed script, the catalog search, the GraphQL server and the GraphQL schema | -| `data/` | The catalog data extracted from the Code of Points: JSON files and one image per element | -| `docs/` | Project plans, data model, API, accessibility, sustainability, AI usage, and catalog extraction documentation | -| `e2e/` | The end-to-end tests (Playwright): whole flows in a real browser, accessibility checks and the API over HTTP. See "End-to-end tests" | -| `public/` | Static files served as they are, such as the favicon | -| `scripts/extract-cop/` | The Python script that extracts `data/` from the Code of Points PDF | -| `src/` | The React frontend, shared UI components, API operations, Apollo Client, and frontend tests | -| `src/pages/` | One file for each route in `src/App.tsx` (the Elements page, the element page, Your gymnasts, Log in and the placeholder pages) | -| `src/components/` | The parts of the pages, in one folder for each part of the site (`elements/`, `gymnasts/`, `auth/`, `navbar/`) | -| `src/components/ui/` | The shadcn/ui components (written by the shadcn/ui CLI) and a few small shared components (`DifficultyBadge`, `ElementValueBadge`, `ElementVariants`) | -| `src/lib/` | The GraphQL operations, Apollo Client and the logic that is not a component | -| `src/tests/` | The frontend tests (Vitest), in the same folders as the code they test (`pages/`, `components/elements/`, `lib/apollo/` ...), and the shared helpers (`apollo.ts` for the Apollo test client, `setupTests.ts`) | +| Folder | Contents | +| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | +| `backend/` | The backend package: TypeScript, Prisma, the seed script, the catalog search, the GraphQL server and the GraphQL schema | +| `backend/src/` | The backend code, with its tests next to the code they test (`*.test.ts`). See "Where the tests are" below | +| `data/` | The catalog data extracted from the Code of Points: JSON files and one image per element | +| `docs/` | Project plans, data model, API, accessibility, sustainability, AI usage, and catalog extraction documentation | +| `e2e/` | The end-to-end tests (Playwright): whole flows in a real browser, accessibility checks and the API over HTTP. See "End-to-end tests" | +| `public/` | Static files served as they are, such as the favicon | +| `scripts/extract-cop/` | The Python script that extracts `data/` from the Code of Points PDF | +| `src/` | The React frontend, shared UI components, API operations, Apollo Client, and frontend tests | +| `src/pages/` | One file for each route in `src/App.tsx` (the Elements page, the element page, Your gymnasts, Log in and the placeholder pages) | +| `src/components/` | The parts of the pages, in one folder for each part of the site (`elements/`, `gymnasts/`, `auth/`, `navbar/`) | +| `src/components/ui/` | The shadcn/ui components (written by the shadcn/ui CLI) and a few small shared components (`DifficultyBadge`, `ElementValueBadge`, `ElementVariants`) | +| `src/lib/` | The GraphQL operations, Apollo Client and the logic that is not a component | +| `src/tests/` | The frontend tests (Vitest), in the same folders as the code they test, and the shared test helpers. See "Where the tests are" below | + +### Where the tests are + +The frontend and the backend keep their tests in different places, on purpose: + +- **Frontend: `src/tests/`.** The tests are in the same folders as the code they test (`src/tests/pages/ElementsPage.test.tsx` tests `src/pages/ElementsPage.tsx`), and they import it with the `@/` alias. The folder also has what the tests share: `setupTests.ts`, the Apollo test client (`apollo.ts`) and the example rows (`elementRows.ts`). Keeping the tests apart from the code means that `src/pages/` and `src/components/` only show what the app is made of. +- **Backend: next to the code, in `backend/src/`.** `backend/src/catalog/search.test.ts` tests `backend/src/catalog/search.ts`. The backend is its own package, with Node's module system and relative imports (`'../errors.js'`), and its folders are small and grouped by job (`auth/`, `catalog/`, `seed/`, `server/`), so a test next to its file is easy to find. This is the usual layout in a Node package. The tests that use the database need a migrated and seeded database (see "Database (local setup)"). +- **End-to-end: `e2e/`** (see "End-to-end tests"). + +A new test goes in the same place as the others of its kind: a frontend test in `src/tests/` with the same path as the file it tests, and a backend test next to its file. ## Working in this repository