docs(audit): journey 3 notes — building a comparison
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,125 @@
|
|||||||
|
# Journey 3: Building a comparison (`/compare`) — audit notes
|
||||||
|
|
||||||
|
**Pages visited:** `/compare` (empty state, direct entry), `/compare?urns=…` (populated, 1–3 schools), add-school modal, `/school/142161-…` (persistence round-trip)
|
||||||
|
**Viewports:** 390×844 (primary), 1440×900 (+720×450 proxy for 200% reflow)
|
||||||
|
**Site:** live https://schoolcompare.co.uk only. Read-only: searched, added/removed schools in the compare basket (client/localStorage + URL state), opened/closed the modal, hovered the chart. Submitted no data-modifying forms. `selectedSchools` was cleared from localStorage only to reproduce a genuine empty state and to test the share-link defect.
|
||||||
|
|
||||||
|
## Task attempt log
|
||||||
|
|
||||||
|
**Direct entry premise:** 20% of sessions enter on `/compare`, so the empty state is judged as a cold arrival.
|
||||||
|
|
||||||
|
**Mobile (390×844) — empty state.**
|
||||||
|
- Navigating to `/compare` directly first showed a *persisted* prior selection (The Castle School, carried in `localStorage.selectedSchools` from an earlier session) — the URL self-rehydrated to `?urns=136916&metric=…`. This is persistence working (see Works well) but means the "empty" state is only seen by first-ever visitors. Cleared storage to capture the true empty state (`j3-compare-empty-mobile.png`).
|
||||||
|
- Empty state is **instructive and actionable**: heading "No schools selected", body "Add schools from the home page or search to start comparing", and a primary button **"+ Add Schools to Compare"** that opens an in-page search modal. A cold arrival can start without leaving the page.
|
||||||
|
|
||||||
|
**Mobile — building a two-school comparison. Taps counted from empty state:**
|
||||||
|
1. Tap **"+ Add Schools to Compare"** → opens modal, auto-focuses the "School name or postcode" search box.
|
||||||
|
2. **Type** "St Mary" → live result list appears (name + LA + school type + "+ Compare" per row).
|
||||||
|
3. Tap **"+ Compare"** on a result → school added; **modal stays open, field clears** ("Comparing 1 school" behind it).
|
||||||
|
4. **Type** "Ottery St Mary" → result list.
|
||||||
|
5. Tap **"+ Compare"** → second school added (`?urns=142161,113105`).
|
||||||
|
6. Tap **"Close modal"** → reveals the comparison (`j3-compare-two-schools-mobile.png`).
|
||||||
|
|
||||||
|
→ **6 distinct interactions (4 taps + 2 text-entry sequences)** to a two-school comparison. Critically, both schools had to be **found by name/postcode** — there is no browse / "schools near this one" / "similar schools" on-ramp inside compare (ties to Journey 2 F2). A parent who knows only one school name cannot build a comparison here.
|
||||||
|
|
||||||
|
**Mobile — comparison output assessment.**
|
||||||
|
- Per-metric **school cards stack vertically** and are fully legible; each shows name, LA, type, the selected metric value (e.g. 77.0% vs 51.0%), colour-keyed to the chart. Good on 390px.
|
||||||
|
- **Chart** ("Performance Over Time"): renders on real scroll (blank in full-page captures = lazy-render artifact, confirmed by element screenshot `j3-compare-chart-mobile.png`). Legend labels each line by **colour + full school name** — colour is not the sole differentiator (WCAG 1.4.1 satisfied). `canvas` has `role="img"` but **`aria-label` = null** (no text alt).
|
||||||
|
- **"Detailed Comparison" table**: wrapper 626px inside a 324px column → only **Year + the first school column fit**; the second school is off-screen behind horizontal scroll (`overflow-x:auto`, no visible scroll affordance). The whole point of the table (side-by-side) can't be seen at once on the primary viewport.
|
||||||
|
- Metric selector carries a **plain-English caption** ("% meeting expected standard in reading, writing and maths") — jargon defined in place (the thing school-detail pages lacked, Journey 2 F3).
|
||||||
|
|
||||||
|
**Mobile — remove / add / persistence.**
|
||||||
|
- **Remove (×)** is immediate: no confirm, no toast, no undo; URL updates instantly.
|
||||||
|
- **Postcode search works**: "TA1 5AU" → The Castle School (advertised feature, functional).
|
||||||
|
- **Add beyond two / phase mixing**: adding a secondary school to a primary comparison keeps the header at "Comparing 2 schools" but renders **only the primary card**; the secondary sits hidden behind a "Secondary (1)" tab with no explanation.
|
||||||
|
- **Persistence**: navigated to a school detail page, then back to bare `/compare` — both schools restored and URL rehydrated to `?urns=142161,136916`. State survives real navigation via localStorage; comparisons are URL-encoded / deep-linkable.
|
||||||
|
|
||||||
|
**Desktop (1440×900).**
|
||||||
|
- Table fits with **both/all school columns visible** (no page overflow). Layout clean (`j3-compare-two-schools-desktop.png`).
|
||||||
|
- **Keyboard add/remove**: "+ Add School" reachable and Enter-activatable; modal **opens with focus moved into the search input**; Tab reaches a result's "+ Compare" with a visible **2px solid orange (#E07256)** focus outline; Enter adds it. **But** after adding, focus **drops to `document.body`**; after **Escape** (which does close the modal) focus is again on `body`, **not** returned to the "+ Add School" trigger.
|
||||||
|
- Modal has **no `role="dialog"` and no `aria-modal`** — not announced as a dialog.
|
||||||
|
- **Chart tooltip** (`j3-compare-chart-tooltip-desktop.png`): hover shows year + per-school values with colour swatch + name — **mouse-only** (no touch/keyboard equivalent), but the same values are present as text in the table below.
|
||||||
|
- **200% zoom (720×450 proxy)**: no page-level horizontal overflow — content reflows (WCAG 1.4.10 pass); the detailed table retains its own contained horizontal scroll.
|
||||||
|
|
||||||
|
### Is 32% of exits task-complete satisfaction or abandonment?
|
||||||
|
**Mixed, with concrete abandonment drivers — it is not safe to read the exits as pure satisfaction.**
|
||||||
|
- *Points to satisfaction:* compare is a terminal "results" tool — the natural next step (visit/apply) is off-site, so a high exit share is partly expected; persistence + shareable URLs mean some exits are "saved for later"; the core output (cards + chart + table) renders and metrics are explained.
|
||||||
|
- *Points to abandonment:* (a) **20% ENTER on `/compare`**, often via a shared link — and the **localStorage-overrides-URL defect (F4)** silently shows a returning recipient *their own* schools instead of the shared ones, a confusing dead end; (b) a 2-school comparison needs 6 interactions **and prior knowledge of both school names** (no discovery on-ramp) — a parent with one school hits a wall; (c) on mobile the **detailed table hides the second school off-screen** behind an unaffordanced scroll (F1), so the comparison can look incomplete; (d) low-contrast primary buttons (F6) degrade the mobile-daylight path. F1/F4/F6 are each capable of turning an intended task-complete exit into premature abandonment, especially for the 20% arriving on shared links.
|
||||||
|
|
||||||
|
## Friction points
|
||||||
|
|
||||||
|
- **F1. "Detailed Comparison" table hides the second/third school off-screen on mobile**
|
||||||
|
- Evidence: at 390px the table is 626px inside a 324px `.tableWrapper` (`overflow-x:auto`); only Year + the first school column are visible, second column cut off with no scroll affordance (`j3-compare-two-schools-mobile.png`).
|
||||||
|
- Criterion violated: mobile-usability standard (horizontal scrolling of primary content is a known antipattern) + Nielsen #6 (Recognition rather than recall) — a parent must remember school A's numbers while scrolling to school B; the tool's core "side-by-side" promise is defeated on the primary viewport.
|
||||||
|
- Argument: the whole reason a parent opens compare is to see schools next to each other; on mobile they can't, and with no scroll cue may believe the second school's data is missing and leave.
|
||||||
|
- Severity guess: P2 (the stacked metric cards + chart do show both, softening it)
|
||||||
|
|
||||||
|
- **F2. Comparison table scroll region is not keyboard-accessible**
|
||||||
|
- Evidence: axe `scrollable-region-focusable` (serious, 1 node) on `.ComparisonView-module__…tableWrapper`; the overflowing wrapper has no `tabindex`.
|
||||||
|
- Criterion violated: WCAG 2.2 **2.1.1 Keyboard (A)**.
|
||||||
|
- Argument: a keyboard-only parent cannot scroll the detailed table to reveal columns beyond the first school — data is literally unreachable without a mouse/touch.
|
||||||
|
- Severity guess: P2
|
||||||
|
|
||||||
|
- **F3. Trend chart exposes no text alternative to assistive tech**
|
||||||
|
- Evidence: axe `role-img-alt` (serious) on `canvas`; `aria-label` = null on both mobile and desktop.
|
||||||
|
- Criterion violated: WCAG 2.2 **1.1.1 Non-text Content (A)**.
|
||||||
|
- Argument: a screen-reader parent gets nothing from "Performance Over Time"; the redundant table softens this but the chart's at-a-glance trend story is lost. Recurs from Journey 2 F5 — systemic to the chart component.
|
||||||
|
- Severity guess: P2
|
||||||
|
|
||||||
|
- **F4. Share link is silently overridden by the visitor's own localStorage**
|
||||||
|
- Evidence: navigating to `/compare?urns=142161,113105` while `selectedSchools` held a different set resolved the page (and URL) back to the stored `urns=142161,136916`; only after clearing localStorage did the shared URL load its intended schools.
|
||||||
|
- Criterion violated: Nielsen #1 (Visibility of system status) / consistency; task-flow obstruction of an **advertised primary feature** ("Share" button).
|
||||||
|
- Argument: a parent shares their shortlist with a partner; if the recipient has ever used compare, they silently see *their own* schools with no error or notice — the shared comparison is unreproducible and the collaboration breaks. This directly hits the 20% who enter on `/compare` via links.
|
||||||
|
- Severity guess: P1
|
||||||
|
|
||||||
|
- **F5. Add-school modal lacks dialog semantics and loses focus on add/close**
|
||||||
|
- Evidence: modal has no `role="dialog"` and no `aria-modal`; after a keyboard "+ Compare" add, `document.activeElement` = `BODY`; after Escape-close (Escape does dismiss it), focus is again on `BODY`, not the "+ Add School" trigger.
|
||||||
|
- Criterion violated: WCAG 2.2 **4.1.2 Name, Role, Value (A)** + **2.4.3 Focus Order (A)**.
|
||||||
|
- Argument: screen-reader users aren't told a dialog opened; keyboard users lose their place after every add (focus jumps to page top) and after closing, so building a multi-school comparison means re-tabbing from the top repeatedly — friction on the add flow every comparison depends on.
|
||||||
|
- Severity guess: P2 (P1-candidate for keyboard/AT users building 3+ school comparisons)
|
||||||
|
|
||||||
|
- **F6. Colour-contrast failures on the primary compare controls**
|
||||||
|
- Evidence: axe `color-contrast` (serious, 5 nodes populated) — sample targets `.btn-primary` (the "+ Add School" / "+ Compare" buttons), `.…phaseTabActive` (active Primary/Secondary tab), `.…tabActive .tabLabel` (active bottom-nav label).
|
||||||
|
- Criterion violated: WCAG 2.2 **1.4.3 Contrast (Minimum) (AA)**.
|
||||||
|
- Argument: the exact orange buttons and active tabs a parent must use to build and read a comparison have insufficient text contrast — hard to read on a phone in daylight (the mobile-primary audience). Recurs site-wide (Journey 1/2).
|
||||||
|
- Severity guess: P1
|
||||||
|
|
||||||
|
- **F7. Mixed-phase selection reports "Comparing 2 schools" while showing one**
|
||||||
|
- Evidence: with a primary + a secondary selected, header reads "Comparing 2 schools" but only the primary card/chart/table render; the secondary is hidden behind a "Secondary (1)" tab with no explanatory copy.
|
||||||
|
- Criterion violated: Nielsen #1 (Visibility of system status) & #2 (Match between system and real world).
|
||||||
|
- Argument: a parent who added two schools sees one and may think the second was dropped; nothing explains the phase split. (Separating KS2 vs GCSE metrics is itself correct — see Works well — only the count/label is misleading.)
|
||||||
|
- Severity guess: P3
|
||||||
|
|
||||||
|
- **F8. Remove is instant with no undo or confirmation**
|
||||||
|
- Evidence: tapping the × removed a school immediately — no confirm, no toast, no undo; URL updated instantly.
|
||||||
|
- Criterion violated: Nielsen #3 (User control & freedom — support undo).
|
||||||
|
- Argument: an accidental tap on the small (28px) × on mobile silently loses a school the parent may have spent a name-search to add; recovery means re-searching. A one-tap "undo" would prevent the loss.
|
||||||
|
- Severity guess: P3
|
||||||
|
|
||||||
|
## Works well — keep
|
||||||
|
- **Empty state is instructive *and* actionable**: clear "No schools selected" message plus a "+ Add Schools to Compare" button that opens an in-page search modal — the 20% who enter cold on `/compare` can start immediately (answers Journey 2's "no on-ramp" concern *at the compare page itself*).
|
||||||
|
- **Metric selector carries a plain-English caption** for the chosen metric (e.g. "% meeting expected standard in reading, writing and maths") — jargon defined in place, exactly what school-detail pages omitted.
|
||||||
|
- **Search accepts name and postcode** (TA1 5AU resolved to the correct school) — the advertised postcode feature works.
|
||||||
|
- **Efficient multi-add**: the modal stays open and clears the field after each add, so adding several schools needs no reopen.
|
||||||
|
- **Robust persistence + deep-linking**: selection survives real navigation away and back (localStorage), and comparisons are fully URL-encoded/shareable.
|
||||||
|
- **Chart accessibility basics done right for sighted users**: legend labels each series by colour **and** full school name (WCAG 1.4.1 satisfied); desktop hover tooltip gives precise per-year values; the table repeats those values as text.
|
||||||
|
- **Phase separation** (Primary/Secondary tabs) sensibly prevents nonsensical KS2-vs-GCSE metric comparisons.
|
||||||
|
- **Desktop keyboard + reflow**: visible 2px orange focus outlines on controls; modal opens with focus moved into the search field; Escape dismisses it; all columns visible without overflow; content reflows at 200% zoom with no page-level horizontal scroll (WCAG 1.4.10 pass).
|
||||||
|
|
||||||
|
## Axe results
|
||||||
|
- `/compare` (empty) @ 390×844: **1 violation** — `color-contrast` (serious, 2 nodes: active nav tab label, `.btn-primary`).
|
||||||
|
- `/compare?urns=142161,113105` (populated, 2 schools) @ 390×844: **3 violations** — `color-contrast` (serious, 5 nodes: `.btn-primary`, active phase tab, active nav tab), `role-img-alt` (serious, 1 node: `canvas`), `scrollable-region-focusable` (serious, 1 node: `.tableWrapper`).
|
||||||
|
- Desktop 1440×900 populated: `canvas` `aria-label` still null (role-img-alt persists); `color-contrast` on `.btn-primary`/active tabs persists (viewport-independent).
|
||||||
|
|
||||||
|
## Manual WCAG spot checks
|
||||||
|
- Touch targets ≥24px on interactive elements (WCAG 2.5.8): **pass** — "+ Add School" 139×38, Share 110×40, metric select 324×41, phase tabs ~130×40. Smallest is the **× remove at 28×28** — above the 24px minimum but the tightest target and below the 44px comfortable norm.
|
||||||
|
- Keyboard: tab order / focus visibility (desktop): **pass with a gap** — logical order and visible 2px orange outlines on controls; modal opens with focus moved into the search box; **fail on focus restoration** — focus drops to `body` after an add and after Escape-close rather than returning to a sensible place (F5).
|
||||||
|
- Zoom 200% text reflow (desktop, 720×450 proxy): **pass** — no page-level horizontal scroll; only the detailed table retains its contained scroll.
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
- `j3-compare-empty-mobile.png`: true empty state (localStorage cleared) — "No schools selected" + "+ Add Schools to Compare".
|
||||||
|
- `j3-compare-addmodal-mobile.png`: add-school modal with search field.
|
||||||
|
- `j3-compare-two-schools-mobile.png`: mobile 2-school comparison (cards stack; detailed table shows only first school column).
|
||||||
|
- `j3-compare-chart-mobile.png`: element capture proving the chart renders with a colour+name legend.
|
||||||
|
- `j3-compare-two-schools-desktop.png`: desktop 2-school comparison (both columns visible).
|
||||||
|
- `j3-compare-chart-tooltip-desktop.png`: mouse-hover tooltip with per-school values (3-school legend, colour + label).
|
||||||
Reference in New Issue
Block a user