From 676c4af4fd56669513e90268821f60599885abfb Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 8 Jun 2020 16:41:17 +0200 Subject: [PATCH] package/test-utils: provide renderInTestApp + act wrapping + docs + tests --- .../src/testUtils/appWrappers.test.tsx | 61 +++++++++++++++---- .../test-utils/src/testUtils/appWrappers.tsx | 32 +++++++++- packages/test-utils/src/testUtils/index.tsx | 2 +- 3 files changed, 79 insertions(+), 16 deletions(-) diff --git a/packages/test-utils/src/testUtils/appWrappers.test.tsx b/packages/test-utils/src/testUtils/appWrappers.test.tsx index e46b6a95ce..a30e8f9823 100644 --- a/packages/test-utils/src/testUtils/appWrappers.test.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.test.tsx @@ -14,22 +14,59 @@ * limitations under the License. */ -import React from 'react'; +import React, { FC } from 'react'; import { render } from '@testing-library/react'; -import { wrapInTestApp } from './appWrappers'; +import { wrapInTestApp, renderInTestApp } from './appWrappers'; import { Route } from 'react-router'; +import { withLogCollector } from '@backstage/test-utils-core'; describe('wrapInTestApp', () => { - it('should provide routing', () => { - const rendered = render( - wrapInTestApp( - <> - Route 1 - Route 2 - , - { routeEntries: ['/route2'] }, + it('should provide routing and warn about missing act()', async () => { + const { error } = await withLogCollector(['error'], async () => { + const rendered = render( + wrapInTestApp( + <> + Route 1 + Route 2 + , + { routeEntries: ['/route2'] }, + ), + ); + expect(rendered.getByText('Route 2')).toBeInTheDocument(); + + // Wait for async actions to trigger the act() warnings that we assert below + await Promise.resolve(); + }); + + expect(error).toEqual([ + expect.stringMatching( + /^Warning: An update to %s inside a test was not wrapped in act\(...\)/, ), - ); - expect(rendered.getByText('Route 2')).toBeInTheDocument(); + expect.stringMatching( + /^Warning: An update to %s inside a test was not wrapped in act\(...\)/, + ), + ]); + }); + + it('should render a component in a test app without warning about missing act()', async () => { + const { error } = await withLogCollector(['error'], async () => { + const Foo: FC<{}> = () => { + return

foo

; + }; + + const rendered = await renderInTestApp(Foo); + expect(rendered.getByText('foo')).toBeInTheDocument(); + }); + + expect(error).toEqual([]); + }); + + it('should render a node in a test app', async () => { + const Foo: FC<{}> = () => { + return

foo

; + }; + + const rendered = await renderInTestApp(); + expect(rendered.getByText('foo')).toBeInTheDocument(); }); }); diff --git a/packages/test-utils/src/testUtils/appWrappers.tsx b/packages/test-utils/src/testUtils/appWrappers.tsx index 2450604976..6d48082b7c 100644 --- a/packages/test-utils/src/testUtils/appWrappers.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { ComponentType, ReactNode, FunctionComponent, FC } from 'react'; +import React, { ComponentType, ReactNode, FC, ReactElement } from 'react'; import { MemoryRouter } from 'react-router'; import { Route } from 'react-router-dom'; import { lightTheme } from '@backstage/theme'; @@ -23,6 +23,8 @@ import privateExports, { ApiTestRegistry, BootErrorPageProps, } from '@backstage/core-api'; +import { RenderResult } from '@testing-library/react'; +import { renderWithEffects } from '@backstage/test-utils-core'; const { PrivateAppImpl } = privateExports; const NotFoundErrorPage = () => { @@ -43,10 +45,17 @@ type TestAppOptions = { routeEntries?: string[]; }; +/** + * 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. + */ export function wrapInTestApp( Component: ComponentType | ReactNode, options: TestAppOptions = {}, -) { +): ReactElement { const { routeEntries = ['/'] } = options; const app = new PrivateAppImpl({ @@ -72,7 +81,7 @@ export function wrapInTestApp( if (Component instanceof Function) { Wrapper = Component; } else { - Wrapper = (() => Component) as FunctionComponent; + Wrapper = (() => Component) as FC; } const AppProvider = app.getProvider(); @@ -85,3 +94,20 @@ export function wrapInTestApp( ); } + +/** + * Renders a component inside a Backstage test app, providing a mocked theme + * and app context, along with mocked APIs. + * + * The render executes async effects similar to `renderWithEffects`. To avoid this + * behavior, use a regular `render()` + `wrapInTestApp()` instead. + * + * @param Component - A component or react node to render inside the test app. + * @param options - Additional options for the rendering. + */ +export async function renderInTestApp( + Component: ComponentType | ReactNode, + options: TestAppOptions = {}, +): Promise { + return renderWithEffects(wrapInTestApp(Component, options)); +} diff --git a/packages/test-utils/src/testUtils/index.tsx b/packages/test-utils/src/testUtils/index.tsx index ea33de2f31..302bdb405f 100644 --- a/packages/test-utils/src/testUtils/index.tsx +++ b/packages/test-utils/src/testUtils/index.tsx @@ -15,4 +15,4 @@ */ export { default as mockBreakpoint } from './mockBreakpoint'; -export * from './appWrappers'; +export { wrapInTestApp, renderInTestApp } from './appWrappers';