diff --git a/backend/src/main/java/org/raddatz/familienarchiv/config/DataInitializer.java b/backend/src/main/java/org/raddatz/familienarchiv/config/DataInitializer.java index f101f5a1..9f2b7b75 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/config/DataInitializer.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/config/DataInitializer.java @@ -84,6 +84,14 @@ public class DataInitializer { TagRepository tagRepo, PasswordEncoder passwordEncoder) { return args -> { + // Always reset the admin password to the configured value so a failed password-reset + // test from a previous run can never leave the account locked out. + userRepository.findByUsername(adminUsername).ifPresent(admin -> { + admin.setPassword(passwordEncoder.encode(adminPassword)); + userRepository.save(admin); + log.info("E2E seed: Admin-Passwort auf konfigurierten Wert zurückgesetzt."); + }); + // Always ensure the read-only test user exists, even when seed data was already loaded. if (userRepository.findByUsername("reader").isEmpty()) { log.info("E2E seed: Erstelle 'reader'-Testbenutzer..."); diff --git a/frontend/e2e/theme.spec.ts b/frontend/e2e/theme.spec.ts new file mode 100644 index 00000000..95b87392 --- /dev/null +++ b/frontend/e2e/theme.spec.ts @@ -0,0 +1,73 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Theme toggle', () => { + test.beforeEach(async ({ page }) => { + // Clear any saved theme preference before each test + await page.goto('/'); + await page.evaluate(() => localStorage.removeItem('theme')); + }); + + test('toggle button is visible in the header', async ({ page }) => { + await page.goto('/'); + await expect( + page.getByRole('banner').getByRole('button', { name: /dark mode|light mode/i }) + ).toBeVisible(); + }); + + test('clicking the toggle switches to dark mode', async ({ page }) => { + await page.goto('/'); + await page.waitForSelector('[data-hydrated]'); + + const html = page.locator('html'); + await expect(html).not.toHaveAttribute('data-theme', 'dark'); + + await page + .getByRole('banner') + .getByRole('button', { name: /dark mode/i }) + .click(); + + await expect(html).toHaveAttribute('data-theme', 'dark'); + }); + + test('clicking the toggle again switches back to light mode', async ({ page }) => { + await page.goto('/'); + await page.waitForSelector('[data-hydrated]'); + + await page + .getByRole('banner') + .getByRole('button', { name: /dark mode/i }) + .click(); + await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark'); + + await page + .getByRole('banner') + .getByRole('button', { name: /light mode/i }) + .click(); + await expect(page.locator('html')).toHaveAttribute('data-theme', 'light'); + }); + + test('theme persists after page reload', async ({ page }) => { + await page.goto('/'); + await page.waitForSelector('[data-hydrated]'); + + await page + .getByRole('banner') + .getByRole('button', { name: /dark mode/i }) + .click(); + await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark'); + + await page.reload(); + await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark'); + }); + + test('saved theme is applied before first paint (no flash)', async ({ page }) => { + // Set dark theme in localStorage before navigating + await page.goto('/'); + await page.evaluate(() => localStorage.setItem('theme', 'dark')); + + // Intercept the initial HTML to verify data-theme is set immediately + await page.goto('/'); + const theme = await page.evaluate(() => document.documentElement.getAttribute('data-theme')); + expect(theme).toBe('dark'); + }); +}); diff --git a/frontend/src/app.html b/frontend/src/app.html index 50bd0b52..69f27222 100644 --- a/frontend/src/app.html +++ b/frontend/src/app.html @@ -3,6 +3,12 @@
+ %sveltekit.head% diff --git a/frontend/src/lib/components/AnnotationCommentPanel.svelte b/frontend/src/lib/components/AnnotationCommentPanel.svelte index b1bb59fe..d863e4a1 100644 --- a/frontend/src/lib/components/AnnotationCommentPanel.svelte +++ b/frontend/src/lib/components/AnnotationCommentPanel.svelte @@ -25,16 +25,16 @@ let {