Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
29 changes: 14 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ This project is a web application for exploring the FIG Women's Artistic Gymnast

The beta now has a working React frontend connected to the GraphQL API and PostgreSQL:

- **Elements:** search the catalog and load more results. Filter and sort controls are visible but are not connected yet.
- **Elements:** search, filter, sort, and page through the catalog.
- **Element details:** open an item from the results and view its details and variants.
- **Your gymnasts:** view and create gymnast profiles using the development test login, which accepts any email address.
- **Backend:** the GraphQL API and seeded PostgreSQL catalog run on the VM.
Expand All @@ -23,7 +23,7 @@ The calculator and About pages are placeholders. Adding items from the catalog t

| 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 |
| [Running application](http://it2810-29.idi.ntnu.no/project2) | Search, pagination, 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 |
Expand All @@ -50,9 +50,9 @@ The data is used for educational purposes. All rights to the Code of Points belo

For everyone, without logging in:

- **Done:** Search the catalog and load more results.
- **Done:** Search the catalog and page through results.
- **Done:** View details for an element or vault, including its code, difficulty, description, image, and variants.
- **Done:** Filter and sort results; the controls are currently visual placeholders.
- **Done:** Filter and sort results.
- **Not done:** Build a routine in the calculator UI and see its D-value. The calculator page is currently a placeholder.

For coaches (the development test login is the only login flow):
Expand Down Expand Up @@ -211,21 +211,20 @@ const SEARCH = gql`
}
`

const { data, fetchMore } = useQuery(SEARCH, {
variables: { search, first: 20 },
const after = searchParams.getAll('cursor').at(-1)
const { data } = useQuery(SEARCH, {
variables: { search, first: 20, ...(after ? { after } : {}) },
})
// "Load more": the next page is added to the list in the cache.
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.
- **Previous / Next pagination.** The current page number and its cursor history are in the URL (for example `?page=3&cursor=<end-of-page-1>&cursor=<end-of-page-2>`). This lets browser Back, reload, and shared links restore a page. A page number without its cursor history cannot restore a keyset page, so an invalid or incomplete page address is reset to page one. The backend remains cursor-based.
- **One cached page.** Apollo keeps one `searchCatalog` connection at a time and only reads it when the search, filters, sort, page size, and cursor all match. Replacing the connection and running `cache.gc()` collects rows from the prior page. The same complete query runs on each page so direct links have the fields the list needs; `totalCount` is therefore requested on each page and remains capped at 1001 by the server.
- **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/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.
The Elements page (`/elements`, `src/pages/ElementsPage.tsx`) is where a coach searches the catalog. It shows one cursor page at a time in a table (cards on a phone) with Previous and Next controls. Search, filters, sort, page number, and the cursor stack are in the address (`?search=salto&apparatus=UB&group=UB-6&difficulty=A,B&sort=difficulty&dir=desc&page=2&cursor=<cursor>`), so reload, sharing, and browser Back can restore a page.

### What the user can do

Expand All @@ -236,7 +235,7 @@ The Elements page (`/elements`, `src/pages/ElementsPage.tsx`) is where a coach s

### Choices and why

- **Ready-made parts only.** The menus are the Base UI `Menu` and `Tooltip` that shadcn/ui is built on (already a dependency), so the arrow keys, Enter, Space, Escape, the type-ahead and "checked" are handled by a library that is tested for it. The address is `useSearchParams` from React Router. The Apollo cache already keeps one list per `search`, `filter` and `sort` (`keyArgs` in `src/lib/apollo/cache.ts`), so a new filter is a new list and "Load more" adds to the right one.
- **Ready-made parts only.** The menus are the Base UI `Menu` and `Tooltip` that shadcn/ui is built on (already a dependency), so the arrow keys, Enter, Space, Escape, the type-ahead and "checked" are handled by a library that is tested for it. The address is `useSearchParams` from React Router. The Apollo cache keeps only the current result page and matches it against the full query arguments, including the cursor.
- **Scales with the catalog.** What the filters choose from is small and fixed: the 4 apparatus with their groups and difficulty values, fetched once (`CatalogStructure`, kept by the Apollo cache and by the server in memory for ten minutes) and the letters A-J. Nothing in the interface grows with the number of elements, and no filter or sort option is read from the results.
- **Difficulty is a letter for elements and a band for vault.** An element has a value from 0.1 to 1.0, which the coach knows as a letter (A = 0.1 ... J = 1.0). A vault has a D-value from 1.6 to 6.4 and no letter. There are 23 different D-values, which is too many to choose between one by one, so the menu offers bands of one whole number (2.0-2.9, 3.0-3.9 ...). A band is sent to the API as the D-values that exist inside it, which `Apparatus.difficultyValues` lists, so the API needs no ranges. Only bands that have a vault are offered, and the chosen difficulty is dropped when the apparatus changes between vault and the others, because the scales differ.
- **The sort is one list of outcomes.** "Code: low to high", "Difficulty: high to low", "Name: A to Z" ... instead of a field and a separate "Ascending"/"Descending". Practitioner guidance on sort menus (for example Vitaly Friedman's) is to name what the user gets and not the jargon, and the ARIA Authoring Practices put the sort state on the column header (`aria-sort`) for tables. Difficulty needs an apparatus, because element values and vault D-scores cannot be compared (see "Why difficulty is only sorted within an apparatus" in [docs/API.md](docs/API.md)), so its two choices are disabled until one is chosen, and the menu says why. On vault the difficulty sort is by the D-score.
Expand All @@ -248,13 +247,13 @@ The Elements page (`/elements`, `src/pages/ElementsPage.tsx`) is where a coach s

- **A filter that cannot be used is disabled, but still reachable.** The group filter before an apparatus is chosen uses `aria-disabled` and not the `disabled` attribute: a disabled button is skipped by the keyboard, and then nobody can find out why it is disabled. The reason is a tooltip (on hover, on keyboard focus, and when the button is pressed, because a tooltip does not open on a tap) and also its `aria-describedby`, so a screen reader says it. This is the pattern that PatternFly, Salt and Vivid recommend for a disabled control that has to explain itself.
- **Each button says its name and its choice** ("Apparatus: Uneven bars", "Difficulty: A, B"), the menus have names, a chosen item is `aria-checked`, and the focus goes back to the button when a menu closes. "Clear filters" removes itself, so it moves the focus to the first filter.
- **The number of results is announced** (`role="status"`, "Showing 20 of 178"), and the results section is `aria-busy` while it loads.
- **Page changes are announced and focused.** A `role="status"` region says "Page N, showing X results", focus moves to the Results heading, and the results section is `aria-busy` while it loads. Pagination is a named navigation landmark with keyboard-operable Previous and Next buttons.
- **Every page has a title** that says what the page is (`PageTitle`, "Elements – WAG CoP Planner"; WCAG 2.4.2). React 19 moves a `<title>` to the head wherever it is rendered, so no library is needed. The element page names the element and the apparatus, also while it is loading or has failed.
- **Checked** with axe-core (no violations in light and dark mode, with the menus open and at phone width), Lighthouse (100), keyboard-only use, contrast (about 15.9:1), touch target sizes, reflow at 320 px, text spacing and reduced motion. **Not checked** with a screen reader such as NVDA or VoiceOver: that has to be done by a person.

### Testing

- `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.
- `src/tests/pages/ElementsPage.test.tsx` runs the page on the real Apollo Client with a stub for `fetch` that answers like the server: filters, sorting, URL cursor history, Previous/Next, page errors and retry, focus/status announcements, and a ten-page bounded DOM/cache check.
- 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.

Expand Down Expand Up @@ -549,7 +548,7 @@ The assignment asks for "a form of automated end-to-end testing" of "a longer se

| File | What it tests |
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `e2e/elements-flow.e2e.ts` | One long flow: search, filter by apparatus and group, "Load more", sort, open an element, go back, "Clear filters". It checks the address, the results, the title and the focus at each step. |
| `e2e/elements-flow.e2e.ts` | Search, filter by apparatus and group, paginate, sort, open an element, go back, and clear; a second journey checks page 2 scroll/cache restoration after opening an element. It checks the address, results, title, focus, scroll, and that Back does not refetch the search. |
| `e2e/vault.e2e.ts` | The vault difficulty bands, and sorting vault by its D-score in both directions. |
| `e2e/atypical.e2e.ts` | Atypical use: an address with filters that do not exist, a search without a result, the 80-character limit, a structure that fails to load (the request is made to fail), the disabled group filter, and keyboard-only use. |
| `e2e/accessibility.e2e.ts` | axe-core on the page in its main states: filtered and sorted, a menu open, the reason for a disabled filter shown, vault, no result, dark mode, a phone (375 px) and reflow at 320 px. A violation fails the test. |
Expand Down
Loading