import { test, expect } from '@playwright/test'; import { loginViaAPI, CONFIG, navigateTo } from './helpers'; /** * DEEP — Notifications & Settings behavioural tests. * * Source components: * - apps/web/src/components/notifications/notification-menu/ * - apps/web/src/features/notifications/components/notifications-page/ * - apps/web/src/features/settings/pages/SettingsPage.tsx * - apps/web/src/features/settings/components/ (AccountSettings, PreferenceSettings, * NotificationSettings, PrivacySettings, PlaybackSettings, SettingsTabs, TwoFactorSettings) * * Settings API: GET/PUT /api/v1/users/settings * Notifications API: GET /api/v1/notifications, POST /api/v1/notifications/:id/read, * POST /api/v1/notifications/read-all */ const BASE = CONFIG.baseURL; // ============================================================================ // NOTIFICATIONS — Bell button (4 tests) // ============================================================================ test.describe('NOTIFICATIONS — Bell button', () => { test.beforeEach(async ({ page }) => { await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password); }); test('01. Bell button visible in header on authenticated pages @critical', async ({ page }) => { await navigateTo(page, '/dashboard'); const bellBtn = page.getByRole('button', { name: 'Notifications' }); await expect(bellBtn).toBeVisible({ timeout: 10_000 }); await expect(bellBtn).toHaveAttribute('aria-haspopup', 'true'); await expect(bellBtn).toHaveAttribute('aria-expanded', 'false'); }); test('02. Shows unread count badge when > 0', async ({ page }) => { await navigateTo(page, '/dashboard'); const bellBtn = page.getByRole('button', { name: 'Notifications' }); await expect(bellBtn).toBeVisible({ timeout: 10_000 }); // Badge is rendered when unreadCount > 0. It uses the exact aria-label // `${unreadCount} notifications non lues`. Absence => inbox empty (valid state). const badge = page.locator('[aria-label*="notifications non lues"]'); const hasBadge = await badge.isVisible({ timeout: 2_000 }).catch(() => false); if (hasBadge) { const badgeText = (await badge.textContent()) || ''; // Badge displays count or "9+" when > 9 expect(badgeText.trim()).toMatch(/^(\d+|9\+)$/); const ariaLabel = await badge.getAttribute('aria-label'); expect(ariaLabel).toMatch(/^\d+ notifications non lues$/); } else { // No badge => bell has no visible count child span const innerSpans = await bellBtn.locator('span').count(); expect(innerSpans).toBeLessThanOrEqual(1); } }); test('03. Click opens dropdown with motion.div.max-h-96', async ({ page }) => { await navigateTo(page, '/dashboard'); const bellBtn = page.getByRole('button', { name: 'Notifications' }); await expect(bellBtn).toBeVisible({ timeout: 10_000 }); await expect(bellBtn).toHaveAttribute('aria-expanded', 'false'); await bellBtn.click(); // Dropdown is a motion.div with specific classes const dropdown = page.locator('div.max-h-96.flex.flex-col').first(); await expect(dropdown).toBeVisible({ timeout: 5_000 }); // aria-expanded flips to true await expect(bellBtn).toHaveAttribute('aria-expanded', 'true'); }); test('04. Dropdown has header "Notifications" + list + footer', async ({ page }) => { await navigateTo(page, '/dashboard'); const bellBtn = page.getByRole('button', { name: 'Notifications' }); await bellBtn.click(); const dropdown = page.locator('div.max-h-96.flex.flex-col').first(); await expect(dropdown).toBeVisible({ timeout: 5_000 }); // Header h3 "Notifications" (text-sm semibold) const header = dropdown.locator('h3.font-semibold', { hasText: 'Notifications' }); await expect(header).toBeVisible(); // List region (overflow-y-auto) const list = dropdown.locator('div.overflow-y-auto.flex-1'); await expect(list).toBeVisible(); // Footer "Voir toutes les notifications" only when notifications exist, // empty state shows "Aucune notification" instead. const footerBtn = dropdown.getByRole('button', { name: /voir toutes les notifications/i }); const emptyState = dropdown.getByText('Aucune notification', { exact: true }); const hasFooter = await footerBtn.isVisible({ timeout: 1_500 }).catch(() => false); const hasEmpty = await emptyState.isVisible({ timeout: 1_500 }).catch(() => false); expect(hasFooter || hasEmpty).toBeTruthy(); }); }); // ============================================================================ // NOTIFICATIONS — List (5 tests) // ============================================================================ test.describe('NOTIFICATIONS — List', () => { test.beforeEach(async ({ page }) => { await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password); }); test('05. Dropdown shows recent notifications (max 50 from menu, display max 10)', async ({ page }) => { await navigateTo(page, '/dashboard'); const bellBtn = page.getByRole('button', { name: 'Notifications' }); await bellBtn.click(); const dropdown = page.locator('div.max-h-96.flex.flex-col').first(); await expect(dropdown).toBeVisible({ timeout: 5_000 }); // Items are rendered as