Merge pull request #4407 from backstage/rugvip/testrr
core,test-util: fix type inference of createRouteRef + add mountedRoutes option to wrapInTestApp
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/test-utils': patch
|
||||
---
|
||||
|
||||
Added `mountedRoutes` option to `wrapInTestApp`, allowing routes to be associated to concrete paths to make `useRouteRef` usable in tested components.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core': patch
|
||||
---
|
||||
|
||||
Fixed type inference of `createRouteRef`.
|
||||
@@ -14,7 +14,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { RouteRefConfig, RouteRef } from './types';
|
||||
import { RouteRef } from './types';
|
||||
import { IconComponent } from '../icons';
|
||||
|
||||
export type RouteRefConfig<Params extends { [param in string]: string }> = {
|
||||
params?: Array<keyof Params>;
|
||||
/** @deprecated Route refs no longer decide their own path */
|
||||
path?: string;
|
||||
icon?: IconComponent;
|
||||
title: string;
|
||||
};
|
||||
|
||||
export class AbsoluteRouteRef<Params extends { [param in string]: string }> {
|
||||
constructor(private readonly config: RouteRefConfig<Params>) {}
|
||||
@@ -38,9 +47,20 @@ export class AbsoluteRouteRef<Params extends { [param in string]: string }> {
|
||||
}
|
||||
|
||||
export function createRouteRef<
|
||||
ParamKeys extends string,
|
||||
Params extends { [param in string]: string } = { [name in ParamKeys]: string }
|
||||
>(config: RouteRefConfig<Params>): RouteRef<Params> {
|
||||
// Params is the type that we care about and the one to be embedded in the route ref.
|
||||
// For example, given the params ['name', 'kind'], Params will be {name: string, kind: string}
|
||||
Params extends { [param in ParamKey]: string },
|
||||
// ParamKey is here to make sure the Params type properly has its keys narrowed down
|
||||
// to only the elements of params. Defaulting to never makes sure we end up with
|
||||
// Param = {} if the params array is empty.
|
||||
ParamKey extends string = never
|
||||
>(config: {
|
||||
params?: ParamKey[];
|
||||
/** @deprecated Route refs no longer decide their own path */
|
||||
path?: string;
|
||||
icon?: IconComponent;
|
||||
title: string;
|
||||
}): RouteRef<Params> {
|
||||
return new AbsoluteRouteRef<Params>(config);
|
||||
}
|
||||
|
||||
|
||||
@@ -39,8 +39,9 @@ import {
|
||||
createRouteRef,
|
||||
createExternalRouteRef,
|
||||
ExternalRouteRef,
|
||||
RouteRefConfig,
|
||||
} from './RouteRef';
|
||||
import { RouteRef, RouteRefConfig } from './types';
|
||||
import { RouteRef } from './types';
|
||||
|
||||
const mockConfig = (extra?: Partial<RouteRefConfig<{}>>) => ({
|
||||
path: '/unused',
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
|
||||
export type {
|
||||
RouteRef,
|
||||
RouteRefConfig,
|
||||
AbsoluteRouteRef,
|
||||
ConcreteRoute,
|
||||
MutableRouteRef,
|
||||
} from './types';
|
||||
export { FlatRoutes } from './FlatRoutes';
|
||||
export { createRouteRef } from './RouteRef';
|
||||
export type { RouteRefConfig } from './RouteRef';
|
||||
export { useRouteRef } from './hooks';
|
||||
|
||||
@@ -45,14 +45,6 @@ export type AbsoluteRouteRef = RouteRef<{}>;
|
||||
*/
|
||||
export type MutableRouteRef = RouteRef<{}>;
|
||||
|
||||
export type RouteRefConfig<Params extends { [param in string]: string }> = {
|
||||
params?: Array<keyof Params>;
|
||||
/** @deprecated Route refs no longer decide their own path */
|
||||
path?: string;
|
||||
icon?: IconComponent;
|
||||
title: string;
|
||||
};
|
||||
|
||||
// A duplicate of the react-router RouteObject, but with routeRef added
|
||||
export interface BackstageRouteObject {
|
||||
caseSensitive: boolean;
|
||||
|
||||
@@ -21,9 +21,11 @@ import { Route, Routes } from 'react-router';
|
||||
import { withLogCollector } from '@backstage/test-utils-core';
|
||||
import {
|
||||
useApi,
|
||||
useRouteRef,
|
||||
errorApiRef,
|
||||
ApiProvider,
|
||||
ApiRegistry,
|
||||
createRouteRef,
|
||||
} from '@backstage/core-api';
|
||||
import { MockErrorApi } from './apis';
|
||||
|
||||
@@ -113,4 +115,29 @@ describe('wrapInTestApp', () => {
|
||||
expect(rendered.getByText('foo')).toBeInTheDocument();
|
||||
expect(mockErrorApi.getErrors()).toEqual([{ error: new Error('NOPE') }]);
|
||||
});
|
||||
|
||||
it('should allow route refs to be mounted on specific paths', async () => {
|
||||
const aRouteRef = createRouteRef({ title: 'A' });
|
||||
const bRouteRef = createRouteRef({ title: 'B', params: ['name'] });
|
||||
|
||||
const MyComponent = () => {
|
||||
const a = useRouteRef(aRouteRef);
|
||||
const b = useRouteRef(bRouteRef);
|
||||
return (
|
||||
<div>
|
||||
<div>Link A: {a()}</div>
|
||||
<div>Link B: {b({ name: 'x' })}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const rendered = await renderInTestApp(<MyComponent />, {
|
||||
mountedRoutes: {
|
||||
'/my-a-path': aRouteRef,
|
||||
'/my-b-path/:name': bRouteRef,
|
||||
},
|
||||
});
|
||||
expect(rendered.getByText('Link A: /my-a-path')).toBeInTheDocument();
|
||||
expect(rendered.getByText('Link B: /my-b-path/x')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,6 +21,9 @@ import { lightTheme } from '@backstage/theme';
|
||||
import privateExports, {
|
||||
defaultSystemIcons,
|
||||
BootErrorPageProps,
|
||||
RouteRef,
|
||||
createPlugin,
|
||||
createRoutableExtension,
|
||||
} from '@backstage/core-api';
|
||||
import { RenderResult } from '@testing-library/react';
|
||||
import { renderWithEffects } from '@backstage/test-utils-core';
|
||||
@@ -44,6 +47,22 @@ type TestAppOptions = {
|
||||
* Initial route entries to pass along as `initialEntries` to the router.
|
||||
*/
|
||||
routeEntries?: string[];
|
||||
|
||||
/**
|
||||
* An object of paths to mount route ref on, with the key being the path and the value
|
||||
* being the RouteRef that the path will be bound to. This allows the route refs to be
|
||||
* used by `useRouteRef` in the rendered elements.
|
||||
*
|
||||
* @example
|
||||
* wrapInTestApp(<MyComponent />, {
|
||||
* mountedRoutes: {
|
||||
* '/my-path': myRouteRef,
|
||||
* }
|
||||
* })
|
||||
* // ...
|
||||
* const link = useRouteRef(myRouteRef)
|
||||
*/
|
||||
mountedRoutes?: { [path: string]: RouteRef };
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -90,12 +109,27 @@ export function wrapInTestApp(
|
||||
Wrapper = () => Component as React.ReactElement;
|
||||
}
|
||||
|
||||
const routePlugin = createPlugin({ id: 'mock-route-plugin' });
|
||||
const routeElements = Object.entries(options.mountedRoutes ?? {}).map(
|
||||
([path, routeRef]) => {
|
||||
const PageComponent = () => <div>Mounted at {path}</div>;
|
||||
const Page = routePlugin.provide(
|
||||
createRoutableExtension({
|
||||
component: async () => PageComponent,
|
||||
mountPoint: routeRef,
|
||||
}),
|
||||
);
|
||||
return <Route key={path} path={path} element={<Page />} />;
|
||||
},
|
||||
);
|
||||
|
||||
const AppProvider = app.getProvider();
|
||||
const AppRouter = app.getRouter();
|
||||
|
||||
return (
|
||||
<AppProvider>
|
||||
<AppRouter>
|
||||
{routeElements}
|
||||
{/* The path of * here is needed to be set as a catch all, so it will render the wrapper element
|
||||
* and work with nested routes if they exist too */}
|
||||
<Route path="*" element={<Wrapper />} />
|
||||
|
||||
Reference in New Issue
Block a user