veza/tests/e2e/33-visual-bugs.spec.ts
senke 20a16f7cbe test: add comprehensive e2e test suite (34 spec files)
New tests/e2e/ suite covering:
- Auth, navigation, player, tracks, playlists
- Search, discover, social, marketplace, chat
- Accessibility, API, workflows, edge cases
- Routes coverage, forms validation, modals
- Empty states, responsive, network errors
- Error boundary, performance, visual regression
- Cross-browser, profile, smoke, upload
- Storybook, deep pages, visual bugs
- Includes fixtures, helpers, global setup/teardown
- Playwright config and coverage map

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

197 lines
7.1 KiB
TypeScript

import { test, expect } from '@playwright/test';
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');
}
}
});
});