Files
familienarchiv/frontend/src/routes/admin/groups/[id]/page.svelte.spec.ts
2026-05-05 14:35:15 +02:00

201 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
import Page from './+page.svelte';
import { createConfirmService, CONFIRM_KEY } from '$lib/shared/services/confirm.svelte.js';
vi.mock('$app/forms', () => ({ enhance: () => () => {} }));
vi.mock('$app/navigation', () => ({ beforeNavigate: vi.fn(), goto: vi.fn() }));
import { beforeNavigate, goto } from '$app/navigation';
const baseGroup = { id: 'g1', name: 'Editoren', permissions: ['WRITE_ALL'] };
const baseData = { group: baseGroup };
type PageProps = { data: typeof baseData; form: Record<string, unknown> | null };
function renderPage(props: PageProps) {
const service = createConfirmService();
const result = render(Page, {
props,
context: new Map([[CONFIRM_KEY, service]])
});
return { ...result, service };
}
afterEach(cleanup);
// ─── Rendering ────────────────────────────────────────────────────────────────
describe('Admin edit group page rendering', () => {
it('renders the heading with group name', async () => {
renderPage({ data: baseData, form: null });
await expect.element(page.getByText(/Gruppe: Editoren/i)).toBeInTheDocument();
});
it('pre-fills the name input', async () => {
renderPage({ data: baseData, form: null });
const input = document.querySelector<HTMLInputElement>('input[name="name"]');
expect(input?.value).toBe('Editoren');
});
it('pre-checks permissions that the group already has', async () => {
renderPage({ data: baseData, form: null });
const checkbox = document.querySelector<HTMLInputElement>(
'input[type="checkbox"][name="permissions"][value="WRITE_ALL"]'
);
expect(checkbox?.checked).toBe(true);
});
it('renders the cancel link pointing to /admin/groups', async () => {
renderPage({ data: baseData, form: null });
await expect
.element(page.getByRole('link', { name: /Abbrechen/i }))
.toHaveAttribute('href', '/admin/groups');
});
it('renders a READ_ALL checkbox in the standard permissions section', async () => {
renderPage({ data: baseData, form: null });
const cb = document.querySelector<HTMLInputElement>(
'input[type="checkbox"][name="permissions"][value="READ_ALL"]'
);
expect(cb).not.toBeNull();
});
it('renders an ANNOTATE_ALL checkbox in the standard permissions section', async () => {
renderPage({ data: baseData, form: null });
const cb = document.querySelector<HTMLInputElement>(
'input[type="checkbox"][name="permissions"][value="ANNOTATE_ALL"]'
);
expect(cb).not.toBeNull();
});
it('pre-checks READ_ALL when group has it', async () => {
const data = { group: { id: 'g2', name: 'Leser', permissions: ['READ_ALL'] } };
renderPage({ data, form: null });
const cb = document.querySelector<HTMLInputElement>(
'input[type="checkbox"][name="permissions"][value="READ_ALL"]'
);
expect(cb?.checked).toBe(true);
});
it('pre-checks ANNOTATE_ALL when group has it', async () => {
const data = {
group: { id: 'g3', name: 'Annotatoren', permissions: ['READ_ALL', 'ANNOTATE_ALL'] }
};
renderPage({ data, form: null });
const cb = document.querySelector<HTMLInputElement>(
'input[type="checkbox"][name="permissions"][value="ANNOTATE_ALL"]'
);
expect(cb?.checked).toBe(true);
});
});
// ─── Delete confirmation ──────────────────────────────────────────────────────
describe('Admin edit group page delete confirmation', () => {
it('delete button has type=button (does not submit natively)', async () => {
renderPage({ data: baseData, form: null });
const deleteForm = document.querySelector<HTMLFormElement>('form[action="?/delete"]')!;
const deleteBtn = deleteForm.querySelector('button') as HTMLButtonElement;
expect(deleteBtn.type).toBe('button');
});
it('does not submit delete form when user cancels', async () => {
const { service } = renderPage({ data: baseData, form: null });
const deleteForm = document.querySelector<HTMLFormElement>('form[action="?/delete"]')!;
const requestSubmit = vi.spyOn(deleteForm, 'requestSubmit').mockImplementation(() => {});
const deleteBtn = deleteForm.querySelector('button[type="button"]') as HTMLButtonElement;
deleteBtn.click();
await vi.waitFor(() => expect(service.options).not.toBeNull());
service.settle(false);
await vi.waitFor(() => expect(service.options).toBeNull());
expect(requestSubmit).not.toHaveBeenCalled();
});
it('submits delete form when user confirms', async () => {
const { service } = renderPage({ data: baseData, form: null });
const deleteForm = document.querySelector<HTMLFormElement>('form[action="?/delete"]')!;
const requestSubmit = vi.spyOn(deleteForm, 'requestSubmit').mockImplementation(() => {});
const deleteBtn = deleteForm.querySelector('button[type="button"]') as HTMLButtonElement;
deleteBtn.click();
await vi.waitFor(() => expect(service.options).not.toBeNull());
service.settle(true);
await vi.waitFor(() => expect(service.options).toBeNull());
expect(requestSubmit).toHaveBeenCalledOnce();
});
});
// ─── Unsaved-changes guard ────────────────────────────────────────────────────
describe('Admin edit group page unsaved-changes guard', () => {
beforeEach(() => vi.clearAllMocks());
it('does not show unsaved warning initially', async () => {
renderPage({ data: baseData, form: null });
await expect.element(page.getByText(/ungespeicherte Änderungen/i)).not.toBeInTheDocument();
});
it('cancels navigation and shows warning when form is dirty', async () => {
renderPage({ data: baseData, form: null });
const [callback] = vi.mocked(beforeNavigate).mock.calls[0];
document
.querySelector<HTMLInputElement>('input[name="name"]')!
.dispatchEvent(new InputEvent('input', { bubbles: true }));
const cancel = vi.fn();
callback({ cancel, to: { url: new URL('http://localhost/admin/groups/g2') } });
expect(cancel).toHaveBeenCalled();
await expect.element(page.getByText(/ungespeicherte Änderungen/i)).toBeInTheDocument();
});
it('does not cancel navigation when form is clean', async () => {
renderPage({ data: baseData, form: null });
const [callback] = vi.mocked(beforeNavigate).mock.calls[0];
const cancel = vi.fn();
callback({ cancel, to: { url: new URL('http://localhost/admin/groups/g2') } });
expect(cancel).not.toHaveBeenCalled();
});
it('discard button calls goto with the target URL', async () => {
renderPage({ data: baseData, form: null });
const [callback] = vi.mocked(beforeNavigate).mock.calls[0];
document
.querySelector<HTMLInputElement>('input[name="name"]')!
.dispatchEvent(new InputEvent('input', { bubbles: true }));
callback({ cancel: vi.fn(), to: { url: new URL('http://localhost/admin/groups/g2') } });
await page.getByRole('button', { name: /verwerfen/i }).click();
expect(vi.mocked(goto)).toHaveBeenCalledWith('http://localhost/admin/groups/g2');
});
it('clears dirty state when form saves successfully', async () => {
const { rerender } = renderPage({ data: baseData, form: null });
const [callback] = vi.mocked(beforeNavigate).mock.calls[0];
document
.querySelector<HTMLInputElement>('input[name="name"]')!
.dispatchEvent(new InputEvent('input', { bubbles: true }));
callback({ cancel: vi.fn(), to: { url: new URL('http://localhost/admin/groups/g2') } });
await expect.element(page.getByText(/ungespeicherte Änderungen/i)).toBeInTheDocument();
await rerender({ data: baseData, form: { success: true } });
const cancel = vi.fn();
callback({ cancel, to: { url: new URL('http://localhost/admin/groups/g2') } });
expect(cancel).not.toHaveBeenCalled();
});
});