From 0e640893dfeb944a84c4c7ec993e1af5f3696253 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 5 Feb 2021 13:56:53 +0100 Subject: [PATCH 1/4] test-utils: add mountedRoutes option to wrapInTestApp --- .../src/testUtils/appWrappers.test.tsx | 27 +++++++++++++++ .../test-utils/src/testUtils/appWrappers.tsx | 34 +++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/packages/test-utils/src/testUtils/appWrappers.test.tsx b/packages/test-utils/src/testUtils/appWrappers.test.tsx index 29f8d97793..4899f07061 100644 --- a/packages/test-utils/src/testUtils/appWrappers.test.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.test.tsx @@ -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 ( +
+
Link A: {a()}
+
Link B: {b({ name: 'x' })}
+
+ ); + }; + + const rendered = await renderInTestApp(, { + 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(); + }); }); diff --git a/packages/test-utils/src/testUtils/appWrappers.tsx b/packages/test-utils/src/testUtils/appWrappers.tsx index 1f6c7622b8..cc67a5b707 100644 --- a/packages/test-utils/src/testUtils/appWrappers.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.tsx @@ -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(, { + * 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 = () =>
Mounted at {path}
; + const Page = routePlugin.provide( + createRoutableExtension({ + component: async () => PageComponent, + mountPoint: routeRef, + }), + ); + return } />; + }, + ); + const AppProvider = app.getProvider(); const AppRouter = app.getRouter(); return ( + {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 */} } /> From 2ab3b3f0e3614de2faed69f23387b2969de4d510 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 5 Feb 2021 17:49:41 +0100 Subject: [PATCH 2/4] core-api: fix type inference of createRouteRef --- packages/core-api/src/routing/RouteRef.ts | 23 ++++++++++++++++---- packages/core-api/src/routing/hooks.test.tsx | 3 ++- packages/core-api/src/routing/index.ts | 2 +- packages/core-api/src/routing/types.ts | 8 ------- 4 files changed, 22 insertions(+), 14 deletions(-) diff --git a/packages/core-api/src/routing/RouteRef.ts b/packages/core-api/src/routing/RouteRef.ts index 4c2da34098..3f48d70827 100644 --- a/packages/core-api/src/routing/RouteRef.ts +++ b/packages/core-api/src/routing/RouteRef.ts @@ -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?: Array; + /** @deprecated Route refs no longer decide their own path */ + path?: string; + icon?: IconComponent; + title: string; +}; export class AbsoluteRouteRef { constructor(private readonly config: RouteRefConfig) {} @@ -38,9 +47,15 @@ export class AbsoluteRouteRef { } export function createRouteRef< - ParamKeys extends string, - Params extends { [param in string]: string } = { [name in ParamKeys]: string } ->(config: RouteRefConfig): RouteRef { + Params extends { [param in ParamKey]: string }, + ParamKey extends string = never +>(config: { + params?: ParamKey[]; + /** @deprecated Route refs no longer decide their own path */ + path?: string; + icon?: IconComponent; + title: string; +}): RouteRef { return new AbsoluteRouteRef(config); } diff --git a/packages/core-api/src/routing/hooks.test.tsx b/packages/core-api/src/routing/hooks.test.tsx index 3dbeaf8e3e..84a8314807 100644 --- a/packages/core-api/src/routing/hooks.test.tsx +++ b/packages/core-api/src/routing/hooks.test.tsx @@ -39,8 +39,9 @@ import { createRouteRef, createExternalRouteRef, ExternalRouteRef, + RouteRefConfig, } from './RouteRef'; -import { RouteRef, RouteRefConfig } from './types'; +import { RouteRef } from './types'; const mockConfig = (extra?: Partial>) => ({ path: '/unused', diff --git a/packages/core-api/src/routing/index.ts b/packages/core-api/src/routing/index.ts index 9564b3b225..af71e0c3e9 100644 --- a/packages/core-api/src/routing/index.ts +++ b/packages/core-api/src/routing/index.ts @@ -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'; diff --git a/packages/core-api/src/routing/types.ts b/packages/core-api/src/routing/types.ts index 99a20c31e9..5f9376c1d2 100644 --- a/packages/core-api/src/routing/types.ts +++ b/packages/core-api/src/routing/types.ts @@ -45,14 +45,6 @@ export type AbsoluteRouteRef = RouteRef<{}>; */ export type MutableRouteRef = RouteRef<{}>; -export type RouteRefConfig = { - params?: Array; - /** @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; From b51ee6ece987552331c8673ba7452ab4bcd95575 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 5 Feb 2021 17:56:01 +0100 Subject: [PATCH 3/4] added changesets --- .changeset/poor-sheep-give.md | 5 +++++ .changeset/tricky-dancers-rush.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/poor-sheep-give.md create mode 100644 .changeset/tricky-dancers-rush.md diff --git a/.changeset/poor-sheep-give.md b/.changeset/poor-sheep-give.md new file mode 100644 index 0000000000..4c52594b35 --- /dev/null +++ b/.changeset/poor-sheep-give.md @@ -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. diff --git a/.changeset/tricky-dancers-rush.md b/.changeset/tricky-dancers-rush.md new file mode 100644 index 0000000000..301b23d98e --- /dev/null +++ b/.changeset/tricky-dancers-rush.md @@ -0,0 +1,5 @@ +--- +'@backstage/core': patch +--- + +Fixed type inference of `createRouteRef`. From 1585732a7bf9e67a0506c378d08c7698706f2c2d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 7 Feb 2021 20:10:54 +0100 Subject: [PATCH 4/4] core-api: document createRouteRef type inference logic --- packages/core-api/src/routing/RouteRef.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/core-api/src/routing/RouteRef.ts b/packages/core-api/src/routing/RouteRef.ts index 3f48d70827..0a3df49b58 100644 --- a/packages/core-api/src/routing/RouteRef.ts +++ b/packages/core-api/src/routing/RouteRef.ts @@ -47,7 +47,12 @@ export class AbsoluteRouteRef { } export function createRouteRef< + // 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[];