// ai summary · method·11 Oct 2026·8 min read

Synthetic UX testing with Grok Bot

How we tested Parking NL with simulated people: three personas, 17 scripted flows against production, a think-aloud for every step grounded in the real screenshot, then fixes and the exact same scripts again.

AI summary —written by an AI agent from the test report, results and scripts in the Parking NL repo. The personas, quotes and ease scores are a simulation, not real users; the sessions, screenshots and pass/fail numbers are real measurements on production.
PlaywrightWebKit + ChromiumSynthetic personasGrok Bot
Pipeline: personas, Playwright flows on production, per-step evidence, AI think-aloud grounded in screenshots, severity-ranked findings, fix and rerun the same scripts
The loop. Everything left of the think-aloud is measured; the think-aloud and the ease score are simulated.

// why

Scripted tests said "works". People would have said "where are the results?"

Parking NL already had end-to-end tests, and they paid off. They caught a first load stuck on "Calculating prices…": two requests in one Worker isolate shared the same in-flight data promise, and the second one waited forever. The fix was to never share an in-flight load between requests, plus a regression test that fails if the page still says "Calculating prices" with no options listed. They also caught phone search losing focus: the vaul drawer didn't pass modal=false to the Radix Dialog underneath, so its focus trap stole focus from the search input.

Those are bugs with a clear yes/no. What scripted checks don't tell you is whether a commuter standing next to her car, phone in one hand, can actually compare prices and open a tariff. That's what this round was for: a cheap first pass at friction, before spending real people's time.

// 1 · personas

A persona is a test config with a goal

Each persona is a goal, a device, a language and constraints. The device and constraints become browser settings. The goal becomes the tasks.

Persona (synthetic)Device / contextGoal
Sanne, 34, commuter Delft → Den HaagiPhone 14 (WebKit), Dutch UI, location allowed; one-handed, in a hurryCheapest or free spot near work; tomorrow a full 8 h day near Delft station
Tom, 41, visitor from the UKPixel 7 (Chromium), English, location deniedZiggo Dome tomorrow 19:00 for 4 h; also Ahoy Rotterdam. Doesn't know Dutch zones or P+R
Priya, 38, PM at a fleet/navigation companyDesktop 1440×900, EnglishEvaluate the API/MCP, limits, pricing and licence terms; leave contact details

A second round added a fourth persona, Henk, 68, on an iPhone with large text. More on that below.

// 2–3 · flows and evidence

Real taps on production, measured from the DOM

The flows are one Playwright script, e2e/ux.ts: 17 of them (6 + 6 + 5), run against parking.davidbroza.dev. Sanne runs in WebKit with the iPhone 14 profile, Dutch locale and a fake geolocation in Den Haag. Tom runs in Chromium as a Pixel 7 with geolocation not granted. Priya runs on desktop and calls the REST and MCP examples exactly as written on /developers. Taps are real touchscreen taps; typing is real keystrokes.

ts
const c = await b.newContext({
  ...devices["iPhone 14"], locale: "nl-NL",
  timezoneId: "Europe/Amsterdam",
  geolocation: { latitude: 52.0799, longitude: 4.3113 },
  permissions: ["geolocation"],
  recordVideo: { dir: `${OUT}/video/p1` },
})

Every step records:

  • a screenshot, plus a video per persona;
  • time per step and per flow, and taps;
  • off-screen swipes: how often a control the task needed was off-screen and had to be scrolled into view first. That's a cheap, honest friction metric;
  • page errors, and observations pulled from the DOM (result counts, top results, which nav links exist);
  • strict success criteria, also from the DOM. Example: the 8-hour task passes only if the tariff card actually opens, "navigate" passes only if an iPhone user is offered Apple Maps or Waze, and "share" passes only if a share control exists.

// 4 · think-aloud

The AI only gets to react to what was on screen

Then Grok Bot writes each persona's think-aloud, one step at a time. The rule: every quote must be based on that step's screenshot and DOM observations. No imagined screens, no features that weren't there. A few, before the fixes:

