Files
webmail/components/admin/__tests__/save-bar.test.tsx
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

54 lines
2.3 KiB
TypeScript

import { fireEvent, render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { AdminSaveBar } from '../save-bar';
import { AdminShellContext } from '../shell-context';
describe('AdminSaveBar', () => {
it('shows nothing while there is nothing to save', () => {
render(<AdminSaveBar dirty={false} onSave={() => {}} />);
expect(screen.queryByTestId('admin-save-bar')).toBeNull();
});
it('offers Save and Discard once something changed', () => {
const onSave = vi.fn();
const onDiscard = vi.fn();
render(<AdminSaveBar dirty onSave={onSave} onDiscard={onDiscard} saveLabel="Save policy" note="Users see it on login" />);
expect(screen.getByText('Unsaved changes')).toBeTruthy();
expect(screen.getByText(/Users see it on login/)).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: 'Save policy' }));
expect(onSave).toHaveBeenCalledTimes(1);
fireEvent.click(screen.getByRole('button', { name: 'Discard' }));
expect(onDiscard).toHaveBeenCalledTimes(1);
});
it('saves on Ctrl+S, but not while a save is running', () => {
const onSave = vi.fn();
const { rerender } = render(<AdminSaveBar dirty onSave={onSave} />);
fireEvent.keyDown(window, { key: 's', ctrlKey: true });
expect(onSave).toHaveBeenCalledTimes(1);
rerender(<AdminSaveBar dirty saving onSave={onSave} />);
fireEvent.keyDown(window, { key: 's', ctrlKey: true });
expect(onSave).toHaveBeenCalledTimes(1);
});
it('ignores Ctrl+S when nothing is dirty', () => {
const onSave = vi.fn();
render(<AdminSaveBar dirty={false} onSave={onSave} />);
fireEvent.keyDown(window, { key: 's', ctrlKey: true });
expect(onSave).not.toHaveBeenCalled();
});
it('renders into the shell host when there is one', () => {
const host = document.createElement('div');
document.body.appendChild(host);
render(
<AdminShellContext.Provider value={{ saveBarHost: host, refreshBadges: () => {} }}>
<div data-testid="page"><AdminSaveBar dirty onSave={() => {}} /></div>
</AdminShellContext.Provider>,
);
expect(host.querySelector('[data-testid="admin-save-bar"]')).not.toBeNull();
expect(screen.getByTestId('page').querySelector('[data-testid="admin-save-bar"]')).toBeNull();
host.remove();
});
});