import { expect, test as base, type APIRequestContext } from '../../../../../../job-tracker-ui/node_modules/@playwright/test'; import { translations } from '../../../../../../job-tracker-ui/src/i18n/translations'; const apiUrl = 'http://localhost:5302/api'; const password = 'E2ePassword123!'; async function login(api: APIRequestContext, email: string) { expect((await api.post(`${apiUrl}/auth/login`, { data: { email, password } })).status()).toBe(200); const me = await api.get(`${apiUrl}/auth/me`); expect(me.status()).toBe(200); const csrf = (await api.storageState()).cookies.find(cookie => cookie.name === 'XSRF-TOKEN')!.value; return { 'X-CSRF-TOKEN': csrf, 'X-Session-Context': (await me.json()).sessionContext as string }; } const test = base.extend<{ headers: Record }>({ headers: async ({ playwright, context, page }, provide) => { const admin = await playwright.request.newContext(); try { const adminHeaders = await login(admin, 'e2e@example.test'); const email = `preference-diagnostic-${Date.now()}-${Math.random().toString(36).slice(2)}@example.test`; expect((await admin.post(`${apiUrl}/users`, { headers: adminHeaders, data: { email, password, roles: [] } })).status()).toBe(200); await context.addInitScript(() => localStorage.setItem('uiLanguage', 'en')); await context.route('**/*', route => { const url = new URL(route.request().url()); return ['localhost', '127.0.0.1'].includes(url.hostname) || ['data:', 'blob:', 'about:'].includes(url.protocol) ? route.continue() : route.abort(); }); await provide(await login(page.request, email)); } finally { await admin.dispose(); } }, }); test('[P5-15-01 diagnostic] an unsaved Rules draft survives switching to Notifications and back', async ({ page, headers }, info) => { expect((await page.request.get(`${apiUrl}/auth/me`, { headers })).status()).toBe(200); const before = await page.request.get(`${apiUrl}/rules`); expect(before.status()).toBe(200); const original = (await before.json()).appliedFollowUpDays as number; const draft = original === 5 ? 6 : 5; let ruleWrites = 0; page.on('request', request => { if (new URL(request.url()).pathname === '/api/rules' && request.method() === 'PUT') ruleWrites++; }); await page.goto('/settings'); await page.getByRole('tab', { name: 'Follow-ups', exact: true }).click(); await page.getByLabel('Applied: follow-up days', { exact: true }).fill(String(draft)); await page.getByRole('tab', { name: 'Notifications', exact: true }).click(); await expect(page.getByLabel('Email reminders for follow-ups', { exact: true })).toBeVisible(); await page.getByRole('tab', { name: 'Follow-ups', exact: true }).click(); const field = page.getByLabel('Applied: follow-up days', { exact: true }); await expect(field).toBeVisible(); // Wait for the remounted card's genuine read, without treating the original value as success. await expect(field).not.toHaveValue(''); const readback = await page.request.get(`${apiUrl}/rules`); expect(readback.status()).toBe(200); const persisted = (await readback.json()).appliedFollowUpDays; const visibleDraft = await field.inputValue(); await info.attach('unsaved-rule-tab-retention', { body: JSON.stringify({ original, draft, visibleDraft, persisted, ruleWrites, readbackStatus: readback.status() }), contentType: 'application/json' }); expect(ruleWrites).toBe(0); expect(persisted).toBe(original); expect(visibleDraft).toBe(String(draft)); }); test('[P5-15-01 diagnostic] native language and notification saves persist during concurrent report reads', async ({ page, context, headers }, info) => { test.setTimeout(90_000); const notification = await context.newPage(); const evidence: unknown[] = []; try { await page.goto('/settings'); await notification.goto('/settings'); await notification.getByRole('tab', { name: 'Notifications', exact: true }).click(); // Three alternating rounds reproduce concurrency; this is bounded diagnosis, not a load test. for (const language of ['nb', 'en', 'nb'] as const) { const current = language === 'nb' ? translations.en : translations.nb; const target = translations[language]; const enabled = language === 'en'; await notification.getByLabel(current.settingsNotificationsFollowUpReminders, { exact: true }).setChecked(enabled); await page.getByRole('combobox', { name: current.settingsPreferredLanguage, exact: true }).click(); const languageResponse = page.waitForResponse(response => response.url() === `${apiUrl}/auth/preferences/language` && response.request().method() === 'PUT'); const notificationResponse = notification.waitForResponse(response => response.url() === `${apiUrl}/notification-settings` && response.request().method() === 'PUT'); const readPaths = ['/jobapplications/stats', '/jobapplications/analytics-overview', '/jobapplications/analytics?months=6', '/notifications?limit=10']; const reads = readPaths.map(path => page.request.get(`${apiUrl}${path}`, { headers })); await Promise.all([ page.getByRole('option', { name: current[language === 'nb' ? 'settingsNorwegian' : 'settingsEnglish'], exact: true }).click(), notification.getByRole('button', { name: new RegExp(`^(${translations.en.settingsNotificationsSave}|${translations.nb.settingsNotificationsSave})$`) }).click(), ]); const [savedLanguage, savedNotification, ...readResponses] = await Promise.all([languageResponse, notificationResponse, ...reads]); const me = await page.request.get(`${apiUrl}/auth/me`, { headers }); const prefs = await page.request.get(`${apiUrl}/notification-settings`, { headers }); const languageReadback = me.ok() ? (await me.json()).uiLanguage : undefined; const notificationReadback = prefs.ok() ? (await prefs.json()).emailFollowUpRemindersEnabled : undefined; const errorsVisible = { language: await page.getByRole('alert').count(), notification: await notification.getByRole('alert').count() }; evidence.push({ requestedLanguage: language === 'nb' ? 'nb-NO' : 'en', requestedNotifications: enabled, languageStatus: savedLanguage.status(), notificationStatus: savedNotification.status(), reads: readPaths.map((path, index) => ({ path, status: readResponses[index].status() })), readback: { meStatus: me.status(), notificationStatus: prefs.status(), language: languageReadback, notifications: notificationReadback }, localLanguage: await page.locator('html').getAttribute('lang'), errorsVisible }); // Capture genuine 500s and persistence before asserting; no expected-failure or success mock. await info.attach(`preference-round-${evidence.length}`, { body: JSON.stringify(evidence[evidence.length - 1], null, 2), contentType: 'application/json' }); expect(savedLanguage.status()).toBe(204); expect(savedNotification.status()).toBe(200); for (const response of readResponses) expect(response.status()).toBe(200); expect(me.status()).toBe(200); expect(prefs.status()).toBe(200); expect(languageReadback).toBe(language === 'nb' ? 'nb-NO' : 'en'); expect(notificationReadback).toBe(enabled); await expect(page.getByRole('combobox', { name: target.settingsPreferredLanguage, exact: true })).toBeVisible(); } await page.reload(); await notification.reload(); await expect(page.locator('html')).toHaveAttribute('lang', 'nb-NO'); await notification.getByRole('tab', { name: translations.nb.settingsTabNotifications, exact: true }).click(); await expect(notification.getByLabel(translations.nb.settingsNotificationsFollowUpReminders, { exact: true })).not.toBeChecked(); } finally { await info.attach('native-preference-concurrency', { body: JSON.stringify(evidence, null, 2), contentType: 'application/json' }); await notification.close(); } });