“The date, time and 7 duration chips eat the whole sheet. '8 uur' was off-screen to the right; I only found it by swiping the row. I see exactly one result under all those controls, and the list won't scroll unless I drag the sheet all the way up.”

Sanne · tomorrow 08:30 for 8 h · synthetic

“I said no to location and now it shows me … Utrecht? 'now: Utrecht centrum'. I'm not in Utrecht. Nothing tells me why or what to do.”

Tom · location denied · synthetic

“There's only a Google Maps button. I use Apple Maps and Waze in the car.”

Sanne · navigate · synthetic

“Limits say 'Anonymous: 20 tool calls/min', but the OpenAPI spec says 20 and the API terms say 30/min + 500/day. Which one is true?”

Priya · developer docs · synthetic

At the end of their flows, each persona also gets a 0–100 "SUS-style" ease score. That's an estimate from the simulated perspective. It is not a measured SUS, and I only use it to compare before with after.

// 5 · findings

Ranked by severity, each with evidence

SevFindingFix
CriticalOn phones the bottom sheet showed day, time, 7 duration chips and 6 filter chips, and about one result. The list couldn't scroll until the sheet was dragged to full height, so comparing options and opening a card failed.When + duration collapse into one summary button ("Morgen 08:30 · 8 uur ▾") that opens the pickers; chips wrap; the list scrolls at every sheet height.
MajorChip rows cut off at the right edge with no hint there was more ("8 uur", "1 dag", "Blauwe zone").Durations wrap; the filter row fades at the edge.
MajorLocation denied → silent Utrecht results, no explanation.Banner: "Location is off. Search an address or place, or try again"; fallback labelled as an example.
MajorNavigation was Google Maps only.Apple Maps by default on Apple devices; "Open in: Google Maps · Apple Maps · Waze".
MajorNo way to share a place; the URL didn't identify it.Share button (native sheet or copy link); links carry the place and reopen its card.
MinorP+R not explained to visitors; API limits inconsistent between docs, OpenAPI and terms.P+R explainer in the card (NL/EN); docs now say 30/min + 500/day anonymous.

// 6 · fix and rerun

Same scripts, same flows: 14/17 → 17/17

After deploying the fixes, all 17 flows ran again, unchanged, on production. The only change to the test was one extra tap on the new "when" summary button before using the pickers.

PersonaTasks beforeTasks afterEase est. before → after
Sanne (iPhone, NL)3/66/658 → 79
Tom (Android, EN)6/66/666 → 80
Priya (desktop, API)5/55/580 → 83
All14/1717/17synthetic estimates, not SUS

Tom passed all six flows even before the fixes, but only by the letter of the criteria. Being dropped in Utrecht passed the check and still confused him. That's exactly the kind of thing a pass/fail script misses and the think-aloud catches.

Before: phone bottom sheet full of date, time and duration chips with one result visible
After: one 'Vandaag 09:15 · 2 uur' summary row, one filter row, results visible
Phone sheet, before → after. Controls collapsed into one summary row; results come back into view.
Before: location denied, the app silently shows Utrecht
After: location denied banner telling the user to search or try again
Location denied (Tom, Pixel 7). Before: silent Utrecht. After: a banner that says what happened and what to do.

// the real report

Round 1 report (PDF, 10 pages)

This is the full report the test produced: personas, method, every flow before and after, the observed DOM facts, the think-aloud, the findings table and the before/after evidence. It's unedited, just compressed for the web.

Tasks given

Task text is verbatim from the Playwright script (e2e/ux.ts); there were no spoken prompts. The script did the steps and checked pass/fail in the page. Success checks per task are in the PDF.

PersonaDeviceScenarioTasks
P1 Sanne (commuter, iPhone, NL)iPhone 14Opens the app next to her car in Den Haag; works near Delft station tomorrow.
  • P1F1 Eerste bezoek: begrijpen wat de app doet en goedkoopste plek bij huidige locatie (Den Haag Centrum) zien
  • P1F2 Gratis parkeren in de buurt vinden
  • P1F3 Zoeken naar werkadres 'Station Delft' (één hand), resultaten bekijken
  • P1F4 Morgen (maandag) 08:30 voor 8 uur werkdag: opties vergelijken, kaart openen en tarief begrijpen
  • P1F5 Navigeren met Apple Kaarten (iPhone-standaard) of Waze
  • P1F6 Deel de plek met collega (deel-knop)
