Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/process-hardening.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -87,6 +87,6 @@ This document turns the current process review into phased, durable repo practic

- Making the dashboard and document viewer client-only via `dynamic(..., { ssr: false })` (PRs #144/#147) meant "page loaded" no longer implies "app mounted". Firefox/WebKit paint the client chunk later than Chromium, so three release-browser-matrix specs raced and failed on `main` while Chromium stayed green. All three were test-timing gaps, not product regressions — the app renders correctly in every browser.
- `tests/ui-overlap.spec.ts`: `gotoHome` waited on `networkidle` (Playwright discourages it) and then measured the header, which had not mounted yet — every failure was `header#search not found` (count 0), never a real overlap. Now waits for `header#search` to be visible before measuring.
- `tests/ui-tools.spec.ts` (forms detail → shared search): the shell re-syncs its query from the URL on mount via `requestAnimationFrame`, which on Firefox/WebKit can land just after a programmatic `fill` and wipe the value (button stays disabled) or drop the submit before the router navigates. The fill-and-submit now runs as one `toPass` unit that retries until the search routes.
- `tests/ui-tools.spec.ts` (forms detail → shared search): the shell re-syncs its query from the URL on mount via `requestAnimationFrame`, which on Firefox/WebKit can land just after a programmatic `fill` and wipe the value (button stays disabled) or drop the submit before the router navigates. The fill-and-submit now runs as one `toPass` unit that retries until the search routes. Root-cause fix: the `requestAnimationFrame` effect in `GlobalMockupSearchShellClient` now skips the `setQuery("")` reset on detail pages (where the URL carries no `q`/`query` param), so the programmatic fill is never wiped.
- `tests/ui-stress.spec.ts` (desktop evidence panel): the evidence `<details>` is opened by focusing its `<summary>` and pressing Enter; in CI WebKit the key event could fire before focus landed, so it never toggled. Now asserts `toBeFocused()` before pressing Enter.
- Rule of thumb for these client-only surfaces: never gate an interaction on `networkidle` or a bare `goto`. Wait for the specific mounted element, and wrap fill→submit→navigate races in `toPass` (the same idiom `openAppModeMenu`/`openDailyActions` already use). The `verify` + `ui-smoke` PR gates run Chromium only, so Firefox/WebKit-specific races surface solely in the gated `release-browser-matrix` (main/release/dispatch/schedule) — keep that job green rather than letting these re-accumulate.
23 changes: 20 additions & 3 deletions src/components/clinical-dashboard/global-mockup-search-shell.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -84,10 +84,12 @@ function GlobalMockupSearchShellClient({
const initialSearchMode =
availableModeIds?.length && !availableModeIds.includes(initialMode) ? fallbackMode : initialMode;
const requestedRun = searchParams.get("run") === "1";
const currentUrlHasQuery = searchParams.has("q") || searchParams.has("query");
const requestedQuery = (searchParams.get("q") ?? searchParams.get("query") ?? "").trim();
const requestedMode = searchParams.get("mode");
const searchParamString = searchParams.toString();
const [query, setQuery] = useState(requestedQuery);
const previousUrlHadQueryRef = useRef(currentUrlHasQuery);
const [searchMode, setSearchMode] = useState<AppModeId>(initialSearchMode);
const [queryMode, setQueryMode] = useState<ClinicalQueryMode>("auto");
const [scopeFilters, setScopeFilters] = useState<SearchScopeFilters>({});
Expand All@@ -114,6 +116,10 @@ function GlobalMockupSearchShellClient({
(searchMode === "favourites" && pathname === "/favourites") ||
(searchMode === "differentials" && pathname === "/differentials"));
const isDifferentialPresentationWorkflow = pathname.startsWith("/differentials/presentations");
// True when on a sub-route of a mode home (e.g. /forms/transport-crisis-form,
// /services/13yarn) rather than the mode home itself (/forms, /services).
const isDetailPage =
/^\/(forms|services|favourites)\/.+/.test(pathname) || /^\/differentials\/diagnoses\/.+/.test(pathname);

useEffect(() => {
const frame = window.requestAnimationFrame(() => {
Expand All@@ -127,13 +133,24 @@ function GlobalMockupSearchShellClient({
: initialSearchMode;
setSearchMode(nextMode);

const requestedQuery = (params.get("q") ?? params.get("query"))?.trim();
setQuery(requestedQuery ?? "");
const urlHasQuery = params.has("q") || params.has("query");
const hadQueryBeforeThisSync = previousUrlHadQueryRef.current;
previousUrlHadQueryRef.current = urlHasQuery;
if (urlHasQuery) {
// Sync the controlled query state from the URL query param.
const requestedQuery = (params.get("q") ?? params.get("query"))?.trim();
setQuery(requestedQuery ?? "");
} else if (!isDetailPage || hadQueryBeforeThisSync) {
// On no-query routes, clear any stale URL-derived query. Initial detail
// page mounts still skip the deferred clear so programmatic fills are
// not wiped by the WebKit requestAnimationFrame race.
setQuery("");
}

if (params.get("focus") === "1") inputRef.current?.focus({ preventScroll: true });
});
return () => window.cancelAnimationFrame(frame);
}, [availableModeIds, initialSearchMode, pathname, searchParamString]);
}, [availableModeIds, initialSearchMode, isDetailPage, pathname, searchParamString]);

useEffect(() => {
let cancelled = false;
Expand Down
Loading