frontend-test-utils: shorthand mocks everywhere

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2026-02-05 22:37:38 +01:00
parent 14611301e5
commit 0d7e331265
7 changed files with 61 additions and 50 deletions
+10 -12
View File
@@ -49,7 +49,6 @@ import { RouteRef } from '@backstage/frontend-plugin-api';
import { StorageApi } from '@backstage/core-plugin-api';
import { StorageApi as StorageApi_2 } from '@backstage/frontend-plugin-api';
import { StorageValueSnapshot } from '@backstage/core-plugin-api';
import { testingLibraryDomTypesQueries } from '@testing-library/dom/types/queries';
import { TranslationApi } from '@backstage/core-plugin-api/alpha';
import { TranslationApi as TranslationApi_2 } from '@backstage/frontend-plugin-api';
import { TranslationRef } from '@backstage/core-plugin-api/alpha';
@@ -551,8 +550,8 @@ export function renderInTestApp<TApiPairs extends any[] = any[]>(
// @public
export function renderTestApp<TApiPairs extends any[] = any[]>(
options: RenderTestAppOptions<TApiPairs>,
): RenderResult<testingLibraryDomTypesQueries, HTMLElement, HTMLElement>;
options?: RenderTestAppOptions<TApiPairs>,
): RenderResult;
// @public
export type RenderTestAppOptions<TApiPairs extends any[] = any[]> = {
@@ -563,7 +562,9 @@ export type RenderTestAppOptions<TApiPairs extends any[] = any[]> = {
mountedRoutes?: {
[path: string]: RouteRef;
};
apis?: readonly [...TestApiPairs<TApiPairs>];
apis?: readonly [
...(TestApiProviderPropsApiPairs<TApiPairs> | MockWithApiFactory<any>[]),
];
};
// @public
@@ -588,13 +589,8 @@ export type TestApiProviderProps<TApiPairs extends any[]> = {
};
// @public
export type TestApiProviderPropsApiPair<TApi> = TApi extends readonly [
infer _Ref,
infer _Impl,
]
? TApi
: TApi extends infer TImpl
? readonly [ApiRef<TApi>, Partial<TImpl>] | MockWithApiFactory<TApi>
export type TestApiProviderPropsApiPair<TApi> = TApi extends infer TImpl
? readonly [ApiRef<TApi>, Partial<TImpl>]
: never;
// @public
@@ -616,7 +612,9 @@ export type TestAppOptions<TApiPairs extends any[] = any[]> = {
config?: JsonObject;
features?: FrontendFeature[];
initialRouteEntries?: string[];
apis?: readonly [...TestApiPairs<TApiPairs>];
apis?: readonly [
...(TestApiProviderPropsApiPairs<TApiPairs> | MockWithApiFactory<any>[]),
];
};
export { withLogCollector };
@@ -14,9 +14,9 @@
* limitations under the License.
*/
import { rest } from 'msw';
import { setupServer } from 'msw';
import { registerMswTestHooks } from '@backstage/test-utils';
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';
import { registerMswTestHooks } from '../../..';
import { MockFetchApi } from './MockFetchApi';
describe('MockFetchApi', () => {
@@ -25,8 +25,8 @@ describe('MockFetchApi', () => {
it('works with default constructor', async () => {
worker.use(
rest.get('http://example.com/data.json', (_, res, ctx) =>
res(ctx.status(200), ctx.json({ a: 'foo' })),
http.get('http://example.com/data.json', () =>
HttpResponse.json({ a: 'foo' }, { status: 200 }),
),
);
const m = new MockFetchApi();
@@ -173,11 +173,11 @@ describe('createExtensionTester', () => {
});
const tester = createExtensionTester(extension, {
apis: [[analyticsApiRef, analyticsApiMock]],
apis: [[analyticsApiRef, analyticsApiMock] as const],
});
renderInTestApp(tester.reactElement(), {
apis: [[analyticsApiRef, analyticsApiMock]],
apis: [[analyticsApiRef, analyticsApiMock] as const],
});
expect(screen.getByText('Test')).toBeInTheDocument();
@@ -16,11 +16,8 @@
import { useCallback } from 'react';
import { screen, fireEvent } from '@testing-library/react';
import {
MockAnalyticsApi,
TestApiProvider,
} from '@backstage/frontend-test-utils';
import { analyticsApiRef, useAnalytics } from '@backstage/frontend-plugin-api';
import { mockApis, TestApiProvider } from '@backstage/frontend-test-utils';
import { useAnalytics } from '@backstage/frontend-plugin-api';
import { Routes, Route } from 'react-router-dom';
import { renderInTestApp } from './renderInTestApp';
@@ -47,10 +44,10 @@ describe('renderInTestApp', () => {
);
};
const analyticsApiMock = new MockAnalyticsApi();
const analyticsApiMock = mockApis.analytics();
renderInTestApp(
<TestApiProvider apis={[[analyticsApiRef, analyticsApiMock]]}>
<TestApiProvider apis={[analyticsApiMock]}>
<IndexPage />
</TestApiProvider>,
);
@@ -94,10 +91,10 @@ describe('renderInTestApp', () => {
);
};
const analyticsApiMock = new MockAnalyticsApi();
const analyticsApiMock = mockApis.analytics();
renderInTestApp(<IndexPage />, {
apis: [[analyticsApiRef, analyticsApiMock]],
apis: [analyticsApiMock],
});
fireEvent.click(screen.getByRole('button', { name: 'Click me' }));
@@ -32,10 +32,12 @@ import {
FrontendFeature,
createFrontendModule,
createApiFactory,
type ApiRef,
} from '@backstage/frontend-plugin-api';
import { RouterBlueprint } from '@backstage/plugin-app-react';
import appPlugin from '@backstage/plugin-app';
import { type TestApiPairs } from '../utils';
import { type TestApiProviderPropsApiPairs } from '../utils';
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';
@@ -88,15 +90,16 @@ export type TestAppOptions<TApiPairs extends any[] = any[]> = {
*
* @example
* ```ts
* import { identityApiRef } from '@backstage/frontend-plugin-api';
* import { mockApis } from '@backstage/frontend-test-utils';
*
* renderInTestApp(<MyComponent />, {
* apis: [[identityApiRef, mockApis.identity({ userEntityRef: 'user:default/guest' })]],
* apis: [mockApis.identity({ userEntityRef: 'user:default/guest' })],
* })
* ```
*/
apis?: readonly [...TestApiPairs<TApiPairs>];
apis?: readonly [
...(TestApiProviderPropsApiPairs<TApiPairs> | MockWithApiFactory<any>[]),
];
};
const NavItem = (props: {
@@ -241,9 +244,14 @@ export function renderInTestApp<TApiPairs extends any[] = any[]>(
},
]),
__internal: options?.apis && {
apiFactoryOverrides: options.apis.map(([apiRef, implementation]) =>
createApiFactory(apiRef, implementation),
),
apiFactoryOverrides: options.apis.map(entry => {
const mockFactory = getMockApiFactory(entry);
if (mockFactory) {
return mockFactory;
}
const [apiRef, implementation] = entry as readonly [ApiRef<any>, any];
return createApiFactory(apiRef, implementation);
}),
},
} as CreateSpecializedAppInternalOptions);
@@ -25,14 +25,16 @@ import {
ExtensionDefinition,
FrontendFeature,
RouteRef,
type ApiRef,
} from '@backstage/frontend-plugin-api';
import { render } from '@testing-library/react';
import { render, type RenderResult } from '@testing-library/react';
import appPlugin from '@backstage/plugin-app';
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 TestApiPairs } from '../utils';
import { type TestApiProviderPropsApiPairs } from '../utils';
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';
@@ -89,16 +91,17 @@ export type RenderTestAppOptions<TApiPairs extends any[] = any[]> = {
*
* @example
* ```ts
* import { identityApiRef } from '@backstage/frontend-plugin-api';
* import { mockApis } from '@backstage/frontend-test-utils';
*
* renderTestApp({
* apis: [[identityApiRef, mockApis.identity({ userEntityRef: 'user:default/guest' })]],
* apis: [mockApis.identity({ userEntityRef: 'user:default/guest' })],
* extensions: [...],
* })
* ```
*/
apis?: readonly [...TestApiPairs<TApiPairs>];
apis?: readonly [
...(TestApiProviderPropsApiPairs<TApiPairs> | MockWithApiFactory<any>[]),
];
};
const appPluginOverride = appPlugin.withOverrides({
@@ -116,11 +119,11 @@ const appPluginOverride = appPlugin.withOverrides({
* @public
*/
export function renderTestApp<TApiPairs extends any[] = any[]>(
options: RenderTestAppOptions<TApiPairs>,
) {
const extensions = [...(options.extensions ?? [])];
options?: RenderTestAppOptions<TApiPairs>,
): RenderResult {
const extensions = [...(options?.extensions ?? [])];
if (options.mountedRoutes) {
if (options?.mountedRoutes) {
for (const [path, routeRef] of Object.entries(options.mountedRoutes)) {
extensions.push(
createExtension({
@@ -150,7 +153,7 @@ export function renderTestApp<TApiPairs extends any[] = any[]>(
params: {
component: ({ children }) => (
<MemoryRouter
initialEntries={options.initialRouteEntries}
initialEntries={options?.initialRouteEntries}
future={{
v7_relativeSplatPath: true,
v7_startTransition: true,
@@ -170,7 +173,7 @@ export function renderTestApp<TApiPairs extends any[] = any[]>(
appPluginOverride,
];
if (options.features) {
if (options?.features) {
features.push(...options.features);
}
@@ -183,9 +186,14 @@ export function renderTestApp<TApiPairs extends any[] = any[]>(
},
]),
__internal: options?.apis && {
apiFactoryOverrides: options.apis.map(([apiRef, implementation]) =>
createApiFactory(apiRef, implementation),
),
apiFactoryOverrides: options.apis.map(entry => {
const mockFactory = getMockApiFactory(entry);
if (mockFactory) {
return mockFactory;
}
const [apiRef, implementation] = entry as readonly [ApiRef<any>, any];
return createApiFactory(apiRef, implementation);
}),
},
} as CreateSpecializedAppInternalOptions);
@@ -45,7 +45,7 @@ export type TestApiPairs<TApiPairs> = TestApiProviderPropsApiPairs<TApiPairs>;
/**
* Type for entries that can be passed to TestApiProvider/TestApiRegistry.
* Can be either a traditional [apiRef, implementation] tuple or a mock API instance
* marked with the MockApiSymbol.
* marked with the mockApiFactorySymbol.
*
* @public
*/