/* * 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 { createExternalRouteRef, createRouteRef, createSubRouteRef, errorApiRef, useApi, useApp, useRouteRef, } from '@backstage/core-plugin-api'; import { withLogCollector } from './logCollector'; import { render } from '@testing-library/react'; import React, { useEffect } from 'react'; import { Route, Routes } from 'react-router'; import { MockErrorApi } from './apis'; import { renderInTestApp, wrapInTestApp } from './appWrappers'; import { TestApiProvider } from './TestApiProvider'; describe('wrapInTestApp', () => { it('should provide routing and warn about missing act()', async () => { const { error } = await withLogCollector(['error'], async () => { const rendered = render( wrapInTestApp( <Routes> <Route path="/route1" element={<p>Route 1</p>} /> <Route path="/route2" element={<p>Route 2</p>} /> </Routes>, { 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.some(e => e.includes( 'Warning: An update to %s inside a test was not wrapped in act(...)', ), ), ).toBeTruthy(); }); it('should render a component in a test app without warning about missing act()', async () => { const { error } = await withLogCollector(['error'], async () => { const Foo = () => { return <p>foo</p>; }; 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 = () => { return <p>foo</p>; }; const rendered = await renderInTestApp(<Foo />); expect(rendered.getByText('foo')).toBeInTheDocument(); }); it('should provide mock API implementations', async () => { const A = () => { const errorApi = useApi(errorApiRef); errorApi.post(new Error('NOPE')); return null; }; const { error } = await withLogCollector(['error'], async () => { await expect(renderInTestApp(A)).rejects.toThrow('NOPE'); }); expect(error).toEqual([ expect.stringMatching( /^Error: Uncaught \[Error: MockErrorApi received unexpected error, Error: NOPE\]/, ), expect.stringMatching(/^The above error occurred in the <A> component:/), ]); }); it('should allow custom API implementations', async () => { const mockErrorApi = new MockErrorApi({ collect: true }); const A = () => { const errorApi = useApi(errorApiRef); useEffect(() => { errorApi.post(new Error('NOPE')); }, [errorApi]); return <p>foo</p>; }; const rendered = await renderInTestApp( <TestApiProvider apis={[[errorApiRef, mockErrorApi]]}> <A /> </TestApiProvider>, ); expect(rendered.getByText('foo')).toBeInTheDocument(); expect(mockErrorApi.getErrors()).toEqual([{ error: new Error('NOPE') }]); }); it('should allow route refs to be mounted on specific paths', async () => { const aRouteRef = createRouteRef({ id: 'A' }); const bRouteRef = createRouteRef({ id: 'B', params: ['name'] }); const subRouteRef = createSubRouteRef({ id: 'S', parent: bRouteRef, path: '/:page', }); const externalRouteRef = createExternalRouteRef({ id: 'E', params: ['name'], }); const MyComponent = () => { const a = useRouteRef(aRouteRef); const b = useRouteRef(bRouteRef); const s = useRouteRef(subRouteRef); const e = useRouteRef(externalRouteRef); return ( <div> <div>Link A: {a()}</div> <div>Link B: {b({ name: 'x' })}</div> <div>Link S: {s({ name: 'y', page: 'p' })}</div> <div>Link E: {e({ name: 'z' })}</div> </div> ); }; const rendered = await renderInTestApp(<MyComponent />, { mountedRoutes: { '/my-a-path': aRouteRef, '/my-b-path/:name': bRouteRef, '/my-e-path/:name': externalRouteRef, }, }); expect(rendered.getByText('Link A: /my-a-path')).toBeInTheDocument(); expect(rendered.getByText('Link B: /my-b-path/x')).toBeInTheDocument(); expect(rendered.getByText('Link S: /my-b-path/y/p')).toBeInTheDocument(); expect(rendered.getByText('Link E: /my-e-path/z')).toBeInTheDocument(); }); it('should not make route mounting elements visible during tests', async () => { const routeRef = createRouteRef({ id: 'foo' }); const rendered = await renderInTestApp(<span>foo</span>, { mountedRoutes: { '/foo': routeRef }, }); const [root] = rendered.baseElement.children; expect(root).toBeInTheDocument(); 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(); }); });