# QA Test Plan

*Branch: experimental-polar-steps. Tester role: Senior Staff QA Engineer.*

---

## Scope

All features implemented in Phase 4 (Milestones 1–4):
- M1: Entry enrichment (location badge, weather badge, photo gallery, hero image)
- M2: Interactive map page
- M3: Statistics page
- M4: Mini-map on tracker feed

Test URLs:
- Desktop: http://localhost:8081
- Mobile: http://100.96.115.96:8081 (Tailscale — requires physical phone)

---

## Milestone 1 — Entry Enrichment

### TC-1.1: Location badge on entry page

| Step | Action | Expected Result |
|---|---|---|
| 1 | Open http://localhost:8081/tracker/2026-06-17.entry | Entry page loads (200) |
| 2 | Look at entry header | `📍 Amsterdam, Netherlands` visible below date |
| 3 | Inspect HTML | `<p class="entry-location">` present with city and country |

**Automation:** grep for `.entry-location` and "Amsterdam" in curl output

---

### TC-1.2: Weather badge on entry page

| Step | Action | Expected Result |
|---|---|---|
| 1 | Open http://localhost:8081/tracker/2026-06-17.entry | Entry page loads |
| 2 | Look at entry header | `⛅ Partly cloudy · 19°C` visible |
| 3 | Inspect HTML | `<p class="entry-weather">` present |

**Automation:** grep for `.entry-weather` and "Partly cloudy" and "19°C"

---

### TC-1.3: Location badge hidden when fields empty

| Step | Action | Expected Result |
|---|---|---|
| 1 | Create test entry with no location_city/location_country | — |
| 2 | Open that entry | No `📍` badge shown, no empty `<p>` rendered |

**Automation:** Check example entry before fields were added (not needed — fields are now set); create a second test entry without location

---

### TC-1.4: Weather badge hidden when fields empty

| Step | Action | Expected Result |
|---|---|---|
| 1 | Entry with no weather fields | No weather section in HTML |

**Automation:** grep for `entry-weather` in HTML — should only appear if value present

---

### TC-1.5: Hero image on tracker feed card

| Step | Action | Expected Result |
|---|---|---|
| 1 | Open http://localhost:8081/tracker | Feed loads |
| 2 | Entry card for 2026-06-17 | No image shown (example entry has no photos) |
| 3 | Upload a photo to the entry via Admin media manager | — |
| 4 | Reload tracker | Hero image shows as 16:9 thumbnail |

**Manual verification required:** Photo upload requires browser Admin interaction

---

### TC-1.6: Location badge on tracker feed card

| Step | Action | Expected Result |
|---|---|---|
| 1 | Open http://localhost:8081/tracker | Feed loads |
| 2 | Entry card | `📍 Amsterdam, Netherlands` visible |

**Automation:** grep feed HTML for `entry-location--card` and "Amsterdam"

---

### TC-1.7: Photo gallery renders on entry page (with photos)

| Step | Action | Expected Result |
|---|---|---|
| 1 | Upload 3 photos to the example entry via Admin | — |
| 2 | Open entry page | Gallery grid appears below entry body |
| 3 | Count thumbnails | 3 thumbnails in 2-col (mobile) / 3-col (desktop) grid |
| 4 | Click a thumbnail | Lightbox overlay opens with full-size image |
| 5 | Press Escape | Lightbox closes |
| 6 | Click left/right arrow buttons | Navigates between images |
| 7 | Click outside lightbox | Lightbox closes |

**Manual verification required:** Photo upload and interactive lightbox require browser

---

### TC-1.8: Post form has location and weather fields

| Step | Action | Expected Result |
|---|---|---|
| 1 | Open http://localhost:8081/post (logged in) | Post form renders |
| 2 | Inspect form | `City` and `Country` text inputs present |
| 3 | Inspect form | `📍 Get Current Location` and `🌤 Get Weather` buttons present |

**Automation:** grep /post HTML for `location_city`, `location_country`, `get-location`, `get-weather`

---

### TC-1.9: Get Weather button fills fields

| Step | Action | Expected Result |
|---|---|---|
| 1 | Open /post on phone | Post form loads |
| 2 | Tap "Get Current Location" | Lat/lng fields fill with coordinates |
| 3 | Tap "Get Weather" | Status shows "🌤 Weather set: [desc] · [temp]°C" |
| 4 | Submit form | New entry created with weather in frontmatter |
| 5 | Open entry in Admin | weather_temp_c and weather_desc fields populated |

**Manual verification required:** Geolocation and form submission require mobile browser

---

## Milestone 2 — Interactive Map

