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..503079ad00 --- /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 `@backstage/frontend-plugin-api`. 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..8495f961db 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,64 @@ 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 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 */} + {rootEl} + + + + + ); + + 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, 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..88bdcc6617 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,7 @@ describe('ExtensionBoundary', () => { const TextComponent = () => { return

{text}

; }; - await renderExtensionInTestApp(wrapInBoundaryExtension()); + createExtensionTester(wrapInBoundaryExtension()).render(); await waitFor(() => expect(screen.getByText(text)).toBeInTheDocument()); }); @@ -95,7 +63,7 @@ describe('ExtensionBoundary', () => { const ErrorComponent = () => { throw new Error(error); }; - await renderExtensionInTestApp(wrapInBoundaryExtension()); + createExtensionTester(wrapInBoundaryExtension()).render(); await waitFor(() => expect(screen.getByText(error)).toBeInTheDocument()); }); @@ -112,13 +80,13 @@ describe('ExtensionBoundary', () => { return null; }; - await renderExtensionInTestApp( + createExtensionTester( wrapInBoundaryExtension( , ), - ); + ).render(); await waitFor(() => expect(analyticsApiMock.getEvents()[0]).toMatchObject({ 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..50f853d9ed --- /dev/null +++ b/packages/frontend-test-utils/README.md @@ -0,0 +1,12 @@ +# @backstage/frontend-test-utils + +Contains utilities that can be used when testing frontend features such as extensions. + +## Installation + +Install the package via Yarn into your own packages: + +```sh +cd # if within a monorepo +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 new file mode 100644 index 0000000000..8d7837b90a --- /dev/null +++ b/packages/frontend-test-utils/api-report.md @@ -0,0 +1,30 @@ +## 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 }): RenderResult; +} +``` 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 diff --git a/packages/frontend-test-utils/package.json b/packages/frontend-test-utils/package.json new file mode 100644 index 0000000000..7e833016dd --- /dev/null +++ b/packages/frontend-test-utils/package.json @@ -0,0 +1,42 @@ +{ + "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" + ], + "dependencies": { + "@backstage/frontend-app-api": "workspace:^", + "@backstage/frontend-plugin-api": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@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/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..946dd3cb0c --- /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 () => { + 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 () => { + 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 () => { + expect(() => + createExtensionTester( + createExtension({ + id: 'test', + attachTo: { id: 'ignored', input: 'ignored' }, + disabled: true, + output: { path: coreExtensionData.routePath }, + factory: () => ({ path: '/foo' }), + }), + ).render(), + ).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 new file mode 100644 index 0000000000..34833dfc69 --- /dev/null +++ b/packages/frontend-test-utils/src/app/createExtensionTester.ts @@ -0,0 +1,114 @@ +/* + * 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 { 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 } from '@testing-library/react'; + +/** @public */ +export 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; + } + + render(options?: { config?: JsonObject }): RenderResult { + 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 = createSpecializedApp({ + features: [ + createPlugin({ + id: 'test', + extensions: this.#extensions.map(entry => entry.extension), + }), + ], + config: new MockConfigApi(finalConfig), + }); + + return render(app.createRoot()); + } +} + +/** @public */ +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..3152fac6e7 --- /dev/null +++ b/packages/frontend-test-utils/src/app/index.ts @@ -0,0 +1,20 @@ +/* + * 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, + type ExtensionTester, +} from './createExtensionTester'; diff --git a/packages/frontend-test-utils/src/index.ts b/packages/frontend-test-utils/src/index.ts new file mode 100644 index 0000000000..6a28cc2abe --- /dev/null +++ b/packages/frontend-test-utils/src/index.ts @@ -0,0 +1,23 @@ +/* + * 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. + */ + +/** + * @packageDocumentation + * + * Contains utilities that can be used when testing frontend features such as extensions. + */ + +export * from './app'; 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/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..e1a8166390 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()); + 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 0a5ad1d720..bce2a24505 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4354,6 +4354,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:^" @@ -4371,6 +4372,22 @@ __metadata: languageName: unknown linkType: soft +"@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: + "@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 + 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 + "@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" @@ -9117,6 +9134,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:^"