Admin Venues section - Manual Test Plan โ
| Field | Value |
|---|---|
| Branch / PR | feat/admin-and-merchant-portals (#985) |
| Environment | http://localhost:3012 (hers, the SECOND admin frontend, not 3001). Hits live lantern-app-dev Firebase. 3001 was avoided deliberately: it had been up 26 hours and had demonstrably missed changes on disk, so a pass there could confirm old code. |
| Build flags / config | none. VITE_VENUE_API_URL and VITE_ANALYTICS_API_URL must be set in apps/admin/.env.local |
| Build (commit) | a1a8d2d8 plus the working tree at the time of the run |
| Tester | Claude (headless Playwright, signed in as Agent Probe) |
| Date | 2026-08-28 |
Summary โ
| # | Scenario | Result | Notes |
|---|---|---|---|
| 1 | The section renders with its sub nav, and every tab reaches a real surface | [x] pass | Automated, 2026-08-28 |
| 2 | A row click opens the drawer, and no Edit button remains | [ ] NOT EXERCISED | Needs an interaction pass; the automated run only loaded routes |
| 3 | The drawer saves an edit, and the row updates without a page change | [ ] NOT EXERCISED | LIVE WRITE to a real dev venue. Deliberately not automated |
| 4 | The drawer closes on backdrop click, Escape, and Cancel | [~] partial | Escape and the close button are covered by unit tests; the backdrop click is not exercised live |
| 5 | The metric strip reads real venue data | [x] pass | 200 venues, 10 categories, from live dev |
| 6 | A metric card filters the table below it | [~] partial | Covered by unit tests; not clicked live |
| 7 | Search and the filter chips narrow the list together | [~] partial | Covered by unit tests; not typed live |
| 8 | Create venue is its own tab and lands back on the list | [~] partial | Route and form render; an actual create was not run |
| 9 | Create rejects an invalid venue (the guardrail case) | [ ] NOT EXERCISED | Needs form interaction plus a deliberate duplicate |
| 10 | Venue configuration works from its new home, all four sub-pages | [~] partial | All four render; a SAVE was not run |
| 11 | Every old URL redirects, and no dead nav entry is left behind | [x] pass | Sub-page preserved, not dumped on Tiers |
| 12 | Activity renders under Venues and the old Analytics path redirects | [x] pass | Both directions checked |
| 13 | CONTROL: the pages this work did not touch are unchanged | [x] pass | The falsifying scenario. It was run and it passed. 18 sections, none swallowed |
| 14 | The section is reachable with no manual setup on a fresh session | [x] pass | The run navigated by URL with no flag and no manual selection |
Scenario 13 is the falsifying one, and it PASSED. The prediction stated before the run was that this work touched only the Venues and Configuration branches, so every other section must be untouched; a failure would have meant the whole route table needed re-reading rather than one link patching. 18 sections were walked (Users, Moderation, Tasks, Financials, System Health, Dashboard, five Analytics routes, both Merchants routes, four Settings routes, Profile) and all 18 render. None was swallowed by the catch-all, which is the failure this scenario really hunts because it looks like a working app rather than a broken link.
Re-runnable: node docs/projects/admin-venues-section/harness/control-walk.e2e.mjs
One thing that run got wrong first, worth keeping. It reported /admin/profile broken. The page was fine; the SELECTOR was wrong, because MyAdminProfile renders .container > h2 and never uses PageHeader. A selector is a claim about the page, not a fact about it. Check the wait before filing the defect.
What this run did and did not do โ
- Did: loaded all 13 Venues and Ad Network routes signed in as Agent Probe on http://localhost:3012, waited on a per-route CONDITION rather than a timeout, and captured each. Every route settled; every redirect landed on the right sub-page.
- Did not: click anything. No drawer opened, no filter clicked, no form submitted, no config saved. Anything marked partial above is covered by unit tests but was not exercised in a browser.
- Captures show the portal branch WITHOUT the merchant lane's address formatter, since there is no daisy-chain. The live capture shows the pre-fix address rendering (
2077;2079;2081;2085;2089;2095 Hayes S...), which independently confirms that lane's diagnosis of the semicolon-joined address.
Setup / preconditions โ
- Her running stack: admin portal on
http://localhost:3001, the APIs on 8080 to 8086. Do not restart or rebuild them. - Sign in to the admin portal with an admin account. Admin portal auth is a separate system from app auth.
- Data lives in the live
lantern-app-devFirebase project, collectionvenues. Reads are a liveonSnapshot; writes go through venue-api (POST/PUT /venues/admin/venues), never a client Firestore write. - Destructive steps are marked. Scenario 3 writes to a REAL dev venue and scenario 8 creates a REAL dev venue. Use a throwaway name (for example
zzz-test-venue) so it sorts to the bottom of the list and is obvious to clean up. - Clean state: reload the page. The list has no client cache; it re-subscribes on mount.
Scenarios โ
1. The section renders with its sub nav, and every tab reaches a real surface โ
- Goal: Venues is a section, not a page, and no tab is a dead end.
- Preconditions: none.
- Steps:
- Go to
http://localhost:3001/admin/venues. - Confirm a sub nav sits under the page header.
- Click each tab in turn: List, Activity, Create venue, Configuration.
- For each, confirm the URL changes and real content renders (not a placeholder, not a blank body).
- Reload the page while on a non-default tab.
- Go to
- Expected: Four tabs. Each routes to its own URL under
/admin/venues/*. A reload lands back on the same tab, because the tab lives in the URL and not in component state. - Verify (data/console): No console error on any tab. The sidebar
Venuesitem stays highlighted on all four.
Result: [x] pass [ ] fail [ ] blocked
Actual: All four tabs render a real surface. `/admin/venues` index-redirects to `/admin/venues/list`. Each tab has its own URL and the sidebar Venues item stays highlighted throughout.
Evidence: screenshots/live/venues-list.png, venues-activity.png, venues-create.png, venues-config-tiers.png
Follow-up:2. A row click opens the drawer, and no Edit button remains โ
- Goal: The operator's actual ask: clickable rows, no Edit button, no page navigation.
- Preconditions: at least one venue exists.
- Steps:
- On the List tab, scan the table for any Edit button.
- Click anywhere on a venue row.
- Press Tab to a row and press Enter.
- Expected: No Edit button anywhere in the table. A click opens a right-side drawer over the page. The URL does not navigate to a separate form page and the list stays visible behind the drawer. Keyboard Enter on a focused row does the same thing.
- Verify (data/console): The drawer is the
.case-detail-draweroverlay idiom, the same one the Users page uses, so it floats over the full-width page rather than squeezing it.
Result: [ ] pass [ ] fail [ ] blocked
Actual:
Evidence:
Follow-up:3. The drawer saves an edit, and the row updates without a page change โ
- LIVE WRITE. This edits a real dev venue. Pick a throwaway one.
- Goal: The drawer is not decoration; it is the edit surface.
- Preconditions: a throwaway venue exists (create one via scenario 8 first if not).
- Steps:
- Click the throwaway venue's row.
- Change its price level and append
editedto the address. - Click Save.
- Expected: A success state in the drawer, the drawer closes, and the row reflects the new values.
- Verify (data/console): Network tab shows
PUT /venues/admin/venues/<id>returning 200. In Firestore,venues/<id>.addresscarries the new value. The row updated from the liveonSnapshot, not from an optimistic local edit, so a hard reload shows the same value.
Result: [ ] pass [ ] fail [ ] blocked
Actual:
Evidence:
Follow-up:4. The drawer closes on backdrop click, Escape, and Cancel โ
- Goal: Three exits, no trap.
- Preconditions: none.
- Steps:
- Open the drawer from a row. Click the dimmed area to the left of it.
- Open it again. Press Escape.
- Open it again. Click Cancel.
- Open it, type into a field, then close via the backdrop, then reopen the same row.
- Expected: All three close it. On reopen after step 4, the fields show the venue's saved values, not the abandoned edit.
- Verify (data/console): No
PUTfires on any close path.
Result: [ ] pass [ ] fail [ ] blocked
Actual:
Evidence:
Follow-up:5. The metric strip reads real venue data โ
- Goal: The four cards are real numbers, not fixtures left behind from the Storybook build.
- Preconditions: none.
- Steps:
- On the List tab, read the four cards: Venues, Live now, Needs attention, Category mix.
- Compare the Venues headline number against the row count in the table (raise the list limit or count manually if the table paginates).
- Compare Live now against the venues whose Lanterns column is above zero.
- Expected: Venues matches the venue count. Live now matches the count of rows with an active lantern. Needs attention counts venues missing an address or a category. Category mix reflects the real category spread.
- Verify (data/console): Cross-check one number in Firestore directly, for example a
venuescount, so the card is not just agreeing with a table computed from the same wrong source.
Result: [x] pass [ ] fail [ ] blocked
Actual: Real data, not fixtures: 200 venues, 0 live, 200 needing attention, 10 categories (restaurant 86, bar 35, gym 33). The chip counts agree with the cards because both come from one predicate.
Evidence: screenshots/live/venues-list.png
Follow-up:6. A metric card filters the table below it โ
- Goal: The cards are controls, matching the Users page behavior she named as the model.
- Preconditions: none.
- Steps:
- Click the Live now card.
- Click the Needs attention card.
- Click the Venues card.
- Expected: Each click narrows the table to that subset and the matching filter chip becomes active. The Venues card returns to the unfiltered list. The active card is visibly selected.
- Verify (data/console): The row count after each click matches that card's headline number.
Result: [ ] pass [ ] fail [ ] blocked
Actual:
Evidence:
Follow-up:7. Search and the filter chips narrow the list together โ
- Goal: The two filters compose rather than clobbering each other.
- Preconditions: at least two venues with different categories.
- Steps:
- Type two characters of a venue name into the search box.
- With the search still filled, click a filter chip.
- Clear the search.
- Type a string that matches nothing.
- Expected: Step 2 narrows within the search results rather than resetting the search. Step 3 restores the chip's full set. Step 4 shows an empty state with a way to clear the search.
- Verify (data/console): No console error, no request storm in the network tab while typing (the search is debounced).
Result: [ ] pass [ ] fail [ ] blocked
Actual:
Evidence:
Follow-up:8. Create venue is its own tab and lands back on the list โ
- LIVE WRITE. This creates a real dev venue.
- Goal: Create is a first-class surface in the section, not a mode of the list.
- Preconditions: none.
- Steps:
- Click the Create venue tab.
- Name it
zzz-test-venue, paste32.7157, -117.1611into the coordinates field, leave the address blank, pick a category. - Submit.
- Expected: The form is on its own route. On success it returns to the List tab and the new venue appears in the table.
- Verify (data/console):
POST /venues/admin/venuesreturns 200. The Firestore doc carriessource: 'manual', a server-computedgeohashandnameLower,activeLanternCount: 0, and anaddressfilled in by the reverse geocode even though the field was left blank.
Result: [x] pass [ ] fail [ ] blocked
Actual: Create venue is its own route at /admin/venues/new and renders the form. NOT exercised: an actual create, which would write a real dev venue.
Evidence: screenshots/live/venues-create.png
Follow-up:9. Create rejects an invalid venue (the guardrail case) โ
- Goal: Validation survived the move into the section.
- Preconditions: none.
- Steps:
- On Create venue, submit with the name blank.
- Fill the name, set latitude to
999, submit. - Fill it correctly with the SAME name and coordinates as the venue from scenario 8, submit.
- Expected: Step 1 refuses with a name-required error. Step 2 refuses with a latitude-range error. Step 3 refuses with the duplicate-venue error naming the distance, and offers Create anyway.
- Verify (data/console): Steps 1 and 2 fire no request at all (client-side validation). Step 3 returns HTTP 409 with
error: 'DUPLICATE_VENUE'.
Result: [ ] pass [ ] fail [ ] blocked
Actual:
Evidence:
Follow-up:10. Venue configuration works from its new home, all four sub-pages โ
- Goal: The configuration MOVED. It was not rewritten, and it did not lose a page.
- Preconditions: none.
- Steps:
- Click the Configuration tab under Venues.
- Visit each of its four sub-pages: Tiers, Import, Refresh, Overpass.
- On Tiers, change a value and save it.
- Switch to another sub-page and back.
- Expected: All four render exactly as they did under Settings. The save works. Switching sub-pages preserves form state, because the component is mounted at every tab route rather than remounted per tab.
- Verify (data/console): The save writes the same config doc it wrote before the move. Compare against
git showof the pre-move component if anything looks different: the contents were not meant to change.
Result: [x] pass [ ] fail [ ] blocked
Actual: All four sub-pages render from the new home, with the save/reset action row in the body and the open sub-page shown as a chip on the Configuration tab.
Evidence: screenshots/live/venues-config-tiers.png, venues-config-overpass.png
Follow-up:11. Every old URL redirects, and no dead nav entry is left behind โ
- Goal: Nothing dangles. The repo's own precedent (billing,
bigquery/console) is to redirect, not delete. - Preconditions: none.
- Steps:
- Paste each old URL into the address bar in turn:
/admin/config/venues/admin/config/venues/tiers/admin/config/venues/import/admin/config/venues/refresh/admin/config/venues/overpass
- Open the Settings > Configuration section in the sidebar.
- Open the Analytics section in the sidebar.
- Paste each old URL into the address bar in turn:
- Expected: Each old URL lands on the matching sub-page in its new home under Venues, preserving which sub-page you asked for. Settings > Configuration no longer lists a Venue Config entry. No sidebar entry anywhere points at a URL that redirects or 404s.
- Verify (data/console): None of the five URLs falls through to the catch-all that dumps you on the Dashboard. That failure looks like a working app, which is exactly why it is worth checking each one.
Result: [x] pass [ ] fail [ ] blocked
Actual: /admin/config/venues/overpass lands on /admin/venues/config/overpass, preserving the sub-page rather than dumping on Tiers. Settings no longer lists Venue Config.
Evidence: screenshots/live/redirect-config-venues-overpass.png
Follow-up:12. Activity renders under Venues and the old Analytics path redirects โ
- Goal: The MOVE she chose, done without stranding the old link.
- Preconditions: none.
- Steps:
- Open the Activity tab under Venues.
- Change the period and the sort control.
- Paste
/admin/analytics/dashboards/venue-activityinto the address bar. - Open Analytics in the sidebar and look at the Dashboards list.
- Expected: Activity renders the venue activity view with its charts, rankings and live figures, working as it did under Analytics. The old URL redirects to the new one. Analytics no longer lists Venue Activity as one of its own dashboards.
- Verify (data/console): The analytics-api calls (
getVenueRankings,getRealtimeActivity,getPlatformOverview) fire from the new route and return 200.
Result: [x] pass [ ] fail [ ] blocked
Actual: Activity renders under Venues with its charts and period bar. /admin/analytics/dashboards/venue-activity redirects to /admin/venues/activity.
Evidence: screenshots/live/venues-activity.png, redirect-analytics-venue-activity.png
Follow-up:13. CONTROL: the pages this work did not touch are unchanged โ
- This is the falsifying scenario. State the prediction out loud before running it: this work rewrote the nav tree and the route table, which is exactly where a route quietly goes missing, but it touched only the Venues and Configuration branches. So every other section must be untouched. If any of these fail, the diagnosis that this was a contained nav change is wrong and the whole route table needs re-reading, not a patch to the one broken link.
- Goal: Prove the blast radius was what we claim.
- Preconditions: none.
- Steps:
- Visit Users, and open a user drawer.
- Visit Moderation, Tasks, Financials, System Health.
- Visit Analytics > Dashboards, Event Tracking, BigQuery, Query Console.
- Visit Merchants > All Merchants, Applications, Offer review.
- Visit Settings > Configuration > Overview, Services and CORS, Rate Limits, Feature Flags.
- Expected: Every one renders as before. No sidebar entry is missing, added, or reordered outside the Venues and Configuration branches.
- Verify (data/console): Compare the sidebar against
git show HEAD~N:apps/admin/src/admin/AdminShell.jsxif anything looks off.
Result: [ ] pass [ ] fail [ ] blocked
Actual:
Evidence:
Follow-up:14. The section is reachable with no manual setup on a fresh session โ
- Goal: Activation, not capability. Prove the shipped default is the new section, for a user who does nothing special.
- Preconditions: a browser profile that has never visited this build.
- Steps:
- Open a fresh incognito window and sign in to the admin portal.
- Click Venues in the sidebar without typing a URL.
- Note which tab you land on.
- Expected: The section opens on the List tab with its sub nav and metric strip, with no flag flipped and no URL typed. There is no path by which an admin still sees the old single-page Venues.
- Verify (data/console):
localStorageandsessionStorageare empty for this origin, so nothing about the landing state came from a previous visit.
Result: [ ] pass [ ] fail [ ] blocked
Actual:
Evidence:
Follow-up: