From 67972f83f90f5b988a87545568866e658af6303a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 15 Nov 2023 10:49:08 +0100 Subject: [PATCH 01/12] packages: new frontend-test-utils package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Vincenzo Scamporlino Co-authored-by: Camila Belo Co-authored-by: Fredrik Adelöw Signed-off-by: Patrik Oldsberg --- packages/frontend-test-utils/.eslintrc.js | 1 + packages/frontend-test-utils/README.md | 12 +++++++ packages/frontend-test-utils/package.json | 32 +++++++++++++++++++ packages/frontend-test-utils/src/index.ts | 17 ++++++++++ .../frontend-test-utils/src/setupTests.ts | 17 ++++++++++ yarn.lock | 9 ++++++ 6 files changed, 88 insertions(+) create mode 100644 packages/frontend-test-utils/.eslintrc.js create mode 100644 packages/frontend-test-utils/README.md create mode 100644 packages/frontend-test-utils/package.json create mode 100644 packages/frontend-test-utils/src/index.ts create mode 100644 packages/frontend-test-utils/src/setupTests.ts diff --git a/packages/frontend-test-utils/.eslintrc.js b/packages/frontend-test-utils/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/frontend-test-utils/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/frontend-test-utils/README.md b/packages/frontend-test-utils/README.md new file mode 100644 index 0000000000..b3325ac192 --- /dev/null +++ b/packages/frontend-test-utils/README.md @@ -0,0 +1,12 @@ +# @backstage/frontend-test-utils + +_This package was created through the Backstage CLI_. + +## Installation + +Install the package via Yarn: + +```sh +cd # if within a monorepo +yarn add @backstage/frontend-test-utils +``` diff --git a/packages/frontend-test-utils/package.json b/packages/frontend-test-utils/package.json new file mode 100644 index 0000000000..c7ce21161e --- /dev/null +++ b/packages/frontend-test-utils/package.json @@ -0,0 +1,32 @@ +{ + "name": "@backstage/frontend-test-utils", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "web-library" + }, + "sideEffects": false, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@testing-library/jest-dom": "^6.0.0" + }, + "files": [ + "dist" + ] +} diff --git a/packages/frontend-test-utils/src/index.ts b/packages/frontend-test-utils/src/index.ts new file mode 100644 index 0000000000..aa70772592 --- /dev/null +++ b/packages/frontend-test-utils/src/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 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. + */ + +export {}; diff --git a/packages/frontend-test-utils/src/setupTests.ts b/packages/frontend-test-utils/src/setupTests.ts new file mode 100644 index 0000000000..c30f1d15cb --- /dev/null +++ b/packages/frontend-test-utils/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 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 '@testing-library/jest-dom'; diff --git a/yarn.lock b/yarn.lock index 53e1959f8b..a2f0d1b8fd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4345,6 +4345,15 @@ __metadata: languageName: unknown linkType: soft +"@backstage/frontend-test-utils@workspace:packages/frontend-test-utils": + version: 0.0.0-use.local + resolution: "@backstage/frontend-test-utils@workspace:packages/frontend-test-utils" + dependencies: + "@backstage/cli": "workspace:^" + "@testing-library/jest-dom": ^6.0.0 + languageName: unknown + linkType: soft + "@backstage/integration-aws-node@workspace:^, @backstage/integration-aws-node@workspace:packages/integration-aws-node": version: 0.0.0-use.local resolution: "@backstage/integration-aws-node@workspace:packages/integration-aws-node" From 59fabd510671b17017867ff16c4e850698a17fb6 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 15 Nov 2023 12:24:01 +0100 Subject: [PATCH 02/12] frontend-test-utils: add createExtensionTester MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Vincenzo Scamporlino Co-authored-by: Camila Belo Co-authored-by: Fredrik Adelöw Signed-off-by: Patrik Oldsberg --- .changeset/angry-eagles-end.md | 5 + .changeset/bright-eyes-film.md | 5 + packages/frontend-test-utils/package.json | 9 +- .../src/app/createExtensionTester.ts | 117 ++++++++++++++++++ packages/frontend-test-utils/src/app/index.ts | 17 +++ packages/frontend-test-utils/src/index.ts | 2 +- yarn.lock | 5 + 7 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 .changeset/angry-eagles-end.md create mode 100644 .changeset/bright-eyes-film.md create mode 100644 packages/frontend-test-utils/src/app/createExtensionTester.ts create mode 100644 packages/frontend-test-utils/src/app/index.ts diff --git a/.changeset/angry-eagles-end.md b/.changeset/angry-eagles-end.md new file mode 100644 index 0000000000..eec1e7b710 --- /dev/null +++ b/.changeset/angry-eagles-end.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-test-utils': patch +--- + +Added `createExtensionTester` for rendering extensions in tests. diff --git a/.changeset/bright-eyes-film.md b/.changeset/bright-eyes-film.md new file mode 100644 index 0000000000..5c6211622a --- /dev/null +++ b/.changeset/bright-eyes-film.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-test-utils': minor +--- + +New testing utility library for `@backstage/frontend-app-api` and `@backstagr/frontend-plugin-api`. diff --git a/packages/frontend-test-utils/package.json b/packages/frontend-test-utils/package.json index c7ce21161e..064e996f82 100644 --- a/packages/frontend-test-utils/package.json +++ b/packages/frontend-test-utils/package.json @@ -28,5 +28,12 @@ }, "files": [ "dist" - ] + ], + "dependencies": { + "@backstage/frontend-app-api": "workspace:^", + "@backstage/frontend-plugin-api": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@backstage/types": "workspace:^", + "@testing-library/react": "^14.0.0" + } } diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.ts b/packages/frontend-test-utils/src/app/createExtensionTester.ts new file mode 100644 index 0000000000..211c7bb69a --- /dev/null +++ b/packages/frontend-test-utils/src/app/createExtensionTester.ts @@ -0,0 +1,117 @@ +/* + * Copyright 2023 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 { createApp } from '@backstage/frontend-app-api'; +import { Extension, createPlugin } from '@backstage/frontend-plugin-api'; +import { MockConfigApi } from '@backstage/test-utils'; +import { JsonArray, JsonObject, JsonValue } from '@backstage/types'; +import { RenderResult, render, waitFor } from '@testing-library/react'; + +class ExtensionTester { + /** @internal */ + static forSubject( + subject: Extension, + options?: { config?: TConfig }, + ): ExtensionTester { + const tester = new ExtensionTester(); + tester.add(subject, options); + return tester; + } + + readonly #extensions = new Array<{ + extension: Extension; + config?: JsonValue; + }>(); + + add( + extension: Extension, + options?: { config?: TConfig }, + ): ExtensionTester { + this.#extensions.push({ + extension, + config: options?.config as JsonValue, + }); + + return this; + } + + async render(options?: { config?: JsonObject }): Promise { + const { config = {} } = options ?? {}; + + const [subject, ...rest] = this.#extensions; + if (!subject) { + throw new Error( + 'No subject found. At least one extension should be added to the tester.', + ); + } + + const extensionsConfig: JsonArray = [ + ...rest.map(entry => ({ + [entry.extension.id]: { + config: entry.config, + }, + })), + { + [subject.extension.id]: { + attachTo: { id: 'core', input: 'root' }, + config: subject.config, + disabled: false, + }, + }, + { + 'core.layout': false, + }, + { + 'core.nav': false, + }, + { + 'core.routes': false, + }, + ]; + + const finalConfig = { + ...config, + app: { + ...(typeof config.app === 'object' ? config.app : undefined), + extensions: extensionsConfig, + }, + }; + + const app = createApp({ + features: [ + createPlugin({ + id: 'test', + extensions: this.#extensions.map(entry => entry.extension), + }), + ], + configLoader: async () => new MockConfigApi(finalConfig), + }); + + const result = render(app.createRoot()); + await waitFor(() => + // eslint-disable-next-line jest/no-standalone-expect + expect(result.queryByText('Loading...')).not.toBeInTheDocument(), + ); + return result; + } +} + +export function createExtensionTester( + subject: Extension, + options?: { config?: TConfig }, +): ExtensionTester { + return ExtensionTester.forSubject(subject, options); +} diff --git a/packages/frontend-test-utils/src/app/index.ts b/packages/frontend-test-utils/src/app/index.ts new file mode 100644 index 0000000000..caefbfd697 --- /dev/null +++ b/packages/frontend-test-utils/src/app/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 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. + */ + +export { createExtensionTester } from './createExtensionTester'; diff --git a/packages/frontend-test-utils/src/index.ts b/packages/frontend-test-utils/src/index.ts index aa70772592..6c49276721 100644 --- a/packages/frontend-test-utils/src/index.ts +++ b/packages/frontend-test-utils/src/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export {}; +export * from './app'; diff --git a/yarn.lock b/yarn.lock index a2f0d1b8fd..45a07bc83a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4350,7 +4350,12 @@ __metadata: resolution: "@backstage/frontend-test-utils@workspace:packages/frontend-test-utils" dependencies: "@backstage/cli": "workspace:^" + "@backstage/frontend-app-api": "workspace:^" + "@backstage/frontend-plugin-api": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@backstage/types": "workspace:^" "@testing-library/jest-dom": ^6.0.0 + "@testing-library/react": ^14.0.0 languageName: unknown linkType: soft From 37f4f98ba2af52bc097b7828e98613b9d771717a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 15 Nov 2023 12:29:54 +0100 Subject: [PATCH 03/12] search-react: use frontend-test-utils MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Vincenzo Scamporlino Co-authored-by: Camila Belo Co-authored-by: Fredrik Adelöw Signed-off-by: Patrik Oldsberg --- plugins/search-react/package.json | 1 + plugins/search-react/src/alpha.test.tsx | 70 +++++++------------------ yarn.lock | 1 + 3 files changed, 22 insertions(+), 50 deletions(-) diff --git a/plugins/search-react/package.json b/plugins/search-react/package.json index bef3865069..50f4de28ba 100644 --- a/plugins/search-react/package.json +++ b/plugins/search-react/package.json @@ -69,6 +69,7 @@ "@backstage/cli": "workspace:^", "@backstage/core-app-api": "workspace:^", "@backstage/frontend-app-api": "workspace:^", + "@backstage/frontend-test-utils": "workspace:^", "@backstage/test-utils": "workspace:^", "@testing-library/dom": "^9.0.0", "@testing-library/jest-dom": "^6.0.0", diff --git a/plugins/search-react/src/alpha.test.tsx b/plugins/search-react/src/alpha.test.tsx index 5222e4dad5..102ad35cb3 100644 --- a/plugins/search-react/src/alpha.test.tsx +++ b/plugins/search-react/src/alpha.test.tsx @@ -14,41 +14,28 @@ * limitations under the License. */ -import React from 'react'; - -import { render, screen } from '@testing-library/react'; - import { createExtensionInput, createPageExtension, - createPlugin, createSchemaFromZod, } from '@backstage/frontend-plugin-api'; +import { createExtensionTester } from '@backstage/frontend-test-utils'; import { SearchResult } from '@backstage/plugin-search-common'; -import { createApp } from '@backstage/frontend-app-api'; -import { MockConfigApi } from '@backstage/test-utils'; - +import { screen } from '@testing-library/react'; +import React from 'react'; import { BaseSearchResultListItemProps, createSearchResultListItemExtension, searchResultItemExtensionData as searchResultListItemExtensionData, } from './alpha'; -// TODO: Remove this mock when we have a permanent solution for nav items extensions -// The `GraphiQLIcon` used in "packages/frontend-app-api/src/extensions/CoreNav.tsx" file -// is throwing a "ReferenceError: ref is not defined" error during test -jest.mock('@backstage/plugin-graphiql', () => ({ - ...jest.requireActual('@backstage/plugin-graphiql'), - GraphiQLIcon: () => null, -})); - describe('createSearchResultListItemExtension', () => { it('Should use the correct result component', async () => { - type TechDocsSearchReasulListItemProps = BaseSearchResultListItemProps<{ + type TechDocsSearchResultListItemProps = BaseSearchResultListItemProps<{ lineClamp: number; }>; const TechDocsSearchResultItemComponent = ( - props: TechDocsSearchReasulListItemProps, + props: TechDocsSearchResultListItemProps, ) => (
TechDocs - Rank: {props.rank} - Line clamp: {props.lineClamp} @@ -60,10 +47,12 @@ describe('createSearchResultListItemExtension', () => { id: 'techdocs', attachTo: { id: 'plugin.search.page', input: 'items' }, configSchema: createSchemaFromZod(z => - z.object({ - noTrack: z.boolean().default(true), - lineClamp: z.number().default(5), - }), + z + .object({ + noTrack: z.boolean().default(true), + lineClamp: z.number().default(5), + }) + .default({}), ), predicate: result => result.type === 'techdocs', component: @@ -158,34 +147,15 @@ describe('createSearchResultListItemExtension', () => { }, }); - const SearchPlugin = createPlugin({ - id: 'search.plugin', - extensions: [ - SearchPageExtension, - ExploreSearchResultItemExtension, - TechDocsSearchResultItemExtension, - ], - }); - - const app = createApp({ - features: [SearchPlugin], - configLoader: async () => - new MockConfigApi({ - app: { - extensions: [ - { - 'plugin.search.result.item.techdocs': { - config: { - lineClamp: 3, - }, - }, - }, - ], - }, - }), - }); - - render(app.createRoot()); + await createExtensionTester(SearchPageExtension) + .add(TechDocsSearchResultItemExtension, { + // TODO(Rugvip): We need to make the config input type available for use here + config: { + lineClamp: 3, + } as any, + }) + .add(ExploreSearchResultItemExtension) + .render(); expect(await screen.findByText(/Search Page/)).toBeInTheDocument(); diff --git a/yarn.lock b/yarn.lock index 45a07bc83a..3121aa5326 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9105,6 +9105,7 @@ __metadata: "@backstage/core-plugin-api": "workspace:^" "@backstage/frontend-app-api": "workspace:^" "@backstage/frontend-plugin-api": "workspace:^" + "@backstage/frontend-test-utils": "workspace:^" "@backstage/plugin-search-common": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" From b8cb7804c83dd114fcb9162c7199b9cc7869c348 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 15 Nov 2023 13:03:59 +0100 Subject: [PATCH 04/12] frontend-app-api: split out and export createSpecializedApp Signed-off-by: Patrik Oldsberg --- .changeset/sixty-adults-kiss.md | 5 + packages/frontend-app-api/api-report.md | 9 ++ .../frontend-app-api/src/wiring/createApp.tsx | 109 +++++++++++------- packages/frontend-app-api/src/wiring/index.ts | 1 + 4 files changed, 84 insertions(+), 40 deletions(-) create mode 100644 .changeset/sixty-adults-kiss.md diff --git a/.changeset/sixty-adults-kiss.md b/.changeset/sixty-adults-kiss.md new file mode 100644 index 0000000000..eb73387308 --- /dev/null +++ b/.changeset/sixty-adults-kiss.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-app-api': patch +--- + +Added `createSpecializedApp`, which is a synchronous version of `createApp` where config and features already need to be loaded. diff --git a/packages/frontend-app-api/api-report.md b/packages/frontend-app-api/api-report.md index 3d6d936018..3ca51f6331 100644 --- a/packages/frontend-app-api/api-report.md +++ b/packages/frontend-app-api/api-report.md @@ -41,6 +41,15 @@ export function createApp(options?: { // @public (undocumented) export function createExtensionTree(options: { config: Config }): ExtensionTree; +// @public +export function createSpecializedApp(options?: { + features?: (BackstagePlugin | ExtensionOverrides)[]; + config?: ConfigApi; + bindRoutes?(context: { bind: AppRouteBinder }): void; +}): { + createRoot(): JSX_2.Element; +}; + // @public (undocumented) export interface ExtensionTree { // (undocumented) diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index ca0e1d60d1..dd20604e8c 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -244,49 +244,18 @@ export function createApp(options?: { const discoveredFeatures = getAvailableFeatures(config); const loadedFeatures = (await options?.featureLoader?.({ config })) ?? []; - const allFeatures = deduplicateFeatures([ - ...discoveredFeatures, - ...loadedFeatures, - ...(options?.features ?? []), - ]); - const tree = createAppTree({ - features: allFeatures, - builtinExtensions, + const app = createSpecializedApp({ config, - }); + features: [ + ...discoveredFeatures, + ...loadedFeatures, + ...(options?.features ?? []), + ], + bindRoutes: options?.bindRoutes, + }).createRoot(); - const appContext = createLegacyAppContext( - allFeatures.filter( - (f): f is BackstagePlugin => f.$$type === '@backstage/BackstagePlugin', - ), - ); - - const routeIds = collectRouteIds(allFeatures); - - const App = () => ( - - - - - {/* TODO: set base path using the logic from AppRouter */} - - {tree.root.instance!.getData(coreExtensionData.reactElement)} - - - - - - ); - - return { default: App }; + return { default: () => app }; } return { @@ -301,6 +270,66 @@ export function createApp(options?: { }; } +/** + * Synchronous version of {@link createApp}, expecting all features and + * config to have been loaded already. + * @public + */ +export function createSpecializedApp(options?: { + features?: (BackstagePlugin | ExtensionOverrides)[]; + config?: ConfigApi; + bindRoutes?(context: { bind: AppRouteBinder }): void; +}): { createRoot(): JSX.Element } { + const { + features: duplicatedFeatures = [], + config = new ConfigReader({}, 'empty-config'), + } = options ?? {}; + + const features = deduplicateFeatures(duplicatedFeatures); + + const tree = createAppTree({ + features, + builtinExtensions, + config, + }); + + const appContext = createLegacyAppContext( + features.filter( + (f): f is BackstagePlugin => f.$$type === '@backstage/BackstagePlugin', + ), + ); + + const routeIds = collectRouteIds(features); + + const App = () => ( + + + + + {/* TODO: set base path using the logic from AppRouter */} + + {tree.root.instance!.getData(coreExtensionData.reactElement)} + + + + + + ); + + return { + createRoot() { + return ; + }, + }; +} + // Make sure that we only convert each new plugin instance to its legacy equivalent once const legacyPluginStore = getOrCreateGlobalSingleton( 'legacy-plugin-compatibility-store', diff --git a/packages/frontend-app-api/src/wiring/index.ts b/packages/frontend-app-api/src/wiring/index.ts index dc1ba3bc13..412e643523 100644 --- a/packages/frontend-app-api/src/wiring/index.ts +++ b/packages/frontend-app-api/src/wiring/index.ts @@ -16,6 +16,7 @@ export { createApp, + createSpecializedApp, createExtensionTree, type ExtensionTreeNode, type ExtensionTree, From d69b0fcd2b29206d9ac592b421255703351bd245 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 15 Nov 2023 13:11:42 +0100 Subject: [PATCH 05/12] frontend-test-utils: refactor to use createSpecializedApp + API report Signed-off-by: Patrik Oldsberg --- packages/frontend-test-utils/api-report.md | 32 +++++++++++++++++++ packages/frontend-test-utils/package.json | 3 ++ .../src/app/createExtensionTester.ts | 19 +++++------ packages/frontend-test-utils/src/app/index.ts | 5 ++- yarn.lock | 4 ++- 5 files changed, 50 insertions(+), 13 deletions(-) create mode 100644 packages/frontend-test-utils/api-report.md diff --git a/packages/frontend-test-utils/api-report.md b/packages/frontend-test-utils/api-report.md new file mode 100644 index 0000000000..c5afb3204d --- /dev/null +++ b/packages/frontend-test-utils/api-report.md @@ -0,0 +1,32 @@ +## API Report File for "@backstage/frontend-test-utils" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { Extension } from '@backstage/frontend-plugin-api'; +import { JsonObject } from '@backstage/types'; +import { RenderResult } from '@testing-library/react'; + +// @public (undocumented) +export function createExtensionTester( + subject: Extension, + options?: { + config?: TConfig; + }, +): ExtensionTester; + +// @public (undocumented) +export class ExtensionTester { + // (undocumented) + add( + extension: Extension, + options?: { + config?: TConfig; + }, + ): ExtensionTester; + // (undocumented) + render(options?: { config?: JsonObject }): Promise; +} + +// (No @packageDocumentation comment for this package) +``` diff --git a/packages/frontend-test-utils/package.json b/packages/frontend-test-utils/package.json index 064e996f82..b3438aa15f 100644 --- a/packages/frontend-test-utils/package.json +++ b/packages/frontend-test-utils/package.json @@ -35,5 +35,8 @@ "@backstage/test-utils": "workspace:^", "@backstage/types": "workspace:^", "@testing-library/react": "^14.0.0" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0 || ^18.0.0" } } diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.ts b/packages/frontend-test-utils/src/app/createExtensionTester.ts index 211c7bb69a..c2d14d6a06 100644 --- a/packages/frontend-test-utils/src/app/createExtensionTester.ts +++ b/packages/frontend-test-utils/src/app/createExtensionTester.ts @@ -14,13 +14,14 @@ * limitations under the License. */ -import { createApp } from '@backstage/frontend-app-api'; +import { createSpecializedApp } from '@backstage/frontend-app-api'; import { Extension, createPlugin } from '@backstage/frontend-plugin-api'; import { MockConfigApi } from '@backstage/test-utils'; import { JsonArray, JsonObject, JsonValue } from '@backstage/types'; -import { RenderResult, render, waitFor } from '@testing-library/react'; +import { RenderResult, render } from '@testing-library/react'; -class ExtensionTester { +/** @public */ +export class ExtensionTester { /** @internal */ static forSubject( subject: Extension, @@ -90,25 +91,21 @@ class ExtensionTester { }, }; - const app = createApp({ + const app = createSpecializedApp({ features: [ createPlugin({ id: 'test', extensions: this.#extensions.map(entry => entry.extension), }), ], - configLoader: async () => new MockConfigApi(finalConfig), + config: new MockConfigApi(finalConfig), }); - const result = render(app.createRoot()); - await waitFor(() => - // eslint-disable-next-line jest/no-standalone-expect - expect(result.queryByText('Loading...')).not.toBeInTheDocument(), - ); - return result; + return render(app.createRoot()); } } +/** @public */ export function createExtensionTester( subject: Extension, options?: { config?: TConfig }, diff --git a/packages/frontend-test-utils/src/app/index.ts b/packages/frontend-test-utils/src/app/index.ts index caefbfd697..3152fac6e7 100644 --- a/packages/frontend-test-utils/src/app/index.ts +++ b/packages/frontend-test-utils/src/app/index.ts @@ -14,4 +14,7 @@ * limitations under the License. */ -export { createExtensionTester } from './createExtensionTester'; +export { + createExtensionTester, + type ExtensionTester, +} from './createExtensionTester'; diff --git a/yarn.lock b/yarn.lock index 3121aa5326..0bfa28f06c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4345,7 +4345,7 @@ __metadata: languageName: unknown linkType: soft -"@backstage/frontend-test-utils@workspace:packages/frontend-test-utils": +"@backstage/frontend-test-utils@workspace:^, @backstage/frontend-test-utils@workspace:packages/frontend-test-utils": version: 0.0.0-use.local resolution: "@backstage/frontend-test-utils@workspace:packages/frontend-test-utils" dependencies: @@ -4356,6 +4356,8 @@ __metadata: "@backstage/types": "workspace:^" "@testing-library/jest-dom": ^6.0.0 "@testing-library/react": ^14.0.0 + peerDependencies: + react: ^16.13.1 || ^17.0.0 || ^18.0.0 languageName: unknown linkType: soft From f07f6173c99759ed2b28fc04628322ce07be629a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 15 Nov 2023 13:11:59 +0100 Subject: [PATCH 06/12] frontend-test-utils: added tests for createExtensionTester Signed-off-by: Patrik Oldsberg --- .../src/app/createExtensionTester.test.tsx | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 packages/frontend-test-utils/src/app/createExtensionTester.test.tsx diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx b/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx new file mode 100644 index 0000000000..2c08b31837 --- /dev/null +++ b/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx @@ -0,0 +1,68 @@ +/* + * Copyright 2023 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 { + coreExtensionData, + createExtension, +} from '@backstage/frontend-plugin-api'; +import { screen } from '@testing-library/react'; +import React from 'react'; +import { createExtensionTester } from './createExtensionTester'; + +describe('createExtensionTester', () => { + it('should render a simple extension', async () => { + await createExtensionTester( + createExtension({ + id: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + output: { element: coreExtensionData.reactElement }, + factory: () => ({ element:
test
}), + }), + ).render(); + + await expect(screen.findByText('test')).resolves.toBeInTheDocument(); + }); + + it('should render an extension even if disabled by default', async () => { + await createExtensionTester( + createExtension({ + id: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + disabled: true, + output: { element: coreExtensionData.reactElement }, + factory: () => ({ element:
test
}), + }), + ).render(); + + await expect(screen.findByText('test')).resolves.toBeInTheDocument(); + }); + + it("should fail to render an extension that doesn't output a react element", async () => { + await expect( + createExtensionTester( + createExtension({ + id: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + disabled: true, + output: { path: coreExtensionData.routePath }, + factory: () => ({ path: '/foo' }), + }), + ).render(), + ).rejects.toThrow( + "Failed to instantiate extension 'core', input 'root' did not receive required extension data 'core.reactElement' from extension 'test'", + ); + }); +}); From 3f0f3bbf10adb6bd3aa43b6914e0115f451cbd2a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 15 Nov 2023 13:25:41 +0100 Subject: [PATCH 07/12] frontend-plugin-api: switch EXtensionBoundary test to use createExtensionTester Signed-off-by: Patrik Oldsberg --- packages/frontend-plugin-api/package.json | 1 + .../src/components/ExtensionBoundary.test.tsx | 50 ++++--------------- yarn.lock | 1 + 3 files changed, 13 insertions(+), 39 deletions(-) diff --git a/packages/frontend-plugin-api/package.json b/packages/frontend-plugin-api/package.json index d739cb8fdd..2a1d741d3d 100644 --- a/packages/frontend-plugin-api/package.json +++ b/packages/frontend-plugin-api/package.json @@ -25,6 +25,7 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@backstage/frontend-app-api": "workspace:^", + "@backstage/frontend-test-utils": "workspace:^", "@backstage/test-utils": "workspace:^", "@testing-library/jest-dom": "^6.0.0", "@testing-library/react": "^14.0.0", diff --git a/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx b/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx index 7b27c79360..483280dc3b 100644 --- a/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx +++ b/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx @@ -16,44 +16,12 @@ import React, { useEffect } from 'react'; import { screen, waitFor } from '@testing-library/react'; -import { - MockAnalyticsApi, - MockConfigApi, - TestApiProvider, - renderWithEffects, -} from '@backstage/test-utils'; +import { MockAnalyticsApi, TestApiProvider } from '@backstage/test-utils'; import { ExtensionBoundary } from './ExtensionBoundary'; -import { - Extension, - coreExtensionData, - createExtension, - createPlugin, -} from '../wiring'; +import { coreExtensionData, createExtension } from '../wiring'; import { analyticsApiRef, useAnalytics } from '@backstage/core-plugin-api'; -import { createApp } from '@backstage/frontend-app-api'; -import { JsonObject } from '@backstage/types'; import { createRouteRef } from '../routing'; - -function renderExtensionInTestApp( - extension: Extension, - options?: { - config?: JsonObject; - }, -) { - const { config = {} } = options ?? {}; - - const app = createApp({ - features: [ - createPlugin({ - id: 'plugin', - extensions: [extension], - }), - ], - configLoader: async () => new MockConfigApi(config), - }); - - return renderWithEffects(app.createRoot()); -} +import { createExtensionTester } from '@backstage/frontend-test-utils'; const wrapInBoundaryExtension = (element: JSX.Element) => { const id = 'plugin.extension'; @@ -86,7 +54,9 @@ describe('ExtensionBoundary', () => { const TextComponent = () => { return

