From bb8945e8ad751ef725e53fb9d78c9340771e38cb Mon Sep 17 00:00:00 2001 From: lrnselfreliance <33263708+lrnselfreliance@users.noreply.github.com> Date: Fri, 9 Oct 2026 15:00:36 -0600 Subject: [PATCH] Render unknown URLs as a 404 page inside the app layout (#805) An unknown URL (e.g. /archive instead of /archives) had no matching route, so react-router rendered the root errorElement in place of Root. That put PageNotFound outside ThemeProvider, and its Mantine Box threw "MantineProvider was not found", leaving a blank page. - A catch-all route renders PageNotFound inside Root, with the navigation bar, footer and theme. - The root errorElement brings its own ThemeProvider, so it can render when Root itself fails. - App exports its routes so a test can mount them in a memory router. Co-authored-by: Claude Opus 5.5 --- app/src/App.js | 12 ++++++++--- app/src/App.test.js | 52 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 app/src/App.test.js 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(); + }); +});