diff --git a/.changeset/eager-wolves-enjoy.md b/.changeset/eager-wolves-enjoy.md new file mode 100644 index 0000000000..7807cfafd2 --- /dev/null +++ b/.changeset/eager-wolves-enjoy.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-test-utils': patch +--- + +Added support for `ExternalRouteRef` in the `mountedRoutes` option of `renderInTestApp` and `renderTestApp`. diff --git a/packages/frontend-test-utils/report.api.md b/packages/frontend-test-utils/report.api.md index 6d9f8964b1..d24046953f 100644 --- a/packages/frontend-test-utils/report.api.md +++ b/packages/frontend-test-utils/report.api.md @@ -26,6 +26,7 @@ import { EvaluatePermissionResponse } from '@backstage/plugin-permission-common' import { ExtensionDataRef } from '@backstage/frontend-plugin-api'; import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; import { ExtensionDefinitionParameters } from '@backstage/frontend-plugin-api'; +import { ExternalRouteRef } from '@backstage/frontend-plugin-api'; import { FeatureFlag } from '@backstage/frontend-plugin-api'; import { FeatureFlagsApi } from '@backstage/frontend-plugin-api'; import { FeatureFlagsSaveOptions } from '@backstage/frontend-plugin-api'; @@ -444,7 +445,7 @@ export type RenderTestAppOptions = { features?: FrontendFeature[]; initialRouteEntries?: string[]; mountedRoutes?: { - [path: string]: RouteRef; + [path: string]: RouteRef | ExternalRouteRef; }; apis?: readonly [...TestApiPairs]; }; @@ -473,7 +474,7 @@ export type TestApiProviderProps = { // @public export type TestAppOptions = { mountedRoutes?: { - [path: string]: RouteRef; + [path: string]: RouteRef | ExternalRouteRef; }; config?: JsonObject; features?: FrontendFeature[]; diff --git a/packages/frontend-test-utils/src/app/renderInTestApp.test.tsx b/packages/frontend-test-utils/src/app/renderInTestApp.test.tsx index c9d90715f6..aa3236be86 100644 --- a/packages/frontend-test-utils/src/app/renderInTestApp.test.tsx +++ b/packages/frontend-test-utils/src/app/renderInTestApp.test.tsx @@ -17,7 +17,12 @@ import { useCallback } from 'react'; import { screen, fireEvent } from '@testing-library/react'; import { mockApis, TestApiProvider } from '@backstage/frontend-test-utils'; -import { useAnalytics } from '@backstage/frontend-plugin-api'; +import { + useAnalytics, + createRouteRef, + createExternalRouteRef, + useRouteRef, +} from '@backstage/frontend-plugin-api'; import { Routes, Route } from 'react-router-dom'; import { renderInTestApp } from './renderInTestApp'; @@ -108,4 +113,40 @@ describe('renderInTestApp', () => { ]), ); }); + + it('should allow mounting route refs', () => { + const testRouteRef = createRouteRef({ + params: ['name'], + }); + + const LinkComponent = () => { + const link = useRouteRef(testRouteRef); + return
Link: {link?.({ name: 'test-name' }) ?? 'none'}
; + }; + + renderInTestApp(, { + mountedRoutes: { + '/test-path/:name': testRouteRef, + }, + }); + + expect(screen.getByText('Link: /test-path/test-name')).toBeInTheDocument(); + }); + + it('should allow mounting external route refs', () => { + const externalRef = createExternalRouteRef({ params: ['name'] }); + + const ExternalLinkComponent = () => { + const link = useRouteRef(externalRef); + return
Link: {link?.({ name: 'test' }) ?? 'none'}
; + }; + + renderInTestApp(, { + mountedRoutes: { + '/items/:name': externalRef, + }, + }); + + expect(screen.getByText('Link: /items/test')).toBeInTheDocument(); + }); }); diff --git a/packages/frontend-test-utils/src/app/renderInTestApp.tsx b/packages/frontend-test-utils/src/app/renderInTestApp.tsx index bdb9929635..935e479f9c 100644 --- a/packages/frontend-test-utils/src/app/renderInTestApp.tsx +++ b/packages/frontend-test-utils/src/app/renderInTestApp.tsx @@ -32,6 +32,8 @@ import { FrontendFeature, createFrontendModule, createApiFactory, + createRouteRef, + ExternalRouteRef, type ApiRef, } from '@backstage/frontend-plugin-api'; import { RouterBlueprint } from '@backstage/plugin-app-react'; @@ -40,6 +42,7 @@ import { getMockApiFactory } from '../apis/MockWithApiFactory'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import type { CreateSpecializedAppInternalOptions } from '../../../frontend-app-api/src/wiring/createSpecializedApp'; import { TestApiPairs } from '../apis/TestApiProvider'; +import { OpaqueExternalRouteRef } from '@internal/frontend'; const DEFAULT_MOCK_CONFIG = { app: { baseUrl: 'http://localhost:3000' }, @@ -67,7 +70,7 @@ export type TestAppOptions = { * const link = useRouteRef(myRouteRef) * ``` */ - mountedRoutes?: { [path: string]: RouteRef }; + mountedRoutes?: { [path: string]: RouteRef | ExternalRouteRef }; /** * Additional configuration passed to the app when rendering elements inside it. @@ -180,9 +183,20 @@ export function renderInTestApp( }), ]; + const externalBindings = new Map(); + if (options?.mountedRoutes) { - for (const [path, routeRef] of Object.entries(options.mountedRoutes)) { - // TODO(Rugvip): add support for external route refs + for (const [path, optionRef] of Object.entries(options.mountedRoutes)) { + let routeRef: RouteRef; + + if (OpaqueExternalRouteRef.isType(optionRef)) { + // Create an actual route ref for the external route, then bind the external ref to it + routeRef = createRouteRef(); + externalBindings.set(optionRef, routeRef); + } else { + routeRef = optionRef; + } + extensions.push( createExtension({ kind: 'test-route', @@ -253,6 +267,14 @@ export function renderInTestApp( return createApiFactory(apiRef, implementation); }), }, + bindRoutes: + externalBindings.size > 0 + ? ({ bind }) => { + for (const [externalRef, targetRef] of externalBindings) { + bind({ ref: externalRef }, { ref: targetRef }); + } + } + : undefined, } as CreateSpecializedAppInternalOptions).finalize(); return render( diff --git a/packages/frontend-test-utils/src/app/renderTestApp.tsx b/packages/frontend-test-utils/src/app/renderTestApp.tsx index 7708fa4220..d459f29ac1 100644 --- a/packages/frontend-test-utils/src/app/renderTestApp.tsx +++ b/packages/frontend-test-utils/src/app/renderTestApp.tsx @@ -25,6 +25,8 @@ import { ExtensionDefinition, FrontendFeature, RouteRef, + ExternalRouteRef, + createRouteRef, type ApiRef, } from '@backstage/frontend-plugin-api'; import { render, type RenderResult } from '@testing-library/react'; @@ -37,6 +39,7 @@ import { getMockApiFactory } from '../apis/MockWithApiFactory'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import type { CreateSpecializedAppInternalOptions } from '../../../frontend-app-api/src/wiring/createSpecializedApp'; import { TestApiPairs } from '../apis/TestApiProvider'; +import { OpaqueExternalRouteRef } from '@internal/frontend'; const DEFAULT_MOCK_CONFIG = { app: { baseUrl: 'http://localhost:3000' }, @@ -83,7 +86,7 @@ export type RenderTestAppOptions = { * }) * ``` */ - mountedRoutes?: { [path: string]: RouteRef }; + mountedRoutes?: { [path: string]: RouteRef | ExternalRouteRef }; /** * API overrides to provide to the test app. Use `mockApis` helpers @@ -121,8 +124,20 @@ export function renderTestApp( ): RenderResult { const extensions = [...(options?.extensions ?? [])]; + const externalBindings = new Map(); + if (options?.mountedRoutes) { - for (const [path, routeRef] of Object.entries(options.mountedRoutes)) { + for (const [path, optionRef] of Object.entries(options.mountedRoutes)) { + let routeRef: RouteRef; + + if (OpaqueExternalRouteRef.isType(optionRef)) { + // Create an actual route ref for the external route, then bind the external ref to it + routeRef = createRouteRef(); + externalBindings.set(optionRef, routeRef); + } else { + routeRef = optionRef; + } + extensions.push( createExtension({ kind: 'test-route', @@ -193,6 +208,14 @@ export function renderTestApp( return createApiFactory(apiRef, implementation); }), }, + bindRoutes: + externalBindings.size > 0 + ? ({ bind }) => { + for (const [externalRef, targetRef] of externalBindings) { + bind({ ref: externalRef }, { ref: targetRef }); + } + } + : undefined, } as CreateSpecializedAppInternalOptions).finalize(); return render(