{text}

; }; - await renderExtensionInTestApp(wrapInBoundaryExtension()); + await createExtensionTester( + wrapInBoundaryExtension(), + ).render(); await waitFor(() => expect(screen.getByText(text)).toBeInTheDocument()); }); @@ -95,7 +65,9 @@ describe('ExtensionBoundary', () => { const ErrorComponent = () => { throw new Error(error); }; - await renderExtensionInTestApp(wrapInBoundaryExtension()); + await createExtensionTester( + wrapInBoundaryExtension(), + ).render(); await waitFor(() => expect(screen.getByText(error)).toBeInTheDocument()); }); @@ -112,13 +84,13 @@ describe('ExtensionBoundary', () => { return null; }; - await renderExtensionInTestApp( + await createExtensionTester( wrapInBoundaryExtension( , ), - ); + ).render(); await waitFor(() => expect(analyticsApiMock.getEvents()[0]).toMatchObject({ diff --git a/yarn.lock b/yarn.lock index 0bfa28f06c..82375d0b30 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4328,6 +4328,7 @@ __metadata: "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/frontend-app-api": "workspace:^" + "@backstage/frontend-test-utils": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/types": "workspace:^" "@backstage/version-bridge": "workspace:^" From 326a9e40e10d5c740dbb9d8cf3230ed1a814026e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 15 Nov 2023 13:43:33 +0100 Subject: [PATCH 08/12] frontend-test-utils: switch createExtensionTester render to be sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: Camila Belo Co-authored-by: Philipp Hugenroth Signed-off-by: Patrik Oldsberg --- .../src/components/ExtensionBoundary.test.tsx | 10 +++------- packages/frontend-test-utils/api-report.md | 2 +- .../src/app/createExtensionTester.test.tsx | 8 ++++---- .../src/app/createExtensionTester.ts | 2 +- plugins/search-react/src/alpha.test.tsx | 2 +- 5 files changed, 10 insertions(+), 14 deletions(-) diff --git a/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx b/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx index 483280dc3b..88bdcc6617 100644 --- a/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx +++ b/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx @@ -54,9 +54,7 @@ describe('ExtensionBoundary', () => { const TextComponent = () => { return

{text}

; }; - await createExtensionTester( - wrapInBoundaryExtension(), - ).render(); + createExtensionTester(wrapInBoundaryExtension()).render(); await waitFor(() => expect(screen.getByText(text)).toBeInTheDocument()); }); @@ -65,9 +63,7 @@ describe('ExtensionBoundary', () => { const ErrorComponent = () => { throw new Error(error); }; - await createExtensionTester( - wrapInBoundaryExtension(), - ).render(); + createExtensionTester(wrapInBoundaryExtension()).render(); await waitFor(() => expect(screen.getByText(error)).toBeInTheDocument()); }); @@ -84,7 +80,7 @@ describe('ExtensionBoundary', () => { return null; }; - await createExtensionTester( + createExtensionTester( wrapInBoundaryExtension( diff --git a/packages/frontend-test-utils/api-report.md b/packages/frontend-test-utils/api-report.md index c5afb3204d..2703efa228 100644 --- a/packages/frontend-test-utils/api-report.md +++ b/packages/frontend-test-utils/api-report.md @@ -25,7 +25,7 @@ export class ExtensionTester { }, ): ExtensionTester; // (undocumented) - render(options?: { config?: JsonObject }): Promise; + render(options?: { config?: JsonObject }): RenderResult; } // (No @packageDocumentation comment for this package) diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx b/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx index 2c08b31837..946dd3cb0c 100644 --- a/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx +++ b/packages/frontend-test-utils/src/app/createExtensionTester.test.tsx @@ -24,7 +24,7 @@ import { createExtensionTester } from './createExtensionTester'; describe('createExtensionTester', () => { it('should render a simple extension', async () => { - await createExtensionTester( + createExtensionTester( createExtension({ id: 'test', attachTo: { id: 'ignored', input: 'ignored' }, @@ -37,7 +37,7 @@ describe('createExtensionTester', () => { }); it('should render an extension even if disabled by default', async () => { - await createExtensionTester( + createExtensionTester( createExtension({ id: 'test', attachTo: { id: 'ignored', input: 'ignored' }, @@ -51,7 +51,7 @@ describe('createExtensionTester', () => { }); it("should fail to render an extension that doesn't output a react element", async () => { - await expect( + expect(() => createExtensionTester( createExtension({ id: 'test', @@ -61,7 +61,7 @@ describe('createExtensionTester', () => { factory: () => ({ path: '/foo' }), }), ).render(), - ).rejects.toThrow( + ).toThrow( "Failed to instantiate extension 'core', input 'root' did not receive required extension data 'core.reactElement' from extension 'test'", ); }); diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.ts b/packages/frontend-test-utils/src/app/createExtensionTester.ts index c2d14d6a06..34833dfc69 100644 --- a/packages/frontend-test-utils/src/app/createExtensionTester.ts +++ b/packages/frontend-test-utils/src/app/createExtensionTester.ts @@ -49,7 +49,7 @@ export class ExtensionTester { return this; } - async render(options?: { config?: JsonObject }): Promise { + render(options?: { config?: JsonObject }): RenderResult { const { config = {} } = options ?? {}; const [subject, ...rest] = this.#extensions; diff --git a/plugins/search-react/src/alpha.test.tsx b/plugins/search-react/src/alpha.test.tsx index 102ad35cb3..e1a8166390 100644 --- a/plugins/search-react/src/alpha.test.tsx +++ b/plugins/search-react/src/alpha.test.tsx @@ -147,7 +147,7 @@ describe('createSearchResultListItemExtension', () => { }, }); - await createExtensionTester(SearchPageExtension) + createExtensionTester(SearchPageExtension) .add(TechDocsSearchResultItemExtension, { // TODO(Rugvip): We need to make the config input type available for use here config: { From 2331e2a66b842c4862fee777bb52d9c8dd47805f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 15 Nov 2023 13:46:21 +0100 Subject: [PATCH 09/12] frontend-test-utils: README + doc cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Camila Belo Co-authored-by: Fredrik Adelöw Co-authored-by: Philipp Hugenroth Signed-off-by: Patrik Oldsberg --- .changeset/bright-eyes-film.md | 2 +- packages/frontend-test-utils/README.md | 6 +++--- packages/frontend-test-utils/api-report.md | 2 -- packages/frontend-test-utils/src/index.ts | 6 ++++++ 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.changeset/bright-eyes-film.md b/.changeset/bright-eyes-film.md index 5c6211622a..503079ad00 100644 --- a/.changeset/bright-eyes-film.md +++ b/.changeset/bright-eyes-film.md @@ -2,4 +2,4 @@ '@backstage/frontend-test-utils': minor --- -New testing utility library for `@backstage/frontend-app-api` and `@backstagr/frontend-plugin-api`. +New testing utility library for `@backstage/frontend-app-api` and `@backstage/frontend-plugin-api`. diff --git a/packages/frontend-test-utils/README.md b/packages/frontend-test-utils/README.md index b3325ac192..50f853d9ed 100644 --- a/packages/frontend-test-utils/README.md +++ b/packages/frontend-test-utils/README.md @@ -1,12 +1,12 @@ # @backstage/frontend-test-utils -_This package was created through the Backstage CLI_. +Contains utilities that can be used when testing frontend features such as extensions. ## Installation -Install the package via Yarn: +Install the package via Yarn into your own packages: ```sh cd # if within a monorepo -yarn add @backstage/frontend-test-utils +yarn add --dev @backstage/frontend-test-utils ``` diff --git a/packages/frontend-test-utils/api-report.md b/packages/frontend-test-utils/api-report.md index 2703efa228..8d7837b90a 100644 --- a/packages/frontend-test-utils/api-report.md +++ b/packages/frontend-test-utils/api-report.md @@ -27,6 +27,4 @@ export class ExtensionTester { // (undocumented) render(options?: { config?: JsonObject }): RenderResult; } - -// (No @packageDocumentation comment for this package) ``` diff --git a/packages/frontend-test-utils/src/index.ts b/packages/frontend-test-utils/src/index.ts index 6c49276721..6a28cc2abe 100644 --- a/packages/frontend-test-utils/src/index.ts +++ b/packages/frontend-test-utils/src/index.ts @@ -14,4 +14,10 @@ * limitations under the License. */ +/** + * @packageDocumentation + * + * Contains utilities that can be used when testing frontend features such as extensions. + */ + export * from './app'; From 48d53d1d344ff900a0255c5ab227d161e5519fdd Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 15 Nov 2023 13:47:03 +0100 Subject: [PATCH 10/12] frontend-app-api: refactor to resolve app info ahead of render MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: Camila Belo Co-authored-by: Philipp Hugenroth Signed-off-by: Patrik Oldsberg --- .../frontend-app-api/src/wiring/createApp.tsx | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx index dd20604e8c..8495f961db 100644 --- a/packages/frontend-app-api/src/wiring/createApp.tsx +++ b/packages/frontend-app-api/src/wiring/createApp.tsx @@ -299,24 +299,22 @@ export function createSpecializedApp(options?: { ), ); - const routeIds = collectRouteIds(features); + const apiHolder = createApiHolder(tree, config); + const routeInfo = extractRouteInfoFromAppNode(tree.root); + const routeBindings = resolveRouteBindings( + options?.bindRoutes, + config, + collectRouteIds(features), + ); + const rootEl = tree.root.instance!.getData(coreExtensionData.reactElement); const App = () => ( - + - + {/* TODO: set base path using the logic from AppRouter */} - - {tree.root.instance!.getData(coreExtensionData.reactElement)} - + {rootEl} From 5fb8bdec0c158cad18c1bed5a3bec837f9540d64 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 15 Nov 2023 13:48:33 +0100 Subject: [PATCH 11/12] frontend-test-utils: move TLR to peer dep with wider range MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: Camila Belo Co-authored-by: Philipp Hugenroth Signed-off-by: Patrik Oldsberg --- packages/frontend-test-utils/package.json | 4 ++-- yarn.lock | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/frontend-test-utils/package.json b/packages/frontend-test-utils/package.json index b3438aa15f..7e833016dd 100644 --- a/packages/frontend-test-utils/package.json +++ b/packages/frontend-test-utils/package.json @@ -33,10 +33,10 @@ "@backstage/frontend-app-api": "workspace:^", "@backstage/frontend-plugin-api": "workspace:^", "@backstage/test-utils": "workspace:^", - "@backstage/types": "workspace:^", - "@testing-library/react": "^14.0.0" + "@backstage/types": "workspace:^" }, "peerDependencies": { + "@testing-library/react": "^12.1.3 || ^13.0.0 || ^14.0.0", "react": "^16.13.1 || ^17.0.0 || ^18.0.0" } } diff --git a/yarn.lock b/yarn.lock index 82375d0b30..c030b13f91 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4356,8 +4356,8 @@ __metadata: "@backstage/test-utils": "workspace:^" "@backstage/types": "workspace:^" "@testing-library/jest-dom": ^6.0.0 - "@testing-library/react": ^14.0.0 peerDependencies: + "@testing-library/react": ^12.1.3 || ^13.0.0 || ^14.0.0 react: ^16.13.1 || ^17.0.0 || ^18.0.0 languageName: unknown linkType: soft From 050fad198b7042ec013fb15e88f88fa7a81d8f5f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 15 Nov 2023 14:42:39 +0100 Subject: [PATCH 12/12] frontend-test-api: add missing catalog-info.yaml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Camila Belo Co-authored-by: Vincenzo Scamporlino Co-authored-by: Fredrik Adelöw Co-authored-by: Philipp Hugenroth Signed-off-by: Patrik Oldsberg --- packages/frontend-test-utils/catalog-info.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 packages/frontend-test-utils/catalog-info.yaml diff --git a/packages/frontend-test-utils/catalog-info.yaml b/packages/frontend-test-utils/catalog-info.yaml new file mode 100644 index 0000000000..e2d2a57897 --- /dev/null +++ b/packages/frontend-test-utils/catalog-info.yaml @@ -0,0 +1,9 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-frontend-test-utils + title: '@backstage/frontend-test-utils' +spec: + lifecycle: experimental + type: backstage-web-library + owner: maintainers