@backstage/frontend-test-utils 0.5.3-next.0 → 0.5.3-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @backstage/frontend-test-utils
2
2
 
3
+ ## 0.5.3-next.1
4
+
5
+ ### Patch Changes
6
+
7
+ - fa363f9: Added support for `ExternalRouteRef` in the `mountedRoutes` option of `renderInTestApp` and `renderTestApp`.
8
+ - Updated dependencies
9
+ - @backstage/frontend-plugin-api@0.17.0-next.1
10
+ - @backstage/core-plugin-api@1.12.6-next.1
11
+ - @backstage/frontend-app-api@0.16.3-next.1
12
+ - @backstage/plugin-app@0.4.6-next.1
13
+ - @backstage/plugin-permission-common@0.9.9-next.1
14
+
3
15
  ## 0.5.3-next.0
4
16
 
5
17
  ### Patch Changes
@@ -4,10 +4,13 @@ import { Link, MemoryRouter } from 'react-router-dom';
4
4
  import { prepareSpecializedApp } from '@backstage/frontend-app-api';
5
5
  import { render } from '@testing-library/react';
6
6
  import { ConfigReader } from '@backstage/config';
7
- import { coreExtensionData, NavItemBlueprint, useRouteRef, createExtension, createFrontendModule, createFrontendPlugin, createApiFactory } from '@backstage/frontend-plugin-api';
7
+ import { coreExtensionData, NavItemBlueprint, useRouteRef, createExtension, createRouteRef, createFrontendModule, createFrontendPlugin, createApiFactory } from '@backstage/frontend-plugin-api';
8
8
  import { RouterBlueprint } from '@backstage/plugin-app-react';
9
9
  import appPlugin from '@backstage/plugin-app';
10
10
  import { getMockApiFactory } from '../apis/MockWithApiFactory.esm.js';
11
+ import { OpaqueExternalRouteRef } from '../frontend-internal/src/routing/OpaqueExternalRouteRef.esm.js';
12
+ import '../frontend-internal/src/routing/OpaqueRouteRef.esm.js';
13
+ import '../frontend-internal/src/routing/OpaqueSubRouteRef.esm.js';
11
14
 
12
15
  const DEFAULT_MOCK_CONFIG = {
13
16
  app: { baseUrl: "http://localhost:3000" },
@@ -73,8 +76,16 @@ function renderInTestApp(element, options) {
73
76
  }
74
77
  })
75
78
  ];
