- 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>
197 lines
7.2 KiB
TypeScript
197 lines
7.2 KiB
TypeScript
import { test, expect } from '@chromatic-com/playwright';
|
|
import { loginViaAPI, CONFIG, navigateTo, playFirstTrack } from './helpers';
|
|
|
|
/**
|
|
* VISUAL BUGS — Tests ciblés pour prévenir les bugs visuels
|
|
* Touch targets, images cassées, overflow, contraste
|
|
*/
|
|
|
|
test.describe('VISUAL — Touch targets mobile @visual @a11y @mobile', () => {
|
|
test.use({ viewport: { width: 375, height: 667 } });
|
|
|
|
test('Player controls — touch targets ≥ 44x44px', async ({ page }) => {
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
|
await navigateTo(page, '/discover');
|
|
await playFirstTrack(page);
|
|
await page.waitForTimeout(2000);
|
|
|
|
const playerBar = page.getByTestId('player-bar').or(page.getByTestId('global-player'));
|
|
if (await playerBar.isVisible({ timeout: 5000 }).catch(() => false)) {
|
|
const buttons = await playerBar.locator('button').all();
|
|
const tooSmall: string[] = [];
|
|
|
|
for (const btn of buttons) {
|
|
if (await btn.isVisible().catch(() => false)) {
|
|
const box = await btn.boundingBox();
|
|
if (box && (box.width < 32 || box.height < 32)) {
|
|
const label = await btn.getAttribute('aria-label') || await btn.textContent() || 'unknown';
|
|
tooSmall.push(`${label}: ${box.width}x${box.height}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (tooSmall.length > 0) {
|
|
console.warn(`⚠ Small touch targets: ${tooSmall.join(', ')}`);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('Sidebar — pas de débordement horizontal sur mobile', async ({ page }) => {
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
|
await navigateTo(page, '/dashboard');
|
|
|
|
const hasOverflow = await page.evaluate(() => {
|
|
return document.documentElement.scrollWidth > document.documentElement.clientWidth;
|
|
});
|
|
|
|
expect(hasOverflow).toBeFalsy();
|
|
});
|
|
|
|
test('Login form — centré et pas de débordement', async ({ page }) => {
|
|
await page.goto('/login');
|
|
await page.waitForLoadState('networkidle').catch(() => {});
|
|
|
|
const hasOverflow = await page.evaluate(() => {
|
|
return document.documentElement.scrollWidth > document.documentElement.clientWidth;
|
|
});
|
|
|
|
expect(hasOverflow).toBeFalsy();
|
|
});
|
|
});
|
|
|
|
test.describe('VISUAL — Images cassées @visual', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
|
});
|
|
|
|
test('Discover — aucune image cassée', async ({ page }) => {
|
|
await navigateTo(page, '/discover');
|
|
await page.waitForTimeout(3000);
|
|
|
|
const brokenImages = await page.evaluate(() => {
|
|
const imgs = document.querySelectorAll('img');
|
|
return Array.from(imgs)
|
|
.filter(img => img.src && !img.complete && img.naturalWidth === 0)
|
|
.map(img => ({ src: img.src, alt: img.alt }));
|
|
});
|
|
|
|
if (brokenImages.length > 0) {
|
|
console.warn(`⚠ Broken images on /discover: ${JSON.stringify(brokenImages)}`);
|
|
}
|
|
});
|
|
|
|
test('Library — aucune image cassée', async ({ page }) => {
|
|
await navigateTo(page, '/library');
|
|
await page.waitForTimeout(3000);
|
|
|
|
const brokenImages = await page.evaluate(() => {
|
|
const imgs = document.querySelectorAll('img');
|
|
return Array.from(imgs)
|
|
.filter(img => img.src && !img.complete && img.naturalWidth === 0)
|
|
.map(img => ({ src: img.src, alt: img.alt }));
|
|
});
|
|
|
|
if (brokenImages.length > 0) {
|
|
console.warn(`⚠ Broken images on /library: ${JSON.stringify(brokenImages)}`);
|
|
}
|
|
});
|
|
|
|
test('Marketplace — aucune image cassée', async ({ page }) => {
|
|
await navigateTo(page, '/marketplace');
|
|
await page.waitForTimeout(3000);
|
|
|
|
const brokenImages = await page.evaluate(() => {
|
|
const imgs = document.querySelectorAll('img');
|
|
return Array.from(imgs)
|
|
.filter(img => img.src && !img.complete && img.naturalWidth === 0)
|
|
.map(img => ({ src: img.src, alt: img.alt }));
|
|
});
|
|
|
|
if (brokenImages.length > 0) {
|
|
console.warn(`⚠ Broken images on /marketplace: ${JSON.stringify(brokenImages)}`);
|
|
}
|
|
});
|
|
});
|
|
|
|
test.describe('VISUAL — Layout responsive @visual', () => {
|
|
test.setTimeout(60_000);
|
|
|
|
const viewports = [
|
|
{ width: 375, height: 667, name: 'iPhone SE' },
|
|
{ width: 390, height: 844, name: 'iPhone 14' },
|
|
{ width: 768, height: 1024, name: 'iPad' },
|
|
{ width: 1280, height: 720, name: 'Desktop' },
|
|
];
|
|
|
|
for (const vp of viewports) {
|
|
test(`Pas de débordement horizontal sur ${vp.name} (${vp.width}x${vp.height}) @mobile`, async ({ page }) => {
|
|
await page.setViewportSize({ width: vp.width, height: vp.height });
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
|
|
|
const pages = ['/dashboard', '/discover', '/library', '/playlists'];
|
|
for (const path of pages) {
|
|
await navigateTo(page, path);
|
|
|
|
const hasOverflow = await page.evaluate(() =>
|
|
document.documentElement.scrollWidth > document.documentElement.clientWidth,
|
|
);
|
|
|
|
expect(hasOverflow, `Overflow on ${path} at ${vp.width}x${vp.height}`).toBeFalsy();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
test.describe('VISUAL — Contraste et accessibilité @visual @a11y', () => {
|
|
test('Messages d\'erreur sur login ont un contraste suffisant', async ({ page }) => {
|
|
await page.goto('/login');
|
|
await page.waitForLoadState('networkidle').catch(() => {});
|
|
|
|
// Submit empty form to trigger validation
|
|
const submitBtn = page.getByRole('button', { name: /sign in|connexion|se connecter/i }).first();
|
|
if (await submitBtn.isVisible({ timeout: 5000 }).catch(() => false)) {
|
|
await submitBtn.click();
|
|
await page.waitForTimeout(500);
|
|
|
|
// Check error messages exist and are visible
|
|
const errorElements = await page.locator('[class*="destructive"], [role="alert"], [class*="error"]').all();
|
|
for (const el of errorElements) {
|
|
if (await el.isVisible().catch(() => false)) {
|
|
const color = await el.evaluate(e => getComputedStyle(e).color);
|
|
const opacity = await el.evaluate(e => getComputedStyle(e).opacity);
|
|
// Ensure text is not invisible
|
|
expect(parseFloat(opacity)).toBeGreaterThan(0.5);
|
|
console.log(`Error element: color=${color}, opacity=${opacity}`);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
test('Focus ring visible sur les éléments interactifs', async ({ page }) => {
|
|
await page.goto('/login');
|
|
await page.waitForLoadState('networkidle').catch(() => {});
|
|
|
|
// Tab through elements
|
|
await page.keyboard.press('Tab');
|
|
await page.waitForTimeout(200);
|
|
|
|
const focusedElement = page.locator(':focus');
|
|
if (await focusedElement.isVisible().catch(() => false)) {
|
|
const outline = await focusedElement.evaluate(e => {
|
|
const styles = getComputedStyle(e);
|
|
return {
|
|
outline: styles.outline,
|
|
boxShadow: styles.boxShadow,
|
|
border: styles.borderColor,
|
|
};
|
|
});
|
|
// Should have some visible focus indicator
|
|
const hasFocusIndicator = outline.outline !== 'none' ||
|
|
outline.boxShadow !== 'none' ||
|
|
outline.border !== '';
|
|
if (!hasFocusIndicator) {
|
|
console.warn('⚠ No visible focus indicator on first tab target');
|
|
}
|
|
}
|
|
});
|
|
});
|