Skip to content

feat(patient): symptom-based search with autocomplete - #165

Open
Rafiat30 wants to merge 4 commits into
DocForum:mainfrom
Rafiat30:feat/symptom-based-search-with-autocomplete
Open

Rafiat30 wants to merge 4 commits into
DocForum:mainfrom
Rafiat30:feat/symptom-based-search-with-autocomplete

Conversation

@Rafiat30

@Rafiat30 Rafiat30 commented Oct 19, 2026 •

Copy link
Copy Markdown

Closes #30

What this does

Implements the W2 patient core flow (PRD §4 — a patient finds the doctor who can treat what's wrong): symptom-based doctor search with an accessible autocomplete on a new page, /patient/search, linked from the patient header nav and dashboard.

  • Symptom search input with accessible autocomplete (combobox pattern, debounced). A new reusable Combobox implements the WAI-ARIA 1.2 combobox pattern properly: the role="combobox" input keeps DOM focus at all times, aria-expanded / aria-controls / aria-autocomplete="list" are wired to the listbox, the highlighted option is communicated with aria-activedescendant (never by moving focus), Arrow/Home/End/Enter/Escape/Tab all behave per the APG, and a polite role="status" live region announces suggestion counts, the active option, loading and no-match states. Suggestions come from the backend's controlled vocabulary (GET /symptoms?q=) — free text never reaches the doctor-search endpoint, and typing is debounced (250 ms).
  • Shows which specialties the symptom maps to. After picking a symptom, GET /symptoms/:slug returns its specialties and the page shows "Showing doctors in Cardiology and General practice." (with a Clear button and a matching live-region announcement).
  • Combined with the availability filter, state kept in the URL. The applied symptom slug and the availability filter (Any time / This week / Today) live in the URL (?symptom=chest-pain&availability=today), so a search is shareable and survives back/forward. Values parsed from the URL are validated; invalid ones are dropped with a visible notice and never sent to the backend.
  • Cancel in-flight requests on new keystrokes. The suggestion query key is the debounced text and the query function forwards React Query's AbortSignal to fetch; when a newer keystroke supersedes a fetch, cancelQueries aborts the old one — proven by a test asserting signal.aborted === true.
  • No-match state with guidance. Zero results shows exactly why ("None of the doctors in Cardiology and General practice have open slots today.") plus concrete ways out: widen to any availability (one click), and try a different symptom. An unknown/stale symptom slug (404) gets its own explanation instead of a generic error.
  • Doctor search underneath (issue [Phase W2] Doctor search: specialty + availability filter #1, a dependency still open). Since [Phase W2] Doctor search: specialty + availability filter #1 hadn't landed, this PR also adds the doctor-search page/service/mocks it needs: GET /doctors?specialtyIds=…&availability=… returning doctors in any mapped specialty, soonest-available first, verified only.

All data goes through docforum-core's API via src/services/ (ARCHITECTURE_ESSENTIALS.md hard rules 1–3); assumed shapes are documented under "Known assumptions — Phase W2 — Doctor search & symptom-based search". DoctorSearchResult reuses the existing DoctorAvailability shape rather than inventing a parallel one.

Files

New files

  • src/components/Combobox.tsx, Combobox.module.css — accessible combobox/listbox
  • src/features/search/search-params.ts — URL state parse/serialize + validation
  • src/features/search/specialty-list.ts — "A, B and C" formatting
  • src/features/search/hooks/useSymptomSuggestions.ts (debounce + abort), useSymptomMapping.ts, useDoctorSearch.ts, useSearchUrlState.ts
  • src/features/search/components/SymptomCombobox.tsx, DoctorSearchView.tsx, DoctorResultCard.tsx, AvailabilityFilter.tsx, DoctorSearch.module.css
  • src/pages/DoctorSearchPage.tsx — route /patient/search
  • src/services/search-service.ts — /symptoms, /symptoms/:slug, /doctors
  • src/mocks/handlers/search.ts — MSW handlers + vocabulary/specialty/doctor fixtures (per CONTRIBUTING.md, every new endpoint ships with mocks)
  • screenshots/doctor-search-{1-initial,2-suggestions,3-results,4-no-match}-{desktop,mobile}.png

Modified files

  • src/types/models.ts — AvailabilityFilter, SymptomSuggestion, SymptomSpecialtyMapping, DoctorSearchQuery, DoctorSearchResult
  • src/pages/PatientDashboardPage.tsx (route + dashboard link), src/components/Layout.tsx (header link)
  • src/mocks/fixtures.ts, src/mocks/handlers.ts, src/mocks/setup.tsx, src/main.tsx (mock bootstrap now awaits MSW readiness — deep-linked states previously raced the worker and bypassed it), src/mocks/handlers.test.ts
  • ROADMAP.md (item [Phase W2] Symptom-based search with autocomplete #30 marked done + changelog line), ARCHITECTURE_ESSENTIALS.md (assumed API shapes)

Test files

  • src/components/Combobox.test.tsx (9 tests)
  • src/features/search/components/SymptomCombobox.test.tsx (5)
  • src/features/search/components/DoctorSearchView.test.tsx (8)
  • src/features/search/search-params.test.ts (8)
  • src/features/search/specialty-list.test.ts (1)
  • src/mocks/handlers.test.ts (3 new tests, 29 total)

Implementation details

  • One combobox, controlled. Combobox is pure presentation + interaction (text/options/intent via props); debounce, fetching and state live in SymptomCombobox, which makes the ARIA behaviour testable in isolation and reusable for future pickers.
  • Debounce + cancellation. useSymptomSuggestions debounces the raw input, keys React Query on the debounced text, and cancels the whole symptom-suggestions cache prefix whenever that text changes or the component unmounts, so a superseded response can never land on top of newer results.
  • URL as the source of truth. parseSearchParams/toSearchParams round-trip state; useSearchUrlState exposes selectSymptom / setAvailability / clearAll. No symptom ⇒ no specialty ids ⇒ the doctor query is disabled — an unfiltered "every doctor" request doesn't exist, so a mapping failure can't degrade into a directory listing.
  • Mock API mirrors the contract. /symptoms does case-insensitive substring matching with limit; /symptoms/:slug 404s for unknown slugs; /doctors OR-matches specialtyIds, filters today (≈next 24 h in the mock) vs week (7 days), sorts soonest-first and hides unverified doctors. Fixtures use relative availability windows so the demo never goes stale.
  • startMockApi() is now awaited before first render (src/main.tsx/src/mocks/setup.tsx) — previously worker.start() was fire-and-forget, so deep-linked pages fired their first queries before the worker controlled the page and silently bypassed the mocks.

Tests added

  • Keyboard / ARIA (Combobox): collapsed wiring (aria-expanded, aria-controls, aria-autocomplete, accessible description), open-on-type with live-region count announcement, ArrowDown/ArrowUp wrapping, Home/End, aria-activedescendant tracking the option DOM id while focus never leaves the input, Enter commits + closes, mouse click commits, two-stage Escape (close, then clear), Clear button, empty state, loading state.
  • Debounce: a burst of keystrokes produces exactly one suggestion request with the final text.
  • Cancellation: with genuinely in-flight (never-resolving) requests, a newer debounced query leaves signals[0].aborted === true and the new signal untouched.
  • Integration (DoctorSearchView): mapping line + doctors rendered and searched with the mapped specialty ids; availability tab updates both the URL and the service call; availability=week read from the URL on load; no-match guidance incl. "Show doctors with any availability" resetting the URL and query; picking a symptom from the combobox updates ?symptom= and searches the mapped specialties; invalid URL params dropped with notices and zero bogus requests; unknown slug shows the friendly 404 path with no doctor request; initial guidance state fires no requests.
  • Units: URL parse/serialize round-trip and every invalid-value path; specialty-list formatting for 0/1/2/3+ names.
  • Handlers (through the real service layer): vocabulary autocomplete (substring, case-insensitive, limit, empty), mapping + 404, doctor search by OR-specialty/availability windows incl. a genuinely empty "fever + today" case, plus the empty-scenario covering both new list endpoints.

Test output

$ npm run typecheck
> tsc -b --noEmit
(clean — no errors)

$ npm test
 Test Files  45 passed (45)
      Tests  407 passed (407)
   Duration  68.61s

$ npx vitest run src/components/Combobox.test.tsx src/features/search src/mocks/handlers.test.ts
 ✓ src/components/Combobox.test.tsx (9 tests) 368ms
 ✓ src/features/search/components/DoctorSearchView.test.tsx (8 tests) 625ms
 ✓ src/features/search/components/SymptomCombobox.test.tsx (5 tests) 537ms
 ✓ src/features/search/search-params.test.ts (8 tests) 20ms
 ✓ src/features/search/specialty-list.test.ts (1 test) 10ms
 ✓ src/mocks/handlers.test.ts (29 tests) 575ms
 Test Files  6 passed (6)
      Tests  60 passed (60)

$ npm run build
dist/assets/index-*.css  106.57 kB │ gzip: 16.27 kB
dist/assets/index-*.js   653.13 kB │ gzip: 201.60 kB
✓ built in 5.25s

$ npm run check:dist
✓ no mock code in production bundle (4 files scanned)

& npm run build` (this is exactly what CI runs).

Adds symptom-based doctor search for issue DocForum#30: a WAI-ARIA 1.2 combobox
on a new patient search page autocompletes against the backend's
controlled symptom vocabulary (docforum-core#60), the applied symptom
discloses which specialties it maps to ("Showing doctors in Cardiology
and General practice"), the availability filter combines with it, and
all state (symptom slug + availability) lives in the URL with validation
for hand-edited links. Suggestion requests are debounced and superseded
in-flight requests are cancelled through React Query's AbortSignal. A
no-match result always offers a concrete way forward.

Issue DocForum#30 depends on DocForum#1 (doctor search), which was still open, so this
also lands the underlying doctor-search page, service and MSW
handlers/fixtures built against the documented assumed contracts
(docforum-core#23 doctor list, DocForum#60 symptom mapping), plus the reusable
Combobox component. The mock bootstrap now awaits MSW readiness before
the first render so deep-linked states don't race the worker's startup.

Tests: Combobox keyboard/ARIA suite, SymptomCombobox debounce +
cancellation suite, DoctorSearchView integration suite, URL-param and
specialty-list units, and handler coverage for /symptoms,
/symptoms/:slug and /doctors — 45 files, 407/407 passing, typecheck,
build and check:dist green. ROADMAP.md and ARCHITECTURE_ESSENTIALS.md
"Known assumptions" updated; desktop + mobile screenshots attached.
…search-with-autocomplete

# Conflicts:
#	src/types/models.ts
@drips-wave

drips-wave Bot commented Oct 19, 2026

Copy link
Copy Markdown

@Rafiat30 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@afeezorobsco-cyber

Copy link
Copy Markdown
Contributor

@Rafiat30 Thanks for the PR. It can't be merged yet. To unblock it:

Merge conflicts: this branch conflicts with main. Please rebase onto the latest main (git fetch upstream && git rebase upstream/main), resolve the conflicts (often ROADMAP.md, ARCHITECTURE*.md or prisma/schema.prisma), and force-push.

Push to this same branch and CI will re-run. Once it's green and conflict-free, it'll be reviewed for merge.

@github-actions github-actions Bot added the needs-rebase Has merge conflicts with main; the author needs to rebase label Oct 20, 2026
…search-with-autocomplete

Resolve conflicts against upstream main (DocForum#156, DocForum#163, DocForum#184, DocForum#185, DocForum#208 and
follow-ups): keep both sides in models/fixtures/handlers/ROADMAP/
ARCHITECTURE_ESSENTIALS, keep the resilient mock-worker bootstrap, and
re-apply the PR's Find-a-doctor nav link, dashboard link and /patient/search
route on top of upstream's responsive-layout and care-hub rewrites.
@github-actions github-actions Bot added needs-rebase Has merge conflicts with main; the author needs to rebase and removed needs-rebase Has merge conflicts with main; the author needs to rebase labels Oct 24, 2026
@github-actions

Copy link
Copy Markdown

@Rafiat30 This PR now has merge conflicts with main, most likely because #157 (feat(platform): complete W7 API client, errors, PWA, and i18n) was just merged, so it can't be merged until it's rebased:

git fetch upstream            # upstream = https://github.com/DocForum/docforum-web.git
git rebase upstream/main
# resolve the conflicts, re-run the tests, then:
git push --force-with-lease

The usual culprits are shared docs like ROADMAP.md and ARCHITECTURE*.md. If the conflict is in the ROADMAP.md changelog, move your entry into a new file under changelog.d/ (see changelog.d/README.md) rather than resolving it in place, so it can't conflict again. CI re-runs when you push, and the needs-rebase label is removed automatically once the conflicts are resolved.

Automated message. It won't repeat unless the PR conflicts again after your next push.

…search-with-autocomplete

Resolve conflicts with upstream's W7 i18n work (DocForum#157, DocForum#151): keep the PR's
Find-a-doctor nav item and dashboard link, adopt upstream's t()/messageId
convention for both, and register nav.findADoctor and patient.nav.findDoctor
in the en and es catalogues so check:i18n stays green.
@github-actions github-actions Bot removed the needs-rebase Has merge conflicts with main; the author needs to rebase label Oct 26, 2026
@afeezorobsco-cyber

Copy link
Copy Markdown
Contributor

@Rafiat30 CI is failing in the Browser job (npm run test:e2e). The visual regression check no longer matches the committed screenshots:

Expected: e2e/__screenshots__/visual-desktop/patient-dashboard.png
Received: test-results/visual-patient-dashboard-screenshot-visual-desktop/patient-dashboard-actual.png
Expected: e2e/__screenshots__/visual-mobile/patient-dashboard.png
Received: test-results/visual-patient-dashboard-screenshot-visual-mobile/patient-dashboard-actual.png

If the patient dashboard change is intentional, regenerate the baselines (see the visual-testing notes in the repo; they must be generated in the same Linux/Playwright environment CI uses) and commit them. If it isn't intentional, fix the layout regression. Then push.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Phase W2] Symptom-based search with autocomplete

2 participants