home canvas
- }, - }) - const sectionRoute = createRoute({ - getParentRoute: () => rootRoute, - path: '/section', - staticData: { - Sidebar: function SectionSidebar() { - returnsection sidebar
- }, - }, - component: function SectionCanvas() { - returnsection canvas
- }, - }) - const router = createRouter({ - routeTree: rootRoute.addChildren([homeRoute, sectionRoute]), - history: createMemoryHistory({ initialEntries: [path] }), - }) - const client = createAuthQueryClient({ - queries: { retry: false, staleTime: Infinity }, - }) - seedSession(client, defaultUser) - client.setQueryData(versionQueryKey, '0.0.0') - render( -section sidebar
+section canvas
+} + +/** A plugin whose one row opens a sub menu, the way a plugin brings one into the admin. */ +export const sectionPlugin: FrontendPlugin = { + id: 'section', + nav: [{ label: 'Section', to: '/section' }], + routes: (parent) => [ + createRoute({ + getParentRoute: () => parent, + path: '/section', + staticData: { Sidebar: SectionSidebar }, + component: SectionCanvas, + }), + ], +} diff --git a/frontend/src/test/rail-version.test.tsx b/frontend/src/test/sidebar-version.test.tsx similarity index 100% rename from frontend/src/test/rail-version.test.tsx rename to frontend/src/test/sidebar-version.test.tsx diff --git a/frontend/src/test/small-shell.test.tsx b/frontend/src/test/small-shell.test.tsx index 2df44b81..286d2144 100644 --- a/frontend/src/test/small-shell.test.tsx +++ b/frontend/src/test/small-shell.test.tsx @@ -8,9 +8,13 @@ import { beforeEach, expect, test } from 'vitest' import { renderAt, renderRoutedAt } from './render' +/** The name godmin gives the button that opens the sidebar on a phone, as WordPress's new admin names it. */ +const MENU_BUTTON = 'Open navigation panel' + beforeEach(() => { server.use( http.get('/api/content', () => HttpResponse.json({ items: [], total: 0 })), + http.get('/api/content/counts', () => HttpResponse.json({})), http.get('/api/media', () => HttpResponse.json({ items: [], total: 0, per_page: 20 })), ) }) @@ -20,52 +24,68 @@ test('holds the navigation behind a menu button on a narrow viewport', async () renderAt('/') - expect(await screen.findByRole('button', { name: 'Open navigation' })).toBeInTheDocument() - expect(screen.queryByRole('navigation', { name: 'Navigation' })).toBeNull() + expect(await screen.findByRole('button', { name: MENU_BUTTON })).toBeInTheDocument() + expect(screen.queryByRole('region', { name: 'Navigation' })).toBeNull() }) -test('opens the navigation in a drawer', async () => { +test('opens the navigation in a drawer named for it, holding no region of its own', async () => { setViewport({ matches: true }) renderAt('/') - await userEvent.click(await screen.findByRole('button', { name: 'Open navigation' })) + await userEvent.click(await screen.findByRole('button', { name: MENU_BUTTON })) - const drawer = await screen.findByRole('dialog') - expect(within(drawer).getByRole('navigation', { name: 'Navigation' })).toBeInTheDocument() + const drawer = await screen.findByRole('dialog', { name: 'Navigation' }) expect(await within(drawer).findByRole('link', { name: 'Posts' })).toBeInTheDocument() + expect(within(drawer).queryByRole('region')).toBeNull() }) test('puts the navigation away once it takes the reader somewhere', async () => { setViewport({ matches: true }) renderAt('/media') - await userEvent.click(await screen.findByRole('button', { name: 'Open navigation' })) + await userEvent.click(await screen.findByRole('button', { name: MENU_BUTTON })) const drawer = await screen.findByRole('dialog') await userEvent.click(await within(drawer).findByRole('link', { name: 'Posts' })) - expect(await screen.findByRole('button', { name: 'Open navigation' })).toBeInTheDocument() + expect(await screen.findByRole('button', { name: MENU_BUTTON })).toBeInTheDocument() await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull()) }) -test('keeps the navigation open when its row names the list the root already opened', async () => { +/** + * Opens the drawer on a phone over the Posts list at the path, and follows its Posts row. + * @param path - The address the reader starts on. + * @returns The router and the drawer the row was followed from. + */ +async function openPostsFromDrawer(path: string) { setViewport({ matches: true }) - const { router } = renderRoutedAt('/') - await screen.findByRole('heading', { name: 'Posts', level: 1 }) - await userEvent.click(await screen.findByRole('button', { name: 'Open navigation' })) + const { router } = renderRoutedAt(path) + await screen.findByRole('heading', { name: 'Posts', level: 2 }) + await userEvent.click(await screen.findByRole('button', { name: MENU_BUTTON })) const drawer = await screen.findByRole('dialog') - await userEvent.click(await within(drawer).findByRole('link', { name: 'Posts' })) + return { router, drawer } +} + +test('puts the navigation away when its row leaves a status view of the same list', async () => { + const { router } = await openPostsFromDrawer('/content/post?status=trash') + + await waitFor(() => expect(router.state.location.searchStr).toBe('')) + await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull()) +}) + +test('keeps the navigation open when its row names the list the root already opened', async () => { + const { router, drawer } = await openPostsFromDrawer('/') await waitFor(() => expect(router.state.status).toBe('idle')) expect(router.state.location.pathname).toBe('/content/post') expect(screen.getByRole('dialog')).toBe(drawer) }) -test('keeps the rail beside the canvas when there is room', async () => { +test('keeps the sidebar beside the canvas when there is room', async () => { setViewport({ matches: false }) renderAt('/') - expect(await screen.findByRole('navigation', { name: 'Navigation' })).toBeInTheDocument() - expect(screen.queryByRole('button', { name: 'Open navigation' })).toBeNull() + expect(await screen.findByRole('region', { name: 'Navigation' })).toBeInTheDocument() + expect(screen.queryByRole('button', { name: MENU_BUTTON })).toBeNull() }) diff --git a/frontend/src/test/themes-manage.test.tsx b/frontend/src/test/themes-manage.test.tsx index bb1e45ea..55e7c2d0 100644 --- a/frontend/src/test/themes-manage.test.tsx +++ b/frontend/src/test/themes-manage.test.tsx @@ -53,14 +53,15 @@ beforeEach(() => * Returns a theme as the screen models it. * @param name - The theme name. * @param active - Whether it is the chosen theme. - * @param broken - The reason it will not load, empty when it loads. - * @param version - The version its manifest declares. - * @param serving - Whether the public site is answered by it. - * @param startFailed - Whether its supervisor stopped trying to start it. + * @param state - What differs from a theme that loads, declares 1.0.0 and answers the site while chosen. * @returns The theme. */ -function theme(name: string, active: boolean, broken = '', version = '1.0.0', serving = active, startFailed = false) { - return { name, version, broken, active, serving, startFailed } +function theme( + name: string, + active: boolean, + state: { broken?: string, version?: string, serving?: boolean, startFailed?: boolean } = {}, +) { + return { name, version: '1.0.0', broken: '', active, serving: active, startFailed: false, ...state } } test('names the theme serving the public site', async () => { @@ -97,7 +98,7 @@ test('says the built-in renderer is serving when no theme is active', async () = }) test('says the renderer is serving when the active theme will not load', async () => { - listing([theme('aurora', true, 'the manifest is missing', '1.0.0', false)]) + listing([theme('aurora', true, { broken: 'the manifest is missing', serving: false })]) renderAt('/themes') expect( @@ -106,7 +107,7 @@ test('says the renderer is serving when the active theme will not load', async ( }) test('still offers to deactivate a chosen theme whose files are gone', async () => { - listing([theme('aurora', true, 'the theme is not installed', '', false)]) + listing([theme('aurora', true, { broken: 'the theme is not installed', version: '', serving: false })]) renderAt('/themes') const listed = await screen.findByRole('row', { name: /aurora/ }) @@ -134,20 +135,20 @@ test('phrases what is serving from the themes alone', () => { expect(servingLine({ themes: [theme('aurora', true)], rollback: null })).toBe( 'aurora 1.0.0 is serving the public site.', ) - expect(servingLine({ themes: [theme('aurora', true, '', '')], rollback: null })).toBe( + expect(servingLine({ themes: [theme('aurora', true, { version: '' })], rollback: null })).toBe( 'aurora is serving the public site.', ) expect( servingLine({ - themes: [theme('aurora', true, 'the manifest is missing')], + themes: [theme('aurora', true, { broken: 'the manifest is missing' })], rollback: null, }), ).toBe('aurora will not load, so the built-in renderer is serving.') expect( - servingLine({ themes: [theme('aurora', true, '', '1.0.0', false)], rollback: null }), + servingLine({ themes: [theme('aurora', true, { serving: false })], rollback: null }), ).toBe('aurora is still starting, so the built-in renderer is serving.') expect( - servingLine({ themes: [theme('aurora', true, '', '1.0.0', false, true)], rollback: null }), + servingLine({ themes: [theme('aurora', true, { serving: false, startFailed: true })], rollback: null }), ).toBe('aurora failed to start, so the built-in renderer is serving.') }) @@ -199,7 +200,7 @@ test('keeps the themes heading when the themes cannot be listed', async () => { renderAt('/themes') expect(await screen.findByRole('alert')).toHaveTextContent('Themes could not be loaded.') - expect(screen.getByRole('heading', { level: 1, name: 'Themes' })).toBeTruthy() + expect(screen.getByRole('heading', { level: 2, name: 'Themes' })).toBeTruthy() }) test('serves the public site through a theme at the reader s request', async () => { diff --git a/frontend/src/test/users-route.test.tsx b/frontend/src/test/users-route.test.tsx index 7af41936..b83ee862 100644 --- a/frontend/src/test/users-route.test.tsx +++ b/frontend/src/test/users-route.test.tsx @@ -31,7 +31,7 @@ test('keeps the users heading while the accounts are on their way', async () => const main = await screen.findByRole('main') - expect(await within(main).findByRole('heading', { level: 1 })).toHaveTextContent('Users') + expect(await within(main).findByRole('heading', { level: 2 })).toHaveTextContent('Users') const status = await within(main).findByRole('status') expect(status).toHaveTextContent('Loading users.') expect(status.closest('.godmin-loading-rows')).not.toBeNull() @@ -53,17 +53,17 @@ test('keeps the users heading when the accounts cannot be loaded', async () => { expect(alert).toHaveTextContent(/could not be loaded/i) const main = screen.getByRole('main') - expect(within(main).getByRole('heading', { level: 1 })).toHaveTextContent('Users') - expect(within(main).getAllByRole('heading', { level: 1 })).toHaveLength(1) + expect(within(main).getByRole('heading', { level: 2 })).toHaveTextContent('Users') + expect(within(main).getAllByRole('heading', { level: 2 })).toHaveLength(1) }) -test('names the add user screen with its only first level heading, as WordPress does', async () => { +test('names the add user screen with its only second level heading, under the sidebar title', async () => { renderAt('/users/new') const main = await screen.findByRole('main') - expect(within(main).getByRole('heading', { level: 1 })).toHaveTextContent('Add User') - expect(within(main).getAllByRole('heading', { level: 1 })).toHaveLength(1) + expect(within(main).getByRole('heading', { level: 2 })).toHaveTextContent('Add User') + expect(within(main).getAllByRole('heading', { level: 2 })).toHaveLength(1) }) test('names the add user screen as WordPress es_ES does for a Spanish reader', async () => { @@ -71,7 +71,7 @@ test('names the add user screen as WordPress es_ES does for a Spanish reader', a onTestFinished(() => resetLocaleData({}, 'gophenberg')) renderAt('/users/new') - expect(await screen.findByRole('heading', { level: 1, name: 'Añadir usuario' })).toBeInTheDocument() + expect(await screen.findByRole('heading', { level: 2, name: 'Añadir usuario' })).toBeInTheDocument() }) test('serves the users screen at /users', async () => { diff --git a/frontend/src/test/version-display.test.tsx b/frontend/src/test/version-display.test.tsx index 5a6e358b..261964c6 100644 --- a/frontend/src/test/version-display.test.tsx +++ b/frontend/src/test/version-display.test.tsx @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -import { screen } from '@testing-library/react' +import { screen, within } from '@testing-library/react' import { expect, test } from 'vitest' import { renderAt } from './render' @@ -11,9 +11,21 @@ test('shows the app version in the sidebar', async () => { expect(await screen.findByText('v2.5.1')).toBeInTheDocument() }) +test('notes the version under the account panel, in the sidebar footer outside the menu', async () => { + renderAt('/', undefined, '2.5.1') + + const note = await screen.findByText('v2.5.1') + const footer = note.closest('.godmin-sidebar__footer') as HTMLElement + expect(note).toHaveClass('godmin-sidebar__footer-note') + expect(within(footer).getByRole('button', { name: 'Log out' })).toBeInTheDocument() + const sidebar = screen.getByRole('region', { name: 'Navigation' }) + expect(sidebar).toContainElement(footer) + expect(within(within(sidebar).getByRole('list')).queryByText('v2.5.1')).toBeNull() +}) + test('omits the version when it is unavailable', async () => { renderAt('/', undefined, null) - await screen.findByRole('link', { name: 'Gophenberg' }) + await screen.findByRole('heading', { name: 'Gophenberg' }) expect(screen.queryByText(/^v\d/)).toBeNull() }) diff --git a/frontend/src/themes/ThemesScreen.tsx b/frontend/src/themes/ThemesScreen.tsx index 2e813f5b..e57d1c10 100644 --- a/frontend/src/themes/ThemesScreen.tsx +++ b/frontend/src/themes/ThemesScreen.tsx @@ -133,6 +133,7 @@ export function ThemesScreen() { return (