From 0801db61832fd55a4ad2dd2d72a9496ab2a06e3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 7 Oct 2024 12:35:52 +0200 Subject: [PATCH] add the api mock type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/great-eagles-repair.md | 5 + .changeset/healthy-years-search.md | 7 ++ .changeset/lovely-bees-walk.md | 5 + packages/frontend-test-utils/report.api.md | 11 ++ .../frontend-test-utils/src/apis/ApiMock.ts | 32 ++++++ .../frontend-test-utils/src/apis/index.ts | 1 + plugins/catalog-node/src/testUtils.ts | 6 + plugins/catalog-react/package.json | 2 +- plugins/catalog-react/report-testUtils.api.md | 17 +++ plugins/catalog-react/src/testUtils.ts | 1 + .../src/testUtils/catalogApiMock.test.ts | 63 +++++++++++ .../src/testUtils/catalogApiMock.ts | 104 ++++++++++++++++++ 12 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 .changeset/great-eagles-repair.md create mode 100644 .changeset/healthy-years-search.md create mode 100644 .changeset/lovely-bees-walk.md create mode 100644 packages/frontend-test-utils/src/apis/ApiMock.ts create mode 100644 plugins/catalog-react/src/testUtils/catalogApiMock.test.ts create mode 100644 plugins/catalog-react/src/testUtils/catalogApiMock.ts diff --git a/.changeset/great-eagles-repair.md b/.changeset/great-eagles-repair.md new file mode 100644 index 0000000000..886fb5df7a --- /dev/null +++ b/.changeset/great-eagles-repair.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-test-utils': patch +--- + +Added an `ApiMock`, analogous to `ServiceMock` from the backend test utils. diff --git a/.changeset/healthy-years-search.md b/.changeset/healthy-years-search.md new file mode 100644 index 0000000000..a613d11eea --- /dev/null +++ b/.changeset/healthy-years-search.md @@ -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. diff --git a/.changeset/lovely-bees-walk.md b/.changeset/lovely-bees-walk.md new file mode 100644 index 0000000000..223acf1769 --- /dev/null +++ b/.changeset/lovely-bees-walk.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-node': patch +--- + +Documentation for the `testUtils` named export diff --git a/packages/frontend-test-utils/report.api.md b/packages/frontend-test-utils/report.api.md index 1337e5eb4f..3229da9898 100644 --- a/packages/frontend-test-utils/report.api.md +++ b/packages/frontend-test-utils/report.api.md @@ -3,11 +3,13 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +/// /// 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 = { + factory: ApiFactory; +} & { + [Key in keyof TApi]: TApi[Key] extends (...args: infer Args) => infer Return + ? TApi[Key] & jest.MockInstance + : TApi[Key]; +}; + // @public (undocumented) export function createExtensionTester( subject: ExtensionDefinition, diff --git a/packages/frontend-test-utils/src/apis/ApiMock.ts b/packages/frontend-test-utils/src/apis/ApiMock.ts new file mode 100644 index 0000000000..d11689f98a --- /dev/null +++ b/packages/frontend-test-utils/src/apis/ApiMock.ts @@ -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 = { + factory: ApiFactory; +} & { + [Key in keyof TApi]: TApi[Key] extends (...args: infer Args) => infer Return + ? TApi[Key] & jest.MockInstance + : TApi[Key]; +}; diff --git a/packages/frontend-test-utils/src/apis/index.ts b/packages/frontend-test-utils/src/apis/index.ts index 1230be9be0..564c327aef 100644 --- a/packages/frontend-test-utils/src/apis/index.ts +++ b/packages/frontend-test-utils/src/apis/index.ts @@ -26,4 +26,5 @@ export { type MockStorageBucket, } from '@backstage/test-utils'; +export { type ApiMock } from './ApiMock'; export { MockAnalyticsApi } from './AnalyticsApi/MockAnalyticsApi'; diff --git a/plugins/catalog-node/src/testUtils.ts b/plugins/catalog-node/src/testUtils.ts index de60594701..fd1134daf4 100644 --- a/plugins/catalog-node/src/testUtils.ts +++ b/plugins/catalog-node/src/testUtils.ts @@ -14,4 +14,10 @@ * limitations under the License. */ +/** + * Backend test helpers for the Catalog plugin. + * + * @packageDocumentation + */ + export { catalogServiceMock } from './testUtils/catalogServiceMock'; diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index cc59c46a6c..672ca2e0ed 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -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:^", diff --git a/plugins/catalog-react/report-testUtils.api.md b/plugins/catalog-react/report-testUtils.api.md index 5b00512236..331ef45988 100644 --- a/plugins/catalog-react/report-testUtils.api.md +++ b/plugins/catalog-react/report-testUtils.api.md @@ -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; + const mock: ( + partialImpl?: Partial | undefined, + ) => ApiMock; +} + // @public export function MockEntityListContextProvider< T extends DefaultEntityFilters = DefaultEntityFilters, diff --git a/plugins/catalog-react/src/testUtils.ts b/plugins/catalog-react/src/testUtils.ts index 4688e73feb..ffabf4f10c 100644 --- a/plugins/catalog-react/src/testUtils.ts +++ b/plugins/catalog-react/src/testUtils.ts @@ -20,4 +20,5 @@ * @packageDocumentation */ +export { catalogApiMock } from './testUtils/catalogApiMock'; export { MockEntityListContextProvider } from './testUtils/MockEntityListContextProvider'; diff --git a/plugins/catalog-react/src/testUtils/catalogApiMock.test.ts b/plugins/catalog-react/src/testUtils/catalogApiMock.test.ts new file mode 100644 index 0000000000..3501f808f2 --- /dev/null +++ b/plugins/catalog-react/src/testUtils/catalogApiMock.test.ts @@ -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] }); + }); +}); diff --git a/plugins/catalog-react/src/testUtils/catalogApiMock.ts b/plugins/catalog-react/src/testUtils/catalogApiMock.ts new file mode 100644 index 0000000000..8cf3b740b0 --- /dev/null +++ b/plugins/catalog-react/src/testUtils/catalogApiMock.ts @@ -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( + ref: ApiRef, + mockFactory: () => jest.Mocked, +): (partialImpl?: Partial) => ApiMock { + 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; + }; +} + +/** + * 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 => + 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(), + })); +}