- 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>
149 lines
6 KiB
TypeScript
149 lines
6 KiB
TypeScript
import { test, expect } from '@chromatic-com/playwright';
|
|
import { loginViaAPI, navigateTo } from '../helpers';
|
|
import { TEST_USERS, ROUTES } from '../design-tokens';
|
|
|
|
test.describe('ÉTHIQUE — Anti-gamification, métriques privées, pas de dark patterns', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await loginViaAPI(page, TEST_USERS.listener.email, TEST_USERS.listener.password);
|
|
});
|
|
|
|
test('Aucun compteur de "likes" ou "plays" visible publiquement', async ({ page }) => {
|
|
// Les métriques de popularité ne doivent PAS être visibles pour les listeners
|
|
// Elles sont réservées aux créateurs dans leur dashboard analytics
|
|
const pagesToCheck = ['/feed', '/discover', '/dashboard', '/library'];
|
|
|
|
for (const path of pagesToCheck) {
|
|
await navigateTo(page, path);
|
|
|
|
const metricsExposed = await page.evaluate(() => {
|
|
const body = document.body.textContent?.toLowerCase() || '';
|
|
const issues: string[] = [];
|
|
|
|
// Patterns de métriques publiques interdites
|
|
// Note: "plays", "views", "likes" en tant que compteurs visibles sont interdits
|
|
// Mais "play" en tant que bouton d'action est OK
|
|
document.querySelectorAll('[class*="play-count"], [class*="like-count"], [class*="view-count"], [data-testid*="play-count"], [data-testid*="like-count"]').forEach(el => {
|
|
if (getComputedStyle(el).display !== 'none') {
|
|
issues.push(`Métrique publique visible: ${el.className} — texte: "${el.textContent?.trim().slice(0, 30)}"`);
|
|
}
|
|
});
|
|
|
|
return issues;
|
|
});
|
|
|
|
for (const issue of metricsExposed) {
|
|
console.log(`[ETHICAL] ${path}: ${issue}`);
|
|
}
|
|
|
|
expect(metricsExposed.length,
|
|
`Métriques de popularité visibles publiquement sur ${path} (interdit par CLAUDE.md §4):\n${metricsExposed.join('\n')}`
|
|
).toBe(0);
|
|
}
|
|
});
|
|
|
|
test('Aucun élément de gamification (XP, streak, badge, leaderboard)', async ({ page }) => {
|
|
const pagesToCheck = ['/dashboard', '/profile', '/settings', '/library'];
|
|
|
|
for (const path of pagesToCheck) {
|
|
await navigateTo(page, path);
|
|
|
|
const gamificationElements = await page.evaluate(() => {
|
|
const body = document.body.textContent || '';
|
|
const issues: string[] = [];
|
|
|
|
// Patterns de gamification interdits
|
|
const forbidden = [
|
|
/\bXP\b/,
|
|
/\bstreak\b/i,
|
|
/\bleaderboard\b/i,
|
|
/\blevel\s*up\b/i,
|
|
/\bclassement\b/i,
|
|
/\bscore\b(?!.*password)/i, // "score" sauf "password score"
|
|
];
|
|
|
|
for (const pattern of forbidden) {
|
|
if (pattern.test(body)) {
|
|
const match = body.match(pattern);
|
|
if (match) {
|
|
issues.push(`Texte de gamification trouvé: "${match[0]}" (interdit par CLAUDE.md §3)`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Vérifier les éléments UI de gamification
|
|
document.querySelectorAll('[class*="streak"], [class*="xp-"], [class*="leaderboard"], [class*="level-up"], [data-testid*="streak"], [data-testid*="xp"]').forEach(el => {
|
|
if (getComputedStyle(el).display !== 'none') {
|
|
issues.push(`Élément de gamification: ${el.className.toString().slice(0, 50)}`);
|
|
}
|
|
});
|
|
|
|
return issues;
|
|
});
|
|
|
|
for (const issue of gamificationElements) {
|
|
console.log(`[ETHICAL] ${path}: ${issue}`);
|
|
}
|
|
|
|
expect(gamificationElements.length,
|
|
`Éléments de gamification sur ${path}:\n${gamificationElements.join('\n')}`
|
|
).toBe(0);
|
|
}
|
|
});
|
|
|
|
test('Pas de dark patterns UX — désinscription facile', async ({ page }) => {
|
|
await navigateTo(page, '/settings');
|
|
|
|
// Vérifier qu'il y a un moyen de supprimer son compte
|
|
const body = await page.textContent('body') || '';
|
|
const hasAccountDeletion = /supprimer|delete.*account|effacer.*compte|close.*account|fermer.*compte|désinscription/i.test(body);
|
|
|
|
console.log(`[ETHICAL] Option de suppression de compte visible: ${hasAccountDeletion}`);
|
|
|
|
// Vérifier qu'il n'y a pas de dark pattern "Êtes-vous sûr de vouloir partir ?"
|
|
// avec des boutons de taille asymétrique
|
|
});
|
|
|
|
test('Pas de notifications push manipulatrices', async ({ page }) => {
|
|
// Vérifier que l'app ne demande pas les permissions de notification de manière agressive
|
|
const notificationPermission = await page.evaluate(() => {
|
|
return Notification?.permission || 'not-supported';
|
|
}).catch(() => 'not-supported');
|
|
|
|
console.log(`[ETHICAL] Notification permission: ${notificationPermission}`);
|
|
// L'app ne devrait pas demander les permissions push automatiquement
|
|
});
|
|
|
|
test('Le feed est chronologique (pas de ranking comportemental)', async ({ page }) => {
|
|
await navigateTo(page, '/feed');
|
|
|
|
// Vérifier qu'il n'y a pas de "Recommandé pour vous" basé sur des algorithmes comportementaux
|
|
const body = await page.textContent('body') || '';
|
|
const behavioralPatterns = /recommended for you|basé sur.*écoutes|algorithme|trending|populaire|for you/i;
|
|
|
|
const hasBehavioralRanking = behavioralPatterns.test(body);
|
|
if (hasBehavioralRanking) {
|
|
console.log(`[ETHICAL] Pattern de ranking comportemental détecté dans le feed`);
|
|
}
|
|
|
|
// La découverte doit être par tags/genres déclaratifs, pas par comportement
|
|
});
|
|
|
|
test('Pas d\'imports AI/ML/blockchain interdits dans le bundle', async ({ page }) => {
|
|
await navigateTo(page, '/dashboard');
|
|
|
|
// Vérifier qu'aucun script chargé ne contient des références aux bibliothèques interdites
|
|
const scripts = await page.evaluate(() => {
|
|
const scriptTags = document.querySelectorAll('script[src]');
|
|
return Array.from(scriptTags).map(s => s.getAttribute('src') || '');
|
|
});
|
|
|
|
const forbidden = ['tensorflow', 'pytorch', 'sklearn', 'web3', 'ethers', 'metamask', 'nft'];
|
|
const violations = scripts.filter(src =>
|
|
forbidden.some(f => src.toLowerCase().includes(f))
|
|
);
|
|
|
|
expect(violations.length,
|
|
`Scripts interdits chargés: ${violations.join(', ')}`
|
|
).toBe(0);
|
|
});
|
|
});
|