P2 Tom (visitor, Android, EN)Pixel 7UK visitor going to a Ziggo Dome concert tomorrow 19:00 for 4 h; also checks Ahoy Rotterdam.
  • P2F1 First visit with location denied: understand what to do
  • P2F2 Search the venue 'Ziggo Dome' and land there
  • P2F3 Set tomorrow 19:00 for 4 h
  • P2F4 Find a P+R / cheap option and understand what P+R is
  • P2F5 Open the cheapest option, understand price/flags, share with friend
  • P2F6 Search 'Ahoy Rotterdam' with Enter (keyboard Go)
P3 Priya (fleet PM, desktop)Desktop 1440×900; locale en-GBProduct manager evaluating the API/MCP, limits, pricing and licence terms for business use.
  • P3F1 From the app, find developer docs: MCP URL, REST example, limits
  • P3F2 Try the REST example from the docs
  • P3F3 Connect via MCP: initialize, tools/list, tools/call find_parking
  • P3F4 Pricing: understand tiers and send an interest form
  • P3F5 Legal check: API terms + data licences (ODbL, resale)
Page 1: tasks given per persona
Page 6: severity-ranked findings
Page 8: before / after evidence

Pages 1, 5 and 7. Click to open the full PDF.

Download round 1 report (PDF, 0.5 MB) ↓

// what we changed because of the test

Finding → shipped fix

All of these fixes shipped in one Parking NL commit, f1ca657 ("Synthetic persona UX test (report + PDF); fixes: list-first phone sheet, wrapping durations, location-off banner, Apple Maps/Waze, share + deep link, P+R explainer, consistent API limits"). The screenshots are the same Playwright step before and after that deploy.

F1 · Collapsed "when" button, list scrolls at every sheet height

The date, time and 7 duration chips used to fill the phone sheet, leaving about one result, and the list wouldn't scroll until the sheet was dragged to full height. Now arrival and duration are one summary button ("Morgen 08:30 · 8 uur ▾") that opens the pickers, and the list scrolls at every height. Sanne's 8-hour task went from ❌ to ✅, with 2 → 1 off-screen swipes.

Before: Morgen, 08:30 and a row of duration chips cut off at the edge, one result visible
After: one 'Morgen 08:30 · 8 uur' button, one filter row, two results visible
Same step (P1F4, tomorrow 08:30 for 8 h), before → after.

F2 · Durations wrap, filter row fades at the edge

"8 uur" and "1 dag" were cut off at the right edge with no hint there was more. The durations now wrap inside the picker, so none are hidden, and the filter row gets a fade at the right edge. You can see the fade after "Terrein" in the after shot above.

F3 · Location-off banner

With location denied, Tom silently got Utrecht. Now a banner explains it, and the fallback is labelled as an example:

yaml
loc_off:     "Location is off. Search an address or place, or"
loc_try:     "try again"
example_loc: "example: Utrecht centrum"
Before: location denied, Utrecht results with no explanation
After: 'Location is off' banner with a try-again link
P2F1 on a Pixel 7 with location denied, before → after.

F4 + F5 · Apple Maps / Waze row and a Share button

Navigation was Google Maps only, and there was no way to share a place. The main button now opens Apple Maps on Apple devices and Google elsewhere, with an "Open in: Google Maps · Apple Maps · Waze" row. A Share button opens the native share sheet or copies the link. Links now carry the place (a=<id>) and reopen its card. Sanne's navigate and share tasks both went from ❌ to ✅.

Before: card with only a Navigate button
After: card with Open in Google Maps, Apple Kaarten/Maps, Waze and a Share button
Same card (P2F5, free parking for 4 h from 19:00), before → after.
Before: the tariff card never opened
After: P6 TU Delft, €10 for 8 hours, Navigate, Open in Google Maps / Apple Maps
P1F4 on iPhone: the tariff card now opens, with Apple Maps next to Google.

