frontend-test-utils: review and type fixes + cleanup
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -2,4 +2,4 @@
|
||||
'@backstage/frontend-test-utils': minor
|
||||
---
|
||||
|
||||
**BREAKING**: Removed the `TestApiRegistry` class, use `TestApiProvider` directory instead, storing resused APIs in an a variable instead, e.g. `const apis = [...] as const`.
|
||||
**BREAKING**: Removed the `TestApiRegistry` class, use `TestApiProvider` directly instead, storing reused APIs in a variable, e.g. `const apis = [...] as const`.
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-react': patch
|
||||
---
|
||||
|
||||
Added `@backstage/frontend-test-utils` as a dev dependency for mock API usage in tests.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
---
|
||||
|
||||
Added `@backstage/frontend-test-utils` as a dev dependency for mock API usage in tests.
|
||||
@@ -32,14 +32,15 @@ Call `.mock()` to get an instance where every method is a `jest.fn()`. You can o
|
||||
|
||||
```ts
|
||||
import { mockApis } from '@backstage/frontend-test-utils';
|
||||
import { AuthorizeResult } from '@backstage/plugin-permission-common';
|
||||
|
||||
const catalogApi = mockApis.permission.mock({
|
||||
const permissionApi = mockApis.permission.mock({
|
||||
authorize: async () => ({ result: AuthorizeResult.ALLOW }),
|
||||
});
|
||||
|
||||
// ... exercise the component ...
|
||||
|
||||
expect(catalogApi.authorize).toHaveBeenCalledTimes(1);
|
||||
expect(permissionApi.authorize).toHaveBeenCalledTimes(1);
|
||||
```
|
||||
|
||||
## Providing mock APIs in tests
|
||||
|
||||
@@ -8,7 +8,6 @@ import { AlertMessage } from '@backstage/frontend-plugin-api';
|
||||
import { AnalyticsApi } from '@backstage/frontend-plugin-api';
|
||||
import { AnalyticsEvent } from '@backstage/frontend-plugin-api';
|
||||
import { ApiFactory } from '@backstage/frontend-plugin-api';
|
||||
import { ApiHolder } from '@backstage/frontend-plugin-api';
|
||||
import { ApiRef } from '@backstage/frontend-plugin-api';
|
||||
import { AppNode } from '@backstage/frontend-plugin-api';
|
||||
import { AppNodeInstance } from '@backstage/frontend-plugin-api';
|
||||
@@ -39,7 +38,6 @@ import { IdentityApi } from '@backstage/frontend-plugin-api';
|
||||
import { IdentityApi as IdentityApi_2 } from '@backstage/core-plugin-api';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { JsonValue } from '@backstage/types';
|
||||
import { JSX as JSX_2 } from 'react/jsx-runtime';
|
||||
import { Observable } from '@backstage/types';
|
||||
import { PermissionApi } from '@backstage/plugin-permission-react';
|
||||
import { ReactNode } from 'react';
|
||||
@@ -546,13 +544,13 @@ export type MockWithApiFactory<TApi> = TApi & {
|
||||
export { registerMswTestHooks };
|
||||
|
||||
// @public
|
||||
export function renderInTestApp<TApiPairs extends any[] = any[]>(
|
||||
export function renderInTestApp<const TApiPairs extends any[] = any[]>(
|
||||
element: JSX.Element,
|
||||
options?: TestAppOptions<TApiPairs>,
|
||||
): RenderResult;
|
||||
|
||||
// @public
|
||||
export function renderTestApp<TApiPairs extends any[] = any[]>(
|
||||
export function renderTestApp<const TApiPairs extends any[] = any[]>(
|
||||
options?: RenderTestAppOptions<TApiPairs>,
|
||||
): RenderResult;
|
||||
|
||||
@@ -565,24 +563,27 @@ export type RenderTestAppOptions<TApiPairs extends any[] = any[]> = {
|
||||
mountedRoutes?: {
|
||||
[path: string]: RouteRef;
|
||||
};
|
||||
apis?: readonly [
|
||||
...(TestApiProviderPropsApiPairs<TApiPairs> | MockWithApiFactory<any>[]),
|
||||
];
|
||||
apis?: readonly [...TestApiPairs<TApiPairs>];
|
||||
};
|
||||
|
||||
// @public
|
||||
export type TestApiPairs<TApiPairs> = TestApiProviderPropsApiPairs<TApiPairs>;
|
||||
export type TestApiPair<TApi> =
|
||||
| readonly [ApiRef<TApi>, TApi extends infer TImpl ? Partial<TImpl> : never]
|
||||
| MockWithApiFactory<TApi>;
|
||||
|
||||
// @public
|
||||
export const TestApiProvider: <T extends any[]>(
|
||||
props: TestApiProviderProps<T>,
|
||||
) => JSX_2.Element;
|
||||
export type TestApiPairs<TApiPairs> = {
|
||||
[TIndex in keyof TApiPairs]: TestApiPair<TApiPairs[TIndex]>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export function TestApiProvider<const TApiPairs extends any[]>(
|
||||
props: TestApiProviderProps<TApiPairs>,
|
||||
): JSX.Element;
|
||||
|
||||
// @public
|
||||
export type TestApiProviderProps<TApiPairs extends any[]> = {
|
||||
apis: readonly [
|
||||
...(TestApiProviderPropsApiPairs<TApiPairs> | MockWithApiFactory<any>[]),
|
||||
];
|
||||
apis: readonly [...TestApiPairs<TApiPairs>];
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
@@ -594,9 +595,7 @@ export type TestAppOptions<TApiPairs extends any[] = any[]> = {
|
||||
config?: JsonObject;
|
||||
features?: FrontendFeature[];
|
||||
initialRouteEntries?: string[];
|
||||
apis?: readonly [
|
||||
...(TestApiProviderPropsApiPairs<TApiPairs> | MockWithApiFactory<any>[]),
|
||||
];
|
||||
apis?: readonly [...TestApiPairs<TApiPairs>];
|
||||
};
|
||||
|
||||
export { withLogCollector };
|
||||
|
||||
@@ -46,22 +46,26 @@ describe('MockAlertApi', () => {
|
||||
expect(api.getAlerts()).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should notify observers', done => {
|
||||
it('should notify observers', async () => {
|
||||
const api = new MockAlertApi();
|
||||
const messages: string[] = [];
|
||||
|
||||
api.alert$().subscribe({
|
||||
next: alert => {
|
||||
messages.push(alert.message);
|
||||
if (messages.length === 2) {
|
||||
expect(messages).toEqual(['First', 'Second']);
|
||||
done();
|
||||
}
|
||||
},
|
||||
const collected = new Promise<void>(resolve => {
|
||||
api.alert$().subscribe({
|
||||
next: alert => {
|
||||
messages.push(alert.message);
|
||||
if (messages.length === 2) {
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
api.post({ message: 'First' });
|
||||
api.post({ message: 'Second' });
|
||||
|
||||
await collected;
|
||||
expect(messages).toEqual(['First', 'Second']);
|
||||
});
|
||||
|
||||
it('should wait for matching alert', async () => {
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 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 { ApiFactory } from '@backstage/frontend-plugin-api';
|
||||
import { mockApiFactorySymbol } from './utils';
|
||||
|
||||
/**
|
||||
* Represents a mocked version of an API, where you automatically have access to
|
||||
* the mocked versions of all of its methods along with a factory that returns
|
||||
* that same mock.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type ApiMock<TApi> = {
|
||||
factory: ApiFactory<TApi, TApi, {}>;
|
||||
[mockApiFactorySymbol]: ApiFactory<TApi, TApi, {}>;
|
||||
} & {
|
||||
[Key in keyof TApi]: TApi[Key] extends (...args: infer Args) => infer Return
|
||||
? TApi[Key] & jest.MockInstance<Return, Args>
|
||||
: TApi[Key];
|
||||
};
|
||||
-16
@@ -33,22 +33,6 @@ export const mockApiFactorySymbol = Symbol.for('@backstage/mock-api');
|
||||
*/
|
||||
export type MockApiFactorySymbol = typeof mockApiFactorySymbol;
|
||||
|
||||
/**
|
||||
* Represents a mocked version of an API, where you automatically have access to
|
||||
* the mocked versions of all of its methods along with a factory that returns
|
||||
* that same mock.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type ApiMock<TApi> = {
|
||||
factory: ApiFactory<TApi, TApi, {}>;
|
||||
[mockApiFactorySymbol]: ApiFactory<TApi, TApi, {}>;
|
||||
} & {
|
||||
[Key in keyof TApi]: TApi[Key] extends (...args: infer Args) => infer Return
|
||||
? TApi[Key] & jest.MockInstance<Return, Args>
|
||||
: TApi[Key];
|
||||
};
|
||||
|
||||
/**
|
||||
* Type for an API instance that has been marked as a mock API.
|
||||
*
|
||||
@@ -30,7 +30,6 @@ describe('WebStorage Storage API', () => {
|
||||
key: 'myfakekey',
|
||||
presence: 'absent',
|
||||
value: undefined,
|
||||
newValue: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -135,7 +134,6 @@ describe('WebStorage Storage API', () => {
|
||||
key: 'correctKey',
|
||||
presence: 'absent',
|
||||
value: undefined,
|
||||
newValue: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -18,44 +18,30 @@ 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 './utils';
|
||||
import {
|
||||
getMockApiFactory,
|
||||
type MockWithApiFactory,
|
||||
} from './MockWithApiFactory';
|
||||
|
||||
/**
|
||||
* Helper type for representing an API reference paired with a partial implementation.
|
||||
* @ignore
|
||||
*/
|
||||
export type TestApiProviderPropsApiPair<TApi> = TApi extends infer TImpl
|
||||
? readonly [ApiRef<TApi>, Partial<TImpl>]
|
||||
: never;
|
||||
|
||||
/**
|
||||
* Helper type for representing an array of API reference pairs.
|
||||
* @ignore
|
||||
*/
|
||||
export type TestApiProviderPropsApiPairs<TApiPairs> = {
|
||||
[TIndex in keyof TApiPairs]: TestApiProviderPropsApiPair<TApiPairs[TIndex]>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Shorter alias for TestApiProviderPropsApiPairs for use in function signatures.
|
||||
* Represents a single API implementation, either as a tuple of the reference and the implementation, or a mock with an embedded factory.
|
||||
* @public
|
||||
*/
|
||||
export type TestApiPairs<TApiPairs> = TestApiProviderPropsApiPairs<TApiPairs>;
|
||||
export type TestApiPair<TApi> =
|
||||
| readonly [ApiRef<TApi>, TApi extends infer TImpl ? Partial<TImpl> : never]
|
||||
| MockWithApiFactory<TApi>;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @internal
|
||||
* Represents an array of mock API implementation.
|
||||
* @public
|
||||
*/
|
||||
export type TestApiProviderEntry =
|
||||
| readonly [ApiRef<any>, any]
|
||||
| MockWithApiFactory<any>;
|
||||
export type TestApiPairs<TApiPairs> = {
|
||||
[TIndex in keyof TApiPairs]: TestApiPair<TApiPairs[TIndex]>;
|
||||
};
|
||||
|
||||
/** @internal */
|
||||
export function resolveTestApiEntries(
|
||||
apis: readonly (TestApiProviderEntry | readonly [ApiRef<any>, any])[],
|
||||
apis: readonly TestApiPairs<any>[],
|
||||
): ApiHolder {
|
||||
const apiMap = new Map<string, unknown>();
|
||||
|
||||
@@ -80,9 +66,7 @@ export function resolveTestApiEntries(
|
||||
* @public
|
||||
*/
|
||||
export type TestApiProviderProps<TApiPairs extends any[]> = {
|
||||
apis: readonly [
|
||||
...(TestApiProviderPropsApiPairs<TApiPairs> | MockWithApiFactory<any>[]),
|
||||
];
|
||||
apis: readonly [...TestApiPairs<TApiPairs>];
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
@@ -131,13 +115,13 @@ export type TestApiProviderProps<TApiPairs extends any[]> = {
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const TestApiProvider = <T extends any[]>(
|
||||
props: TestApiProviderProps<T>,
|
||||
) => {
|
||||
export function TestApiProvider<const TApiPairs extends any[]>(
|
||||
props: TestApiProviderProps<TApiPairs>,
|
||||
): JSX.Element {
|
||||
return (
|
||||
<ApiProvider
|
||||
apis={resolveTestApiEntries(props.apis)}
|
||||
children={props.children}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,21 +14,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { mockApis } from './mockApis';
|
||||
export { type ApiMock, mockApis } from './mockApis';
|
||||
export {
|
||||
type MockApiFactorySymbol,
|
||||
type ApiMock,
|
||||
type MockWithApiFactory,
|
||||
attachMockApiFactory,
|
||||
} from './utils';
|
||||
} from './MockWithApiFactory';
|
||||
export {
|
||||
TestApiProvider,
|
||||
type TestApiProviderPropsApiPair,
|
||||
type TestApiProviderPropsApiPairs,
|
||||
type TestApiProviderProps,
|
||||
type TestApiPair,
|
||||
type TestApiPairs,
|
||||
type TestApiProviderEntry,
|
||||
} from './TestApiProvider';
|
||||
export type { TestApiProviderProps } from './TestApiProvider';
|
||||
|
||||
/**
|
||||
* Mock API classes are exported as types only to prevent direct instantiation.
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
type IdentityApi,
|
||||
type StorageApi,
|
||||
type TranslationApi,
|
||||
ApiFactory,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import {
|
||||
permissionApiRef,
|
||||
@@ -57,11 +58,26 @@ import { MockStorageApi } from './StorageApi';
|
||||
import { MockPermissionApi } from './PermissionApi';
|
||||
import { MockTranslationApi } from './TranslationApi';
|
||||
import {
|
||||
ApiMock,
|
||||
mockWithApiFactory,
|
||||
mockApiFactorySymbol,
|
||||
type MockWithApiFactory,
|
||||
} from './utils';
|
||||
} from './MockWithApiFactory';
|
||||
|
||||
/**
|
||||
* Represents a mocked version of an API, where you automatically have access to
|
||||
* the mocked versions of all of its methods along with a factory that returns
|
||||
* that same mock.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type ApiMock<TApi> = {
|
||||
factory: ApiFactory<TApi, TApi, {}>;
|
||||
[mockApiFactorySymbol]: ApiFactory<TApi, TApi, {}>;
|
||||
} & {
|
||||
[Key in keyof TApi]: TApi[Key] extends (...args: infer Args) => infer Return
|
||||
? TApi[Key] & jest.MockInstance<Return, Args>
|
||||
: TApi[Key];
|
||||
};
|
||||
|
||||
/** @internal */
|
||||
function simpleMock<TApi>(
|
||||
|
||||
@@ -38,8 +38,7 @@ 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 { type TestApiPairs } from '../apis';
|
||||
import { resolveTestApiEntries } from '../apis/TestApiProvider';
|
||||
import { resolveTestApiEntries, TestApiPairs } from '../apis/TestApiProvider';
|
||||
|
||||
/**
|
||||
* Represents a snapshot of an extension in the app tree.
|
||||
@@ -97,7 +96,7 @@ export class ExtensionTester<UOutput extends ExtensionDataRef> {
|
||||
/** @internal */
|
||||
static forSubject<
|
||||
T extends ExtensionDefinitionParameters,
|
||||
TApiPairs extends any[],
|
||||
const TApiPairs extends any[],
|
||||
>(
|
||||
subject: ExtensionDefinition<T>,
|
||||
options?: {
|
||||
|
||||
@@ -36,10 +36,10 @@ import {
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { RouterBlueprint } from '@backstage/plugin-app-react';
|
||||
import appPlugin from '@backstage/plugin-app';
|
||||
import { type TestApiProviderPropsApiPairs } from '../apis';
|
||||
import { getMockApiFactory, type MockWithApiFactory } from '../apis/utils';
|
||||
import { getMockApiFactory } from '../apis/MockWithApiFactory';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import type { CreateSpecializedAppInternalOptions } from '../../../frontend-app-api/src/wiring/createSpecializedApp';
|
||||
import { TestApiPairs } from '../apis/TestApiProvider';
|
||||
|
||||
const DEFAULT_MOCK_CONFIG = {
|
||||
app: { baseUrl: 'http://localhost:3000' },
|
||||
@@ -97,9 +97,7 @@ export type TestAppOptions<TApiPairs extends any[] = any[]> = {
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
apis?: readonly [
|
||||
...(TestApiProviderPropsApiPairs<TApiPairs> | MockWithApiFactory<any>[]),
|
||||
];
|
||||
apis?: readonly [...TestApiPairs<TApiPairs>];
|
||||
};
|
||||
|
||||
const NavItem = (props: {
|
||||
@@ -166,7 +164,7 @@ const appPluginOverride = appPlugin.withOverrides({
|
||||
* @public
|
||||
* Renders the given element in a test app, for use in unit tests.
|
||||
*/
|
||||
export function renderInTestApp<TApiPairs extends any[] = any[]>(
|
||||
export function renderInTestApp<const TApiPairs extends any[] = any[]>(
|
||||
element: JSX.Element,
|
||||
options?: TestAppOptions<TApiPairs>,
|
||||
): RenderResult {
|
||||
|
||||
@@ -33,10 +33,10 @@ 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 '../apis';
|
||||
import { getMockApiFactory, type MockWithApiFactory } from '../apis/utils';
|
||||
import { getMockApiFactory } from '../apis/MockWithApiFactory';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import type { CreateSpecializedAppInternalOptions } from '../../../frontend-app-api/src/wiring/createSpecializedApp';
|
||||
import { TestApiPairs } from '../apis/TestApiProvider';
|
||||
|
||||
const DEFAULT_MOCK_CONFIG = {
|
||||
app: { baseUrl: 'http://localhost:3000' },
|
||||
@@ -99,9 +99,7 @@ export type RenderTestAppOptions<TApiPairs extends any[] = any[]> = {
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
apis?: readonly [
|
||||
...(TestApiProviderPropsApiPairs<TApiPairs> | MockWithApiFactory<any>[]),
|
||||
];
|
||||
apis?: readonly [...TestApiPairs<TApiPairs>];
|
||||
};
|
||||
|
||||
const appPluginOverride = appPlugin.withOverrides({
|
||||
@@ -118,7 +116,7 @@ const appPluginOverride = appPlugin.withOverrides({
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function renderTestApp<TApiPairs extends any[] = any[]>(
|
||||
export function renderTestApp<const TApiPairs extends any[] = any[]>(
|
||||
options?: RenderTestAppOptions<TApiPairs>,
|
||||
): RenderResult {
|
||||
const extensions = [...(options?.extensions ?? [])];
|
||||
|
||||
@@ -23,9 +23,6 @@
|
||||
export * from './apis';
|
||||
export * from './app';
|
||||
|
||||
// Explicit export to satisfy API Extractor
|
||||
export type { TestApiPairs } from './apis';
|
||||
|
||||
export { withLogCollector } from '@backstage/test-utils';
|
||||
|
||||
export { registerMswTestHooks } from '@backstage/test-utils';
|
||||
|
||||
Reference in New Issue
Block a user