diff --git a/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.test.tsx index f97798a64c..61b23e56f9 100644 --- a/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.test.tsx @@ -16,9 +16,11 @@ import React from 'react'; import { SignInPageBlueprint } from './SignInPageBlueprint'; -import { createExtensionTester } from '@backstage/frontend-test-utils'; +import { + createExtensionTester, + renderInTestApp, +} from '@backstage/frontend-test-utils'; import { screen, waitFor } from '@testing-library/react'; -import { coreExtensionData, createExtension } from '../wiring'; describe('SignInPageBlueprint', () => { it('should create an extension with sensible defaults', () => { @@ -60,20 +62,11 @@ describe('SignInPageBlueprint', () => { const tester = createExtensionTester(extension); - expect(tester.data(SignInPageBlueprint.dataRefs.component)).toBeDefined(); + const Element = tester.data(SignInPageBlueprint.dataRefs.component)!; - createExtensionTester( - createExtension({ - name: 'dummy', - attachTo: { id: 'ignored', input: 'ignored' }, - output: { - element: coreExtensionData.reactElement, - }, - factory: () => ({ element:
}), - }), - ) - .add(extension) - .render(); + expect(Element).toBeDefined(); + + renderInTestApp( {}} />); await waitFor(() => { expect(screen.getByTestId('mock-sign-in')).toBeInTheDocument(); diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.tsx b/packages/frontend-test-utils/src/app/createExtensionTester.tsx index 4bfc8392cb..2d842062fc 100644 --- a/packages/frontend-test-utils/src/app/createExtensionTester.tsx +++ b/packages/frontend-test-utils/src/app/createExtensionTester.tsx @@ -49,57 +49,6 @@ import { instantiateAppNodeTree } from '../../../frontend-app-api/src/tree/insta // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { readAppExtensionsConfig } from '../../../frontend-app-api/src/tree/readAppExtensionsConfig'; -const NavItem = (props: { - routeRef: RouteRef; - title: string; - icon: IconComponent; -}) => { - const { routeRef, title, icon: Icon } = props; - const link = useRouteRef(routeRef); - if (!link) { - return null; - } - return ( -
  • - - {title} - -
  • - ); -}; - -const TestAppNavExtension = createExtension({ - namespace: 'app', - name: 'nav', - attachTo: { id: 'app/layout', input: 'nav' }, - inputs: { - items: createExtensionInput({ - target: createNavItemExtension.targetDataRef, - }), - }, - output: { - element: coreExtensionData.reactElement, - }, - factory({ inputs }) { - return { - element: ( - - ), - }; - }, -}); - /** @public */ export class ExtensionQuery { #node: AppNode; @@ -245,6 +194,25 @@ export class ExtensionTester { return new ExtensionQuery(node); } + element(): JSX.Element { + const tree = this.#resolveTree(); + + const element = new ExtensionQuery(tree.root).data( + coreExtensionData.reactElement, + ); + + if (!element) { + throw new Error( + 'No element found. Make sure the extension has a `coreExtensionData.reactElement` output, or use the `.get(myComponentDataRef)` method to get the component', + ); + } + + return element; + } + + /** + * @deprecated Switch to using `renderInTestApp` directly and using `.element()` or `.get(myComponentDataRef)` to get the component you would like to wrap up + */ render(options?: { config?: JsonObject }): RenderResult { const { config = {} } = options ?? {}; @@ -260,7 +228,6 @@ export class ExtensionTester { createExtensionOverrides({ extensions: [ ...this.#extensions.map(extension => extension.definition), - TestAppNavExtension, createRouterExtension({ namespace: 'test', Component: ({ children }) => ( diff --git a/packages/frontend-test-utils/src/app/renderInTestApp.tsx b/packages/frontend-test-utils/src/app/renderInTestApp.tsx index e0483f68d8..d828c89f12 100644 --- a/packages/frontend-test-utils/src/app/renderInTestApp.tsx +++ b/packages/frontend-test-utils/src/app/renderInTestApp.tsx @@ -16,11 +16,24 @@ import React from 'react'; import { + ExtensionDefinition, + IconComponent, RouteRef, coreExtensionData, createExtension, + createExtensionInput, + createExtensionOverrides, + createNavItemExtension, + createRouterExtension, + useRouteRef, } from '@backstage/frontend-plugin-api'; -import { createExtensionTester } from './createExtensionTester'; +import { Link, MemoryRouter } from 'react-router-dom'; +import { createSpecializedApp } from '@backstage/frontend-app-api'; +import { render } from '@testing-library/react'; +import { resolveExtensionDefinition } from '@backstage/frontend-plugin-api/src/wiring/resolveExtensionDefinition'; +import { resolve } from 'path'; +import { ConfigReader } from '@backstage/config'; +import { JsonObject } from '@backstage/types'; /** * Options to customize the behavior of the test app. @@ -44,8 +57,64 @@ export type TestAppOptions = { * ``` */ mountedRoutes?: { [path: string]: RouteRef }; + + /** + * Additional configuration passed to the app when rendering elements inside it. + */ + config?: JsonObject; }; +const NavItem = (props: { + routeRef: RouteRef; + title: string; + icon: IconComponent; +}) => { + const { routeRef, title, icon: Icon } = props; + const link = useRouteRef(routeRef); + if (!link) { + return null; + } + return ( +
  • + + {title} + +
  • + ); +}; + +const TestAppNavExtension = createExtension({ + namespace: 'app', + name: 'nav', + attachTo: { id: 'app/layout', input: 'nav' }, + inputs: { + items: createExtensionInput({ + target: createNavItemExtension.targetDataRef, + }), + }, + output: { + element: coreExtensionData.reactElement, + }, + factory({ inputs }) { + return { + element: ( + + ), + }; + }, +}); + /** * @public * Renders the given element in a test app, for use in unit tests. @@ -54,20 +123,28 @@ export function renderInTestApp( element: JSX.Element, options?: TestAppOptions, ) { - const extension = createExtension({ - namespace: 'test', - attachTo: { id: 'app', input: 'root' }, - output: { - element: coreExtensionData.reactElement, - }, - factory: () => ({ element }), - }); - const tester = createExtensionTester(extension); + const extensions: Array> = [ + createExtension({ + namespace: 'test', + attachTo: { id: 'app/routes', input: 'routes' }, + output: [coreExtensionData.reactElement, coreExtensionData.routePath], + factory: () => { + return [ + coreExtensionData.reactElement(element), + coreExtensionData.routePath('/'), + ]; + }, + }), + createRouterExtension({ + namespace: 'test', + Component: ({ children }) => {children}, + }), + ]; if (options?.mountedRoutes) { for (const [path, routeRef] of Object.entries(options.mountedRoutes)) { // TODO(Rugvip): add support for external route refs - tester.add( + extensions.push( createExtension({ kind: 'test-route', name: path, @@ -84,5 +161,17 @@ export function renderInTestApp( ); } } - return tester.render(); + + const app = createSpecializedApp({ + features: [ + createExtensionOverrides({ + extensions, + }), + ], + config: ConfigReader.fromConfigs([ + { context: 'render-config', data: options?.config ?? {} }, + ]), + }); + + return render(app.createRoot()); }