test-utils: add rerender support for renderInTestApp

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2022-04-29 15:44:07 +02:00
parent 183948fa85
commit 1da8b248c2
9 changed files with 98 additions and 27 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/test-utils': patch
---
Fixed `renderInTestApp` so that it is able to re-render the result without removing the app wrapping.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/test-utils': minor
---
Added the options parameter to `renderWithEffects`, which if forwarded to the `render` function from `@testling-library/react`. Initially only the `wrapper` option is supported.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/test-utils': minor
---
Added `createTestAppWrapper`, which returns a component that can be used as the `wrapper` option for `render` or `renderWithEffects`.
+10 -1
View File
@@ -27,6 +27,7 @@ import { Observable } from '@backstage/types';
import { PermissionApi } from '@backstage/plugin-permission-react';
import { ReactElement } from 'react';
import { ReactNode } from 'react';
import { RenderOptions } from '@testing-library/react';
import { RenderResult } from '@testing-library/react';
import { RouteRef } from '@backstage/core-plugin-api';
import { StorageApi } from '@backstage/core-plugin-api';
@@ -40,6 +41,11 @@ export type CollectedLogs<T extends LogFuncs> = {
[key in T]: string[];
};
// @public
export function createTestAppWrapper(
options?: TestAppOptions,
): (props: { children: ReactNode }) => JSX.Element;
// @public
export type ErrorWithContext = {
error: ErrorApiError;
@@ -187,7 +193,10 @@ export function renderInTestApp(
): Promise<RenderResult>;
// @public
export function renderWithEffects(nodes: ReactElement): Promise<RenderResult>;
export function renderWithEffects(
nodes: ReactElement,
options?: Pick<RenderOptions, 'wrapper'>,
): Promise<RenderResult>;
// @public
export function setupRequestMockHandlers(worker: {
@@ -20,6 +20,7 @@ import {
createSubRouteRef,
errorApiRef,
useApi,
useApp,
useRouteRef,
} from '@backstage/core-plugin-api';
import { withLogCollector } from './logCollector';
@@ -172,4 +173,18 @@ describe('wrapInTestApp', () => {
expect(root.children.length).toBe(1);
expect(root.children[0].textContent).toBe('foo');
});
it('should support rerenders', async () => {
const MyComponent = () => {
const app = useApp();
const { Progress } = app.getComponents();
return <Progress />;
};
const rendered = await renderInTestApp(<MyComponent />);
expect(rendered.getByTestId('progress')).toBeInTheDocument();
rendered.rerender(<MyComponent />);
expect(rendered.getByTestId('progress')).toBeInTheDocument();
});
});
@@ -107,17 +107,15 @@ function isExternalRouteRef(
}
/**
* Wraps a component inside a Backstage test app, providing a mocked theme
* and app context, along with mocked APIs.
* Creates a Wrapper component that wraps a component inside a Backstage test app,
* providing a mocked theme and app context, along with mocked APIs.
*
* @param Component - A component or react node to render inside the test app.
* @param options - Additional options for the rendering.
* @public
*/
export function wrapInTestApp(
Component: ComponentType | ReactNode,
export function createTestAppWrapper(
options: TestAppOptions = {},
): ReactElement {
): (props: { children: ReactNode }) => JSX.Element {
const { routeEntries = ['/'] } = options;
const boundRoutes = new Map<ExternalRouteRef, RouteRef>();
@@ -162,13 +160,6 @@ export function wrapInTestApp(
},
});
let wrappedElement: React.ReactElement;
if (Component instanceof Function) {
wrappedElement = <Component />;
} else {
wrappedElement = Component as React.ReactElement;
}
const routeElements = Object.entries(options.mountedRoutes ?? {}).map(
([path, routeRef]) => {
const Page = () => <div>Mounted at {path}</div>;
@@ -189,18 +180,44 @@ export function wrapInTestApp(
const AppProvider = app.getProvider();
const AppRouter = app.getRouter();
return (
const TestAppWrapper = ({ children }: { children: ReactNode }) => (
<AppProvider>
<AppRouter>
<NoRender>{routeElements}</NoRender>
{/* The path of * here is needed to be set as a catch all, so it will render the wrapper element
* and work with nested routes if they exist too */}
<Routes>
<Route path="/*" element={wrappedElement} />
<Route path="/*" element={<>{children}</>} />
</Routes>
</AppRouter>
</AppProvider>
);
return TestAppWrapper;
}
/**
* Wraps a component inside a Backstage test app, providing a mocked theme
* and app context, along with mocked APIs.
*
* @param Component - A component or react node to render inside the test app.
* @param options - Additional options for the rendering.
* @public
*/
export function wrapInTestApp(
Component: ComponentType | ReactNode,
options: TestAppOptions = {},
): ReactElement {
const TestAppWrapper = createTestAppWrapper(options);
let wrappedElement: React.ReactElement;
if (Component instanceof Function) {
wrappedElement = <Component />;
} else {
wrappedElement = Component as React.ReactElement;
}
return <TestAppWrapper>{wrappedElement}</TestAppWrapper>;
}
/**
@@ -218,5 +235,14 @@ export async function renderInTestApp(
Component: ComponentType | ReactNode,
options: TestAppOptions = {},
): Promise<RenderResult> {
return renderWithEffects(wrapInTestApp(Component, options));
let wrappedElement: React.ReactElement;
if (Component instanceof Function) {
wrappedElement = <Component />;
} else {
wrappedElement = Component as React.ReactElement;
}
return renderWithEffects(wrappedElement, {
wrapper: createTestAppWrapper(options),
});
}
+5 -1
View File
@@ -16,7 +16,11 @@
export * from './apis';
export { default as mockBreakpoint } from './mockBreakpoint';
export { wrapInTestApp, renderInTestApp } from './appWrappers';
export {
wrapInTestApp,
renderInTestApp,
createTestAppWrapper,
} from './appWrappers';
export type { TestAppOptions } from './appWrappers';
export * from './msw';
export * from './logCollector';
@@ -15,7 +15,12 @@
*/
import { ReactElement } from 'react';
import { act, render, RenderResult } from '@testing-library/react';
import {
act,
render,
RenderOptions,
RenderResult,
} from '@testing-library/react';
/**
* @public
@@ -31,10 +36,11 @@ import { act, render, RenderResult } from '@testing-library/react';
*/
export async function renderWithEffects(
nodes: ReactElement,
options?: Pick<RenderOptions, 'wrapper'>,
): Promise<RenderResult> {
let value: RenderResult;
await act(async () => {
value = render(nodes);
value = render(nodes, options);
});
return value!;
}
+3 -7
View File
@@ -19,11 +19,7 @@ import { screen, waitFor } from '@testing-library/react';
import { ShortcutItem } from './ShortcutItem';
import { Shortcut } from './types';
import { LocalStoredShortcuts } from './api';
import {
MockStorageApi,
renderInTestApp,
wrapInTestApp,
} from '@backstage/test-utils';
import { MockStorageApi, renderInTestApp } from '@backstage/test-utils';
import { SidebarContext } from '@backstage/core-components';
describe('ShortcutItem', () => {
@@ -66,12 +62,12 @@ describe('ShortcutItem', () => {
);
expect(screen.getByText('On')).toBeInTheDocument();
rerender(wrapInTestApp(<ShortcutItem api={api} shortcut={shortcut2} />));
rerender(<ShortcutItem api={api} shortcut={shortcut2} />);
await waitFor(() => {
expect(screen.getByText('TT')).toBeInTheDocument();
});
rerender(wrapInTestApp(<ShortcutItem api={api} shortcut={shortcut3} />));
rerender(<ShortcutItem api={api} shortcut={shortcut3} />);
await waitFor(() => {
expect(screen.getByText('MT')).toBeInTheDocument();
});