F6 · P+R explained in the card

Tom found P+R Duivendrecht at €10.76 but nothing explained what P+R is. P+R cards now carry a short explanation in Dutch and English. That text is in the card, below where the test screenshots end, so here's the shipped string:

yaml
pr_explain: "Park + Ride: park at the edge of town and continue
             by public transport. Often cheaper when …"

F7 · One set of API limits everywhere

The docs and OpenAPI said 20 per minute, but the terms and the actual limiter said 30 per minute plus 500 per day. The /developers page and the OpenAPI description now match the limiter:

diff
- Anonymous: 20 tool calls / minute per IP …
+ Anonymous: 30 calls / minute and 500 / day per IP …
+ API key:   600 calls / minute and 50,000 / day …

The report left four minor items open. A later commit, a201def ("Landmark index, display names, blue zone explainer, …"), went after three of them: landmark search, clean display names and the blue-zone explainer. The draft legal pages still need a lawyer.

// round 2 · clarity

Four personas, one question: is it obvious?

Round 2 ran the same way, scripted Playwright sessions on production, but asked a different question. Can people tell what they're looking at? It added a fourth persona, Henk, 68: non-technical, on an iPhone with large text (the page is rendered at 130% text size to approximate iOS "Larger Text"). It also took screenshots at zoom 12, 14 and 16 in Den Haag, Amsterdam and Haarlem.

BeforeAfter
Tasks passed7/28 (25%)38/40 (95%)
Tasks per personaT1–T7T1–T10 (adds T8 walking ranking, T9 isochrones, T10 door-to-door)

The before run had 28 tasks (7 tasks × 4 personas). The after run had 40, because three new tasks tested the new walking and door-to-door features. On the same 28 tasks, after the fixes, 27 passed.

Top confusions (before)

  • No "best option": you had to read the whole list, and distances were straight-line metres.
  • Restrictions used red and amber, colliding with the price colours (red meant both "> €20" and "no parking").
  • Occupancy colours on garage badges reused the price ramp.
  • On phones the legend was tall and always open (about 45% of the screen), with no explanation of "approximate" or "may have changed".
  • Clutter at zoom 12–14: labels, dots and hatching everywhere.

What changed

  • One visual language. Price is shown only as zone fill (colour-blind-safe ramp, ΔE checked). Occupancy is only a ring on a blue "P" badge. Restrictions are a subtle dark hatch, behind a toggle, and only appear from zoom 16.
  • Collapsible "Legenda" chip, which takes 0% of the screen until you open it, plus "What do the labels mean?" help in the list and in every card.
  • "Best option" card on top (best balance, cheapest within N min walk, or closest under €X), real walking minutes, and drive + walk door-to-door totals.
  • Walking isochrones: dark rings with a white halo, labelled 5/10/15 min, with a one-tap "5·10·15" chip on the map.
  • iOS-style blue location dot. The red pin now means the destination only. Plus a one-line hint on first visit.
Round 2 before: plain list, legend always open over the map
Round 2 after: 'Beste optie' card on top, collapsed Legenda chip, first-visit hint
T1, first 10 seconds on iPhone: the best option is now a card at the top; the legend is a chip.
Round 2 before: red restriction popup, permit-only area in red like expensive zones
Round 2 after: garage badge with '190 vrij · €8,00', price only in zone fill
Garage badges and restrictions: no more red-on-red, and the badge says '190 vrij · €8,00'.

The bug the screenshots caught: outlines only

The zone screenshots showed Scheveningen with outlines but no colour. MapLibre was rejecting the zones-fill layer because a zoom expression was nested inside a "case". The fix shipped with a style validation unit test and an e2e check (zones-shots.ts) that fails if no fill renders.

Scheveningen before: zone outlines only, no price fill
Scheveningen after: zones filled by price, green free, orange paid
Scheveningen, before → after the zones-fill fix.
Amsterdam on iPhone with 5 and 10 minute walking rings and the 5·10·15 chip
After: walking rings around the destination, the blue location dot, and the 5·10·15 chip.

