fix(web): Phase 3 hotfix — UsersPage.test.tsx Router context + Breadcrumbs defensive guard

CI failure on Phase 3 commit (e761ae40):
  FAIL  src/pages/auth/UsersPage.test.tsx > 8 tests (all)
  Error: useLocation() may be used only in the context of a <Router> component.

Root cause:
  Phase 3 wired <Breadcrumbs /> into PageHeader (UX-M5 closure). UsersPage
  renders PageHeader at the top of its tree. UsersPage.test.tsx was the
  only auth-page test file whose renderWithProviders helper lacked a
  MemoryRouter wrapper — every other sibling (BreakglassPage, KeysPage,
  OIDCProvidersPage, SessionsPage, RolesPage, AuthSettingsPage,
  ApprovalsPage, etc.) already wraps in MemoryRouter. The 2026-05-11
  MED-11 closure that shipped UsersPage + 8 tests predated Phase 3 and so
  predated the need for Router context in test trees.

Fix is two-layered:

(1) Targeted — add MemoryRouter to UsersPage.test.tsx renderWithProviders
    so the test tree has the same Router context the production tree gets
    from <BrowserRouter> in main.tsx.

(2) Defensive — Breadcrumbs.tsx now gates useLocation() behind
    useInRouterContext(). If a future test mounts PageHeader (or any
    other Breadcrumbs consumer) without a Router wrapper, the component
    renders null instead of crashing. The actual useLocation() + render
    work moves into a BreadcrumbsInner sub-component called only after
    the Router-context check passes. This prevents the same class of
    failure ever happening again — any new auth-page test author who
    forgets MemoryRouter will see a missing breadcrumb (cosmetic),
    not 8 red test failures.

Verification (sandbox):
  • TypeScript clean — npx tsc --noEmit exits 0
  • UsersPage suite — 8/8 green (was 0/8 in CI)
  • Breadcrumbs suite — 8/8 green
  • All sibling auth tests — 72/72 green (BreakglassPage 6 + KeysPage 7
    + OIDCProvidersPage 13 + SessionsPage 11 + RolesPage 6 +
    AuthSettingsPage 6 + ApprovalsPage 23). Unchanged because they
    already had MemoryRouter; pinned to confirm defensive guard didn't
    regress them.

CI expectation: web-test job goes from red to green on next push.
No behavior change to production — Breadcrumbs still renders identically
under <BrowserRouter> at runtime; useInRouterContext returns true and
delegates to BreadcrumbsInner unchanged.

Touches:
  web/src/components/Breadcrumbs.tsx       (+14 / -2)
  web/src/pages/auth/UsersPage.test.tsx    (+8  / -1)
This commit is contained in:
shankar0123
2026-05-14 15:42:55 +00:00
parent e761ae40a4
commit 0987e222dd
2 changed files with 20 additions and 2 deletions
+13 -1
View File
@@ -20,7 +20,7 @@
// upgrading to data-router-driven crumbs is a future task once the
// router migration ships.
import { Link, useLocation } from 'react-router-dom';
import { Link, useLocation, useInRouterContext } from 'react-router-dom';
import { ChevronRight } from 'lucide-react';
// pathSegmentLabels — map first-segment URL keys to human labels.
@@ -126,7 +126,19 @@ function looksLikeID(s: string): boolean {
return s.includes('-') || /^[a-f0-9]{8,}$/i.test(s);
}
// Breadcrumbs is the public entry. Defensive against missing Router
// context (a test that mounts a PageHeader without a <MemoryRouter>
// wrapper used to crash here). useLocation() throws an invariant
// error if there's no Router; gate it behind useInRouterContext()
// + render the actual logic in a sibling so useLocation() is only
// called when we know the context is present.
export default function Breadcrumbs() {
const inRouter = useInRouterContext();
if (!inRouter) return null;
return <BreadcrumbsInner />;
}
function BreadcrumbsInner() {
const { pathname } = useLocation();
const crumbs = crumbsFor(pathname);
+7 -1
View File
@@ -1,6 +1,7 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { MemoryRouter } from 'react-router-dom';
import type { ReactNode } from 'react';
// =============================================================================
@@ -29,8 +30,13 @@ function renderWithProviders(ui: ReactNode) {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
});
// MemoryRouter required because PageHeader now renders Breadcrumbs
// (Phase 3 UX-M5), which calls useLocation() and throws when there
// is no Router context in the tree.
return render(
<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>,
<QueryClientProvider client={queryClient}>
<MemoryRouter>{ui}</MemoryRouter>
</QueryClientProvider>,
);
}