add the api mock type

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2024-10-07 12:35:52 +02:00
parent e5944f626a
commit 0801db6183
12 changed files with 253 additions and 1 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/frontend-test-utils': patch
---
Added an `ApiMock`, analogous to `ServiceMock` from the backend test utils.
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-catalog-react': minor
---
Add catalog service mocks under the `/testUtils` subpath export.
You can now use e.g. `const catalog = catalogApiMock.mock()` in your test and then do assertions on `catalog.getEntities` without awkward type casting.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-node': patch
---
Documentation for the `testUtils` named export
@@ -3,11 +3,13 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
/// <reference types="jest" />
/// <reference types="react" />
import { AnalyticsApi } from '@backstage/frontend-plugin-api';
import { AnalyticsEvent } from '@backstage/frontend-plugin-api';
import { AnyExtensionDataRef } from '@backstage/frontend-plugin-api';
import { ApiFactory } from '@backstage/frontend-plugin-api';
import { AppNode } from '@backstage/frontend-plugin-api';
import { AppNodeInstance } from '@backstage/frontend-plugin-api';
import { ErrorWithContext } from '@backstage/test-utils';
@@ -32,6 +34,15 @@ import { TestApiProviderProps } from '@backstage/test-utils';
import { TestApiRegistry } from '@backstage/test-utils';
import { withLogCollector } from '@backstage/test-utils';
// @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 (undocumented)
export function createExtensionTester<T extends ExtensionDefinitionParameters>(
subject: ExtensionDefinition<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/frontend-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];
};
@@ -26,4 +26,5 @@ export {
type MockStorageBucket,
} from '@backstage/test-utils';
export { type ApiMock } from './ApiMock';
export { MockAnalyticsApi } from './AnalyticsApi/MockAnalyticsApi';
+6
View File
@@ -14,4 +14,10 @@
* limitations under the License.
*/
/**
* Backend test helpers for the Catalog plugin.
*
* @packageDocumentation
*/
export { catalogServiceMock } from './testUtils/catalogServiceMock';
+1 -1
View File
@@ -68,6 +68,7 @@
"@backstage/core-plugin-api": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/frontend-plugin-api": "workspace:^",
"@backstage/frontend-test-utils": "workspace:^",
"@backstage/integration-react": "workspace:^",
"@backstage/plugin-catalog-common": "workspace:^",
"@backstage/plugin-permission-common": "workspace:^",
@@ -89,7 +90,6 @@
"devDependencies": {
"@backstage/cli": "workspace:^",
"@backstage/core-app-api": "workspace:^",
"@backstage/frontend-test-utils": "workspace:^",
"@backstage/plugin-catalog-common": "workspace:^",
"@backstage/plugin-scaffolder-common": "workspace:^",
"@backstage/test-utils": "workspace:^",
@@ -3,11 +3,28 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
import { ApiFactory } from '@backstage/frontend-plugin-api';
import { ApiMock } from '@backstage/frontend-test-utils';
import { CatalogApi } from '@backstage/catalog-client';
import { DefaultEntityFilters } from '@backstage/plugin-catalog-react';
import { Entity } from '@backstage/catalog-model';
import { EntityListContextProps } from '@backstage/plugin-catalog-react';
import { PropsWithChildren } from 'react';
import { default as React_2 } from 'react';
// @public
export function catalogApiMock(options?: { entities?: Entity[] }): CatalogApi;
// @public
export namespace catalogApiMock {
const factory: (options?: {
entities?: Entity[];
}) => ApiFactory<CatalogApi, CatalogApi, {}>;
const mock: (
partialImpl?: Partial<CatalogApi> | undefined,
) => ApiMock<CatalogApi>;
}
// @public
export function MockEntityListContextProvider<
T extends DefaultEntityFilters = DefaultEntityFilters,
+1
View File
@@ -20,4 +20,5 @@
* @packageDocumentation
*/
export { catalogApiMock } from './testUtils/catalogApiMock';
export { MockEntityListContextProvider } from './testUtils/MockEntityListContextProvider';
@@ -0,0 +1,63 @@
/*
* 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 { Entity } from '@backstage/catalog-model';
import { catalogApiMock } from './catalogApiMock';
const entity1: Entity = {
apiVersion: 'v1',
kind: 'CustomKind',
metadata: {
namespace: 'default',
name: 'e1',
uid: 'u1',
},
};
const entity2: Entity = {
apiVersion: 'v1',
kind: 'CustomKind',
metadata: {
namespace: 'default',
name: 'e2',
uid: 'u2',
},
};
const entities = [entity1, entity2];
describe('catalogApiMock', () => {
it('exports the expected functionality', async () => {
const emptyFake = catalogApiMock();
const notEmptyFake = catalogApiMock({ entities });
await expect(emptyFake.getEntities()).resolves.toEqual({ items: [] });
await expect(notEmptyFake.getEntities()).resolves.toEqual({
items: entities,
});
const mock = catalogApiMock.mock();
expect(mock.getEntities).toHaveBeenCalledTimes(0);
expect(mock.getEntities()).toBeUndefined();
mock.getEntities.mockResolvedValue({ items: entities });
await expect(mock.getEntities()).resolves.toEqual({ items: entities });
const mock2 = catalogApiMock.mock({
getEntities: async () => ({ items: [entity1] }),
});
await expect(mock2.getEntities()).resolves.toEqual({ items: [entity1] });
});
});
@@ -0,0 +1,104 @@
/*
* 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,
ApiRef,
createApiFactory,
} from '@backstage/frontend-plugin-api';
import { InMemoryCatalogClient } from '@backstage/catalog-client/testUtils';
import { Entity } from '@backstage/catalog-model';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import { CatalogApi } from '@backstage/catalog-client';
import { ApiMock } from '@backstage/frontend-test-utils';
/** @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>;
};
}
/**
* Creates a fake catalog client that handles entities in memory storage. Note
* that this client may be severely limited in functionality, and advanced
* functions may not be available at all.
*
* @public
*/
export function catalogApiMock(options?: { entities?: Entity[] }): CatalogApi {
return new InMemoryCatalogClient(options);
}
/**
* A collection of mock functionality for the catalog service.
*
* @public
*/
export namespace catalogApiMock {
/**
* Creates a fake catalog client that handles entities in memory storage. Note
* that this client may be severely limited in functionality, and advanced
* functions may not be available at all.
*/
export const factory = (options?: {
entities?: Entity[];
}): ApiFactory<CatalogApi, CatalogApi, {}> =>
createApiFactory({
api: catalogApiRef,
deps: {},
factory: () => new InMemoryCatalogClient(options),
});
/**
* Creates a catalog client whose methods are mock functions, possibly with
* some of them overloaded by the caller.
*/
export const mock = simpleMock(catalogApiRef, () => ({
getEntities: jest.fn(),
getEntitiesByRefs: jest.fn(),
queryEntities: jest.fn(),
getEntityAncestors: jest.fn(),
getEntityByRef: jest.fn(),
removeEntityByUid: jest.fn(),
refreshEntity: jest.fn(),
getEntityFacets: jest.fn(),
getLocationById: jest.fn(),
getLocationByRef: jest.fn(),
addLocation: jest.fn(),
removeLocationById: jest.fn(),
getLocationByEntity: jest.fn(),
validateEntity: jest.fn(),
}));
}