79
+ const externalBindings = /* @__PURE__ */ new Map();
76
80
  if (options?.mountedRoutes) {
77
- for (const [path, routeRef] of Object.entries(options.mountedRoutes)) {
81
+ for (const [path, optionRef] of Object.entries(options.mountedRoutes)) {
82
+ let routeRef;
83
+ if (OpaqueExternalRouteRef.isType(optionRef)) {
84
+ routeRef = createRouteRef();
85
+ externalBindings.set(optionRef, routeRef);
86
+ } else {
87
+ routeRef = optionRef;
88
+ }
78
89
  extensions.push(
79
90
  createExtension({
80
91
  kind: "test-route",
@@ -141,7 +152,12 @@ function renderInTestApp(element, options) {
141
152
  const [apiRef, implementation] = entry;
142
153
  return createApiFactory(apiRef, implementation);
143
154
  })
144
- }
155
+ },
156
+ bindRoutes: externalBindings.size > 0 ? ({ bind }) => {
157
+ for (const [externalRef, targetRef] of externalBindings) {
158
+ bind({ ref: externalRef }, { ref: targetRef });
159
+ }
160
+ } : void 0
145
161
  }).finalize();
146
162
  return render(
147
163
  app.tree.root.instance.getData(coreExtensionData.reactElement)
@@ -1 +1 @@
1
- {"version":3,"file":"renderInTestApp.esm.js","sources":["../../src/app/renderInTestApp.tsx"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Fragment } from 'react';\nimport { Link, MemoryRouter } from 'react-router-dom';\nimport { prepareSpecializedApp } from '@backstage/frontend-app-api';\nimport { RenderResult, render } from '@testing-library/react';\nimport { ConfigReader } from '@backstage/config';\nimport { JsonObject } from '@backstage/types';\nimport {\n createExtension,\n ExtensionDefinition,\n coreExtensionData,\n RouteRef,\n useRouteRef,\n IconComponent,\n NavItemBlueprint,\n createFrontendPlugin,\n FrontendFeature,\n createFrontendModule,\n createApiFactory,\n type ApiRef,\n} from '@backstage/frontend-plugin-api';\nimport { RouterBlueprint } from '@backstage/plugin-app-react';\nimport appPlugin from '@backstage/plugin-app';\nimport { getMockApiFactory } from '../apis/MockWithApiFactory';\n// eslint-disable-next-line @backstage/no-relative-monorepo-imports\nimport type { CreateSpecializedAppInternalOptions } from '../../../frontend-app-api/src/wiring/createSpecializedApp';\nimport { TestApiPairs } from '../apis/TestApiProvider';\n\nconst DEFAULT_MOCK_CONFIG = {\n app: { baseUrl: 'http://localhost:3000' },\n backend: { baseUrl: 'http://localhost:7007' },\n};\n\n/**\n * Options to customize the behavior of the test app.\n * @public\n */\nexport type TestAppOptions<TApiPairs extends any[] = any[]> = {\n /**\n * An object of paths to mount route ref on, with the key being the path and the value\n * being the RouteRef that the path will be bound to. This allows the route refs to be\n * used by `useRouteRef` in the rendered elements.\n *\n * @example\n * ```ts\n * renderInTestApp(<MyComponent />, {\n * mountedRoutes: {\n * '/my-path': myRouteRef,\n * }\n * })\n * // ...\n * const link = useRouteRef(myRouteRef)\n * ```\n */\n mountedRoutes?: { [path: string]: RouteRef };\n\n /**\n * Additional configuration passed to the app when rendering elements inside it.\n */\n config?: JsonObject;\n\n /**\n * Additional features to add to the test app.\n */\n features?: FrontendFeature[];\n\n /**\n * Initial route entries to use for the router.\n */\n initialRouteEntries?: string[];\n\n /**\n * API overrides to provide to the test app. Use `mockApis` helpers\n * from `@backstage/frontend-test-utils` to create mock implementations.\n *\n * @example\n * ```ts\n * import { mockApis } from '@backstage/frontend-test-utils';\n *\n * renderInTestApp(<MyComponent />, {\n * apis: [mockApis.identity({ userEntityRef: 'user:default/guest' })],\n * })\n * ```\n */\n apis?: readonly [...TestApiPairs<TApiPairs>];\n};\n\nconst NavItem = (props: {\n routeRef: RouteRef<undefined>;\n title: string;\n icon: IconComponent;\n}) => {\n const { routeRef, title, icon: Icon } = props;\n const link = useRouteRef(routeRef);\n if (!link) {\n return null;\n }\n return (\n <li>\n <Link to={link()}>\n <Icon /> {title}\n </Link>\n </li>\n );\n};\n\nconst appPluginOverride = appPlugin.withOverrides({\n extensions: [\n appPlugin.getExtension('sign-in-page:app').override({\n disabled: true,\n }),\n appPlugin.getExtension('app/layout').override({\n disabled: true,\n }),\n appPlugin.getExtension('app/routes').override({\n disabled: true,\n }),\n appPlugin.getExtension('app/nav').override({\n output: [coreExtensionData.reactElement],\n factory(_originalFactory, { inputs }) {\n return [\n coreExtensionData.reactElement(\n <nav>\n <ul>\n {inputs.items.map(\n (item: (typeof inputs.items)[number], index: number) => {\n const { icon, title, routeRef } = item.get(\n NavItemBlueprint.dataRefs.target,\n );\n\n return (\n <NavItem\n key={index}\n icon={icon}\n title={title}\n routeRef={routeRef}\n />\n );\n },\n )}\n </ul>\n </nav>,\n ),\n ];\n },\n }),\n ],\n});\n\n/**\n * @public\n * Renders the given element in a test app, for use in unit tests.\n */\nexport function renderInTestApp<const TApiPairs extends any[] = any[]>(\n element: JSX.Element,\n options?: TestAppOptions<TApiPairs>,\n): RenderResult {\n const extensions: Array<ExtensionDefinition> = [\n createExtension({\n attachTo: { id: 'app/root', input: 'children' },\n output: [coreExtensionData.reactElement],\n factory: () => {\n return [coreExtensionData.reactElement(element)];\n },\n }),\n ];\n\n if (options?.mountedRoutes) {\n for (const [path, routeRef] of Object.entries(options.mountedRoutes)) {\n // TODO(Rugvip): add support for external route refs\n extensions.push(\n createExtension({\n kind: 'test-route',\n name: path,\n attachTo: { id: 'app/root', input: 'elements' },\n output: [\n coreExtensionData.reactElement,\n coreExtensionData.routePath,\n coreExtensionData.routeRef,\n ],\n factory: () => [\n coreExtensionData.reactElement(<Fragment />),\n coreExtensionData.routePath(path),\n coreExtensionData.routeRef(routeRef),\n ],\n }),\n );\n }\n }\n\n const features: FrontendFeature[] = [\n createFrontendModule({\n pluginId: 'app',\n extensions: [\n RouterBlueprint.make({\n params: {\n component: ({ children }) => (\n <MemoryRouter\n initialEntries={options?.initialRouteEntries}\n future={{\n v7_relativeSplatPath: false,\n v7_startTransition: false,\n }}\n >\n {children}\n </MemoryRouter>\n ),\n },\n }),\n ],\n }),\n createFrontendPlugin({\n pluginId: 'test',\n extensions,\n }),\n appPluginOverride,\n ];\n\n if (options?.features) {\n features.push(...options.features);\n }\n\n const app = prepareSpecializedApp({\n features,\n config: ConfigReader.fromConfigs([\n {\n context: 'render-config',\n data: options?.config ?? DEFAULT_MOCK_CONFIG,\n },\n ]),\n __internal: options?.apis && {\n apiFactoryOverrides: options.apis.map(entry => {\n const mockFactory = getMockApiFactory(entry);\n if (mockFactory) {\n return mockFactory;\n }\n const [apiRef, implementation] = entry as readonly [ApiRef<any>, any];\n return createApiFactory(apiRef, implementation);\n }),\n },\n } as CreateSpecializedAppInternalOptions).finalize();\n\n return render(\n app.tree.root.instance!.getData(coreExtensionData.reactElement),\n );\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"renderInTestApp.esm.js","sources":["../../src/app/renderInTestApp.tsx"],"sourcesContent":["/*\n * Copyright 2023 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Fragment } from 'react';\nimport { Link, MemoryRouter } from 'react-router-dom';\nimport { prepareSpecializedApp } from '@backstage/frontend-app-api';\nimport { RenderResult, render } from '@testing-library/react';\nimport { ConfigReader } from '@backstage/config';\nimport { JsonObject } from '@backstage/types';\nimport {\n createExtension,\n ExtensionDefinition,\n coreExtensionData,\n RouteRef,\n useRouteRef,\n IconComponent,\n NavItemBlueprint,\n createFrontendPlugin,\n FrontendFeature,\n createFrontendModule,\n createApiFactory,\n createRouteRef,\n ExternalRouteRef,\n type ApiRef,\n} from '@backstage/frontend-plugin-api';\nimport { RouterBlueprint } from '@backstage/plugin-app-react';\nimport appPlugin from '@backstage/plugin-app';\nimport { getMockApiFactory } from '../apis/MockWithApiFactory';\n// eslint-disable-next-line @backstage/no-relative-monorepo-imports\nimport type { CreateSpecializedAppInternalOptions } from '../../../frontend-app-api/src/wiring/createSpecializedApp';\nimport { TestApiPairs } from '../apis/TestApiProvider';\nimport { OpaqueExternalRouteRef } from '@internal/frontend';\n\nconst DEFAULT_MOCK_CONFIG = {\n app: { baseUrl: 'http://localhost:3000' },\n backend: { baseUrl: 'http://localhost:7007' },\n};\n\n/**\n * Options to customize the behavior of the test app.\n * @public\n */\nexport type TestAppOptions<TApiPairs extends any[] = any[]> = {\n /**\n * An object of paths to mount route ref on, with the key being the path and the value\n * being the route ref that the path will be bound to. This allows the route refs to be\n * used by `useRouteRef` in the rendered elements.\n *\n * @example\n * ```ts\n * renderInTestApp(<MyComponent />, {\n * mountedRoutes: {\n * '/my-path': myRouteRef,\n * }\n * })\n * // ...\n * const link = useRouteRef(myRouteRef)\n * ```\n */\n mountedRoutes?: { [path: string]: RouteRef | ExternalRouteRef };\n\n /**\n * Additional configuration passed to the app when rendering elements inside it.\n */\n config?: JsonObject;\n\n /**\n * Additional features to add to the test app.\n */\n features?: FrontendFeature[];\n\n /**\n * Initial route entries to use for the router.\n */\n initialRouteEntries?: string[];\n\n /**\n * API overrides to provide to the test app. Use `mockApis` helpers\n * from `@backstage/frontend-test-utils` to create mock implementations.\n *\n * @example\n * ```ts\n * import { mockApis } from '@backstage/frontend-test-utils';\n *\n * renderInTestApp(<MyComponent />, {\n * apis: [mockApis.identity({ userEntityRef: 'user:default/guest' })],\n * })\n * ```\n */\n apis?: readonly [...TestApiPairs<TApiPairs>];\n};\n\nconst NavItem = (props: {\n routeRef: RouteRef<undefined>;\n title: string;\n icon: IconComponent;\n}) => {\n const { routeRef, title, icon: Icon } = props;\n const link = useRouteRef(routeRef);\n if (!link) {\n return null;\n }\n return (\n <li>\n <Link to={link()}>\n <Icon /> {title}\n </Link>\n </li>\n );\n};\n\nconst appPluginOverride = appPlugin.withOverrides({\n extensions: [\n appPlugin.getExtension('sign-in-page:app').override({\n disabled: true,\n }),\n appPlugin.getExtension('app/layout').override({\n disabled: true,\n }),\n appPlugin.getExtension('app/routes').override({\n disabled: true,\n }),\n appPlugin.getExtension('app/nav').override({\n output: [coreExtensionData.reactElement],\n factory(_originalFactory, { inputs }) {\n return [\n coreExtensionData.reactElement(\n <nav>\n <ul>\n {inputs.items.map(\n (item: (typeof inputs.items)[number], index: number) => {\n const { icon, title, routeRef } = item.get(\n NavItemBlueprint.dataRefs.target,\n );\n\n return (\n <NavItem\n key={index}\n icon={icon}\n title={title}\n routeRef={routeRef}\n />\n );\n },\n )}\n </ul>\n </nav>,\n ),\n ];\n },\n }),\n ],\n});\n\n/**\n * @public\n * Renders the given element in a test app, for use in unit tests.\n */\nexport function renderInTestApp<const TApiPairs extends any[] = any[]>(\n element: JSX.Element,\n options?: TestAppOptions<TApiPairs>,\n): RenderResult {\n const extensions: Array<ExtensionDefinition> = [\n createExtension({\n attachTo: { id: 'app/root', input: 'children' },\n output: [coreExtensionData.reactElement],\n factory: () => {\n return [coreExtensionData.reactElement(element)];\n },\n }),\n ];\n\n const externalBindings = new Map<ExternalRouteRef, RouteRef>();\n\n if (options?.mountedRoutes) {\n for (const [path, optionRef] of Object.entries(options.mountedRoutes)) {\n let routeRef: RouteRef;\n\n if (OpaqueExternalRouteRef.isType(optionRef)) {\n // Create an actual route ref for the external route, then bind the external ref to it\n routeRef = createRouteRef();\n externalBindings.set(optionRef, routeRef);\n } else {\n routeRef = optionRef;\n }\n\n extensions.push(\n createExtension({\n kind: 'test-route',\n name: path,\n attachTo: { id: 'app/root', input: 'elements' },\n output: [\n coreExtensionData.reactElement,\n coreExtensionData.routePath,\n coreExtensionData.routeRef,\n ],\n factory: () => [\n coreExtensionData.reactElement(<Fragment />),\n coreExtensionData.routePath(path),\n coreExtensionData.routeRef(routeRef),\n ],\n }),\n );\n }\n }\n\n const features: FrontendFeature[] = [\n createFrontendModule({\n pluginId: 'app',\n extensions: [\n RouterBlueprint.make({\n params: {\n component: ({ children }) => (\n <MemoryRouter\n initialEntries={options?.initialRouteEntries}\n future={{\n v7_relativeSplatPath: false,\n v7_startTransition: false,\n }}\n >\n {children}\n </MemoryRouter>\n ),\n },\n }),\n ],\n }),\n createFrontendPlugin({\n pluginId: 'test',\n extensions,\n }),\n appPluginOverride,\n ];\n\n if (options?.features) {\n features.push(...options.features);\n }\n\n const app = prepareSpecializedApp({\n features,\n config: ConfigReader.fromConfigs([\n {\n context: 'render-config',\n data: options?.config ?? DEFAULT_MOCK_CONFIG,\n },\n ]),\n __internal: options?.apis && {\n apiFactoryOverrides: options.apis.map(entry => {\n const mockFactory = getMockApiFactory(entry);\n if (mockFactory) {\n return mockFactory;\n }\n const [apiRef, implementation] = entry as readonly [ApiRef<any>, any];\n return createApiFactory(apiRef, implementation);\n }),\n },\n bindRoutes:\n externalBindings.size > 0\n ? ({ bind }) => {\n for (const [externalRef, targetRef] of externalBindings) {\n bind({ ref: externalRef }, { ref: targetRef });\n }\n }\n : undefined,\n } as CreateSpecializedAppInternalOptions).finalize();\n\n return render(\n app.tree.root.instance!.getData(coreExtensionData.reactElement),\n );\n}\n"],"names":[],"mappings":"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"}
@@ -1,13 +1,16 @@
1
1
  import { jsx } from 'react/jsx-runtime';
2
2
  import { Fragment } from 'react';
3
3
  import { prepareSpecializedApp } from '@backstage/frontend-app-api';
4
- import { createExtension, coreExtensionData, createFrontendModule, createFrontendPlugin, createApiFactory } from '@backstage/frontend-plugin-api';
4
+ import { createRouteRef, createExtension, coreExtensionData, createFrontendModule, createFrontendPlugin, createApiFactory } from '@backstage/frontend-plugin-api';
5
5
  import { render } from '@testing-library/react';
6
6
  import appPlugin from '@backstage/plugin-app';
7
7
  import { ConfigReader } from '@backstage/config';
8
8
  import { MemoryRouter } from 'react-router-dom';
9
9
  import { RouterBlueprint } from '@backstage/plugin-app-react';
10
10
  import { getMockApiFactory } from '../apis/MockWithApiFactory.esm.js';
11
+ import { OpaqueExternalRouteRef } from '../frontend-internal/src/routing/OpaqueExternalRouteRef.esm.js';
12
+ import '../frontend-internal/src/routing/OpaqueRouteRef.esm.js';
13
+ import '../frontend-internal/src/routing/OpaqueSubRouteRef.esm.js';
11
14
 
12
15
  const DEFAULT_MOCK_CONFIG = {
13
16
  app: { baseUrl: "http://localhost:3000" },
@@ -22,8 +25,16 @@ const appPluginOverride = appPlugin.withOverrides({
22
25
  });
23
26
  function renderTestApp(options) {
24
27
  const extensions = [...options?.extensions ?? []];
28
+ const externalBindings = /* @__PURE__ */ new Map();
25
29
  if (options?.mountedRoutes) {
26
- for (const [path, routeRef] of Object.entries(options.mountedRoutes)) {
30
+ for (const [path, optionRef] of Object.entries(options.mountedRoutes)) {
31
+ let routeRef;
32
+ if (OpaqueExternalRouteRef.isType(optionRef)) {
33
+ routeRef = createRouteRef();
34
+ externalBindings.set(optionRef, routeRef);
35
+ } else {
36
+ routeRef = optionRef;
37
+ }
27
38
  extensions.push(
28
39
  createExtension({
29
40
  kind: "test-route",
@@ -90,7 +101,12 @@ function renderTestApp(options) {
90
101
  const [apiRef, implementation] = entry;
91
102
  return createApiFactory(apiRef, implementation);
92
103
  })
93
- }
104
+ },
105
+ bindRoutes: externalBindings.size > 0 ? ({ bind }) => {
106
+ for (const [externalRef, targetRef] of externalBindings) {
107
+ bind({ ref: externalRef }, { ref: targetRef });
108
+ }
109
+ } : void 0
94
110
  }).finalize();
95
111
  return render(
96
112
  app.tree.root.instance.getData(coreExtensionData.reactElement)
@@ -1 +1 @@
1
- {"version":3,"file":"renderTestApp.esm.js","sources":["../../src/app/renderTestApp.tsx"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Fragment } from 'react';\nimport { prepareSpecializedApp } from '@backstage/frontend-app-api';\nimport {\n coreExtensionData,\n createApiFactory,\n createExtension,\n createFrontendModule,\n createFrontendPlugin,\n ExtensionDefinition,\n FrontendFeature,\n RouteRef,\n type ApiRef,\n} from '@backstage/frontend-plugin-api';\nimport { render, type RenderResult } from '@testing-library/react';\nimport appPlugin from '@backstage/plugin-app';\nimport { JsonObject } from '@backstage/types';\nimport { ConfigReader } from '@backstage/config';\nimport { MemoryRouter } from 'react-router-dom';\nimport { RouterBlueprint } from '@backstage/plugin-app-react';\nimport { getMockApiFactory } from '../apis/MockWithApiFactory';\n// eslint-disable-next-line @backstage/no-relative-monorepo-imports\nimport type { CreateSpecializedAppInternalOptions } from '../../../frontend-app-api/src/wiring/createSpecializedApp';\nimport { TestApiPairs } from '../apis/TestApiProvider';\n\nconst DEFAULT_MOCK_CONFIG = {\n app: { baseUrl: 'http://localhost:3000' },\n backend: { baseUrl: 'http://localhost:7007' },\n};\n\n/**\n * Options for `renderTestApp`.\n *\n * @public\n */\nexport type RenderTestAppOptions<TApiPairs extends any[] = any[]> = {\n /**\n * Additional configuration passed to the app when rendering elements inside it.\n */\n config?: JsonObject;\n /**\n * Additional extensions to add to the test app.\n */\n extensions?: ExtensionDefinition<any>[];\n\n /**\n * Additional features to add to the test app.\n */\n features?: FrontendFeature[];\n\n /**\n * Initial route entries to use for the router.\n */\n initialRouteEntries?: string[];\n\n /**\n * An object of paths to mount route refs on, with the key being the path and\n * the value being the RouteRef that the path will be bound to. This allows\n * the route refs to be used by `useRouteRef` in the rendered elements.\n *\n * @example\n * ```ts\n * renderTestApp({\n * mountedRoutes: {\n * '/my-path': myRouteRef,\n * },\n * extensions: [...],\n * })\n * ```\n */\n mountedRoutes?: { [path: string]: RouteRef };\n\n /**\n * API overrides to provide to the test app. Use `mockApis` helpers\n * from `@backstage/frontend-test-utils` to create mock implementations.\n *\n * @example\n * ```ts\n * import { mockApis } from '@backstage/frontend-test-utils';\n *\n * renderTestApp({\n * apis: [mockApis.identity({ userEntityRef: 'user:default/guest' })],\n * extensions: [...],\n * })\n * ```\n */\n apis?: readonly [...TestApiPairs<TApiPairs>];\n};\n\nconst appPluginOverride = appPlugin.withOverrides({\n extensions: [\n appPlugin.getExtension('sign-in-page:app').override({\n disabled: true,\n }),\n ],\n});\n\n/**\n * Renders the provided extensions inside a Backstage app, returning the same\n * utilities as `@testing-library/react` `render` function.\n *\n * @public\n */\nexport function renderTestApp<const TApiPairs extends any[] = any[]>(\n options?: RenderTestAppOptions<TApiPairs>,\n): RenderResult {\n const extensions = [...(options?.extensions ?? [])];\n\n if (options?.mountedRoutes) {\n for (const [path, routeRef] of Object.entries(options.mountedRoutes)) {\n extensions.push(\n createExtension({\n kind: 'test-route',\n name: path,\n attachTo: { id: 'app/routes', input: 'routes' },\n output: [\n coreExtensionData.reactElement,\n coreExtensionData.routePath,\n coreExtensionData.routeRef,\n ],\n factory: () => [\n coreExtensionData.reactElement(<Fragment />),\n coreExtensionData.routePath(path),\n coreExtensionData.routeRef(routeRef),\n ],\n }),\n );\n }\n }\n\n const features: FrontendFeature[] = [\n createFrontendModule({\n pluginId: 'app',\n extensions: [\n RouterBlueprint.make({\n params: {\n component: ({ children }) => (\n <MemoryRouter\n initialEntries={options?.initialRouteEntries}\n future={{\n v7_relativeSplatPath: false,\n v7_startTransition: false,\n }}\n >\n {children}\n </MemoryRouter>\n ),\n },\n }),\n ],\n }),\n createFrontendPlugin({\n pluginId: 'test',\n extensions,\n }),\n appPluginOverride,\n ];\n\n if (options?.features) {\n features.push(...options.features);\n }\n\n const app = prepareSpecializedApp({\n features,\n config: ConfigReader.fromConfigs([\n {\n context: 'render-config',\n data: options?.config ?? DEFAULT_MOCK_CONFIG,\n },\n ]),\n __internal: options?.apis && {\n apiFactoryOverrides: options.apis.map(entry => {\n const mockFactory = getMockApiFactory(entry);\n if (mockFactory) {\n return mockFactory;\n }\n const [apiRef, implementation] = entry as readonly [ApiRef<any>, any];\n return createApiFactory(apiRef, implementation);\n }),\n },\n } as CreateSpecializedAppInternalOptions).finalize();\n\n return render(\n app.tree.root.instance!.getData(coreExtensionData.reactElement),\n );\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"renderTestApp.esm.js","sources":["../../src/app/renderTestApp.tsx"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Fragment } from 'react';\nimport { prepareSpecializedApp } from '@backstage/frontend-app-api';\nimport {\n coreExtensionData,\n createApiFactory,\n createExtension,\n createFrontendModule,\n createFrontendPlugin,\n ExtensionDefinition,\n FrontendFeature,\n RouteRef,\n ExternalRouteRef,\n createRouteRef,\n type ApiRef,\n} from '@backstage/frontend-plugin-api';\nimport { render, type RenderResult } from '@testing-library/react';\nimport appPlugin from '@backstage/plugin-app';\nimport { JsonObject } from '@backstage/types';\nimport { ConfigReader } from '@backstage/config';\nimport { MemoryRouter } from 'react-router-dom';\nimport { RouterBlueprint } from '@backstage/plugin-app-react';\nimport { getMockApiFactory } from '../apis/MockWithApiFactory';\n// eslint-disable-next-line @backstage/no-relative-monorepo-imports\nimport type { CreateSpecializedAppInternalOptions } from '../../../frontend-app-api/src/wiring/createSpecializedApp';\nimport { TestApiPairs } from '../apis/TestApiProvider';\nimport { OpaqueExternalRouteRef } from '@internal/frontend';\n\nconst DEFAULT_MOCK_CONFIG = {\n app: { baseUrl: 'http://localhost:3000' },\n backend: { baseUrl: 'http://localhost:7007' },\n};\n\n/**\n * Options for `renderTestApp`.\n *\n * @public\n */\nexport type RenderTestAppOptions<TApiPairs extends any[] = any[]> = {\n /**\n * Additional configuration passed to the app when rendering elements inside it.\n */\n config?: JsonObject;\n /**\n * Additional extensions to add to the test app.\n */\n extensions?: ExtensionDefinition<any>[];\n\n /**\n * Additional features to add to the test app.\n */\n features?: FrontendFeature[];\n\n /**\n * Initial route entries to use for the router.\n */\n initialRouteEntries?: string[];\n\n /**\n * An object of paths to mount route refs on, with the key being the path and\n * the value being the route ref that the path will be bound to. This allows\n * the route refs to be used by `useRouteRef` in the rendered elements.\n *\n * @example\n * ```ts\n * renderTestApp({\n * mountedRoutes: {\n * '/my-path': myRouteRef,\n * },\n * extensions: [...],\n * })\n * ```\n */\n mountedRoutes?: { [path: string]: RouteRef | ExternalRouteRef };\n\n /**\n * API overrides to provide to the test app. Use `mockApis` helpers\n * from `@backstage/frontend-test-utils` to create mock implementations.\n *\n * @example\n * ```ts\n * import { mockApis } from '@backstage/frontend-test-utils';\n *\n * renderTestApp({\n * apis: [mockApis.identity({ userEntityRef: 'user:default/guest' })],\n * extensions: [...],\n * })\n * ```\n */\n apis?: readonly [...TestApiPairs<TApiPairs>];\n};\n\nconst appPluginOverride = appPlugin.withOverrides({\n extensions: [\n appPlugin.getExtension('sign-in-page:app').override({\n disabled: true,\n }),\n ],\n});\n\n/**\n * Renders the provided extensions inside a Backstage app, returning the same\n * utilities as `@testing-library/react` `render` function.\n *\n * @public\n */\nexport function renderTestApp<const TApiPairs extends any[] = any[]>(\n options?: RenderTestAppOptions<TApiPairs>,\n): RenderResult {\n const extensions = [...(options?.extensions ?? [])];\n\n const externalBindings = new Map<ExternalRouteRef, RouteRef>();\n\n if (options?.mountedRoutes) {\n for (const [path, optionRef] of Object.entries(options.mountedRoutes)) {\n let routeRef: RouteRef;\n\n if (OpaqueExternalRouteRef.isType(optionRef)) {\n // Create an actual route ref for the external route, then bind the external ref to it\n routeRef = createRouteRef();\n externalBindings.set(optionRef, routeRef);\n } else {\n routeRef = optionRef;\n }\n\n extensions.push(\n createExtension({\n kind: 'test-route',\n name: path,\n attachTo: { id: 'app/routes', input: 'routes' },\n output: [\n coreExtensionData.reactElement,\n coreExtensionData.routePath,\n coreExtensionData.routeRef,\n ],\n factory: () => [\n coreExtensionData.reactElement(<Fragment />),\n coreExtensionData.routePath(path),\n coreExtensionData.routeRef(routeRef),\n ],\n }),\n );\n }\n }\n\n const features: FrontendFeature[] = [\n createFrontendModule({\n pluginId: 'app',\n extensions: [\n RouterBlueprint.make({\n params: {\n component: ({ children }) => (\n <MemoryRouter\n initialEntries={options?.initialRouteEntries}\n future={{\n v7_relativeSplatPath: false,\n v7_startTransition: false,\n }}\n >\n {children}\n </MemoryRouter>\n ),\n },\n }),\n ],\n }),\n createFrontendPlugin({\n pluginId: 'test',\n extensions,\n }),\n appPluginOverride,\n ];\n\n if (options?.features) {\n features.push(...options.features);\n }\n\n const app = prepareSpecializedApp({\n features,\n config: ConfigReader.fromConfigs([\n {\n context: 'render-config',\n data: options?.config ?? DEFAULT_MOCK_CONFIG,\n },\n ]),\n __internal: options?.apis && {\n apiFactoryOverrides: options.apis.map(entry => {\n const mockFactory = getMockApiFactory(entry);\n if (mockFactory) {\n return mockFactory;\n }\n const [apiRef, implementation] = entry as readonly [ApiRef<any>, any];\n return createApiFactory(apiRef, implementation);\n }),\n },\n bindRoutes:\n externalBindings.size > 0\n ? ({ bind }) => {\n for (const [externalRef, targetRef] of externalBindings) {\n bind({ ref: externalRef }, { ref: targetRef });\n }\n }\n : undefined,\n } as CreateSpecializedAppInternalOptions).finalize();\n\n return render(\n app.tree.root.instance!.getData(coreExtensionData.reactElement),\n );\n}\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"createErrorCollector.esm.js","sources":["../../../../../frontend-app-api/src/wiring/createErrorCollector.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { AppNode, FrontendPlugin } from '@backstage/frontend-plugin-api';\n\n/**\n * @public\n */\nexport type AppErrorTypes = {\n // resolveAppNodeSpecs\n EXTENSION_IGNORED: {\n context: { plugin: FrontendPlugin; extensionId: string };\n };\n INVALID_EXTENSION_CONFIG_KEY: {\n context: { extensionId: string };\n };\n // resolveAppTree\n EXTENSION_INPUT_REDIRECT_CONFLICT: {\n context: { node: AppNode; inputName: string };\n };\n // instantiateAppNodeTree\n EXTENSION_INPUT_DATA_IGNORED: {\n context: { node: AppNode; inputName: string };\n };\n EXTENSION_INPUT_DATA_MISSING: {\n context: { node: AppNode; inputName: string };\n };\n EXTENSION_INPUT_INTERNAL_IGNORED: {\n context: {\n node: AppNode;\n inputName: string;\n extensionId: string;\n plugin: FrontendPlugin;\n };\n };\n EXTENSION_ATTACHMENT_CONFLICT: {\n context: { node: AppNode; inputName: string };\n };\n EXTENSION_ATTACHMENT_MISSING: {\n context: { node: AppNode; inputName: string };\n };\n EXTENSION_CONFIGURATION_INVALID: {\n context: { node: AppNode };\n };\n EXTENSION_INVALID: {\n context: { node: AppNode };\n };\n EXTENSION_OUTPUT_CONFLICT: {\n context: { node: AppNode; dataRefId: string };\n };\n EXTENSION_OUTPUT_MISSING: {\n context: { node: AppNode; dataRefId: string };\n };\n EXTENSION_OUTPUT_IGNORED: {\n context: { node: AppNode; dataRefId: string };\n };\n EXTENSION_FACTORY_ERROR: {\n context: { node: AppNode };\n };\n // createSpecializedApp\n API_EXTENSION_INVALID: {\n context: { node: AppNode };\n };\n API_FACTORY_CONFLICT: {\n context: {\n node: AppNode;\n apiRefId: string;\n pluginId: string;\n existingPluginId: string;\n };\n };\n EXTENSION_BOOTSTRAP_PREDICATE_IGNORED: {\n context: { node: AppNode };\n };\n EXTENSION_BOOTSTRAP_API_UNAVAILABLE: {\n context: { node: AppNode; apiRefId: string };\n };\n EXTENSION_BOOTSTRAP_API_OVERRIDE_IGNORED: {\n context: {\n node: AppNode;\n apiRefId: string;\n bootstrapNode: AppNode;\n pluginId: string;\n bootstrapPluginId: string;\n };\n };\n // routing\n ROUTE_DUPLICATE: {\n context: { routeId: string };\n };\n ROUTE_BINDING_INVALID_VALUE: {\n context: { routeId: string };\n };\n ROUTE_NOT_FOUND: {\n context: { routeId: string };\n };\n};\n\n/**\n * @public\n */\nexport type AppError =\n keyof AppErrorTypes extends infer ICode extends keyof AppErrorTypes\n ? ICode extends any\n ? {\n code: ICode;\n message: string;\n context: AppErrorTypes[ICode]['context'];\n }\n : never\n : never;\n\n/** @internal */\nexport interface ErrorCollector<TContext extends {} = {}> {\n report<TCode extends keyof AppErrorTypes>(\n report: Omit<\n AppErrorTypes[TCode]['context'],\n keyof TContext\n > extends infer IContext extends {}\n ? {} extends IContext\n ? {\n code: TCode;\n message: string;\n }\n : {\n code: TCode;\n message: string;\n context: IContext;\n }\n : never,\n ): void;\n child<TAdditionalContext extends {}>(\n context: TAdditionalContext,\n ): ErrorCollector<TContext & TAdditionalContext>;\n collectErrors(): AppError[] | undefined;\n}\n\n/** @internal */\nexport function createErrorCollector(\n context?: Partial<AppError['context']>,\n): ErrorCollector {\n const errors: AppError[] = [];\n const children: ErrorCollector[] = [];\n return {\n report(report: { code: string; message: string; context?: {} }) {\n errors.push({\n ...report,\n context: { ...context, ...report.context },\n } as AppError);\n },\n collectErrors() {\n const allErrors = [\n ...errors,\n ...children.flatMap(child => child.collectErrors() ?? []),\n ];\n errors.length = 0;\n if (allErrors.length === 0) {\n return undefined;\n }\n return allErrors;\n },\n child(childContext) {\n const child = createErrorCollector({ ...context, ...childContext });\n children.push(child);\n return child as ErrorCollector<any>;\n },\n };\n}\n"],"names":[],"mappings":"AAuJO,SAAS,qBACd,OAAA,EACgB;AAChB,EAAA,MAAM,SAAqB,EAAC;AAC5B,EAAA,MAAM,WAA6B,EAAC;AACpC,EAAA,OAAO;AAAA,IACL,OAAO,MAAA,EAAyD;AAC9D,MAAA,MAAA,CAAO,IAAA,CAAK;AAAA,QACV,GAAG,MAAA;AAAA,QACH,SAAS,EAAE,GAAG,OAAA,EAAS,GAAG,OAAO,OAAA;AAAQ,OAC9B,CAAA;AAAA,IACf,CAAA;AAAA,IACA,aAAA,GAAgB;AACd,MAAA,MAAM,SAAA,GAAY;AAAA,QAChB,GAAG,MAAA;AAAA,QACH,GAAG,SAAS,OAAA,CAAQ,CAAA,KAAA,KAAS,MAAM,aAAA,EAAc,IAAK,EAAE;AAAA,OAC1D;AACA,MAAA,MAAA,CAAO,MAAA,GAAS,CAAA;AAChB,MAAA,IAAI,SAAA,CAAU,WAAW,CAAA,EAAG;AAC1B,QAAA,OAAO,MAAA;AAAA,MACT;AACA,MAAA,OAAO,SAAA;AAAA,IACT,CAAA;AAAA,IACA,MAAM,YAAA,EAAc;AAClB,MAAA,MAAM,QAAQ,oBAAA,CAAqB,EAAE,GAAG,OAAA,EAAS,GAAG,cAAc,CAAA;AAClE,MAAA,QAAA,CAAS,KAAK,KAAK,CAAA;AACnB,MAAA,OAAO,KAAA;AAAA,IACT;AAAA,GACF;AACF;;;;"}
1
+ {"version":3,"file":"createErrorCollector.esm.js","sources":["../../../../../frontend-app-api/src/wiring/createErrorCollector.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { AppNode, FrontendPlugin } from '@backstage/frontend-plugin-api';\n\n/**\n * @public\n */\nexport type AppErrorTypes = {\n // resolveAppNodeSpecs\n EXTENSION_IGNORED: {\n context: { plugin: FrontendPlugin; extensionId: string };\n };\n INVALID_EXTENSION_CONFIG_KEY: {\n context: { extensionId: string };\n };\n // resolveAppTree\n EXTENSION_INPUT_REDIRECT_CONFLICT: {\n context: { node: AppNode; inputName: string };\n };\n // instantiateAppNodeTree\n EXTENSION_INPUT_DATA_IGNORED: {\n context: { node: AppNode; inputName: string };\n };\n EXTENSION_INPUT_DATA_MISSING: {\n context: { node: AppNode; inputName: string };\n };\n EXTENSION_INPUT_INTERNAL_IGNORED: {\n context: {\n node: AppNode;\n inputName: string;\n extensionId: string;\n plugin: FrontendPlugin;\n };\n };\n EXTENSION_ATTACHMENT_CONFLICT: {\n context: { node: AppNode; inputName: string };\n };\n EXTENSION_ATTACHMENT_MISSING: {\n context: { node: AppNode; inputName: string };\n };\n EXTENSION_CONFIGURATION_INVALID: {\n context: { node: AppNode };\n };\n EXTENSION_INVALID: {\n context: { node: AppNode };\n };\n EXTENSION_OUTPUT_CONFLICT: {\n context: { node: AppNode; dataRefId: string };\n };\n EXTENSION_OUTPUT_MISSING: {\n context: { node: AppNode; dataRefId: string };\n };\n EXTENSION_OUTPUT_IGNORED: {\n context: { node: AppNode; dataRefId: string };\n };\n EXTENSION_FACTORY_ERROR: {\n context: { node: AppNode };\n };\n // createSpecializedApp\n API_EXTENSION_INVALID: {\n context: { node: AppNode };\n };\n API_FACTORY_CONFLICT: {\n context: {\n node: AppNode;\n apiRefId: string;\n pluginId: string;\n existingPluginId: string;\n };\n };\n EXTENSION_BOOTSTRAP_PREDICATE_IGNORED: {\n context: { node: AppNode };\n };\n EXTENSION_BOOTSTRAP_API_UNAVAILABLE: {\n context: { node: AppNode; apiRefId: string };\n };\n EXTENSION_BOOTSTRAP_API_OVERRIDE_IGNORED: {\n context: {\n node: AppNode;\n apiRefId: string;\n bootstrapNode: AppNode;\n pluginId: string;\n bootstrapPluginId: string;\n };\n };\n FEATURE_FLAG_INVALID: {\n context: { pluginId: string; flagName: string; error: Error };\n };\n // routing\n ROUTE_DUPLICATE: {\n context: { routeId: string };\n };\n ROUTE_BINDING_INVALID_VALUE: {\n context: { routeId: string };\n };\n ROUTE_NOT_FOUND: {\n context: { routeId: string };\n };\n};\n\n/**\n * @public\n */\nexport type AppError =\n keyof AppErrorTypes extends infer ICode extends keyof AppErrorTypes\n ? ICode extends any\n ? {\n code: ICode;\n message: string;\n context: AppErrorTypes[ICode]['context'];\n }\n : never\n : never;\n\n/** @internal */\nexport interface ErrorCollector<TContext extends {} = {}> {\n report<TCode extends keyof AppErrorTypes>(\n report: Omit<\n AppErrorTypes[TCode]['context'],\n keyof TContext\n > extends infer IContext extends {}\n ? {} extends IContext\n ? {\n code: TCode;\n message: string;\n }\n : {\n code: TCode;\n message: string;\n context: IContext;\n }\n : never,\n ): void;\n child<TAdditionalContext extends {}>(\n context: TAdditionalContext,\n ): ErrorCollector<TContext & TAdditionalContext>;\n collectErrors(): AppError[] | undefined;\n}\n\n/** @internal */\nexport function createErrorCollector(\n context?: Partial<AppError['context']>,\n): ErrorCollector {\n const errors: AppError[] = [];\n const children: ErrorCollector[] = [];\n return {\n report(report: { code: string; message: string; context?: {} }) {\n errors.push({\n ...report,\n context: { ...context, ...report.context },\n } as AppError);\n },\n collectErrors() {\n const allErrors = [\n ...errors,\n ...children.flatMap(child => child.collectErrors() ?? []),\n ];\n errors.length = 0;\n if (allErrors.length === 0) {\n return undefined;\n }\n return allErrors;\n },\n child(childContext) {\n const child = createErrorCollector({ ...context, ...childContext });\n children.push(child);\n return child as ErrorCollector<any>;\n },\n };\n}\n"],"names":[],"mappings":"AA0JO,SAAS,qBACd,OAAA,EACgB;AAChB,EAAA,MAAM,SAAqB,EAAC;AAC5B,EAAA,MAAM,WAA6B,EAAC;AACpC,EAAA,OAAO;AAAA,IACL,OAAO,MAAA,EAAyD;AAC9D,MAAA,MAAA,CAAO,IAAA,CAAK;AAAA,QACV,GAAG,MAAA;AAAA,QACH,SAAS,EAAE,GAAG,OAAA,EAAS,GAAG,OAAO,OAAA;AAAQ,OAC9B,CAAA;AAAA,IACf,CAAA;AAAA,IACA,aAAA,GAAgB;AACd,MAAA,MAAM,SAAA,GAAY;AAAA,QAChB,GAAG,MAAA;AAAA,QACH,GAAG,SAAS,OAAA,CAAQ,CAAA,KAAA,KAAS,MAAM,aAAA,EAAc,IAAK,EAAE;AAAA,OAC1D;AACA,MAAA,MAAA,CAAO,MAAA,GAAS,CAAA;AAChB,MAAA,IAAI,SAAA,CAAU,WAAW,CAAA,EAAG;AAC1B,QAAA,OAAO,MAAA;AAAA,MACT;AACA,MAAA,OAAO,SAAA;AAAA,IACT,CAAA;AAAA,IACA,MAAM,YAAA,EAAc;AAClB,MAAA,MAAM,QAAQ,oBAAA,CAAqB,EAAE,GAAG,OAAA,EAAS,GAAG,cAAc,CAAA;AAClE,MAAA,QAAA,CAAS,KAAK,KAAK,CAAA;AACnB,MAAA,OAAO,KAAA;AAAA,IACT;AAAA,GACF;AACF;;;;"}
@@ -0,0 +1,9 @@
1
+ import { OpaqueType } from '../../../opaque-internal/src/OpaqueType.esm.js';
2
+
3
+ const OpaqueExternalRouteRef = OpaqueType.create({
4
+ type: "@backstage/ExternalRouteRef",
5
+ versions: ["v1"]
6
+ });
7
+
8
+ export { OpaqueExternalRouteRef };
9
+ //# sourceMappingURL=OpaqueExternalRouteRef.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OpaqueExternalRouteRef.esm.js","sources":["../../../../../frontend-internal/src/routing/OpaqueExternalRouteRef.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { ExternalRouteRef } from '@backstage/frontend-plugin-api';\nimport { OpaqueType } from '@internal/opaque';\n\nexport const OpaqueExternalRouteRef = OpaqueType.create<{\n public: ExternalRouteRef;\n versions: {\n readonly version: 'v1';\n\n getParams(): string[];\n getDescription(): string;\n getDefaultTarget(): string | undefined;\n\n setId(id: string): void;\n };\n}>({\n type: '@backstage/ExternalRouteRef',\n versions: ['v1'],\n});\n"],"names":[],"mappings":";;AAmBO,MAAM,sBAAA,GAAyB,WAAW,MAAA,CAW9C;AAAA,EACD,IAAA,EAAM,6BAAA;AAAA,EACN,QAAA,EAAU,CAAC,IAAI;AACjB,CAAC;;;;"}
@@ -0,0 +1,7 @@
1
+ import { OpaqueType } from '../../../opaque-internal/src/OpaqueType.esm.js';
2
+
3
+ OpaqueType.create({
4
+ type: "@backstage/RouteRef",
5
+ versions: ["v1"]
6
+ });
7
+ //# sourceMappingURL=OpaqueRouteRef.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OpaqueRouteRef.esm.js","sources":["../../../../../frontend-internal/src/routing/OpaqueRouteRef.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { RouteRef } from '@backstage/frontend-plugin-api';\nimport { OpaqueType } from '@internal/opaque';\n\nexport const OpaqueRouteRef = OpaqueType.create<{\n public: RouteRef;\n versions: {\n readonly version: 'v1';\n\n getParams(): string[];\n getDescription(): string;\n\n alias: string | undefined;\n\n setId(id: string): void;\n };\n}>({\n type: '@backstage/RouteRef',\n versions: ['v1'],\n});\n"],"names":[],"mappings":";;AAmB8B,WAAW,MAAA,CAYtC;AAAA,EACD,IAAA,EAAM,qBAAA;AAAA,EACN,QAAA,EAAU,CAAC,IAAI;AACjB,CAAC"}
@@ -0,0 +1,7 @@
1
+ import { OpaqueType } from '../../../opaque-internal/src/OpaqueType.esm.js';
2
+
3
+ OpaqueType.create({
4
+ type: "@backstage/SubRouteRef",
5
+ versions: ["v1"]
6
+ });
7
+ //# sourceMappingURL=OpaqueSubRouteRef.esm.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"OpaqueSubRouteRef.esm.js","sources":["../../../../../frontend-internal/src/routing/OpaqueSubRouteRef.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { RouteRef, SubRouteRef } from '@backstage/frontend-plugin-api';\nimport { OpaqueType } from '@internal/opaque';\n\nexport const OpaqueSubRouteRef = OpaqueType.create<{\n public: SubRouteRef;\n versions: {\n readonly version: 'v1';\n\n getParams(): string[];\n getParent(): RouteRef;\n getDescription(): string;\n };\n}>({\n type: '@backstage/SubRouteRef',\n versions: ['v1'],\n});\n"],"names":[],"mappings":";;AAmBiC,WAAW,MAAA,CASzC;AAAA,EACD,IAAA,EAAM,wBAAA;AAAA,EACN,QAAA,EAAU,CAAC,IAAI;AACjB,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as _backstage_config from '@backstage/config';
2
2
  import { Config } from '@backstage/config';
3
3
  import * as _backstage_frontend_plugin_api from '@backstage/frontend-plugin-api';
4
- import { ApiFactory, ApiRef, AlertApi, AlertMessage, FeatureFlagsApi, FeatureFlagState, FeatureFlag, FeatureFlagsSaveOptions, AnalyticsApi, AnalyticsEvent, TranslationApi, TranslationRef, TranslationSnapshot, ConfigApi as ConfigApi$1, DiscoveryApi as DiscoveryApi$1, IdentityApi as IdentityApi$1, StorageApi as StorageApi$1, ErrorApi as ErrorApi$1, FetchApi as FetchApi$1, ExtensionDataRef, AppNode, ExtensionDefinitionParameters, ExtensionDefinition, RouteRef, FrontendFeature } from '@backstage/frontend-plugin-api';
4
+ import { ApiFactory, ApiRef, AlertApi, AlertMessage, FeatureFlagsApi, FeatureFlagState, FeatureFlag, FeatureFlagsSaveOptions, AnalyticsApi, AnalyticsEvent, TranslationApi, TranslationRef, TranslationSnapshot, ConfigApi as ConfigApi$1, DiscoveryApi as DiscoveryApi$1, IdentityApi as IdentityApi$1, StorageApi as StorageApi$1, ErrorApi as ErrorApi$1, FetchApi as FetchApi$1, ExtensionDataRef, AppNode, ExtensionDefinitionParameters, ExtensionDefinition, RouteRef, ExternalRouteRef, FrontendFeature } from '@backstage/frontend-plugin-api';
5
5
  import { PermissionApi } from '@backstage/plugin-permission-react';
6
6
  import { Observable, JsonObject, JsonValue } from '@backstage/types';
7
7
  import { EvaluatePermissionRequest, AuthorizeResult, EvaluatePermissionResponse } from '@backstage/plugin-permission-common';
@@ -794,7 +794,7 @@ declare function createExtensionTester<T extends ExtensionDefinitionParameters,
794
794
  type TestAppOptions<TApiPairs extends any[] = any[]> = {
795
795
  /**
796
796
  * An object of paths to mount route ref on, with the key being the path and the value
797
- * being the RouteRef that the path will be bound to. This allows the route refs to be
797
+ * being the route ref that the path will be bound to. This allows the route refs to be
798
798
  * used by `useRouteRef` in the rendered elements.
799
799
  *
800
800
  * @example
@@ -809,7 +809,7 @@ type TestAppOptions<TApiPairs extends any[] = any[]> = {
809
809
  * ```
810
810
  */
811
811
  mountedRoutes?: {
812
- [path: string]: RouteRef;
812
+ [path: string]: RouteRef | ExternalRouteRef;
813
813
  };
814
814
  /**
815
815
  * Additional configuration passed to the app when rendering elements inside it.
@@ -868,7 +868,7 @@ type RenderTestAppOptions<TApiPairs extends any[] = any[]> = {
868
868
  initialRouteEntries?: string[];
869
869
  /**
870
870
  * An object of paths to mount route refs on, with the key being the path and
871
- * the value being the RouteRef that the path will be bound to. This allows
871
+ * the value being the route ref that the path will be bound to. This allows
872
872
  * the route refs to be used by `useRouteRef` in the rendered elements.
873
873
  *
874
874
  * @example
@@ -882,7 +882,7 @@ type RenderTestAppOptions<TApiPairs extends any[] = any[]> = {
882
882
  * ```
883
883
  */
884
884
  mountedRoutes?: {
885
- [path: string]: RouteRef;
885
+ [path: string]: RouteRef | ExternalRouteRef;
886
886
  };
887
887
  /**
888
888
  * API overrides to provide to the test app. Use `mockApis` helpers
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage/frontend-test-utils",
3
- "version": "0.5.3-next.0",
3
+ "version": "0.5.3-next.1",
4
4
  "backstage": {
5
5
  "role": "web-library"
6
6
  },
@@ -33,13 +33,13 @@
33
33
  "dependencies": {
34
34
  "@backstage/config": "1.3.8-next.0",
35
35
  "@backstage/core-app-api": "1.20.1-next.0",
36
- "@backstage/core-plugin-api": "1.12.6-next.0",
36
+ "@backstage/core-plugin-api": "1.12.6-next.1",
37
37
  "@backstage/filter-predicates": "0.1.3-next.0",
38
- "@backstage/frontend-app-api": "0.16.3-next.0",
39
- "@backstage/frontend-plugin-api": "0.17.0-next.0",
40
- "@backstage/plugin-app": "0.4.6-next.0",
38
+ "@backstage/frontend-app-api": "0.16.3-next.1",
39
+ "@backstage/frontend-plugin-api": "0.17.0-next.1",
40
+ "@backstage/plugin-app": "0.4.6-next.1",
41
41
  "@backstage/plugin-app-react": "0.2.3-next.0",
42
- "@backstage/plugin-permission-common": "0.9.9-next.0",
42
+ "@backstage/plugin-permission-common": "0.9.9-next.1",
43
43
  "@backstage/plugin-permission-react": "0.5.1-next.0",
44
44
  "@backstage/test-utils": "1.7.18-next.0",
45
45
  "@backstage/types": "1.2.2",
@@ -49,7 +49,7 @@
49
49
  "zod": "^3.25.76 || ^4.0.0"
50
50
  },
51
51
  "devDependencies": {
52
- "@backstage/cli": "0.36.2-next.0",
52
+ "@backstage/cli": "0.36.2-next.1",
53
53
  "@testing-library/jest-dom": "^6.0.0",
54
54
  "@types/jest": "*",
55
55
  "@types/react": "^18.0.0",