### TC-2.1: Map page loads

| Step | Action | Expected Result |
|---|---|---|
| 1 | GET http://localhost:8081/map | HTTP 200 |
| 2 | Inspect HTML | `<div id="trip-map">` present |
| 3 | Inspect HTML | Leaflet CSS and JS from CDN present |

**Automation:** curl + HTTP status check; grep for "trip-map" and "leaflet"

---

### TC-2.2: Entry with GPS appears in ENTRIES JSON

| Step | Action | Expected Result |
|---|---|---|
| 1 | curl http://localhost:8081/map | Map page HTML |
| 2 | grep for `var ENTRIES` | Array contains Amsterdam entry with lat 52.3676 |
| 3 | Check entry has title, date, url | All fields present |

**Automation:** grep output for ENTRIES and lat value

---

### TC-2.3: Map renders marker and route in browser

| Step | Action | Expected Result |
|---|---|---|
| 1 | Open /map in browser | Map tiles load, marker visible |
| 2 | Click marker | Popup opens with "The Journey Begins" title and "Read entry →" link |
| 3 | Click "Read entry →" | Navigates to entry page |

**Manual verification required:** Leaflet rendering requires browser

---

### TC-2.4: Map navigation link in header

| Step | Action | Expected Result |
|---|---|---|
| 1 | Open any page | Header shows Journal, Map, Stats nav links |
| 2 | Click Map | Navigates to /map |

**Automation:** grep base template output for "/map" nav link

---

### TC-2.5: Empty state (no GPS entries)

| Step | Action | Expected Result |
|---|---|---|
| 1 | Remove lat/lng from test entry temporarily | — |
| 2 | Visit /map | Map at world zoom, "No locations yet" message shown |
| 3 | Restore lat/lng | — |

**Manual verification required:** Requires temporarily editing entry

---

### TC-2.6: Map page is full-height on mobile

| Step | Action | Expected Result |
|---|---|---|
| 1 | Open /map on mobile browser | Map fills screen below header |
| 2 | Pinch to zoom | Map zooms without page scrolling |
| 3 | Pan with finger | Map pans without page scrolling |

**Manual verification required:** Touch interaction requires physical device

---

## Milestone 3 — Statistics Page

### TC-3.1: Stats page loads

| Step | Action | Expected Result |
|---|---|---|
| 1 | GET http://localhost:8081/stats | HTTP 200 |
| 2 | Inspect HTML | Four stat blocks present |

**Automation:** curl + HTTP status + grep for "stat-block"

---

### TC-3.2: Days on the road count

| Step | Action | Expected Result |
|---|---|---|
| 1 | curl /stats | Page HTML |
| 2 | grep for "days" | Shows "1 day on the road" (entry date: 2026-06-17, today: 2026-06-18) |

**Automation:** grep stat-value output and compare to expected day count

---

### TC-3.3: Entries count

| Step | Action | Expected Result |
|---|---|---|
| 1 | curl /stats | grep for "entry posted" | Shows "1 entry posted" |

**Automation:** grep for "entry posted"

---

### TC-3.4: Countries visited

| Step | Action | Expected Result |
|---|---|---|
| 1 | curl /stats | grep for "Netherlands" | "Netherlands" appears in countries list |
| 2 | grep for "country visited" | Shows "1 country visited" |

**Automation:** grep output

---

### TC-3.5: Distance shows "—" for single GPS point

| Step | Action | Expected Result |
|---|---|---|
| 1 | curl /stats | grep for GPS_POINTS | One point in array |
| 2 | In browser, check stat-distance | Shows "—" (JS computes, needs browser) |

**Automation:** grep GPS_POINTS array length from page source

---

### TC-3.6: Stats navigation link

| Step | Action | Expected Result |
|---|---|---|
| 1 | Open any page header | "Stats" link present in nav |
| 2 | Click Stats | Navigates to /stats |

**Automation:** grep any page HTML for "/stats" in nav

---

## Milestone 4 — Mini-map on Tracker Feed

### TC-4.1: Mini-map appears on tracker feed

| Step | Action | Expected Result |
|---|---|---|
| 1 | GET http://localhost:8081/tracker | HTTP 200 |
| 2 | grep for "feed-map" | Mini-map div present |
| 3 | grep for "FEED_ENTRIES" | JSON array with Amsterdam entry |
| 4 | grep for "View full map →" | Link to /map present |

**Automation:** curl + grep

---

### TC-4.2: Mini-map hidden when no GPS entries

