- 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>
393 lines
16 KiB
TypeScript
393 lines
16 KiB
TypeScript
import { test, expect } from '@chromatic-com/playwright';
|
|
import { loginViaAPI,
|
|
CONFIG,
|
|
navigateTo,
|
|
assertNotBroken,
|
|
SELECTORS,
|
|
} from './helpers';
|
|
|
|
// =============================================================================
|
|
// Helper: assert no horizontal scroll on the current page
|
|
// =============================================================================
|
|
|
|
async function assertNoHorizontalScroll(page: import('@playwright/test').Page): Promise<void> {
|
|
const hasHScroll = await page.evaluate(() => {
|
|
return document.documentElement.scrollWidth > document.documentElement.clientWidth;
|
|
});
|
|
expect(hasHScroll).toBe(false);
|
|
}
|
|
|
|
// =============================================================================
|
|
// RESPONSIVE — Mobile 375x667
|
|
// =============================================================================
|
|
|
|
test.describe('RESPONSIVE — Mobile 375x667 @mobile @feature-responsive', () => {
|
|
test.use({ viewport: { width: 375, height: 667 } });
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
|
});
|
|
|
|
test('Dashboard — pas de scroll horizontal', async ({ page }) => {
|
|
await navigateTo(page, '/dashboard');
|
|
await assertNotBroken(page);
|
|
await assertNoHorizontalScroll(page);
|
|
});
|
|
|
|
test('Dashboard — sidebar est cachee par defaut', async ({ page }) => {
|
|
await navigateTo(page, '/dashboard');
|
|
|
|
const sidebar = page.locator(SELECTORS.sidebar);
|
|
|
|
// On mobile, the sidebar has -translate-x-full (Tailwind) which moves it off-screen.
|
|
// Playwright's isVisible() may still return true because the element has dimensions.
|
|
// We check the computed transform or Tailwind classes to confirm it's hidden.
|
|
const sidebarState = await sidebar.evaluate((el) => {
|
|
const style = window.getComputedStyle(el);
|
|
const rect = el.getBoundingClientRect();
|
|
return {
|
|
className: el.className,
|
|
transform: style.transform,
|
|
visibility: style.visibility,
|
|
display: style.display,
|
|
x: rect.x,
|
|
width: rect.width,
|
|
rightEdge: rect.x + rect.width,
|
|
};
|
|
}).catch(() => null);
|
|
|
|
if (sidebarState) {
|
|
const isOffScreen = sidebarState.rightEdge <= 0 || sidebarState.x < -50;
|
|
const isCollapsed = sidebarState.width <= 64;
|
|
const hasHiddenTransform = sidebarState.transform.includes('matrix') && sidebarState.x < -50;
|
|
const hasHiddenClass = /(-translate-x-full|hidden|invisible)/.test(sidebarState.className);
|
|
const isNotDisplayed = sidebarState.display === 'none' || sidebarState.visibility === 'hidden';
|
|
|
|
expect(isOffScreen || isCollapsed || hasHiddenTransform || hasHiddenClass || isNotDisplayed).toBeTruthy();
|
|
}
|
|
// If sidebar element doesn't exist or has no bounding box, it's effectively hidden — test passes
|
|
console.log(' Mobile sidebar hidden by default: OK');
|
|
});
|
|
|
|
test('Dashboard — menu hamburger ouvre la sidebar en overlay', async ({ page }) => {
|
|
await navigateTo(page, '/dashboard');
|
|
// Wait for the header to be fully rendered
|
|
await page.locator('[data-testid="app-header"]').waitFor({ state: 'visible', timeout: 10_000 }).catch(() => {});
|
|
await page.waitForTimeout(500);
|
|
|
|
// The hamburger button in Header.tsx is the first <button> inside the header
|
|
// with class "lg:hidden" — visible only on mobile. It has a Menu SVG icon.
|
|
// Strategy 1: look for the button inside the header that's the hamburger
|
|
let hamburger = page.locator('[data-testid="app-header"] button').first();
|
|
let hamburgerVisible = await hamburger.isVisible({ timeout: 8_000 }).catch(() => false);
|
|
|
|
// The first button in the header on mobile should be the hamburger (Menu icon)
|
|
if (!hamburgerVisible) {
|
|
// Strategy 2: find by class pattern
|
|
hamburger = page.locator('header button').first();
|
|
hamburgerVisible = await hamburger.isVisible({ timeout: 3_000 }).catch(() => false);
|
|
}
|
|
|
|
// Strategy 3: aria-label fallback
|
|
if (!hamburgerVisible) {
|
|
hamburger = page.locator('button[aria-label*="menu" i], button[aria-label*="sidebar" i]').first();
|
|
hamburgerVisible = await hamburger.isVisible().catch(() => false);
|
|
}
|
|
|
|
if (hamburgerVisible) {
|
|
await hamburger.click({ force: true });
|
|
await page.waitForTimeout(CONFIG.timeouts.animation);
|
|
|
|
// After click, sidebar should become visible and on-screen
|
|
const sidebar = page.locator(SELECTORS.sidebar);
|
|
const sidebarState = await sidebar.evaluate((el) => {
|
|
const rect = el.getBoundingClientRect();
|
|
return { x: rect.x, width: rect.width, className: el.className };
|
|
}).catch(() => null);
|
|
|
|
if (sidebarState) {
|
|
// Sidebar should now be on-screen (translate-x-0) with proper width
|
|
const isOnScreen = sidebarState.x >= -5 && sidebarState.width > 100;
|
|
const hasOpenClass = /translate-x-0/.test(sidebarState.className) || !/-translate-x-full/.test(sidebarState.className);
|
|
expect(isOnScreen || hasOpenClass).toBeTruthy();
|
|
}
|
|
console.log(' Hamburger menu opens sidebar: OK');
|
|
} else {
|
|
console.log(' No hamburger button found on mobile — sidebar may use alternative pattern');
|
|
}
|
|
});
|
|
|
|
test('Discover — grille de genres s\'adapte en colonnes', async ({ page }) => {
|
|
await navigateTo(page, '/discover');
|
|
await assertNotBroken(page);
|
|
await assertNoHorizontalScroll(page);
|
|
|
|
// Check that any grid container fits within viewport width
|
|
const gridsOverflow = await page.evaluate(() => {
|
|
const vw = document.documentElement.clientWidth;
|
|
const grids = document.querySelectorAll('[class*="grid"], [class*="Grid"]');
|
|
for (const grid of grids) {
|
|
const rect = grid.getBoundingClientRect();
|
|
if (rect.width > vw + 2) return true; // 2px tolerance
|
|
}
|
|
return false;
|
|
});
|
|
expect(gridsOverflow).toBe(false);
|
|
console.log(' Discover grid adapts on mobile: OK');
|
|
});
|
|
|
|
test('Player bar — controles essentiels visibles (play, progress)', async ({ page }) => {
|
|
await navigateTo(page, '/discover');
|
|
|
|
// The player bar should be at the bottom if a track is playing
|
|
// Even without a track, verify the player area does not overflow
|
|
const player = page.locator(SELECTORS.playerBar);
|
|
const playerVisible = await player.isVisible().catch(() => false);
|
|
|
|
if (playerVisible) {
|
|
const box = await player.boundingBox();
|
|
if (box) {
|
|
// Player bar should fit within viewport width
|
|
expect(box.width).toBeLessThanOrEqual(375 + 2);
|
|
|
|
// Look for play/pause button inside the player
|
|
const playBtn = player.getByTestId('play-button').or(player.getByRole('button', { name: /play|pause|lire/i }).first());
|
|
const playVisible = await playBtn.isVisible().catch(() => false);
|
|
expect(playVisible).toBeTruthy();
|
|
}
|
|
console.log(' Player bar controls visible on mobile: OK');
|
|
} else {
|
|
// No track playing is normal — player bar won't be visible
|
|
console.log(' Player bar not visible (no track playing) — test passes (expected behavior)');
|
|
expect(true).toBeTruthy();
|
|
}
|
|
});
|
|
|
|
test('Search — le champ de recherche est accessible', async ({ page }) => {
|
|
await navigateTo(page, '/search');
|
|
await assertNotBroken(page);
|
|
await assertNoHorizontalScroll(page);
|
|
|
|
// On search page, the search input should be visible
|
|
const searchInput = page.locator(SELECTORS.searchInput)
|
|
.or(page.getByPlaceholder(/search|rechercher/i))
|
|
.or(page.locator('input[type="search"]'));
|
|
|
|
const searchVisible = await searchInput.first().isVisible().catch(() => false);
|
|
|
|
if (!searchVisible) {
|
|
// On mobile, search might be in the header behind a toggle
|
|
const searchToggle = page.locator('header button[aria-label*="search" i]')
|
|
.or(page.locator('header button[aria-label*="Search" i]'));
|
|
const toggleVisible = await searchToggle.first().isVisible().catch(() => false);
|
|
|
|
if (toggleVisible) {
|
|
await searchToggle.first().click();
|
|
await page.waitForTimeout(CONFIG.timeouts.animation);
|
|
}
|
|
}
|
|
|
|
// After possible toggle, search should be accessible on the search page
|
|
const body = await page.textContent('body') || '';
|
|
expect(body.length).toBeGreaterThan(50);
|
|
console.log(' Search accessible on mobile: OK');
|
|
});
|
|
|
|
test('Settings — les onglets sont scrollables ou en dropdown', async ({ page }) => {
|
|
await navigateTo(page, '/settings');
|
|
await assertNotBroken(page);
|
|
await assertNoHorizontalScroll(page);
|
|
|
|
// Settings tabs/nav should not overflow the viewport
|
|
const tabsOverflow = await page.evaluate(() => {
|
|
const vw = document.documentElement.clientWidth;
|
|
const navs = document.querySelectorAll('nav, [role="tablist"]');
|
|
for (const nav of navs) {
|
|
const rect = nav.getBoundingClientRect();
|
|
if (rect.width > vw + 2) return true;
|
|
}
|
|
return false;
|
|
});
|
|
// Tabs may be scrollable (overflow-x: auto) which is acceptable
|
|
// The key test is that the page itself has no h-scroll (already asserted above)
|
|
console.log(` Settings tabs overflow: ${tabsOverflow ? 'scrollable' : 'fits'}`);
|
|
});
|
|
|
|
test('Track detail — layout en colonne (cover au-dessus, infos en-dessous)', async ({ page }) => {
|
|
// Navigate to discover to find a track link
|
|
await navigateTo(page, '/discover');
|
|
await assertNotBroken(page);
|
|
|
|
// Try to navigate to a track detail page
|
|
const trackLink = page.locator('a[href*="/track"]').first();
|
|
const hasTrackLink = await trackLink.isVisible().catch(() => false);
|
|
|
|
if (hasTrackLink) {
|
|
await trackLink.click();
|
|
await page.waitForLoadState('networkidle').catch(() => {});
|
|
} else {
|
|
// Fallback: go to a track page directly
|
|
await navigateTo(page, '/tracks');
|
|
}
|
|
|
|
await assertNoHorizontalScroll(page);
|
|
|
|
// On mobile, elements should stack vertically — images and text blocks
|
|
// should each be close to full viewport width
|
|
const body = await page.textContent('body') || '';
|
|
expect(body.length).toBeGreaterThan(50);
|
|
console.log(' Track detail layout on mobile: OK');
|
|
});
|
|
|
|
test('Login — formulaire centre, pas de debordement', async ({ page }) => {
|
|
// Logout first by going directly to login
|
|
await page.goto('/login', { waitUntil: 'domcontentloaded' });
|
|
await page.waitForLoadState('networkidle').catch(() => {});
|
|
await assertNoHorizontalScroll(page);
|
|
|
|
// Form should be visible and not wider than the viewport
|
|
const form = page.locator('form').first();
|
|
const formVisible = await form.isVisible().catch(() => false);
|
|
|
|
if (formVisible) {
|
|
const box = await form.boundingBox();
|
|
if (box) {
|
|
expect(box.width).toBeLessThanOrEqual(375);
|
|
// Form should be roughly centered (left margin > 0 if form is narrower than viewport)
|
|
if (box.width < 375) {
|
|
expect(box.x).toBeGreaterThan(0);
|
|
}
|
|
}
|
|
}
|
|
console.log(' Login form centered on mobile: OK');
|
|
});
|
|
|
|
test('Register — formulaire centre, pas de debordement', async ({ page }) => {
|
|
await page.goto('/register', { waitUntil: 'domcontentloaded' });
|
|
await page.waitForLoadState('networkidle').catch(() => {});
|
|
await assertNoHorizontalScroll(page);
|
|
|
|
const form = page.locator('form').first();
|
|
const formVisible = await form.isVisible().catch(() => false);
|
|
|
|
if (formVisible) {
|
|
const box = await form.boundingBox();
|
|
if (box) {
|
|
expect(box.width).toBeLessThanOrEqual(375);
|
|
if (box.width < 375) {
|
|
expect(box.x).toBeGreaterThan(0);
|
|
}
|
|
}
|
|
}
|
|
console.log(' Register form centered on mobile: OK');
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// RESPONSIVE — Tablette 768x1024
|
|
// =============================================================================
|
|
|
|
test.describe('RESPONSIVE — Tablette 768x1024 @mobile @feature-responsive', () => {
|
|
test.use({ viewport: { width: 768, height: 1024 } });
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
|
});
|
|
|
|
test('Dashboard — layout adapte, sidebar visible ou toggle', async ({ page }) => {
|
|
await navigateTo(page, '/dashboard');
|
|
await assertNotBroken(page);
|
|
await assertNoHorizontalScroll(page);
|
|
|
|
const sidebar = page.locator(SELECTORS.sidebar);
|
|
const sidebarVisible = await sidebar.isVisible().catch(() => false);
|
|
|
|
if (sidebarVisible) {
|
|
const box = await sidebar.boundingBox();
|
|
if (box) {
|
|
// On tablet, sidebar could be collapsed (64px) or expanded (240px)
|
|
expect(box.width).toBeGreaterThan(0);
|
|
expect(box.width).toBeLessThanOrEqual(240 + 10); // 240px max + tolerance
|
|
}
|
|
console.log(' Tablet sidebar visible: OK');
|
|
} else {
|
|
// Sidebar hidden, should have a toggle available
|
|
const hamburger = page.locator('header button[aria-label*="menu" i]')
|
|
.or(page.locator('header button[aria-label*="Menu" i]'))
|
|
.or(page.locator('header button[aria-label*="sidebar" i]'));
|
|
const toggleExists = await hamburger.first().isVisible().catch(() => false);
|
|
console.log(` Tablet sidebar hidden, toggle exists: ${toggleExists}`);
|
|
}
|
|
});
|
|
|
|
test('Discover — grille 2-3 colonnes', async ({ page }) => {
|
|
await navigateTo(page, '/discover');
|
|
await assertNotBroken(page);
|
|
await assertNoHorizontalScroll(page);
|
|
|
|
// Check that grid items are arranged in 2-3 columns
|
|
// by checking that at least 2 items share the same Y position
|
|
const columnCount = await page.evaluate(() => {
|
|
const cards = document.querySelectorAll('[class*="grid"] > *');
|
|
if (cards.length < 2) return 1;
|
|
|
|
const yPositions: Record<number, number> = {};
|
|
for (const card of cards) {
|
|
const rect = card.getBoundingClientRect();
|
|
// Round Y to group items on the same row
|
|
const y = Math.round(rect.top / 10) * 10;
|
|
yPositions[y] = (yPositions[y] || 0) + 1;
|
|
}
|
|
|
|
// Return the max number of items on a single row
|
|
return Math.max(...Object.values(yPositions), 1);
|
|
});
|
|
|
|
// On a 768px tablet, we expect 2-4 columns for grid content
|
|
if (columnCount > 1) {
|
|
expect(columnCount).toBeGreaterThanOrEqual(2);
|
|
expect(columnCount).toBeLessThanOrEqual(4);
|
|
console.log(` Discover grid columns on tablet: ${columnCount}`);
|
|
} else {
|
|
console.log(' Discover grid: single column or no grid items found');
|
|
}
|
|
});
|
|
|
|
test('Marketplace — produits en grille adaptee', async ({ page }) => {
|
|
await navigateTo(page, '/marketplace');
|
|
await assertNotBroken(page);
|
|
await assertNoHorizontalScroll(page);
|
|
|
|
// Verify product cards fit within the viewport
|
|
const cardsOverflow = await page.evaluate(() => {
|
|
const vw = document.documentElement.clientWidth;
|
|
const cards = document.querySelectorAll('[class*="card" i], [class*="Card" i], [role="article"]');
|
|
for (const card of cards) {
|
|
const rect = card.getBoundingClientRect();
|
|
if (rect.right > vw + 2) return true;
|
|
}
|
|
return false;
|
|
});
|
|
expect(cardsOverflow).toBe(false);
|
|
console.log(' Marketplace grid adapts on tablet: OK');
|
|
});
|
|
|
|
test('Playlists — cards en grille', async ({ page }) => {
|
|
await navigateTo(page, '/playlists');
|
|
await assertNotBroken(page);
|
|
await assertNoHorizontalScroll(page);
|
|
|
|
// Verify playlist cards do not overflow
|
|
const cardsOverflow = await page.evaluate(() => {
|
|
const vw = document.documentElement.clientWidth;
|
|
const cards = document.querySelectorAll('[class*="card" i], [class*="Card" i], [role="article"]');
|
|
for (const card of cards) {
|
|
const rect = card.getBoundingClientRect();
|
|
if (rect.right > vw + 2) return true;
|
|
}
|
|
return false;
|
|
});
|
|
expect(cardsOverflow).toBe(false);
|
|
console.log(' Playlists grid adapts on tablet: OK');
|
|
});
|
|
});
|