import { test, expect } from '@playwright/test'; test('E2E 2FA Flow with German Localization and Dark Mode Email', async ({ page, context }) => { const username = 'admin_' + Date.now(); const email = username + '@example.com'; const password = 'admin'; test.setTimeout(120000); // Listen for console logs and errors page.on('console', msg => console.log(`BROWSER LOG: ${msg.text()} `)); page.on('pageerror', exception => console.log(`BROWSER ERROR: ${exception} \nStack: ${exception.stack} `)); page.on('response', async response => { if (response.status() >= 500) { console.log(`BROWSER RESP ${response.status()}: ` + await response.text().catch(() => 'No Body')); } }); // 1. Reset Settings to ensure SMTP works (Backdoor) await page.request.post('http://localhost:5001/api/debug/fix-settings'); // 1. Logic: Register or Login as Admin await page.goto('http://localhost:5001'); // Wait for Auth Page to load (Login text or Register button) try { const loginText = page.locator('text="Sign In"'); // Try finding "Sign In" or "Login" await expect(page.locator('form input[name="username"]')).toBeVisible({ timeout: 10000 }); } catch (e) { console.log('Not on login page directly? checking...'); } // Attempt to register first (fresh env) let isLoggedIn = false; try { // Check if we are on auth page by looking for Register tab btn const registerBtn = page.getByRole('button', { name: 'Register' }); // Check if username input is visible (meaning we are on Auth page) if (await page.locator('input[name="username"]').isVisible()) { if (await registerBtn.isVisible()) { await registerBtn.click(); await page.fill('input[name="username"]', username); await page.fill('input[name="email"]', email); await page.fill('input[name="password"]', password); await page.click('button[type="submit"]'); // Create Account // Wait a bit to see if we logged in try { await expect(page.locator('[data-testid="fab-create-task"]').or(page.locator('[data-testid="logged-in-debug"]'))).toBeVisible({ timeout: 2000 }); isLoggedIn = true; } catch (e) { console.log('Registration did not log us in immediately. checking for errors or trying login...'); } } if (!isLoggedIn) { // Try Login console.log('Switching to Login...'); const loginBtn = page.getByRole('button', { name: 'Login' }); // Tab button if (await loginBtn.isVisible()) await loginBtn.click(); await page.fill('input[name="username"]', username); await page.fill('input[name="password"]', password); await page.click('button[type="submit"]'); } } else { console.log('Username input not visible, assuming already logged in.'); isLoggedIn = true; } } catch (e) { console.log('Auth flow error:', e); } // Verify we are actually logged in OR hit 2FA try { // Check for FAB (Logged in) OR 2FA Header // Check for FAB (Logged in) OR Debug Div (LoggedIn) OR 2FA Header const fab = page.locator('[data-testid="fab-create-task"]').or(page.locator('[data-testid="logged-in-debug"]')); const twoFaHeader = page.locator('text=2FA Verification'); // English default const twoFaHeaderDe = page.locator('text=2FA Verifizierung'); // German // Wait for any of these await Promise.race([ expect(fab).toBeVisible({ timeout: 5000 }), expect(twoFaHeader).toBeVisible({ timeout: 5000 }), expect(twoFaHeaderDe).toBeVisible({ timeout: 5000 }) ]); if (await twoFaHeader.isVisible() || await twoFaHeaderDe.isVisible()) { console.log('Hit 2FA screen, solving...'); // Fetch Code await page.waitForTimeout(2000); const mailhogRes = await page.request.get('http://localhost:8025/api/v2/messages'); const messages = await mailhogRes.json(); const latestMessage = messages.items[0]; const body = latestMessage.Content.Body; const code = body.match(/\d{6}/)[0]; await page.fill('input[type="text"]', code); await page.click('button[type="submit"]'); // Verify // Now wait for FAB await expect(fab).toBeVisible({ timeout: 5000 }); } else { console.log('Logged in directly (No 2FA)'); } // Force Complete Routines to bypass Blocker console.log('Force completing routines...'); await page.request.post('http://localhost:5001/api/user/routine/morning/complete'); await page.request.post('http://localhost:5001/api/user/routine/evening/complete'); // Disable Global Routine Blocker via API (Admin Settings) console.log('Disabling Global Routine Blocker via API...'); await page.request.post('http://localhost:5001/api/admin/settings', { data: { evening_routine_enabled: "false", morning_routine_enabled: "false", smtp_host: "localhost", smtp_port: "1025", smtp_user: "", smtp_pass: "", smtp_from: "noreply@example.com", smtp_secure: "false" } }); // Reload to pick up new settings console.log('Reloading to apply settings...'); await page.reload(); await page.waitForTimeout(3000); // Wait for initialization } catch (e) { console.log('Current URL:', page.url()); throw new Error('Failed to login/register'); } // 2. Logic: Ensure 2FA is Enabled & Set Language to German await page.goto('http://localhost:5001/settings'); // Wait for loader to disappear await expect(page.locator('.animate-spin')).toHaveCount(0, { timeout: 10000 }); // Verify we are on settings page. try { await expect(page.locator('[data-testid="text-settings-title"]')).toBeVisible({ timeout: 10000 }); } catch (e) { console.log('Current URL:', page.url()); console.log('Page content snapshot:', await page.content()); throw e; } // Ensure Language is English first (to reliably find 2FA switch if using text) or use ID // Ensure Language is English first (to reliably find 2FA switch if using text) or use ID await page.addStyleTag({ content: 'vite-error-overlay { display: none !important; }' }); await page.click('[data-testid="select-language"]'); await page.click('[data-testid="option-language-en"]'); await page.waitForTimeout(500); // persist // Enable 2FA if not enabled // We can check the privacy API or just toggle. // Let's toggle it ON. // Switch ID/locating // In settings.tsx loop: // Use getByText to be more robust const twoFaText = page.getByText('Two-Factor Authentication', { exact: false }).first(); try { await expect(twoFaText).toBeVisible({ timeout: 5000 }); } catch (e) { console.log('2FA Text not found. Page content:'); console.log(await page.content()); throw e; } // Find switch in the same container. // Structure: div > [text], Switch // We can go up to parent div. const twoFaSwitch = page.locator('div.flex.items-center.justify-between').filter({ has: twoFaText }).getByRole('switch'); const isChecked = await twoFaSwitch.getAttribute('aria-checked') === 'true'; if (!isChecked) { console.log('Enabling 2FA...'); await twoFaSwitch.click(); await page.waitForTimeout(1000); // persist } // Set Language to German await page.click('[data-testid="select-language"]'); await page.click('[data-testid="option-language-de"]'); // Wait for persistence (API call) await page.waitForTimeout(1000); // 3. Logic: Logout // Use sidebar logout or header logout? // AppSidebar has logout button. // We need to trigger sidebar if mobile, or just find the button. // Button title="Logout" // Assuming Sidebar is visible (desktop) or we open it. // The test runs in desktop view by default in playwright config usually. const logoutBtn = page.locator('button[title="Logout"]'); // If not visible, might be in a menu. // Sidebar usually has it. if (await logoutBtn.isVisible()) { await logoutBtn.click(); } else { // Try finding by icon or text "Logout" / "Abmelden" // In German: "Abmelden"? // Let's use URL fallback if UI fails await page.goto('http://localhost:5001/api/logout'); // API logout returns 200 or redirect. // But client state needs to be cleared? // Better to use UI. // Find "Log out" text? // Sidebar footer user menu? // Let's try locating any button with Log out text (or German Abmelden) const logoutTextBtn = page.locator('button:has-text("Abmelden")'); if (await logoutTextBtn.isVisible()) { await logoutTextBtn.click(); } else { // Fallback: clear cookies/storage manually? // No, let's assume Sidebar is there. // Maybe checking 'button[data-testid="sidebar-logout"]' if added? // If failing, let's force navigate await page.goto('http://localhost:5001/auth?mode=login'); // But query cache might persist user? // The app checks /api/user. If cookie is gone, it returns 401. await context.clearCookies(); await page.reload(); } } await page.waitForURL(/.*\/auth/); // 4. Logic: Login again to trigger 2FA (Now in German context) await page.fill('input[name="username"]', username); await page.fill('input[name="password"]', password); // Capture response to get debug code if SMTP fails const loginResponsePromise = page.waitForResponse(response => response.url().includes('/api/login') && response.request().method() === 'POST'); await page.click('button[type="submit"]'); const loginResponse = await loginResponsePromise; const loginJson = await loginResponse.json(); const debugCode = loginJson.debugCode; // Verify 2FA Screen - Should have German title "2FA-Verifizierung" // Note: Translation key 'auth.2faVerification'. // Ensure we check for the GERMAN text. await expect(page.locator('text=2FA Verifizierung')).toBeVisible({ timeout: 5000 }); let code = debugCode; if (!code) { // 5. Logic: Fetch Code from MailHog // Use existing request context await page.waitForTimeout(2000); const mailhogRes2 = await page.request.get('http://localhost:8025/api/v2/messages'); const messages2 = await mailhogRes2.json(); const latestMessage2 = messages2.items[0]; // Verify Email Subject and Body (German) expect(latestMessage2.Content.Headers.Subject[0]).toContain('Ihr 2FA-Verifizierungscode'); const emailBody = latestMessage2.Content.Body; // Verify Dark Mode (HTML check) expect(emailBody).toContain('background-color: #09090b'); // Dark background expect(emailBody).toContain('#fafafa'); // Light text expect(emailBody).toContain('Verifizierungscode'); code = emailBody.match(/\d{6}/)[0]; console.log('Got German 2FA Code from MailHog:', code); } else { console.log('Using Debug 2FA Code from response:', code); } // 6. Logic: Enter Code await page.getByPlaceholder('123456').fill(code); // Button text might be "Überprüfen" or "Verifizieren" // We can use the button type submit, or look for the text. // In German de.json, auth.verify usually translates to "Verifizieren" or "Bestätigen" // Let's use the submit button generic locator since it's the only one await page.click('button[type="submit"]'); // 7. Logic: Success await page.waitForTimeout(3000); await expect(page).toHaveURL('http://localhost:5001/'); // 8. Restore Language to English for future tests (Optional) await page.goto('http://localhost:5001/settings'); await page.click('[data-testid="select-language"]'); await page.click('[data-testid="option-language-en"]'); });