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>
185 lines
6.9 KiB
TypeScript
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();
|
|
}
|
|
});
|
|
});
|