implement the beginnings of mockApis

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2024-10-08 17:03:22 +02:00
parent f91809d6f6
commit 9cc7dd6cfd
45 changed files with 632 additions and 299 deletions
+5
View File
@@ -65,6 +65,7 @@
"devDependencies": {
"@backstage/cli": "workspace:^",
"@testing-library/jest-dom": "^6.0.0",
"@types/jest": "*",
"@types/react": "^18.0.0",
"msw": "^1.0.0",
"react": "^18.0.2",
@@ -73,12 +74,16 @@
},
"peerDependencies": {
"@testing-library/react": "^16.0.0",
"@types/jest": "*",
"@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0",
"react": "^16.13.1 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0",
"react-router-dom": "6.0.0-beta.0 || ^6.3.0"
},
"peerDependenciesMeta": {
"@types/jest": {
"optional": true
},
"@types/react": {
"optional": true
}
+30 -1
View File
@@ -3,8 +3,11 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
/// <reference types="jest" />
import { AnalyticsApi } from '@backstage/core-plugin-api';
import { AnalyticsEvent } from '@backstage/core-plugin-api';
import { ApiFactory } from '@backstage/core-plugin-api';
import { ApiHolder } from '@backstage/core-plugin-api';
import { ApiRef } from '@backstage/core-plugin-api';
import { AppComponents } from '@backstage/core-plugin-api';
@@ -39,6 +42,15 @@ import { RouteRef } from '@backstage/core-plugin-api';
import { StorageApi } from '@backstage/core-plugin-api';
import { StorageValueSnapshot } from '@backstage/core-plugin-api';
// @public
export type ApiMock<TApi> = {
factory: ApiFactory<TApi, TApi, {}>;
} & {
[Key in keyof TApi]: TApi[Key] extends (...args: infer Args) => infer Return
? TApi[Key] & jest.MockInstance<Return, Args>
: TApi[Key];
};
// @public
export type AsyncLogCollector = () => Promise<void>;
@@ -77,10 +89,27 @@ export class MockAnalyticsApi implements AnalyticsApi {
getEvents(): AnalyticsEvent[];
}
// @public
export namespace mockApis {
export function config(options?: { data?: JsonObject }): jest.Mocked<Config>;
export namespace config {
const factory: (
options?:
| {
data?: JsonObject | undefined;
}
| undefined,
) => ApiFactory<Config, Config, {}>;
const mock: (partialImpl?: Partial<Config> | undefined) => ApiMock<Config>;
}
}
// @public @deprecated
export function mockBreakpoint(options: { matches: boolean }): void;
// @public
// Warning: (ae-unresolved-link) The @link reference could not be resolved: The reference is ambiguous because "config" has more than one declaration; you need to add a TSDoc member reference selector
//
// @public @deprecated
export class MockConfigApi implements ConfigApi {
constructor(data: JsonObject);
get<T = JsonValue>(key?: string): T;
@@ -0,0 +1,32 @@
/*
* 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/core-plugin-api';
/**
* 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, {}>;
} & {
[Key in keyof TApi]: TApi[Key] extends (...args: infer Args) => infer Return
? TApi[Key] & jest.MockInstance<Return, Args>
: TApi[Key];
};
@@ -23,6 +23,7 @@ import { ConfigApi } from '@backstage/core-plugin-api';
* that can be used to mock configuration using a plain object.
*
* @public
* @deprecated Use {@link mockApis.config} instead
* @example
* ```tsx
* const mockConfig = new MockConfigApi({
@@ -20,3 +20,5 @@ export * from './ErrorApi';
export * from './FetchApi';
export * from './PermissionApi';
export * from './StorageApi';
export { type ApiMock } from './ApiMock';
export { mockApis } from './mockApis';
@@ -0,0 +1,40 @@
/*
* 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 { mockApis } from './mockApis';
describe('mockApis', () => {
describe('config', () => {
const data = { backend: { baseUrl: 'http://test.com' } };
it('can create an instance and make assertions on it', () => {
const empty = mockApis.config();
const notEmpty = mockApis.config({ data });
expect(empty.getOptional('backend.baseUrl')).toBeUndefined();
expect(empty.getOptional).toHaveBeenCalledTimes(1);
expect(notEmpty.getOptional('backend.baseUrl')).toEqual(
'http://test.com',
);
expect(notEmpty.getOptional).toHaveBeenCalledTimes(1);
});
it('can create a mock and make assertions on it', async () => {
const mock = mockApis.config.mock({ getString: () => 'replaced' });
expect(mock.getString('a')).toEqual('replaced');
expect(mock.getString).toHaveBeenCalledTimes(1);
});
});
});
@@ -0,0 +1,188 @@
/*
* 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 { ConfigReader } from '@backstage/config';
import {
ApiFactory,
ApiRef,
configApiRef,
createApiFactory,
} from '@backstage/core-plugin-api';
import { JsonObject } from '@backstage/types';
import { ApiMock } from './ApiMock';
/** @internal */
function simpleInstance<TApi extends object>(
_ref: ApiRef<TApi>,
instance: TApi,
mockSkeleton: () => jest.Mocked<TApi>,
): jest.Mocked<TApi> {
const mock = mockSkeleton();
const result = Object.create(instance) as any;
for (const [key, impl] of Object.entries(mock)) {
result[key] = (impl as any).mockImplementation((instance as any)[key]);
}
return result;
}
/** @internal */
function simpleFactory<TApi, TArgs extends unknown[]>(
ref: ApiRef<TApi>,
factory: (...args: TArgs) => TApi,
): (...args: TArgs) => ApiFactory<TApi, TApi, {}> {
return (...args) =>
createApiFactory({
api: ref,
deps: {},
factory: () => factory(...args),
});
}
/** @internal */
function simpleMock<TApi>(
ref: 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;
}
}
}
return Object.assign(mock, {
factory: createApiFactory({
api: ref,
deps: {},
factory: () => mock,
}),
}) as ApiMock<TApi>;
};
}
/**
* Mock implementations of the core utility APIs, to be used in tests.
*
* @public
* @remarks
*
* There are some variations among the APIs depending on what needs tests
* might have, but overall there are three main usage patterns:
*
* 1: Creating an actual fake API instance, often with a simplified version
* of functionality, by calling the mock API itself as a function.
*
* ```ts
* // The function often accepts parameters that control its behavior
* const foo = mockApis.foo();
* ```
*
* 2: Creating a mock API, where all methods are replaced with jest mocks, by
* calling the API's `mock` function.
*
* ```ts
* // You can optionally supply a subset of its methods to implement
* const foo = mockApis.foo.mock({
* someMethod: () => 'mocked result',
* });
* // After exercising your test, you can make assertions on the mock:
* expect(foo.someMethod).toHaveBeenCalledTimes(2);
* expect(foo.otherMethod).toHaveBeenCalledWith(testData);
* ```
*
* 3: Creating an API factory that behaves similarly to the mock as per above.
*
* ```ts
* const factory = mockApis.foo.factory({
* someMethod: () => 'mocked result',
* });
* ```
*/
export namespace mockApis {
const configMockSkeleton = () => ({
has: jest.fn(),
keys: jest.fn(),
get: jest.fn(),
getOptional: jest.fn(),
getConfig: jest.fn(),
getOptionalConfig: jest.fn(),
getConfigArray: jest.fn(),
getOptionalConfigArray: jest.fn(),
getNumber: jest.fn(),
getOptionalNumber: jest.fn(),
getBoolean: jest.fn(),
getOptionalBoolean: jest.fn(),
getString: jest.fn(),
getOptionalString: jest.fn(),
getStringArray: jest.fn(),
getOptionalStringArray: jest.fn(),
});
/**
* Fake implementation of {@link @backstage/frontend-plugin-api#ConfigApi}
* with optional data supplied.
*
* @public
* @example
*
* ```tsx
* const config = mockApis.config({
* data: { app: { baseUrl: 'https://example.com' } },
* });
*
* const rendered = await renderInTestApp(
* <TestApiProvider apis={[[configApiRef, config]]}>
* <MyTestedComponent />
* </TestApiProvider>,
* );
* ```
*/
export function config(options?: { data?: JsonObject }) {
return simpleInstance(
configApiRef,
new ConfigReader(options?.data, 'mock-config'),
configMockSkeleton,
);
}
/**
* Mock helpers for {@link @backstage/frontend-plugin-api#ConfigApi}.
*
* @see {@link @backstage/frontend-plugin-api#mockApis.config}
* @public
*/
export namespace config {
/**
* Creates a factory for a fake implementation of
* {@link @backstage/frontend-plugin-api#ConfigApi} with optional
* configuration data supplied.
*
* @public
*/
export const factory = simpleFactory(configApiRef, config);
/**
* Creates a mock implementation of
* {@link @backstage/frontend-plugin-api#ConfigApi}. All methods are
* replaced with jest mock functions, and you can optionally pass in a
* subset of methods with an explicit implementation.
*
* @public
*/
export const mock = simpleMock(configApiRef, configMockSkeleton);
}
}