The two remaining fails

  • Henk, large text, T8: with large text, the ranking chips sit below the fold at the drawer's default height. The script's click timed out after 30 s, and a real Henk would have to drag the drawer up first. Not fixed yet.
  • Desktop, T2: "Best balance" ranked a free street zone with a 14-minute walk above a paid spot right at the door, so the "cheapest legal option ranked first" check failed. That's by design, and "closest" mode is available, but it's an honest disagreement between the ranking and the test.
  • Also flagged: on Sunday, Den Haag's "Kernwinkelstraten" zone shows as the best free option. That's what RDW says, but the shopping streets may be car-free in practice.
Tasks given

Task text is verbatim from the Playwright script (e2e/ux2.ts); there were no spoken prompts. The script did the steps and checked pass/fail in the page. Success checks per task are in the PDF. All four personas got the same tasks; T8–T10 were added in the after run.

PersonaDeviceScenarioTasks
Sanne (34, commuter, NL)iPhone 14Den Haag Centrum (5-second test also at Dam Amsterdam, Grote Markt Haarlem)
  • T1 First 10 s: can I see the best option (name + price) without scrolling or tapping?
  • T2 Cheapest legal option near destination is ranked first and fully priced
  • T3 Garage badge: tap a garage on the map, does it explain the numbers (free / total spaces)?
  • T4 Legend: find what the colours mean; is it readable (≥11 px) and not covering > 8 % of the screen?
  • T5 Restrictions vs price: are prohibited areas distinguishable from expensive zones (not both red)?
  • T6 'approximate' and 'may have changed': is there a plain-language explanation one tap away?
  • T7 Onboarding: does a first-visit hint say what the colours/list mean?
  • T8 Walk: rows show real walking minutes; can switch to 'cheapest within 10 min walk'
  • T9 Walk rings: 5/10/15-min isochrones visible around the destination
  • T10 Door-to-door: from my location (Delft) to Den Haag Centrum: drive + walk + total and trade-offs shown
Henk (68, non-technical, NL, large text)iPhone 14 · 130 % textDen Haag Centrum (5-second test also at Dam Amsterdam, Grote Markt Haarlem)
  • T1 First 10 s: can I see the best option (name + price) without scrolling or tapping?
  • T2 Cheapest legal option near destination is ranked first and fully priced
  • T3 Garage badge: tap a garage on the map, does it explain the numbers (free / total spaces)?
  • T4 Legend: find what the colours mean; is it readable (≥11 px) and not covering > 8 % of the screen?
  • T5 Restrictions vs price: are prohibited areas distinguishable from expensive zones (not both red)?
  • T6 'approximate' and 'may have changed': is there a plain-language explanation one tap away?
  • T7 Onboarding: does a first-visit hint say what the colours/list mean?
  • T8 Walk: rows show real walking minutes; can switch to 'cheapest within 10 min walk'
  • T9 Walk rings: 5/10/15-min isochrones visible around the destination
  • T10 Door-to-door: from my location (Delft) to Den Haag Centrum: drive + walk + total and trade-offs shown
Tom (tourist, EN)Pixel 7Den Haag Centrum (5-second test also at Dam Amsterdam, Grote Markt Haarlem)
  • T1 First 10 s: can I see the best option (name + price) without scrolling or tapping?
  • T2 Cheapest legal option near destination is ranked first and fully priced
  • T3 Garage badge: tap a garage on the map, does it explain the numbers (free / total spaces)?
  • T4 Legend: find what the colours mean; is it readable (≥11 px) and not covering > 8 % of the screen?
  • T5 Restrictions vs price: are prohibited areas distinguishable from expensive zones (not both red)?
  • T6 'approximate' and 'may have changed': is there a plain-language explanation one tap away?
  • T7 Onboarding: does a first-visit hint say what the colours/list mean?
  • T8 Walk: rows show real walking minutes; can switch to 'cheapest within 10 min walk'
  • T9 Walk rings: 5/10/15-min isochrones visible around the destination
  • T10 Door-to-door: from my location (Delft) to Den Haag Centrum: drive + walk + total and trade-offs shown
