veza/tests/e2e/02-navigation.spec.ts
senke 6fad0ad68d fix: stabilize frontend — 98 TS errors to 0, align API endpoints, optimize bundle
- Fix 98 TypeScript errors across 37 files:
  - Service layer double-unwrapping (subscriptionService, distributionService, gearService)
  - Self-referencing variables in SearchPageResults
  - FeedView/ExploreView .posts→.items alignment
  - useQueueSync Zustand subscribe API
  - AdminAuditLogsView missing interface fields
  - Toast proxy type, interceptor type narrowing
  - 22 unused imports/variables removed
  - 5 storybook mock data fixes

- Align frontend API calls with backend endpoints:
  - Analytics: useAnalyticsView now calls /creator/analytics/dashboard (was /analytics)
  - Chat: chatService uses /conversations (was mock data), WS URL from backend token
  - Dashboard StatsSection: uses real /dashboard API data (was hardcoded zeros)
  - Settings: suppress 2FA toast error when endpoint unavailable

- Fix marketplace products: seed uses 'active' status (was 'published')
- Enrich seed: admin follows all creators (feed has content)

- Optimize bundle: vendor catch-all 793KB→318KB gzip (-60%)
  Split into vendor-charts, vendor-emoji, vendor-swagger, vendor-media, etc.

- Clean repo: remove ~100 orphaned screenshots, audit reports, logs from root

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 21:18:49 +01:00

225 lines
8.7 KiB
TypeScript

