Merge pull request #8089 from backstage/rugvip/test-api

core,test-utils: deprecate ApiRegistry, add new TestApiProvider and TestApiRegistry
This commit is contained in:
Patrik Oldsberg
2021-11-22 13:22:33 +01:00
committed by GitHub
18 changed files with 462 additions and 129 deletions
+22
View File
@@ -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';