From 5b6a8ff5d5734e895e00f86a9b359a7e494f96f6 Mon Sep 17 00:00:00 2001 From: lorentz Date: Sun, 3 May 2026 18:17:18 -0400 Subject: [PATCH] test(04): persist human verification items as UAT --- .../phases/04-tickets-restyle/04-HUMAN-UAT.md | 74 +++++++ .../04-tickets-restyle/04-VERIFICATION.md | 206 ++++++++++++++++++ 2 files changed, 280 insertions(+) create mode 100644 .planning/phases/04-tickets-restyle/04-HUMAN-UAT.md create mode 100644 .planning/phases/04-tickets-restyle/04-VERIFICATION.md diff --git a/.planning/phases/04-tickets-restyle/04-HUMAN-UAT.md b/.planning/phases/04-tickets-restyle/04-HUMAN-UAT.md new file mode 100644 index 0000000..343e2a9 --- /dev/null +++ b/.planning/phases/04-tickets-restyle/04-HUMAN-UAT.md @@ -0,0 +1,74 @@ +--- +status: partial +phase: 04-tickets-restyle +source: [04-VERIFICATION.md] +started: 2026-05-03T00:00:00Z +updated: 2026-05-03T00:00:00Z +--- + +## Current Test + +[awaiting human testing] + +## Tests + +### 1. Filter strip default-collapsed and expand/collapse +expected: | + Open `/mobile/tickets` on iPhone 15 Pro emulation (393×852). + - On load: filter strip is collapsed (only "Filters" toggle and search visible) + - Tap "Filters" → strip expands smoothly revealing status chips, priority chips, queue Select, and "Assigned to me" Switch + - Tap "Filters" again → collapses +result: [pending] + +### 2. URL deep-link hydration +expected: | + - Navigate to `/mobile/tickets?priority=2&mine=1` directly + - Filters hydrate: "High" priority chip selected, "Assigned to me" toggled on + - Adjust a filter — URL updates via router.replace (no new history entry) + - Browser back exits the page rather than reverting the filter mutation +result: [pending] + +### 3. IntersectionObserver infinite scroll +expected: | + - Load page, scroll to bottom of first ~25 rows + - Next page (~25 more) auto-loads when last row is ~200px from viewport + - No double-fetch when scrolling fast + - When `nextCursor` is null, no further fetches occur +result: [pending] + +### 4. Load more fallback button (accessibility) +expected: | + - Tab key reaches the "Load more" button below the sentinel + - Press Enter or Space triggers the next page fetch + - Button hides when `hasMore` is false +result: [pending] + +### 5. Priority stripe colors render correctly +expected: | + - Critical (priority=1) row has a red left border (`border-red-500`) + - High (priority=2) → orange left border + - Medium (priority=3) → amber left border + - Low (priority=4) → slate left border + - Confirms Tailwind didn't purge dynamic priority classes +result: [pending] + +### 6. Detail page header layout +expected: | + - Tap a ticket row → navigate to `/mobile/tickets/[id]` + - Shell HeaderBar (Wulf mark + bell + avatar) renders at top + - Below it: new in-page header with back chevron + "Tickets" + breadcrumb "Tickets / #{number}" + external-link icon + - Tapping back chevron returns to list (preserving filter URL state) + - Tapping external-link icon opens desktop ticket page in new tab + - No double-border or visual overlap between the two header levels +result: [pending] + +## Summary + +total: 6 +passed: 0 +issues: 0 +pending: 6 +skipped: 0 +blocked: 0 + +## Gaps diff --git a/.planning/phases/04-tickets-restyle/04-VERIFICATION.md b/.planning/phases/04-tickets-restyle/04-VERIFICATION.md new file mode 100644 index 0000000..89648b5 --- /dev/null +++ b/.planning/phases/04-tickets-restyle/04-VERIFICATION.md @@ -0,0 +1,206 @@ +--- +phase: 04-tickets-restyle +verified: 2026-05-03T22:15:02Z +status: human_needed +score: 5/5 must-haves verified +gaps: [] +human_verification: + - test: "Filter strip collapse/expand on real device or DevTools phone emulation" + expected: "Opening /mobile/tickets shows collapsed filter strip (search input + Filters button only); tapping Filters reveals status chips, priority chips, queue Select, and Assigned-to-me Switch" + why_human: "Collapsible open/close behavior and touch interaction require a browser with rendered DOM — cannot be verified from source code alone" + - test: "URL deep-link round-trip on reload" + expected: "Navigating to /mobile/tickets?priority=2 hydrates the filter strip with High selected; the list shows only priority-2 tickets; back button leaves /mobile/tickets rather than reverting filters" + why_human: "router.replace history behavior and URL param hydration require a running Next.js app in a real browser" + - test: "IntersectionObserver infinite scroll trigger" + expected: "Scrolling to within ~200px of the list end automatically fetches the next 25 tickets and appends them without a page navigation" + why_human: "IntersectionObserver is browser-native; rootMargin behavior cannot be simulated from static code inspection" + - test: "Load more button keyboard accessibility" + expected: "Tab key reaches the Load more button when hasMore is true; pressing Enter/Space triggers the next page fetch" + why_human: "Focus management and keyboard interaction require a rendered browser" + - test: "Priority stripe colors rendered correctly per priority value" + expected: "Priority 1 rows show red left border, priority 2 orange, priority 3 amber, priority 4 slate — matching the locked D-15 color map" + why_human: "Tailwind class purge in production build could drop dynamic class strings unless all four are explicitly referenced — visual confirmation validates they render" + - test: "Detail page three-slot header renders correctly under shell HeaderBar" + expected: "On /mobile/tickets/[id] the Wulf mark + Bell + avatar HeaderBar renders at top, then the in-page header with ArrowLeft+Tickets / breadcrumb / ExternalLink icon below it, then the unchanged body" + why_human: "Nested header layout and safe-area spacing require a rendered browser to confirm no visual overlap or double-border" +--- + +# Phase 4: Tickets Restyle Verification Report + +**Phase Goal:** A manager triages tickets on a phone with a collapsible filter bar that deep-links via URL, priority-coloured rows, and infinite scroll — and the detail page header matches the new shell. +**Verified:** 2026-05-03T22:15:02Z +**Status:** human_needed +**Re-verification:** No — initial verification + +## Goal Achievement + +### Observable Truths (from Roadmap Success Criteria) + +| # | Truth | Status | Evidence | +|---|-------|--------|----------| +| 1 | Tickets page opens with filter strip collapsed; expanding reveals status, priority, queue, mine toggle; changing any filter updates the URL (deep link works on reload) | ✓ VERIFIED | `TicketFilterStrip` uses `useState(false)` as Collapsible default; `router.replace()` called in filter effect; `parseFilterFromSearch()` hydrates state from `useSearchParams()` on mount; `filterToSearch()` serializes all 5 params | +| 2 | Each list row has a left-edge stripe matching priority (Critical/High/Medium/Low → red/orange/amber/slate) and shows ticket #, title, company, age, and assignee | ✓ VERIFIED | `PRIORITY_BORDER` record maps `{1: 'border-red-500', 2: 'border-orange-400', 3: 'border-amber-400', 4: 'border-slate-300'}`; `border-l-4` applied to Link wrapper; row renders `ticket_number`, `title`, `company_name`, `relTime(last_activity_date)`, initials avatar from `assigned_to` | +| 3 | Single-tapping a row navigates to `/mobile/tickets/[id]` | ✓ VERIFIED | Each row is a `` — native Next.js navigation | +| 4 | Scrolling to the bottom auto-loads next ~25 rows (no Next button); a Load more fallback button is visible/focusable for accessibility | ✓ VERIFIED | `IntersectionObserver` with `rootMargin: '200px'` on `sentinelRef`; guard `loadingMore \|\| !hasMore`; focusable `}` | ✓ | +| D-05: `router.replace()` not `router.push()` | `router.replace(...)` in filter effect; no `router.push` | ✓ | +| D-06: URL param keys q/status/priority/queue/mine | All 5 serialized/deserialized | ✓ | +| D-07: Cursor NOT in URL | `filterToSearch()` has no cursor field; cursor only in `buildApiParams()` | ✓ | +| D-08: `?cursor=&limit=25` replaces `?page=N&limit=30` | No `OFFSET`, no `?page=`, cursor param accepted | ✓ | +| D-09: Cursor is `base64(JSON({last_activity_date, id}))` snake_case | `encodeCursor`/`decodeCursor` helpers in route | ✓ | +| D-10: API returns `{tickets, nextCursor, hasMore}` | `satisfies MobileTicketListResponse` in return | ✓ | +| D-11: Page size 25, server-side cap | `Math.min(25, Math.max(1, parseInt(...)))` | ✓ | +| D-12: `IntersectionObserver` with `rootMargin: '200px'` | Exact string present in `page.tsx` | ✓ | +| D-13: Guard against duplicate fetches | `if (loadingMore \|\| !hasMore \|\| !nextCursor) return` | ✓ | +| D-14: Focusable Load more button always present when hasMore | `{hasMore &&