PRACTICAL GUIDE / Playwright React component router provider testing
Test routed React components without faking the browser state
Build reliable Playwright component tests for React Router, including route params, loaders, redirects, history, diagnostics, and migration advice.
In this guide6 sections
- Why a routed component fails outside the application shell
- Put the route contract in a browser-side story
- Exercise loaders, errors, and history as separate failures
- Read the evidence before changing waits
- Separate an absent provider from split module identity
- Read diagnostic fields as one timeline
- Move from inline JSX without a flag-day rewrite
- Roll out the observable boundary before the difficult routes
- Give each boundary an owner and a complete handoff
- Know what this test cannot prove
What you will learn
- Why a routed component fails outside the application shell
- Put the route contract in a browser-side story
- Exercise loaders, errors, and history as separate failures
- Read the evidence before changing waits
The component renders correctly in the application, but its isolated test dies with useLocation() may be used only in the context of a <Router> component. You add a router, the hook error disappears, and then the navigation assertion watches page.url() forever. The locator is not the problem. The test has recreated only part of the routing boundary the component actually depends on.
React Router is part of the component's runtime environment, just like a theme, query client, or authentication provider. A useful test must supply the right kind of router, start it at a deliberate location, and observe navigation without confusing the gallery page URL with the router's in-memory location. It also has to create fresh state for every mount.
Why a routed component fails outside the application shell
Calling a routing hook does not read a global browser variable directly. Hooks such as useLocation, useNavigate, useParams, and useLoaderData read context installed by a router higher in the React tree. The production entry point usually supplies that context around the whole application. A component story does not inherit the production entry point unless you put it there.
That missing context produces a clean failure. It is useful because it says the story is incomplete. The worse failure appears when a test adds any router that makes the exception disappear but does not match the component's real contract. A MemoryRouter can supply useLocation and declarative <Routes>, but it does not execute a data router's loaders or actions. A RouterProvider created with createMemoryRouter does support those data APIs. Treating the two as interchangeable can leave the screen green while skipping the behavior that matters.
There are three locations in play during a component test:
- The gallery document has a real browser URL, such as
http://localhost:5173/playwright/gallery/index.html. - A memory router has its own current location, such as
/orders/A-1042. - A loader may fetch a network URL, such as
/api/orders/A-1042.
Only the first value appears in page.url(). Clicking a React Router <Link> inside MemoryRouter changes the second value. A loader or component fetch creates the third. An assertion against the gallery URL after an in-memory navigation is therefore testing the wrong object.
Playwright 1.62 introduced the stable story-gallery component model documented for @playwright/test. A story runs in the browser and owns the React composition. The mount() fixture navigates to the gallery, asks it to render a named story, and returns a locator scoped to the gallery root. This is a good fit for providers because the router, routes, and scenario data live together on the browser side.
Check the installed version before copying the examples:
npx playwright --versionSuites on Playwright 1.61 or older do not have this stable mount() fixture. They may still use an experimental component package, but its JSX-in-the-test model has different setup and lifecycle rules. Upgrade deliberately rather than mixing examples from both models in one spec.
Provider placement also controls isolation. Defining a router once at module scope creates a singleton:
// Avoid this in a story module.
const router = createMemoryRouter(routes, {
initialEntries: ['/orders/A-1042'],
});
export const Routed = () => <RouterProvider router={router} />;That object owns navigation history, loader state, cached route data, and subscribers. A component.update() call then re-renders the story with new props while the singleton stays on its old route. Custom gallery code that calls window.mount() repeatedly without a fresh navigation can expose the same problem. Playwright's built-in mount() navigates to the gallery for each call, so ordinary test-to-test leakage is not the expected behavior. Construct the router inside the story function or a wrapper component so every rendered scenario receives a new instance.
Put the route contract in a browser-side story
Start with a component that reads a path parameter and navigates to a sibling route. The production behavior is small enough to see clearly: the order number comes from the URL, and clicking the customer link should render the customer page.
// src/components/OrderSummary.tsx
import { Link, useParams } from 'react-router-dom';
export function OrderSummary() {
const { orderId } = useParams<{ orderId: string }>();
return (
<section aria-labelledby="order-heading">
<h1 id="order-heading">Order {orderId}</h1>
<Link to={`/orders/${orderId}/customer`}>View customer</Link>
</section>
);
}
export function CustomerSummary() {
const { orderId } = useParams<{ orderId: string }>();
return <h1>Customer for order {orderId}</h1>;
}The story names the route shape and initial entry. It also provides a visible route probe. The probe is not required for the user-facing assertion, but it turns an otherwise invisible in-memory location into useful failure evidence.
// src/components/OrderSummary.story.tsx
import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom';
import { CustomerSummary, OrderSummary } from './OrderSummary';
function RouteProbe() {
const location = useLocation();
return (
<output data-testid="route-path" aria-label="Current route">
{location.pathname}
</output>
);
}
export function RoutedOrder({ orderId = 'A-1042' }: { orderId?: string }) {
return (
<MemoryRouter initialEntries={[`/orders/${orderId}`]}>
<Routes>
<Route path="/orders/:orderId" element={<OrderSummary />} />
<Route
path="/orders/:orderId/customer"
element={<CustomerSummary />}
/>
</Routes>
<RouteProbe />
</MemoryRouter>
);
}The test scopes every query to the returned component locator. This matters when the gallery itself contains buttons, headings, or debug controls.
// tests/components/order-summary.spec.ts
import { expect, test } from '@playwright/test';
test('uses the route parameter and follows the customer link', async ({ mount }) => {
const component = await mount('components/OrderSummary/RoutedOrder', {
orderId: 'A-1042',
});
await expect(
component.getByRole('heading', { name: 'Order A-1042' }),
).toBeVisible();
await expect(component.getByTestId('route-path')).toHaveText('/orders/A-1042');
await component.getByRole('link', { name: 'View customer' }).click();
await expect(
component.getByRole('heading', { name: 'Customer for order A-1042' }),
).toBeVisible();
await expect(component.getByTestId('route-path')).toHaveText(
'/orders/A-1042/customer',
);
});This case proves parameter parsing, link generation, route selection, and the visible destination. It does not prove that a deployed server accepts /orders/A-1042/customer as a deep link. That claim belongs in an end-to-end test that opens the real URL.
A wrapper should reflect only dependencies the component truly has. Do not import the entire production application shell because one child calls useParams. Pulling in analytics, authentication refresh, global notifications, and five unrelated providers makes the story slower and makes failures harder to assign. The smallest truthful route tree is better than the largest convenient wrapper.
Exercise loaders, errors, and history as separate failures
Data routers deserve their own story. A component using useLoaderData needs a RouterProvider; wrapping it in MemoryRouter only solves the router context and still leaves the loader contract absent. The following example loads an order from an API and renders a route error for a missing record.
// src/components/OrderPage.story.tsx
import {
createMemoryRouter,
isRouteErrorResponse,
RouterProvider,
useLoaderData,
useRouteError,
} from 'react-router-dom';
type Order = {
id: string;
total: number;
};
async function orderLoader({ params }: { params: { orderId?: string } }) {
const response = await fetch(`/api/orders/${params.orderId}`);
if (!response.ok) throw response;
return (await response.json()) as Order;
}
function OrderPage() {
const order = useLoaderData() as Order;
return (
<main>
<h1>Order {order.id}</h1>
<p>Total: ${order.total.toFixed(2)}</p>
</main>
);
}
function OrderRouteError() {
const error = useRouteError();
if (isRouteErrorResponse(error) && error.status === 404)
return <div role="alert">Order not found</div>;
return <div role="alert">Could not load order</div>;
}
export function LoadedOrder({ initialPath = '/orders/A-1042' }) {
const router = createMemoryRouter(
[
{
path: '/orders/:orderId',
loader: orderLoader,
element: <OrderPage />,
errorElement: <OrderRouteError />,
},
],
{ initialEntries: [initialPath] },
);
return <RouterProvider router={router} />;
}Register network routes before mount(). Mounting performs a navigation to the gallery and immediately renders the story, so a handler installed afterward can lose the loader request.
// tests/components/order-page.spec.ts
import { expect, test } from '@playwright/test';
test('renders loader data for the route parameter', async ({ page, mount }) => {
let requestedUrl = '';
await page.route('**/api/orders/A-1042', async route => {
requestedUrl = route.request().url();
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ id: 'A-1042', total: 81.5 }),
});
});
const component = await mount('components/OrderPage/LoadedOrder', {
initialPath: '/orders/A-1042',
});
await expect(
component.getByRole('heading', { name: 'Order A-1042' }),
).toBeVisible();
await expect(component.getByText('Total: $81.50')).toBeVisible();
expect(new URL(requestedUrl).pathname).toBe('/api/orders/A-1042');
});
test('renders the route error for a missing order', async ({ page, mount }) => {
await page.route('**/api/orders/missing', route =>
route.fulfill({
status: 404,
contentType: 'application/json',
body: JSON.stringify({ code: 'ORDER_NOT_FOUND' }),
}),
);
const component = await mount('components/OrderPage/LoadedOrder', {
initialPath: '/orders/missing',
});
await expect(component.getByRole('alert')).toHaveText('Order not found');
});These are different failures. The success test catches a broken parameter, request path, response mapping, or value rendering. The error test catches a route that throws without an errorElement, a status that is flattened into a generic message, or a handler that mistakenly turns a 404 into 200. Combining both into a parameterized assertion often hides which contract changed.
History is a third behavior. Seed more than one entry when the component offers Back or Cancel behavior. Keep the current route observable because the browser address bar will not move.
// src/components/Checkout.story.tsx
import { MemoryRouter, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
function Checkout() {
const navigate = useNavigate();
return (
<>
<h1>Checkout</h1>
<button type="button" onClick={() => navigate(-1)}>Back to cart</button>
</>
);
}
function Cart() {
return <h1>Your cart</h1>;
}
function Pathname() {
return <output data-testid="pathname">{useLocation().pathname}</output>;
}
export function CheckoutWithHistory() {
return (
<MemoryRouter
initialEntries={['/products', '/cart', '/checkout']}
initialIndex={2}
>
<Routes>
<Route path="/cart" element={<Cart />} />
<Route path="/checkout" element={<Checkout />} />
</Routes>
<Pathname />
</MemoryRouter>
);
}import { expect, test } from '@playwright/test';
test('returns to the previous history entry', async ({ mount }) => {
const component = await mount('components/Checkout/CheckoutWithHistory');
await component.getByRole('button', { name: 'Back to cart' }).click();
await expect(component.getByRole('heading', { name: 'Your cart' })).toBeVisible();
await expect(component.getByTestId('pathname')).toHaveText('/cart');
});The seeded stack matters. Starting directly at /checkout and calling navigate(-1) can leave the memory history without the application entry the product assumes. A passing test built on the wrong stack says nothing about the actual Cancel behavior.
Read the evidence before changing waits
Provider problems usually announce themselves before the first meaningful locator action. Run the single spec with a trace and one worker:
npx playwright test tests/components/order-page.spec.ts \
--project=components --workers=1 --trace=onOpen the retained trace from the HTML report or directly:
npx playwright show-trace test-results/path-to-trace/trace.zipClassify the first trustworthy symptom before editing the wrapper. These signals come from different layers and should not receive the same fix:
| First trustworthy symptom | Likely boundary | Evidence that rejects the nearest alternative |
|---|---|---|
useLocation reports that no router exists | Missing provider | The render stack reaches the hook before any route or request runs |
| React reports a router nested inside another router | Duplicate provider | The story tree already contains a router above the wrapper being added |
No routes matched location appears | Initial entry or route pattern | A route probe shows a location, but no declared pattern accepts it |
The destination content changes while page.url() does not | Normal memory navigation | The route probe and destination heading both show the new in-memory route |
| A loader remains pending and no API request appears | Loader creation, cache, or interception order | The trace contains the mount but lacks the expected request entirely |
| Updated story props render while the old path remains | Module-scoped router | A route probe stays unchanged after component.update() creates the new scenario |
Keep the render exception or warning in the report. Replacing it with a custom message such as component failed to mount throws away the component name and React stack that identify the bad boundary. If the team wraps mount() in a helper, let the original error remain the cause and attach the story id and initial route as additional context.
Relative links deserve one focused check when route nesting is important. A link declared as to="customer" resolves against the matched route hierarchy, while to="/customer" starts at the router root. Both can render a page in a forgiving test route table. Seed the production-like parent path, click the link, and assert the route probe. That catches an accidental leading slash without importing the whole application router.
Look at the failed mount() action first. If it rejects before returning a component locator, inspect the browser console and the gallery DOM snapshot. A routing hook outside a provider produces a React stack pointing at the component that called the hook. A nested provider commonly reports that a router cannot be rendered inside another router. Both failures occur during render. Adding a longer locator timeout cannot change them.
When mount() completes but the root stays empty, check the gallery console for route-selection warnings and record the route probe. A message such as No routes matched location "/orders/A-1042" points to the route table or initial entry. If the probe shows /orders/A-1042 but the route declares /order/:id, the mismatch is deterministic. Fix the path, not the wait.
For a loader that never settles, use the trace Network tab to answer four questions:
- Did the request start after the handler was registered?
- Does its pathname contain the expected route parameter?
- Which status and content type came back?
- Did another request, such as a service worker fetch, bypass the intended mock?
The Network tab can show the request, response headers, body, timing, and the action window that triggered it. It cannot tell you whether the route object was accidentally shared across tests. The visible probe, a per-story router creation point, and the test's worker identity provide that evidence.
Open the gallery manually when React's render stack is clearer outside the runner. With the development server running, load the configured baseURL and call the same gallery contract used by the fixture:
await window.mount({ story: 'components/OrderSummary/RoutedOrder' });An unknown story id rejects immediately. A render exception preserves its browser-side stack. A valid story lets you click through the route by hand and watch the probe change. This separates story discovery from router behavior without modifying the spec.
Failures that appear order-dependent need a different reproduction. Run the file repeatedly with several workers, then reverse or filter the cases:
npx playwright test tests/components/order-summary.spec.ts \
--project=components --workers=4 --repeat-each=10If a case fails only after another route scenario, first confirm that the test uses the built-in mount() fixture. That fixture navigates fresh, even when the component project reuses its browser context. A custom helper that invokes window.mount() directly may skip that boundary. Search its stories and decorators for routers, histories, stores, and mutable arrays created outside the exported function. If the standard fixture is in use, investigate context state that reuseContext does not guarantee to reset, browser-process state, or data outside the browser before blaming a module-scoped router.
One near-miss looks like a provider leak but is actually a late network mock. The first test primes an application cache, the next loader returns instantly, and the route appears to remember data. Evidence differs: a leaked router shows the previous pathname or navigation history; a cache hit shows a newly created router but no expected API request. Block application service workers in the component project when they are outside the test contract, or clear the specific application cache in the story. Do not reset every browser feature blindly because that can remove behavior the component genuinely uses.
Separate an absent provider from split module identity
The missing-router exception has another cause that produces almost the same console text: the provider and the hook can come from different instances of React Router's context modules. This appears most often when a workspace library carries its own installed routing dependency or a linked package is bundled differently from the story. The MemoryRouter in the story then publishes context from one module instance while the imported component's useLocation reads context from another. From the hook's point of view, no provider exists, even though the React component stack visibly includes one.
Use a same-module consumer to separate the two failures. Put a tiny location reader in the story file, under the same provider and beside the imported component. If both readers throw, the provider is absent, below the consumers, or never reached. If the local reader displays the seeded pathname but the imported component throws the context exception, the provider is working and the route entry is valid. The import boundary is now the useful suspect. A longer wait, a second wrapper, or a different initial entry cannot join two context identities.
The browser stack and dependency resolution output provide the next evidence. A healthy result resolves the provider and hook through one physical routing dependency. A broken result shows distinct package locations or bundled module sources for the story and library. A top-level list naming one version can mislead because a linked or prebuilt library may already contain another copy. Read the source location attached to the browser error and the full dependency path, not only the workspace-root version.
Record three fields together when this fails: the story-local probe value, the imported component's first stack frame, and the resolved module location on each side of the boundary. A healthy record might contain /orders/A-1042, one shared module location, and no hook exception. A missing-provider record has no successful local probe. A split-identity record has a healthy local probe beside the imported component's exception and two module locations. That combination is much more decisive than the exception sentence alone.
Read diagnostic fields as one timeline
For an ordinary route failure, read the trace as a short sequence rather than treating every visible URL as equivalent. The mount action's result says whether React returned a root at all. The first browser-console entry after that action supplies a severity, message, and source location. The route probe supplies the router pathname. The first matching Network entry supplies the request method, pathname, status, and response content type. Those fields describe four different boundaries.
In a healthy loader render, mount completes, the probe contains /orders/A-1042, and the matching request row contains /api/orders/A-1042, status 200, and JSON content. With an initial-entry mismatch, mount can still complete and the probe can show that pathname, but the console reports that no route matched and no loader request follows. With a missing provider, mount itself rejects, so a missing request is only a consequence. A 200 status is misleading when the response content type is HTML or the request pathname belongs to a gallery asset. That row proves a server answered, not that the loader received its mocked JSON.
Move from inline JSX without a flag-day rewrite
Older React component suites often import test from @playwright/experimental-ct-react and call mount(<Component />) in the test file. That model compiles JSX from the test bundle and can pass data across the Node and browser boundary in ways the stable gallery model intentionally avoids. A direct search-and-replace from JSX to a string id is not enough.
Create the component project and gallery first while the old project still runs. Point the new project's baseURL at the gallery document and give it a separate testDir. A minimal project shape is:
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
{
name: 'components',
testDir: './tests/components',
use: {
...devices['Desktop Chrome'],
baseURL: 'http://localhost:5173/playwright/gallery/index.html',
serviceWorkers: 'block',
reuseContext: true,
},
},
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:5173/playwright/gallery/index.html',
reuseExistingServer: !process.env.CI,
},
});Move one routing scenario at a time into a named *.story.tsx export. Put callback behavior inside the story and expose its result in the DOM rather than passing a Node function into React. Keep route entries and mock data serializable when a test supplies them as props. After the new story passes alone, run it beside the remaining experimental project before deleting the old counterpart.
Use a small migration matrix during review:
| Old responsibility | Stable owner | Regression to watch |
|---|---|---|
| JSX composition in the spec | Named browser-side story | A provider omitted during the move |
| Router instance in test setup | Fresh instance inside the story | History leaking through module scope |
| Callback variable in Node | Observable state rendered by the story | Assertion reading an impossible cross-process value |
| CT-specific bundler settings | Application dev server and gallery | Alias or CSS behavior differing from production |
| Route mock after inline mount | page.route() before string-id mount | Loader escaping before interception |
Keep both projects in CI until the migrated slice covers success, rejection, and navigation. Compare failure artifacts, not just pass counts. The new test should still fail when the route parameter is wrong, the loader returns 404, or history lacks the previous entry. A migration that only preserves green results may have dropped the negative branch.
The cost is temporary duplication. Two component projects consume more setup time and make reporting noisier. That cost is preferable to changing the bundler, provider boundary, and every assertion in one commit with no way to identify which layer broke.
Roll out the observable boundary before the difficult routes
Choose one parameter-only canary before moving loader tests. Keep its old counterpart active and expose its memory pathname. This slice reveals story naming, application aliases, CSS loading, and provider placement without adding network diagnosis while the new project is still small.
Move link and history cases next. Their first breakage is often an assertion that still reads the gallery address bar or a scenario that seeds only the final history entry. Require both the destination content and the in-memory pathname before counting a case as migrated. Move loader and route-error cases only after the team has agreed where interception is registered and whether application service workers belong in the scenario. Otherwise a gallery wiring problem, an early request, and a cache hit arrive in the same review.
For each migrated slice, prove that the new test can fail for the intended reason. A temporary local change to an initial entry should produce route-selection evidence, while a temporary wrong loader status should reach the route error. Restore the change before landing it. A passing replacement plus an observed, correctly classified failure is stronger migration evidence than matching green counts. Remove the old case only after the new case also survives the suite's normal parallel run without retaining a prior pathname or payload.
The overlap has a concrete cost. If an illustrative batch contains 30 migrated cases, keeping both projects active executes 60 versions until the old 30 are removed. Reviewers also compare two artifacts for one behavior. Loader stories add a maintenance point for every representative body and interception rule. Keep batches small, then remove each old slice once its replacement supplies the negative evidence above.
Give each boundary an owner and a complete handoff
The feature team owns the story's route tree, provider choice, seeded history, and visible assertions because those encode component behavior. The test-infrastructure team owns gallery discovery, module resolution, browser project lifecycle, and CI artifacts. The API or platform team owns a real response or redirect defect, but not a component mock that returned the wrong fixture. Assigning the issue from the final locator timeout alone sends it to the wrong group.
A handoff should contain the story id, initial entries, first failing action, first component-owned console frame, route-probe value, and the matching request's pathname, status, and content type. For a suspected duplicate dependency, include both resolved module locations and the result of the same-module probe. For an order-dependent failure, include the preceding scenario and whether a fresh gallery navigation occurred. That package lets the receiving team reproduce the boundary it owns without rebuilding the reporter's theory from screenshots.
Know what this test cannot prove
An in-memory router is fast because it excludes the deployed routing stack. That exclusion is also its limit. Do not use this test as evidence that a reverse proxy rewrites deep links correctly, that a CDN serves the right document for /orders/A-1042, or that the application's basename matches its production subdirectory. Open a real deployed or production-like URL for those claims.
Avoid a component route test when the behavior crosses origins for sign-in, payment, or identity-provider redirects. Memory history cannot reproduce browser process boundaries, cookie policies, popup ownership, or a server-generated redirect chain. An end-to-end browser test costs more time, but it exercises the mechanism that can fail.
This technique does not catch server-rendering or hydration failures. The gallery creates the component on the client, so it never proves that server markup, hydration data, and the first client route agree. A page can pass every memory-router story and still discard server markup or show the wrong initial loader state during hydration. Cover that failure with a production-like page render that includes the server response.
Do not mock a loader when the contract under review is the loader's integration with the backend. The mock is valuable for rendering a 404 or a precise payload edge case. It cannot prove authentication headers, gateway transformations, response caching, or the backend schema. Pair a few routed component cases with API contract tests or end-to-end coverage instead of stretching one layer beyond its evidence.
There is also a maintenance cost. Every story adds a route tree that can drift from the application tree. Keep route paths in shared production constants when that is already the application's design, or make the story's intentionally smaller route shape obvious in its name. Importing the full production router solely to avoid duplication usually replaces a small drift risk with a large setup surface.
Provider-heavy stories can become mini applications. Stop adding wrappers when the failure under review belongs to the shell rather than the component. If the story needs real authentication bootstrapping, production feature discovery, live localization downloads, analytics initialization, and server routing before it can render, the useful unit is probably a page. Test that page through the running application.
Finally, do not serialize tests to conceal a shared router. Serial execution trades away parallel speed and leaves the ownership defect in place. A fresh router per story costs a small amount of construction time and gives each case honest history, loader state, and subscriptions. That is the trade worth making for component coverage that teammates can trust.
// FIELD DISPATCH
Get the QA Field Notes
Weekly QA battles, AI testing guides, and interview drills. Free on Substack.
// LIVE COURSE / THE TESTING ACADEMY
Playwright Automation Mastery
Go beyond Selenium. Master Playwright with JS/TS in 90 days.
From the instructor behind this guide.
Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.
PRIMARY REFERENCES
Verify the details at the source
QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.
- 01Official playwright.dev reference
playwright.dev
Primary documentation selected and verified for the claims in this guide.
- 02Official playwright.dev reference
playwright.dev
Primary documentation selected and verified for the claims in this guide.
- 03Official playwright.dev reference
playwright.dev
Primary documentation selected and verified for the claims in this guide.
- 04Official playwright.dev reference
playwright.dev
Primary documentation selected and verified for the claims in this guide.
FAQ / QUICK ANSWERS
Questions testers ask
Why does useLocation fail in a Playwright component test?
A hook such as `useLocation` needs a router above the component in the React tree. Put that provider in the story or wrapper, create a fresh router for the scenario, and mount the routed story instead of the bare component.
Should I use MemoryRouter or RouterProvider for component tests?
Choose `MemoryRouter` for components that only need declarative routes and history. Use `RouterProvider` with `createMemoryRouter` when loaders, actions, route errors, or data-router navigation are part of the contract.
Can a component test prove that a production URL works?
Only if the claim is limited to the in-memory route tree. A component test does not prove that the web server serves a deep link, that a reverse proxy preserves a basename, or that the deployed bundle loads for that URL.
How do I mock a React Router loader request in Playwright?
Register `page.route()` before calling `mount()`, because mounting navigates to the gallery and renders the story. Match the precise API request, return a realistic status and body, then assert both the rendered state and the request inputs.
Does clicking a Link update page.url() when I use MemoryRouter?
No. `MemoryRouter` keeps its location in memory, so the gallery page URL stays unchanged. Expose the current pathname in the story or assert the destination route's visible content instead.
RELATED GUIDES
Continue the learning route
GUIDE 01
Playwright Component Testing for React State, Events, and Routing
Test React components in a real browser with Playwright mount fixtures, semantic locators, callback assertions, route hooks, and controlled data.
GUIDE 02
Test Reduced Motion with Playwright
Use Playwright reduced motion testing with media emulation to verify static alternatives, disabled animations, usable content, and regression checks in CI.
GUIDE 03
Test localStorage Directly with the Playwright API
Learn Playwright localStorage API testing through practical setup, failure analysis, CI evidence, security boundaries, and measurable release gates for QA and SDET teams.
GUIDE 04
Test sessionStorage Directly with the Playwright API
A practical guide to Playwright sessionStorage API testing, with implementation examples, debugging workflows, CI evidence, security controls, and release gates.
GUIDE 05
Test WebAuthn Passkey Registration with Playwright
Master Playwright WebAuthn passkey registration testing with implementation examples, failure analysis, evidence design, CI controls, and release-ready QA checklists.