From e5da0ab00f679bff80ebc6306cfbe556a5af6b19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 19 May 2026 23:58:21 +0200 Subject: [PATCH 01/12] fix(catalog): fix flaky entity page test by providing initial route and in-memory catalog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test rendered at '/' instead of a URL matching the entity route pattern, causing useEntityFromUrl to sometimes fail to extract route params. Use initialRouteEntries to start at the correct entity URL, and switch from catalogApiMock.mock() to catalogApiMock() for a working in-memory catalog client. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Fredrik Adelöw --- plugins/catalog/src/alpha/pages.test.tsx | 98 ++++++++---------------- 1 file changed, 34 insertions(+), 64 deletions(-) diff --git a/plugins/catalog/src/alpha/pages.test.tsx b/plugins/catalog/src/alpha/pages.test.tsx index 239f286515..2b66704eab 100644 --- a/plugins/catalog/src/alpha/pages.test.tsx +++ b/plugins/catalog/src/alpha/pages.test.tsx @@ -28,7 +28,6 @@ import { } from '@backstage/plugin-catalog-react/alpha'; import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; import { - catalogApiRef, entityRouteRef, MockStarredEntitiesApi, starredEntitiesApiRef, @@ -108,8 +107,8 @@ describe('Entity page', () => { ], }; - const mockCatalogApi = catalogApiMock.mock({ - getEntityByRef: async () => entityMock, + const mockCatalogApi = catalogApiMock({ + entities: [entityMock as Entity], }); const mockStarredEntitiesApi = new MockStarredEntitiesApi(); @@ -152,10 +151,8 @@ describe('Entity page', () => { .add(apidocsEntityContent); await renderInTestApp(tester.reactElement(), { - apis: [ - [catalogApiRef, mockCatalogApi], - [starredEntitiesApiRef, mockStarredEntitiesApi], - ], + apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], + initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { title: 'Custom app', @@ -199,10 +196,8 @@ describe('Entity page', () => { .add(apidocsEntityContent); await renderInTestApp(tester.reactElement(), { - apis: [ - [catalogApiRef, mockCatalogApi], - [starredEntitiesApiRef, mockStarredEntitiesApi], - ], + apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], + initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { title: 'Custom app', @@ -239,10 +234,8 @@ describe('Entity page', () => { }); await renderInTestApp(tester.reactElement(), { - apis: [ - [catalogApiRef, mockCatalogApi], - [starredEntitiesApiRef, mockStarredEntitiesApi], - ], + apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], + initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { title: 'Custom app', @@ -292,10 +285,8 @@ describe('Entity page', () => { }); await renderInTestApp(tester.reactElement(), { - apis: [ - [catalogApiRef, mockCatalogApi], - [starredEntitiesApiRef, mockStarredEntitiesApi], - ], + apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], + initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { title: 'Custom app', @@ -333,10 +324,8 @@ describe('Entity page', () => { }); await renderInTestApp(tester.reactElement(), { - apis: [ - [catalogApiRef, mockCatalogApi], - [starredEntitiesApiRef, mockStarredEntitiesApi], - ], + apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], + initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { title: 'Custom app', @@ -367,10 +356,8 @@ describe('Entity page', () => { .add(overviewEntityContent); await renderInTestApp(tester.reactElement(), { - apis: [ - [catalogApiRef, mockCatalogApi], - [starredEntitiesApiRef, mockStarredEntitiesApi], - ], + apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], + initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { title: 'Custom app', @@ -413,10 +400,8 @@ describe('Entity page', () => { .add(apidocsEntityContent); await renderInTestApp(tester.reactElement(), { - apis: [ - [catalogApiRef, mockCatalogApi], - [starredEntitiesApiRef, mockStarredEntitiesApi], - ], + apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], + initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { title: 'Custom app', @@ -449,10 +434,8 @@ describe('Entity page', () => { .add(apidocsEntityContent); await renderInTestApp(tester.reactElement(), { - apis: [ - [catalogApiRef, mockCatalogApi], - [starredEntitiesApiRef, mockStarredEntitiesApi], - ], + apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], + initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { title: 'Custom app', @@ -490,10 +473,8 @@ describe('Entity page', () => { .add(apidocsEntityContent); await renderInTestApp(tester.reactElement(), { - apis: [ - [catalogApiRef, mockCatalogApi], - [starredEntitiesApiRef, mockStarredEntitiesApi], - ], + apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], + initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { title: 'Custom app', @@ -539,10 +520,8 @@ describe('Entity page', () => { .add(apidocsEntityContent); await renderInTestApp(tester.reactElement(), { - apis: [ - [catalogApiRef, mockCatalogApi], - [starredEntitiesApiRef, mockStarredEntitiesApi], - ], + apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], + initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { title: 'Custom app', @@ -588,10 +567,8 @@ describe('Entity page', () => { }); await renderInTestApp(tester.reactElement(), { - apis: [ - [catalogApiRef, mockCatalogApi], - [starredEntitiesApiRef, mockStarredEntitiesApi], - ], + apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], + initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { title: 'Custom app', @@ -625,10 +602,8 @@ describe('Entity page', () => { ); await renderInTestApp(tester.reactElement(), { - apis: [ - [catalogApiRef, mockCatalogApi], - [starredEntitiesApiRef, mockStarredEntitiesApi], - ], + apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], + initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { title: 'Custom app', @@ -664,10 +639,8 @@ describe('Entity page', () => { ).add(customEntityHeader); await renderInTestApp(tester.reactElement(), { - apis: [ - [catalogApiRef, mockCatalogApi], - [starredEntitiesApiRef, mockStarredEntitiesApi], - ], + apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], + initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { title: 'Custom app', @@ -723,10 +696,8 @@ describe('Entity page', () => { ).add(menuItem); await renderInTestApp(tester.reactElement(), { - apis: [ - [catalogApiRef, mockCatalogApi], - [starredEntitiesApiRef, mockStarredEntitiesApi], - ], + apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], + initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { title: 'Custom app', @@ -778,10 +749,8 @@ describe('Entity page', () => { ).add(menuItem); await renderInTestApp(tester.reactElement(), { - apis: [ - [catalogApiRef, mockCatalogApi], - [starredEntitiesApiRef, mockStarredEntitiesApi], - ], + apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], + initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { title: 'Custom app', @@ -882,9 +851,10 @@ describe('Entity page', () => { convertLegacyRouteRef(entityRouteRef), }, apis: [ - [catalogApiRef, mockCatalogApi], + mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi], ], + initialRouteEntries: ['/catalog/default/component/artist-lookup'], }); await userEvent.click(await screen.findByTestId('menu-button')); From ac1a4ab3c9ed86556a776edb1d0dfa12f9d5d2b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 20 May 2026 00:02:02 +0200 Subject: [PATCH 02/12] Type entityMock as Entity and remove casts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Fredrik Adelöw --- plugins/catalog/src/alpha/pages.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/catalog/src/alpha/pages.test.tsx b/plugins/catalog/src/alpha/pages.test.tsx index 2b66704eab..3123826bd5 100644 --- a/plugins/catalog/src/alpha/pages.test.tsx +++ b/plugins/catalog/src/alpha/pages.test.tsx @@ -39,7 +39,7 @@ import { Entity } from '@backstage/catalog-model'; jest.setTimeout(30_000); describe('Entity page', () => { - const entityMock = { + const entityMock: Entity = { metadata: { namespace: 'default', annotations: { @@ -108,7 +108,7 @@ describe('Entity page', () => { }; const mockCatalogApi = catalogApiMock({ - entities: [entityMock as Entity], + entities: [entityMock], }); const mockStarredEntitiesApi = new MockStarredEntitiesApi(); From 479d78ed5c2c147de266ec9692e05a845c7f247a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 20 May 2026 08:34:28 +0200 Subject: [PATCH 03/12] Revert to catalogApiMock.mock(), only keep Entity type annotation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The InMemoryCatalogClient + initialRouteEntries approach didn't work because route params weren't being resolved by the test harness, causing getEntityByRef to receive an empty ref. The original mock with getEntityByRef: async () => entityMock works correctly. The only change from master is adding the Entity type annotation. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Fredrik Adelöw --- plugins/catalog/src/alpha/pages.test.tsx | 98 ++++++++++++++++-------- 1 file changed, 64 insertions(+), 34 deletions(-) diff --git a/plugins/catalog/src/alpha/pages.test.tsx b/plugins/catalog/src/alpha/pages.test.tsx index 3123826bd5..48eac13d5b 100644 --- a/plugins/catalog/src/alpha/pages.test.tsx +++ b/plugins/catalog/src/alpha/pages.test.tsx @@ -28,6 +28,7 @@ import { } from '@backstage/plugin-catalog-react/alpha'; import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; import { + catalogApiRef, entityRouteRef, MockStarredEntitiesApi, starredEntitiesApiRef, @@ -107,8 +108,8 @@ describe('Entity page', () => { ], }; - const mockCatalogApi = catalogApiMock({ - entities: [entityMock], + const mockCatalogApi = catalogApiMock.mock({ + getEntityByRef: async () => entityMock, }); const mockStarredEntitiesApi = new MockStarredEntitiesApi(); @@ -151,8 +152,10 @@ describe('Entity page', () => { .add(apidocsEntityContent); await renderInTestApp(tester.reactElement(), { - apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], - initialRouteEntries: ['/catalog/default/component/artist-lookup'], + apis: [ + [catalogApiRef, mockCatalogApi], + [starredEntitiesApiRef, mockStarredEntitiesApi], + ], config: { app: { title: 'Custom app', @@ -196,8 +199,10 @@ describe('Entity page', () => { .add(apidocsEntityContent); await renderInTestApp(tester.reactElement(), { - apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], - initialRouteEntries: ['/catalog/default/component/artist-lookup'], + apis: [ + [catalogApiRef, mockCatalogApi], + [starredEntitiesApiRef, mockStarredEntitiesApi], + ], config: { app: { title: 'Custom app', @@ -234,8 +239,10 @@ describe('Entity page', () => { }); await renderInTestApp(tester.reactElement(), { - apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], - initialRouteEntries: ['/catalog/default/component/artist-lookup'], + apis: [ + [catalogApiRef, mockCatalogApi], + [starredEntitiesApiRef, mockStarredEntitiesApi], + ], config: { app: { title: 'Custom app', @@ -285,8 +292,10 @@ describe('Entity page', () => { }); await renderInTestApp(tester.reactElement(), { - apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], - initialRouteEntries: ['/catalog/default/component/artist-lookup'], + apis: [ + [catalogApiRef, mockCatalogApi], + [starredEntitiesApiRef, mockStarredEntitiesApi], + ], config: { app: { title: 'Custom app', @@ -324,8 +333,10 @@ describe('Entity page', () => { }); await renderInTestApp(tester.reactElement(), { - apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], - initialRouteEntries: ['/catalog/default/component/artist-lookup'], + apis: [ + [catalogApiRef, mockCatalogApi], + [starredEntitiesApiRef, mockStarredEntitiesApi], + ], config: { app: { title: 'Custom app', @@ -356,8 +367,10 @@ describe('Entity page', () => { .add(overviewEntityContent); await renderInTestApp(tester.reactElement(), { - apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], - initialRouteEntries: ['/catalog/default/component/artist-lookup'], + apis: [ + [catalogApiRef, mockCatalogApi], + [starredEntitiesApiRef, mockStarredEntitiesApi], + ], config: { app: { title: 'Custom app', @@ -400,8 +413,10 @@ describe('Entity page', () => { .add(apidocsEntityContent); await renderInTestApp(tester.reactElement(), { - apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], - initialRouteEntries: ['/catalog/default/component/artist-lookup'], + apis: [ + [catalogApiRef, mockCatalogApi], + [starredEntitiesApiRef, mockStarredEntitiesApi], + ], config: { app: { title: 'Custom app', @@ -434,8 +449,10 @@ describe('Entity page', () => { .add(apidocsEntityContent); await renderInTestApp(tester.reactElement(), { - apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], - initialRouteEntries: ['/catalog/default/component/artist-lookup'], + apis: [ + [catalogApiRef, mockCatalogApi], + [starredEntitiesApiRef, mockStarredEntitiesApi], + ], config: { app: { title: 'Custom app', @@ -473,8 +490,10 @@ describe('Entity page', () => { .add(apidocsEntityContent); await renderInTestApp(tester.reactElement(), { - apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], - initialRouteEntries: ['/catalog/default/component/artist-lookup'], + apis: [ + [catalogApiRef, mockCatalogApi], + [starredEntitiesApiRef, mockStarredEntitiesApi], + ], config: { app: { title: 'Custom app', @@ -520,8 +539,10 @@ describe('Entity page', () => { .add(apidocsEntityContent); await renderInTestApp(tester.reactElement(), { - apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], - initialRouteEntries: ['/catalog/default/component/artist-lookup'], + apis: [ + [catalogApiRef, mockCatalogApi], + [starredEntitiesApiRef, mockStarredEntitiesApi], + ], config: { app: { title: 'Custom app', @@ -567,8 +588,10 @@ describe('Entity page', () => { }); await renderInTestApp(tester.reactElement(), { - apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], - initialRouteEntries: ['/catalog/default/component/artist-lookup'], + apis: [ + [catalogApiRef, mockCatalogApi], + [starredEntitiesApiRef, mockStarredEntitiesApi], + ], config: { app: { title: 'Custom app', @@ -602,8 +625,10 @@ describe('Entity page', () => { ); await renderInTestApp(tester.reactElement(), { - apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], - initialRouteEntries: ['/catalog/default/component/artist-lookup'], + apis: [ + [catalogApiRef, mockCatalogApi], + [starredEntitiesApiRef, mockStarredEntitiesApi], + ], config: { app: { title: 'Custom app', @@ -639,8 +664,10 @@ describe('Entity page', () => { ).add(customEntityHeader); await renderInTestApp(tester.reactElement(), { - apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], - initialRouteEntries: ['/catalog/default/component/artist-lookup'], + apis: [ + [catalogApiRef, mockCatalogApi], + [starredEntitiesApiRef, mockStarredEntitiesApi], + ], config: { app: { title: 'Custom app', @@ -696,8 +723,10 @@ describe('Entity page', () => { ).add(menuItem); await renderInTestApp(tester.reactElement(), { - apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], - initialRouteEntries: ['/catalog/default/component/artist-lookup'], + apis: [ + [catalogApiRef, mockCatalogApi], + [starredEntitiesApiRef, mockStarredEntitiesApi], + ], config: { app: { title: 'Custom app', @@ -749,8 +778,10 @@ describe('Entity page', () => { ).add(menuItem); await renderInTestApp(tester.reactElement(), { - apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], - initialRouteEntries: ['/catalog/default/component/artist-lookup'], + apis: [ + [catalogApiRef, mockCatalogApi], + [starredEntitiesApiRef, mockStarredEntitiesApi], + ], config: { app: { title: 'Custom app', @@ -851,10 +882,9 @@ describe('Entity page', () => { convertLegacyRouteRef(entityRouteRef), }, apis: [ - mockCatalogApi, + [catalogApiRef, mockCatalogApi], [starredEntitiesApiRef, mockStarredEntitiesApi], ], - initialRouteEntries: ['/catalog/default/component/artist-lookup'], }); await userEvent.click(await screen.findByTestId('menu-button')); From a58bcdcb47ae4135c16f35a525c0f629e0f9c8e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 20 May 2026 08:41:04 +0200 Subject: [PATCH 04/12] Add initialRouteEntries to fix flaky entity page test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The entity page resolves route params via useRouteRefParams. Without initialRouteEntries, the MemoryRouter starts at '/' which doesn't match the entity route pattern. On fast machines the mock entity resolves before the useEffect error check fires, but on slow CI runners the timing differs and the "No name provided!" error path wins, causing the test to flake. Adding initialRouteEntries ensures the router starts at a URL that matches /catalog/:namespace/:kind/:name, so route params are always available regardless of timing. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Fredrik Adelöw --- plugins/catalog/src/alpha/pages.test.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/plugins/catalog/src/alpha/pages.test.tsx b/plugins/catalog/src/alpha/pages.test.tsx index 48eac13d5b..3cd14cb510 100644 --- a/plugins/catalog/src/alpha/pages.test.tsx +++ b/plugins/catalog/src/alpha/pages.test.tsx @@ -156,6 +156,7 @@ describe('Entity page', () => { [catalogApiRef, mockCatalogApi], [starredEntitiesApiRef, mockStarredEntitiesApi], ], + initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { title: 'Custom app', @@ -203,6 +204,7 @@ describe('Entity page', () => { [catalogApiRef, mockCatalogApi], [starredEntitiesApiRef, mockStarredEntitiesApi], ], + initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { title: 'Custom app', @@ -243,6 +245,7 @@ describe('Entity page', () => { [catalogApiRef, mockCatalogApi], [starredEntitiesApiRef, mockStarredEntitiesApi], ], + initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { title: 'Custom app', @@ -296,6 +299,7 @@ describe('Entity page', () => { [catalogApiRef, mockCatalogApi], [starredEntitiesApiRef, mockStarredEntitiesApi], ], + initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { title: 'Custom app', @@ -337,6 +341,7 @@ describe('Entity page', () => { [catalogApiRef, mockCatalogApi], [starredEntitiesApiRef, mockStarredEntitiesApi], ], + initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { title: 'Custom app', @@ -371,6 +376,7 @@ describe('Entity page', () => { [catalogApiRef, mockCatalogApi], [starredEntitiesApiRef, mockStarredEntitiesApi], ], + initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { title: 'Custom app', @@ -417,6 +423,7 @@ describe('Entity page', () => { [catalogApiRef, mockCatalogApi], [starredEntitiesApiRef, mockStarredEntitiesApi], ], + initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { title: 'Custom app', @@ -453,6 +460,7 @@ describe('Entity page', () => { [catalogApiRef, mockCatalogApi], [starredEntitiesApiRef, mockStarredEntitiesApi], ], + initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { title: 'Custom app', @@ -494,6 +502,7 @@ describe('Entity page', () => { [catalogApiRef, mockCatalogApi], [starredEntitiesApiRef, mockStarredEntitiesApi], ], + initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { title: 'Custom app', @@ -543,6 +552,7 @@ describe('Entity page', () => { [catalogApiRef, mockCatalogApi], [starredEntitiesApiRef, mockStarredEntitiesApi], ], + initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { title: 'Custom app', @@ -592,6 +602,7 @@ describe('Entity page', () => { [catalogApiRef, mockCatalogApi], [starredEntitiesApiRef, mockStarredEntitiesApi], ], + initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { title: 'Custom app', @@ -629,6 +640,7 @@ describe('Entity page', () => { [catalogApiRef, mockCatalogApi], [starredEntitiesApiRef, mockStarredEntitiesApi], ], + initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { title: 'Custom app', @@ -668,6 +680,7 @@ describe('Entity page', () => { [catalogApiRef, mockCatalogApi], [starredEntitiesApiRef, mockStarredEntitiesApi], ], + initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { title: 'Custom app', @@ -727,6 +740,7 @@ describe('Entity page', () => { [catalogApiRef, mockCatalogApi], [starredEntitiesApiRef, mockStarredEntitiesApi], ], + initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { title: 'Custom app', @@ -782,6 +796,7 @@ describe('Entity page', () => { [catalogApiRef, mockCatalogApi], [starredEntitiesApiRef, mockStarredEntitiesApi], ], + initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { title: 'Custom app', From 95e107bc6ee58767c814fd2f812747a640e79b7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 20 May 2026 09:42:47 +0200 Subject: [PATCH 05/12] Wrap test element in Routes when mountedRoutes are provided MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit renderInTestApp disables app/routes, so the test element was rendered without any Route matching. This meant useParams() always returned {} even when initialRouteEntries and mountedRoutes were set — the mounted routes only fed the route resolution API for link generation, never created actual React Router matching for param extraction. Wrap the test element in with a for each mounted path (plus a path="*" fallback), mirroring what the real AppRoutes extension does via useRoutes(). This makes useParams() return real params when initialRouteEntries matches a mounted route path. Update entity page test href expectations to match the now-correct production behavior where sub-route links resolve relative to the matched parent Route, and replace synchronous getByTestId/getByText with async findBy queries for lazy-loaded content. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Fredrik Adelöw --- .../src/app/renderInTestApp.tsx | 22 +++++++++- plugins/catalog/src/alpha/pages.test.tsx | 44 ++++++++++++++----- 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/packages/frontend-test-utils/src/app/renderInTestApp.tsx b/packages/frontend-test-utils/src/app/renderInTestApp.tsx index d599bdca01..e45bd9a8ca 100644 --- a/packages/frontend-test-utils/src/app/renderInTestApp.tsx +++ b/packages/frontend-test-utils/src/app/renderInTestApp.tsx @@ -15,7 +15,7 @@ */ import { Fragment } from 'react'; -import { MemoryRouter } from 'react-router-dom'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; import { prepareSpecializedApp } from '@backstage/frontend-app-api'; import { RenderResult, render } from '@testing-library/react'; import { ConfigReader } from '@backstage/config'; @@ -125,11 +125,31 @@ export function renderInTestApp( element: JSX.Element, options?: TestAppOptions, ): RenderResult { + const mountedPaths = options?.mountedRoutes + ? Object.keys(options.mountedRoutes) + : []; + const extensions: Array = [ createExtension({ attachTo: { id: 'app/root', input: 'children' }, output: [coreExtensionData.reactElement], factory: () => { + if (mountedPaths.length > 0) { + return [ + coreExtensionData.reactElement( + + {mountedPaths.map(path => ( + + ))} + + , + ), + ]; + } return [coreExtensionData.reactElement(element)]; }, }), diff --git a/plugins/catalog/src/alpha/pages.test.tsx b/plugins/catalog/src/alpha/pages.test.tsx index 3cd14cb510..6ed586abd0 100644 --- a/plugins/catalog/src/alpha/pages.test.tsx +++ b/plugins/catalog/src/alpha/pages.test.tsx @@ -176,11 +176,17 @@ describe('Entity page', () => { await expect( screen.findByRole('button', { name: /TechDocs/ }), - ).resolves.toHaveAttribute('href', '/techdocs'); + ).resolves.toHaveAttribute( + 'href', + '/catalog/default/component/artist-lookup/techdocs', + ); await expect( screen.findByRole('button', { name: /ApiDocs/ }), - ).resolves.toHaveAttribute('href', '/apidocs'); + ).resolves.toHaveAttribute( + 'href', + '/catalog/default/component/artist-lookup/apidocs', + ); }); it('Should rename a default group', async () => { @@ -222,11 +228,17 @@ describe('Entity page', () => { await expect( screen.findByRole('button', { name: /TechDocs/ }), - ).resolves.toHaveAttribute('href', '/techdocs'); + ).resolves.toHaveAttribute( + 'href', + '/catalog/default/component/artist-lookup/techdocs', + ); await expect( screen.findByRole('button', { name: /ApiDocs/ }), - ).resolves.toHaveAttribute('href', '/apidocs'); + ).resolves.toHaveAttribute( + 'href', + '/catalog/default/component/artist-lookup/apidocs', + ); }); it('Should disassociate a content with a default group', async () => { @@ -317,11 +329,17 @@ describe('Entity page', () => { await expect( screen.findByRole('button', { name: /TechDocs/ }), - ).resolves.toHaveAttribute('href', '/techdocs'); + ).resolves.toHaveAttribute( + 'href', + '/catalog/default/component/artist-lookup/techdocs', + ); await expect( screen.findByRole('button', { name: /ApiDocs/ }), - ).resolves.toHaveAttribute('href', '/apidocs'); + ).resolves.toHaveAttribute( + 'href', + '/catalog/default/component/artist-lookup/apidocs', + ); }); it('Should render a single-content groups as a normal tab', async () => { @@ -441,11 +459,17 @@ describe('Entity page', () => { await expect( screen.findByRole('button', { name: /TechDocs/ }), - ).resolves.toHaveAttribute('href', '/techdocs'); + ).resolves.toHaveAttribute( + 'href', + '/catalog/default/component/artist-lookup/techdocs', + ); await expect( screen.findByRole('button', { name: /ApiDocs/ }), - ).resolves.toHaveAttribute('href', '/apidocs'); + ).resolves.toHaveAttribute( + 'href', + '/catalog/default/component/artist-lookup/apidocs', + ); }); it('Should sort content by title by default', async () => { @@ -816,14 +840,14 @@ describe('Entity page', () => { screen.findByText(/artist-lookup/), ).resolves.toBeInTheDocument(); - await userEvent.click(screen.getByTestId('menu-button')); + await userEvent.click(await screen.findByTestId('menu-button')); await expect( screen.findByText('Test Title'), ).resolves.toBeInTheDocument(); await expect(screen.findByText('Test Icon')).resolves.toBeInTheDocument(); - const listItem = screen.getByText('Test Title').closest('li'); + const listItem = (await screen.findByText('Test Title')).closest('li'); expect(listItem).toHaveAttribute('aria-disabled', disabled.toString()); if (!disabled) { await userEvent.click(screen.getByText('Test Title')); From a06003bc90d43389f5c89587df3ea383cb8704b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 20 May 2026 10:04:51 +0200 Subject: [PATCH 06/12] Switch to in-memory catalog client for entity page tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now that renderInTestApp properly sets up Route matching, the test can use the real in-memory catalog client instead of jest stubs. The in-memory client does actual ref-based lookups, verifying that useEntityFromUrl receives correct route params end-to-end. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Fredrik Adelöw --- .../src/app/renderInTestApp.tsx | 29 ++++--- plugins/catalog/src/alpha/pages.test.tsx | 83 ++++--------------- 2 files changed, 32 insertions(+), 80 deletions(-) diff --git a/packages/frontend-test-utils/src/app/renderInTestApp.tsx b/packages/frontend-test-utils/src/app/renderInTestApp.tsx index e45bd9a8ca..170d6d7294 100644 --- a/packages/frontend-test-utils/src/app/renderInTestApp.tsx +++ b/packages/frontend-test-utils/src/app/renderInTestApp.tsx @@ -134,23 +134,22 @@ export function renderInTestApp( attachTo: { id: 'app/root', input: 'children' }, output: [coreExtensionData.reactElement], factory: () => { + let content: JSX.Element = element; if (mountedPaths.length > 0) { - return [ - coreExtensionData.reactElement( - - {mountedPaths.map(path => ( - - ))} - - , - ), - ]; + content = ( + + {mountedPaths.map(path => ( + + ))} + + + ); } - return [coreExtensionData.reactElement(element)]; + return [coreExtensionData.reactElement(content)]; }, }), ]; diff --git a/plugins/catalog/src/alpha/pages.test.tsx b/plugins/catalog/src/alpha/pages.test.tsx index 6ed586abd0..1d98c84403 100644 --- a/plugins/catalog/src/alpha/pages.test.tsx +++ b/plugins/catalog/src/alpha/pages.test.tsx @@ -28,7 +28,6 @@ import { } from '@backstage/plugin-catalog-react/alpha'; import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; import { - catalogApiRef, entityRouteRef, MockStarredEntitiesApi, starredEntitiesApiRef, @@ -108,9 +107,7 @@ describe('Entity page', () => { ], }; - const mockCatalogApi = catalogApiMock.mock({ - getEntityByRef: async () => entityMock, - }); + const mockCatalogApi = catalogApiMock({ entities: [entityMock] }); const mockStarredEntitiesApi = new MockStarredEntitiesApi(); @@ -152,10 +149,7 @@ describe('Entity page', () => { .add(apidocsEntityContent); await renderInTestApp(tester.reactElement(), { - apis: [ - [catalogApiRef, mockCatalogApi], - [starredEntitiesApiRef, mockStarredEntitiesApi], - ], + apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { @@ -206,10 +200,7 @@ describe('Entity page', () => { .add(apidocsEntityContent); await renderInTestApp(tester.reactElement(), { - apis: [ - [catalogApiRef, mockCatalogApi], - [starredEntitiesApiRef, mockStarredEntitiesApi], - ], + apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { @@ -253,10 +244,7 @@ describe('Entity page', () => { }); await renderInTestApp(tester.reactElement(), { - apis: [ - [catalogApiRef, mockCatalogApi], - [starredEntitiesApiRef, mockStarredEntitiesApi], - ], + apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { @@ -307,10 +295,7 @@ describe('Entity page', () => { }); await renderInTestApp(tester.reactElement(), { - apis: [ - [catalogApiRef, mockCatalogApi], - [starredEntitiesApiRef, mockStarredEntitiesApi], - ], + apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { @@ -355,10 +340,7 @@ describe('Entity page', () => { }); await renderInTestApp(tester.reactElement(), { - apis: [ - [catalogApiRef, mockCatalogApi], - [starredEntitiesApiRef, mockStarredEntitiesApi], - ], + apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { @@ -390,10 +372,7 @@ describe('Entity page', () => { .add(overviewEntityContent); await renderInTestApp(tester.reactElement(), { - apis: [ - [catalogApiRef, mockCatalogApi], - [starredEntitiesApiRef, mockStarredEntitiesApi], - ], + apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { @@ -437,10 +416,7 @@ describe('Entity page', () => { .add(apidocsEntityContent); await renderInTestApp(tester.reactElement(), { - apis: [ - [catalogApiRef, mockCatalogApi], - [starredEntitiesApiRef, mockStarredEntitiesApi], - ], + apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { @@ -480,10 +456,7 @@ describe('Entity page', () => { .add(apidocsEntityContent); await renderInTestApp(tester.reactElement(), { - apis: [ - [catalogApiRef, mockCatalogApi], - [starredEntitiesApiRef, mockStarredEntitiesApi], - ], + apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { @@ -522,10 +495,7 @@ describe('Entity page', () => { .add(apidocsEntityContent); await renderInTestApp(tester.reactElement(), { - apis: [ - [catalogApiRef, mockCatalogApi], - [starredEntitiesApiRef, mockStarredEntitiesApi], - ], + apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { @@ -572,10 +542,7 @@ describe('Entity page', () => { .add(apidocsEntityContent); await renderInTestApp(tester.reactElement(), { - apis: [ - [catalogApiRef, mockCatalogApi], - [starredEntitiesApiRef, mockStarredEntitiesApi], - ], + apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { @@ -622,10 +589,7 @@ describe('Entity page', () => { }); await renderInTestApp(tester.reactElement(), { - apis: [ - [catalogApiRef, mockCatalogApi], - [starredEntitiesApiRef, mockStarredEntitiesApi], - ], + apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { @@ -660,10 +624,7 @@ describe('Entity page', () => { ); await renderInTestApp(tester.reactElement(), { - apis: [ - [catalogApiRef, mockCatalogApi], - [starredEntitiesApiRef, mockStarredEntitiesApi], - ], + apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { @@ -700,10 +661,7 @@ describe('Entity page', () => { ).add(customEntityHeader); await renderInTestApp(tester.reactElement(), { - apis: [ - [catalogApiRef, mockCatalogApi], - [starredEntitiesApiRef, mockStarredEntitiesApi], - ], + apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { @@ -760,10 +718,7 @@ describe('Entity page', () => { ).add(menuItem); await renderInTestApp(tester.reactElement(), { - apis: [ - [catalogApiRef, mockCatalogApi], - [starredEntitiesApiRef, mockStarredEntitiesApi], - ], + apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { @@ -816,10 +771,7 @@ describe('Entity page', () => { ).add(menuItem); await renderInTestApp(tester.reactElement(), { - apis: [ - [catalogApiRef, mockCatalogApi], - [starredEntitiesApiRef, mockStarredEntitiesApi], - ], + apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { @@ -909,6 +861,7 @@ describe('Entity page', () => { .add(filteredMenuItem); await renderInTestApp(tester.reactElement(), { + initialRouteEntries: ['/catalog/default/component/artist-lookup'], config: { app: { title: 'Custom app', @@ -921,7 +874,7 @@ describe('Entity page', () => { convertLegacyRouteRef(entityRouteRef), }, apis: [ - [catalogApiRef, mockCatalogApi], + mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi], ], }); From 62dd4fc3bc027d5664d13e2e50caf8bef4f0673c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 20 May 2026 10:29:27 +0200 Subject: [PATCH 07/12] Add changeset for renderInTestApp route matching fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Fredrik Adelöw --- .changeset/fix-test-route-matching.md | 5 +++++ packages/frontend-test-utils/src/app/renderInTestApp.tsx | 6 ++++-- 2 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 .changeset/fix-test-route-matching.md diff --git a/.changeset/fix-test-route-matching.md b/.changeset/fix-test-route-matching.md new file mode 100644 index 0000000000..4f69688f86 --- /dev/null +++ b/.changeset/fix-test-route-matching.md @@ -0,0 +1,5 @@ +--- +'@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 `` elements for param extraction (`useParams`). This meant that `initialRouteEntries` had no effect on route param availability. The test element is now wrapped in `` with a `` for each mounted path, matching the behavior of the real `AppRoutes` extension. diff --git a/packages/frontend-test-utils/src/app/renderInTestApp.tsx b/packages/frontend-test-utils/src/app/renderInTestApp.tsx index 170d6d7294..07dccd9eee 100644 --- a/packages/frontend-test-utils/src/app/renderInTestApp.tsx +++ b/packages/frontend-test-utils/src/app/renderInTestApp.tsx @@ -135,6 +135,7 @@ export function renderInTestApp( output: [coreExtensionData.reactElement], factory: () => { let content: JSX.Element = element; + if (mountedPaths.length > 0) { content = ( @@ -142,13 +143,14 @@ export function renderInTestApp( ))} - + ); } + return [coreExtensionData.reactElement(content)]; }, }), From 5dca781385cdc8c6bc212de47e525653f5465052 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 21 May 2026 13:39:52 +0200 Subject: [PATCH 08/12] Replace mountedRoutes-based wrapping with mountPath option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Signed-off-by: Fredrik Adelöw --- .changeset/fix-test-route-matching.md | 2 +- .../src/app/renderInTestApp.tsx | 44 +++++++++++++------ plugins/catalog/src/alpha/pages.test.tsx | 16 +++++++ 3 files changed, 48 insertions(+), 14 deletions(-) diff --git a/.changeset/fix-test-route-matching.md b/.changeset/fix-test-route-matching.md index 4f69688f86..359fe19164 100644 --- a/.changeset/fix-test-route-matching.md +++ b/.changeset/fix-test-route-matching.md @@ -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 `` elements for param extraction (`useParams`). This meant that `initialRouteEntries` had no effect on route param availability. The test element is now wrapped in `` with a `` 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 `` 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`. diff --git a/packages/frontend-test-utils/src/app/renderInTestApp.tsx b/packages/frontend-test-utils/src/app/renderInTestApp.tsx index 07dccd9eee..1344fc612e 100644 --- a/packages/frontend-test-utils/src/app/renderInTestApp.tsx +++ b/packages/frontend-test-utils/src/app/renderInTestApp.tsx @@ -80,7 +80,28 @@ export type TestAppOptions = { 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 `` 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(, { + * 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( element: JSX.Element, options?: TestAppOptions, ): RenderResult { - const mountedPaths = options?.mountedRoutes - ? Object.keys(options.mountedRoutes) - : []; + const mountPath = options?.mountPath; const extensions: Array = [ createExtension({ @@ -136,16 +155,12 @@ export function renderInTestApp( factory: () => { let content: JSX.Element = element; - if (mountedPaths.length > 0) { + if (mountPath) { + const routePath = + mountPath === '/' ? mountPath : `${mountPath.replace(/\/$/, '')}/*`; content = ( - {mountedPaths.map(path => ( - - ))} + ); @@ -198,7 +213,10 @@ export function renderInTestApp( params: { component: ({ children }) => ( { 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: { From bc1b4263c9efbfeb12a6258caa567427c19d4758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 21 May 2026 15:40:39 +0200 Subject: [PATCH 09/12] Update API report for frontend-test-utils MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Fredrik Adelöw --- packages/frontend-test-utils/report.api.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/frontend-test-utils/report.api.md b/packages/frontend-test-utils/report.api.md index d24046953f..309d304fe1 100644 --- a/packages/frontend-test-utils/report.api.md +++ b/packages/frontend-test-utils/report.api.md @@ -478,6 +478,7 @@ export type TestAppOptions = { }; config?: JsonObject; features?: FrontendFeature[]; + mountPath?: string; initialRouteEntries?: string[]; apis?: readonly [...TestApiPairs]; }; From fcde31d871c4894cae9eedb08f737f23d1ffd048 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 21 May 2026 15:49:36 +0200 Subject: [PATCH 10/12] Address review: remove fallback route, handle trailing splat, extract shared path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove the path="*" fallback Route so mismatches between mountPath and initialRouteEntries surface as test failures instead of silently rendering without params - Handle mountPath values that already end with /* to avoid double splat - Extract repeated entity path string into a shared constant Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Fredrik Adelöw --- .../src/app/renderInTestApp.tsx | 5 +- plugins/catalog/src/alpha/pages.test.tsx | 74 +++++++------------ 2 files changed, 29 insertions(+), 50 deletions(-) diff --git a/packages/frontend-test-utils/src/app/renderInTestApp.tsx b/packages/frontend-test-utils/src/app/renderInTestApp.tsx index 1344fc612e..82b9619566 100644 --- a/packages/frontend-test-utils/src/app/renderInTestApp.tsx +++ b/packages/frontend-test-utils/src/app/renderInTestApp.tsx @@ -157,11 +157,12 @@ export function renderInTestApp( if (mountPath) { const routePath = - mountPath === '/' ? mountPath : `${mountPath.replace(/\/$/, '')}/*`; + mountPath === '/' || mountPath.endsWith('/*') + ? mountPath + : `${mountPath.replace(/\/$/, '')}/*`; content = ( - ); } diff --git a/plugins/catalog/src/alpha/pages.test.tsx b/plugins/catalog/src/alpha/pages.test.tsx index bea98edf09..6807817967 100644 --- a/plugins/catalog/src/alpha/pages.test.tsx +++ b/plugins/catalog/src/alpha/pages.test.tsx @@ -107,6 +107,8 @@ describe('Entity page', () => { ], }; + const entityPath = '/catalog/default/component/artist-lookup'; + const mockCatalogApi = catalogApiMock({ entities: [entityMock] }); const mockStarredEntitiesApi = new MockStarredEntitiesApi(); @@ -151,7 +153,7 @@ describe('Entity page', () => { await renderInTestApp(tester.reactElement(), { apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], mountPath: '/catalog/:namespace/:kind/:name', - initialRouteEntries: ['/catalog/default/component/artist-lookup'], + initialRouteEntries: [entityPath], config: { app: { title: 'Custom app', @@ -171,17 +173,11 @@ describe('Entity page', () => { await expect( screen.findByRole('button', { name: /TechDocs/ }), - ).resolves.toHaveAttribute( - 'href', - '/catalog/default/component/artist-lookup/techdocs', - ); + ).resolves.toHaveAttribute('href', `${entityPath}/techdocs`); await expect( screen.findByRole('button', { name: /ApiDocs/ }), - ).resolves.toHaveAttribute( - 'href', - '/catalog/default/component/artist-lookup/apidocs', - ); + ).resolves.toHaveAttribute('href', `${entityPath}/apidocs`); }); it('Should rename a default group', async () => { @@ -203,7 +199,7 @@ describe('Entity page', () => { await renderInTestApp(tester.reactElement(), { apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], mountPath: '/catalog/:namespace/:kind/:name', - initialRouteEntries: ['/catalog/default/component/artist-lookup'], + initialRouteEntries: [entityPath], config: { app: { title: 'Custom app', @@ -221,17 +217,11 @@ describe('Entity page', () => { await expect( screen.findByRole('button', { name: /TechDocs/ }), - ).resolves.toHaveAttribute( - 'href', - '/catalog/default/component/artist-lookup/techdocs', - ); + ).resolves.toHaveAttribute('href', `${entityPath}/techdocs`); await expect( screen.findByRole('button', { name: /ApiDocs/ }), - ).resolves.toHaveAttribute( - 'href', - '/catalog/default/component/artist-lookup/apidocs', - ); + ).resolves.toHaveAttribute('href', `${entityPath}/apidocs`); }); it('Should disassociate a content with a default group', async () => { @@ -248,7 +238,7 @@ describe('Entity page', () => { await renderInTestApp(tester.reactElement(), { apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], mountPath: '/catalog/:namespace/:kind/:name', - initialRouteEntries: ['/catalog/default/component/artist-lookup'], + initialRouteEntries: [entityPath], config: { app: { title: 'Custom app', @@ -300,7 +290,7 @@ describe('Entity page', () => { await renderInTestApp(tester.reactElement(), { apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], mountPath: '/catalog/:namespace/:kind/:name', - initialRouteEntries: ['/catalog/default/component/artist-lookup'], + initialRouteEntries: [entityPath], config: { app: { title: 'Custom app', @@ -318,17 +308,11 @@ describe('Entity page', () => { await expect( screen.findByRole('button', { name: /TechDocs/ }), - ).resolves.toHaveAttribute( - 'href', - '/catalog/default/component/artist-lookup/techdocs', - ); + ).resolves.toHaveAttribute('href', `${entityPath}/techdocs`); await expect( screen.findByRole('button', { name: /ApiDocs/ }), - ).resolves.toHaveAttribute( - 'href', - '/catalog/default/component/artist-lookup/apidocs', - ); + ).resolves.toHaveAttribute('href', `${entityPath}/apidocs`); }); it('Should render a single-content groups as a normal tab', async () => { @@ -346,7 +330,7 @@ describe('Entity page', () => { await renderInTestApp(tester.reactElement(), { apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], mountPath: '/catalog/:namespace/:kind/:name', - initialRouteEntries: ['/catalog/default/component/artist-lookup'], + initialRouteEntries: [entityPath], config: { app: { title: 'Custom app', @@ -379,7 +363,7 @@ describe('Entity page', () => { await renderInTestApp(tester.reactElement(), { apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], mountPath: '/catalog/:namespace/:kind/:name', - initialRouteEntries: ['/catalog/default/component/artist-lookup'], + initialRouteEntries: [entityPath], config: { app: { title: 'Custom app', @@ -424,7 +408,7 @@ describe('Entity page', () => { await renderInTestApp(tester.reactElement(), { apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], mountPath: '/catalog/:namespace/:kind/:name', - initialRouteEntries: ['/catalog/default/component/artist-lookup'], + initialRouteEntries: [entityPath], config: { app: { title: 'Custom app', @@ -442,17 +426,11 @@ describe('Entity page', () => { await expect( screen.findByRole('button', { name: /TechDocs/ }), - ).resolves.toHaveAttribute( - 'href', - '/catalog/default/component/artist-lookup/techdocs', - ); + ).resolves.toHaveAttribute('href', `${entityPath}/techdocs`); await expect( screen.findByRole('button', { name: /ApiDocs/ }), - ).resolves.toHaveAttribute( - 'href', - '/catalog/default/component/artist-lookup/apidocs', - ); + ).resolves.toHaveAttribute('href', `${entityPath}/apidocs`); }); it('Should sort content by title by default', async () => { @@ -465,7 +443,7 @@ describe('Entity page', () => { await renderInTestApp(tester.reactElement(), { apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], mountPath: '/catalog/:namespace/:kind/:name', - initialRouteEntries: ['/catalog/default/component/artist-lookup'], + initialRouteEntries: [entityPath], config: { app: { title: 'Custom app', @@ -505,7 +483,7 @@ describe('Entity page', () => { await renderInTestApp(tester.reactElement(), { apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], mountPath: '/catalog/:namespace/:kind/:name', - initialRouteEntries: ['/catalog/default/component/artist-lookup'], + initialRouteEntries: [entityPath], config: { app: { title: 'Custom app', @@ -553,7 +531,7 @@ describe('Entity page', () => { await renderInTestApp(tester.reactElement(), { apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], mountPath: '/catalog/:namespace/:kind/:name', - initialRouteEntries: ['/catalog/default/component/artist-lookup'], + initialRouteEntries: [entityPath], config: { app: { title: 'Custom app', @@ -601,7 +579,7 @@ describe('Entity page', () => { await renderInTestApp(tester.reactElement(), { apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], mountPath: '/catalog/:namespace/:kind/:name', - initialRouteEntries: ['/catalog/default/component/artist-lookup'], + initialRouteEntries: [entityPath], config: { app: { title: 'Custom app', @@ -637,7 +615,7 @@ describe('Entity page', () => { await renderInTestApp(tester.reactElement(), { apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], mountPath: '/catalog/:namespace/:kind/:name', - initialRouteEntries: ['/catalog/default/component/artist-lookup'], + initialRouteEntries: [entityPath], config: { app: { title: 'Custom app', @@ -675,7 +653,7 @@ describe('Entity page', () => { await renderInTestApp(tester.reactElement(), { apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], mountPath: '/catalog/:namespace/:kind/:name', - initialRouteEntries: ['/catalog/default/component/artist-lookup'], + initialRouteEntries: [entityPath], config: { app: { title: 'Custom app', @@ -733,7 +711,7 @@ describe('Entity page', () => { await renderInTestApp(tester.reactElement(), { apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], mountPath: '/catalog/:namespace/:kind/:name', - initialRouteEntries: ['/catalog/default/component/artist-lookup'], + initialRouteEntries: [entityPath], config: { app: { title: 'Custom app', @@ -787,7 +765,7 @@ describe('Entity page', () => { await renderInTestApp(tester.reactElement(), { apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]], mountPath: '/catalog/:namespace/:kind/:name', - initialRouteEntries: ['/catalog/default/component/artist-lookup'], + initialRouteEntries: [entityPath], config: { app: { title: 'Custom app', @@ -877,7 +855,7 @@ describe('Entity page', () => { await renderInTestApp(tester.reactElement(), { mountPath: '/catalog/:namespace/:kind/:name', - initialRouteEntries: ['/catalog/default/component/artist-lookup'], + initialRouteEntries: [entityPath], config: { app: { title: 'Custom app', From 7efdc27464c8ff45c285c6fe06f37739e6661cd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 25 May 2026 15:18:11 +0200 Subject: [PATCH 11/12] =?UTF-8?q?Remove=20mountPath=20=E2=86=92=20initialR?= =?UTF-8?q?outeEntries=20defaulting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Don't auto-default initialRouteEntries from mountPath — for parameterized paths this silently produces wrong params (literal ':name' values). Require callers to set both explicitly. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Fredrik Adelöw --- .../src/app/renderInTestApp.tsx | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/packages/frontend-test-utils/src/app/renderInTestApp.tsx b/packages/frontend-test-utils/src/app/renderInTestApp.tsx index 82b9619566..ada78a35a3 100644 --- a/packages/frontend-test-utils/src/app/renderInTestApp.tsx +++ b/packages/frontend-test-utils/src/app/renderInTestApp.tsx @@ -82,12 +82,10 @@ export type TestAppOptions = { /** * The route path pattern that the test element is rendered at. When set, * the element is wrapped in a `` with this path, enabling - * `useParams()` to extract parameters from the URL. Defaults to `'/'`. + * `useParams()` to extract parameters from the URL. * - * 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. + * Should be used together with `initialRouteEntries` to set a concrete + * URL that matches the pattern. * * @example * ```ts @@ -100,8 +98,7 @@ export type TestAppOptions = { mountPath?: string; /** - * Initial route entries to use for the router. When `mountPath` is set - * and this is not, defaults to `[mountPath]`. + * Initial route entries to use for the router. */ initialRouteEntries?: string[]; @@ -214,10 +211,7 @@ export function renderInTestApp( params: { component: ({ children }) => ( Date: Mon, 25 May 2026 15:24:02 +0200 Subject: [PATCH 12/12] Update changeset and PR description MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Fredrik Adelöw --- .changeset/fix-test-route-matching.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fix-test-route-matching.md b/.changeset/fix-test-route-matching.md index 359fe19164..c8adb6f8d5 100644 --- a/.changeset/fix-test-route-matching.md +++ b/.changeset/fix-test-route-matching.md @@ -2,4 +2,4 @@ '@backstage/frontend-test-utils': patch --- -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 `` 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`. +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 `` with the given path, enabling `useParams()` to extract route parameters from the URL. Use together with `initialRouteEntries` to set a concrete URL that matches the pattern. This is useful for testing page components that depend on URL parameters, such as entity pages that use `useRouteRefParams`.