diff --git a/packages/frontend-test-utils/report.api.md b/packages/frontend-test-utils/report.api.md index d3951a03e8..aa24db3a96 100644 --- a/packages/frontend-test-utils/report.api.md +++ b/packages/frontend-test-utils/report.api.md @@ -580,11 +580,6 @@ export const TestApiProvider: ( props: TestApiProviderProps, ) => JSX_2.Element; -// @public -export type TestApiProviderEntry = - | readonly [ApiRef, any] - | MockWithApiFactory; - // @public export type TestApiProviderProps = { apis: readonly [ @@ -593,22 +588,6 @@ export type TestApiProviderProps = { children: ReactNode; }; -// @public -export type TestApiProviderPropsApiPair = TApi extends infer TImpl - ? readonly [ApiRef, Partial] - : never; - -// @public -export type TestApiProviderPropsApiPairs = { - [TIndex in keyof TApiPairs]: TestApiProviderPropsApiPair; -}; - -// @public -export class TestApiRegistry implements ApiHolder { - static from(...apis: readonly TestApiProviderEntry[]): TestApiRegistry; - get(api: ApiRef): T | undefined; -} - // @public export type TestAppOptions = { mountedRoutes?: { diff --git a/packages/frontend-test-utils/src/utils/TestApiProvider.tsx b/packages/frontend-test-utils/src/apis/TestApiProvider.tsx similarity index 51% rename from packages/frontend-test-utils/src/utils/TestApiProvider.tsx rename to packages/frontend-test-utils/src/apis/TestApiProvider.tsx index 4bd02a76c4..90dd53f61a 100644 --- a/packages/frontend-test-utils/src/utils/TestApiProvider.tsx +++ b/packages/frontend-test-utils/src/apis/TestApiProvider.tsx @@ -18,11 +18,11 @@ 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 '../apis/utils'; +import { getMockApiFactory, type MockWithApiFactory } from './utils'; /** * Helper type for representing an API reference paired with a partial implementation. - * @public + * @ignore */ export type TestApiProviderPropsApiPair = TApi extends infer TImpl ? readonly [ApiRef, Partial] @@ -30,7 +30,7 @@ export type TestApiProviderPropsApiPair = TApi extends infer TImpl /** * Helper type for representing an array of API reference pairs. - * @public + * @ignore */ export type TestApiProviderPropsApiPairs = { [TIndex in keyof TApiPairs]: TestApiProviderPropsApiPair; @@ -43,16 +43,37 @@ export type TestApiProviderPropsApiPairs = { export type TestApiPairs = TestApiProviderPropsApiPairs; /** - * Type for entries that can be passed to TestApiProvider/TestApiRegistry. + * 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. * - * @public + * @internal */ export type TestApiProviderEntry = | readonly [ApiRef, any] | MockWithApiFactory; +/** @internal */ +export function resolveTestApiEntries( + apis: readonly (TestApiProviderEntry | readonly [ApiRef, any])[], +): ApiHolder { + const apiMap = new Map(); + + for (const entry of apis) { + const mockFactory = getMockApiFactory(entry); + if (mockFactory) { + apiMap.set(mockFactory.api.id, mockFactory.factory({})); + } else { + const [apiRef, impl] = entry as readonly [ApiRef, any]; + apiMap.set(apiRef.id, impl); + } + } + + return { + get: (ref: ApiRef) => apiMap.get(ref.id) as T | undefined, + }; +} + /** * Properties for the {@link TestApiProvider} component. * @@ -65,81 +86,6 @@ export type TestApiProviderProps = { children: ReactNode; }; -/** - * The `TestApiRegistry` is an {@link @backstage/frontend-plugin-api#ApiHolder} implementation - * that is particularly well suited for development and test environments such as - * unit tests, storybooks, and isolated plugin development setups. - * - * @remarks - * - * For most test scenarios, prefer using the `apis` option in `renderInTestApp` or - * `createExtensionTester` instead of creating a registry directly. - * - * @public - */ -export class TestApiRegistry implements ApiHolder { - /** - * Creates a new {@link TestApiRegistry} with a list of API implementation pairs. - * - * Similar to the {@link TestApiProvider}, there is no need to provide a full - * implementation of each API, it's enough to implement the methods that are tested. - * - * @example - * ```ts - * import { identityApiRef } from '@backstage/frontend-plugin-api'; - * import { mockApis } from '@backstage/frontend-test-utils'; - * - * // Traditional tuple syntax - * const apis1 = TestApiRegistry.from( - * [identityApiRef, mockApis.identity({ userEntityRef: 'user:default/guest' })], - * ); - * - * // Direct mock API instance (no tuple needed) - * const apis2 = TestApiRegistry.from( - * mockApis.identity({ userEntityRef: 'user:default/guest' }), - * mockApis.alert(), - * ); - * ``` - * - * @public - * @param apis - A list of pairs mapping an ApiRef to its respective implementation, - * or mock API instances marked with the mockApiSymbol. - */ - static from(...apis: readonly TestApiProviderEntry[]) { - const apiMap = new Map(); - - for (const entry of apis) { - const mockFactory = getMockApiFactory(entry); - if (mockFactory) { - // Handle mock API instances marked with the symbol - const impl = mockFactory.factory({}); - apiMap.set(mockFactory.api.id, impl); - } else if (Array.isArray(entry)) { - // Handle traditional [apiRef, impl] tuples - const [apiRef, impl] = entry; - apiMap.set(apiRef.id, impl); - } else { - throw new Error( - `Invalid API entry provided to TestApiRegistry.from(). Expected either [apiRef, impl] tuple or a mock API instance.`, - ); - } - } - - return new TestApiRegistry(apiMap); - } - - private constructor(private readonly apis: Map) {} - - /** - * Returns an implementation of the API. - * - * @public - */ - get(api: ApiRef): T | undefined { - return this.apis.get(api.id) as T | undefined; - } -} - /** * The `TestApiProvider` is a Utility API context provider for standalone rendering * scenarios where you're not using `renderInTestApp` or other test utilities. @@ -155,19 +101,22 @@ export class TestApiRegistry implements ApiHolder { * @example * ```tsx * import { render } from '\@testing-library/react'; - * import { identityApiRef } from '\@backstage/frontend-plugin-api'; + * import { myCustomApiRef } from '../apis'; * import { TestApiProvider, mockApis } from '\@backstage/frontend-test-utils'; * - * // Traditional tuple syntax + * // Mock custom APIs with tuple syntax + * const myCustomApiMock = { myMethod: jest.fn() }; * render( * * * * ); * - * // Direct mock API instances (no tuples needed) + * // Use with built-in mock APIs (no tuples needed) * render( * ( ) => { return ( ); diff --git a/packages/frontend-test-utils/src/apis/index.ts b/packages/frontend-test-utils/src/apis/index.ts index 068c0a05ec..1a3ce150c9 100644 --- a/packages/frontend-test-utils/src/apis/index.ts +++ b/packages/frontend-test-utils/src/apis/index.ts @@ -21,6 +21,14 @@ export { type MockWithApiFactory, attachMockApiFactory, } from './utils'; +export { + TestApiProvider, + type TestApiProviderPropsApiPair, + type TestApiProviderPropsApiPairs, + type TestApiPairs, + type TestApiProviderEntry, +} from './TestApiProvider'; +export type { TestApiProviderProps } from './TestApiProvider'; /** * Mock API classes are exported as types only to prevent direct instantiation. diff --git a/packages/frontend-test-utils/src/app/createExtensionTester.tsx b/packages/frontend-test-utils/src/app/createExtensionTester.tsx index 91a7bf9022..ddceab52bd 100644 --- a/packages/frontend-test-utils/src/app/createExtensionTester.tsx +++ b/packages/frontend-test-utils/src/app/createExtensionTester.tsx @@ -38,7 +38,8 @@ 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 { TestApiRegistry, type TestApiPairs } from '../utils'; +import { type TestApiPairs } from '../apis'; +import { resolveTestApiEntries } from '../apis/TestApiProvider'; /** * Represents a snapshot of an extension in the app tree. @@ -281,9 +282,7 @@ export class ExtensionTester { collector, ); - const apiHolder = this.#apis - ? TestApiRegistry.from(...this.#apis) - : TestApiRegistry.from(); + const apiHolder = resolveTestApiEntries(this.#apis ?? []); instantiateAppNodeTree(tree.root, apiHolder, collector); diff --git a/packages/frontend-test-utils/src/app/renderInTestApp.tsx b/packages/frontend-test-utils/src/app/renderInTestApp.tsx index a84b7063d6..79bab70653 100644 --- a/packages/frontend-test-utils/src/app/renderInTestApp.tsx +++ b/packages/frontend-test-utils/src/app/renderInTestApp.tsx @@ -36,7 +36,7 @@ import { } from '@backstage/frontend-plugin-api'; import { RouterBlueprint } from '@backstage/plugin-app-react'; import appPlugin from '@backstage/plugin-app'; -import { type TestApiProviderPropsApiPairs } from '../utils'; +import { type TestApiProviderPropsApiPairs } from '../apis'; import { getMockApiFactory, type MockWithApiFactory } from '../apis/utils'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import type { CreateSpecializedAppInternalOptions } from '../../../frontend-app-api/src/wiring/createSpecializedApp'; diff --git a/packages/frontend-test-utils/src/app/renderTestApp.tsx b/packages/frontend-test-utils/src/app/renderTestApp.tsx index b7b2c2dd6e..78efb05a41 100644 --- a/packages/frontend-test-utils/src/app/renderTestApp.tsx +++ b/packages/frontend-test-utils/src/app/renderTestApp.tsx @@ -33,7 +33,7 @@ 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 '../utils'; +import { type TestApiProviderPropsApiPairs } from '../apis'; import { getMockApiFactory, type MockWithApiFactory } from '../apis/utils'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import type { CreateSpecializedAppInternalOptions } from '../../../frontend-app-api/src/wiring/createSpecializedApp'; diff --git a/packages/frontend-test-utils/src/index.ts b/packages/frontend-test-utils/src/index.ts index b709e7ac03..03f08ab929 100644 --- a/packages/frontend-test-utils/src/index.ts +++ b/packages/frontend-test-utils/src/index.ts @@ -22,10 +22,9 @@ export * from './apis'; export * from './app'; -export * from './utils'; // Explicit export to satisfy API Extractor -export type { TestApiPairs } from './utils'; +export type { TestApiPairs } from './apis'; export { withLogCollector } from '@backstage/test-utils'; diff --git a/packages/frontend-test-utils/src/utils/index.ts b/packages/frontend-test-utils/src/utils/index.ts deleted file mode 100644 index 32724fabba..0000000000 --- a/packages/frontend-test-utils/src/utils/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2023 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. - */ - -export { - TestApiProvider, - TestApiRegistry, - type TestApiProviderPropsApiPair, - type TestApiProviderPropsApiPairs, - type TestApiPairs, - type TestApiProviderEntry, -} from './TestApiProvider'; -export type { TestApiProviderProps } from './TestApiProvider'; diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedKindsFilter.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedKindsFilter.test.tsx index d362c9c7d9..b8822de1c1 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedKindsFilter.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedKindsFilter.test.tsx @@ -14,11 +14,8 @@ * limitations under the License. */ -import { ApiProvider } from '@backstage/core-app-api'; -import { errorApiRef } from '@backstage/core-plugin-api'; -import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { renderWithEffects } from '@backstage/test-utils'; -import { mockApis, TestApiRegistry } from '@backstage/frontend-test-utils'; +import { mockApis, TestApiProvider } from '@backstage/frontend-test-utils'; import { waitFor, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { SelectedKindsFilter } from './SelectedKindsFilter'; @@ -36,28 +33,28 @@ const catalogApi = catalogApiMock.mock({ }, }), }); -const apis = TestApiRegistry.from( +const apis = [ mockApis.alert(), mockApis.translation(), - [catalogApiRef, catalogApi], - [errorApiRef, { post: jest.fn() }], -); + mockApis.error(), + catalogApi, +] as const; describe('', () => { it('should not explode while loading', async () => { const { baseElement } = await renderWithEffects( - + {}} /> - , + , ); expect(baseElement).toBeInTheDocument(); }); it('should render current value', async () => { await renderWithEffects( - + {}} /> - , + , ); expect(screen.getByText('API')).toBeInTheDocument(); @@ -67,9 +64,9 @@ describe('', () => { it('should select value', async () => { const onChange = jest.fn(); await renderWithEffects( - + - , + , ); await userEvent.click(screen.getByLabelText('Open')); @@ -85,12 +82,12 @@ describe('', () => { it('should return undefined if all values are selected', async () => { const onChange = jest.fn(); await renderWithEffects( - + - , + , ); await userEvent.click(screen.getByLabelText('Open')); @@ -108,9 +105,9 @@ describe('', () => { it('should return all values when cleared', async () => { const onChange = jest.fn(); await renderWithEffects( - + - , + , ); await userEvent.click(screen.getByRole('combobox'));