frontend-test-utils: adjust fetch mock + add createApiMock
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -17,3 +17,5 @@ renderInTestApp(<MyComponent />, {
|
||||
apis: [mockApis.identity()],
|
||||
});
|
||||
```
|
||||
|
||||
This change also adds `createApiMock`, a public utility for creating mock API factories, intended for plugin authors to create their own `.mock()` variants.
|
||||
|
||||
@@ -124,28 +124,53 @@ renderTestApp({
|
||||
});
|
||||
```
|
||||
|
||||
### Creating your own mock APIs with `attachMockApiFactory`
|
||||
### Creating your own mock APIs
|
||||
|
||||
If you maintain a plugin that exposes a utility API, you can use `attachMockApiFactory` to create mock instances that can be passed directly to test utilities:
|
||||
If you maintain a plugin that exposes a utility API, you can provide mock utilities that follow the same function + `.mock()` pattern as the built-in `mockApis`.
|
||||
|
||||
Use `attachMockApiFactory` for fake instances with real behavior, and `createApiMock` for the jest-mocked `.mock()` variant where all methods are `jest.fn()`. The full pattern looks like this:
|
||||
|
||||
```ts
|
||||
import { attachMockApiFactory } from '@backstage/frontend-test-utils';
|
||||
import {
|
||||
attachMockApiFactory,
|
||||
createApiMock,
|
||||
type ApiMock,
|
||||
} from '@backstage/frontend-test-utils';
|
||||
import { myApiRef, type MyApi } from '@internal/plugin-example-react';
|
||||
|
||||
// Fake instance with real behavior
|
||||
export function myApiMock(options?: { greeting?: string }): MyApi {
|
||||
const instance: MyApi = {
|
||||
return attachMockApiFactory(myApiRef, {
|
||||
greet: async () => options?.greeting ?? 'Hello!',
|
||||
};
|
||||
return attachMockApiFactory(myApiRef, instance);
|
||||
});
|
||||
}
|
||||
|
||||
// Jest mock variant where all methods are jest.fn()
|
||||
export namespace myApiMock {
|
||||
export const mock = createApiMock(myApiRef, () => ({
|
||||
greet: jest.fn(),
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
Consumers can then use it like the built-in mocks:
|
||||
Consumers can then use it just like the core mocks:
|
||||
|
||||
```tsx
|
||||
// Fake with real behavior
|
||||
await renderInTestApp(<MyComponent />, {
|
||||
apis: [myApiMock({ greeting: 'Hi there!' })],
|
||||
});
|
||||
|
||||
// Jest mock for assertions
|
||||
const api = myApiMock.mock({
|
||||
greet: async () => 'mocked',
|
||||
});
|
||||
|
||||
await renderInTestApp(<MyComponent />, {
|
||||
apis: [api],
|
||||
});
|
||||
|
||||
expect(api.greet).toHaveBeenCalledTimes(1);
|
||||
```
|
||||
|
||||
## Available mock APIs
|
||||
@@ -160,7 +185,7 @@ The table below lists all core APIs available through the `mockApis` namespace.
|
||||
| `mockApis.discovery({ baseUrl })` | Inline | Returns `${baseUrl}/api/${pluginId}`, defaults to `http://example.com` |
|
||||
| `mockApis.error(options?)` | `MockErrorApi` | Collects errors; has `getErrors()`, `waitForError()` |
|
||||
| `mockApis.featureFlags(options?)` | `MockFeatureFlagsApi` | In-memory flag state; has `getState()`, `setState()`, `clearState()` |
|
||||
| `mockApis.fetch(options?)` | `MockFetchApi` | Wraps `cross-fetch`; supports identity injection and plugin protocol |
|
||||
| `mockApis.fetch(options?)` | `MockFetchApi` | Wraps native `fetch`; supports identity injection and plugin protocol |
|
||||
| `mockApis.identity(options?)` | Inline | Configurable user ref, ownership, token, profile |
|
||||
| `mockApis.permission(options?)` | `MockPermissionApi` | Defaults to `ALLOW`; accepts a handler function |
|
||||
| `mockApis.storage({ data })` | `MockStorageApi` | In-memory storage with bucket support |
|
||||
|
||||
@@ -43,7 +43,6 @@
|
||||
"@backstage/test-utils": "workspace:^",
|
||||
"@backstage/types": "workspace:^",
|
||||
"@backstage/version-bridge": "workspace:^",
|
||||
"cross-fetch": "^4.0.0",
|
||||
"i18next": "^22.4.15",
|
||||
"zen-observable": "^0.10.0",
|
||||
"zod": "^3.25.76"
|
||||
|
||||
@@ -15,7 +15,6 @@ import { AuthorizeResult } from '@backstage/plugin-permission-common';
|
||||
import { Config } from '@backstage/config';
|
||||
import { ConfigApi } from '@backstage/core-plugin-api';
|
||||
import { ConfigApi as ConfigApi_2 } from '@backstage/frontend-plugin-api';
|
||||
import crossFetch from 'cross-fetch';
|
||||
import { DiscoveryApi } from '@backstage/frontend-plugin-api';
|
||||
import { DiscoveryApi as DiscoveryApi_2 } from '@backstage/core-plugin-api';
|
||||
import { ErrorApi } from '@backstage/core-plugin-api';
|
||||
@@ -55,7 +54,6 @@ import { withLogCollector } from '@backstage/test-utils';
|
||||
|
||||
// @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
|
||||
@@ -71,6 +69,12 @@ export function attachMockApiFactory<TApi, TImpl extends TApi = TApi>(
|
||||
[mockApiFactorySymbol]: ApiFactory<TApi, TApi, {}>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export function createApiMock<TApi>(
|
||||
apiRef: ApiRef<TApi>,
|
||||
mockFactory: () => jest.Mocked<TApi>,
|
||||
): (partialImpl?: Partial<TApi>) => ApiMock<TApi>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function createExtensionTester<
|
||||
T extends ExtensionDefinitionParameters,
|
||||
@@ -170,75 +174,23 @@ export namespace mockApis {
|
||||
export function alert(): MockWithApiFactory<MockAlertApi>;
|
||||
export namespace alert {
|
||||
const mock: (
|
||||
partialImpl?:
|
||||
| Partial<{
|
||||
post: jest.Mock<any, any, any>;
|
||||
alert$: jest.Mock<any, any, any>;
|
||||
}>
|
||||
| undefined,
|
||||
) => ApiMock<{
|
||||
post: jest.Mock<any, any, any>;
|
||||
alert$: jest.Mock<any, any, any>;
|
||||
}>;
|
||||
partialImpl?: Partial<AlertApi> | undefined,
|
||||
) => ApiMock<AlertApi>;
|
||||
}
|
||||
export function analytics(): MockAnalyticsApi &
|
||||
MockWithApiFactory<AnalyticsApi>;
|
||||
export namespace analytics {
|
||||
const // (undocumented)
|
||||
mock: (
|
||||
partialImpl?:
|
||||
| Partial<{
|
||||
captureEvent: jest.Mock<any, any, any>;
|
||||
}>
|
||||
| undefined,
|
||||
) => ApiMock<{
|
||||
captureEvent: jest.Mock<any, any, any>;
|
||||
}>;
|
||||
partialImpl?: Partial<AnalyticsApi> | undefined,
|
||||
) => ApiMock<AnalyticsApi>;
|
||||
}
|
||||
export function config(options?: {
|
||||
data?: JsonObject;
|
||||
}): MockConfigApi & MockWithApiFactory<ConfigApi_2>;
|
||||
export namespace config {
|
||||
const // (undocumented)
|
||||
mock: (
|
||||
partialImpl?:
|
||||
| Partial<{
|
||||
has: jest.Mock<any, any, any>;
|
||||
keys: jest.Mock<any, any, any>;
|
||||
get: jest.Mock<any, any, any>;
|
||||
getOptional: jest.Mock<any, any, any>;
|
||||
getConfig: jest.Mock<any, any, any>;
|
||||
getOptionalConfig: jest.Mock<any, any, any>;
|
||||
getConfigArray: jest.Mock<any, any, any>;
|
||||
getOptionalConfigArray: jest.Mock<any, any, any>;
|
||||
getNumber: jest.Mock<any, any, any>;
|
||||
getOptionalNumber: jest.Mock<any, any, any>;
|
||||
getBoolean: jest.Mock<any, any, any>;
|
||||
getOptionalBoolean: jest.Mock<any, any, any>;
|
||||
getString: jest.Mock<any, any, any>;
|
||||
getOptionalString: jest.Mock<any, any, any>;
|
||||
getStringArray: jest.Mock<any, any, any>;
|
||||
getOptionalStringArray: jest.Mock<any, any, any>;
|
||||
}>
|
||||
| undefined,
|
||||
) => ApiMock<{
|
||||
has: jest.Mock<any, any, any>;
|
||||
keys: jest.Mock<any, any, any>;
|
||||
get: jest.Mock<any, any, any>;
|
||||
getOptional: jest.Mock<any, any, any>;
|
||||
getConfig: jest.Mock<any, any, any>;
|
||||
getOptionalConfig: jest.Mock<any, any, any>;
|
||||
getConfigArray: jest.Mock<any, any, any>;
|
||||
getOptionalConfigArray: jest.Mock<any, any, any>;
|
||||
getNumber: jest.Mock<any, any, any>;
|
||||
getOptionalNumber: jest.Mock<any, any, any>;
|
||||
getBoolean: jest.Mock<any, any, any>;
|
||||
getOptionalBoolean: jest.Mock<any, any, any>;
|
||||
getString: jest.Mock<any, any, any>;
|
||||
getOptionalString: jest.Mock<any, any, any>;
|
||||
getStringArray: jest.Mock<any, any, any>;
|
||||
getOptionalStringArray: jest.Mock<any, any, any>;
|
||||
}>;
|
||||
mock: (partialImpl?: Partial<Config> | undefined) => ApiMock<Config>;
|
||||
}
|
||||
export function discovery(options?: {
|
||||
baseUrl?: string;
|
||||
@@ -246,14 +198,8 @@ export namespace mockApis {
|
||||
export namespace discovery {
|
||||
const // (undocumented)
|
||||
mock: (
|
||||
partialImpl?:
|
||||
| Partial<{
|
||||
getBaseUrl: jest.Mock<any, any, any>;
|
||||
}>
|
||||
| undefined,
|
||||
) => ApiMock<{
|
||||
getBaseUrl: jest.Mock<any, any, any>;
|
||||
}>;
|
||||
partialImpl?: Partial<DiscoveryApi> | undefined,
|
||||
) => ApiMock<DiscoveryApi>;
|
||||
}
|
||||
export function error(
|
||||
options?: MockErrorApiOptions,
|
||||
@@ -261,36 +207,16 @@ export namespace mockApis {
|
||||
export namespace error {
|
||||
const // (undocumented)
|
||||
mock: (
|
||||
partialImpl?:
|
||||
| Partial<{
|
||||
post: jest.Mock<any, any, any>;
|
||||
error$: jest.Mock<any, any, any>;
|
||||
}>
|
||||
| undefined,
|
||||
) => ApiMock<{
|
||||
post: jest.Mock<any, any, any>;
|
||||
error$: jest.Mock<any, any, any>;
|
||||
}>;
|
||||
partialImpl?: Partial<ErrorApi_2> | undefined,
|
||||
) => ApiMock<ErrorApi_2>;
|
||||
}
|
||||
export function featureFlags(
|
||||
options?: MockFeatureFlagsApiOptions,
|
||||
): MockWithApiFactory<MockFeatureFlagsApi>;
|
||||
export namespace featureFlags {
|
||||
const mock: (
|
||||
partialImpl?:
|
||||
| Partial<{
|
||||
registerFlag: jest.Mock<any, any, any>;
|
||||
getRegisteredFlags: jest.Mock<any, any, any>;
|
||||
isActive: jest.Mock<any, any, any>;
|
||||
save: jest.Mock<any, any, any>;
|
||||
}>
|
||||
| undefined,
|
||||
) => ApiMock<{
|
||||
registerFlag: jest.Mock<any, any, any>;
|
||||
getRegisteredFlags: jest.Mock<any, any, any>;
|
||||
isActive: jest.Mock<any, any, any>;
|
||||
save: jest.Mock<any, any, any>;
|
||||
}>;
|
||||
partialImpl?: Partial<FeatureFlagsApi> | undefined,
|
||||
) => ApiMock<FeatureFlagsApi>;
|
||||
}
|
||||
export function fetch(
|
||||
options?: MockFetchApiOptions,
|
||||
@@ -298,14 +224,8 @@ export namespace mockApis {
|
||||
export namespace fetch {
|
||||
const // (undocumented)
|
||||
mock: (
|
||||
partialImpl?:
|
||||
| Partial<{
|
||||
fetch: jest.Mock<any, any, any>;
|
||||
}>
|
||||
| undefined,
|
||||
) => ApiMock<{
|
||||
fetch: jest.Mock<any, any, any>;
|
||||
}>;
|
||||
partialImpl?: Partial<FetchApi_2> | undefined,
|
||||
) => ApiMock<FetchApi_2>;
|
||||
}
|
||||
export function identity(options?: {
|
||||
userEntityRef?: string;
|
||||
@@ -318,20 +238,8 @@ export namespace mockApis {
|
||||
export namespace identity {
|
||||
const // (undocumented)
|
||||
mock: (
|
||||
partialImpl?:
|
||||
| Partial<{
|
||||
getBackstageIdentity: jest.Mock<any, any, any>;
|
||||
getCredentials: jest.Mock<any, any, any>;
|
||||
getProfileInfo: jest.Mock<any, any, any>;
|
||||
signOut: jest.Mock<any, any, any>;
|
||||
}>
|
||||
| undefined,
|
||||
) => ApiMock<{
|
||||
getBackstageIdentity: jest.Mock<any, any, any>;
|
||||
getCredentials: jest.Mock<any, any, any>;
|
||||
getProfileInfo: jest.Mock<any, any, any>;
|
||||
signOut: jest.Mock<any, any, any>;
|
||||
}>;
|
||||
partialImpl?: Partial<IdentityApi> | undefined,
|
||||
) => ApiMock<IdentityApi>;
|
||||
}
|
||||
export function permission(options?: {
|
||||
authorize?:
|
||||
@@ -344,14 +252,8 @@ export namespace mockApis {
|
||||
export namespace permission {
|
||||
const // (undocumented)
|
||||
mock: (
|
||||
partialImpl?:
|
||||
| Partial<{
|
||||
authorize: jest.Mock<any, any, any>;
|
||||
}>
|
||||
| undefined,
|
||||
) => ApiMock<{
|
||||
authorize: jest.Mock<any, any, any>;
|
||||
}>;
|
||||
partialImpl?: Partial<PermissionApi> | undefined,
|
||||
) => ApiMock<PermissionApi>;
|
||||
}
|
||||
export function storage(options?: {
|
||||
data?: JsonObject;
|
||||
@@ -359,43 +261,21 @@ export namespace mockApis {
|
||||
export namespace storage {
|
||||
const // (undocumented)
|
||||
mock: (
|
||||
partialImpl?:
|
||||
| Partial<{
|
||||
forBucket: jest.Mock<any, any, any>;
|
||||
snapshot: jest.Mock<any, any, any>;
|
||||
set: jest.Mock<any, any, any>;
|
||||
remove: jest.Mock<any, any, any>;
|
||||
observe$: jest.Mock<any, any, any>;
|
||||
}>
|
||||
| undefined,
|
||||
) => ApiMock<{
|
||||
forBucket: jest.Mock<any, any, any>;
|
||||
snapshot: jest.Mock<any, any, any>;
|
||||
set: jest.Mock<any, any, any>;
|
||||
remove: jest.Mock<any, any, any>;
|
||||
observe$: jest.Mock<any, any, any>;
|
||||
}>;
|
||||
partialImpl?: Partial<StorageApi_2> | undefined,
|
||||
) => ApiMock<StorageApi_2>;
|
||||
}
|
||||
export function translation(): MockTranslationApi &
|
||||
MockWithApiFactory<TranslationApi_2>;
|
||||
export namespace translation {
|
||||
const mock: (
|
||||
partialImpl?:
|
||||
| Partial<{
|
||||
getTranslation: jest.Mock<any, any, any>;
|
||||
translation$: jest.Mock<any, any, any>;
|
||||
}>
|
||||
| undefined,
|
||||
) => ApiMock<{
|
||||
getTranslation: jest.Mock<any, any, any>;
|
||||
translation$: jest.Mock<any, any, any>;
|
||||
}>;
|
||||
partialImpl?: Partial<TranslationApi_2> | undefined,
|
||||
) => ApiMock<TranslationApi_2>;
|
||||
}
|
||||
}
|
||||
|
||||
// @public
|
||||
export class MockConfigApi implements ConfigApi {
|
||||
constructor(data: JsonObject);
|
||||
constructor({ data }: { data: JsonObject });
|
||||
get<T = JsonValue>(key?: string): T;
|
||||
getBoolean(key: string): boolean;
|
||||
getConfig(key: string): Config;
|
||||
@@ -459,12 +339,12 @@ export interface MockFeatureFlagsApiOptions {
|
||||
// @public
|
||||
export class MockFetchApi implements FetchApi {
|
||||
constructor(options?: MockFetchApiOptions);
|
||||
get fetch(): typeof crossFetch;
|
||||
get fetch(): typeof fetch;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface MockFetchApiOptions {
|
||||
baseImplementation?: undefined | 'none' | typeof crossFetch;
|
||||
baseImplementation?: undefined | 'none' | typeof fetch;
|
||||
injectIdentityAuth?:
|
||||
| undefined
|
||||
| {
|
||||
|
||||
@@ -24,8 +24,6 @@ import {
|
||||
FetchApi,
|
||||
IdentityApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import crossFetch, { Response } from 'cross-fetch';
|
||||
|
||||
/**
|
||||
* The options given when constructing a {@link MockFetchApi}.
|
||||
*
|
||||
@@ -47,7 +45,7 @@ export interface MockFetchApiOptions {
|
||||
* `jest.fn()`, if you want to use a custom implementation or to just track
|
||||
* and assert on calls.
|
||||
*/
|
||||
baseImplementation?: undefined | 'none' | typeof crossFetch;
|
||||
baseImplementation?: undefined | 'none' | typeof fetch;
|
||||
|
||||
/**
|
||||
* Add translation from `plugin://` URLs to concrete http(s) URLs, basically
|
||||
@@ -103,7 +101,7 @@ export class MockFetchApi implements FetchApi {
|
||||
}
|
||||
|
||||
/** {@inheritdoc @backstage/core-plugin-api#FetchApi.fetch} */
|
||||
get fetch(): typeof crossFetch {
|
||||
get fetch(): typeof fetch {
|
||||
return this.implementation.fetch;
|
||||
}
|
||||
}
|
||||
@@ -124,7 +122,7 @@ function build(options?: MockFetchApiOptions): FetchApi {
|
||||
|
||||
function baseImplementation(
|
||||
options: MockFetchApiOptions | undefined,
|
||||
): typeof crossFetch {
|
||||
): typeof fetch {
|
||||
const implementation = options?.baseImplementation;
|
||||
if (!implementation) {
|
||||
// Return a wrapper that evaluates global.fetch at call time, not construction time.
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright 2025 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 { createApiRef } from '@backstage/frontend-plugin-api';
|
||||
import { createApiMock } from './createApiMock';
|
||||
import { getMockApiFactory } from './MockWithApiFactory';
|
||||
|
||||
describe('createApiMock', () => {
|
||||
type TestApi = {
|
||||
greet(name: string): string;
|
||||
count: number;
|
||||
};
|
||||
|
||||
const testApiRef = createApiRef<TestApi>({ id: 'test.create-mock' });
|
||||
|
||||
it('returns a factory function that produces jest mocks', () => {
|
||||
const mock = createApiMock(testApiRef, () => ({
|
||||
greet: jest.fn(),
|
||||
count: 0 as any,
|
||||
}));
|
||||
|
||||
const api = mock();
|
||||
api.greet('world');
|
||||
expect(api.greet).toHaveBeenCalledTimes(1);
|
||||
expect(api.greet).toHaveBeenCalledWith('world');
|
||||
});
|
||||
|
||||
it('applies partial implementations via mockImplementation', () => {
|
||||
const mock = createApiMock(testApiRef, () => ({
|
||||
greet: jest.fn(),
|
||||
count: 0 as any,
|
||||
}));
|
||||
|
||||
const api = mock({ greet: (name: string) => `Hello ${name}!` });
|
||||
expect(api.greet('world')).toBe('Hello world!');
|
||||
expect(api.greet).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('preserves non-function partial values', () => {
|
||||
const mock = createApiMock(testApiRef, () => ({
|
||||
greet: jest.fn(),
|
||||
count: 0 as any,
|
||||
}));
|
||||
|
||||
const api = mock({ count: 42 });
|
||||
expect(api.count).toBe(42);
|
||||
});
|
||||
|
||||
it('attaches a mock API factory via the symbol', () => {
|
||||
const mock = createApiMock(testApiRef, () => ({
|
||||
greet: jest.fn(),
|
||||
count: 0 as any,
|
||||
}));
|
||||
|
||||
const api = mock();
|
||||
const factory = getMockApiFactory(api);
|
||||
expect(factory).toBeDefined();
|
||||
expect(factory!.api).toBe(testApiRef);
|
||||
});
|
||||
|
||||
it('creates fresh mocks on each call', () => {
|
||||
const mock = createApiMock(testApiRef, () => ({
|
||||
greet: jest.fn(),
|
||||
count: 0 as any,
|
||||
}));
|
||||
|
||||
const api1 = mock();
|
||||
const api2 = mock();
|
||||
api1.greet('a');
|
||||
expect(api1.greet).toHaveBeenCalledTimes(1);
|
||||
expect(api2.greet).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2026 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, type ApiRef } from '@backstage/frontend-plugin-api';
|
||||
import { mockApiFactorySymbol } 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> = {
|
||||
[mockApiFactorySymbol]: ApiFactory<TApi, TApi, {}>;
|
||||
} & {
|
||||
[Key in keyof TApi]: TApi[Key] extends (...args: infer Args) => infer Return
|
||||
? TApi[Key] & jest.MockInstance<Return, Args>
|
||||
: TApi[Key];
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a standardized Backstage Utility API mockfactory function for
|
||||
* producing mock API instances.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* Each method in the mock factory is a `jest.fn()`, and you can optionally pass
|
||||
* partial implementations when calling the returned function. No type
|
||||
* parameters should be provided to this function, they will be inferred from
|
||||
* the provided API reference.
|
||||
*
|
||||
* @public
|
||||
* @example
|
||||
* ```ts
|
||||
* import { createApiMock } from '@backstage/frontend-test-utils';
|
||||
* import { myApiRef } from '../apis';
|
||||
*
|
||||
* // Set up the mock factory
|
||||
* const mock = createApiMock(myApiRef, () => ({
|
||||
* greet: jest.fn(),
|
||||
* }));
|
||||
*
|
||||
* // Create a mock with default behavior
|
||||
* const api = mock();
|
||||
*
|
||||
* // Or with a partial implementation
|
||||
* const api = mock({ greet: async () => 'Hello!' });
|
||||
* expect(api.greet).toHaveBeenCalledTimes(1);
|
||||
* ```
|
||||
*/
|
||||
export function createApiMock<TApi>(
|
||||
apiRef: ApiRef<TApi>,
|
||||
mockFactory: () => jest.Mocked<TApi>,
|
||||
): (partialImpl?: Partial<TApi>) => ApiMock<TApi> {
|
||||
return partialImpl => {
|
||||
const mock = mockFactory();
|
||||
if (partialImpl) {
|
||||
for (const [key, impl] of Object.entries(partialImpl)) {
|
||||
if (typeof impl === 'function') {
|
||||
(mock as any)[key].mockImplementation(impl);
|
||||
} else {
|
||||
(mock as any)[key] = impl;
|
||||
}
|
||||
}
|
||||
}
|
||||
(mock as any)[mockApiFactorySymbol] = {
|
||||
api: apiRef,
|
||||
deps: {},
|
||||
factory: () => mock,
|
||||
};
|
||||
return mock as unknown as ApiMock<TApi>;
|
||||
};
|
||||
}
|
||||
@@ -14,7 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { type ApiMock, mockApis } from './mockApis';
|
||||
export { mockApis } from './mockApis';
|
||||
export { createApiMock, type ApiMock } from './createApiMock';
|
||||
export {
|
||||
type MockApiFactorySymbol,
|
||||
type MockWithApiFactory,
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
alertApiRef,
|
||||
analyticsApiRef,
|
||||
configApiRef,
|
||||
createApiFactory,
|
||||
discoveryApiRef,
|
||||
errorApiRef,
|
||||
fetchApiRef,
|
||||
@@ -34,7 +33,6 @@ import {
|
||||
type IdentityApi,
|
||||
type StorageApi,
|
||||
type TranslationApi,
|
||||
ApiFactory,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import {
|
||||
permissionApiRef,
|
||||
@@ -59,53 +57,9 @@ import { MockPermissionApi } from './PermissionApi';
|
||||
import { MockTranslationApi } from './TranslationApi';
|
||||
import {
|
||||
mockWithApiFactory,
|
||||
mockApiFactorySymbol,
|
||||
type MockWithApiFactory,
|
||||
} 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>(
|
||||
ref: any,
|
||||
mockFactory: () => jest.Mocked<TApi>,
|
||||
): (partialImpl?: Partial<TApi>) => ApiMock<TApi> {
|
||||
return partialImpl => {
|
||||
const mock = mockFactory();
|
||||
if (partialImpl) {
|
||||
for (const [key, impl] of Object.entries(partialImpl)) {
|
||||
if (typeof impl === 'function') {
|
||||
(mock as any)[key].mockImplementation(impl);
|
||||
} else {
|
||||
(mock as any)[key] = impl;
|
||||
}
|
||||
}
|
||||
}
|
||||
const factory = createApiFactory({
|
||||
api: ref,
|
||||
deps: {},
|
||||
factory: () => mock,
|
||||
});
|
||||
const apiMock = Object.assign(mock, { factory }) as ApiMock<TApi>;
|
||||
// Set the mock API symbol to the same factory
|
||||
(apiMock as any)[mockApiFactorySymbol] = factory;
|
||||
return apiMock;
|
||||
};
|
||||
}
|
||||
import { createApiMock } from './createApiMock';
|
||||
|
||||
/**
|
||||
* Mock implementations of the core utility APIs, to be used in tests.
|
||||
@@ -172,7 +126,7 @@ export namespace mockApis {
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const mock = simpleMock(alertApiRef, () => ({
|
||||
export const mock = createApiMock(alertApiRef, () => ({
|
||||
post: jest.fn(),
|
||||
alert$: jest.fn(),
|
||||
}));
|
||||
@@ -215,7 +169,7 @@ export namespace mockApis {
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const mock = simpleMock(featureFlagsApiRef, () => ({
|
||||
export const mock = createApiMock(featureFlagsApiRef, () => ({
|
||||
registerFlag: jest.fn(),
|
||||
getRegisteredFlags: jest.fn(),
|
||||
isActive: jest.fn(),
|
||||
@@ -241,7 +195,7 @@ export namespace mockApis {
|
||||
* @public
|
||||
*/
|
||||
export namespace analytics {
|
||||
export const mock = simpleMock(analyticsApiRef, () => ({
|
||||
export const mock = createApiMock(analyticsApiRef, () => ({
|
||||
captureEvent: jest.fn(),
|
||||
}));
|
||||
}
|
||||
@@ -273,7 +227,7 @@ export namespace mockApis {
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const mock = simpleMock(translationApiRef, () => ({
|
||||
export const mock = createApiMock(translationApiRef, () => ({
|
||||
getTranslation: jest.fn(),
|
||||
translation$: jest.fn(),
|
||||
}));
|
||||
@@ -298,7 +252,7 @@ export namespace mockApis {
|
||||
* @public
|
||||
*/
|
||||
export namespace config {
|
||||
export const mock = simpleMock(configApiRef, () => ({
|
||||
export const mock = createApiMock(configApiRef, () => ({
|
||||
has: jest.fn(),
|
||||
keys: jest.fn(),
|
||||
get: jest.fn(),
|
||||
@@ -342,7 +296,7 @@ export namespace mockApis {
|
||||
* @public
|
||||
*/
|
||||
export namespace discovery {
|
||||
export const mock = simpleMock(discoveryApiRef, () => ({
|
||||
export const mock = createApiMock(discoveryApiRef, () => ({
|
||||
getBaseUrl: jest.fn(),
|
||||
}));
|
||||
}
|
||||
@@ -390,7 +344,7 @@ export namespace mockApis {
|
||||
* @public
|
||||
*/
|
||||
export namespace identity {
|
||||
export const mock = simpleMock(identityApiRef, () => ({
|
||||
export const mock = createApiMock(identityApiRef, () => ({
|
||||
getBackstageIdentity: jest.fn(),
|
||||
getCredentials: jest.fn(),
|
||||
getProfileInfo: jest.fn(),
|
||||
@@ -427,7 +381,7 @@ export namespace mockApis {
|
||||
* @public
|
||||
*/
|
||||
export namespace permission {
|
||||
export const mock = simpleMock(permissionApiRef, () => ({
|
||||
export const mock = createApiMock(permissionApiRef, () => ({
|
||||
authorize: jest.fn(),
|
||||
}));
|
||||
}
|
||||
@@ -451,7 +405,7 @@ export namespace mockApis {
|
||||
* @public
|
||||
*/
|
||||
export namespace storage {
|
||||
export const mock = simpleMock(storageApiRef, () => ({
|
||||
export const mock = createApiMock(storageApiRef, () => ({
|
||||
forBucket: jest.fn(),
|
||||
snapshot: jest.fn(),
|
||||
set: jest.fn(),
|
||||
@@ -479,7 +433,7 @@ export namespace mockApis {
|
||||
* @public
|
||||
*/
|
||||
export namespace error {
|
||||
export const mock = simpleMock(errorApiRef, () => ({
|
||||
export const mock = createApiMock(errorApiRef, () => ({
|
||||
post: jest.fn(),
|
||||
error$: jest.fn(),
|
||||
}));
|
||||
@@ -504,7 +458,7 @@ export namespace mockApis {
|
||||
* @public
|
||||
*/
|
||||
export namespace fetch {
|
||||
export const mock = simpleMock(fetchApiRef, () => ({
|
||||
export const mock = createApiMock(fetchApiRef, () => ({
|
||||
fetch: jest.fn(),
|
||||
}));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user