From 4bc4cd969c953185d90d1a82ec4fb3ca9b361f51 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 13 Aug 2024 14:49:56 +0200 Subject: [PATCH 01/12] chore: wip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Patrik Oldsberg Co-authored-by: Fredrik Adelöw Co-authored-by: Camila Belo Co-authored-by: Johan Haals Signed-off-by: blam --- .../blueprints/SignInPageBlueprint.test.tsx | 23 ++-- .../src/app/createExtensionTester.tsx | 71 +++-------- .../src/app/renderInTestApp.tsx | 113 ++++++++++++++++-- 3 files changed, 128 insertions(+), 79 deletions(-) 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()); } From d2d4a80937711780ec9b80ea5c4da5655ee979df Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 13 Aug 2024 15:01:58 +0200 Subject: [PATCH 02/12] feat: some more work on splitting out the render method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Patrik Oldsberg Co-authored-by: Fredrik Adelöw Co-authored-by: Camila Belo Co-authored-by: Johan Haals Signed-off-by: blam --- .../src/app/createExtensionTester.tsx | 10 +-- .../src/app/renderInTestApp.tsx | 69 ++++++++++--------- 2 files changed, 39 insertions(+), 40 deletions(-) diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.tsx b/packages/frontend-test-utils/src/app/createExtensionTester.tsx index 2d842062fc..fb05270eb8 100644 --- a/packages/frontend-test-utils/src/app/createExtensionTester.tsx +++ b/packages/frontend-test-utils/src/app/createExtensionTester.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { MemoryRouter, Link } from 'react-router-dom'; +import { MemoryRouter } from 'react-router-dom'; import { RenderResult, render } from '@testing-library/react'; import { createSpecializedApp } from '@backstage/frontend-app-api'; import { @@ -24,15 +24,10 @@ import { Extension, ExtensionDataRef, ExtensionDefinition, - IconComponent, - RouteRef, coreExtensionData, createExtension, - createExtensionInput, createExtensionOverrides, - createNavItemExtension, createRouterExtension, - useRouteRef, } from '@backstage/frontend-plugin-api'; import { Config, ConfigReader } from '@backstage/config'; import { JsonArray, JsonObject, JsonValue } from '@backstage/types'; @@ -215,14 +210,12 @@ export class ExtensionTester { */ render(options?: { config?: JsonObject }): RenderResult { const { config = {} } = options ?? {}; - const [subject] = this.#extensions; if (!subject) { throw new Error( 'No subject found. At least one extension should be added to the tester.', ); } - const app = createSpecializedApp({ features: [ createExtensionOverrides({ @@ -239,7 +232,6 @@ export class ExtensionTester { ], config: this.#getConfig(config), }); - return render(app.createRoot()); } diff --git a/packages/frontend-test-utils/src/app/renderInTestApp.tsx b/packages/frontend-test-utils/src/app/renderInTestApp.tsx index d828c89f12..85cf1c626c 100644 --- a/packages/frontend-test-utils/src/app/renderInTestApp.tsx +++ b/packages/frontend-test-utils/src/app/renderInTestApp.tsx @@ -15,25 +15,23 @@ */ import React from 'react'; -import { - ExtensionDefinition, - IconComponent, - RouteRef, - coreExtensionData, - createExtension, - createExtensionInput, - createExtensionOverrides, - createNavItemExtension, - createRouterExtension, - useRouteRef, -} from '@backstage/frontend-plugin-api'; 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'; +import { + createExtension, + createExtensionOverrides, + createRouterExtension, + ExtensionDefinition, + coreExtensionData, + RouteRef, + useRouteRef, + createExtensionInput, + IconComponent, + createNavItemExtension, +} from '@backstage/frontend-plugin-api'; /** * Options to customize the behavior of the test app. @@ -120,26 +118,35 @@ const TestAppNavExtension = createExtension({ * Renders the given element in a test app, for use in unit tests. */ export function renderInTestApp( - element: JSX.Element, + element: JSX.Element | { extensions: ExtensionDefinition[] }, options?: TestAppOptions, ) { - const extensions: Array> = [ - createExtension({ - namespace: 'test', - attachTo: { id: 'app/routes', input: 'routes' }, - output: [coreExtensionData.reactElement, coreExtensionData.routePath], - factory: () => { - return [ - coreExtensionData.reactElement(element), - coreExtensionData.routePath('/'), + const extensions: Array> = + 'extensions' in element + ? element.extensions + : [ + 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} + ), + }), + TestAppNavExtension, ]; - }, - }), - createRouterExtension({ - namespace: 'test', - Component: ({ children }) => {children}, - }), - ]; if (options?.mountedRoutes) { for (const [path, routeRef] of Object.entries(options.mountedRoutes)) { From 1d6cf554c635fcc4a85df9756e65ddaa473079ac Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 13 Aug 2024 15:11:53 +0200 Subject: [PATCH 03/12] feat: added api-reports Signed-off-by: blam --- packages/frontend-test-utils/api-report.md | 8 +- .../src/app/renderInTestApp.tsx | 86 +++++++++---------- 2 files changed, 47 insertions(+), 47 deletions(-) diff --git a/packages/frontend-test-utils/api-report.md b/packages/frontend-test-utils/api-report.md index 3aa9069219..2c16028dd1 100644 --- a/packages/frontend-test-utils/api-report.md +++ b/packages/frontend-test-utils/api-report.md @@ -27,6 +27,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'; // @public (undocumented) @@ -62,8 +63,10 @@ export class ExtensionTester { // (undocumented) data(ref: ExtensionDataRef): T | undefined; // (undocumented) - query(id: string | ExtensionDefinition): ExtensionQuery; + element(): JSX.Element; // (undocumented) + query(id: string | ExtensionDefinition): ExtensionQuery; + // @deprecated (undocumented) render(options?: { config?: JsonObject }): RenderResult; } @@ -97,7 +100,7 @@ export { registerMswTestHooks }; export function renderInTestApp( element: JSX.Element, options?: TestAppOptions, -): RenderResult; +): RenderResult; // @public @deprecated (undocumented) export function setupRequestMockHandlers(worker: { @@ -117,6 +120,7 @@ export type TestAppOptions = { mountedRoutes?: { [path: string]: RouteRef; }; + config?: JsonObject; }; export { withLogCollector }; diff --git a/packages/frontend-test-utils/src/app/renderInTestApp.tsx b/packages/frontend-test-utils/src/app/renderInTestApp.tsx index 85cf1c626c..73a29ce0bb 100644 --- a/packages/frontend-test-utils/src/app/renderInTestApp.tsx +++ b/packages/frontend-test-utils/src/app/renderInTestApp.tsx @@ -23,7 +23,6 @@ import { JsonObject } from '@backstage/types'; import { createExtension, createExtensionOverrides, - createRouterExtension, ExtensionDefinition, coreExtensionData, RouteRef, @@ -31,6 +30,7 @@ import { createExtensionInput, IconComponent, createNavItemExtension, + RouterBlueprint, } from '@backstage/frontend-plugin-api'; /** @@ -86,30 +86,32 @@ const TestAppNavExtension = createExtension({ name: 'nav', attachTo: { id: 'app/layout', input: 'nav' }, inputs: { - items: createExtensionInput({ - target: createNavItemExtension.targetDataRef, - }), - }, - output: { - element: coreExtensionData.reactElement, + items: createExtensionInput([createNavItemExtension.targetDataRef]), }, + output: [coreExtensionData.reactElement], factory({ inputs }) { - return { - element: ( + return [ + coreExtensionData.reactElement( + , ), - }; + ]; }, }); @@ -118,35 +120,29 @@ const TestAppNavExtension = createExtension({ * Renders the given element in a test app, for use in unit tests. */ export function renderInTestApp( - element: JSX.Element | { extensions: ExtensionDefinition[] }, + element: JSX.Element, options?: TestAppOptions, ) { - const extensions: Array> = - 'extensions' in element - ? element.extensions - : [ - 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} - ), - }), - TestAppNavExtension, + const extensions: Array> = [ + createExtension({ + namespace: 'test', + attachTo: { id: 'app/routes', input: 'routes' }, + output: [coreExtensionData.reactElement, coreExtensionData.routePath], + factory: () => { + return [ + coreExtensionData.reactElement(element), + coreExtensionData.routePath('/'), ]; + }, + }), + RouterBlueprint.make({ + namespace: 'test', + params: { + Component: ({ children }) => {children}, + }, + }), + TestAppNavExtension, + ]; if (options?.mountedRoutes) { for (const [path, routeRef] of Object.entries(options.mountedRoutes)) { From c00e1a00f9beb85af31b92ca9f749c2a91a9d629 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 13 Aug 2024 15:20:12 +0200 Subject: [PATCH 04/12] chore: added changeset Signed-off-by: blam --- .changeset/three-kiwis-turn.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .changeset/three-kiwis-turn.md diff --git a/.changeset/three-kiwis-turn.md b/.changeset/three-kiwis-turn.md new file mode 100644 index 0000000000..25b5e0497b --- /dev/null +++ b/.changeset/three-kiwis-turn.md @@ -0,0 +1,17 @@ +--- +'@backstage/frontend-plugin-api': patch +'@backstage/frontend-test-utils': patch +--- + +Deprecate the `.render` method of the `createExtensionTester` in favour of using `renderInTestApp` directly. + +```tsx +import { renderInTestApp, createExtensionTester } from '@backstage/frontend-test-utils'; + +const tester = createExtensionTester(extension); + +const { getByTestId } = renderInTestApp(tester.element()); + +// or if you're not using `coreExtensionData.reactElement` as the output ref +const { getByTestId } = renderInTestApp(tester.data(myComponentRef))' +``` From 471473f3911a5c90c5936e1e1fa9fec5b5dbda09 Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 13 Aug 2024 15:23:50 +0200 Subject: [PATCH 05/12] chore: ext Signed-off-by: blam Signed-off-by: blam --- .changeset/three-kiwis-turn.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.changeset/three-kiwis-turn.md b/.changeset/three-kiwis-turn.md index 25b5e0497b..48a1ffff33 100644 --- a/.changeset/three-kiwis-turn.md +++ b/.changeset/three-kiwis-turn.md @@ -1,5 +1,4 @@ --- -'@backstage/frontend-plugin-api': patch '@backstage/frontend-test-utils': patch --- From 9eec064aaf317efb9df177fdf4701e8b21047ded Mon Sep 17 00:00:00 2001 From: blam Date: Tue, 13 Aug 2024 15:31:23 +0200 Subject: [PATCH 06/12] chore: small refactor Signed-off-by: blam --- .changeset/three-kiwis-turn.md | 7 +++++-- .../src/blueprints/PageBlueprint.test.tsx | 7 +++++-- packages/frontend-test-utils/api-report.md | 3 +-- .../frontend-test-utils/src/app/createExtensionTester.tsx | 2 ++ packages/frontend-test-utils/src/app/renderInTestApp.tsx | 6 +++--- 5 files changed, 16 insertions(+), 9 deletions(-) diff --git a/.changeset/three-kiwis-turn.md b/.changeset/three-kiwis-turn.md index 48a1ffff33..d5802fa586 100644 --- a/.changeset/three-kiwis-turn.md +++ b/.changeset/three-kiwis-turn.md @@ -5,12 +5,15 @@ Deprecate the `.render` method of the `createExtensionTester` in favour of using `renderInTestApp` directly. ```tsx -import { renderInTestApp, createExtensionTester } from '@backstage/frontend-test-utils'; +import { + renderInTestApp, + createExtensionTester, +} from '@backstage/frontend-test-utils'; const tester = createExtensionTester(extension); const { getByTestId } = renderInTestApp(tester.element()); // or if you're not using `coreExtensionData.reactElement` as the output ref -const { getByTestId } = renderInTestApp(tester.data(myComponentRef))' +const { getByTestId } = renderInTestApp(tester.data(myComponentRef)); ``` diff --git a/packages/frontend-plugin-api/src/blueprints/PageBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.test.tsx index c4bcb4b467..d70a426a57 100644 --- a/packages/frontend-plugin-api/src/blueprints/PageBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.test.tsx @@ -16,7 +16,10 @@ import React from 'react'; import { createRouteRef } from '../routing'; import { PageBlueprint } from './PageBlueprint'; -import { createExtensionTester } from '@backstage/frontend-test-utils'; +import { + createExtensionTester, + renderInTestApp, +} from '@backstage/frontend-test-utils'; import { coreExtensionData, createExtensionBlueprint, @@ -144,7 +147,7 @@ describe('PageBlueprint', () => { CardBlueprint.make({ name: 'card', params: {} }), ); - const { getByTestId, getByText } = tester.render(); + const { getByTestId, getByText } = renderInTestApp(tester.element()); await waitFor(() => expect(getByTestId('card')).toBeInTheDocument()); await waitFor(() => diff --git a/packages/frontend-test-utils/api-report.md b/packages/frontend-test-utils/api-report.md index 2c16028dd1..0cbdace03e 100644 --- a/packages/frontend-test-utils/api-report.md +++ b/packages/frontend-test-utils/api-report.md @@ -27,7 +27,6 @@ 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'; // @public (undocumented) @@ -100,7 +99,7 @@ export { registerMswTestHooks }; export function renderInTestApp( element: JSX.Element, options?: TestAppOptions, -): RenderResult; +): RenderResult; // @public @deprecated (undocumented) export function setupRequestMockHandlers(worker: { diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.tsx b/packages/frontend-test-utils/src/app/createExtensionTester.tsx index fb05270eb8..83ac2aa600 100644 --- a/packages/frontend-test-utils/src/app/createExtensionTester.tsx +++ b/packages/frontend-test-utils/src/app/createExtensionTester.tsx @@ -43,6 +43,7 @@ import { resolveAppNodeSpecs } from '../../../frontend-app-api/src/tree/resolveA import { instantiateAppNodeTree } from '../../../frontend-app-api/src/tree/instantiateAppNodeTree'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { readAppExtensionsConfig } from '../../../frontend-app-api/src/tree/readAppExtensionsConfig'; +import { TestAppNavExtension } from './renderInTestApp'; /** @public */ export class ExtensionQuery { @@ -227,6 +228,7 @@ export class ExtensionTester { {children} ), }), + TestAppNavExtension, ], }), ], diff --git a/packages/frontend-test-utils/src/app/renderInTestApp.tsx b/packages/frontend-test-utils/src/app/renderInTestApp.tsx index 73a29ce0bb..dd03cdd2a2 100644 --- a/packages/frontend-test-utils/src/app/renderInTestApp.tsx +++ b/packages/frontend-test-utils/src/app/renderInTestApp.tsx @@ -17,7 +17,7 @@ import React from 'react'; import { Link, MemoryRouter } from 'react-router-dom'; import { createSpecializedApp } from '@backstage/frontend-app-api'; -import { render } from '@testing-library/react'; +import { RenderResult, render } from '@testing-library/react'; import { ConfigReader } from '@backstage/config'; import { JsonObject } from '@backstage/types'; import { @@ -81,7 +81,7 @@ const NavItem = (props: { ); }; -const TestAppNavExtension = createExtension({ +export const TestAppNavExtension = createExtension({ namespace: 'app', name: 'nav', attachTo: { id: 'app/layout', input: 'nav' }, @@ -122,7 +122,7 @@ const TestAppNavExtension = createExtension({ export function renderInTestApp( element: JSX.Element, options?: TestAppOptions, -) { +): RenderResult { const extensions: Array> = [ createExtension({ namespace: 'test', From f1e9c08f978c078692f8fea69f6bd82fc16842fe Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 14 Aug 2024 15:37:09 +0200 Subject: [PATCH 07/12] chore: code review comments Signed-off-by: blam --- .../src/blueprints/NavItemBlueprint.test.tsx | 4 +- .../src/blueprints/NavLogoBlueprint.test.tsx | 2 +- .../src/blueprints/PageBlueprint.test.tsx | 4 +- .../blueprints/SignInPageBlueprint.test.tsx | 2 +- .../src/blueprints/ThemeBlueprint.test.ts | 2 +- .../blueprints/TranslationBlueprint.test.ts | 2 +- .../src/wiring/createExtension.test.ts | 30 +++---- .../src/app/createExtensionTester.test.tsx | 8 +- .../src/app/createExtensionTester.tsx | 78 ++++++++++++++++--- 9 files changed, 95 insertions(+), 37 deletions(-) diff --git a/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.test.tsx index 94b9e7b8e6..daa21f88bb 100644 --- a/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/blueprints/NavItemBlueprint.test.tsx @@ -77,7 +77,7 @@ describe('NavItemBlueprint', () => { const tester = createExtensionTester(extension); - expect(tester.data(NavItemBlueprint.dataRefs.target)).toEqual({ + expect(tester.get(NavItemBlueprint.dataRefs.target)).toEqual({ title: 'TEST', icon: MockIcon, routeRef: mockRouteRef, @@ -97,7 +97,7 @@ describe('NavItemBlueprint', () => { config: { title: 'OVERRIDDEN' }, }); - expect(tester.data(NavItemBlueprint.dataRefs.target)).toEqual({ + expect(tester.get(NavItemBlueprint.dataRefs.target)).toEqual({ title: 'OVERRIDDEN', icon: MockIcon, routeRef: mockRouteRef, diff --git a/packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.test.tsx index 4a96475cf5..b976196080 100644 --- a/packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/blueprints/NavLogoBlueprint.test.tsx @@ -64,7 +64,7 @@ describe('NavLogoBlueprint', () => { const tester = createExtensionTester(extension); - expect(tester.data(NavLogoBlueprint.dataRefs.logoElements)).toEqual({ + expect(tester.get(NavLogoBlueprint.dataRefs.logoElements)).toEqual({ logoFull, logoIcon, }); diff --git a/packages/frontend-plugin-api/src/blueprints/PageBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.test.tsx index d70a426a57..dedc5454bd 100644 --- a/packages/frontend-plugin-api/src/blueprints/PageBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/blueprints/PageBlueprint.test.tsx @@ -101,7 +101,7 @@ describe('PageBlueprint', () => { // TODO(blam): test for the routePath output doesn't work, due to the the way the test harness works // expect(tester.data(coreExtensionData.routePath)).toBe('/test'); - expect(tester.data(coreExtensionData.routeRef)).toBe(mockRouteRef); + expect(tester.get(coreExtensionData.routeRef)).toBe(mockRouteRef); const { getByTestId } = tester.render(); @@ -147,7 +147,7 @@ describe('PageBlueprint', () => { CardBlueprint.make({ name: 'card', params: {} }), ); - const { getByTestId, getByText } = renderInTestApp(tester.element()); + const { getByTestId, getByText } = renderInTestApp(tester.reactElement()); await waitFor(() => expect(getByTestId('card')).toBeInTheDocument()); await waitFor(() => diff --git a/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.test.tsx index 61b23e56f9..064021bba2 100644 --- a/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.test.tsx @@ -62,7 +62,7 @@ describe('SignInPageBlueprint', () => { const tester = createExtensionTester(extension); - const Element = tester.data(SignInPageBlueprint.dataRefs.component)!; + const Element = tester.get(SignInPageBlueprint.dataRefs.component)!; expect(Element).toBeDefined(); diff --git a/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.test.ts b/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.test.ts index 40323b6a31..e6daf510eb 100644 --- a/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.test.ts +++ b/packages/frontend-plugin-api/src/blueprints/ThemeBlueprint.test.ts @@ -56,7 +56,7 @@ describe('ThemeBlueprint', () => { const extension = ThemeBlueprint.make({ params: { theme } }); expect( - createExtensionTester(extension).data(ThemeBlueprint.dataRefs.theme), + createExtensionTester(extension).get(ThemeBlueprint.dataRefs.theme), ).toEqual(theme); }); }); diff --git a/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.test.ts b/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.test.ts index dfb918053a..3c7b271322 100644 --- a/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.test.ts +++ b/packages/frontend-plugin-api/src/blueprints/TranslationBlueprint.test.ts @@ -76,7 +76,7 @@ describe('TranslationBlueprint', () => { }); expect( - createExtensionTester(extension).data( + createExtensionTester(extension).get( TranslationBlueprint.dataRefs.translation, ), ).toBe(messages); diff --git a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts index e33bdcb9a1..5902c5c3e7 100644 --- a/packages/frontend-plugin-api/src/wiring/createExtension.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createExtension.test.ts @@ -688,7 +688,7 @@ describe('createExtension', () => { const tester = createExtensionTester(overridden); - expect(tester.data(numberDataRef)).toBe(43); + expect(tester.get(numberDataRef)).toBe(43); }); it('should work functionally with overrides', () => { @@ -722,14 +722,14 @@ describe('createExtension', () => { }, }); - expect(createExtensionTester(overriden).data(stringDataRef)).toBe( + expect(createExtensionTester(overriden).get(stringDataRef)).toBe( 'foo-boom-override-hello', ); expect( createExtensionTester(overriden, { config: { foo: 'hello', bar: 'world' }, - }).data(stringDataRef), + }).get(stringDataRef), ).toBe('foo-hello-override-world'); }); @@ -809,7 +809,7 @@ describe('createExtension', () => { .add(singleExt) .add(multi1Ext) .add(multi2Ext) - .data(outputRef), + .get(outputRef), ).toEqual({ opt: 'orig-opt', single: 'orig-single', @@ -836,7 +836,7 @@ describe('createExtension', () => { .add(singleExt) .add(multi1Ext) .add(multi2Ext) - .data(outputRef), + .get(outputRef), ).toEqual({ opt: 'opt', single: 'single', @@ -862,7 +862,7 @@ describe('createExtension', () => { .add(singleExt) .add(multi1Ext) .add(multi2Ext) - .data(outputRef), + .get(outputRef), ).toEqual({ opt: 'none', single: 'single', @@ -885,7 +885,7 @@ describe('createExtension', () => { .add(singleExt) .add(multi1Ext) .add(multi2Ext) - .data(outputRef), + .get(outputRef), ).toEqual({ opt: 'orig-opt', single: 'orig-single', @@ -912,7 +912,7 @@ describe('createExtension', () => { .add(singleExt) .add(multi1Ext) .add(multi2Ext) - .data(outputRef), + .get(outputRef), ).toEqual({ opt: 'orig-opt', single: 'orig-single', @@ -939,7 +939,7 @@ describe('createExtension', () => { .add(singleExt) .add(multi1Ext) .add(multi2Ext) - .data(outputRef), + .get(outputRef), ).toEqual({ opt: 'orig-opt', single: 'orig-single', @@ -966,7 +966,7 @@ describe('createExtension', () => { .add(singleExt) .add(multi1Ext) .add(multi2Ext) - .data(outputRef), + .get(outputRef), ).toEqual({ opt: 'orig-opt', single: 'orig-single', @@ -997,7 +997,7 @@ describe('createExtension', () => { .add(singleExt) .add(multi1Ext) .add(multi2Ext) - .data(outputRef), + .get(outputRef), ).toEqual({ opt: 'none', single: 'override-orig-single', @@ -1023,7 +1023,7 @@ describe('createExtension', () => { .add(singleExt) .add(multi1Ext) .add(multi2Ext) - .data(outputRef), + .get(outputRef), ).toThrowErrorMatchingInlineSnapshot( `"Failed to instantiate extension 'subject', override data provided for input 'multi' must match the length of the original inputs"`, ); @@ -1046,7 +1046,7 @@ describe('createExtension', () => { .add(singleExt) .add(multi1Ext) .add(multi2Ext) - .data(outputRef), + .get(outputRef), ).toThrowErrorMatchingInlineSnapshot( `"Failed to instantiate extension 'subject', override data for input 'multi' may not mix forwarded inputs with data overrides"`, ); @@ -1069,7 +1069,7 @@ describe('createExtension', () => { .add(singleExt) .add(multi1Ext) .add(multi2Ext) - .data(outputRef), + .get(outputRef), ).toThrowErrorMatchingInlineSnapshot( `"Failed to instantiate extension 'subject', missing required extension data value(s) 'test1'"`, ); @@ -1098,7 +1098,7 @@ describe('createExtension', () => { .add(singleExt) .add(multi1Ext) .add(multi2Ext) - .data(outputRef), + .get(outputRef), ).toThrowErrorMatchingInlineSnapshot( `"Failed to instantiate extension 'subject', extension data 'test2' was provided but not declared"`, ); diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx b/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx index 889c815fc7..540a986733 100644 --- a/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx +++ b/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx @@ -246,7 +246,7 @@ describe('createExtensionTester', () => { const tester = createExtensionTester(extension); - expect(tester.data(stringDataRef)).toBe('test-text'); + expect(tester.get(stringDataRef)).toBe('test-text'); }); it('should throw an error if trying to access an instance not provided to the tester', () => { @@ -328,8 +328,8 @@ describe('createExtensionTester', () => { const tester = createExtensionTester(extension).add(extension2); - expect(tester.query(extension).data(stringDataRef)).toBe('nest-test-text'); - expect(tester.query(extension2).data(stringDataRef)).toBe('test-text'); + expect(tester.query(extension).get(stringDataRef)).toBe('nest-test-text'); + expect(tester.query(extension2).get(stringDataRef)).toBe('test-text'); // @ts-expect-error expect(tester.query(extension).input('input').data(stringDataRef)).toBe( 'nest-test-text', @@ -362,6 +362,6 @@ describe('createExtensionTester', () => { inputs: { input: 'test-text' }, }); - expect(tester.query(extension).data(stringDataRef)).toBe('nest-test-text'); + expect(tester.query(extension).get(stringDataRef)).toBe('nest-test-text'); }); }); diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.tsx b/packages/frontend-test-utils/src/app/createExtensionTester.tsx index 83ac2aa600..1ca65478dc 100644 --- a/packages/frontend-test-utils/src/app/createExtensionTester.tsx +++ b/packages/frontend-test-utils/src/app/createExtensionTester.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { MemoryRouter } from 'react-router-dom'; +import { MemoryRouter, Link } from 'react-router-dom'; import { RenderResult, render } from '@testing-library/react'; import { createSpecializedApp } from '@backstage/frontend-app-api'; import { @@ -24,10 +24,15 @@ import { Extension, ExtensionDataRef, ExtensionDefinition, + IconComponent, + RouteRef, coreExtensionData, createExtension, + createExtensionInput, createExtensionOverrides, + createNavItemExtension, createRouterExtension, + useRouteRef, } from '@backstage/frontend-plugin-api'; import { Config, ConfigReader } from '@backstage/config'; import { JsonArray, JsonObject, JsonValue } from '@backstage/types'; @@ -43,7 +48,57 @@ import { resolveAppNodeSpecs } from '../../../frontend-app-api/src/tree/resolveA import { instantiateAppNodeTree } from '../../../frontend-app-api/src/tree/instantiateAppNodeTree'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { readAppExtensionsConfig } from '../../../frontend-app-api/src/tree/readAppExtensionsConfig'; -import { TestAppNavExtension } from './renderInTestApp'; + +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 { @@ -69,7 +124,7 @@ export class ExtensionQuery { return instance; } - data(ref: ExtensionDataRef): T | undefined { + get(ref: ExtensionDataRef): T | undefined { return this.instance.getData(ref); } } @@ -164,10 +219,10 @@ export class ExtensionTester { return this; } - data(ref: ExtensionDataRef): T | undefined { + get(ref: ExtensionDataRef): T | undefined { const tree = this.#resolveTree(); - return new ExtensionQuery(tree.root).data(ref); + return new ExtensionQuery(tree.root).get(ref); } query(id: string | ExtensionDefinition): ExtensionQuery { @@ -190,16 +245,16 @@ export class ExtensionTester { return new ExtensionQuery(node); } - element(): JSX.Element { + reactElement(): JSX.Element { const tree = this.#resolveTree(); - const element = new ExtensionQuery(tree.root).data( + const element = new ExtensionQuery(tree.root).get( 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', + 'No element found. Make sure the extension has a `coreExtensionData.reactElement` output, or use the `.get(...)` to access output data directly instead', ); } @@ -207,33 +262,36 @@ export class ExtensionTester { } /** - * @deprecated Switch to using `renderInTestApp` directly and using `.element()` or `.get(myComponentDataRef)` to get the component you would like to wrap up + * @deprecated Switch to using `renderInTestApp` directly and using `.reactElement()` or `.get(...)` to get the component you w */ render(options?: { config?: JsonObject }): RenderResult { const { config = {} } = options ?? {}; + const [subject] = this.#extensions; if (!subject) { throw new Error( 'No subject found. At least one extension should be added to the tester.', ); } + const app = createSpecializedApp({ features: [ createExtensionOverrides({ extensions: [ ...this.#extensions.map(extension => extension.definition), + TestAppNavExtension, createRouterExtension({ namespace: 'test', Component: ({ children }) => ( {children} ), }), - TestAppNavExtension, ], }), ], config: this.#getConfig(config), }); + return render(app.createRoot()); } From 52367e47f49784b5b0b8620884ac4eb49f3968b0 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 14 Aug 2024 15:37:46 +0200 Subject: [PATCH 08/12] chore: update changeset Signed-off-by: blam Signed-off-by: blam --- .changeset/three-kiwis-turn.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/three-kiwis-turn.md b/.changeset/three-kiwis-turn.md index d5802fa586..eb67cbbbdc 100644 --- a/.changeset/three-kiwis-turn.md +++ b/.changeset/three-kiwis-turn.md @@ -12,8 +12,8 @@ import { const tester = createExtensionTester(extension); -const { getByTestId } = renderInTestApp(tester.element()); +const { getByTestId } = renderInTestApp(tester.reactElement()); // or if you're not using `coreExtensionData.reactElement` as the output ref -const { getByTestId } = renderInTestApp(tester.data(myComponentRef)); +const { getByTestId } = renderInTestApp(tester.get(myComponentRef)); ``` From 2ffa5d730eafde64eb3c1e7bf1d2bea66a3c7367 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 14 Aug 2024 16:23:38 +0200 Subject: [PATCH 09/12] chore: update api-reports Signed-off-by: blam --- packages/frontend-test-utils/api-report.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/frontend-test-utils/api-report.md b/packages/frontend-test-utils/api-report.md index 0cbdace03e..e0f9c164e9 100644 --- a/packages/frontend-test-utils/api-report.md +++ b/packages/frontend-test-utils/api-report.md @@ -43,7 +43,7 @@ export { ErrorWithContext }; export class ExtensionQuery { constructor(node: AppNode); // (undocumented) - data(ref: ExtensionDataRef): T | undefined; + get(ref: ExtensionDataRef): T | undefined; // (undocumented) get instance(): AppNodeInstance; // (undocumented) @@ -60,11 +60,11 @@ export class ExtensionTester { }, ): ExtensionTester; // (undocumented) - data(ref: ExtensionDataRef): T | undefined; - // (undocumented) - element(): JSX.Element; + get(ref: ExtensionDataRef): T | undefined; // (undocumented) query(id: string | ExtensionDefinition): ExtensionQuery; + // (undocumented) + reactElement(): JSX.Element; // @deprecated (undocumented) render(options?: { config?: JsonObject }): RenderResult; } From 1d58ff6425799055d373789303867a3516e4153a Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 15 Aug 2024 13:19:49 +0200 Subject: [PATCH 10/12] chore: update the types a little bit to help with parsing Signed-off-by: blam Signed-off-by: blam --- .../blueprints/SignInPageBlueprint.test.tsx | 2 +- .../src/app/createExtensionTester.test.tsx | 77 ++++++++++++++++++- .../src/app/createExtensionTester.tsx | 46 +++++++---- 3 files changed, 108 insertions(+), 17 deletions(-) diff --git a/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.test.tsx index 064021bba2..b45d8ead15 100644 --- a/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.test.tsx @@ -62,7 +62,7 @@ describe('SignInPageBlueprint', () => { const tester = createExtensionTester(extension); - const Element = tester.get(SignInPageBlueprint.dataRefs.component)!; + const Element = tester.get(SignInPageBlueprint.dataRefs.component); expect(Element).toBeDefined(); diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx b/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx index 540a986733..13bdca1990 100644 --- a/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx +++ b/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx @@ -240,8 +240,8 @@ describe('createExtensionTester', () => { const extension = createExtension({ namespace: 'test', attachTo: { id: 'ignored', input: 'ignored' }, - output: { text: stringDataRef }, - factory: () => ({ text: 'test-text' }), + output: [stringDataRef], + factory: () => [stringDataRef('test-text')], }); const tester = createExtensionTester(extension); @@ -297,6 +297,76 @@ describe('createExtensionTester', () => { ); }); + it('should not allow getting extension data for an output that was not defined in the extension', () => { + const internalRef = createExtensionDataRef().with({ + id: 'test.internal', + }); + + const internalRef2 = createExtensionDataRef().with({ + id: 'test.internal2', + }); + + const extension = createExtension({ + namespace: 'test', + name: 'e1', + attachTo: { id: 'ignored', input: 'ignored' }, + output: [stringDataRef, internalRef.optional()], + factory: () => [stringDataRef('test-text')], + }); + + const tester = createExtensionTester(extension); + + const test: string = tester.get(stringDataRef); + + // @ts-expect-error - internalRef is optional + const test2: number = tester.get(internalRef); + + // @ts-expect-error - internalRef2 is not defined in the extension + const test3: number = tester.get(internalRef2); + + expect([test, test2, test3]).toBeDefined(); + }); + + it('should support getting outputs from a query response', () => { + const internalRef = createExtensionDataRef().with({ + id: 'test.internal', + }); + + const internalRef2 = createExtensionDataRef().with({ + id: 'test.internal2', + }); + + const extension = createExtension({ + namespace: 'test', + name: 'e1', + attachTo: { id: 'ignored', input: 'ignored' }, + output: [coreExtensionData.reactElement], + factory: () => [coreExtensionData.reactElement(
    bob
    )], + }); + + const extraExtension = createExtension({ + namespace: 'test', + name: 'e1', + attachTo: { id: 'ignored', input: 'ignored' }, + output: [stringDataRef, internalRef.optional()], + factory: () => [stringDataRef('test-text')], + }); + + const tester = createExtensionTester(extension) + .add(extraExtension) + .query(extraExtension); + + const test: string = tester.get(stringDataRef); + + // @ts-expect-error - internalRef is optional + const test2: number = tester.get(internalRef); + + // @ts-expect-error - internalRef2 is not defined in the extension + const test3: number = tester.get(internalRef2); + + expect([test, test2, test3]).toBeDefined(); + }); + // TODO: this should be implemented // eslint-disable-next-line jest/no-disabled-tests it.skip('should allow querying an extension and getting outputs', () => { @@ -328,7 +398,9 @@ describe('createExtensionTester', () => { const tester = createExtensionTester(extension).add(extension2); + // @ts-expect-error expect(tester.query(extension).get(stringDataRef)).toBe('nest-test-text'); + // @ts-expect-error expect(tester.query(extension2).get(stringDataRef)).toBe('test-text'); // @ts-expect-error expect(tester.query(extension).input('input').data(stringDataRef)).toBe( @@ -362,6 +434,7 @@ describe('createExtensionTester', () => { inputs: { input: 'test-text' }, }); + // @ts-expect-error expect(tester.query(extension).get(stringDataRef)).toBe('nest-test-text'); }); }); diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.tsx b/packages/frontend-test-utils/src/app/createExtensionTester.tsx index 1ca65478dc..b3c1b385e3 100644 --- a/packages/frontend-test-utils/src/app/createExtensionTester.tsx +++ b/packages/frontend-test-utils/src/app/createExtensionTester.tsx @@ -19,6 +19,7 @@ import { MemoryRouter, Link } from 'react-router-dom'; import { RenderResult, render } from '@testing-library/react'; import { createSpecializedApp } from '@backstage/frontend-app-api'; import { + AnyExtensionDataRef, AppNode, AppTree, Extension, @@ -101,7 +102,7 @@ const TestAppNavExtension = createExtension({ }); /** @public */ -export class ExtensionQuery { +export class ExtensionQuery { #node: AppNode; constructor(node: AppNode) { @@ -124,18 +125,24 @@ export class ExtensionQuery { return instance; } - get(ref: ExtensionDataRef): T | undefined { + get( + ref: ExtensionDataRef, + ): UOutput extends ExtensionDataRef + ? IConfig['optional'] extends true + ? IData | undefined + : IData + : never { return this.instance.getData(ref); } } /** @public */ -export class ExtensionTester { +export class ExtensionTester { /** @internal */ - static forSubject( + static forSubject( subject: ExtensionDefinition, options?: { config?: TConfigInput }, - ): ExtensionTester { + ): ExtensionTester { const tester = new ExtensionTester(); const internal = toInternalExtensionDefinition(subject); @@ -192,7 +199,7 @@ export class ExtensionTester { add( extension: ExtensionDefinition, options?: { config?: TConfigInput }, - ): ExtensionTester { + ): ExtensionTester { if (this.#tree) { throw new Error( 'Cannot add more extensions accessing the extension tree', @@ -219,17 +226,24 @@ export class ExtensionTester { return this; } - get(ref: ExtensionDataRef): T | undefined { + get( + ref: ExtensionDataRef, + ): UOutput extends ExtensionDataRef + ? IConfig['optional'] extends true + ? IData | undefined + : IData + : never { const tree = this.#resolveTree(); return new ExtensionQuery(tree.root).get(ref); } - query(id: string | ExtensionDefinition): ExtensionQuery { + query( + extension: ExtensionDefinition, + ): ExtensionQuery { const tree = this.#resolveTree(); - const actualId = - typeof id === 'string' ? id : resolveExtensionDefinition(id).id; + const actualId = resolveExtensionDefinition(extension).id; const node = tree.nodes.get(actualId); @@ -355,9 +369,13 @@ export class ExtensionTester { } /** @public */ -export function createExtensionTester( - subject: ExtensionDefinition, - options?: { config?: TConfig }, -): ExtensionTester { +export function createExtensionTester< + TConfig, + TConfigInput, + UOutput extends AnyExtensionDataRef, +>( + subject: ExtensionDefinition, + options?: { config?: TConfigInput }, +): ExtensionTester { return ExtensionTester.forSubject(subject, options); } From 025b9fe183aab6f62413da657a40164031632fee Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 15 Aug 2024 13:26:15 +0200 Subject: [PATCH 11/12] chore: reflect a data container Signed-off-by: blam Signed-off-by: blam --- packages/frontend-test-utils/api-report.md | 39 ++++++++++++++----- .../blueprints/EntityCardBlueprint.test.tsx | 6 +-- .../EntityContentBlueprint.test.tsx | 12 +++--- 3 files changed, 38 insertions(+), 19 deletions(-) diff --git a/packages/frontend-test-utils/api-report.md b/packages/frontend-test-utils/api-report.md index e0f9c164e9..164c222d84 100644 --- a/packages/frontend-test-utils/api-report.md +++ b/packages/frontend-test-utils/api-report.md @@ -7,6 +7,7 @@ import { AnalyticsApi } from '@backstage/frontend-plugin-api'; import { AnalyticsEvent } from '@backstage/frontend-plugin-api'; +import { AnyExtensionDataRef } from '@backstage/frontend-plugin-api'; import { AppNode } from '@backstage/frontend-plugin-api'; import { AppNodeInstance } from '@backstage/frontend-plugin-api'; import { ErrorWithContext } from '@backstage/test-utils'; @@ -30,20 +31,30 @@ import { TestApiRegistry } from '@backstage/test-utils'; import { withLogCollector } from '@backstage/test-utils'; // @public (undocumented) -export function createExtensionTester( - subject: ExtensionDefinition, +export function createExtensionTester< + TConfig, + TConfigInput, + UOutput extends AnyExtensionDataRef, +>( + subject: ExtensionDefinition, options?: { - config?: TConfig; + config?: TConfigInput; }, -): ExtensionTester; +): ExtensionTester; export { ErrorWithContext }; // @public (undocumented) -export class ExtensionQuery { +export class ExtensionQuery { constructor(node: AppNode); // (undocumented) - get(ref: ExtensionDataRef): T | undefined; + get( + ref: ExtensionDataRef, + ): UOutput extends ExtensionDataRef + ? IConfig['optional'] extends true + ? IData | undefined + : IData + : never; // (undocumented) get instance(): AppNodeInstance; // (undocumented) @@ -51,18 +62,26 @@ export class ExtensionQuery { } // @public (undocumented) -export class ExtensionTester { +export class ExtensionTester { // (undocumented) add( extension: ExtensionDefinition, options?: { config?: TConfigInput; }, - ): ExtensionTester; + ): ExtensionTester; // (undocumented) - get(ref: ExtensionDataRef): T | undefined; + get( + ref: ExtensionDataRef, + ): UOutput extends ExtensionDataRef + ? IConfig['optional'] extends true + ? IData | undefined + : IData + : never; // (undocumented) - query(id: string | ExtensionDefinition): ExtensionQuery; + query( + extension: ExtensionDefinition, + ): ExtensionQuery; // (undocumented) reactElement(): JSX.Element; // @deprecated (undocumented) diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.test.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.test.tsx index db787766fd..0cb7cdead8 100644 --- a/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.test.tsx +++ b/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.test.tsx @@ -100,7 +100,7 @@ describe('EntityCardBlueprint', () => { filter: 'test', }, }), - ).data(EntityCardBlueprint.dataRefs.filterExpression), + ).get(EntityCardBlueprint.dataRefs.filterExpression), ).toBe('test'); expect( @@ -112,7 +112,7 @@ describe('EntityCardBlueprint', () => { }, }), { config: { filter: 'test' } }, - ).data(EntityCardBlueprint.dataRefs.filterExpression), + ).get(EntityCardBlueprint.dataRefs.filterExpression), ).toBe('test'); expect( @@ -124,7 +124,7 @@ describe('EntityCardBlueprint', () => { loader: async () =>
    Test!
    , }, }), - ).data(EntityCardBlueprint.dataRefs.filterFunction), + ).get(EntityCardBlueprint.dataRefs.filterFunction), ).toBe(mockFilter); }); diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityContentBlueprint.test.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityContentBlueprint.test.tsx index dab6013bab..5d91f402de 100644 --- a/plugins/catalog-react/src/alpha/blueprints/EntityContentBlueprint.test.tsx +++ b/plugins/catalog-react/src/alpha/blueprints/EntityContentBlueprint.test.tsx @@ -122,10 +122,10 @@ describe('EntityContentBlueprint', () => { const tester = createExtensionTester(extension); // todo(blam): route paths are always set to / in the createExtensionTester. This will work eventually. - // expect(tester.data(coreExtensionData.routePath)).toBe('/test'); + // expect(tester.get(coreExtensionData.routePath)).toBe('/test'); - expect(tester.data(coreExtensionData.routeRef)).toBe(mockRouteRef); - expect(tester.data(EntityContentBlueprint.dataRefs.title)).toBe('Test'); + expect(tester.get(coreExtensionData.routeRef)).toBe(mockRouteRef); + expect(tester.get(EntityContentBlueprint.dataRefs.title)).toBe('Test'); }); it('should emit the correct filter output', () => { @@ -142,7 +142,7 @@ describe('EntityContentBlueprint', () => { filter: 'test', }, }), - ).data(EntityContentBlueprint.dataRefs.filterExpression), + ).get(EntityContentBlueprint.dataRefs.filterExpression), ).toBe('test'); expect( @@ -156,7 +156,7 @@ describe('EntityContentBlueprint', () => { }, }), { config: { filter: 'test' } }, - ).data(EntityContentBlueprint.dataRefs.filterExpression), + ).get(EntityContentBlueprint.dataRefs.filterExpression), ).toBe('test'); expect( @@ -170,7 +170,7 @@ describe('EntityContentBlueprint', () => { loader: async () =>
    Test!
    , }, }), - ).data(EntityContentBlueprint.dataRefs.filterFunction), + ).get(EntityContentBlueprint.dataRefs.filterFunction), ).toBe(mockFilter); }); From f149bfc4f8610c07f079fc31cf681030c4a2647d Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 15 Aug 2024 13:29:46 +0200 Subject: [PATCH 12/12] chore: remove the needless thing Signed-off-by: blam --- .../src/blueprints/SignInPageBlueprint.test.tsx | 6 ++---- .../src/app/createExtensionTester.test.tsx | 7 +++++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.test.tsx b/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.test.tsx index b45d8ead15..e6389da574 100644 --- a/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.test.tsx +++ b/packages/frontend-plugin-api/src/blueprints/SignInPageBlueprint.test.tsx @@ -62,11 +62,9 @@ describe('SignInPageBlueprint', () => { const tester = createExtensionTester(extension); - const Element = tester.get(SignInPageBlueprint.dataRefs.component); + const Component = tester.get(SignInPageBlueprint.dataRefs.component); - expect(Element).toBeDefined(); - - renderInTestApp( {}} />); + renderInTestApp( {}} />); await waitFor(() => { expect(screen.getByTestId('mock-sign-in')).toBeInTheDocument(); diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx b/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx index 13bdca1990..73220419b7 100644 --- a/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx +++ b/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx @@ -339,6 +339,9 @@ describe('createExtensionTester', () => { const extension = createExtension({ namespace: 'test', name: 'e1', + inputs: { + ignored: createExtensionInput([stringDataRef]), + }, attachTo: { id: 'ignored', input: 'ignored' }, output: [coreExtensionData.reactElement], factory: () => [coreExtensionData.reactElement(
    bob
    )], @@ -346,8 +349,8 @@ describe('createExtensionTester', () => { const extraExtension = createExtension({ namespace: 'test', - name: 'e1', - attachTo: { id: 'ignored', input: 'ignored' }, + name: 'e2', + attachTo: { id: 'test/e1', input: 'ignored' }, output: [stringDataRef, internalRef.optional()], factory: () => [stringDataRef('test-text')], });