Priya (fleet PM, EN)Desktop 1440×900; en-GBDen Haag Centrum (5-second test also at Dam Amsterdam, Grote Markt Haarlem)
  • T1 First 10 s: can I see the best option (name + price) without scrolling or tapping?
  • T2 Cheapest legal option near destination is ranked first and fully priced
  • T3 Garage badge: tap a garage on the map, does it explain the numbers (free / total spaces)?
  • T4 Legend: find what the colours mean; is it readable (≥11 px) and not covering > 8 % of the screen?
  • T5 Restrictions vs price: are prohibited areas distinguishable from expensive zones (not both red)?
  • T6 'approximate' and 'may have changed': is there a plain-language explanation one tap away?
  • T7 Onboarding: does a first-visit hint say what the colours/list mean?
  • T8 Walk: rows show real walking minutes; can switch to 'cheapest within 10 min walk'
  • T9 Walk rings: 5/10/15-min isochrones visible around the destination
  • T10 Door-to-door: from my location (Delft) to Den Haag Centrum: drive + walk + total and trade-offs shown
Round 2 report page 2: tasks given
Round 2 report page 1: summary, confusions, changes, remaining
Round 2 report page 6: before / after screenshots

Round 2 report, pages 2, 1 and 6. Click to open the full PDF.

Download round 2 report (PDF, 0.7 MB) ↓

// round 3 · information hierarchy

"It's super busy": a redesign driven by the tests

My brief after round 2 was: "It's super busy. Focus on information hierarchy, and use the usability test results as input." Round 3 added a fifth persona: Lisa, a parent driving from Pijnacker to an event at Ahoy, who needs a door-to-door answer. It ran the same 59 tasks on the shipped UI and then on the redesign.

The baseline had every feature but a crowded first view: 10–12 floating controls on a phone and 33 on desktop, 18 of our own map layers, and 2 taps to start navigating. The redesign uses three levels. What you always see: search, one best card, the map. One tap away: the list, sort and time. On demand: filters, walking rings, restrictions and the legend, all in one map menu.

Round 3BeforeAfter
Tasks passed48/5955/59
Taps to start navigation21
Time to first decision (median)2.3 s1.4 s
5-second comprehension5/55/5
Floating controls on a phone6–74

One tap opens your default maps app from the card's navigate icon; choosing Apple Maps, Google Maps or Waze is still 2 taps, inside the detail card. The only failure left on phones was the strict "≤ 3 floating controls" check: the phone had 4 (search, ×, map menu, and the attribution ⓘ).

Before the redesign: legend, hint toast, walking rings, theme and language buttons, attribution text and a large best card all on the first screen
After the redesign: search, one map menu button, context line with Wijzig, and one best card with a chevron and navigate icon
Sanne's first view at Den Haag Centraal, before → after the information-hierarchy redesign.
Tasks given

Task text is verbatim from the Playwright script (e2e/ux3.ts); there were no spoken prompts. The script did the steps and checked pass/fail in the page. Success checks per task are in the PDF. Same script before and after the redesign.

Every persona:

  • C1 5-second comprehension: where, when, best option, price, walk, why
  • C2 Time to first decision (best option with price visible)
  • C3 Busyness of the first view
  • N1 Taps to start navigation to the best option
  • L2a Change time/duration
  • L2b Switch ranking to cheapest; why-line updates
  • L2c Open the ranked list (sheet up / list visible)
  • L3a Find walking rings
  • L3b Find restrictions / legend explanation
  • L3c Share the best option
PersonaDeviceScenarioExtra tasks
Sanne (34, commuter, iPhone, NL)iPhone 14Destination Den Haag Centraal.
  • P1 Installed (standalone): no install banner, safe-area header
  • P2 Offline: reopen the app without network
  • D1 Tap the sheet handle to switch between map view and list view
Tom (visitor, Android, EN, location off)Pixel 7Opens the app with no location, searches the Rijksmuseum.
  • S1 Search 'Rijksmuseum' and keep context visible
Priya (fleet PM, desktop, EN, + MCP)Desktop 1440×900; en-GB; no locationDestination Utrecht Centraal; also checks the MCP answer.
  • M1 MCP: same best option via find_parking
