veza/tests/e2e/33-visual-bugs.spec.ts
senke 3640aec716 test(e2e): convert all remaining 298 console.log to real expect()
Convert 20 files from fake assertions (console.log with ✓/✗) to real
expect() assertions. This completes the conversion started in the
previous session — zero console.log calls remain in the E2E suite.

Files converted (by batch):
Batch 1: 16-forms-validation (38→0), 13-workflows (18→0), 14-edge-cases (8→0)
Batch 2: 15-routes-coverage (8→0), 20-network-errors (5→0), 04-tracks (4→0),
         32-deep-pages (4→0), 19-responsive (3→0), 11-accessibility-ethics (3→0)
Batch 3: 25-profile (2→0), 12-api (2→0), 29-chat-functional (2→0),
         30-marketplace-checkout (1→0), 22-performance (1→0),
         31-auth-sessions (1→0), 26-smoke (1→0), 02-navigation (1→0)
Batch 4: 24-cross-browser (0 fakes, 12 info→0), 34-workflows-empty (0→0),
         33-visual-bugs (0→0)

Total: 139 fake assertions → real expect(), 159 informational logs removed

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

185 lines
6.9 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}`);
}
}
}
expect(tooSmall, `Touch targets too small: ${tooSmall.join(', ')}`).toHaveLength(0);
}
});
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 }));
});
expect(brokenImages, `Broken images on /discover: ${JSON.stringify(brokenImages)}`).toHaveLength(0);
});
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 }));
});
expect(brokenImages, `Broken images on /library: ${JSON.stringify(brokenImages)}`).toHaveLength(0);
});
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 }));
});
expect(brokenImages, `Broken images on /marketplace: ${JSON.stringify(brokenImages)}`).toHaveLength(0);
});
});
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 opacity = await el.evaluate(e => getComputedStyle(e).opacity);
// Ensure text is not invisible
expect(parseFloat(opacity)).toBeGreaterThan(0.5);
}
}
}
});
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 !== '';
expect(hasFocusIndicator, 'No visible focus indicator on first tab target').toBeTruthy();
}
});
});