import { expect, test } from '@playwright/test'; import { login, DEV_PASSWORD } from './helpers'; // Uses max@pablan.dev so a failure here cannot lock the other specs out of // pablo@pablan.dev. The password is changed back at the end (zero residue). const NEW_PASSWORD = 'ben-neues-geheimnis'; test('a user changes their own password and stays signed in', async ({ page }) => { await login(page, 'max@pablan.dev'); await page.getByTestId('user-menu').click(); await page.getByTestId('change-password').click(); await page.getByLabel('Current password').fill(DEV_PASSWORD); await page.getByLabel('New password', { exact: true }).fill(NEW_PASSWORD); await page.getByLabel('Repeat new password').fill(NEW_PASSWORD); await page.getByTestId('submit-password').click(); await expect(page.getByTestId('password-changed')).toBeVisible({ timeout: 15_000 }); // This session survived the change — no redirect to the login page. await page.reload(); await page.locator('body[data-hydrated]').waitFor(); await expect(page).toHaveURL('/'); // The new password is the one that works now. await page.request.post('/api/auth/logout'); const stale = await page.request.post('/api/auth/login', { data: { email: 'max@pablan.dev', password: DEV_PASSWORD } }); expect(stale.status()).toBe(401); // Change it back, so the suite can run again. await login(page, 'max@pablan.dev', NEW_PASSWORD); await page.getByTestId('user-menu').click(); await page.getByTestId('change-password').click(); await page.getByLabel('Current password').fill(NEW_PASSWORD); await page.getByLabel('New password', { exact: true }).fill(DEV_PASSWORD); await page.getByLabel('Repeat new password').fill(DEV_PASSWORD); await page.getByTestId('submit-password').click(); await expect(page.getByTestId('password-changed')).toBeVisible({ timeout: 15_000 }); await page.request.post('/api/auth/logout'); }); test('the wrong current password is rejected', async ({ page }) => { await login(page, 'max@pablan.dev'); await page.getByTestId('user-menu').click(); await page.getByTestId('change-password').click(); await page.getByLabel('Current password').fill('definitely-wrong'); await page.getByLabel('New password', { exact: true }).fill(NEW_PASSWORD); await page.getByLabel('Repeat new password').fill(NEW_PASSWORD); await page.getByTestId('submit-password').click(); await expect(page.getByRole('alert')).toContainText('current password'); // The old password still works. await page.request.post('/api/auth/logout'); const still = await page.request.post('/api/auth/login', { data: { email: 'max@pablan.dev', password: DEV_PASSWORD } }); expect(still.status()).toBe(200); await page.request.post('/api/auth/logout'); }); test('the theme survives a reload without flashing the other one', async ({ page }) => { await login(page, 'max@pablan.dev'); await page.getByTestId('user-menu').click(); await page.getByTestId('settings-dialog').waitFor(); await page.getByTestId('theme-switch').getByRole('button', { name: 'Light' }).click(); await expect(page.locator('html')).toHaveAttribute('data-theme', 'light'); // The boot script applies it before hydration, so it is already correct // on the very first frame after a reload. await page.reload(); await expect(page.locator('html')).toHaveAttribute('data-theme', 'light'); await page.locator('body[data-hydrated]').waitFor(); await expect(page.locator('html')).toHaveAttribute('data-theme', 'light'); // Back to following the OS, so the next spec starts clean. await page.getByTestId('user-menu').click(); await page.getByTestId('theme-switch').getByRole('button', { name: 'System' }).click(); await expect(page.locator('html')).not.toHaveAttribute('data-theme', /.*/); await page.request.post('/api/auth/logout'); }); test('the language switch flips the interface in place, with no reload', async ({ page }) => { await login(page, 'max@pablan.dev'); await page.getByTestId('user-menu').click(); const dialog = page.getByTestId('settings-dialog'); const locales = page.getByTestId('locale-switch'); // Start from English, whatever a previous run left behind. await locales.getByRole('button', { name: 'English' }).click(); await expect(dialog).toContainText('Settings'); await expect(page.locator('html')).toHaveAttribute('lang', 'en'); // A marker on window survives a re-render but not a reload, and a typed // value survives neither if the DOM is rebuilt: together they are the // assertion that "no reload artifacts" actually holds. await page.evaluate(() => ((window as unknown as Record).__i18nMarker = 'alive')); await page.getByTestId('change-password').click(); await page.getByLabel('Current password').fill('typed-before-switch'); await locales.getByRole('button', { name: 'Deutsch' }).click(); await expect(dialog).toContainText('Einstellungen'); await expect(dialog).toContainText('Passwort ändern'); await expect(page.locator('html')).toHaveAttribute('lang', 'de'); expect( await page.evaluate(() => (window as unknown as Record).__i18nMarker) ).toBe('alive'); await expect(page.getByLabel('Aktuelles Passwort')).toHaveValue('typed-before-switch'); // The choice is on the account, not just in the tab. await expect .poll(async () => (await (await page.request.get('/api/auth/me')).json()).locale) .toBe('de'); // Zero residue: back to following the browser. await locales.getByRole('button', { name: 'Automatisch' }).click(); await expect .poll(async () => (await (await page.request.get('/api/auth/me')).json()).locale) .toBe(null); await page.request.post('/api/auth/logout'); });