Merge pull request #8089 from backstage/rugvip/test-api
core,test-utils: deprecate ApiRegistry, add new TestApiProvider and TestApiRegistry
This commit is contained in:
@@ -5,6 +5,8 @@
|
||||
```ts
|
||||
import { AnalyticsApi } from '@backstage/core-plugin-api';
|
||||
import { AnalyticsEvent } from '@backstage/core-plugin-api';
|
||||
import { ApiHolder } from '@backstage/core-plugin-api';
|
||||
import { ApiRef } from '@backstage/core-plugin-api';
|
||||
import { ComponentType } from 'react';
|
||||
import { ErrorApi } from '@backstage/core-plugin-api';
|
||||
import { ErrorApiError } from '@backstage/core-plugin-api';
|
||||
@@ -175,6 +177,26 @@ export function setupRequestMockHandlers(worker: {
|
||||
// @public
|
||||
export type SyncLogCollector = () => void;
|
||||
|
||||
// @public
|
||||
export const TestApiProvider: <T extends any[]>({
|
||||
apis,
|
||||
children,
|
||||
}: TestApiProviderProps<T>) => JSX.Element;
|
||||
|
||||
// @public
|
||||
export type TestApiProviderProps<TApiPairs extends any[]> = {
|
||||
apis: readonly [...TestApiProviderPropsApiPairs<TApiPairs>];
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
// @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 = {
|
||||
routeEntries?: string[];
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 React from 'react';
|
||||
import { createApiRef, useApiHolder } from '@backstage/core-plugin-api';
|
||||
import { TestApiProvider, TestApiRegistry } from './TestApiProvider';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
const xApiRef = createApiRef<{ a: string; b: number }>({
|
||||
id: 'x',
|
||||
});
|
||||
const yApiRef = createApiRef<string>({
|
||||
id: 'y',
|
||||
});
|
||||
|
||||
function Verifier() {
|
||||
const holder = useApiHolder();
|
||||
const x = holder.get(xApiRef);
|
||||
const y = holder.get(yApiRef);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{x ? (
|
||||
<span>
|
||||
x={x.a},{x.b}
|
||||
</span>
|
||||
) : (
|
||||
<span>no x</span>
|
||||
)}
|
||||
{y ? <span>y={y}</span> : <span>no y</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
describe('TestApiProvider', () => {
|
||||
it('should provide APIs', () => {
|
||||
render(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[xApiRef, { a: 'a', b: 3 }],
|
||||
[yApiRef, 'y'],
|
||||
]}
|
||||
>
|
||||
<Verifier />
|
||||
</TestApiProvider>,
|
||||
);
|
||||
expect(screen.getByText('x=a,3')).toBeInTheDocument();
|
||||
expect(screen.getByText('y=y')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should provide partial APIs', () => {
|
||||
render(
|
||||
<TestApiProvider apis={[[xApiRef, { a: 'a' }]]}>
|
||||
<Verifier />
|
||||
</TestApiProvider>,
|
||||
);
|
||||
expect(screen.getByText('x=a,')).toBeInTheDocument();
|
||||
expect(screen.getByText('no y')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should require partial implementations to still match types', () => {
|
||||
render(
|
||||
// @ts-expect-error
|
||||
<TestApiProvider apis={[[xApiRef, { a: 3 }]]}>
|
||||
<Verifier />
|
||||
</TestApiProvider>,
|
||||
);
|
||||
expect(screen.getByText('x=3,')).toBeInTheDocument();
|
||||
expect(screen.getByText('no y')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should allow empty APIs', () => {
|
||||
render(
|
||||
<TestApiProvider apis={[]}>
|
||||
<Verifier />
|
||||
</TestApiProvider>,
|
||||
);
|
||||
expect(screen.getByText('no x')).toBeInTheDocument();
|
||||
expect(screen.getByText('no y')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('TestApiRegistry', () => {
|
||||
it('should be created with APIs', () => {
|
||||
const x = { a: 'a', b: 3 };
|
||||
const y = 'y';
|
||||
const registry = TestApiRegistry.from([xApiRef, x], [yApiRef, y]);
|
||||
|
||||
expect(registry.get(xApiRef)).toBe(x);
|
||||
expect(registry.get(yApiRef)).toBe(y);
|
||||
});
|
||||
|
||||
it('should allow partial implementations', () => {
|
||||
const x = { a: 'a' };
|
||||
const registry = TestApiRegistry.from([xApiRef, x]);
|
||||
|
||||
expect(registry.get(xApiRef)).toBe(x);
|
||||
expect(registry.get(yApiRef)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should require partial implementations to match types', () => {
|
||||
const x = { a: 2 };
|
||||
// @ts-expect-error
|
||||
const registry = TestApiRegistry.from([xApiRef, x]);
|
||||
|
||||
expect(registry.get(xApiRef)).toBe(x);
|
||||
expect(registry.get(yApiRef)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should prefer last duplicate API that was provided', () => {
|
||||
const x1 = { a: 'a' };
|
||||
const x2 = { a: 's' };
|
||||
const x3 = { a: 'd' };
|
||||
const registry = TestApiRegistry.from(
|
||||
[xApiRef, x1],
|
||||
[xApiRef, x2],
|
||||
[xApiRef, x3],
|
||||
);
|
||||
|
||||
expect(registry.get(xApiRef)).toBe(x3);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 React, { ReactNode } from 'react';
|
||||
import { ApiProvider } from '@backstage/core-app-api';
|
||||
import { ApiRef, ApiHolder } from '@backstage/core-plugin-api';
|
||||
|
||||
/** @ignore */
|
||||
type TestApiProviderPropsApiPair<TApi> = TApi extends infer TImpl
|
||||
? readonly [ApiRef<TApi>, Partial<TImpl>]
|
||||
: never;
|
||||
|
||||
/** @ignore */
|
||||
type TestApiProviderPropsApiPairs<TApiPairs> = {
|
||||
[TIndex in keyof TApiPairs]: TestApiProviderPropsApiPair<TApiPairs[TIndex]>;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* 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[]>({
|
||||
apis,
|
||||
children,
|
||||
}: TestApiProviderProps<T>) => {
|
||||
return (
|
||||
<ApiProvider apis={TestApiRegistry.from(...apis)} children={children} />
|
||||
);
|
||||
};
|
||||
@@ -22,3 +22,5 @@ export * from './msw';
|
||||
export * from './Keyboard';
|
||||
export * from './logCollector';
|
||||
export * from './testingLibrary';
|
||||
export { TestApiProvider, TestApiRegistry } from './TestApiProvider';
|
||||
export type { TestApiProviderProps } from './TestApiProvider';
|
||||
|
||||
Reference in New Issue
Block a user