From d95d88cd23118f414017e4fb971b5d4e021bcdb8 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Fri, 3 Oct 2025 15:36:57 +0200 Subject: [PATCH 01/15] app: fix routing when PageExtensions are mounted on / Signed-off-by: Vincenzo Scamporlino --- plugins/app/src/extensions/AppRoutes.tsx | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/plugins/app/src/extensions/AppRoutes.tsx b/plugins/app/src/extensions/AppRoutes.tsx index 5cc3c3f4b8..ddd7962004 100644 --- a/plugins/app/src/extensions/AppRoutes.tsx +++ b/plugins/app/src/extensions/AppRoutes.tsx @@ -36,12 +36,18 @@ export const AppRoutes = createExtension({ factory({ inputs }) { const Routes = () => { const element = useRoutes([ - ...inputs.routes.map(route => ({ - path: `${route - .get(coreExtensionData.routePath) - .replace(/\/$/, '')}/*`, - element: route.get(coreExtensionData.reactElement), - })), + ...inputs.routes.map(route => { + const routePath = route.get(coreExtensionData.routePath); + + return { + path: + routePath === '/' + ? routePath + : `${routePath.replace(/\/$/, '')}/*`, + + element: route.get(coreExtensionData.reactElement), + }; + }), { path: '*', element: , From a26508eea4dd247df8d5bc3a536b41bc4e71d59c Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Tue, 7 Oct 2025 11:43:03 +0200 Subject: [PATCH 02/15] fix renderInTestApp routing Signed-off-by: Vincenzo Scamporlino --- .../src/app/renderInTestApp.tsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/frontend-test-utils/src/app/renderInTestApp.tsx b/packages/frontend-test-utils/src/app/renderInTestApp.tsx index 80818509da..e1b35eba6c 100644 --- a/packages/frontend-test-utils/src/app/renderInTestApp.tsx +++ b/packages/frontend-test-utils/src/app/renderInTestApp.tsx @@ -107,6 +107,12 @@ const appPluginOverride = appPlugin.withOverrides({ appPlugin.getExtension('sign-in-page:app').override({ disabled: true, }), + appPlugin.getExtension('app/layout').override({ + disabled: true, + }), + appPlugin.getExtension('app/routes').override({ + disabled: true, + }), appPlugin.getExtension('app/nav').override({ output: [coreExtensionData.reactElement], factory(_originalFactory, { inputs }) { @@ -147,13 +153,10 @@ export function renderInTestApp( ): RenderResult { const extensions: Array = [ createExtension({ - attachTo: { id: 'app/routes', input: 'routes' }, - output: [coreExtensionData.reactElement, coreExtensionData.routePath], + attachTo: { id: 'app/root', input: 'children' }, + output: [coreExtensionData.reactElement], factory: () => { - return [ - coreExtensionData.reactElement(element), - coreExtensionData.routePath('/'), - ]; + return [coreExtensionData.reactElement(element)]; }, }), RouterBlueprint.make({ From 23c72e7e6537dfc972a5b16486cf8d517ddd32bb Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Tue, 7 Oct 2025 12:40:12 +0200 Subject: [PATCH 03/15] frontend-test-utils: render the element as Page Signed-off-by: Vincenzo Scamporlino --- .../src/app/renderInTestApp.test.tsx | 35 +++++++++++-------- .../src/app/renderInTestApp.tsx | 21 +++++------ 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/packages/frontend-test-utils/src/app/renderInTestApp.test.tsx b/packages/frontend-test-utils/src/app/renderInTestApp.test.tsx index 8e1b1e1ea0..ae72e6a622 100644 --- a/packages/frontend-test-utils/src/app/renderInTestApp.test.tsx +++ b/packages/frontend-test-utils/src/app/renderInTestApp.test.tsx @@ -20,15 +20,18 @@ import { MockAnalyticsApi, TestApiProvider, } from '@backstage/frontend-test-utils'; -import { analyticsApiRef, useAnalytics } from '@backstage/frontend-plugin-api'; -import { Routes, Route } from 'react-router-dom'; +import { + analyticsApiRef, + PageBlueprint, + useAnalytics, +} from '@backstage/frontend-plugin-api'; import { renderInTestApp } from './renderInTestApp'; describe('renderInTestApp', () => { it('should render the given component in a page', async () => { const IndexPage = () =>
Index Page
; renderInTestApp(); - expect(screen.getByText('Index Page')).toBeInTheDocument(); + expect(await screen.findByText('Index Page')).toBeInTheDocument(); }); it('should works with apis provider', async () => { @@ -55,7 +58,7 @@ describe('renderInTestApp', () => { , ); - fireEvent.click(screen.getByRole('link', { name: 'See details' })); + fireEvent.click(await screen.findByRole('link', { name: 'See details' })); expect(analyticsApiMock.getEvents()).toEqual( expect.arrayContaining([ @@ -68,16 +71,20 @@ describe('renderInTestApp', () => { }); it('should support setting different locations in the history stack', async () => { - renderInTestApp( - - Index Page} /> - Second Page} /> - , - { - initialRouteEntries: ['/second-page'], - }, - ); + renderInTestApp(

Index page

, { + extensions: [ + PageBlueprint.make({ + name: 'second-page', + params: defineParams => + defineParams({ + path: '/second-page', + loader: async () =>

Second Page

, + }), + }), + ], + initialRouteEntries: ['/second-page'], + }); - expect(screen.getByText('Second Page')).toBeInTheDocument(); + expect(await screen.findByText('Second Page')).toBeInTheDocument(); }); }); diff --git a/packages/frontend-test-utils/src/app/renderInTestApp.tsx b/packages/frontend-test-utils/src/app/renderInTestApp.tsx index e1b35eba6c..8f52f349fe 100644 --- a/packages/frontend-test-utils/src/app/renderInTestApp.tsx +++ b/packages/frontend-test-utils/src/app/renderInTestApp.tsx @@ -31,6 +31,7 @@ import { NavItemBlueprint, createFrontendPlugin, FrontendFeature, + PageBlueprint, } from '@backstage/frontend-plugin-api'; import appPlugin from '@backstage/plugin-app'; @@ -107,12 +108,6 @@ const appPluginOverride = appPlugin.withOverrides({ appPlugin.getExtension('sign-in-page:app').override({ disabled: true, }), - appPlugin.getExtension('app/layout').override({ - disabled: true, - }), - appPlugin.getExtension('app/routes').override({ - disabled: true, - }), appPlugin.getExtension('app/nav').override({ output: [coreExtensionData.reactElement], factory(_originalFactory, { inputs }) { @@ -152,13 +147,6 @@ export function renderInTestApp( options?: TestAppOptions, ): RenderResult { const extensions: Array = [ - createExtension({ - attachTo: { id: 'app/root', input: 'children' }, - output: [coreExtensionData.reactElement], - factory: () => { - return [coreExtensionData.reactElement(element)]; - }, - }), RouterBlueprint.make({ params: { component: ({ children }) => ( @@ -197,6 +185,13 @@ export function renderInTestApp( extensions.push(...options.extensions); } + extensions.push( + PageBlueprint.make({ + name: 'mocked-root-page', + params: define => define({ path: '/', loader: async () => element }), + }), + ); + const features: FrontendFeature[] = [ createFrontendPlugin({ pluginId: 'test', From 34680e981c623fcd6db5bfcebc355efef547717b Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Tue, 7 Oct 2025 14:09:16 +0200 Subject: [PATCH 04/15] frontend-test-utils: tweak test Signed-off-by: Vincenzo Scamporlino --- .../src/app/renderInTestApp.test.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/frontend-test-utils/src/app/renderInTestApp.test.tsx b/packages/frontend-test-utils/src/app/renderInTestApp.test.tsx index ae72e6a622..4da1c4632c 100644 --- a/packages/frontend-test-utils/src/app/renderInTestApp.test.tsx +++ b/packages/frontend-test-utils/src/app/renderInTestApp.test.tsx @@ -26,6 +26,7 @@ import { useAnalytics, } from '@backstage/frontend-plugin-api'; import { renderInTestApp } from './renderInTestApp'; +import { Route, Routes } from 'react-router-dom'; describe('renderInTestApp', () => { it('should render the given component in a page', async () => { @@ -78,13 +79,18 @@ describe('renderInTestApp', () => { params: defineParams => defineParams({ path: '/second-page', - loader: async () =>

Second Page

, + loader: async () => ( + + Second page} /> + Subpage} /> + + ), }), }), ], - initialRouteEntries: ['/second-page'], + initialRouteEntries: ['/second-page/subpage'], }); - expect(await screen.findByText('Second Page')).toBeInTheDocument(); + expect(await screen.findByText('Subpage')).toBeInTheDocument(); }); }); From ae1dad0e734bb550803b42585409cb8164b68600 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Tue, 7 Oct 2025 14:32:53 +0200 Subject: [PATCH 05/15] changeset notfound page Signed-off-by: Vincenzo Scamporlino --- .changeset/fluffy-cities-sip.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/fluffy-cities-sip.md diff --git a/.changeset/fluffy-cities-sip.md b/.changeset/fluffy-cities-sip.md new file mode 100644 index 0000000000..f6692d4e27 --- /dev/null +++ b/.changeset/fluffy-cities-sip.md @@ -0,0 +1,6 @@ +--- +'@backstage/frontend-test-utils': patch +'@backstage/plugin-app': patch +--- + +Fixed an issue that caused the `NotFound` page to not render correctly when a Page was mounted at `/`. From 1013eb72cb06d431ee3a8f02fa6b95f538730ea1 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Tue, 7 Oct 2025 17:04:39 +0200 Subject: [PATCH 06/15] Revert "frontend-test-utils: render the element as Page" This reverts commit 23c72e7e6537dfc972a5b16486cf8d517ddd32bb. Signed-off-by: Vincenzo Scamporlino --- .../src/app/renderInTestApp.test.tsx | 41 +++++++------------ .../src/app/renderInTestApp.tsx | 21 ++++++---- 2 files changed, 27 insertions(+), 35 deletions(-) diff --git a/packages/frontend-test-utils/src/app/renderInTestApp.test.tsx b/packages/frontend-test-utils/src/app/renderInTestApp.test.tsx index 4da1c4632c..8e1b1e1ea0 100644 --- a/packages/frontend-test-utils/src/app/renderInTestApp.test.tsx +++ b/packages/frontend-test-utils/src/app/renderInTestApp.test.tsx @@ -20,19 +20,15 @@ import { MockAnalyticsApi, TestApiProvider, } from '@backstage/frontend-test-utils'; -import { - analyticsApiRef, - PageBlueprint, - useAnalytics, -} from '@backstage/frontend-plugin-api'; +import { analyticsApiRef, useAnalytics } from '@backstage/frontend-plugin-api'; +import { Routes, Route } from 'react-router-dom'; import { renderInTestApp } from './renderInTestApp'; -import { Route, Routes } from 'react-router-dom'; describe('renderInTestApp', () => { it('should render the given component in a page', async () => { const IndexPage = () =>
Index Page
; renderInTestApp(); - expect(await screen.findByText('Index Page')).toBeInTheDocument(); + expect(screen.getByText('Index Page')).toBeInTheDocument(); }); it('should works with apis provider', async () => { @@ -59,7 +55,7 @@ describe('renderInTestApp', () => { , ); - fireEvent.click(await screen.findByRole('link', { name: 'See details' })); + fireEvent.click(screen.getByRole('link', { name: 'See details' })); expect(analyticsApiMock.getEvents()).toEqual( expect.arrayContaining([ @@ -72,25 +68,16 @@ describe('renderInTestApp', () => { }); it('should support setting different locations in the history stack', async () => { - renderInTestApp(

Index page

, { - extensions: [ - PageBlueprint.make({ - name: 'second-page', - params: defineParams => - defineParams({ - path: '/second-page', - loader: async () => ( - - Second page} /> - Subpage} /> - - ), - }), - }), - ], - initialRouteEntries: ['/second-page/subpage'], - }); + renderInTestApp( + + Index Page} /> + Second Page} /> + , + { + initialRouteEntries: ['/second-page'], + }, + ); - expect(await screen.findByText('Subpage')).toBeInTheDocument(); + expect(screen.getByText('Second Page')).toBeInTheDocument(); }); }); diff --git a/packages/frontend-test-utils/src/app/renderInTestApp.tsx b/packages/frontend-test-utils/src/app/renderInTestApp.tsx index 8f52f349fe..e1b35eba6c 100644 --- a/packages/frontend-test-utils/src/app/renderInTestApp.tsx +++ b/packages/frontend-test-utils/src/app/renderInTestApp.tsx @@ -31,7 +31,6 @@ import { NavItemBlueprint, createFrontendPlugin, FrontendFeature, - PageBlueprint, } from '@backstage/frontend-plugin-api'; import appPlugin from '@backstage/plugin-app'; @@ -108,6 +107,12 @@ const appPluginOverride = appPlugin.withOverrides({ appPlugin.getExtension('sign-in-page:app').override({ disabled: true, }), + appPlugin.getExtension('app/layout').override({ + disabled: true, + }), + appPlugin.getExtension('app/routes').override({ + disabled: true, + }), appPlugin.getExtension('app/nav').override({ output: [coreExtensionData.reactElement], factory(_originalFactory, { inputs }) { @@ -147,6 +152,13 @@ export function renderInTestApp( options?: TestAppOptions, ): RenderResult { const extensions: Array = [ + createExtension({ + attachTo: { id: 'app/root', input: 'children' }, + output: [coreExtensionData.reactElement], + factory: () => { + return [coreExtensionData.reactElement(element)]; + }, + }), RouterBlueprint.make({ params: { component: ({ children }) => ( @@ -185,13 +197,6 @@ export function renderInTestApp( extensions.push(...options.extensions); } - extensions.push( - PageBlueprint.make({ - name: 'mocked-root-page', - params: define => define({ path: '/', loader: async () => element }), - }), - ); - const features: FrontendFeature[] = [ createFrontendPlugin({ pluginId: 'test', From 898859f41b40615691642802593a7b816262c077 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Tue, 7 Oct 2025 20:56:58 +0200 Subject: [PATCH 07/15] frontend-plugin-api: migrate to renderTestApp Signed-off-by: Vincenzo Scamporlino --- .../AppRootWrapperBlueprint.test.tsx | 16 ++++---- .../SwappableComponentBlueprint.test.tsx | 38 +++++++------------ 2 files changed, 23 insertions(+), 31 deletions(-) diff --git a/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.test.tsx index b4ad8ed2d2..0f26af3243 100644 --- a/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.test.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - +import { Fragment } from 'react'; import { AppRootWrapperBlueprint } from './AppRootWrapperBlueprint'; import { screen, waitFor } from '@testing-library/react'; import { @@ -21,7 +21,7 @@ import { createExtension, createExtensionInput, } from '../wiring'; -import { renderInTestApp } from '@backstage/frontend-test-utils'; +import { renderTestApp } from '@backstage/frontend-test-utils'; describe('AppRootWrapperBlueprint', () => { it('should return an extension with sensible defaults', () => { @@ -63,7 +63,7 @@ describe('AppRootWrapperBlueprint', () => { }, }); - renderInTestApp(
, { extensions: [extension] }); + renderTestApp({ extensions: [extension] }); await waitFor(() => expect(screen.getByText('Hello')).toBeInTheDocument()); }); @@ -83,16 +83,18 @@ describe('AppRootWrapperBlueprint', () => { component: ({ children }) => (
{children} - {inputs.children.flatMap(c => - c.get(coreExtensionData.reactElement), - )} + {inputs.children.flatMap((c, index) => ( + + {c.get(coreExtensionData.reactElement)} + + ))}
), }); }, }); - renderInTestApp(
, { + renderTestApp({ extensions: [ extension, createExtension({ diff --git a/packages/frontend-plugin-api/src/blueprints/SwappableComponentBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/SwappableComponentBlueprint.test.tsx index fae4bf7ebf..f51750b1e7 100644 --- a/packages/frontend-plugin-api/src/blueprints/SwappableComponentBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/blueprints/SwappableComponentBlueprint.test.tsx @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { renderInTestApp } from '@backstage/frontend-test-utils'; +import { renderTestApp } from '@backstage/frontend-test-utils'; import { createSwappableComponent } from '../components'; import { SwappableComponentBlueprint } from './SwappableComponentBlueprint'; import { PageBlueprint } from './PageBlueprint'; -import { waitFor, screen } from '@testing-library/react'; +import { screen } from '@testing-library/react'; describe('SwappableComponentBlueprint', () => { it('should allow defining a component override for a component ref', () => { @@ -48,21 +48,19 @@ describe('SwappableComponentBlueprint', () => { loader: () => (props: { hello: string }) =>
{props.hello}
, }); - renderInTestApp(
, { + renderTestApp({ extensions: [ PageBlueprint.make({ params: define => define({ - // todo(blam): there's a bug that this path cannot be `/`? - path: '/test', + path: '/', loader: async () => , }), }), ], - initialRouteEntries: ['/test'], }); - await waitFor(() => expect(screen.getByText('test!')).toBeInTheDocument()); + expect(await screen.findByText('test!')).toBeInTheDocument(); }); it('should render a component ref without a default implementation', async () => { @@ -70,22 +68,19 @@ describe('SwappableComponentBlueprint', () => { id: 'test.component', }); - renderInTestApp(
, { + renderTestApp({ extensions: [ PageBlueprint.make({ params: define => define({ - path: '/test', + path: '/', loader: async () => , }), }), ], - initialRouteEntries: ['/test'], }); - await waitFor(() => - expect(screen.getByTestId('test.component')).toBeInTheDocument(), - ); + expect(await screen.findByTestId('test.component')).toBeInTheDocument(); }); it('should render a component ref with an async loader implementation', async () => { @@ -95,21 +90,20 @@ describe('SwappableComponentBlueprint', () => {
{props.hello}
, }); - renderInTestApp(
, { + renderTestApp({ extensions: [ PageBlueprint.make({ params: define => define({ // todo(blam): there's a bug that this path cannot be `/`? - path: '/test', + path: '/', loader: async () => , }), }), ], - initialRouteEntries: ['/test'], }); - await waitFor(() => expect(screen.getByText('test!')).toBeInTheDocument()); + expect(await screen.findByText('test!')).toBeInTheDocument(); }); it('should render a component ref with an async loader implementation and prop transform', async () => { @@ -120,22 +114,18 @@ describe('SwappableComponentBlueprint', () => { transformProps: ({ hello }) => ({ hello: `tr ${hello}` }), }); - renderInTestApp(
, { + renderTestApp({ extensions: [ PageBlueprint.make({ params: define => define({ - // todo(blam): there's a bug that this path cannot be `/`? - path: '/test', + path: '/', loader: async () => , }), }), ], - initialRouteEntries: ['/test'], }); - await waitFor(() => - expect(screen.getByText('tr test!')).toBeInTheDocument(), - ); + expect(await screen.findByText('tr test!')).toBeInTheDocument(); }); }); From 4a494abf78cec101e8bbabd04d05aa96b3a25a76 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Tue, 7 Oct 2025 20:57:39 +0200 Subject: [PATCH 08/15] frontend-test-utils: introduce renderTestApp and mark features option as deprecated Signed-off-by: Vincenzo Scamporlino --- packages/frontend-test-utils/src/app/index.ts | 1 + .../src/app/renderInTestApp.tsx | 2 + .../src/app/renderTestApp.tsx | 81 +++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 packages/frontend-test-utils/src/app/renderTestApp.tsx diff --git a/packages/frontend-test-utils/src/app/index.ts b/packages/frontend-test-utils/src/app/index.ts index 6c6ef26e4c..9db67dbb49 100644 --- a/packages/frontend-test-utils/src/app/index.ts +++ b/packages/frontend-test-utils/src/app/index.ts @@ -21,3 +21,4 @@ export { } from './createExtensionTester'; export { renderInTestApp, type TestAppOptions } from './renderInTestApp'; +export { renderTestApp, type RenderTestAppOptions } from './renderTestApp'; diff --git a/packages/frontend-test-utils/src/app/renderInTestApp.tsx b/packages/frontend-test-utils/src/app/renderInTestApp.tsx index e1b35eba6c..2b8efb643f 100644 --- a/packages/frontend-test-utils/src/app/renderInTestApp.tsx +++ b/packages/frontend-test-utils/src/app/renderInTestApp.tsx @@ -69,6 +69,8 @@ export type TestAppOptions = { /** * Additional extensions to add to the test app. + * + * @deprecated Use `renderTestApp` instead. */ extensions?: ExtensionDefinition[]; diff --git a/packages/frontend-test-utils/src/app/renderTestApp.tsx b/packages/frontend-test-utils/src/app/renderTestApp.tsx new file mode 100644 index 0000000000..1644ee1c30 --- /dev/null +++ b/packages/frontend-test-utils/src/app/renderTestApp.tsx @@ -0,0 +1,81 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createSpecializedApp } from '@backstage/frontend-app-api'; +import { + coreExtensionData, + createFrontendPlugin, + ExtensionDefinition, + FrontendFeature, +} from '@backstage/frontend-plugin-api'; +import { render } from '@testing-library/react'; +import appPlugin from '@backstage/plugin-app'; +import { JsonObject } from '@backstage/types'; +import { ConfigReader } from '@backstage/config'; + +export type RenderTestAppOptions = { + /** + * Additional configuration passed to the app when rendering elements inside it. + */ + config?: JsonObject; + /** + * Additional extensions to add to the test app. + */ + extensions: ExtensionDefinition[]; + + /** + * Additional features to add to the test app. + */ + features?: FrontendFeature[]; +}; + +const appPluginOverride = appPlugin.withOverrides({ + extensions: [ + appPlugin.getExtension('sign-in-page:app').override({ + disabled: true, + }), + ], +}); + +export function renderTestApp(options: RenderTestAppOptions) { + const features: FrontendFeature[] = [ + createFrontendPlugin({ + pluginId: 'test', + extensions: options.extensions, + }), + appPluginOverride, + ]; + + if (options.features) { + features.push(...options.features); + } + + const app = createSpecializedApp({ + features, + config: options.config + ? ConfigReader.fromConfigs([ + { + context: 'render-config', + data: options.config, + }, + ]) + : undefined, + }); + + return render( + app.tree.root.instance!.getData(coreExtensionData.reactElement), + ); +} From 7db384cef28fb61a113c7624de6c1c2800eff4d7 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Tue, 7 Oct 2025 20:58:42 +0200 Subject: [PATCH 09/15] app: migrate to renderTestApp Signed-off-by: Vincenzo Scamporlino --- .../DefaultSwappableComponentsApi.test.tsx | 81 ++++++++++++++++--- .../app/src/extensions/DialogDisplay.test.tsx | 4 +- 2 files changed, 70 insertions(+), 15 deletions(-) diff --git a/plugins/app/src/apis/SwappableComponentsApi/DefaultSwappableComponentsApi.test.tsx b/plugins/app/src/apis/SwappableComponentsApi/DefaultSwappableComponentsApi.test.tsx index 07ef891926..bdd81f338c 100644 --- a/plugins/app/src/apis/SwappableComponentsApi/DefaultSwappableComponentsApi.test.tsx +++ b/plugins/app/src/apis/SwappableComponentsApi/DefaultSwappableComponentsApi.test.tsx @@ -16,6 +16,7 @@ import { ApiBlueprint, + AppRootElementBlueprint, createExtensionInput, createFrontendModule, createSwappableComponent, @@ -24,7 +25,7 @@ import { } from '@backstage/frontend-plugin-api'; import { DefaultSwappableComponentsApi } from './DefaultSwappableComponentsApi'; import { render, screen } from '@testing-library/react'; -import { renderInTestApp } from '@backstage/frontend-test-utils'; +import { renderInTestApp, renderTestApp } from '@backstage/frontend-test-utils'; const { ref: testRefA } = createSwappableComponent({ id: 'test.a' }); const { ref: testRefB1 } = createSwappableComponent({ id: 'test.b' }); @@ -149,8 +150,17 @@ describe('DefaultSwappableComponentsApi', () => { id: 'test.mock', }); - renderInTestApp(, { - extensions: [api], + renderTestApp({ + extensions: [ + AppRootElementBlueprint.make({ + name: 'derp', + params: define => + define({ + element: , + }), + }), + api, + ], }); await expect( @@ -164,8 +174,17 @@ describe('DefaultSwappableComponentsApi', () => { loader: () => () =>
test.mock
, }); - renderInTestApp(, { - extensions: [api], + renderTestApp({ + extensions: [ + AppRootElementBlueprint.make({ + name: 'derp', + params: define => + define({ + element: , + }), + }), + api, + ], }); await expect( @@ -179,8 +198,17 @@ describe('DefaultSwappableComponentsApi', () => { loader: async () => () =>
test.mock
, }); - renderInTestApp(, { - extensions: [api], + renderTestApp({ + extensions: [ + AppRootElementBlueprint.make({ + name: 'derp', + params: define => + define({ + element: , + }), + }), + api, + ], }); await expect( @@ -202,8 +230,17 @@ describe('DefaultSwappableComponentsApi', () => { }), }); - renderInTestApp(, { - extensions: [api], + renderTestApp({ + extensions: [ + AppRootElementBlueprint.make({ + name: 'derp', + params: define => + define({ + element: , + }), + }), + api, + ], features: [ createFrontendModule({ pluginId: 'app', @@ -231,8 +268,17 @@ describe('DefaultSwappableComponentsApi', () => { }), }); - renderInTestApp(, { - extensions: [api], + renderTestApp({ + extensions: [ + AppRootElementBlueprint.make({ + name: 'derp', + params: define => + define({ + element: , + }), + }), + api, + ], features: [ createFrontendModule({ pluginId: 'app', @@ -264,8 +310,17 @@ describe('DefaultSwappableComponentsApi', () => { }), }); - renderInTestApp(, { - extensions: [api], + renderTestApp({ + extensions: [ + AppRootElementBlueprint.make({ + name: 'derp', + params: define => + define({ + element: , + }), + }), + api, + ], features: [ createFrontendModule({ pluginId: 'app', diff --git a/plugins/app/src/extensions/DialogDisplay.test.tsx b/plugins/app/src/extensions/DialogDisplay.test.tsx index 8f7eb4e857..dbad551fcb 100644 --- a/plugins/app/src/extensions/DialogDisplay.test.tsx +++ b/plugins/app/src/extensions/DialogDisplay.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { renderInTestApp } from '@backstage/frontend-test-utils'; +import { renderTestApp } from '@backstage/frontend-test-utils'; import { act, useEffect } from 'react'; import { AppRootElementBlueprint, @@ -29,7 +29,7 @@ async function withDialogApi( callback: (dialogApi: DialogApi) => Promise, ) { const deferred = createDeferred(); - await renderInTestApp(
, { + await renderTestApp({ extensions: [ AppRootElementBlueprint.makeWithOverrides({ name: 'derp', From c41dd803c6cfe646054ca9b23a60a4025a930f99 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Tue, 7 Oct 2025 21:04:00 +0200 Subject: [PATCH 10/15] frontend-test-utils: expand changeset Signed-off-by: Vincenzo Scamporlino --- .changeset/fluffy-cities-sip.md | 1 - .changeset/real-seals-dress.md | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 .changeset/real-seals-dress.md diff --git a/.changeset/fluffy-cities-sip.md b/.changeset/fluffy-cities-sip.md index f6692d4e27..1f82540e38 100644 --- a/.changeset/fluffy-cities-sip.md +++ b/.changeset/fluffy-cities-sip.md @@ -1,5 +1,4 @@ --- -'@backstage/frontend-test-utils': patch '@backstage/plugin-app': patch --- diff --git a/.changeset/real-seals-dress.md b/.changeset/real-seals-dress.md new file mode 100644 index 0000000000..7606db6638 --- /dev/null +++ b/.changeset/real-seals-dress.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-test-utils': patch +--- + +Add a new utility `renderTestApp` to `@backstage/frontend-test-utils` that simplifies rendering extensions within the Backstage application context for testing purposes. This utility replaces the use of `renderInTestApp` when the `extensions` option is needed. From 6c23efd539d6938b3c03b8c4f3d38d533e6c8d81 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Tue, 7 Oct 2025 21:44:36 +0200 Subject: [PATCH 11/15] frontend-test-utils: api-reports Signed-off-by: Vincenzo Scamporlino --- packages/frontend-test-utils/report.api.md | 13 +++++++++++++ .../frontend-test-utils/src/app/renderTestApp.tsx | 11 +++++++++++ 2 files changed, 24 insertions(+) diff --git a/packages/frontend-test-utils/report.api.md b/packages/frontend-test-utils/report.api.md index db31c082ea..58e5282f48 100644 --- a/packages/frontend-test-utils/report.api.md +++ b/packages/frontend-test-utils/report.api.md @@ -29,6 +29,7 @@ import { RouteRef } from '@backstage/frontend-plugin-api'; import { TestApiProvider } from '@backstage/test-utils'; import { TestApiProviderProps } from '@backstage/test-utils'; import { TestApiRegistry } from '@backstage/test-utils'; +import { testingLibraryDomTypesQueries } from '@testing-library/dom/types/queries'; import { withLogCollector } from '@backstage/test-utils'; export { ApiMock }; @@ -119,6 +120,18 @@ export function renderInTestApp( options?: TestAppOptions, ): RenderResult; +// @public +export function renderTestApp( + options: RenderTestAppOptions, +): RenderResult; + +// @public +export type RenderTestAppOptions = { + config?: JsonObject; + extensions: ExtensionDefinition[]; + features?: FrontendFeature[]; +}; + export { TestApiProvider }; export { TestApiProviderProps }; diff --git a/packages/frontend-test-utils/src/app/renderTestApp.tsx b/packages/frontend-test-utils/src/app/renderTestApp.tsx index 1644ee1c30..827d14283c 100644 --- a/packages/frontend-test-utils/src/app/renderTestApp.tsx +++ b/packages/frontend-test-utils/src/app/renderTestApp.tsx @@ -26,6 +26,11 @@ import appPlugin from '@backstage/plugin-app'; import { JsonObject } from '@backstage/types'; import { ConfigReader } from '@backstage/config'; +/** + * Options for `renderTestApp`. + * + * @public + */ export type RenderTestAppOptions = { /** * Additional configuration passed to the app when rendering elements inside it. @@ -50,6 +55,12 @@ const appPluginOverride = appPlugin.withOverrides({ ], }); +/** + * Renders the provided extensions inside a Backstage app, returning the same + * utilities as `@testing-library/react` `render` function. + * + * @public + */ export function renderTestApp(options: RenderTestAppOptions) { const features: FrontendFeature[] = [ createFrontendPlugin({ From 980bce1ff8fcc1434072a842b23723aa47403fa4 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Tue, 7 Oct 2025 22:36:16 +0200 Subject: [PATCH 12/15] core-compat-api: use renderTestApp Signed-off-by: Vincenzo Scamporlino --- .../src/collectLegacyRoutes.test.tsx | 4 ++-- .../core-compat-api/src/convertLegacyApp.test.tsx | 14 +++++++------- packages/frontend-test-utils/report.api.md | 2 +- .../frontend-test-utils/src/app/renderTestApp.tsx | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/core-compat-api/src/collectLegacyRoutes.test.tsx b/packages/core-compat-api/src/collectLegacyRoutes.test.tsx index fe4166016e..c5138dac3d 100644 --- a/packages/core-compat-api/src/collectLegacyRoutes.test.tsx +++ b/packages/core-compat-api/src/collectLegacyRoutes.test.tsx @@ -39,7 +39,7 @@ import { useApp, } from '@backstage/core-plugin-api'; import { screen } from '@testing-library/react'; -import { renderInTestApp } from '@backstage/frontend-test-utils'; +import { renderTestApp } from '@backstage/frontend-test-utils'; const exampleApiRef = createApiRef({ id: 'plugin.example.service', @@ -304,7 +304,7 @@ describe('collectLegacyRoutes', () => { , ); - renderInTestApp(
, { features }); + renderTestApp({ features }); await expect( screen.findByText('plugins: app, test'), diff --git a/packages/core-compat-api/src/convertLegacyApp.test.tsx b/packages/core-compat-api/src/convertLegacyApp.test.tsx index 7bbe13867c..1cb2b757e3 100644 --- a/packages/core-compat-api/src/convertLegacyApp.test.tsx +++ b/packages/core-compat-api/src/convertLegacyApp.test.tsx @@ -27,7 +27,7 @@ import { createRouteRef, } from '@backstage/core-plugin-api'; import { EntityLayout, EntitySwitch, isKind } from '@backstage/plugin-catalog'; -import { renderInTestApp } from '@backstage/frontend-test-utils'; +import { renderTestApp } from '@backstage/frontend-test-utils'; import { default as catalogPlugin } from '@backstage/plugin-catalog/alpha'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; @@ -263,7 +263,7 @@ describe('convertLegacyApp', () => { }); // Overview - const renderOverviewTest = await renderInTestApp(
, { + const renderOverviewTest = await renderTestApp({ features: [catalogOverride, ...converted], initialRouteEntries: ['/catalog/default/test/x'], }); @@ -272,7 +272,7 @@ describe('convertLegacyApp', () => { ).resolves.toBeInTheDocument(); renderOverviewTest.unmount(); - const renderOverviewOther = await renderInTestApp(
, { + const renderOverviewOther = await renderTestApp({ features: [catalogOverride, ...converted], initialRouteEntries: ['/catalog/default/other/x'], }); @@ -282,7 +282,7 @@ describe('convertLegacyApp', () => { renderOverviewOther.unmount(); // Foo tab - const renderFooTest = await renderInTestApp(
, { + const renderFooTest = await renderTestApp({ features: [catalogOverride, ...converted], initialRouteEntries: ['/catalog/default/test/x/foo'], }); @@ -291,7 +291,7 @@ describe('convertLegacyApp', () => { ).resolves.toBeInTheDocument(); renderFooTest.unmount(); - const renderFooOther = await renderInTestApp(
, { + const renderFooOther = await renderTestApp({ features: [catalogOverride, ...converted], initialRouteEntries: ['/catalog/default/other/x/foo'], }); @@ -301,7 +301,7 @@ describe('convertLegacyApp', () => { renderFooOther.unmount(); // Bar tab - const renderBarTest = await renderInTestApp(
, { + const renderBarTest = await renderTestApp({ features: [catalogOverride, ...converted], initialRouteEntries: ['/catalog/default/test/x/bar'], }); @@ -310,7 +310,7 @@ describe('convertLegacyApp', () => { ).resolves.toBeInTheDocument(); renderBarTest.unmount(); - const renderBarOther = await renderInTestApp(
, { + const renderBarOther = await renderTestApp({ features: [catalogOverride, ...converted], initialRouteEntries: ['/catalog/default/other/x/bar'], }); diff --git a/packages/frontend-test-utils/report.api.md b/packages/frontend-test-utils/report.api.md index 58e5282f48..bd008feb19 100644 --- a/packages/frontend-test-utils/report.api.md +++ b/packages/frontend-test-utils/report.api.md @@ -128,7 +128,7 @@ export function renderTestApp( // @public export type RenderTestAppOptions = { config?: JsonObject; - extensions: ExtensionDefinition[]; + extensions?: ExtensionDefinition[]; features?: FrontendFeature[]; }; diff --git a/packages/frontend-test-utils/src/app/renderTestApp.tsx b/packages/frontend-test-utils/src/app/renderTestApp.tsx index 827d14283c..f19dcd7d5f 100644 --- a/packages/frontend-test-utils/src/app/renderTestApp.tsx +++ b/packages/frontend-test-utils/src/app/renderTestApp.tsx @@ -39,7 +39,7 @@ export type RenderTestAppOptions = { /** * Additional extensions to add to the test app. */ - extensions: ExtensionDefinition[]; + extensions?: ExtensionDefinition[]; /** * Additional features to add to the test app. From 116e05e29409250517a75ff5335d7e11eb6acfd4 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Tue, 7 Oct 2025 23:04:44 +0200 Subject: [PATCH 13/15] frontend-test-utils: add initialRouteEntries option Signed-off-by: Vincenzo Scamporlino --- packages/frontend-test-utils/report.api.md | 1 + .../src/app/renderTestApp.tsx | 41 +++++++++++++++---- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/packages/frontend-test-utils/report.api.md b/packages/frontend-test-utils/report.api.md index bd008feb19..b22fa6f808 100644 --- a/packages/frontend-test-utils/report.api.md +++ b/packages/frontend-test-utils/report.api.md @@ -130,6 +130,7 @@ export type RenderTestAppOptions = { config?: JsonObject; extensions?: ExtensionDefinition[]; features?: FrontendFeature[]; + initialRouteEntries?: string[]; }; export { TestApiProvider }; diff --git a/packages/frontend-test-utils/src/app/renderTestApp.tsx b/packages/frontend-test-utils/src/app/renderTestApp.tsx index f19dcd7d5f..e8d90cadd6 100644 --- a/packages/frontend-test-utils/src/app/renderTestApp.tsx +++ b/packages/frontend-test-utils/src/app/renderTestApp.tsx @@ -20,11 +20,18 @@ import { createFrontendPlugin, ExtensionDefinition, FrontendFeature, + RouterBlueprint, } from '@backstage/frontend-plugin-api'; import { render } from '@testing-library/react'; import appPlugin from '@backstage/plugin-app'; import { JsonObject } from '@backstage/types'; import { ConfigReader } from '@backstage/config'; +import { MemoryRouter } from 'react-router-dom'; + +const DEFAULT_MOCK_CONFIG = { + app: { baseUrl: 'http://localhost:3000' }, + backend: { baseUrl: 'http://localhost:7007' }, +}; /** * Options for `renderTestApp`. @@ -45,6 +52,11 @@ export type RenderTestAppOptions = { * Additional features to add to the test app. */ features?: FrontendFeature[]; + + /** + * Initial route entries to use for the router. + */ + initialRouteEntries?: string[]; }; const appPluginOverride = appPlugin.withOverrides({ @@ -62,10 +74,23 @@ const appPluginOverride = appPlugin.withOverrides({ * @public */ export function renderTestApp(options: RenderTestAppOptions) { + const extensions = [ + RouterBlueprint.make({ + params: { + component: ({ children }) => ( + + {children} + + ), + }, + }), + ...(options.extensions ?? []), + ]; + const features: FrontendFeature[] = [ createFrontendPlugin({ pluginId: 'test', - extensions: options.extensions, + extensions, }), appPluginOverride, ]; @@ -76,14 +101,12 @@ export function renderTestApp(options: RenderTestAppOptions) { const app = createSpecializedApp({ features, - config: options.config - ? ConfigReader.fromConfigs([ - { - context: 'render-config', - data: options.config, - }, - ]) - : undefined, + config: ConfigReader.fromConfigs([ + { + context: 'render-config', + data: options?.config ?? DEFAULT_MOCK_CONFIG, + }, + ]), }); return render( From a8e223fa89d5c1c97cbf910e67056c1361de569b Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Mon, 13 Oct 2025 15:14:02 +0200 Subject: [PATCH 14/15] frontend-test-utils: marking as breaking Signed-off-by: Vincenzo Scamporlino --- .changeset/real-seals-dress.md | 4 ++-- packages/frontend-test-utils/report.api.md | 1 - .../frontend-test-utils/src/app/renderInTestApp.tsx | 11 ----------- 3 files changed, 2 insertions(+), 14 deletions(-) diff --git a/.changeset/real-seals-dress.md b/.changeset/real-seals-dress.md index 7606db6638..7f490d3ce9 100644 --- a/.changeset/real-seals-dress.md +++ b/.changeset/real-seals-dress.md @@ -1,5 +1,5 @@ --- -'@backstage/frontend-test-utils': patch +'@backstage/frontend-test-utils': minor --- -Add a new utility `renderTestApp` to `@backstage/frontend-test-utils` that simplifies rendering extensions within the Backstage application context for testing purposes. This utility replaces the use of `renderInTestApp` when the `extensions` option is needed. +**BREAKING**: Removed the `extensions` option from `renderInTestApp`. If you need to pass extensions to the test app, use the new `renderTestApp` utility instead. diff --git a/packages/frontend-test-utils/report.api.md b/packages/frontend-test-utils/report.api.md index b22fa6f808..b99cf843d4 100644 --- a/packages/frontend-test-utils/report.api.md +++ b/packages/frontend-test-utils/report.api.md @@ -145,7 +145,6 @@ export type TestAppOptions = { [path: string]: RouteRef; }; config?: JsonObject; - extensions?: ExtensionDefinition[]; features?: FrontendFeature[]; initialRouteEntries?: string[]; }; diff --git a/packages/frontend-test-utils/src/app/renderInTestApp.tsx b/packages/frontend-test-utils/src/app/renderInTestApp.tsx index 2b8efb643f..747229e8d6 100644 --- a/packages/frontend-test-utils/src/app/renderInTestApp.tsx +++ b/packages/frontend-test-utils/src/app/renderInTestApp.tsx @@ -67,13 +67,6 @@ export type TestAppOptions = { */ config?: JsonObject; - /** - * Additional extensions to add to the test app. - * - * @deprecated Use `renderTestApp` instead. - */ - extensions?: ExtensionDefinition[]; - /** * Additional features to add to the test app. */ @@ -195,10 +188,6 @@ export function renderInTestApp( } } - if (options?.extensions) { - extensions.push(...options.extensions); - } - const features: FrontendFeature[] = [ createFrontendPlugin({ pluginId: 'test', From 1c43d5a7c771ecf645aa098e1b5f2d86a29ecba0 Mon Sep 17 00:00:00 2001 From: Vincenzo Scamporlino Date: Mon, 13 Oct 2025 15:16:55 +0200 Subject: [PATCH 15/15] frontend-plugin-api: minor expect improvements Signed-off-by: Vincenzo Scamporlino --- .../blueprints/SwappableComponentBlueprint.test.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/frontend-plugin-api/src/blueprints/SwappableComponentBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/SwappableComponentBlueprint.test.tsx index f51750b1e7..63436308ed 100644 --- a/packages/frontend-plugin-api/src/blueprints/SwappableComponentBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/blueprints/SwappableComponentBlueprint.test.tsx @@ -60,7 +60,7 @@ describe('SwappableComponentBlueprint', () => { ], }); - expect(await screen.findByText('test!')).toBeInTheDocument(); + await expect(screen.findByText('test!')).resolves.toBeInTheDocument(); }); it('should render a component ref without a default implementation', async () => { @@ -80,7 +80,9 @@ describe('SwappableComponentBlueprint', () => { ], }); - expect(await screen.findByTestId('test.component')).toBeInTheDocument(); + await expect( + screen.findByTestId('test.component'), + ).resolves.toBeInTheDocument(); }); it('should render a component ref with an async loader implementation', async () => { @@ -103,7 +105,7 @@ describe('SwappableComponentBlueprint', () => { ], }); - expect(await screen.findByText('test!')).toBeInTheDocument(); + await expect(screen.findByText('test!')).resolves.toBeInTheDocument(); }); it('should render a component ref with an async loader implementation and prop transform', async () => { @@ -126,6 +128,6 @@ describe('SwappableComponentBlueprint', () => { ], }); - expect(await screen.findByText('tr test!')).toBeInTheDocument(); + await expect(screen.findByText('tr test!')).resolves.toBeInTheDocument(); }); });