+ );
+ };
+
+ const analyticsApiMock = new MockAnalyticsApi();
+
+ renderInTestApp(, {
+ apis: [[analyticsApiRef, analyticsApiMock]],
+ });
+
+ fireEvent.click(screen.getByRole('button', { name: 'Click me' }));
+
+ expect(analyticsApiMock.getEvents()).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ action: 'click',
+ subject: 'Test action',
+ }),
+ ]),
+ );
+ });
});
diff --git a/packages/frontend-test-utils/src/app/renderInTestApp.tsx b/packages/frontend-test-utils/src/app/renderInTestApp.tsx
index f7b1d4b58b..0b1a1fcf61 100644
--- a/packages/frontend-test-utils/src/app/renderInTestApp.tsx
+++ b/packages/frontend-test-utils/src/app/renderInTestApp.tsx
@@ -31,9 +31,11 @@ import {
createFrontendPlugin,
FrontendFeature,
createFrontendModule,
+ ApiBlueprint,
} from '@backstage/frontend-plugin-api';
import { RouterBlueprint } from '@backstage/plugin-app-react';
import appPlugin from '@backstage/plugin-app';
+import { type TestApiPairs } from '../utils';
const DEFAULT_MOCK_CONFIG = {
app: { baseUrl: 'http://localhost:3000' },
@@ -44,7 +46,7 @@ const DEFAULT_MOCK_CONFIG = {
* Options to customize the behavior of the test app.
* @public
*/
-export type TestAppOptions = {
+export type TestAppOptions = {
/**
* 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
@@ -77,6 +79,21 @@ export type TestAppOptions = {
* Initial route entries to use for the router.
*/
initialRouteEntries?: string[];
+
+ /**
+ * API overrides to provide to the test app.
+ *
+ * @example
+ * ```ts
+ * renderInTestApp(, {
+ * apis: [
+ * [errorApiRef, mockErrorApi],
+ * [analyticsApiRef, mockAnalyticsApi],
+ * ]
+ * })
+ * ```
+ */
+ apis?: readonly [...TestApiPairs];
};
const NavItem = (props: {
@@ -143,9 +160,9 @@ const appPluginOverride = appPlugin.withOverrides({
* @public
* Renders the given element in a test app, for use in unit tests.
*/
-export function renderInTestApp(
+export function renderInTestApp(
element: JSX.Element,
- options?: TestAppOptions,
+ options?: TestAppOptions,
): RenderResult {
const extensions: Array = [
createExtension({
@@ -206,6 +223,27 @@ export function renderInTestApp(
features.push(...options.features);
}
+ // If API overrides are provided, add them as a module for the 'app' plugin
+ // This must come after appPluginOverride so it can override app's default APIs
+ if (options?.apis) {
+ features.push(
+ createFrontendModule({
+ pluginId: 'app',
+ extensions: options.apis.map(([apiRef, implementation], index) =>
+ ApiBlueprint.make({
+ name: `test-api-override-${index}`,
+ params: defineParams =>
+ defineParams({
+ api: apiRef,
+ deps: {},
+ factory: () => implementation,
+ }),
+ }),
+ ),
+ }),
+ );
+ }
+
const app = createSpecializedApp({
features,
config: ConfigReader.fromConfigs([
diff --git a/packages/frontend-test-utils/src/index.ts b/packages/frontend-test-utils/src/index.ts
index cab66895e0..b709e7ac03 100644
--- a/packages/frontend-test-utils/src/index.ts
+++ b/packages/frontend-test-utils/src/index.ts
@@ -22,9 +22,10 @@
export * from './apis';
export * from './app';
+export * from './utils';
-export { TestApiProvider, TestApiRegistry } from '@backstage/test-utils';
-export type { TestApiProviderProps } from '@backstage/test-utils';
+// Explicit export to satisfy API Extractor
+export type { TestApiPairs } from './utils';
export { withLogCollector } from '@backstage/test-utils';
diff --git a/packages/frontend-test-utils/src/utils/TestApiProvider.tsx b/packages/frontend-test-utils/src/utils/TestApiProvider.tsx
new file mode 100644
index 0000000000..1b09016192
--- /dev/null
+++ b/packages/frontend-test-utils/src/utils/TestApiProvider.tsx
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2020 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { ReactNode } from 'react';
+// eslint-disable-next-line @backstage/no-relative-monorepo-imports
+import { ApiProvider } from '../../../core-app-api/src/apis/system';
+import { ApiHolder, ApiRef } from '@backstage/frontend-plugin-api';
+
+/**
+ * Helper type for representing an API reference paired with a partial implementation.
+ * @public
+ */
+export type TestApiProviderPropsApiPair = TApi extends infer TImpl
+ ? readonly [ApiRef, Partial]
+ : never;
+
+/**
+ * Helper type for representing an array of API reference pairs.
+ * @public
+ */
+export type TestApiProviderPropsApiPairs = {
+ [TIndex in keyof TApiPairs]: TestApiProviderPropsApiPair;
+};
+
+/**
+ * Shorter alias for TestApiProviderPropsApiPairs for use in function signatures.
+ * @public
+ */
+export type TestApiPairs = TestApiProviderPropsApiPairs;
+
+/**
+ * Properties for the {@link TestApiProvider} component.
+ *
+ * @public
+ */
+export type TestApiProviderProps = {
+ apis: readonly [...TestApiProviderPropsApiPairs];
+ children: ReactNode;
+};
+
+/**
+ * The `TestApiRegistry` is an {@link @backstage/core-plugin-api#ApiHolder} implementation
+ * that is particularly well suited for development and test environments such as
+ * unit tests, storybooks, and isolated plugin development setups.
+ *
+ * @public
+ */
+export class TestApiRegistry implements ApiHolder {
+ /**
+ * Creates a new {@link TestApiRegistry} with a list of API implementation pairs.
+ *
+ * Similar to the {@link TestApiProvider}, there is no need to provide a full
+ * implementation of each API, it's enough to implement the methods that are tested.
+ *
+ * @example
+ * ```ts
+ * const apis = TestApiRegistry.from(
+ * [configApiRef, new ConfigReader({})],
+ * [identityApiRef, { getUserId: () => 'tester' }],
+ * );
+ * ```
+ *
+ * @public
+ * @param apis - A list of pairs mapping an ApiRef to its respective implementation.
+ */
+ static from(
+ ...apis: readonly [...TestApiProviderPropsApiPairs]
+ ) {
+ return new TestApiRegistry(
+ new Map(apis.map(([api, impl]) => [api.id, impl])),
+ );
+ }
+
+ private constructor(private readonly apis: Map) {}
+
+ /**
+ * Returns an implementation of the API.
+ *
+ * @public
+ */
+ get(api: ApiRef): T | undefined {
+ return this.apis.get(api.id) as T | undefined;
+ }
+}
+
+/**
+ * The `TestApiProvider` is a Utility API context provider that is particularly
+ * well suited for development and test environments such as unit tests, storybooks,
+ * and isolated plugin development setups.
+ *
+ * It lets you provide any number of API implementations, without necessarily
+ * having to fully implement each of the APIs.
+ *
+ * @remarks
+ * todo: remove this remark tag and ship in the api-reference. There's some odd formatting going on when this is made into a markdown doc, that there's no line break between
+ * the emitted
for To the following
so what happens is that when parsing in docusaurus, it thinks that the code block is mdx rather than a code
+ * snippet. Just omitting this from the report for now until we can work out how to fix later.
+ * A migration from `ApiRegistry` and `ApiProvider` might look like this, from:
+ *
+ * ```tsx
+ * renderInTestApp(
+ *
+ * ...
+ *
+ * )
+ * ```
+ *
+ * To the following:
+ *
+ * ```tsx
+ * renderInTestApp(
+ *
+ * ...
+ *
+ * )
+ * ```
+ *
+ * Note that the cast to `IdentityApi` is no longer needed as long as the mock API
+ * implements a subset of the `IdentityApi`.
+ *
+ * @public
+ */
+export const TestApiProvider = (
+ props: TestApiProviderProps,
+) => {
+ return (
+
+ );
+};
diff --git a/packages/frontend-test-utils/src/utils/index.ts b/packages/frontend-test-utils/src/utils/index.ts
new file mode 100644
index 0000000000..2f6bfb5f9c
--- /dev/null
+++ b/packages/frontend-test-utils/src/utils/index.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export {
+ TestApiProvider,
+ TestApiRegistry,
+ type TestApiProviderPropsApiPair,
+ type TestApiProviderPropsApiPairs,
+ type TestApiPairs,
+} from './TestApiProvider';
+export type { TestApiProviderProps } from './TestApiProvider';