veza/tests/e2e/audit/pixel-perfect/13-interactive-spacing.spec.ts
senke 7b39efa176 fix: stabilize frontend — 98 TS errors to 0, align API endpoints, optimize bundle
- 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>
2026-03-24 21:18:49 +01:00

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);
});
}