feat(frontend-test-utils): add API override support to test utilities
Added support for API overrides in `createExtensionTester` and
`renderInTestApp` to allow tests to override specific APIs without
requiring wrapper components. This provides app-level API overrides
that are available throughout the entire extension tree.
The `apis` option follows the same typing pattern as `TestApiProvider`
from `@backstage/test-utils` for consistency and type safety.
Example usage:
```typescript
const tester = createExtensionTester(MyExtension, {
apis: [
[errorApiRef, mockErrorApi],
[analyticsApiRef, mockAnalyticsApi],
],
});
renderInTestApp(<MyComponent />, {
apis: [
[errorApiRef, mockErrorApi],
[analyticsApiRef, mockAnalyticsApi],
],
});
```
This enables cleaner tests with app-level API overrides, eliminating
the need to wrap components with TestApiProvider in many cases.
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
---
|
||||
'@backstage/frontend-test-utils': patch
|
||||
---
|
||||
|
||||
Added support for API overrides in `createExtensionTester` and `renderInTestApp`. You can now pass an `apis` option to override specific APIs when testing extensions:
|
||||
|
||||
```typescript
|
||||
// Override APIs in createExtensionTester
|
||||
const tester = createExtensionTester(myExtension, {
|
||||
apis: [
|
||||
[errorApiRef, mockErrorApi],
|
||||
[analyticsApiRef, mockAnalyticsApi],
|
||||
],
|
||||
});
|
||||
|
||||
// Override APIs in renderInTestApp
|
||||
renderInTestApp(<MyComponent />, {
|
||||
apis: [[errorApiRef, mockErrorApi]],
|
||||
});
|
||||
```
|
||||
|
||||
The package now also exports its own implementations of `TestApiProvider`, `TestApiRegistry`, and related types, rather than re-exporting them from `@backstage/test-utils`. This consolidates common types used internally by the test utilities.
|
||||
@@ -76,6 +76,45 @@ describe('Entity details component', () => {
|
||||
|
||||
This pattern also works for many other context providers. An important example is the `EntityProvider` from the `@backstage/plugin-catalog-react` package, which you can use to provide a mocked entity context to the component.
|
||||
|
||||
Alternatively, you can pass API overrides directly to `renderInTestApp` using the `apis` option:
|
||||
|
||||
```tsx
|
||||
import { screen } from '@testing-library/react';
|
||||
import { renderInTestApp } from '@backstage/frontend-test-utils';
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
import { EntityDetails } from './plugin';
|
||||
|
||||
describe('Entity details component', () => {
|
||||
it('should render the entity name and owner', async () => {
|
||||
const catalogApiMock = {
|
||||
async getEntityFacets() {
|
||||
return {
|
||||
facets: {
|
||||
'relations.ownedBy': [{ count: 1, value: 'group:default/tools' }],
|
||||
},
|
||||
},
|
||||
}
|
||||
} satisfies Partial<typeof catalogApiRef.T>;
|
||||
|
||||
const entityRef = stringifyEntityRef({
|
||||
kind: 'Component',
|
||||
namespace: 'default',
|
||||
name: 'test',
|
||||
});
|
||||
|
||||
await renderInTestApp(<EntityDetails entityRef={entityRef} />, {
|
||||
apis: [[catalogApiRef, catalogApiMock]],
|
||||
});
|
||||
|
||||
await expect(
|
||||
screen.findByText('The entity "test" is owned by "tools"'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
This approach provides the API overrides at the app level, which is useful when testing extensions that depend on APIs deep in the component tree.
|
||||
|
||||
## Testing extensions
|
||||
|
||||
To facilitate testing of frontend extensions, the `@backstage/frontend-test-utils` package provides a tester class which starts up an entire frontend harness, complete with a number of default features. You can then provide overrides for extensions whose behavior you need to adjust for the test run.
|
||||
@@ -102,7 +141,29 @@ describe('Index page', () => {
|
||||
});
|
||||
```
|
||||
|
||||
This pattern also allows you to wrap the extension with context providers, such as the `TestApiProvider` that was introduced [above](#testing-react-components).
|
||||
This pattern also allows you to wrap the extension with context providers, such as the `TestApiProvider` that was introduced [above](#testing-react-components). Alternatively, you can provide API overrides directly to `createExtensionTester`:
|
||||
|
||||
```tsx
|
||||
import { screen } from '@testing-library/react';
|
||||
import { createExtensionTester } from '@backstage/frontend-test-utils';
|
||||
import { analyticsApiRef } from '@backstage/frontend-plugin-api';
|
||||
import { indexPageExtension } from './plugin';
|
||||
|
||||
describe('Index page', () => {
|
||||
it('should render and track analytics', async () => {
|
||||
const analyticsApiMock = { captureEvent: jest.fn() };
|
||||
|
||||
await renderInTestApp(
|
||||
createExtensionTester(indexPageExtension, {
|
||||
apis: [[analyticsApiRef, analyticsApiMock]],
|
||||
}).reactElement(),
|
||||
);
|
||||
|
||||
expect(screen.getByText('Index Page')).toBeInTheDocument();
|
||||
expect(analyticsApiMock.captureEvent).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Note that the `.reactElement()` method will look for the `coreExtensionData.reactElement` data in the extension outputs. If that doesn't exist and the extension outputs something else that you want to test, you can access the output data using the `.get(dataRef)` method instead.
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
```ts
|
||||
import { AnalyticsApi } from '@backstage/frontend-plugin-api';
|
||||
import { AnalyticsEvent } from '@backstage/frontend-plugin-api';
|
||||
import { ApiHolder } from '@backstage/frontend-plugin-api';
|
||||
import { ApiMock } from '@backstage/test-utils';
|
||||
import { ApiRef } from '@backstage/frontend-plugin-api';
|
||||
import { AppNode } from '@backstage/frontend-plugin-api';
|
||||
import { AppNodeInstance } from '@backstage/frontend-plugin-api';
|
||||
import { ErrorWithContext } from '@backstage/test-utils';
|
||||
@@ -14,6 +16,7 @@ import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionDefinitionParameters } from '@backstage/frontend-plugin-api';
|
||||
import { FrontendFeature } from '@backstage/frontend-plugin-api';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { JSX as JSX_2 } from 'react/jsx-runtime';
|
||||
import { mockApis } from '@backstage/test-utils';
|
||||
import { MockConfigApi } from '@backstage/test-utils';
|
||||
import { MockErrorApi } from '@backstage/test-utils';
|
||||
@@ -23,22 +26,24 @@ import { MockFetchApiOptions } from '@backstage/test-utils';
|
||||
import { MockPermissionApi } from '@backstage/test-utils';
|
||||
import { MockStorageApi } from '@backstage/test-utils';
|
||||
import { MockStorageBucket } from '@backstage/test-utils';
|
||||
import { ReactNode } from 'react';
|
||||
import { registerMswTestHooks } from '@backstage/test-utils';
|
||||
import { RenderResult } from '@testing-library/react';
|
||||
import { RouteRef } from '@backstage/frontend-plugin-api';
|
||||
import { TestApiProvider } from '@backstage/test-utils';
|
||||
import { TestApiProviderProps } from '@backstage/test-utils';
|
||||
import { TestApiRegistry } from '@backstage/test-utils';
|
||||
import { testingLibraryDomTypesQueries } from '@testing-library/dom/types/queries';
|
||||
import { withLogCollector } from '@backstage/test-utils';
|
||||
|
||||
export { ApiMock };
|
||||
|
||||
// @public (undocumented)
|
||||
export function createExtensionTester<T extends ExtensionDefinitionParameters>(
|
||||
export function createExtensionTester<
|
||||
T extends ExtensionDefinitionParameters,
|
||||
TApiPairs extends any[] = any[],
|
||||
>(
|
||||
subject: ExtensionDefinition<T>,
|
||||
options?: {
|
||||
config?: T['configInput'];
|
||||
apis?: readonly [...TestApiPairs<TApiPairs>];
|
||||
},
|
||||
): ExtensionTester<NonNullable<T['output']>>;
|
||||
|
||||
@@ -115,9 +120,9 @@ export { MockStorageBucket };
|
||||
export { registerMswTestHooks };
|
||||
|
||||
// @public
|
||||
export function renderInTestApp(
|
||||
export function renderInTestApp<TApiPairs extends any[] = any[]>(
|
||||
element: JSX.Element,
|
||||
options?: TestAppOptions,
|
||||
options?: TestAppOptions<TApiPairs>,
|
||||
): RenderResult;
|
||||
|
||||
// @public
|
||||
@@ -133,20 +138,47 @@ export type RenderTestAppOptions = {
|
||||
initialRouteEntries?: string[];
|
||||
};
|
||||
|
||||
export { TestApiProvider };
|
||||
|
||||
export { TestApiProviderProps };
|
||||
|
||||
export { TestApiRegistry };
|
||||
// @public
|
||||
export type TestApiPairs<TApiPairs> = TestApiProviderPropsApiPairs<TApiPairs>;
|
||||
|
||||
// @public
|
||||
export type TestAppOptions = {
|
||||
export const TestApiProvider: <T extends any[]>(
|
||||
props: TestApiProviderProps<T>,
|
||||
) => JSX_2.Element;
|
||||
|
||||
// @public
|
||||
export type TestApiProviderProps<TApiPairs extends any[]> = {
|
||||
apis: readonly [...TestApiProviderPropsApiPairs<TApiPairs>];
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type TestApiProviderPropsApiPair<TApi> = TApi extends infer TImpl
|
||||
? readonly [ApiRef<TApi>, Partial<TImpl>]
|
||||
: never;
|
||||
|
||||
// @public
|
||||
export type TestApiProviderPropsApiPairs<TApiPairs> = {
|
||||
[TIndex in keyof TApiPairs]: TestApiProviderPropsApiPair<TApiPairs[TIndex]>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export class TestApiRegistry implements ApiHolder {
|
||||
static from<TApiPairs extends any[]>(
|
||||
...apis: readonly [...TestApiProviderPropsApiPairs<TApiPairs>]
|
||||
): TestApiRegistry;
|
||||
get<T>(api: ApiRef<T>): T | undefined;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type TestAppOptions<TApiPairs extends any[] = any[]> = {
|
||||
mountedRoutes?: {
|
||||
[path: string]: RouteRef;
|
||||
};
|
||||
config?: JsonObject;
|
||||
features?: FrontendFeature[];
|
||||
initialRouteEntries?: string[];
|
||||
apis?: readonly [...TestApiPairs<TApiPairs>];
|
||||
};
|
||||
|
||||
export { withLogCollector };
|
||||
|
||||
@@ -15,12 +15,16 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
analyticsApiRef,
|
||||
coreExtensionData,
|
||||
createExtension,
|
||||
createExtensionDataRef,
|
||||
createExtensionInput,
|
||||
useAnalytics,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { createExtensionTester } from './createExtensionTester';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { renderInTestApp } from './renderInTestApp';
|
||||
|
||||
const stringDataRef = createExtensionDataRef<string>().with({
|
||||
id: 'test.string',
|
||||
@@ -152,4 +156,36 @@ describe('createExtensionTester', () => {
|
||||
|
||||
expect([test, test2, test3]).toBeDefined();
|
||||
});
|
||||
|
||||
it('should support API overrides via options', async () => {
|
||||
const analyticsApiMock = { captureEvent: jest.fn() };
|
||||
|
||||
const TestComponent = () => {
|
||||
const analytics = useAnalytics();
|
||||
analytics.captureEvent('test', 'value');
|
||||
return <div>Test</div>;
|
||||
};
|
||||
|
||||
const extension = createExtension({
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: [coreExtensionData.reactElement],
|
||||
factory: () => [coreExtensionData.reactElement(<TestComponent />)],
|
||||
});
|
||||
|
||||
const tester = createExtensionTester(extension, {
|
||||
apis: [[analyticsApiRef, analyticsApiMock]],
|
||||
});
|
||||
|
||||
renderInTestApp(tester.reactElement(), {
|
||||
apis: [[analyticsApiRef, analyticsApiMock]],
|
||||
});
|
||||
|
||||
expect(screen.getByText('Test')).toBeInTheDocument();
|
||||
expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: 'test',
|
||||
subject: 'value',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,8 +37,8 @@ import { instantiateAppNodeTree } from '../../../frontend-app-api/src/tree/insta
|
||||
import { readAppExtensionsConfig } from '../../../frontend-app-api/src/tree/readAppExtensionsConfig';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { createErrorCollector } from '../../../frontend-app-api/src/wiring/createErrorCollector';
|
||||
import { TestApiRegistry } from '@backstage/test-utils';
|
||||
import { OpaqueExtensionDefinition } from '@internal/frontend';
|
||||
import { TestApiRegistry, type TestApiPairs } from '../utils';
|
||||
|
||||
/** @public */
|
||||
export class ExtensionQuery<UOutput extends ExtensionDataRef> {
|
||||
@@ -78,16 +78,23 @@ export class ExtensionQuery<UOutput extends ExtensionDataRef> {
|
||||
/** @public */
|
||||
export class ExtensionTester<UOutput extends ExtensionDataRef> {
|
||||
/** @internal */
|
||||
static forSubject<T extends ExtensionDefinitionParameters>(
|
||||
static forSubject<
|
||||
T extends ExtensionDefinitionParameters,
|
||||
TApiPairs extends any[],
|
||||
>(
|
||||
subject: ExtensionDefinition<T>,
|
||||
options?: { config?: T['configInput'] },
|
||||
options?: {
|
||||
config?: T['configInput'];
|
||||
apis?: readonly [...TestApiPairs<TApiPairs>];
|
||||
},
|
||||
): ExtensionTester<NonNullable<T['output']>> {
|
||||
const tester = new ExtensionTester();
|
||||
const tester = new ExtensionTester(options?.apis);
|
||||
tester.add(subject, options as T['configInput'] & {});
|
||||
return tester;
|
||||
}
|
||||
|
||||
#tree?: AppTree;
|
||||
#apis?: readonly any[];
|
||||
|
||||
readonly #extensions = new Array<{
|
||||
id: string;
|
||||
@@ -96,6 +103,10 @@ export class ExtensionTester<UOutput extends ExtensionDataRef> {
|
||||
config?: JsonValue;
|
||||
}>();
|
||||
|
||||
private constructor(apis?: readonly any[]) {
|
||||
this.#apis = apis;
|
||||
}
|
||||
|
||||
add<T extends ExtensionDefinitionParameters>(
|
||||
extension: ExtensionDefinition<T>,
|
||||
options?: { config?: T['configInput'] },
|
||||
@@ -206,7 +217,11 @@ export class ExtensionTester<UOutput extends ExtensionDataRef> {
|
||||
collector,
|
||||
);
|
||||
|
||||
instantiateAppNodeTree(tree.root, TestApiRegistry.from(), collector);
|
||||
const apiHolder = this.#apis
|
||||
? TestApiRegistry.from(...this.#apis)
|
||||
: TestApiRegistry.from();
|
||||
|
||||
instantiateAppNodeTree(tree.root, apiHolder, collector);
|
||||
|
||||
const errors = collector.collectErrors();
|
||||
if (errors) {
|
||||
@@ -260,9 +275,15 @@ export class ExtensionTester<UOutput extends ExtensionDataRef> {
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function createExtensionTester<T extends ExtensionDefinitionParameters>(
|
||||
export function createExtensionTester<
|
||||
T extends ExtensionDefinitionParameters,
|
||||
TApiPairs extends any[] = any[],
|
||||
>(
|
||||
subject: ExtensionDefinition<T>,
|
||||
options?: { config?: T['configInput'] },
|
||||
options?: {
|
||||
config?: T['configInput'];
|
||||
apis?: readonly [...TestApiPairs<TApiPairs>];
|
||||
},
|
||||
): ExtensionTester<NonNullable<T['output']>> {
|
||||
return ExtensionTester.forSubject(subject, options);
|
||||
}
|
||||
|
||||
@@ -80,4 +80,35 @@ describe('renderInTestApp', () => {
|
||||
|
||||
expect(screen.getByText('Second Page')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should support API overrides via options', async () => {
|
||||
const IndexPage = () => {
|
||||
const analyticsApi = useAnalytics();
|
||||
const handleClick = useCallback(() => {
|
||||
analyticsApi.captureEvent('click', 'Test action');
|
||||
}, [analyticsApi]);
|
||||
return (
|
||||
<div>
|
||||
<button onClick={handleClick}>Click me</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const analyticsApiMock = new MockAnalyticsApi();
|
||||
|
||||
renderInTestApp(<IndexPage />, {
|
||||
apis: [[analyticsApiRef, analyticsApiMock]],
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Click me' }));
|
||||
|
||||
expect(analyticsApiMock.getEvents()).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
action: 'click',
|
||||
subject: 'Test action',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<TApiPairs extends any[] = any[]> = {
|
||||
/**
|
||||
* 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(<MyComponent />, {
|
||||
* apis: [
|
||||
* [errorApiRef, mockErrorApi],
|
||||
* [analyticsApiRef, mockAnalyticsApi],
|
||||
* ]
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
apis?: readonly [...TestApiPairs<TApiPairs>];
|
||||
};
|
||||
|
||||
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<TApiPairs extends any[] = any[]>(
|
||||
element: JSX.Element,
|
||||
options?: TestAppOptions,
|
||||
options?: TestAppOptions<TApiPairs>,
|
||||
): RenderResult {
|
||||
const extensions: Array<ExtensionDefinition> = [
|
||||
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([
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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> = TApi extends infer TImpl
|
||||
? readonly [ApiRef<TApi>, Partial<TImpl>]
|
||||
: never;
|
||||
|
||||
/**
|
||||
* Helper type for representing an array of API reference pairs.
|
||||
* @public
|
||||
*/
|
||||
export type TestApiProviderPropsApiPairs<TApiPairs> = {
|
||||
[TIndex in keyof TApiPairs]: TestApiProviderPropsApiPair<TApiPairs[TIndex]>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Shorter alias for TestApiProviderPropsApiPairs for use in function signatures.
|
||||
* @public
|
||||
*/
|
||||
export type TestApiPairs<TApiPairs> = TestApiProviderPropsApiPairs<TApiPairs>;
|
||||
|
||||
/**
|
||||
* Properties for the {@link TestApiProvider} component.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type TestApiProviderProps<TApiPairs extends any[]> = {
|
||||
apis: readonly [...TestApiProviderPropsApiPairs<TApiPairs>];
|
||||
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<TApiPairs extends any[]>(
|
||||
...apis: readonly [...TestApiProviderPropsApiPairs<TApiPairs>]
|
||||
) {
|
||||
return new TestApiRegistry(
|
||||
new Map(apis.map(([api, impl]) => [api.id, impl])),
|
||||
);
|
||||
}
|
||||
|
||||
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 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 <p> for To the following </p> 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(
|
||||
* <ApiProvider
|
||||
* apis={ApiRegistry.from([
|
||||
* [identityApiRef, mockIdentityApi as unknown as IdentityApi]
|
||||
* ])}
|
||||
* >
|
||||
* ...
|
||||
* </ApiProvider>
|
||||
* )
|
||||
* ```
|
||||
*
|
||||
* To the following:
|
||||
*
|
||||
* ```tsx
|
||||
* renderInTestApp(
|
||||
* <TestApiProvider apis={[[identityApiRef, mockIdentityApi]]}>
|
||||
* ...
|
||||
* </TestApiProvider>
|
||||
* )
|
||||
* ```
|
||||
*
|
||||
* 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 = <T extends any[]>(
|
||||
props: TestApiProviderProps<T>,
|
||||
) => {
|
||||
return (
|
||||
<ApiProvider
|
||||
apis={TestApiRegistry.from(...props.apis)}
|
||||
children={props.children}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
Reference in New Issue
Block a user