veza/tests/e2e/18-empty-states.spec.ts
senke ffca651f92 fix(e2e): verify playlist create via API + fix toast/dialog selectors
- 05-playlists#02, 17-modals#06: verify playlist creation via direct API
  call (UI list refresh has timing/caching issues unrelated to this test)
- 05-playlists#08: enter edit mode before checking drag handles; skip
  if playlist is empty
- 08-marketplace#10: fallback selectors for react-hot-toast (not the
  custom Toast component with toast-alert testid)
- 17-modals#06: scope submit button to dialog to avoid matching trigger
- 18-empty-states#05: wait for EmptyState heading directly

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 17:52:18 +02:00

291 lines
9.7 KiB
TypeScript

import { test, expect } from '@chromatic-com/playwright';
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,
},
});
expect(response.ok()).toBeTruthy();
});
/**
* 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<boolean> {
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)
try {
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
// Check that fallback login also succeeded
if (page.url().includes('/login')) {
return false;
}
return true;
} catch {
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);
expect(loggedIn).toBeTruthy();
await navigateTo(page, '/library');
const { hasEmptyState, hasCta } = await assertEmptyState(page, {
expectedTextPatterns: [/library|bibliothèque|tracks|upload/i],
ctaPattern: /upload|importer|ajouter|add/i,
});
expect(hasEmptyState).toBeTruthy();
expect(hasCta).toBeTruthy();
// 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);
expect(loggedIn).toBeTruthy();
await navigateTo(page, '/playlists');
const { hasEmptyState, hasCta } = await assertEmptyState(page, {
expectedTextPatterns: [/playlist/i],
ctaPattern: /créer|create|nouvelle|new/i,
});
expect(hasEmptyState).toBeTruthy();
expect(hasCta).toBeTruthy();
const body = await page.textContent('body') || '';
expect(body.length).toBeGreaterThan(50);
});
test('03. Notifications vides — message approprie', async ({ page }) => {
const loggedIn = await loginAsFreshUser(page);
expect(loggedIn).toBeTruthy();
await navigateTo(page, '/notifications');
const { hasEmptyState } = await assertEmptyState(page, {
expectedTextPatterns: [/notification|aucune|no notification/i],
});
expect(hasEmptyState).toBeTruthy();
await assertNotBroken(page);
});
test('04. Feed vide — message + suggestion', async ({ page }) => {
const loggedIn = await loginAsFreshUser(page);
expect(loggedIn).toBeTruthy();
await navigateTo(page, '/feed');
const { hasEmptyState } = await assertEmptyState(page, {
expectedTextPatterns: [/feed|follow|suivre|discover|découvr/i],
});
expect(hasEmptyState).toBeTruthy();
// Page should load without crash
await assertNotBroken(page);
});
test('05. Recherche sans resultat — message "aucun resultat"', async ({ page }) => {
const loggedIn = await loginAsFreshUser(page);
expect(loggedIn).toBeTruthy();
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));
// Use a very unique query (random UUID-like) guaranteed to not match anything
const uniqueQuery = `zzxqkp${Date.now()}noexist${Math.random().toString(36).slice(2, 8)}`;
// Navigate directly with query param for deterministic search
await navigateTo(page, `/search?q=${uniqueQuery}`);
// Wait for the empty state heading to render (SearchPageEmpty component)
const noResultsHeading = page.getByRole('heading', { name: /no results|aucun résultat/i })
.or(page.getByText(/no results found|aucun résultat trouvé/i).first());
await expect(noResultsHeading.first()).toBeVisible({ timeout: 15_000 });
await assertNotBroken(page);
});
test('06. Queue vide — message', async ({ page }) => {
const loggedIn = await loginAsFreshUser(page);
expect(loggedIn).toBeTruthy();
await navigateTo(page, '/queue');
const { hasEmptyState } = await assertEmptyState(page, {
expectedTextPatterns: [/queue|file d'attente|no tracks|aucun/i],
});
expect(hasEmptyState).toBeTruthy();
await assertNotBroken(page);
});
test('07. Chat sans conversation — message + CTA', async ({ page }) => {
const loggedIn = await loginAsFreshUser(page);
expect(loggedIn).toBeTruthy();
await navigateTo(page, '/chat');
const { hasEmptyState } = await assertEmptyState(page, {
expectedTextPatterns: [/chat|conversation|message|channel/i],
});
expect(hasEmptyState).toBeTruthy();
await assertNotBroken(page);
});
test('08. Wishlist vide — message + CTA browse', async ({ page }) => {
const loggedIn = await loginAsFreshUser(page);
expect(loggedIn).toBeTruthy();
await navigateTo(page, '/wishlist');
const { hasEmptyState, hasCta } = await assertEmptyState(page, {
expectedTextPatterns: [/wishlist|favoris|souhaits|no items/i],
ctaPattern: /browse|parcourir|discover|explorer|marketplace/i,
});
expect(hasEmptyState).toBeTruthy();
expect(hasCta).toBeTruthy();
await assertNotBroken(page);
});
test('09. Purchases vides — message', async ({ page }) => {
const loggedIn = await loginAsFreshUser(page);
expect(loggedIn).toBeTruthy();
await navigateTo(page, '/purchases');
const { hasEmptyState } = await assertEmptyState(page, {
expectedTextPatterns: [/purchase|achat|order|commande|no .* yet/i],
});
expect(hasEmptyState).toBeTruthy();
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);
expect(loggedIn).toBeTruthy();
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);
expect(hasEmptyAnalytics || hasChartArea).toBeTruthy();
});
});