- 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>
107 lines
4.3 KiB
TypeScript
107 lines
4.3 KiB
TypeScript
import { test, expect } from '@chromatic-com/playwright';
|
|
import { loginViaAPI, navigateTo } from '../helpers';
|
|
import { TEST_USERS, ROUTES, VIEWPORTS } from '../design-tokens';
|
|
|
|
test.describe('ESPACEMENT — Les éléments cliquables ne sont pas collés', () => {
|
|
for (const route of ROUTES.public) {
|
|
test(`[PUBLIC] ${route.name} — au moins 8px entre éléments interactifs`, async ({ page }) => {
|
|
await navigateTo(page, route.path);
|
|
|
|
const tooClose = await findTooCloseElements(page);
|
|
|
|
for (const issue of tooClose) {
|
|
console.log(`[SPACING] ${issue.fix}`);
|
|
}
|
|
|
|
expect(tooClose.length,
|
|
`${tooClose.length} paire(s) d'éléments trop proches sur ${route.path}:\n` +
|
|
tooClose.map(i => `• ${i.fix}`).join('\n')
|
|
).toBe(0);
|
|
});
|
|
}
|
|
|
|
for (const route of ROUTES.listener.slice(0, 10)) {
|
|
test(`[PROTECTED] ${route.name} — au moins 8px entre éléments interactifs`, async ({ page }) => {
|
|
await loginViaAPI(page, TEST_USERS.listener.email, TEST_USERS.listener.password);
|
|
await navigateTo(page, route.path);
|
|
|
|
const tooClose = await findTooCloseElements(page);
|
|
|
|
for (const issue of tooClose) {
|
|
console.log(`[SPACING] ${issue.fix}`);
|
|
}
|
|
|
|
expect(tooClose.length,
|
|
`${tooClose.length} paire(s) d'éléments trop proches sur ${route.path}:\n` +
|
|
tooClose.map(i => `• ${i.fix}`).join('\n')
|
|
).toBe(0);
|
|
});
|
|
}
|
|
|
|
// Mobile spécifique — encore plus important
|
|
for (const route of [ROUTES.listener[0], ROUTES.listener[2], ROUTES.listener[7]]) {
|
|
test(`[MOBILE] ${route.name} @ mobile — espacement suffisant`, async ({ page }) => {
|
|
await page.setViewportSize(VIEWPORTS.mobileSE);
|
|
await loginViaAPI(page, TEST_USERS.listener.email, TEST_USERS.listener.password);
|
|
await navigateTo(page, route.path);
|
|
|
|
const tooClose = await findTooCloseElements(page);
|
|
|
|
for (const issue of tooClose) {
|
|
console.log(`[SPACING MOBILE] ${issue.fix}`);
|
|
}
|
|
|
|
expect(tooClose.length,
|
|
`${tooClose.length} paire(s) trop proches sur mobile ${route.path}:\n` +
|
|
tooClose.map(i => `• ${i.fix}`).join('\n')
|
|
).toBe(0);
|
|
});
|
|
}
|
|
});
|
|
|
|
async function findTooCloseElements(page: import('@playwright/test').Page) {
|
|
return page.evaluate(() => {
|
|
const interactive = Array.from(document.querySelectorAll('button:not(:disabled), a[href], input:not([type="hidden"]), select, [role="button"]'))
|
|
.filter(el => {
|
|
const r = el.getBoundingClientRect();
|
|
const s = getComputedStyle(el);
|
|
return r.width > 0 && r.height > 0 && s.display !== 'none' && s.visibility !== 'hidden'
|
|
&& r.top >= 0 && r.top < window.innerHeight;
|
|
});
|
|
|
|
const issues: Array<{ elementA: string; elementB: string; gap: number; fix: string }> = [];
|
|
|
|
for (let i = 0; i < interactive.length && i < 50; i++) {
|
|
for (let j = i + 1; j < interactive.length && j < 50; j++) {
|
|
if (interactive[i].contains(interactive[j]) || interactive[j].contains(interactive[i])) continue;
|
|
|
|
const a = interactive[i].getBoundingClientRect();
|
|
const b = interactive[j].getBoundingClientRect();
|
|
|
|
// Only check elements roughly on the same row
|
|
const sameLine = Math.abs(a.top - b.top) < Math.max(a.height, b.height);
|
|
if (!sameLine) continue;
|
|
|
|
const gapX = Math.max(0, Math.max(b.left - a.right, a.left - b.right));
|
|
|
|
if (gapX < 8 && gapX >= 0) {
|
|
const textA = interactive[i].textContent?.trim().slice(0, 15) || interactive[i].getAttribute('aria-label') || '';
|
|
const textB = interactive[j].textContent?.trim().slice(0, 15) || interactive[j].getAttribute('aria-label') || '';
|
|
|
|
// Skip if they overlap (handled by overlap test)
|
|
const overlap = Math.max(0, Math.min(a.right, b.right) - Math.max(a.left, b.left));
|
|
if (overlap > 0) continue;
|
|
|
|
issues.push({
|
|
elementA: textA,
|
|
elementB: textB,
|
|
gap: Math.round(gapX),
|
|
fix: `ÉLÉMENT: "${textA}" ↔ "${textB}" | PAGE: ${location.pathname} | MESURÉ: ${Math.round(gapX)}px d'espace | ATTENDU: >=8px | FIX TAILWIND: Ajouter gap-2 (8px) au parent flex/grid, ou mr-2 après le premier élément`,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return issues.slice(0, 15);
|
|
});
|
|
}
|