Replace mountedRoutes-based wrapping with mountPath option

Address review feedback: mountedRoutes are for abstract route targets
the test subject links to, not for the subject itself. Add a dedicated
mountPath option that wraps the test element in a single Route with
the given path pattern, enabling useParams(). When mountPath is set
and initialRouteEntries is not, the initial entry defaults to mountPath.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2026-05-21 13:39:52 +02:00
parent 62dd4fc3bc
commit 5dca781385
3 changed files with 48 additions and 14 deletions
+1 -1
View File
@@ -2,4 +2,4 @@
'@backstage/frontend-test-utils': patch
---
Fixed `renderInTestApp` to set up React Router route matching when `mountedRoutes` are provided. Previously, mounted routes only fed the route resolution API for link generation (`useRouteRef`), but did not create `<Route>` elements for param extraction (`useParams`). This meant that `initialRouteEntries` had no effect on route param availability. The test element is now wrapped in `<Routes>` with a `<Route>` for each mounted path, matching the behavior of the real `AppRoutes` extension.
Added a `mountPath` option to `renderInTestApp` that controls the route path pattern the test element is rendered at. When set, the element is wrapped in a `<Route>` with the given path, enabling `useParams()` to extract route parameters from the URL. When `mountPath` is set and `initialRouteEntries` is not, the initial route entry defaults to the mount path. This is useful for testing page components that depend on URL parameters, such as entity pages that use `useRouteRefParams`.
@@ -80,7 +80,28 @@ export type TestAppOptions<TApiPairs extends any[] = any[]> = {
features?: FrontendFeature[];
/**
* Initial route entries to use for the router.
* The route path pattern that the test element is rendered at. When set,
* the element is wrapped in a `<Route>` with this path, enabling
* `useParams()` to extract parameters from the URL. Defaults to `'/'`.
*
* When `mountPath` is set and `initialRouteEntries` is not, the
* initial route entry defaults to `mountPath` (which works for paths
* without parameters). For parameterized paths you must also set
* `initialRouteEntries` to a concrete URL that matches the pattern.
*
* @example
* ```ts
* renderInTestApp(<EntityPage />, {
* mountPath: '/catalog/:namespace/:kind/:name',
* initialRouteEntries: ['/catalog/default/component/my-entity'],
* })
* ```
*/
mountPath?: string;
/**
* Initial route entries to use for the router. When `mountPath` is set
* and this is not, defaults to `[mountPath]`.
*/
initialRouteEntries?: string[];
@@ -125,9 +146,7 @@ export function renderInTestApp<const TApiPairs extends any[] = any[]>(
element: JSX.Element,
options?: TestAppOptions<TApiPairs>,
): RenderResult {
const mountedPaths = options?.mountedRoutes
? Object.keys(options.mountedRoutes)
: [];
const mountPath = options?.mountPath;
const extensions: Array<ExtensionDefinition> = [
createExtension({
@@ -136,16 +155,12 @@ export function renderInTestApp<const TApiPairs extends any[] = any[]>(
factory: () => {
let content: JSX.Element = element;
if (mountedPaths.length > 0) {
if (mountPath) {
const routePath =
mountPath === '/' ? mountPath : `${mountPath.replace(/\/$/, '')}/*`;
content = (
<Routes>
{mountedPaths.map(path => (
<Route
key={path}
path={path === '/' ? path : `${path.replace(/\/$/, '')}/*`}
element={content}
/>
))}
<Route path={routePath} element={content} />
<Route path="*" element={content} />
</Routes>
);
@@ -198,7 +213,10 @@ export function renderInTestApp<const TApiPairs extends any[] = any[]>(
params: {
component: ({ children }) => (
<MemoryRouter
initialEntries={options?.initialRouteEntries}
initialEntries={
options?.initialRouteEntries ??
(mountPath ? [mountPath] : undefined)
}
future={{
v7_relativeSplatPath: false,
v7_startTransition: false,
+16
View File
@@ -150,6 +150,7 @@ describe('Entity page', () => {
await renderInTestApp(tester.reactElement(), {
apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]],
mountPath: '/catalog/:namespace/:kind/:name',
initialRouteEntries: ['/catalog/default/component/artist-lookup'],
config: {
app: {
@@ -201,6 +202,7 @@ describe('Entity page', () => {
await renderInTestApp(tester.reactElement(), {
apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]],
mountPath: '/catalog/:namespace/:kind/:name',
initialRouteEntries: ['/catalog/default/component/artist-lookup'],
config: {
app: {
@@ -245,6 +247,7 @@ describe('Entity page', () => {
await renderInTestApp(tester.reactElement(), {
apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]],
mountPath: '/catalog/:namespace/:kind/:name',
initialRouteEntries: ['/catalog/default/component/artist-lookup'],
config: {
app: {
@@ -296,6 +299,7 @@ describe('Entity page', () => {
await renderInTestApp(tester.reactElement(), {
apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]],
mountPath: '/catalog/:namespace/:kind/:name',
initialRouteEntries: ['/catalog/default/component/artist-lookup'],
config: {
app: {
@@ -341,6 +345,7 @@ describe('Entity page', () => {
await renderInTestApp(tester.reactElement(), {
apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]],
mountPath: '/catalog/:namespace/:kind/:name',
initialRouteEntries: ['/catalog/default/component/artist-lookup'],
config: {
app: {
@@ -373,6 +378,7 @@ describe('Entity page', () => {
await renderInTestApp(tester.reactElement(), {
apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]],
mountPath: '/catalog/:namespace/:kind/:name',
initialRouteEntries: ['/catalog/default/component/artist-lookup'],
config: {
app: {
@@ -417,6 +423,7 @@ describe('Entity page', () => {
await renderInTestApp(tester.reactElement(), {
apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]],
mountPath: '/catalog/:namespace/:kind/:name',
initialRouteEntries: ['/catalog/default/component/artist-lookup'],
config: {
app: {
@@ -457,6 +464,7 @@ describe('Entity page', () => {
await renderInTestApp(tester.reactElement(), {
apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]],
mountPath: '/catalog/:namespace/:kind/:name',
initialRouteEntries: ['/catalog/default/component/artist-lookup'],
config: {
app: {
@@ -496,6 +504,7 @@ describe('Entity page', () => {
await renderInTestApp(tester.reactElement(), {
apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]],
mountPath: '/catalog/:namespace/:kind/:name',
initialRouteEntries: ['/catalog/default/component/artist-lookup'],
config: {
app: {
@@ -543,6 +552,7 @@ describe('Entity page', () => {
await renderInTestApp(tester.reactElement(), {
apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]],
mountPath: '/catalog/:namespace/:kind/:name',
initialRouteEntries: ['/catalog/default/component/artist-lookup'],
config: {
app: {
@@ -590,6 +600,7 @@ describe('Entity page', () => {
await renderInTestApp(tester.reactElement(), {
apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]],
mountPath: '/catalog/:namespace/:kind/:name',
initialRouteEntries: ['/catalog/default/component/artist-lookup'],
config: {
app: {
@@ -625,6 +636,7 @@ describe('Entity page', () => {
await renderInTestApp(tester.reactElement(), {
apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]],
mountPath: '/catalog/:namespace/:kind/:name',
initialRouteEntries: ['/catalog/default/component/artist-lookup'],
config: {
app: {
@@ -662,6 +674,7 @@ describe('Entity page', () => {
await renderInTestApp(tester.reactElement(), {
apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]],
mountPath: '/catalog/:namespace/:kind/:name',
initialRouteEntries: ['/catalog/default/component/artist-lookup'],
config: {
app: {
@@ -719,6 +732,7 @@ describe('Entity page', () => {
await renderInTestApp(tester.reactElement(), {
apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]],
mountPath: '/catalog/:namespace/:kind/:name',
initialRouteEntries: ['/catalog/default/component/artist-lookup'],
config: {
app: {
@@ -772,6 +786,7 @@ describe('Entity page', () => {
await renderInTestApp(tester.reactElement(), {
apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]],
mountPath: '/catalog/:namespace/:kind/:name',
initialRouteEntries: ['/catalog/default/component/artist-lookup'],
config: {
app: {
@@ -861,6 +876,7 @@ describe('Entity page', () => {
.add(filteredMenuItem);
await renderInTestApp(tester.reactElement(), {
mountPath: '/catalog/:namespace/:kind/:name',
initialRouteEntries: ['/catalog/default/component/artist-lookup'],
config: {
app: {