| Step | Action | Expected Result |
|---|---|---|
| 1 | Remove lat/lng from example entry | — |
| 2 | curl /tracker | No "feed-map" div in output |
| 3 | Restore lat/lng | — |

**Manual verification:** Requires temporarily editing entry

---

### TC-4.3: Marker click navigates to entry (mobile)

| Step | Action | Expected Result |
|---|---|---|
| 1 | Open /tracker on phone | Mini-map renders above entry list |
| 2 | Tap Amsterdam marker | Navigates to /tracker/2026-06-17.entry |

**Manual verification required:** Touch interaction requires browser

---

### TC-4.4: Entry list still visible below mini-map

| Step | Action | Expected Result |
|---|---|---|
| 1 | Open /tracker | Mini-map shows, scroll down | Entry cards visible below map |

**Manual verification required:** Visual layout check

---

## Post Submission Flow

These scenarios cover the full round-trip: filling the form → saving → verifying values in the UI and on disk. Use the exact test values specified so that each assertion can be precise.

**Test data (use verbatim):**

| Field | Value |
|---|---|
| Title | `QA Test Entry` |
| Date & Time | `2026-06-18 10:00` |
| Content | `This is the QA test body. Second sentence for length.` |
| City | `Tokyo` |
| Country | `Japan` |
| Latitude | `35.689487` |
| Longitude | `139.691711` |
| Photos | none (keep simple for first run) |

**Expected slug:** `2026-06-18-1000-qa-test-entry`
**Expected folder:** `2026-06-18-1000-qa-test-entry.entry/`
**Expected URL:** `/tracker/2026-06-18-1000-qa-test-entry.entry`

The slug is built from `date(Y-m-d-Hi)` + title lowercased with `[^a-z0-9]+` replaced by hyphens.

---

### TC-P.1: Post form requires authentication

| Step | Action | Expected Result |
|---|---|---|
| 1 | Open private/incognito tab (no session) | — |
| 2 | GET http://100.96.115.96:8081/post | Page loads at /post URL (no redirect) but renders the login form inline |
| 3 | Inspect page content | Login form fields (username, password) visible; post form fields absent |

**Automation:** curl /post without auth; assert `login-form-nonce` present AND `data[title]` absent

---

### TC-P.2: Post form renders all fields

| Step | Action | Expected Result |
|---|---|---|
| 1 | Log in at /login | Redirected to /tracker |
| 2 | Navigate to /post | Post form page loads (200) |
| 3 | Check form fields present | Title, Date & Time, description textarea, Photos upload |
| 4 | Check location fields | Latitude, Longitude, City, Country inputs visible |
| 5 | Check action buttons | `📍 Get Current Location` and `🌤 Get Weather` buttons visible |
| 6 | Check submit button | `Post Entry` button visible |
| 7 | Check date field default | Pre-filled with today's date and time (not blank) |

**Automation:** curl /post with auth; grep for `data[title]`, `data[lat]`, `data[location_city]`, `get-location`, `get-weather`

---

### TC-P.3: Required field validation

| Step | Action | Expected Result |
|---|---|---|
| 1 | Log in and open /post | Form loads |
| 2 | Leave Title blank, fill in only the description | — |
| 3 | Submit form | Page reloads with validation error on Title |
| 4 | Error message | Indicates title is required |
| 5 | Fill in Title, clear Description/Content, submit | Validation error on Content field |
| 6 | Confirm | No new entry file created in pages/01.tracker/ during failed submissions |

**Manual verification required:** Validation feedback requires browser

---

### TC-P.4: Successful post submission — all fields

| Step | Action | Expected Result |
|---|---|---|
| 1 | Log in and open /post | Form loads |
| 2 | Enter Title: `QA Test Entry` | — |
| 3 | Set Date to `2026-06-18 10:00` | — |
| 4 | Enter Content: `This is the QA test body. Second sentence for length.` | — |
| 5 | Enter City: `Tokyo`, Country: `Japan` | — |
| 6 | Enter Latitude: `35.689487`, Longitude: `139.691711` | — |
| 7 | Leave Photos empty | — |
| 8 | Click `Post Entry` | Form submits (POST to /post) |
| 9 | Observe result | Success message `Entry posted successfully!` shown on page |
| 10 | Form state | Form is reset / fields cleared |

**Manual verification required:** Form submission and success message require browser

---

### TC-P.5: Entry file created on disk with correct values

