@@ -2,6 +2,7 @@ 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/services/confirm.svelte.js' ;
vi . mock ( '$app/forms' , ( ) = > ( { enhance : ( ) = > ( ) = > { } } ) ) ;
vi . mock ( '$app/navigation' , ( ) = > ( { beforeNavigate : vi.fn ( ) , goto : vi . fn ( ) } ) ) ;
@@ -35,54 +36,65 @@ const baseData = {
groups
} ;
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 user page – rendering' , ( ) = > {
it ( 'renders the heading with username' , async ( ) = > {
render( Page , { data : baseData , form : null } ) ;
renderPage ( { data : baseData , form : null } ) ;
await expect . element ( page . getByText ( /Benutzer bearbeiten: max/i ) ) . toBeInTheDocument ( ) ;
} ) ;
it ( 'pre-fills first name from editUser data' , async ( ) = > {
render( Page , { data : baseData , form : null } ) ;
renderPage ( { data : baseData , form : null } ) ;
const input = document . querySelector < HTMLInputElement > ( 'input[name="firstName"]' ) ;
expect ( input ? . value ) . toBe ( 'Max' ) ;
} ) ;
it ( 'pre-fills last name from editUser data' , async ( ) = > {
render( Page , { data : baseData , form : null } ) ;
renderPage ( { data : baseData , form : null } ) ;
const input = document . querySelector < HTMLInputElement > ( 'input[name="lastName"]' ) ;
expect ( input ? . value ) . toBe ( 'Mustermann' ) ;
} ) ;
it ( 'pre-fills email from editUser data' , async ( ) = > {
render( Page , { data : baseData , form : null } ) ;
renderPage ( { data : baseData , form : null } ) ;
const input = document . querySelector < HTMLInputElement > ( 'input[name="email"]' ) ;
expect ( input ? . value ) . toBe ( 'max@example.com' ) ;
} ) ;
it ( 'pre-fills birth date in German format (dd.mm.yyyy)' , async ( ) = > {
render( Page , { data : baseData , form : null } ) ;
renderPage ( { data : baseData , form : null } ) ;
const input = document . querySelector < HTMLInputElement > ( 'input[placeholder="TT.MM.JJJJ"]' ) ;
expect ( input ? . value ) . toBe ( '22.03.1985' ) ;
} ) ;
it ( 'pre-fills contact field' , async ( ) = > {
render( Page , { data : baseData , form : null } ) ;
renderPage ( { data : baseData , form : null } ) ;
const textarea = document . querySelector < HTMLTextAreaElement > ( 'textarea[name="contact"]' ) ;
expect ( textarea ? . value ) . toBe ( 'Tel: 0123' ) ;
} ) ;
it ( 'renders group checkboxes' , async ( ) = > {
render( Page , { data : baseData , form : null } ) ;
renderPage ( { data : baseData , form : null } ) ;
await expect . element ( page . getByText ( 'Editoren' ) ) . toBeInTheDocument ( ) ;
await expect . element ( page . getByText ( 'Admins' ) ) . toBeInTheDocument ( ) ;
} ) ;
it ( 'pre-selects the groups the user already belongs to' , async ( ) = > {
render( Page , { data : baseData , form : null } ) ;
renderPage ( { data : baseData , form : null } ) ;
const checkbox = document . querySelector < HTMLInputElement > (
'input[type="checkbox"][name="groupIds"][value="g1"]'
) ;
@@ -90,7 +102,7 @@ describe('Admin edit user page – rendering', () => {
} ) ;
it ( 'does not pre-select groups the user does not belong to' , async ( ) = > {
render( Page , { data : baseData , form : null } ) ;
renderPage ( { data : baseData , form : null } ) ;
const checkbox = document . querySelector < HTMLInputElement > (
'input[type="checkbox"][name="groupIds"][value="g2"]'
) ;
@@ -98,7 +110,7 @@ describe('Admin edit user page – rendering', () => {
} ) ;
it ( 'includes pre-selected group ids in FormData at submit time (guards against groupIds being empty)' , async ( ) = > {
render( Page , { data : baseData , form : null } ) ;
renderPage ( { data : baseData , form : null } ) ;
const form = document . querySelector < HTMLFormElement > ( 'form#edit-user-form' ) ! ;
const formData = new FormData ( form ) ;
expect ( formData . getAll ( 'groupIds' ) ) . toContain ( 'g1' ) ;
@@ -106,7 +118,7 @@ describe('Admin edit user page – rendering', () => {
} ) ;
it ( 'password fields are empty by default' , async ( ) = > {
render( Page , { data : baseData , form : null } ) ;
renderPage ( { data : baseData , form : null } ) ;
const passwordInputs = document . querySelectorAll < HTMLInputElement > ( 'input[type="password"]' ) ;
passwordInputs . forEach ( ( input ) = > {
expect ( input . value ) . toBe ( '' ) ;
@@ -114,14 +126,14 @@ describe('Admin edit user page – rendering', () => {
} ) ;
it ( 'cancel link points to /admin/users' , async ( ) = > {
render( Page , { data : baseData , form : null } ) ;
renderPage ( { data : baseData , form : null } ) ;
await expect
. element ( page . getByRole ( 'link' , { name : /Abbrechen/i } ) )
. toHaveAttribute ( 'href' , '/admin/users' ) ;
} ) ;
it ( 'renders the save button' , async ( ) = > {
render( Page , { data : baseData , form : null } ) ;
renderPage ( { data : baseData , form : null } ) ;
await expect . element ( page . getByRole ( 'button' , { name : /Speichern/i } ) ) . toBeInTheDocument ( ) ;
} ) ;
} ) ;
@@ -130,33 +142,72 @@ describe('Admin edit user page – rendering', () => {
describe ( 'Admin edit user page – feedback' , ( ) = > {
it ( 'shows success message when form.success is true' , async ( ) = > {
render( Page , { data : baseData , form : { success : true } } ) ;
renderPage ( { data : baseData , form : { success : true } } ) ;
await expect . element ( page . getByText ( /Änderungen gespeichert/i ) ) . toBeInTheDocument ( ) ;
} ) ;
it ( 'shows error message when form.error is set' , async ( ) = > {
render( Page , { data : baseData , form : { error : 'Ungültige Eingabe.' } } ) ;
renderPage ( { data : baseData , form : { error : 'Ungültige Eingabe.' } } ) ;
await expect . element ( page . getByText ( 'Ungültige Eingabe.' ) ) . toBeInTheDocument ( ) ;
} ) ;
it ( 'does not show success message when form is null' , async ( ) = > {
render( Page , { data : baseData , form : null } ) ;
renderPage ( { data : baseData , form : null } ) ;
await expect . element ( page . getByText ( /Änderungen gespeichert/i ) ) . not . toBeInTheDocument ( ) ;
} ) ;
} ) ;
// ─── Delete confirmation ──────────────────────────────────────────────────────
describe ( 'Admin edit user 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 user page – unsaved-changes guard' , ( ) = > {
beforeEach ( ( ) = > vi . clearAllMocks ( ) ) ;
it ( 'does not show unsaved warning initially' , async ( ) = > {
render( Page , { data : baseData , form : null } ) ;
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 ( ) = > {
render( Page , { data : baseData , form : null } ) ;
renderPage ( { data : baseData , form : null } ) ;
const [ callback ] = vi . mocked ( beforeNavigate ) . mock . calls [ 0 ] ;
document
@@ -171,7 +222,7 @@ describe('Admin edit user page – unsaved-changes guard', () => {
} ) ;
it ( 'does not cancel navigation when form is clean' , async ( ) = > {
render( Page , { data : baseData , form : null } ) ;
renderPage ( { data : baseData , form : null } ) ;
const [ callback ] = vi . mocked ( beforeNavigate ) . mock . calls [ 0 ] ;
const cancel = vi . fn ( ) ;
@@ -181,7 +232,7 @@ describe('Admin edit user page – unsaved-changes guard', () => {
} ) ;
it ( 'discard button calls goto with the target URL' , async ( ) = > {
render( Page , { data : baseData , form : null } ) ;
renderPage ( { data : baseData , form : null } ) ;
const [ callback ] = vi . mocked ( beforeNavigate ) . mock . calls [ 0 ] ;
document
@@ -196,7 +247,7 @@ describe('Admin edit user page – unsaved-changes guard', () => {
} ) ;
it ( 'clears dirty state when form saves successfully' , async ( ) = > {
const { rerender } = render( Page , { data : baseData , form : null } ) ;
const { rerender } = renderPage ( { data : baseData , form : null } ) ;
const [ callback ] = vi . mocked ( beforeNavigate ) . mock . calls [ 0 ] ;
document