Add support for external refs renderInTestApp

Signed-off-by: Dylan O'Gara <dogara@webstaurantstore.com>
This commit is contained in:
Dylan O'Gara
2026-04-27 14:04:03 -04:00
parent ca53b86336
commit fa363f9d3c
5 changed files with 100 additions and 8 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/frontend-test-utils': patch
---
Added support for `ExternalRouteRef` in the `mountedRoutes` option of `renderInTestApp` and `renderTestApp`.
+3 -2
View File
@@ -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<TApiPairs extends any[] = any[]> = {
features?: FrontendFeature[];
initialRouteEntries?: string[];
mountedRoutes?: {
[path: string]: RouteRef;
[path: string]: RouteRef | ExternalRouteRef;
};
apis?: readonly [...TestApiPairs<TApiPairs>];
};
@@ -473,7 +474,7 @@ export type TestApiProviderProps<TApiPairs extends any[]> = {
// @public
export type TestAppOptions<TApiPairs extends any[] = any[]> = {
mountedRoutes?: {
[path: string]: RouteRef;
[path: string]: RouteRef | ExternalRouteRef;
};
config?: JsonObject;
features?: FrontendFeature[];
@@ -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 <div>Link: {link?.({ name: 'test-name' }) ?? 'none'}</div>;
};
renderInTestApp(<LinkComponent />, {
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 <div>Link: {link?.({ name: 'test' }) ?? 'none'}</div>;
};
renderInTestApp(<ExternalLinkComponent />, {
mountedRoutes: {
'/items/:name': externalRef,
},
});
expect(screen.getByText('Link: /items/test')).toBeInTheDocument();
});
});
@@ -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<TApiPairs extends any[] = any[]> = {
* 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 TApiPairs extends any[] = any[]>(
}),
];
const externalBindings = new Map<ExternalRouteRef, RouteRef>();
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<const TApiPairs extends any[] = any[]>(
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(
@@ -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<TApiPairs extends any[] = any[]> = {
* })
* ```
*/
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<const TApiPairs extends any[] = any[]>(
): RenderResult {
const extensions = [...(options?.extensions ?? [])];
const externalBindings = new Map<ExternalRouteRef, RouteRef>();
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<const TApiPairs extends any[] = any[]>(
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(