| Step | Action | Expected Result |
|---|---|---|
| 1 | After TC-P.4 completes | — |
| 2 | Check directory `user/pages/01.tracker/` | Folder `2026-06-18-1000-qa-test-entry.entry/` exists (add-page-by-form appends template name per `physical_template_name: true`) |
| 3 | Read `user/pages/01.tracker/2026-06-18-1000-qa-test-entry.entry/entry.md` | File exists |
| 4 | Verify frontmatter `title` | Equals `QA Test Entry` |
| 5 | Verify frontmatter `date` | Equals `2026-06-18 10:00` |
| 6 | Verify frontmatter `location_city` | Equals `Tokyo` |
| 7 | Verify frontmatter `location_country` | Equals `Japan` |
| 8 | Verify frontmatter `lat` | Equals `35.689487` |
| 9 | Verify frontmatter `lng` | Equals `139.691711` |
| 10 | Verify frontmatter `template` | Equals `entry` |
| 11 | Verify frontmatter `published` | Equals `true` |
| 12 | Verify page body | Contains `This is the QA test body. Second sentence for length.` |

**Automation:** Read file from filesystem; parse YAML frontmatter; assert each field value exactly

---

### TC-P.6: Entry appears in tracker feed

| Step | Action | Expected Result |
|---|---|---|
| 1 | BUG-001 fixed — no manual cache clear needed | — |
| 2 | GET http://100.96.115.96:8081/tracker | Page loads (200) |
| 3 | Entry card present | Card with title `QA Test Entry` visible |
| 4 | Date shown on card | `18 Jun 2026` |
| 5 | Location badge on card | `📍 Tokyo, Japan` visible |
| 6 | Entry card link | `href` points to `/tracker/2026-06-18-1000-qa-test-entry.entry` |
| 7 | Excerpt shown | Partial text of the body content visible |

**Automation:** curl /tracker; grep for "QA Test Entry", "18 Jun 2026", "Tokyo", "Japan", "/tracker/2026-06-18-1000-qa-test-entry.entry"

---

### TC-P.7: Entry detail page shows correct values

| Step | Action | Expected Result |
|---|---|---|
| 1 | GET http://100.96.115.96:8081/tracker/2026-06-18-1000-qa-test-entry.entry | Page loads (200) |
| 2 | Page title | `QA Test Entry` in `<h1>` |
| 3 | Date header | `Thursday, 18 June 2026` (or locale equivalent) |
| 4 | Location badge | `📍 Tokyo, Japan` |
| 5 | Body content | Full text `This is the QA test body. Second sentence for length.` rendered |
| 6 | No gallery | Photo gallery section absent (no photos were uploaded) |
| 7 | Back link | `← Back to journal` link present, points to /tracker |

**Automation:** curl /tracker/2026-06-18-1000-qa-test-entry.entry; grep for "QA Test Entry", "Tokyo", "Japan", "This is the QA test body", "Back to journal"

---

### TC-P.8: Entry appears on map and mini-map

| Step | Action | Expected Result |
|---|---|---|
| 1 | GET http://100.96.115.96:8081/tracker | Mini-map section visible |
| 2 | Inspect FEED_ENTRIES JSON | Contains entry with `lat: "35.689487"`, `lng: "139.691711"`, `title: "QA Test Entry"` |
| 3 | GET http://100.96.115.96:8081/map | Map page loads |
| 4 | Inspect ENTRIES JSON | Contains same entry |

**Automation:** curl /tracker and /map; grep FEED_ENTRIES and ENTRIES JSON for lat/lng values

---

### TC-P.9: Entry appears in stats

| Step | Action | Expected Result |
|---|---|---|
| 1 | GET http://100.96.115.96:8081/stats | Page loads (200) |
| 2 | Entries count | Shows `2` entries (existing test entry + new QA entry) |
| 3 | Countries list | `Japan` and `Netherlands` both listed |

**Automation:** curl /stats; grep entry count and country names

---

### TC-P.10: Two posts on the same day

| Step | Action | Expected Result |
|---|---|---|
| 1 | Submit a first post: date `2026-06-18 10:00`, title `Morning Update` | Success message shown |
| 2 | Submit a second post: date `2026-06-18 14:30`, title `Afternoon Update` | Success message shown |
| 3 | Check filesystem | Two separate folders exist: `2026-06-18-1000-morning-update.entry/` and `2026-06-18-1430-afternoon-update.entry/` |
| 4 | Visit /tracker | Both entries visible as separate cards |

**Note:** The slug encodes date + time + title, so same-day posts are fully supported as long as they have different times or titles. A true collision (same date, same time, same title) would silently fail — treat this as acceptable given solo use.

**Manual verification required:** Requires two browser submissions

---

## Cross-cutting Tests

### TC-X.1: Nav links present on all pages

