diff --git a/app/src/App.js b/app/src/App.js index c3299d98..088cfa95 100644 --- a/app/src/App.js +++ b/app/src/App.js @@ -87,10 +87,12 @@ function Root() { } -const router = createBrowserRouter(createRoutesFromElements(} - errorElement={} + // Root (and its ThemeProvider) is not rendered when Root itself fails, so the error page + // brings its own theme; Mantine components cannot render without one. + errorElement={} > }/> }/> @@ -121,7 +123,11 @@ const router = createBrowserRouter(createRoutesFromElements(}/> }/> }/> -)); + {/* Unknown URLs render inside Root, with the navigation bar and theme. */} + }/> +); + +const router = createBrowserRouter(routes); export default function App() { useEventsInterval(); diff --git a/app/src/App.test.js b/app/src/App.test.js new file mode 100644 index 00000000..f7e78352 --- /dev/null +++ b/app/src/App.test.js @@ -0,0 +1,52 @@ +import React from 'react'; +import {render, screen} from '@testing-library/react'; +import {createMemoryRouter, RouterProvider} from 'react-router'; +import {routes} from './App'; +import {StatusProvider} from './hooks/customHooks'; +import {FileWorkerStatusProvider} from './contexts/FileWorkerStatusContext'; +import {MediaContextProvider} from './contexts/contexts'; + +// Jest does not transform CSS inside node_modules (see transformIgnorePatterns). +jest.mock('@mantine/core/styles.css', () => ({})); +jest.mock('@mantine/notifications/styles.css', () => ({})); +// Routes this test does not visit, whose libraries jsdom/Jest cannot load (maplibre needs +// browser APIs; esptool-js is ESM-only). +jest.mock('./components/Map', () => ({MapRoute: () => null})); +jest.mock('./components/Flasher', () => ({FlasherRoute: () => null})); + +describe('unknown routes', () => { + beforeEach(() => { + // resetMocks clears setupTests' implementation; the theme and media providers read it. + window.matchMedia.mockImplementation(query => ({ + matches: false, + media: query, + addListener: jest.fn(), + removeListener: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + })); + global.fetch = jest.fn(() => Promise.resolve({ + ok: true, + status: 200, + headers: {get: () => 'application/json'}, + json: () => Promise.resolve({tags: []}), + text: () => Promise.resolve('{}'), + })); + }); + + it('renders Page Not Found inside the app layout', async () => { + const router = createMemoryRouter(routes, {initialEntries: ['/no-such-page']}); + // The providers App wraps around the router. + render( + + + + + + ); + + expect(await screen.findByText('Page Not Found!')).toBeInTheDocument(); + // Rendered inside Root's layout (the footer comes from Root), not as a bare error page. + expect(screen.getByText('Donate')).toBeInTheDocument(); + }); +});