Files
webmail/lib/admin/__tests__/admin-nav.test.ts
T
Linus Rath 10b4d2a9db feat: rework the admin panel around a settings-style sidebar and one URL per page
The old panel kept its active tab in a store, so every page sat at
/admin, the sidebar duplicated the webmail rail by hand, and each page
was a stack of boxed cards with their own save buttons. Plugin settings
were only reachable through a gear icon in a row of five.

- one catch-all route for the whole panel; pages switch through
  history.pushState, so `/admin/plugins/<id>`, the back button, bookmarks
  and connector links all work without a route transition
- sidebar laid out like the user settings: back to mail, search over
  pages and their sections, two group headings, change password and
  sign out at the bottom; the phone shows a list, then the page
- pages use SettingsSection rows instead of nested cards, with one page
  header, one flash message and one save bar at the bottom (Discard,
  Save, Ctrl+S); the unsaved-changes prompt still guards leaving
- plugins and themes are lists of rows with a toggle each; a row opens
  the extension's own page with its settings, force-on, permissions,
  kept versions and removal
- overview shows stat tiles, server facts and recent activity
- old `/admin?tab=x` links redirect; the tab store is gone and the
  connector registry resolves admin links to the page URL
- `NEXT_TURBOPACK_ROOT` overrides the Turbopack root so `next dev` runs
  from a worktree with a junctioned node_modules
2026-10-20 15:16:05 +02:00

56 lines
2.6 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { ADMIN_PAGES, ADMIN_PAGE_IDS, adminPath, isAdminPageId, parseAdminPath } from '../admin-nav';
describe('admin page URLs', () => {
it('reads the page and the detail from a pathname', () => {
expect(parseAdminPath('/admin')).toEqual({ page: null, detail: null });
expect(parseAdminPath('/admin/')).toEqual({ page: null, detail: null });
expect(parseAdminPath('/admin/settings')).toEqual({ page: 'settings', detail: null });
expect(parseAdminPath('/admin/plugins/jitsi-meet')).toEqual({ page: 'plugins', detail: 'jitsi-meet' });
expect(parseAdminPath('/admin/marketplace/quick-reply')).toEqual({ page: 'marketplace', detail: 'quick-reply' });
});
it('decodes a detail id and keeps slashes inside it', () => {
expect(parseAdminPath('/admin/plugins/acme%2Fwidget')).toEqual({ page: 'plugins', detail: 'acme/widget' });
expect(parseAdminPath('/admin/plugins/acme/widget')).toEqual({ page: 'plugins', detail: 'acme/widget' });
});
it('ignores a detail on pages that have none, and an unknown page', () => {
expect(parseAdminPath('/admin/settings/anything')).toEqual({ page: 'settings', detail: null });
expect(parseAdminPath('/admin/nope')).toEqual({ page: null, detail: null });
});
it('is null outside the admin tree', () => {
expect(parseAdminPath('/mail')).toBeNull();
expect(parseAdminPath('/administration')).toBeNull();
expect(parseAdminPath('/en/admin')).toBeNull();
});
it('builds the paths it parses', () => {
expect(adminPath()).toBe('/admin');
expect(adminPath(null)).toBe('/admin');
expect(adminPath('policy')).toBe('/admin/policy');
expect(adminPath('themes', 'nord')).toBe('/admin/themes/nord');
expect(adminPath('plugins', 'acme/widget')).toBe('/admin/plugins/acme%2Fwidget');
for (const id of ADMIN_PAGE_IDS) {
expect(parseAdminPath(adminPath(id))).toEqual({ page: id, detail: null });
}
expect(parseAdminPath(adminPath('plugins', 'acme/widget'))).toEqual({ page: 'plugins', detail: 'acme/widget' });
});
it('knows its page ids', () => {
expect(isAdminPageId('plugins')).toBe(true);
expect(isAdminPageId('login')).toBe(false);
expect(isAdminPageId(null)).toBe(false);
});
it('registers every page once, with the detail flag only where a detail page exists', () => {
expect(ADMIN_PAGES.map((p) => p.id)).toEqual([...ADMIN_PAGE_IDS]);
expect(ADMIN_PAGES.filter((p) => p.hasDetail).map((p) => p.id)).toEqual(['plugins', 'themes', 'marketplace']);
for (const page of ADMIN_PAGES) {
const ids = page.sections.map((s) => s.id);
expect(new Set(ids).size).toBe(ids.length);
}
});
});