diff --git a/.changeset/brave-starfishes-try.md b/.changeset/brave-starfishes-try.md new file mode 100644 index 0000000000..a611e78540 --- /dev/null +++ b/.changeset/brave-starfishes-try.md @@ -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. diff --git a/.changeset/hip-monkeys-hide.md b/.changeset/hip-monkeys-hide.md new file mode 100644 index 0000000000..94b8e08330 --- /dev/null +++ b/.changeset/hip-monkeys-hide.md @@ -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. diff --git a/.changeset/six-buckets-tap.md b/.changeset/six-buckets-tap.md new file mode 100644 index 0000000000..c596f6fb85 --- /dev/null +++ b/.changeset/six-buckets-tap.md @@ -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`. diff --git a/packages/test-utils/api-report.md b/packages/test-utils/api-report.md index 94ef7af51e..38d6a85f5f 100644 --- a/packages/test-utils/api-report.md +++ b/packages/test-utils/api-report.md @@ -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 = { [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; // @public -export function renderWithEffects(nodes: ReactElement): Promise; +export function renderWithEffects( + nodes: ReactElement, + options?: Pick, +): Promise; // @public export function setupRequestMockHandlers(worker: { diff --git a/packages/test-utils/src/testUtils/appWrappers.test.tsx b/packages/test-utils/src/testUtils/appWrappers.test.tsx index c59f66aab5..1b0601d030 100644 --- a/packages/test-utils/src/testUtils/appWrappers.test.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.test.tsx @@ -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 ; + }; + + const rendered = await renderInTestApp(); + expect(rendered.getByTestId('progress')).toBeInTheDocument(); + + rendered.rerender(); + expect(rendered.getByTestId('progress')).toBeInTheDocument(); + }); }); diff --git a/packages/test-utils/src/testUtils/appWrappers.tsx b/packages/test-utils/src/testUtils/appWrappers.tsx index b12e8935b6..fd22c1a150 100644 --- a/packages/test-utils/src/testUtils/appWrappers.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.tsx @@ -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(); @@ -162,13 +160,6 @@ export function wrapInTestApp( }, }); - let wrappedElement: React.ReactElement; - if (Component instanceof Function) { - wrappedElement = ; - } else { - wrappedElement = Component as React.ReactElement; - } - const routeElements = Object.entries(options.mountedRoutes ?? {}).map( ([path, routeRef]) => { const Page = () =>
Mounted at {path}
; @@ -189,18 +180,44 @@ export function wrapInTestApp( const AppProvider = app.getProvider(); const AppRouter = app.getRouter(); - return ( + const TestAppWrapper = ({ children }: { children: ReactNode }) => ( {routeElements} {/* 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 */} - + {children}} /> ); + + 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 = ; + } else { + wrappedElement = Component as React.ReactElement; + } + + return {wrappedElement}; } /** @@ -218,5 +235,14 @@ export async function renderInTestApp( Component: ComponentType | ReactNode, options: TestAppOptions = {}, ): Promise { - return renderWithEffects(wrapInTestApp(Component, options)); + let wrappedElement: React.ReactElement; + if (Component instanceof Function) { + wrappedElement = ; + } else { + wrappedElement = Component as React.ReactElement; + } + + return renderWithEffects(wrappedElement, { + wrapper: createTestAppWrapper(options), + }); } diff --git a/packages/test-utils/src/testUtils/index.tsx b/packages/test-utils/src/testUtils/index.tsx index a7850fc158..bfd870e2ad 100644 --- a/packages/test-utils/src/testUtils/index.tsx +++ b/packages/test-utils/src/testUtils/index.tsx @@ -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'; diff --git a/packages/test-utils/src/testUtils/testingLibrary.ts b/packages/test-utils/src/testUtils/testingLibrary.ts index bfe617a436..adb9867de8 100644 --- a/packages/test-utils/src/testUtils/testingLibrary.ts +++ b/packages/test-utils/src/testUtils/testingLibrary.ts @@ -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, ): Promise { let value: RenderResult; await act(async () => { - value = render(nodes); + value = render(nodes, options); }); return value!; } diff --git a/plugins/shortcuts/src/ShortcutItem.test.tsx b/plugins/shortcuts/src/ShortcutItem.test.tsx index 528eb13efa..90b8ff06e5 100644 --- a/plugins/shortcuts/src/ShortcutItem.test.tsx +++ b/plugins/shortcuts/src/ShortcutItem.test.tsx @@ -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()); + rerender(); await waitFor(() => { expect(screen.getByText('TT')).toBeInTheDocument(); }); - rerender(wrapInTestApp()); + rerender(); await waitFor(() => { expect(screen.getByText('MT')).toBeInTheDocument(); });