Merge pull request #34317 from backstage/freben/fix-entity-page-test-flake

fix(catalog): fix flaky entity page test
This commit is contained in:
Fredrik Adelöw
2026-05-25 16:00:49 +02:00
committed by GitHub
4 changed files with 104 additions and 78 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@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 `<Route>` 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`.
@@ -478,6 +478,7 @@ export type TestAppOptions<TApiPairs extends any[] = any[]> = {
};
config?: JsonObject;
features?: FrontendFeature[];
mountPath?: string;
initialRouteEntries?: string[];
apis?: readonly [...TestApiPairs<TApiPairs>];
};
@@ -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';
@@ -79,6 +79,24 @@ export type TestAppOptions<TApiPairs extends any[] = any[]> = {
*/
features?: FrontendFeature[];
/**
* 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.
*
* Should be used together with `initialRouteEntries` to set 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.
*/
@@ -125,12 +143,28 @@ export function renderInTestApp<const TApiPairs extends any[] = any[]>(
element: JSX.Element,
options?: TestAppOptions<TApiPairs>,
): RenderResult {
const mountPath = options?.mountPath;
const extensions: Array<ExtensionDefinition> = [
createExtension({
attachTo: { id: 'app/root', input: 'children' },
output: [coreExtensionData.reactElement],
factory: () => {
return [coreExtensionData.reactElement(element)];
let content: JSX.Element = element;
if (mountPath) {
const routePath =
mountPath === '/' || mountPath.endsWith('/*')
? mountPath
: `${mountPath.replace(/\/$/, '')}/*`;
content = (
<Routes>
<Route path={routePath} element={content} />
</Routes>
);
}
return [coreExtensionData.reactElement(content)];
},
}),
];
+62 -76
View File
@@ -28,7 +28,6 @@ import {
} from '@backstage/plugin-catalog-react/alpha';
import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
import {
catalogApiRef,
entityRouteRef,
MockStarredEntitiesApi,
starredEntitiesApiRef,
@@ -40,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,9 +107,9 @@ describe('Entity page', () => {
],
};
const mockCatalogApi = catalogApiMock.mock({
getEntityByRef: async () => entityMock,
});
const entityPath = '/catalog/default/component/artist-lookup';
const mockCatalogApi = catalogApiMock({ entities: [entityMock] });
const mockStarredEntitiesApi = new MockStarredEntitiesApi();
@@ -152,10 +151,9 @@ describe('Entity page', () => {
.add(apidocsEntityContent);
await renderInTestApp(tester.reactElement(), {
apis: [
[catalogApiRef, mockCatalogApi],
[starredEntitiesApiRef, mockStarredEntitiesApi],
],
apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]],
mountPath: '/catalog/:namespace/:kind/:name',
initialRouteEntries: [entityPath],
config: {
app: {
title: 'Custom app',
@@ -175,11 +173,11 @@ describe('Entity page', () => {
await expect(
screen.findByRole('button', { name: /TechDocs/ }),
).resolves.toHaveAttribute('href', '/techdocs');
).resolves.toHaveAttribute('href', `${entityPath}/techdocs`);
await expect(
screen.findByRole('button', { name: /ApiDocs/ }),
).resolves.toHaveAttribute('href', '/apidocs');
).resolves.toHaveAttribute('href', `${entityPath}/apidocs`);
});
it('Should rename a default group', async () => {
@@ -199,10 +197,9 @@ describe('Entity page', () => {
.add(apidocsEntityContent);
await renderInTestApp(tester.reactElement(), {
apis: [
[catalogApiRef, mockCatalogApi],
[starredEntitiesApiRef, mockStarredEntitiesApi],
],
apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]],
mountPath: '/catalog/:namespace/:kind/:name',
initialRouteEntries: [entityPath],
config: {
app: {
title: 'Custom app',
@@ -220,11 +217,11 @@ describe('Entity page', () => {
await expect(
screen.findByRole('button', { name: /TechDocs/ }),
).resolves.toHaveAttribute('href', '/techdocs');
).resolves.toHaveAttribute('href', `${entityPath}/techdocs`);
await expect(
screen.findByRole('button', { name: /ApiDocs/ }),
).resolves.toHaveAttribute('href', '/apidocs');
).resolves.toHaveAttribute('href', `${entityPath}/apidocs`);
});
it('Should disassociate a content with a default group', async () => {
@@ -239,10 +236,9 @@ describe('Entity page', () => {
});
await renderInTestApp(tester.reactElement(), {
apis: [
[catalogApiRef, mockCatalogApi],
[starredEntitiesApiRef, mockStarredEntitiesApi],
],
apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]],
mountPath: '/catalog/:namespace/:kind/:name',
initialRouteEntries: [entityPath],
config: {
app: {
title: 'Custom app',
@@ -292,10 +288,9 @@ describe('Entity page', () => {
});
await renderInTestApp(tester.reactElement(), {
apis: [
[catalogApiRef, mockCatalogApi],
[starredEntitiesApiRef, mockStarredEntitiesApi],
],
apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]],
mountPath: '/catalog/:namespace/:kind/:name',
initialRouteEntries: [entityPath],
config: {
app: {
title: 'Custom app',
@@ -313,11 +308,11 @@ describe('Entity page', () => {
await expect(
screen.findByRole('button', { name: /TechDocs/ }),
).resolves.toHaveAttribute('href', '/techdocs');
).resolves.toHaveAttribute('href', `${entityPath}/techdocs`);
await expect(
screen.findByRole('button', { name: /ApiDocs/ }),
).resolves.toHaveAttribute('href', '/apidocs');
).resolves.toHaveAttribute('href', `${entityPath}/apidocs`);
});
it('Should render a single-content groups as a normal tab', async () => {
@@ -333,10 +328,9 @@ describe('Entity page', () => {
});
await renderInTestApp(tester.reactElement(), {
apis: [
[catalogApiRef, mockCatalogApi],
[starredEntitiesApiRef, mockStarredEntitiesApi],
],
apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]],
mountPath: '/catalog/:namespace/:kind/:name',
initialRouteEntries: [entityPath],
config: {
app: {
title: 'Custom app',
@@ -367,10 +361,9 @@ describe('Entity page', () => {
.add(overviewEntityContent);
await renderInTestApp(tester.reactElement(), {
apis: [
[catalogApiRef, mockCatalogApi],
[starredEntitiesApiRef, mockStarredEntitiesApi],
],
apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]],
mountPath: '/catalog/:namespace/:kind/:name',
initialRouteEntries: [entityPath],
config: {
app: {
title: 'Custom app',
@@ -413,10 +406,9 @@ describe('Entity page', () => {
.add(apidocsEntityContent);
await renderInTestApp(tester.reactElement(), {
apis: [
[catalogApiRef, mockCatalogApi],
[starredEntitiesApiRef, mockStarredEntitiesApi],
],
apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]],
mountPath: '/catalog/:namespace/:kind/:name',
initialRouteEntries: [entityPath],
config: {
app: {
title: 'Custom app',
@@ -434,11 +426,11 @@ describe('Entity page', () => {
await expect(
screen.findByRole('button', { name: /TechDocs/ }),
).resolves.toHaveAttribute('href', '/techdocs');
).resolves.toHaveAttribute('href', `${entityPath}/techdocs`);
await expect(
screen.findByRole('button', { name: /ApiDocs/ }),
).resolves.toHaveAttribute('href', '/apidocs');
).resolves.toHaveAttribute('href', `${entityPath}/apidocs`);
});
it('Should sort content by title by default', async () => {
@@ -449,10 +441,9 @@ describe('Entity page', () => {
.add(apidocsEntityContent);
await renderInTestApp(tester.reactElement(), {
apis: [
[catalogApiRef, mockCatalogApi],
[starredEntitiesApiRef, mockStarredEntitiesApi],
],
apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]],
mountPath: '/catalog/:namespace/:kind/:name',
initialRouteEntries: [entityPath],
config: {
app: {
title: 'Custom app',
@@ -490,10 +481,9 @@ describe('Entity page', () => {
.add(apidocsEntityContent);
await renderInTestApp(tester.reactElement(), {
apis: [
[catalogApiRef, mockCatalogApi],
[starredEntitiesApiRef, mockStarredEntitiesApi],
],
apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]],
mountPath: '/catalog/:namespace/:kind/:name',
initialRouteEntries: [entityPath],
config: {
app: {
title: 'Custom app',
@@ -539,10 +529,9 @@ describe('Entity page', () => {
.add(apidocsEntityContent);
await renderInTestApp(tester.reactElement(), {
apis: [
[catalogApiRef, mockCatalogApi],
[starredEntitiesApiRef, mockStarredEntitiesApi],
],
apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]],
mountPath: '/catalog/:namespace/:kind/:name',
initialRouteEntries: [entityPath],
config: {
app: {
title: 'Custom app',
@@ -588,10 +577,9 @@ describe('Entity page', () => {
});
await renderInTestApp(tester.reactElement(), {
apis: [
[catalogApiRef, mockCatalogApi],
[starredEntitiesApiRef, mockStarredEntitiesApi],
],
apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]],
mountPath: '/catalog/:namespace/:kind/:name',
initialRouteEntries: [entityPath],
config: {
app: {
title: 'Custom app',
@@ -625,10 +613,9 @@ describe('Entity page', () => {
);
await renderInTestApp(tester.reactElement(), {
apis: [
[catalogApiRef, mockCatalogApi],
[starredEntitiesApiRef, mockStarredEntitiesApi],
],
apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]],
mountPath: '/catalog/:namespace/:kind/:name',
initialRouteEntries: [entityPath],
config: {
app: {
title: 'Custom app',
@@ -664,10 +651,9 @@ describe('Entity page', () => {
).add(customEntityHeader);
await renderInTestApp(tester.reactElement(), {
apis: [
[catalogApiRef, mockCatalogApi],
[starredEntitiesApiRef, mockStarredEntitiesApi],
],
apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]],
mountPath: '/catalog/:namespace/:kind/:name',
initialRouteEntries: [entityPath],
config: {
app: {
title: 'Custom app',
@@ -723,10 +709,9 @@ describe('Entity page', () => {
).add(menuItem);
await renderInTestApp(tester.reactElement(), {
apis: [
[catalogApiRef, mockCatalogApi],
[starredEntitiesApiRef, mockStarredEntitiesApi],
],
apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]],
mountPath: '/catalog/:namespace/:kind/:name',
initialRouteEntries: [entityPath],
config: {
app: {
title: 'Custom app',
@@ -778,10 +763,9 @@ describe('Entity page', () => {
).add(menuItem);
await renderInTestApp(tester.reactElement(), {
apis: [
[catalogApiRef, mockCatalogApi],
[starredEntitiesApiRef, mockStarredEntitiesApi],
],
apis: [mockCatalogApi, [starredEntitiesApiRef, mockStarredEntitiesApi]],
mountPath: '/catalog/:namespace/:kind/:name',
initialRouteEntries: [entityPath],
config: {
app: {
title: 'Custom app',
@@ -801,14 +785,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'));
@@ -870,6 +854,8 @@ describe('Entity page', () => {
.add(filteredMenuItem);
await renderInTestApp(tester.reactElement(), {
mountPath: '/catalog/:namespace/:kind/:name',
initialRouteEntries: [entityPath],
config: {
app: {
title: 'Custom app',
@@ -882,7 +868,7 @@ describe('Entity page', () => {
convertLegacyRouteRef(entityRouteRef),
},
apis: [
[catalogApiRef, mockCatalogApi],
mockCatalogApi,
[starredEntitiesApiRef, mockStarredEntitiesApi],
],
});