Merge pull request #32617 from backstage/rugvip/api-override-test-utils
frontend-test-utils: add API override support
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
---
|
||||
'@backstage/frontend-test-utils': patch
|
||||
---
|
||||
|
||||
Added an `apis` option to `createExtensionTester`, `renderInTestApp`, and `renderTestApp` to override APIs when testing extensions. Use the `mockApis` helpers to create mock implementations:
|
||||
|
||||
```typescript
|
||||
import { identityApiRef } from '@backstage/frontend-plugin-api';
|
||||
import { mockApis } from '@backstage/frontend-test-utils';
|
||||
|
||||
// Override APIs in createExtensionTester
|
||||
const tester = createExtensionTester(myExtension, {
|
||||
apis: [
|
||||
[
|
||||
identityApiRef,
|
||||
mockApis.identity({ userEntityRef: 'user:default/guest' }),
|
||||
],
|
||||
],
|
||||
});
|
||||
|
||||
// Override APIs in renderInTestApp
|
||||
renderInTestApp(<MyComponent />, {
|
||||
apis: [
|
||||
[
|
||||
identityApiRef,
|
||||
mockApis.identity({ userEntityRef: 'user:default/guest' }),
|
||||
],
|
||||
],
|
||||
});
|
||||
|
||||
// Override APIs in renderTestApp
|
||||
renderTestApp({
|
||||
extensions: [myExtension],
|
||||
apis: [
|
||||
[
|
||||
identityApiRef,
|
||||
mockApis.identity({ userEntityRef: 'user:default/guest' }),
|
||||
],
|
||||
],
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-app-api': patch
|
||||
---
|
||||
|
||||
Internal update to simplify testing utility implementations.
|
||||
@@ -31,50 +31,50 @@ describe('Entity details component', () => {
|
||||
});
|
||||
```
|
||||
|
||||
To mock [Utility APIs](../architecture/33-utility-apis.md) that are used by your component you can use the `TestApiProvider` to override individual API implementations. In the snippet below, we wrap the component within a `TestApiProvider` in order to mock the catalog client API:
|
||||
To mock [Utility APIs](../architecture/33-utility-apis.md) that are used by your component, pass API overrides to `renderInTestApp` using the `apis` option. Mock helpers are available from `@backstage/frontend-test-utils` and plugin-specific test utilities:
|
||||
|
||||
```tsx
|
||||
import { screen } from '@testing-library/react';
|
||||
import {
|
||||
renderInTestApp,
|
||||
TestApiProvider,
|
||||
} from '@backstage/frontend-test-utils';
|
||||
import { stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
import { EntityDetails } from './plugin';
|
||||
import { renderInTestApp, mockApis } from '@backstage/frontend-test-utils';
|
||||
import { identityApiRef } from '@backstage/frontend-plugin-api';
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
|
||||
import { MyEntitiesList } from './plugin';
|
||||
|
||||
describe('Entity details component', () => {
|
||||
it('should render the entity name and owner', async () => {
|
||||
const catalogApiMock = {
|
||||
async getEntityFacets() {
|
||||
return {
|
||||
facets: {
|
||||
'relations.ownedBy': [{ count: 1, value: 'group:default/tools' }],
|
||||
},
|
||||
},
|
||||
}
|
||||
} satisfies Partial<typeof catalogApiRef.T>;
|
||||
|
||||
const entityRef = stringifyEntityRef({
|
||||
kind: 'Component',
|
||||
namespace: 'default',
|
||||
name: 'test',
|
||||
describe('MyEntitiesList', () => {
|
||||
it('should render entities owned by the current user', async () => {
|
||||
await renderInTestApp(<MyEntitiesList />, {
|
||||
apis: [
|
||||
[
|
||||
identityApiRef,
|
||||
mockApis.identity({ userEntityRef: 'user:default/guest' }),
|
||||
],
|
||||
[
|
||||
catalogApiRef,
|
||||
catalogApiMock({
|
||||
entities: [
|
||||
{
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'Component',
|
||||
metadata: { name: 'my-component' },
|
||||
spec: { type: 'service', owner: 'user:default/guest' },
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
],
|
||||
});
|
||||
|
||||
await renderInTestApp(
|
||||
<TestApiProvider apis={[[catalogApiRef, catalogApiMock]]}>
|
||||
<EntityDetails entityRef={entityRef} />
|
||||
</TestApiProvider>,
|
||||
);
|
||||
|
||||
await expect(
|
||||
screen.findByText('The entity "test" is owned by "tools"'),
|
||||
screen.findByText('my-component'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
This pattern also works for many other context providers. An important example is the `EntityProvider` from the `@backstage/plugin-catalog-react` package, which you can use to provide a mocked entity context to the component.
|
||||
This approach provides the API overrides at the app level, which is useful when testing components that depend on APIs deep in the component tree.
|
||||
|
||||
The `TestApiProvider` component is also available for standalone rendering scenarios where you're not using `renderInTestApp` or other test utilities. Context providers like `EntityProvider` from `@backstage/plugin-catalog-react` can also be used to provide a mocked entity context to the component.
|
||||
|
||||
## Testing extensions
|
||||
|
||||
@@ -102,7 +102,35 @@ describe('Index page', () => {
|
||||
});
|
||||
```
|
||||
|
||||
This pattern also allows you to wrap the extension with context providers, such as the `TestApiProvider` that was introduced [above](#testing-react-components).
|
||||
You can also provide API overrides directly to `createExtensionTester` using the `apis` option:
|
||||
|
||||
```tsx
|
||||
import { screen } from '@testing-library/react';
|
||||
import {
|
||||
createExtensionTester,
|
||||
mockApis,
|
||||
renderInTestApp,
|
||||
} from '@backstage/frontend-test-utils';
|
||||
import { identityApiRef } from '@backstage/frontend-plugin-api';
|
||||
import { indexPageExtension } from './plugin';
|
||||
|
||||
describe('Index page', () => {
|
||||
it('should render with a custom identity', async () => {
|
||||
await renderInTestApp(
|
||||
createExtensionTester(indexPageExtension, {
|
||||
apis: [
|
||||
[
|
||||
identityApiRef,
|
||||
mockApis.identity({ userEntityRef: 'user:default/guest' }),
|
||||
],
|
||||
],
|
||||
}).reactElement(),
|
||||
);
|
||||
|
||||
expect(screen.getByText('Index Page')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
Note that the `.reactElement()` method will look for the `coreExtensionData.reactElement` data in the extension outputs. If that doesn't exist and the extension outputs something else that you want to test, you can access the output data using the `.get(dataRef)` method instead.
|
||||
|
||||
|
||||
@@ -284,6 +284,14 @@ export type CreateSpecializedAppOptions = {
|
||||
};
|
||||
};
|
||||
|
||||
// Internal options type, not exported in the public API
|
||||
export interface CreateSpecializedAppInternalOptions
|
||||
extends CreateSpecializedAppOptions {
|
||||
__internal?: {
|
||||
apiFactoryOverrides?: AnyApiFactory[];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an empty app without any default features. This is a low-level API is
|
||||
* intended for use in tests or specialized setups. Typically you want to use
|
||||
@@ -296,6 +304,7 @@ export function createSpecializedApp(options?: CreateSpecializedAppOptions): {
|
||||
tree: AppTree;
|
||||
errors?: AppError[];
|
||||
} {
|
||||
const internalOptions = options as CreateSpecializedAppInternalOptions;
|
||||
const config = options?.config ?? new ConfigReader({}, 'empty-config');
|
||||
const features = deduplicateFeatures(options?.features ?? []).map(
|
||||
createPluginInfoAttacher(config, options?.advanced?.pluginInfoResolver),
|
||||
@@ -337,6 +346,7 @@ export function createSpecializedApp(options?: CreateSpecializedAppOptions): {
|
||||
createApiFactory(configApiRef, config),
|
||||
createApiFactory(routeResolutionApiRef, routeResolutionApi),
|
||||
createApiFactory(identityApiRef, appIdentityProxy),
|
||||
...(internalOptions?.__internal?.apiFactoryOverrides ?? []),
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -15,11 +15,7 @@
|
||||
*/
|
||||
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import {
|
||||
MockErrorApi,
|
||||
TestApiProvider,
|
||||
withLogCollector,
|
||||
} from '@backstage/test-utils';
|
||||
import { MockErrorApi, withLogCollector } from '@backstage/test-utils';
|
||||
import { errorApiRef } from '../apis';
|
||||
import {
|
||||
createExtensionTester,
|
||||
@@ -74,11 +70,9 @@ describe('AppRootElementBlueprint', () => {
|
||||
});
|
||||
|
||||
const tester = createExtensionTester(extension);
|
||||
renderInTestApp(
|
||||
<TestApiProvider apis={[[errorApiRef, errorApi]]}>
|
||||
{tester.reactElement()}
|
||||
</TestApiProvider>,
|
||||
);
|
||||
renderInTestApp(tester.reactElement(), {
|
||||
apis: [[errorApiRef, errorApi]],
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const errors = errorApi.getErrors();
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import { useEffect, ReactNode } from 'react';
|
||||
import { act, screen, waitFor } from '@testing-library/react';
|
||||
import { TestApiProvider, withLogCollector } from '@backstage/test-utils';
|
||||
import { withLogCollector } from '@backstage/test-utils';
|
||||
import { ExtensionBoundary } from './ExtensionBoundary';
|
||||
import { coreExtensionData, createExtension } from '../wiring';
|
||||
import { analyticsApiRef } from '../apis/definitions/AnalyticsApi';
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
pluginWrapperApiRef,
|
||||
PluginWrapperApi,
|
||||
} from '../apis/definitions/PluginWrapperApi';
|
||||
import { useAppNode } from '@backstage/frontend-plugin-api';
|
||||
|
||||
const wrapInBoundaryExtension = (element?: JSX.Element) => {
|
||||
const routeRef = createRouteRef();
|
||||
@@ -105,11 +106,12 @@ describe('ExtensionBoundary', () => {
|
||||
};
|
||||
|
||||
renderInTestApp(
|
||||
<TestApiProvider apis={[[analyticsApiRef, analyticsApiMock]]}>
|
||||
{createExtensionTester(
|
||||
wrapInBoundaryExtension(<AnalyticsComponent />),
|
||||
).reactElement()}
|
||||
</TestApiProvider>,
|
||||
createExtensionTester(
|
||||
wrapInBoundaryExtension(<AnalyticsComponent />),
|
||||
).reactElement(),
|
||||
{
|
||||
apis: [[analyticsApiRef, analyticsApiMock]],
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -132,9 +134,10 @@ describe('ExtensionBoundary', () => {
|
||||
};
|
||||
|
||||
const WrapperComponent = ({ children }: { children: ReactNode }) => {
|
||||
const node = useAppNode();
|
||||
return (
|
||||
<div data-testid="plugin-wrapper">
|
||||
<span>Wrapper</span>
|
||||
<span>Wrapper for {node?.spec.id}</span>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
@@ -150,15 +153,18 @@ describe('ExtensionBoundary', () => {
|
||||
};
|
||||
|
||||
renderInTestApp(
|
||||
<TestApiProvider apis={[[pluginWrapperApiRef, pluginWrapperApi]]}>
|
||||
{createExtensionTester(
|
||||
wrapInBoundaryExtension(<TextComponent />),
|
||||
).reactElement()}
|
||||
</TestApiProvider>,
|
||||
createExtensionTester(
|
||||
wrapInBoundaryExtension(<TextComponent />),
|
||||
).reactElement(),
|
||||
{
|
||||
apis: [[pluginWrapperApiRef, pluginWrapperApi]],
|
||||
},
|
||||
);
|
||||
|
||||
expect(await screen.findByTestId('plugin-wrapper')).toBeInTheDocument();
|
||||
expect(screen.getByText('Wrapper')).toBeInTheDocument();
|
||||
const wrappers = await screen.findAllByTestId('plugin-wrapper');
|
||||
expect(wrappers.length).toBeGreaterThan(1);
|
||||
expect(screen.getByText('Wrapper for app')).toBeInTheDocument();
|
||||
expect(screen.getByText('Wrapper for test')).toBeInTheDocument();
|
||||
expect(screen.getByText(text)).toBeInTheDocument();
|
||||
expect(pluginWrapperApi.getPluginWrapper).toHaveBeenCalledWith('app');
|
||||
});
|
||||
@@ -184,11 +190,12 @@ describe('ExtensionBoundary', () => {
|
||||
|
||||
const { error } = await withLogCollector(['error'], async () => {
|
||||
renderInTestApp(
|
||||
<TestApiProvider apis={[[pluginWrapperApiRef, pluginWrapperApi]]}>
|
||||
{createExtensionTester(
|
||||
wrapInBoundaryExtension(<TextComponent />),
|
||||
).reactElement()}
|
||||
</TestApiProvider>,
|
||||
createExtensionTester(
|
||||
wrapInBoundaryExtension(<TextComponent />),
|
||||
).reactElement(),
|
||||
{
|
||||
apis: [[pluginWrapperApiRef, pluginWrapperApi]],
|
||||
},
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText(errorMsg)).toBeInTheDocument(),
|
||||
@@ -204,9 +211,7 @@ describe('ExtensionBoundary', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// TODO(Rugvip): Need a way to be able to override APIs in the app to be able to test this properly
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
it.skip('should emit analytics events if routable', async () => {
|
||||
it('should emit analytics events if routable', async () => {
|
||||
const Emitter = () => {
|
||||
const analytics = useAnalytics();
|
||||
useEffect(() => {
|
||||
@@ -221,22 +226,25 @@ describe('ExtensionBoundary', () => {
|
||||
createExtensionTester(
|
||||
wrapInBoundaryExtension(<Emitter />),
|
||||
).reactElement(),
|
||||
// { apis: [[analyticsApiRef, analyticsApiMock]] },
|
||||
{ apis: [[analyticsApiRef, analyticsApiMock]] },
|
||||
);
|
||||
});
|
||||
|
||||
// The navigate event is emitted by the app's routing, with app context
|
||||
expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: 'navigate',
|
||||
subject: '/',
|
||||
}),
|
||||
);
|
||||
// The dummy event from our test extension has the correct extension context
|
||||
expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: 'dummy',
|
||||
context: expect.objectContaining({
|
||||
pluginId: 'root',
|
||||
extensionId: 'test',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ action: 'dummy' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
```ts
|
||||
import { AnalyticsApi } from '@backstage/frontend-plugin-api';
|
||||
import { AnalyticsEvent } from '@backstage/frontend-plugin-api';
|
||||
import { ApiHolder } from '@backstage/frontend-plugin-api';
|
||||
import { ApiMock } from '@backstage/test-utils';
|
||||
import { ApiRef } 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';
|
||||
@@ -14,6 +16,7 @@ import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionDefinitionParameters } from '@backstage/frontend-plugin-api';
|
||||
import { FrontendFeature } from '@backstage/frontend-plugin-api';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { JSX as JSX_2 } from 'react/jsx-runtime';
|
||||
import { mockApis } from '@backstage/test-utils';
|
||||
import { MockConfigApi } from '@backstage/test-utils';
|
||||
import { MockErrorApi } from '@backstage/test-utils';
|
||||
@@ -23,22 +26,24 @@ import { MockFetchApiOptions } from '@backstage/test-utils';
|
||||
import { MockPermissionApi } from '@backstage/test-utils';
|
||||
import { MockStorageApi } from '@backstage/test-utils';
|
||||
import { MockStorageBucket } from '@backstage/test-utils';
|
||||
import { ReactNode } from 'react';
|
||||
import { registerMswTestHooks } from '@backstage/test-utils';
|
||||
import { RenderResult } from '@testing-library/react';
|
||||
import { RouteRef } from '@backstage/frontend-plugin-api';
|
||||
import { TestApiProvider } from '@backstage/test-utils';
|
||||
import { TestApiProviderProps } from '@backstage/test-utils';
|
||||
import { TestApiRegistry } from '@backstage/test-utils';
|
||||
import { testingLibraryDomTypesQueries } from '@testing-library/dom/types/queries';
|
||||
import { withLogCollector } from '@backstage/test-utils';
|
||||
|
||||
export { ApiMock };
|
||||
|
||||
// @public (undocumented)
|
||||
export function createExtensionTester<T extends ExtensionDefinitionParameters>(
|
||||
export function createExtensionTester<
|
||||
T extends ExtensionDefinitionParameters,
|
||||
TApiPairs extends any[] = any[],
|
||||
>(
|
||||
subject: ExtensionDefinition<T>,
|
||||
options?: {
|
||||
config?: T['configInput'];
|
||||
apis?: readonly [...TestApiPairs<TApiPairs>];
|
||||
},
|
||||
): ExtensionTester<NonNullable<T['output']>>;
|
||||
|
||||
@@ -115,38 +120,66 @@ export { MockStorageBucket };
|
||||
export { registerMswTestHooks };
|
||||
|
||||
// @public
|
||||
export function renderInTestApp(
|
||||
export function renderInTestApp<TApiPairs extends any[] = any[]>(
|
||||
element: JSX.Element,
|
||||
options?: TestAppOptions,
|
||||
options?: TestAppOptions<TApiPairs>,
|
||||
): RenderResult;
|
||||
|
||||
// @public
|
||||
export function renderTestApp(
|
||||
options: RenderTestAppOptions,
|
||||
export function renderTestApp<TApiPairs extends any[] = any[]>(
|
||||
options: RenderTestAppOptions<TApiPairs>,
|
||||
): RenderResult<testingLibraryDomTypesQueries, HTMLElement, HTMLElement>;
|
||||
|
||||
// @public
|
||||
export type RenderTestAppOptions = {
|
||||
export type RenderTestAppOptions<TApiPairs extends any[] = any[]> = {
|
||||
config?: JsonObject;
|
||||
extensions?: ExtensionDefinition<any>[];
|
||||
features?: FrontendFeature[];
|
||||
initialRouteEntries?: string[];
|
||||
apis?: readonly [...TestApiPairs<TApiPairs>];
|
||||
};
|
||||
|
||||
export { TestApiProvider };
|
||||
|
||||
export { TestApiProviderProps };
|
||||
|
||||
export { TestApiRegistry };
|
||||
// @public
|
||||
export type TestApiPairs<TApiPairs> = TestApiProviderPropsApiPairs<TApiPairs>;
|
||||
|
||||
// @public
|
||||
export type TestAppOptions = {
|
||||
export const TestApiProvider: <T extends any[]>(
|
||||
props: TestApiProviderProps<T>,
|
||||
) => JSX_2.Element;
|
||||
|
||||
// @public
|
||||
export type TestApiProviderProps<TApiPairs extends any[]> = {
|
||||
apis: readonly [...TestApiProviderPropsApiPairs<TApiPairs>];
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type TestApiProviderPropsApiPair<TApi> = TApi extends infer TImpl
|
||||
? readonly [ApiRef<TApi>, Partial<TImpl>]
|
||||
: never;
|
||||
|
||||
// @public
|
||||
export type TestApiProviderPropsApiPairs<TApiPairs> = {
|
||||
[TIndex in keyof TApiPairs]: TestApiProviderPropsApiPair<TApiPairs[TIndex]>;
|
||||
};
|
||||
|
||||
// @public
|
||||
export class TestApiRegistry implements ApiHolder {
|
||||
static from<TApiPairs extends any[]>(
|
||||
...apis: readonly [...TestApiProviderPropsApiPairs<TApiPairs>]
|
||||
): TestApiRegistry;
|
||||
get<T>(api: ApiRef<T>): T | undefined;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type TestAppOptions<TApiPairs extends any[] = any[]> = {
|
||||
mountedRoutes?: {
|
||||
[path: string]: RouteRef;
|
||||
};
|
||||
config?: JsonObject;
|
||||
features?: FrontendFeature[];
|
||||
initialRouteEntries?: string[];
|
||||
apis?: readonly [...TestApiPairs<TApiPairs>];
|
||||
};
|
||||
|
||||
export { withLogCollector };
|
||||
|
||||
@@ -15,12 +15,16 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
analyticsApiRef,
|
||||
coreExtensionData,
|
||||
createExtension,
|
||||
createExtensionDataRef,
|
||||
createExtensionInput,
|
||||
useAnalytics,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { createExtensionTester } from './createExtensionTester';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { renderInTestApp } from './renderInTestApp';
|
||||
|
||||
const stringDataRef = createExtensionDataRef<string>().with({
|
||||
id: 'test.string',
|
||||
@@ -152,4 +156,36 @@ describe('createExtensionTester', () => {
|
||||
|
||||
expect([test, test2, test3]).toBeDefined();
|
||||
});
|
||||
|
||||
it('should support API overrides via options', async () => {
|
||||
const analyticsApiMock = { captureEvent: jest.fn() };
|
||||
|
||||
const TestComponent = () => {
|
||||
const analytics = useAnalytics();
|
||||
analytics.captureEvent('test', 'value');
|
||||
return <div>Test</div>;
|
||||
};
|
||||
|
||||
const extension = createExtension({
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: [coreExtensionData.reactElement],
|
||||
factory: () => [coreExtensionData.reactElement(<TestComponent />)],
|
||||
});
|
||||
|
||||
const tester = createExtensionTester(extension, {
|
||||
apis: [[analyticsApiRef, analyticsApiMock]],
|
||||
});
|
||||
|
||||
renderInTestApp(tester.reactElement(), {
|
||||
apis: [[analyticsApiRef, analyticsApiMock]],
|
||||
});
|
||||
|
||||
expect(screen.getByText('Test')).toBeInTheDocument();
|
||||
expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: 'test',
|
||||
subject: 'value',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,8 +37,8 @@ import { instantiateAppNodeTree } from '../../../frontend-app-api/src/tree/insta
|
||||
import { readAppExtensionsConfig } from '../../../frontend-app-api/src/tree/readAppExtensionsConfig';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { createErrorCollector } from '../../../frontend-app-api/src/wiring/createErrorCollector';
|
||||
import { TestApiRegistry } from '@backstage/test-utils';
|
||||
import { OpaqueExtensionDefinition } from '@internal/frontend';
|
||||
import { TestApiRegistry, type TestApiPairs } from '../utils';
|
||||
|
||||
/** @public */
|
||||
export class ExtensionQuery<UOutput extends ExtensionDataRef> {
|
||||
@@ -78,16 +78,23 @@ export class ExtensionQuery<UOutput extends ExtensionDataRef> {
|
||||
/** @public */
|
||||
export class ExtensionTester<UOutput extends ExtensionDataRef> {
|
||||
/** @internal */
|
||||
static forSubject<T extends ExtensionDefinitionParameters>(
|
||||
static forSubject<
|
||||
T extends ExtensionDefinitionParameters,
|
||||
TApiPairs extends any[],
|
||||
>(
|
||||
subject: ExtensionDefinition<T>,
|
||||
options?: { config?: T['configInput'] },
|
||||
options?: {
|
||||
config?: T['configInput'];
|
||||
apis?: readonly [...TestApiPairs<TApiPairs>];
|
||||
},
|
||||
): ExtensionTester<NonNullable<T['output']>> {
|
||||
const tester = new ExtensionTester();
|
||||
const tester = new ExtensionTester(options?.apis);
|
||||
tester.add(subject, options as T['configInput'] & {});
|
||||
return tester;
|
||||
}
|
||||
|
||||
#tree?: AppTree;
|
||||
#apis?: readonly any[];
|
||||
|
||||
readonly #extensions = new Array<{
|
||||
id: string;
|
||||
@@ -96,6 +103,10 @@ export class ExtensionTester<UOutput extends ExtensionDataRef> {
|
||||
config?: JsonValue;
|
||||
}>();
|
||||
|
||||
private constructor(apis?: readonly any[]) {
|
||||
this.#apis = apis;
|
||||
}
|
||||
|
||||
add<T extends ExtensionDefinitionParameters>(
|
||||
extension: ExtensionDefinition<T>,
|
||||
options?: { config?: T['configInput'] },
|
||||
@@ -206,7 +217,11 @@ export class ExtensionTester<UOutput extends ExtensionDataRef> {
|
||||
collector,
|
||||
);
|
||||
|
||||
instantiateAppNodeTree(tree.root, TestApiRegistry.from(), collector);
|
||||
const apiHolder = this.#apis
|
||||
? TestApiRegistry.from(...this.#apis)
|
||||
: TestApiRegistry.from();
|
||||
|
||||
instantiateAppNodeTree(tree.root, apiHolder, collector);
|
||||
|
||||
const errors = collector.collectErrors();
|
||||
if (errors) {
|
||||
@@ -260,9 +275,15 @@ export class ExtensionTester<UOutput extends ExtensionDataRef> {
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function createExtensionTester<T extends ExtensionDefinitionParameters>(
|
||||
export function createExtensionTester<
|
||||
T extends ExtensionDefinitionParameters,
|
||||
TApiPairs extends any[] = any[],
|
||||
>(
|
||||
subject: ExtensionDefinition<T>,
|
||||
options?: { config?: T['configInput'] },
|
||||
options?: {
|
||||
config?: T['configInput'];
|
||||
apis?: readonly [...TestApiPairs<TApiPairs>];
|
||||
},
|
||||
): ExtensionTester<NonNullable<T['output']>> {
|
||||
return ExtensionTester.forSubject(subject, options);
|
||||
}
|
||||
|
||||
@@ -80,4 +80,35 @@ describe('renderInTestApp', () => {
|
||||
|
||||
expect(screen.getByText('Second Page')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should support API overrides via options', async () => {
|
||||
const IndexPage = () => {
|
||||
const analyticsApi = useAnalytics();
|
||||
const handleClick = useCallback(() => {
|
||||
analyticsApi.captureEvent('click', 'Test action');
|
||||
}, [analyticsApi]);
|
||||
return (
|
||||
<div>
|
||||
<button onClick={handleClick}>Click me</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const analyticsApiMock = new MockAnalyticsApi();
|
||||
|
||||
renderInTestApp(<IndexPage />, {
|
||||
apis: [[analyticsApiRef, analyticsApiMock]],
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Click me' }));
|
||||
|
||||
expect(analyticsApiMock.getEvents()).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
action: 'click',
|
||||
subject: 'Test action',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,9 +31,13 @@ import {
|
||||
createFrontendPlugin,
|
||||
FrontendFeature,
|
||||
createFrontendModule,
|
||||
createApiFactory,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { RouterBlueprint } from '@backstage/plugin-app-react';
|
||||
import appPlugin from '@backstage/plugin-app';
|
||||
import { type TestApiPairs } from '../utils';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import type { CreateSpecializedAppInternalOptions } from '../../../frontend-app-api/src/wiring/createSpecializedApp';
|
||||
|
||||
const DEFAULT_MOCK_CONFIG = {
|
||||
app: { baseUrl: 'http://localhost:3000' },
|
||||
@@ -44,7 +48,7 @@ const DEFAULT_MOCK_CONFIG = {
|
||||
* Options to customize the behavior of the test app.
|
||||
* @public
|
||||
*/
|
||||
export type TestAppOptions = {
|
||||
export type TestAppOptions<TApiPairs extends any[] = any[]> = {
|
||||
/**
|
||||
* An object of paths to mount route ref on, with the key being the path and the value
|
||||
* being the RouteRef that the path will be bound to. This allows the route refs to be
|
||||
@@ -77,6 +81,22 @@ export type TestAppOptions = {
|
||||
* Initial route entries to use for the router.
|
||||
*/
|
||||
initialRouteEntries?: string[];
|
||||
|
||||
/**
|
||||
* API overrides to provide to the test app. Use `mockApis` helpers
|
||||
* from `@backstage/frontend-test-utils` to create mock implementations.
|
||||
*
|
||||
* @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?: readonly [...TestApiPairs<TApiPairs>];
|
||||
};
|
||||
|
||||
const NavItem = (props: {
|
||||
@@ -143,9 +163,9 @@ const appPluginOverride = appPlugin.withOverrides({
|
||||
* @public
|
||||
* Renders the given element in a test app, for use in unit tests.
|
||||
*/
|
||||
export function renderInTestApp(
|
||||
export function renderInTestApp<TApiPairs extends any[] = any[]>(
|
||||
element: JSX.Element,
|
||||
options?: TestAppOptions,
|
||||
options?: TestAppOptions<TApiPairs>,
|
||||
): RenderResult {
|
||||
const extensions: Array<ExtensionDefinition> = [
|
||||
createExtension({
|
||||
@@ -214,7 +234,12 @@ export function renderInTestApp(
|
||||
data: options?.config ?? DEFAULT_MOCK_CONFIG,
|
||||
},
|
||||
]),
|
||||
});
|
||||
__internal: options?.apis && {
|
||||
apiFactoryOverrides: options.apis.map(([apiRef, implementation]) =>
|
||||
createApiFactory(apiRef, implementation),
|
||||
),
|
||||
},
|
||||
} as CreateSpecializedAppInternalOptions);
|
||||
|
||||
return render(
|
||||
app.tree.root.instance!.getData(coreExtensionData.reactElement),
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import { createSpecializedApp } from '@backstage/frontend-app-api';
|
||||
import {
|
||||
coreExtensionData,
|
||||
createApiFactory,
|
||||
createFrontendModule,
|
||||
createFrontendPlugin,
|
||||
ExtensionDefinition,
|
||||
@@ -28,6 +29,9 @@ 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';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import type { CreateSpecializedAppInternalOptions } from '../../../frontend-app-api/src/wiring/createSpecializedApp';
|
||||
|
||||
const DEFAULT_MOCK_CONFIG = {
|
||||
app: { baseUrl: 'http://localhost:3000' },
|
||||
@@ -39,7 +43,7 @@ const DEFAULT_MOCK_CONFIG = {
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type RenderTestAppOptions = {
|
||||
export type RenderTestAppOptions<TApiPairs extends any[] = any[]> = {
|
||||
/**
|
||||
* Additional configuration passed to the app when rendering elements inside it.
|
||||
*/
|
||||
@@ -58,6 +62,23 @@ export type RenderTestAppOptions = {
|
||||
* Initial route entries to use for the router.
|
||||
*/
|
||||
initialRouteEntries?: string[];
|
||||
|
||||
/**
|
||||
* API overrides to provide to the test app. Use `mockApis` helpers
|
||||
* from `@backstage/frontend-test-utils` to create mock implementations.
|
||||
*
|
||||
* @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' })]],
|
||||
* extensions: [...],
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
apis?: readonly [...TestApiPairs<TApiPairs>];
|
||||
};
|
||||
|
||||
const appPluginOverride = appPlugin.withOverrides({
|
||||
@@ -74,7 +95,9 @@ const appPluginOverride = appPlugin.withOverrides({
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function renderTestApp(options: RenderTestAppOptions) {
|
||||
export function renderTestApp<TApiPairs extends any[] = any[]>(
|
||||
options: RenderTestAppOptions<TApiPairs>,
|
||||
) {
|
||||
const extensions = [...(options.extensions ?? [])];
|
||||
|
||||
const features: FrontendFeature[] = [
|
||||
@@ -111,7 +134,12 @@ export function renderTestApp(options: RenderTestAppOptions) {
|
||||
data: options?.config ?? DEFAULT_MOCK_CONFIG,
|
||||
},
|
||||
]),
|
||||
});
|
||||
__internal: options?.apis && {
|
||||
apiFactoryOverrides: options.apis.map(([apiRef, implementation]) =>
|
||||
createApiFactory(apiRef, implementation),
|
||||
),
|
||||
},
|
||||
} as CreateSpecializedAppInternalOptions);
|
||||
|
||||
return render(
|
||||
app.tree.root.instance!.getData(coreExtensionData.reactElement),
|
||||
|
||||
@@ -22,9 +22,10 @@
|
||||
|
||||
export * from './apis';
|
||||
export * from './app';
|
||||
export * from './utils';
|
||||
|
||||
export { TestApiProvider, TestApiRegistry } from '@backstage/test-utils';
|
||||
export type { TestApiProviderProps } from '@backstage/test-utils';
|
||||
// Explicit export to satisfy API Extractor
|
||||
export type { TestApiPairs } from './utils';
|
||||
|
||||
export { withLogCollector } from '@backstage/test-utils';
|
||||
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Copyright 2020 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 { 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';
|
||||
|
||||
/**
|
||||
* Helper type for representing an API reference paired with a partial implementation.
|
||||
* @public
|
||||
*/
|
||||
export type TestApiProviderPropsApiPair<TApi> = TApi extends infer TImpl
|
||||
? readonly [ApiRef<TApi>, Partial<TImpl>]
|
||||
: never;
|
||||
|
||||
/**
|
||||
* Helper type for representing an array of API reference pairs.
|
||||
* @public
|
||||
*/
|
||||
export type TestApiProviderPropsApiPairs<TApiPairs> = {
|
||||
[TIndex in keyof TApiPairs]: TestApiProviderPropsApiPair<TApiPairs[TIndex]>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Shorter alias for TestApiProviderPropsApiPairs for use in function signatures.
|
||||
* @public
|
||||
*/
|
||||
export type TestApiPairs<TApiPairs> = TestApiProviderPropsApiPairs<TApiPairs>;
|
||||
|
||||
/**
|
||||
* Properties for the {@link TestApiProvider} component.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type TestApiProviderProps<TApiPairs extends any[]> = {
|
||||
apis: readonly [...TestApiProviderPropsApiPairs<TApiPairs>];
|
||||
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';
|
||||
*
|
||||
* const apis = TestApiRegistry.from(
|
||||
* [identityApiRef, mockApis.identity({ userEntityRef: 'user:default/guest' })],
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
* @param apis - A list of pairs mapping an ApiRef to its respective implementation.
|
||||
*/
|
||||
static from<TApiPairs extends any[]>(
|
||||
...apis: readonly [...TestApiProviderPropsApiPairs<TApiPairs>]
|
||||
) {
|
||||
return new TestApiRegistry(
|
||||
new Map(apis.map(([api, impl]) => [api.id, impl])),
|
||||
);
|
||||
}
|
||||
|
||||
private constructor(private readonly apis: Map<string, unknown>) {}
|
||||
|
||||
/**
|
||||
* Returns an implementation of the API.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
get<T>(api: ApiRef<T>): 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.
|
||||
*
|
||||
* It lets you provide any number of API implementations, without necessarily
|
||||
* having to fully implement each of the APIs.
|
||||
*
|
||||
* @remarks
|
||||
*
|
||||
* For most test scenarios, prefer using the `apis` option in `renderInTestApp` or
|
||||
* `createExtensionTester` instead of wrapping components with `TestApiProvider`.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* import { render } from '\@testing-library/react';
|
||||
* import { identityApiRef } from '\@backstage/frontend-plugin-api';
|
||||
* import { TestApiProvider, mockApis } from '\@backstage/frontend-test-utils';
|
||||
*
|
||||
* render(
|
||||
* <TestApiProvider
|
||||
* apis={[[identityApiRef, mockApis.identity({ userEntityRef: 'user:default/guest' })]]}
|
||||
* >
|
||||
* <MyComponent />
|
||||
* </TestApiProvider>
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const TestApiProvider = <T extends any[]>(
|
||||
props: TestApiProviderProps<T>,
|
||||
) => {
|
||||
return (
|
||||
<ApiProvider
|
||||
apis={TestApiRegistry.from(...props.apis)}
|
||||
children={props.children}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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,
|
||||
} from './TestApiProvider';
|
||||
export type { TestApiProviderProps } from './TestApiProvider';
|
||||
@@ -19,7 +19,6 @@ import userEvent from '@testing-library/user-event';
|
||||
import {
|
||||
createExtensionTester,
|
||||
renderInTestApp,
|
||||
TestApiProvider,
|
||||
} from '@backstage/frontend-test-utils';
|
||||
import { catalogEntityPage } from './pages';
|
||||
import {
|
||||
@@ -150,29 +149,23 @@ describe('Entity page', () => {
|
||||
.add(techdocsEntityContent)
|
||||
.add(apidocsEntityContent);
|
||||
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[catalogApiRef, mockCatalogApi],
|
||||
[starredEntitiesApiRef, mockStarredEntitiesApi],
|
||||
]}
|
||||
>
|
||||
{tester.reactElement()}
|
||||
</TestApiProvider>,
|
||||
{
|
||||
config: {
|
||||
app: {
|
||||
title: 'Custom app',
|
||||
},
|
||||
backend: { baseUrl: 'http://localhost:7000' },
|
||||
},
|
||||
mountedRoutes: {
|
||||
'/catalog': convertLegacyRouteRef(rootRouteRef),
|
||||
'/catalog/:namespace/:kind/:name':
|
||||
convertLegacyRouteRef(entityRouteRef),
|
||||
await renderInTestApp(tester.reactElement(), {
|
||||
apis: [
|
||||
[catalogApiRef, mockCatalogApi],
|
||||
[starredEntitiesApiRef, mockStarredEntitiesApi],
|
||||
],
|
||||
config: {
|
||||
app: {
|
||||
title: 'Custom app',
|
||||
},
|
||||
backend: { baseUrl: 'http://localhost:7000' },
|
||||
},
|
||||
);
|
||||
mountedRoutes: {
|
||||
'/catalog': convertLegacyRouteRef(rootRouteRef),
|
||||
'/catalog/:namespace/:kind/:name':
|
||||
convertLegacyRouteRef(entityRouteRef),
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
@@ -212,29 +205,23 @@ describe('Entity page', () => {
|
||||
.add(techdocsEntityContent)
|
||||
.add(apidocsEntityContent);
|
||||
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[catalogApiRef, mockCatalogApi],
|
||||
[starredEntitiesApiRef, mockStarredEntitiesApi],
|
||||
]}
|
||||
>
|
||||
{tester.reactElement()}
|
||||
</TestApiProvider>,
|
||||
{
|
||||
config: {
|
||||
app: {
|
||||
title: 'Custom app',
|
||||
},
|
||||
backend: { baseUrl: 'http://localhost:7000' },
|
||||
},
|
||||
mountedRoutes: {
|
||||
'/catalog': convertLegacyRouteRef(rootRouteRef),
|
||||
'/catalog/:namespace/:kind/:name':
|
||||
convertLegacyRouteRef(entityRouteRef),
|
||||
await renderInTestApp(tester.reactElement(), {
|
||||
apis: [
|
||||
[catalogApiRef, mockCatalogApi],
|
||||
[starredEntitiesApiRef, mockStarredEntitiesApi],
|
||||
],
|
||||
config: {
|
||||
app: {
|
||||
title: 'Custom app',
|
||||
},
|
||||
backend: { baseUrl: 'http://localhost:7000' },
|
||||
},
|
||||
);
|
||||
mountedRoutes: {
|
||||
'/catalog': convertLegacyRouteRef(rootRouteRef),
|
||||
'/catalog/:namespace/:kind/:name':
|
||||
convertLegacyRouteRef(entityRouteRef),
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByRole('tab', { name: /Docs/ })).toBeInTheDocument(),
|
||||
@@ -267,29 +254,23 @@ describe('Entity page', () => {
|
||||
},
|
||||
});
|
||||
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[catalogApiRef, mockCatalogApi],
|
||||
[starredEntitiesApiRef, mockStarredEntitiesApi],
|
||||
]}
|
||||
>
|
||||
{tester.reactElement()}
|
||||
</TestApiProvider>,
|
||||
{
|
||||
config: {
|
||||
app: {
|
||||
title: 'Custom app',
|
||||
},
|
||||
backend: { baseUrl: 'http://localhost:7000' },
|
||||
},
|
||||
mountedRoutes: {
|
||||
'/catalog': convertLegacyRouteRef(rootRouteRef),
|
||||
'/catalog/:namespace/:kind/:name':
|
||||
convertLegacyRouteRef(entityRouteRef),
|
||||
await renderInTestApp(tester.reactElement(), {
|
||||
apis: [
|
||||
[catalogApiRef, mockCatalogApi],
|
||||
[starredEntitiesApiRef, mockStarredEntitiesApi],
|
||||
],
|
||||
config: {
|
||||
app: {
|
||||
title: 'Custom app',
|
||||
},
|
||||
backend: { baseUrl: 'http://localhost:7000' },
|
||||
},
|
||||
);
|
||||
mountedRoutes: {
|
||||
'/catalog': convertLegacyRouteRef(rootRouteRef),
|
||||
'/catalog/:namespace/:kind/:name':
|
||||
convertLegacyRouteRef(entityRouteRef),
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
@@ -334,29 +315,23 @@ describe('Entity page', () => {
|
||||
},
|
||||
});
|
||||
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[catalogApiRef, mockCatalogApi],
|
||||
[starredEntitiesApiRef, mockStarredEntitiesApi],
|
||||
]}
|
||||
>
|
||||
{tester.reactElement()}
|
||||
</TestApiProvider>,
|
||||
{
|
||||
config: {
|
||||
app: {
|
||||
title: 'Custom app',
|
||||
},
|
||||
backend: { baseUrl: 'http://localhost:7000' },
|
||||
},
|
||||
mountedRoutes: {
|
||||
'/catalog': convertLegacyRouteRef(rootRouteRef),
|
||||
'/catalog/:namespace/:kind/:name':
|
||||
convertLegacyRouteRef(entityRouteRef),
|
||||
await renderInTestApp(tester.reactElement(), {
|
||||
apis: [
|
||||
[catalogApiRef, mockCatalogApi],
|
||||
[starredEntitiesApiRef, mockStarredEntitiesApi],
|
||||
],
|
||||
config: {
|
||||
app: {
|
||||
title: 'Custom app',
|
||||
},
|
||||
backend: { baseUrl: 'http://localhost:7000' },
|
||||
},
|
||||
);
|
||||
mountedRoutes: {
|
||||
'/catalog': convertLegacyRouteRef(rootRouteRef),
|
||||
'/catalog/:namespace/:kind/:name':
|
||||
convertLegacyRouteRef(entityRouteRef),
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByRole('tab', { name: /Docs/ })).toBeInTheDocument(),
|
||||
@@ -390,29 +365,23 @@ describe('Entity page', () => {
|
||||
},
|
||||
});
|
||||
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[catalogApiRef, mockCatalogApi],
|
||||
[starredEntitiesApiRef, mockStarredEntitiesApi],
|
||||
]}
|
||||
>
|
||||
{tester.reactElement()}
|
||||
</TestApiProvider>,
|
||||
{
|
||||
config: {
|
||||
app: {
|
||||
title: 'Custom app',
|
||||
},
|
||||
backend: { baseUrl: 'http://localhost:7000' },
|
||||
},
|
||||
mountedRoutes: {
|
||||
'/catalog': convertLegacyRouteRef(rootRouteRef),
|
||||
'/catalog/:namespace/:kind/:name':
|
||||
convertLegacyRouteRef(entityRouteRef),
|
||||
await renderInTestApp(tester.reactElement(), {
|
||||
apis: [
|
||||
[catalogApiRef, mockCatalogApi],
|
||||
[starredEntitiesApiRef, mockStarredEntitiesApi],
|
||||
],
|
||||
config: {
|
||||
app: {
|
||||
title: 'Custom app',
|
||||
},
|
||||
backend: { baseUrl: 'http://localhost:7000' },
|
||||
},
|
||||
);
|
||||
mountedRoutes: {
|
||||
'/catalog': convertLegacyRouteRef(rootRouteRef),
|
||||
'/catalog/:namespace/:kind/:name':
|
||||
convertLegacyRouteRef(entityRouteRef),
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
@@ -435,29 +404,23 @@ describe('Entity page', () => {
|
||||
.add(apidocsEntityContent)
|
||||
.add(overviewEntityContent);
|
||||
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[catalogApiRef, mockCatalogApi],
|
||||
[starredEntitiesApiRef, mockStarredEntitiesApi],
|
||||
]}
|
||||
>
|
||||
{tester.reactElement()}
|
||||
</TestApiProvider>,
|
||||
{
|
||||
config: {
|
||||
app: {
|
||||
title: 'Custom app',
|
||||
},
|
||||
backend: { baseUrl: 'http://localhost:7000' },
|
||||
},
|
||||
mountedRoutes: {
|
||||
'/catalog': convertLegacyRouteRef(rootRouteRef),
|
||||
'/catalog/:namespace/:kind/:name':
|
||||
convertLegacyRouteRef(entityRouteRef),
|
||||
await renderInTestApp(tester.reactElement(), {
|
||||
apis: [
|
||||
[catalogApiRef, mockCatalogApi],
|
||||
[starredEntitiesApiRef, mockStarredEntitiesApi],
|
||||
],
|
||||
config: {
|
||||
app: {
|
||||
title: 'Custom app',
|
||||
},
|
||||
backend: { baseUrl: 'http://localhost:7000' },
|
||||
},
|
||||
);
|
||||
mountedRoutes: {
|
||||
'/catalog': convertLegacyRouteRef(rootRouteRef),
|
||||
'/catalog/:namespace/:kind/:name':
|
||||
convertLegacyRouteRef(entityRouteRef),
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => expect(screen.getAllByRole('tab')).toHaveLength(2));
|
||||
|
||||
@@ -485,29 +448,23 @@ describe('Entity page', () => {
|
||||
},
|
||||
});
|
||||
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[catalogApiRef, mockCatalogApi],
|
||||
[starredEntitiesApiRef, mockStarredEntitiesApi],
|
||||
]}
|
||||
>
|
||||
{tester.reactElement()}
|
||||
</TestApiProvider>,
|
||||
{
|
||||
config: {
|
||||
app: {
|
||||
title: 'Custom app',
|
||||
},
|
||||
backend: { baseUrl: 'http://localhost:7000' },
|
||||
},
|
||||
mountedRoutes: {
|
||||
'/catalog': convertLegacyRouteRef(rootRouteRef),
|
||||
'/catalog/:namespace/:kind/:name':
|
||||
convertLegacyRouteRef(entityRouteRef),
|
||||
await renderInTestApp(tester.reactElement(), {
|
||||
apis: [
|
||||
[catalogApiRef, mockCatalogApi],
|
||||
[starredEntitiesApiRef, mockStarredEntitiesApi],
|
||||
],
|
||||
config: {
|
||||
app: {
|
||||
title: 'Custom app',
|
||||
},
|
||||
backend: { baseUrl: 'http://localhost:7000' },
|
||||
},
|
||||
);
|
||||
mountedRoutes: {
|
||||
'/catalog': convertLegacyRouteRef(rootRouteRef),
|
||||
'/catalog/:namespace/:kind/:name':
|
||||
convertLegacyRouteRef(entityRouteRef),
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => expect(screen.getAllByRole('tab')).toHaveLength(2));
|
||||
|
||||
@@ -522,29 +479,23 @@ describe('Entity page', () => {
|
||||
Object.assign({ namespace: 'catalog' }, catalogEntityPage),
|
||||
);
|
||||
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[catalogApiRef, mockCatalogApi],
|
||||
[starredEntitiesApiRef, mockStarredEntitiesApi],
|
||||
]}
|
||||
>
|
||||
{tester.reactElement()}
|
||||
</TestApiProvider>,
|
||||
{
|
||||
config: {
|
||||
app: {
|
||||
title: 'Custom app',
|
||||
},
|
||||
backend: { baseUrl: 'http://localhost:7000' },
|
||||
},
|
||||
mountedRoutes: {
|
||||
'/catalog': convertLegacyRouteRef(rootRouteRef),
|
||||
'/catalog/:namespace/:kind/:name':
|
||||
convertLegacyRouteRef(entityRouteRef),
|
||||
await renderInTestApp(tester.reactElement(), {
|
||||
apis: [
|
||||
[catalogApiRef, mockCatalogApi],
|
||||
[starredEntitiesApiRef, mockStarredEntitiesApi],
|
||||
],
|
||||
config: {
|
||||
app: {
|
||||
title: 'Custom app',
|
||||
},
|
||||
backend: { baseUrl: 'http://localhost:7000' },
|
||||
},
|
||||
);
|
||||
mountedRoutes: {
|
||||
'/catalog': convertLegacyRouteRef(rootRouteRef),
|
||||
'/catalog/:namespace/:kind/:name':
|
||||
convertLegacyRouteRef(entityRouteRef),
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText(/artist-lookup/)).toBeInTheDocument(),
|
||||
@@ -567,29 +518,23 @@ describe('Entity page', () => {
|
||||
Object.assign({ namespace: 'catalog' }, catalogEntityPage),
|
||||
).add(customEntityHeader);
|
||||
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[catalogApiRef, mockCatalogApi],
|
||||
[starredEntitiesApiRef, mockStarredEntitiesApi],
|
||||
]}
|
||||
>
|
||||
{tester.reactElement()}
|
||||
</TestApiProvider>,
|
||||
{
|
||||
config: {
|
||||
app: {
|
||||
title: 'Custom app',
|
||||
},
|
||||
backend: { baseUrl: 'http://localhost:7000' },
|
||||
},
|
||||
mountedRoutes: {
|
||||
'/catalog': convertLegacyRouteRef(rootRouteRef),
|
||||
'/catalog/:namespace/:kind/:name':
|
||||
convertLegacyRouteRef(entityRouteRef),
|
||||
await renderInTestApp(tester.reactElement(), {
|
||||
apis: [
|
||||
[catalogApiRef, mockCatalogApi],
|
||||
[starredEntitiesApiRef, mockStarredEntitiesApi],
|
||||
],
|
||||
config: {
|
||||
app: {
|
||||
title: 'Custom app',
|
||||
},
|
||||
backend: { baseUrl: 'http://localhost:7000' },
|
||||
},
|
||||
);
|
||||
mountedRoutes: {
|
||||
'/catalog': convertLegacyRouteRef(rootRouteRef),
|
||||
'/catalog/:namespace/:kind/:name':
|
||||
convertLegacyRouteRef(entityRouteRef),
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
@@ -634,29 +579,23 @@ describe('Entity page', () => {
|
||||
Object.assign({ namespace: 'catalog' }, catalogEntityPage),
|
||||
).add(menuItem);
|
||||
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[catalogApiRef, mockCatalogApi],
|
||||
[starredEntitiesApiRef, mockStarredEntitiesApi],
|
||||
]}
|
||||
>
|
||||
{tester.reactElement()}
|
||||
</TestApiProvider>,
|
||||
{
|
||||
config: {
|
||||
app: {
|
||||
title: 'Custom app',
|
||||
},
|
||||
backend: { baseUrl: 'http://localhost:7000' },
|
||||
},
|
||||
mountedRoutes: {
|
||||
'/catalog': convertLegacyRouteRef(rootRouteRef),
|
||||
'/catalog/:namespace/:kind/:name':
|
||||
convertLegacyRouteRef(entityRouteRef),
|
||||
await renderInTestApp(tester.reactElement(), {
|
||||
apis: [
|
||||
[catalogApiRef, mockCatalogApi],
|
||||
[starredEntitiesApiRef, mockStarredEntitiesApi],
|
||||
],
|
||||
config: {
|
||||
app: {
|
||||
title: 'Custom app',
|
||||
},
|
||||
backend: { baseUrl: 'http://localhost:7000' },
|
||||
},
|
||||
);
|
||||
mountedRoutes: {
|
||||
'/catalog': convertLegacyRouteRef(rootRouteRef),
|
||||
'/catalog/:namespace/:kind/:name':
|
||||
convertLegacyRouteRef(entityRouteRef),
|
||||
},
|
||||
});
|
||||
const { disabled } = params.useProps();
|
||||
|
||||
await userEvent.click(await screen.findByTestId('menu-button'));
|
||||
@@ -697,29 +636,23 @@ describe('Entity page', () => {
|
||||
Object.assign({ namespace: 'catalog' }, catalogEntityPage),
|
||||
).add(menuItem);
|
||||
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[catalogApiRef, mockCatalogApi],
|
||||
[starredEntitiesApiRef, mockStarredEntitiesApi],
|
||||
]}
|
||||
>
|
||||
{tester.reactElement()}
|
||||
</TestApiProvider>,
|
||||
{
|
||||
config: {
|
||||
app: {
|
||||
title: 'Custom app',
|
||||
},
|
||||
backend: { baseUrl: 'http://localhost:7000' },
|
||||
},
|
||||
mountedRoutes: {
|
||||
'/catalog': convertLegacyRouteRef(rootRouteRef),
|
||||
'/catalog/:namespace/:kind/:name':
|
||||
convertLegacyRouteRef(entityRouteRef),
|
||||
await renderInTestApp(tester.reactElement(), {
|
||||
apis: [
|
||||
[catalogApiRef, mockCatalogApi],
|
||||
[starredEntitiesApiRef, mockStarredEntitiesApi],
|
||||
],
|
||||
config: {
|
||||
app: {
|
||||
title: 'Custom app',
|
||||
},
|
||||
backend: { baseUrl: 'http://localhost:7000' },
|
||||
},
|
||||
);
|
||||
mountedRoutes: {
|
||||
'/catalog': convertLegacyRouteRef(rootRouteRef),
|
||||
'/catalog/:namespace/:kind/:name':
|
||||
convertLegacyRouteRef(entityRouteRef),
|
||||
},
|
||||
});
|
||||
|
||||
const { disabled } = params.useProps();
|
||||
|
||||
@@ -797,29 +730,23 @@ describe('Entity page', () => {
|
||||
.add(menuItem)
|
||||
.add(filteredMenuItem);
|
||||
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[catalogApiRef, mockCatalogApi],
|
||||
[starredEntitiesApiRef, mockStarredEntitiesApi],
|
||||
]}
|
||||
>
|
||||
{tester.reactElement()}
|
||||
</TestApiProvider>,
|
||||
{
|
||||
config: {
|
||||
app: {
|
||||
title: 'Custom app',
|
||||
},
|
||||
backend: { baseUrl: 'http://localhost:7000' },
|
||||
},
|
||||
mountedRoutes: {
|
||||
'/catalog': convertLegacyRouteRef(rootRouteRef),
|
||||
'/catalog/:namespace/:kind/:name':
|
||||
convertLegacyRouteRef(entityRouteRef),
|
||||
await renderInTestApp(tester.reactElement(), {
|
||||
config: {
|
||||
app: {
|
||||
title: 'Custom app',
|
||||
},
|
||||
backend: { baseUrl: 'http://localhost:7000' },
|
||||
},
|
||||
);
|
||||
mountedRoutes: {
|
||||
'/catalog': convertLegacyRouteRef(rootRouteRef),
|
||||
'/catalog/:namespace/:kind/:name':
|
||||
convertLegacyRouteRef(entityRouteRef),
|
||||
},
|
||||
apis: [
|
||||
[catalogApiRef, mockCatalogApi],
|
||||
[starredEntitiesApiRef, mockStarredEntitiesApi],
|
||||
],
|
||||
});
|
||||
|
||||
await userEvent.click(await screen.findByTestId('menu-button'));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user