import { test, expect } from '@playwright/test'; import { loginViaAPI, CONFIG, navigateTo, assertNotBroken, assertNoDebugText, testId, SELECTORS, } from './helpers'; // ============================================================================= // EMPTY STATES — Affichage des etats vides // ============================================================================= test.describe('EMPTY STATES — Affichage des etats vides @feature-empty-states', () => { // Fresh user credentials — registered in beforeAll so they have zero data const freshPassword = 'SecurePass123!@#'; let freshUserEmail: string; let freshUsername: string; test.beforeAll(async ({ request }) => { const ts = Date.now(); freshUserEmail = `e2e-empty-${ts}@veza.test`; freshUsername = `e2e_empty_${ts}`; const response = await request.post('/api/v1/auth/register', { data: { email: freshUserEmail, password: freshPassword, username: freshUsername, password_confirmation: freshPassword, }, }); if (response.ok()) { console.log(` Fresh user registered: ${freshUserEmail}`); } else { // If registration fails (e.g. endpoint shape differs), fall back to listener account console.log(` ⚠ Fresh user registration failed (${response.status()}), tests will adapt`); } }); /** * Helper: login as the fresh user. Falls back to listener if fresh user was not created. */ async function loginAsFreshUser(page: import('@playwright/test').Page): Promise { try { await loginViaAPI(page, freshUserEmail, freshPassword); // Verify we left /login if (page.url().includes('/login')) { throw new Error('Still on login page'); } return true; } catch { // Fallback: use listener account (may not have truly empty states) console.log(' ⚠ Falling back to listener account'); try { await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password); // Check that fallback login also succeeded if (page.url().includes('/login')) { console.log(' ⚠ Fallback login also failed — still on /login'); return false; } return true; } catch { console.log(' ⚠ Fallback login threw an error'); return false; } } } /** * Assert that an empty state component is visible on the page. * The app uses EmptyState with a title, description, and optional action button. */ async function assertEmptyState( page: import('@playwright/test').Page, options: { expectedTextPatterns?: RegExp[]; ctaPattern?: RegExp; allowContent?: boolean; } = {}, ): Promise<{ hasEmptyState: boolean; hasCta: boolean }> { await assertNotBroken(page); await assertNoDebugText(page); const body = await page.textContent('body') || ''; // Look for EmptyState component patterns const emptyStateComponent = page.locator('[class*="empty-state"], [class*="EmptyState"], [data-testid*="empty"]') .or(page.getByText(/no .* yet|aucun|vide|nothing|get started|pas encore/i).first()); const hasEmptyState = await emptyStateComponent.first().isVisible().catch(() => false); // Also check for common empty state text patterns const emptyTextPatterns = [ /no .* yet/i, /aucun/i, /nothing (here|found|to show)/i, /get started/i, /pas encore/i, /empty/i, /start by/i, /browse|discover|explore/i, ]; let hasEmptyText = false; for (const pattern of emptyTextPatterns) { if (pattern.test(body)) { hasEmptyText = true; break; } } // Check for additional expected patterns if (options.expectedTextPatterns) { for (const pattern of options.expectedTextPatterns) { const matches = pattern.test(body); if (matches) hasEmptyText = true; } } // Check for CTA button let hasCta = false; if (options.ctaPattern) { const ctaBtn = page.getByRole('button', { name: options.ctaPattern }) .or(page.getByRole('link', { name: options.ctaPattern })); hasCta = await ctaBtn.first().isVisible().catch(() => false); } return { hasEmptyState: hasEmptyState || hasEmptyText, hasCta }; } // --------------------------------------------------------------------------- // Individual empty state tests // --------------------------------------------------------------------------- test('01. Bibliotheque vide — message + CTA upload @critical', async ({ page }) => { const loggedIn = await loginAsFreshUser(page); if (!loggedIn) { console.log(' ⚠ Login failed — skipping'); return; } await navigateTo(page, '/library'); const { hasEmptyState, hasCta } = await assertEmptyState(page, { expectedTextPatterns: [/library|bibliothèque|tracks|upload/i], ctaPattern: /upload|importer|ajouter|add/i, }); console.log(` /library empty state: ${hasEmptyState ? '✓' : '✗'}`); console.log(` /library CTA button: ${hasCta ? '✓' : '✗'}`); // Page should not be blank const body = await page.textContent('body') || ''; expect(body.length).toBeGreaterThan(50); }); test('02. Playlists vides — message + CTA creer', async ({ page }) => { const loggedIn = await loginAsFreshUser(page); if (!loggedIn) { console.log(' ⚠ Login failed — skipping'); return; } await navigateTo(page, '/playlists'); const { hasEmptyState, hasCta } = await assertEmptyState(page, { expectedTextPatterns: [/playlist/i], ctaPattern: /créer|create|nouvelle|new/i, }); console.log(` /playlists empty state: ${hasEmptyState ? '✓' : '✗'}`); console.log(` /playlists CTA button: ${hasCta ? '✓' : '✗'}`); const body = await page.textContent('body') || ''; expect(body.length).toBeGreaterThan(50); }); test('03. Notifications vides — message approprie', async ({ page }) => { await loginAsFreshUser(page); await navigateTo(page, '/notifications'); const { hasEmptyState } = await assertEmptyState(page, { expectedTextPatterns: [/notification|aucune|no notification/i], }); console.log(` /notifications empty state: ${hasEmptyState ? '✓' : '✗'}`); await assertNotBroken(page); }); test('04. Feed vide — message + suggestion', async ({ page }) => { await loginAsFreshUser(page); await navigateTo(page, '/feed'); const { hasEmptyState } = await assertEmptyState(page, { expectedTextPatterns: [/feed|follow|suivre|discover|découvr/i], }); console.log(` /feed empty state: ${hasEmptyState ? '✓' : '✗'}`); // Page should load without crash await assertNotBroken(page); }); test('05. Recherche sans resultat — message "aucun resultat"', async ({ page }) => { await loginAsFreshUser(page); await navigateTo(page, '/search'); // Type a query that will return no results const searchInput = page.locator('input[role="combobox"][aria-label="Search"]') .or(page.getByPlaceholder(/search for tracks/i)) .or(page.locator(SELECTORS.searchInput)); if (await searchInput.first().isVisible().catch(() => false)) { await searchInput.first().fill('xyznoexist999zzz'); // Wait for debounce + network await page.waitForTimeout(2_000); } else { // Navigate with query param await navigateTo(page, '/search?q=xyznoexist999zzz'); await page.waitForTimeout(2_000); } const body = await page.textContent('body') || ''; const hasNoResults = /no results|aucun résultat|nothing found|no .* found/i.test(body); console.log(` Search no-results message: ${hasNoResults ? '✓' : '✗'}`); await assertNotBroken(page); }); test('06. Queue vide — message', async ({ page }) => { const loggedIn = await loginAsFreshUser(page); if (!loggedIn) { console.log(' ⚠ Login failed — skipping'); return; } await navigateTo(page, '/queue'); const { hasEmptyState } = await assertEmptyState(page, { expectedTextPatterns: [/queue|file d'attente|no tracks|aucun/i], }); console.log(` /queue empty state: ${hasEmptyState ? '✓' : '✗'}`); await assertNotBroken(page); }); test('07. Chat sans conversation — message + CTA', async ({ page }) => { await loginAsFreshUser(page); await navigateTo(page, '/chat'); const { hasEmptyState } = await assertEmptyState(page, { expectedTextPatterns: [/chat|conversation|message|channel/i], }); console.log(` /chat empty state: ${hasEmptyState ? '✓' : '✗'}`); await assertNotBroken(page); }); test('08. Wishlist vide — message + CTA browse', async ({ page }) => { const loggedIn = await loginAsFreshUser(page); if (!loggedIn) { console.log(' ⚠ Login failed — skipping'); return; } await navigateTo(page, '/wishlist'); const { hasEmptyState, hasCta } = await assertEmptyState(page, { expectedTextPatterns: [/wishlist|favoris|souhaits|no items/i], ctaPattern: /browse|parcourir|discover|explorer|marketplace/i, }); console.log(` /wishlist empty state: ${hasEmptyState ? '✓' : '✗'}`); console.log(` /wishlist CTA button: ${hasCta ? '✓' : '✗'}`); await assertNotBroken(page); }); test('09. Purchases vides — message', async ({ page }) => { await loginAsFreshUser(page); await navigateTo(page, '/purchases'); const { hasEmptyState } = await assertEmptyState(page, { expectedTextPatterns: [/purchase|achat|order|commande|no .* yet/i], }); console.log(` /purchases empty state: ${hasEmptyState ? '✓' : '✗'}`); await assertNotBroken(page); }); test('10. Analytics sans donnees — message ou graphe a zero (creator)', async ({ page }) => { // Use creator account for analytics, but a fresh creator would have no data // Try fresh user first, fallback to existing creator const loggedIn = await loginAsFreshUser(page); if (!loggedIn) { console.log(' ⚠ Login failed — skipping'); return; } await navigateTo(page, '/analytics'); const body = await page.textContent('body') || ''; // Analytics page may show zero-state graphs, empty messages, or redirect const hasEmptyAnalytics = /no data|aucune donnée|analytics|statistiques|0 plays|0 streams/i.test(body); const hasChartArea = await page.locator('canvas, svg, [class*="chart"], [class*="graph"]') .first().isVisible().catch(() => false); // At minimum, the page should not crash await assertNotBroken(page); console.log(` /analytics empty state text: ${hasEmptyAnalytics ? '✓' : '✗'}`); console.log(` /analytics chart area: ${hasChartArea ? '✓ (zero-state chart)' : '✗'}`); }); });