Henk (68, iPhone, large text, NL)iPhone 14Destination Markt Delft.
  • D1 Tap the sheet handle to switch between map view and list view
  • A1 Large text: tap targets ≥ 44 px and nothing clipped
Lisa (parent, iPhone, NL, Pijnacker → Ahoy event, door-to-door)iPhone 14Event at Rotterdam Ahoy, Friday 16 Oct 19:00 for 3 h; searches “Ahoy”.
  • S1 Search 'Ahoy' and keep context visible
  • DD Door-to-door from Pijnacker: drive + walk + total on the best option

// round 4 · modes and "price here"

77/79, and a change in how we count

Round 4 kept the five personas and the round 3 script, and added four checks. Is the mode clear (my location / destination / map area)? Does the card show the price here first and then a cheaper alternative? Are there type icons? Does the sheet chevron work? It also checked that door-to-door only switches on when you ask for it. Result: 55/59 → 77/79 tasks passed. Time to first decision fell for four of the five personas (Henk: 1,357 → 938 ms; Lisa: 1,073 → 701 ms). Priya went from 1,341 to 1,413 ms.

To be honest about the measurement: between rounds 3 and 4 I changed how the floating-control check counts. It now leaves out the map attribution, which is legally required, and the desktop side panel. Part of the improvement in that check comes from that change, not from the UI. Even so, the check still fails twice: for Priya on desktop, and for Henk with large text.

Bugs found and fixed during the round:

  • Switching to "Mijn locatie" from destination mode did nothing while it waited for a fresh GPS fix. It now uses the last known fix at once.
  • Flying the map to your location flipped the mode to "map area". The app now ignores camera moves it makes itself for 3 s.
  • The GPS dot was announced as a button. It's now an image and not focusable.
  • Drive time appeared without a starting point. Door-to-door is now opt-in.
Tasks given

Task text is verbatim from the Playwright script (e2e/ux4.ts); there were no spoken prompts. The script did the steps and checked pass/fail in the page. Success checks per task are in the PDF. Round 3 tasks plus MO1, H1, I1 and SC1.

Every persona:

  • C1 5-second comprehension: where, when, best option, price, walk, why
  • C2 Time to first decision (best option with price visible)
  • C3 Busyness of the first view
  • N1 Taps to start navigation to the best option
  • MO1 Mode is clear (Mijn locatie / Bestemming / Kaartgebied) and matches the context
  • H1 Here-price card: price of THIS place first, cheaper alternative with saving + extra walk
  • I1 Type icons on card + list (no coloured dot / 'Straat' text)
  • SC1 Sheet chevron: visible 44 px control toggles peek ↔ expanded
  • L2a Change time/duration
  • L2b Switch ranking to cheapest; why-line updates
  • L2c Open the ranked list (sheet up / list visible)
  • L3a Find walking rings
  • L3b Find restrictions / legend explanation
  • L3c Share the best option
PersonaDeviceScenarioExtra tasks
Sanne (34, commuter, iPhone, NL)iPhone 14Destination Den Haag Centraal.
  • P1 Installed (standalone): no install banner, safe-area header
  • P2 Offline: reopen the app without network
  • D1 Tap the sheet handle to switch between map view and list view
Tom (visitor, Android, EN, location off)Pixel 7Opens the app with no location, searches the Rijksmuseum.
  • S1 Search 'Rijksmuseum' and keep context visible
Priya (fleet PM, desktop, EN, + MCP)Desktop 1440×900; en-GB; no locationDestination Utrecht Centraal; also checks the MCP answer.
  • M1 MCP: same best option via find_parking
Henk (68, iPhone, large text, NL)iPhone 14Destination Markt Delft.
  • D1 Tap the sheet handle to switch between map view and list view
  • A1 Large text: tap targets ≥ 44 px and nothing clipped
Lisa (parent, iPhone, NL, Pijnacker → Ahoy event, door-to-door)iPhone 14Event at Rotterdam Ahoy, Friday 16 Oct 19:00 for 3 h; searches “Ahoy”.
  • S1 Search 'Ahoy' and keep context visible
  • DD Door-to-door from Pijnacker: drive + walk + total on the best option