import { test, expect } from '@chromatic-com/playwright';
import { loginViaAPI, CONFIG, navigateTo, assertPageLoads, assertNoDebugText, assertNotBroken } from './helpers';
test.describe('NAVIGATION — Pages publiques (sans auth)', () => {
test('01. Page d\'accueil / redirige vers /dashboard ou /login', async ({ page }) => {
const errors = await assertPageLoads(page, '/');
expect(errors.length).toBeLessThan(3);
// Root / should redirect to /dashboard (if auth) or /login (if not)
await expect(page).toHaveURL(/dashboard|login/);
await assertNoDebugText(page);
});
test('02. Page /login se charge', async ({ page }) => {
await assertPageLoads(page, '/login');
});
test('03. Page /register se charge', async ({ page }) => {
await assertPageLoads(page, '/register');
});
test('04. Page /discover redirige vers /login si non authentifié', async ({ page }) => {
test.setTimeout(60_000);
await page.goto('/discover', { waitUntil: 'domcontentloaded' });
await page.waitForLoadState('networkidle').catch(() => {});
// /discover is a protected route, should redirect to login
// The app may take time to check auth and redirect
await expect(page).toHaveURL(/login/, { timeout: 20_000 });
});
test('05. Page 404 pour route inexistante', async ({ page }) => {
await navigateTo(page, '/this-page-does-not-exist-12345');
const body = await page.textContent('body');
// Should display a proper 404, not a crash
expect(body).toMatch(/404|not found|page.*introuvable|n'existe pas/i);
});
});
test.describe('NAVIGATION — Pages authentifiées', () => {
test.beforeEach(async ({ page }) => {
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
});
const authenticatedPages = [
{ path: '/dashboard', name: 'Dashboard' },
{ path: '/library', name: 'Bibliothèque' },
{ path: '/playlists', name: 'Playlists' },
{ path: '/notifications', name: 'Notifications' },
{ path: '/chat', name: 'Chat' },
{ path: '/settings', name: 'Paramètres' },
{ path: '/profile', name: 'Profil' },
{ path: '/feed', name: 'Feed' },
{ path: '/discover', name: 'Découverte' },
{ path: '/search', name: 'Recherche' },
];
for (const { path, name } of authenticatedPages) {
test(`06. Page ${name} (${path}) se charge @critical`, async ({ page }) => {
await navigateTo(page, path);
// No crash
const body = await page.textContent('body') || '';
expect(body).not.toMatch(/500|Internal Server Error|unexpected error|something went wrong/i);
// Page has content (not just an infinite spinner)
expect(body.length).toBeGreaterThan(100);
await assertNoDebugText(page);
});
}
});
test.describe('NAVIGATION — Layout principal', () => {
test.beforeEach(async ({ page }) => {
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
});
test('07. La sidebar est visible @critical', async ({ page }) => {
// Check login succeeded
await navigateTo(page, '/dashboard');
const sidebar = page.getByTestId('app-sidebar');
await expect(sidebar).toBeVisible({ timeout: 10_000 });
});
test('08. Le header est visible et le logo est dans la sidebar', async ({ page }) => {
await navigateTo(page, '/dashboard');
// Header has data-testid="app-header"
const header = page.locator('[data-testid="app-header"], header').first();
await expect(header).toBeVisible({ timeout: 5_000 });
// Logo "veza" is an h2 in the sidebar — it may be visually hidden when collapsed but still attached
const sidebar = page.getByTestId('app-sidebar');
await expect(sidebar).toBeVisible({ timeout: 5_000 });
// The h2 "veza" may be collapsed (opacity-0 max-w-0) but still in DOM
await expect(sidebar.locator('h2')).toBeAttached();
});
test('09. Les liens de navigation principaux sont présents et cliquables', async ({ page }) => {
await navigateTo(page, '/dashboard');
const navLinks = [
/dashboard/i,
/discover/i,
/library/i,
];
const sidebar = page.getByTestId('app-sidebar');
for (const linkText of navLinks) {
const link = sidebar.getByRole('link', { name: linkText })
.or(sidebar.getByRole('button', { name: linkText }))
.first();
const isVisible = await link.isVisible().catch(() => false);
console.log(` Nav "${linkText.source}": ${isVisible ? 'visible' : 'not found'}`);
}
});
test('10. Le player bar est visible en bas de page', async ({ page }) => {
await navigateTo(page, '/dashboard');
const playerBar = page.getByTestId('global-player');
// The player bar may not be visible until a track is playing
// But the container should exist in the DOM
const exists = await playerBar.isVisible().catch(() => false);
console.log(` Player bar visible: ${exists ? 'yes' : 'no (may be normal if nothing is playing)'}`);
});
test('10b. Le search est dans le header avec role="search"', async ({ page }) => {
await navigateTo(page, '/dashboard');
// Header search: data-testid="search-input" type="search" inside role="search" container
const searchInput = page.locator('[data-testid="search-input"]')
.or(page.locator('[role="search"] input'))
.or(page.locator('input[type="search"]'));
// Check it exists in DOM even if hidden on small viewports (hidden md:block)
await expect(searchInput.first()).toBeAttached({ timeout: 5_000 });
// On desktop viewport the search should be visible
const isVisible = await searchInput.first().isVisible().catch(() => false);
console.log(` Search input visible: ${isVisible ? 'yes' : 'no (hidden on mobile viewport)'}`);
});
});
test.describe('NAVIGATION — Responsive mobile @mobile', () => {
test.beforeEach(async ({ page }) => {
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
});
test('11. La page d\'accueil est utilisable sur mobile', async ({ page }) => {
await navigateTo(page, '/dashboard');
// No horizontal scroll (sign of broken layout)
const hasHorizontalScroll = await page.evaluate(() => {
return document.documentElement.scrollWidth > document.documentElement.clientWidth;
});
expect(hasHorizontalScroll).toBeFalsy();
});
test('12. Le menu hamburger fonctionne sur mobile', async ({ page }) => {
await navigateTo(page, '/dashboard');
const menuButton = page.getByRole('button', { name: /menu/i })
.or(page.locator('[class*="hamburger"]'))
.or(page.locator('[class*="menu-toggle"]'))
.or(page.getByTestId('mobile-menu'));
if (await menuButton.isVisible().catch(() => false)) {
await menuButton.click();
// Menu should open
const sidebar = page.getByTestId('app-sidebar');
await expect(sidebar).toBeVisible({ timeout: 3_000 });
}
});
});
test.describe('NAVIGATION — Internationalisation (i18n)', () => {
test('13. Changement de langue FR → EN', async ({ page }) => {
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
await navigateTo(page, '/settings');
// Find the language selector
const langSelector = page.getByLabel(/langue|language/i)
.or(page.locator('select[name*="lang"]'))
.or(page.getByTestId('language-selector'));
if (await langSelector.isVisible().catch(() => false)) {
await langSelector.selectOption({ label: /english/i });
await page.waitForTimeout(1_000);
// Verify English text appears
const body = await page.textContent('body') || '';
expect(body).toMatch(/settings|profile|account|logout/i);
} else {
console.log(' Language selector not found in /settings');
}
});
test('14. Pas de clés i18n brutes visibles (ex: "auth.login.title")', async ({ page }) => {
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
const pagesToCheck = ['/dashboard', '/discover', '/settings', '/library'];
for (const path of pagesToCheck) {
await navigateTo(page, path);
const body = await page.textContent('body') || '';
// Pattern: "word.word.word" that looks like an untranslated i18n key
const i18nKeyPattern = /\b[a-z]+\.[a-z]+\.[a-z]+\b/g;
const matches = body.match(i18nKeyPattern) || [];
// Filter false positives (URLs, etc.)
const suspiciousKeys = matches.filter(m =>
!m.includes('http') && !m.includes('www') && !m.includes('com') &&
!m.includes('min') && !m.includes('max') && m.length < 50
);
if (suspiciousKeys.length > 5) {
console.warn(` ${path}: ${suspiciousKeys.length} potentially untranslated i18n keys: ${suspiciousKeys.slice(0, 5).join(', ')}`);
}
}
});
});