frontend-test-utils: api cleanup, remove TestApiRegistry

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2026-02-06 22:48:22 +01:00
parent a18c64a285
commit b9110a5724
9 changed files with 63 additions and 157 deletions
@@ -18,11 +18,11 @@ 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';
import { getMockApiFactory, type MockWithApiFactory } from '../apis/utils';
import { getMockApiFactory, type MockWithApiFactory } from './utils';
/**
* Helper type for representing an API reference paired with a partial implementation.
* @public
* @ignore
*/
export type TestApiProviderPropsApiPair<TApi> = TApi extends infer TImpl
? readonly [ApiRef<TApi>, Partial<TImpl>]
@@ -30,7 +30,7 @@ export type TestApiProviderPropsApiPair<TApi> = TApi extends infer TImpl
/**
* Helper type for representing an array of API reference pairs.
* @public
* @ignore
*/
export type TestApiProviderPropsApiPairs<TApiPairs> = {
[TIndex in keyof TApiPairs]: TestApiProviderPropsApiPair<TApiPairs[TIndex]>;
@@ -43,16 +43,37 @@ export type TestApiProviderPropsApiPairs<TApiPairs> = {
export type TestApiPairs<TApiPairs> = TestApiProviderPropsApiPairs<TApiPairs>;
/**
* Type for entries that can be passed to TestApiProvider/TestApiRegistry.
* Type for entries that can be passed to TestApiProvider.
* Can be either a traditional [apiRef, implementation] tuple or a mock API instance
* marked with the mockApiFactorySymbol.
*
* @public
* @internal
*/
export type TestApiProviderEntry =
| readonly [ApiRef<any>, any]
| MockWithApiFactory<any>;
/** @internal */
export function resolveTestApiEntries(
apis: readonly (TestApiProviderEntry | readonly [ApiRef<any>, any])[],
): ApiHolder {
const apiMap = new Map<string, unknown>();
for (const entry of apis) {
const mockFactory = getMockApiFactory(entry);
if (mockFactory) {
apiMap.set(mockFactory.api.id, mockFactory.factory({}));
} else {
const [apiRef, impl] = entry as readonly [ApiRef<any>, any];
apiMap.set(apiRef.id, impl);
}
}
return {
get: <T,>(ref: ApiRef<T>) => apiMap.get(ref.id) as T | undefined,
};
}
/**
* Properties for the {@link TestApiProvider} component.
*
@@ -65,81 +86,6 @@ export type TestApiProviderProps<TApiPairs extends any[]> = {
children: ReactNode;
};
/**
* The `TestApiRegistry` is an {@link @backstage/frontend-plugin-api#ApiHolder} implementation
* that is particularly well suited for development and test environments such as
* unit tests, storybooks, and isolated plugin development setups.
*
* @remarks
*
* For most test scenarios, prefer using the `apis` option in `renderInTestApp` or
* `createExtensionTester` instead of creating a registry directly.
*
* @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
* import { identityApiRef } from '@backstage/frontend-plugin-api';
* import { mockApis } from '@backstage/frontend-test-utils';
*
* // Traditional tuple syntax
* const apis1 = TestApiRegistry.from(
* [identityApiRef, mockApis.identity({ userEntityRef: 'user:default/guest' })],
* );
*
* // Direct mock API instance (no tuple needed)
* const apis2 = TestApiRegistry.from(
* mockApis.identity({ userEntityRef: 'user:default/guest' }),
* mockApis.alert(),
* );
* ```
*
* @public
* @param apis - A list of pairs mapping an ApiRef to its respective implementation,
* or mock API instances marked with the mockApiSymbol.
*/
static from(...apis: readonly TestApiProviderEntry[]) {
const apiMap = new Map<string, unknown>();
for (const entry of apis) {
const mockFactory = getMockApiFactory(entry);
if (mockFactory) {
// Handle mock API instances marked with the symbol
const impl = mockFactory.factory({});
apiMap.set(mockFactory.api.id, impl);
} else if (Array.isArray(entry)) {
// Handle traditional [apiRef, impl] tuples
const [apiRef, impl] = entry;
apiMap.set(apiRef.id, impl);
} else {
throw new Error(
`Invalid API entry provided to TestApiRegistry.from(). Expected either [apiRef, impl] tuple or a mock API instance.`,
);
}
}
return new TestApiRegistry(apiMap);
}
private constructor(private readonly apis: Map<string, unknown>) {}
/**
* Returns an implementation of the API.
*
* @public
*/
get<T>(api: ApiRef<T>): T | undefined {
return this.apis.get(api.id) as T | undefined;
}
}
/**
* The `TestApiProvider` is a Utility API context provider for standalone rendering
* scenarios where you're not using `renderInTestApp` or other test utilities.
@@ -155,19 +101,22 @@ export class TestApiRegistry implements ApiHolder {
* @example
* ```tsx
* import { render } from '\@testing-library/react';
* import { identityApiRef } from '\@backstage/frontend-plugin-api';
* import { myCustomApiRef } from '../apis';
* import { TestApiProvider, mockApis } from '\@backstage/frontend-test-utils';
*
* // Traditional tuple syntax
* // Mock custom APIs with tuple syntax
* const myCustomApiMock = { myMethod: jest.fn() };
* render(
* <TestApiProvider
* apis={[[identityApiRef, mockApis.identity({ userEntityRef: 'user:default/guest' })]]}
* apis={[
* [myCustomApiRef, myCustomApiMock]
* ]}
* >
* <MyComponent />
* </TestApiProvider>
* );
*
* // Direct mock API instances (no tuples needed)
* // Use with built-in mock APIs (no tuples needed)
* render(
* <TestApiProvider
* apis={[
@@ -187,7 +136,7 @@ export const TestApiProvider = <T extends any[]>(
) => {
return (
<ApiProvider
apis={TestApiRegistry.from(...props.apis)}
apis={resolveTestApiEntries(props.apis)}
children={props.children}
/>
);
@@ -21,6 +21,14 @@ export {
type MockWithApiFactory,
attachMockApiFactory,
} from './utils';
export {
TestApiProvider,
type TestApiProviderPropsApiPair,
type TestApiProviderPropsApiPairs,
type TestApiPairs,
type TestApiProviderEntry,
} from './TestApiProvider';
export type { TestApiProviderProps } from './TestApiProvider';
/**
* Mock API classes are exported as types only to prevent direct instantiation.
@@ -38,7 +38,8 @@ import { readAppExtensionsConfig } from '../../../frontend-app-api/src/tree/read
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { createErrorCollector } from '../../../frontend-app-api/src/wiring/createErrorCollector';
import { OpaqueExtensionDefinition } from '@internal/frontend';
import { TestApiRegistry, type TestApiPairs } from '../utils';
import { type TestApiPairs } from '../apis';
import { resolveTestApiEntries } from '../apis/TestApiProvider';
/**
* Represents a snapshot of an extension in the app tree.
@@ -281,9 +282,7 @@ export class ExtensionTester<UOutput extends ExtensionDataRef> {
collector,
);
const apiHolder = this.#apis
? TestApiRegistry.from(...this.#apis)
: TestApiRegistry.from();
const apiHolder = resolveTestApiEntries(this.#apis ?? []);
instantiateAppNodeTree(tree.root, apiHolder, collector);
@@ -36,7 +36,7 @@ import {
} from '@backstage/frontend-plugin-api';
import { RouterBlueprint } from '@backstage/plugin-app-react';
import appPlugin from '@backstage/plugin-app';
import { type TestApiProviderPropsApiPairs } from '../utils';
import { type TestApiProviderPropsApiPairs } from '../apis';
import { getMockApiFactory, type MockWithApiFactory } from '../apis/utils';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import type { CreateSpecializedAppInternalOptions } from '../../../frontend-app-api/src/wiring/createSpecializedApp';
@@ -33,7 +33,7 @@ import { JsonObject } from '@backstage/types';
import { ConfigReader } from '@backstage/config';
import { MemoryRouter } from 'react-router-dom';
import { RouterBlueprint } from '@backstage/plugin-app-react';
import { type TestApiProviderPropsApiPairs } from '../utils';
import { type TestApiProviderPropsApiPairs } from '../apis';
import { getMockApiFactory, type MockWithApiFactory } from '../apis/utils';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import type { CreateSpecializedAppInternalOptions } from '../../../frontend-app-api/src/wiring/createSpecializedApp';
+1 -2
View File
@@ -22,10 +22,9 @@
export * from './apis';
export * from './app';
export * from './utils';
// Explicit export to satisfy API Extractor
export type { TestApiPairs } from './utils';
export type { TestApiPairs } from './apis';
export { withLogCollector } from '@backstage/test-utils';
@@ -1,25 +0,0 @@
/*
* 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,
type TestApiProviderEntry,
} from './TestApiProvider';
export type { TestApiProviderProps } from './TestApiProvider';