// accessibility audit

WCAG 2.2 AA: 71 serious issues → 0

Not synthetic: an automated axe-core run against the WCAG 2.2 AA rules. It covered the app (map, detail card, phone search) and ten subpages, in light and dark, on a Pixel 7 and on desktop. Serious or critical violations went from 71 to 0. Lighthouse mobile accessibility scored 98. The biggest fixes:

RuleNodes beforeFix
color-contrast63Darker amber notes, occupancy colours with dark-mode variants, theme-aware /about
scrollable-region-focusable50Scrollable code blocks and tables are keyboard-focusable
label-content-name-mismatch46Accessible names contain the visible text (search, language, map menu)
select-name / aria-valid-attr-value / link-in-text-block8 / 4 / 8Labelled selects, valid combobox aria-controls, underlined attribution links

Also checked: keyboard order and visible focus, a skip link, 200% text at 320 px with no horizontal scroll, reduced motion, and a colour-blind simulation of the map palette (no confusable pairs). Screen-reader behaviour was only checked through the accessibility tree. No real VoiceOver or TalkBack session was run. The axe check is now one of the seven deploy checks, so any serious violation rolls the deploy back.

// next step

Next: real people, in the real app, with Verbatim

Synthetic personas find the obvious friction quickly. They can't tell me what a real driver expected, or why they hesitated. So the next round uses Verbatim, an async AI interviewer I built, plus a small SDK panel that runs inside Parking NL itself. Every participant gets the same core questions, word for word. The AI only adds guarded follow-ups, and it never gives hints.

  • Invite. A personal, single-use link with no account. It's bound to the first browser that consents and expires after 14 days. I send the links myself. Verbatim never sends anything.
  • Intro questions. Consent first, then a short interview in chat, typed or as a voice note.
  • Tasks in the live app. "Start the test" opens parking.davidbroza.dev in the same tab with the Verbatim panel. It shows one task at a time, with I'm stuck, Done and a chat bubble. After each task the participant rates ease 1–7 and says what was hard.
  • Said vs did. Results show a task funnel (started → interacted → goal → done), median times, click heatmaps per task and page, SEQ and UMUX-Lite, and themes with quotes. Each interview has a privacy-masked session replay synced to the transcript, plus an AI "said vs did" summary. Quotes are checked word for word against the transcript.

The SDK does nothing for normal visitors. It only switches on for invited ?vb= sessions, and it masks inputs by default.

Phone, 2:50. Intro questions, then tasks in Parking NL with the panel.
Desktop, 2:14. The same study, with the panel docked bottom-right.

Honest status: both videos are a synthetic test run of the participant flow. The answers are scripted, not from a real person. There are no real-user results yet. They'll get their own write-up when they exist.

How the two fit together: synthetic personas run first and on every change, because they're cheap and repeatable and catch regressions. Verbatim comes after, with a handful of real people per segment, to find out what the scripts can't tell me: expectations, wording and reasons. More on Verbatim pricing and for developers.

// limits

Simulated people are not users

  • The personas never get tired, distracted or annoyed in surprising ways. They only notice what the script makes them look at. A real person will find problems no flow was written for.
  • The quotes are written by a model. Grounding them in screenshots keeps them honest about what was there, not about how a real person would feel about it.
  • The ease scores are estimates. They're useful for before/after direction, not as a usability benchmark.
  • The run was on a Sunday morning, when many street zones in Den Haag are free. That's why the first screens show a lot of "Gratis"; the 8-hour and concert tasks deliberately set a weekday or an evening.

What it is good for: catching obvious friction fast and cheap. It took one morning and a handful of fixes to go from a core phone task that failed to one that works. The next step is the real one: about five real people per segment (commuters in Den Haag and Delft, event visitors, a fleet PM) before calling it done.

Try the result at parking.davidbroza.dev — or read what Parking NL is →

Note: this article was written by an AI from the test report and scripts. The test sessions and numbers are real; the personas, quotes and ease scores are synthetic. The prose isn't hand-written.