| Page | Expected nav links |
|---|---|
| /tracker | Journal, Map, Stats |
| /map | Journal, Map, Stats |
| /stats | Journal, Map, Stats |
| /tracker/2026-06-17.entry | Journal, Map, Stats |

**Automation:** curl each page, grep for all three links

---

### TC-X.2: All pages return 200

| Page | Expected HTTP status |
|---|---|
| / (redirects to /tracker) | 200 or 302→200 |
| /tracker | 200 |
| /tracker/2026-06-17.entry | 200 |
| /map | 200 |
| /stats | 200 |
| /post | 200 (after login) or 302 (login redirect) |

**Automation:** curl HTTP status checks

---

### TC-X.3: Mobile touch targets ≥44px

| Element | Expected min height/width |
|---|---|
| Nav links | 44px height |
| Gallery thumbnails | 44px on shortest side |
| Lightbox close/prev/next buttons | 44px |
| Post form buttons | 44px height |
| "Get Location" button | 44px height |
| "Get Weather" button | 44px height |

**Manual verification required:** Inspect computed CSS or measure visually on device

---

### TC-X.4: No JS errors in browser console

| Page | Expected |
|---|---|
| /tracker | No console errors |
| /map | No console errors (may have tile 404s for tiles not in viewport — acceptable) |
| /stats | No console errors |
| /tracker/2026-06-17.entry | No console errors |

**Manual verification required:** Open browser DevTools

---

## Visual Design QA — Redesign Checklist

**Design spec:** `user/docs/design/design-spec.md`  
**Implementation plan:** `user/docs/superpowers/plans/2026-06-18-ui-redesign.md`

### Typography
- [ ] DM Serif Display loads for: entry titles, page headings (`h1`), stat numbers, site title
- [ ] DM Sans loads for: body text, nav links, labels, form fields, timestamps
- [ ] No fallback font (Georgia / system-sans) visible in place of custom fonts
- [ ] Body text font-size ≥ 16px (no iOS zoom on form focus)

### Colors
- [ ] Page background is warm paper (#F7F5F2), not pure white
- [ ] All links and CTAs use teal (#1F6B5A), not blue (#0066cc)
- [ ] Active nav link is teal and bold
- [ ] Map markers and route polylines are teal

### Header
- [ ] 3px teal border-top visible at top of header
- [ ] Site title renders in DM Serif Display ("into the east")
- [ ] Header sticks to top on scroll
- [ ] On 320px viewport: title and nav both visible without overlap

### Entry feed cards
- [ ] Cards with photos show full-bleed 16:9 image with rounded corners
- [ ] Date + location text overlay visible on gradient at bottom of photo
- [ ] Entry title below photo in DM Serif Display
- [ ] Subtle photo scale animation on hover (desktop)
- [ ] Cards without photos show date/location meta row above title
- [ ] "Read entry →" link is teal

### Single entry page
- [ ] If entry has photos: hero image spans full content width, max 480px tall
- [ ] Entry title in DM Serif Display at large size (~48px desktop)
- [ ] Thin border rule separates header from body text
- [ ] Body text at 18px (--text-md)
- [ ] "← Back to journal" footer link in teal

### Post form
- [ ] Lat/lng inputs NOT visible (hidden by CSS :has() selector)
- [ ] Inputs have rounded corners and correct border
- [ ] Focus ring on inputs is teal, not default browser blue
- [ ] "Post Entry" submit button is teal, full-width, ≥52px height
- [ ] After tapping "Get Location": status line shows "✓ Location captured · lat, lng" in teal
- [ ] After tapping "Get Weather": status line shows "✓ Weather set · desc · temp°C" in teal
- [ ] On error: status line shows in brick red, not teal

### Stats page
- [ ] Page heading "Trip Statistics" in DM Serif Display
- [ ] Stat numbers in DM Serif Display, teal color
- [ ] Stat cards on white background (not paper), with subtle shadow
- [ ] Labels uppercase, muted gray, small

### Map page
- [ ] Map fills viewport below header with no gap
- [ ] Map container height uses CSS variable (not hardcoded 61px)
- [ ] Markers are teal circles (not blue)
- [ ] Route polyline is teal

### Mobile (375px viewport)
- [ ] All pages scroll without horizontal overflow
- [ ] Header title and nav fit in one row
- [ ] Entry card photo fills full width
- [ ] Post form buttons are thumb-reachable (44px+ targets)
- [ ] Map page: map pans without page scrolling underneath (touch-action)

### Accessibility
- [ ] Focus ring visible on all interactive elements (keyboard navigation)
- [ ] With prefers-reduced-motion: no animations/transitions fire
