- 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>
806 lines
32 KiB
TypeScript
806 lines
32 KiB
TypeScript
import { test, expect } from '@chromatic-com/playwright';
|
|
import { loginViaAPI, CONFIG, navigateTo, assertNotBroken } from './helpers';
|
|
|
|
// =============================================================================
|
|
// FORMS — Validation des formulaires @feature-forms
|
|
//
|
|
// Ce fichier teste la validation cote client de TOUS les formulaires
|
|
// de l'application : soumission vide, champs invalides, messages d'erreur.
|
|
// =============================================================================
|
|
|
|
// =============================================================================
|
|
// LOGIN FORM VALIDATION
|
|
// =============================================================================
|
|
|
|
test.describe('FORMS — Login form validation @feature-forms', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await navigateTo(page, '/login');
|
|
await page.getByTestId('login-form').waitFor({ state: 'visible', timeout: 10_000 });
|
|
});
|
|
|
|
test('01. Soumettre login vide affiche erreurs de validation @critical', async ({ page }) => {
|
|
const submitBtn = page.getByTestId('login-submit');
|
|
await submitBtn.click();
|
|
|
|
// Should stay on login page
|
|
await expect(page).toHaveURL(/login/);
|
|
|
|
// Check for validation errors (custom or HTML5 native)
|
|
const body = await page.textContent('body') || '';
|
|
const hasCustomError = /required|obligatoire|email|invalid|invalide/i.test(body);
|
|
|
|
const emailInput = page.locator('input[type="email"]').first();
|
|
const emailValidation = await emailInput.evaluate(
|
|
(el: HTMLInputElement) => el.validationMessage,
|
|
).catch(() => '');
|
|
|
|
const passwordInput = page.locator('input[type="password"]').first();
|
|
const passwordValidation = await passwordInput.evaluate(
|
|
(el: HTMLInputElement) => el.validationMessage,
|
|
).catch(() => '');
|
|
|
|
const hasValidation = hasCustomError || emailValidation.length > 0 || passwordValidation.length > 0;
|
|
expect(hasValidation).toBeTruthy();
|
|
console.log(` Empty login: validation shown (email: "${emailValidation}", password: "${passwordValidation}")`);
|
|
});
|
|
|
|
test('02. Soumettre login avec email seul affiche erreur mot de passe', async ({ page }) => {
|
|
const emailInput = page.locator('input[type="email"]');
|
|
await emailInput.fill('test@example.com');
|
|
|
|
const submitBtn = page.getByTestId('login-submit');
|
|
await submitBtn.click();
|
|
|
|
// Should stay on login
|
|
await expect(page).toHaveURL(/login/);
|
|
|
|
// Password should show validation
|
|
const passwordInput = page.locator('input[type="password"]').first();
|
|
const validationMessage = await passwordInput.evaluate(
|
|
(el: HTMLInputElement) => el.validationMessage,
|
|
).catch(() => '');
|
|
|
|
const body = await page.textContent('body') || '';
|
|
const hasError = validationMessage.length > 0 || /required|obligatoire|password|mot de passe/i.test(body);
|
|
expect(hasError).toBeTruthy();
|
|
console.log(` Email only: password validation shown ("${validationMessage}")`);
|
|
});
|
|
|
|
test('03. Soumettre login avec password seul affiche erreur email', async ({ page }) => {
|
|
const passwordInput = page.locator('input[type="password"]').first();
|
|
await passwordInput.fill('Password123!');
|
|
|
|
const submitBtn = page.getByTestId('login-submit');
|
|
await submitBtn.click();
|
|
|
|
// Should stay on login
|
|
await expect(page).toHaveURL(/login/);
|
|
|
|
// Email should show validation
|
|
const emailInput = page.locator('input[type="email"]').first();
|
|
const validationMessage = await emailInput.evaluate(
|
|
(el: HTMLInputElement) => el.validationMessage,
|
|
).catch(() => '');
|
|
|
|
const body = await page.textContent('body') || '';
|
|
const hasError = validationMessage.length > 0 || /required|obligatoire|email/i.test(body);
|
|
expect(hasError).toBeTruthy();
|
|
console.log(` Password only: email validation shown ("${validationMessage}")`);
|
|
});
|
|
|
|
test('04. Email invalide format affiche erreur validation', async ({ page }) => {
|
|
const emailInput = page.locator('input[type="email"]');
|
|
await emailInput.fill('not-an-email');
|
|
|
|
const passwordInput = page.locator('input[type="password"]').first();
|
|
await passwordInput.fill('Password123!');
|
|
|
|
const submitBtn = page.getByTestId('login-submit');
|
|
await submitBtn.click();
|
|
|
|
// Should stay on login
|
|
await expect(page).toHaveURL(/login/);
|
|
|
|
// Check for email validation error
|
|
const emailEl = page.locator('input[type="email"]').first();
|
|
const validationMessage = await emailEl.evaluate(
|
|
(el: HTMLInputElement) => el.validationMessage,
|
|
).catch(() => '');
|
|
|
|
const body = await page.textContent('body') || '';
|
|
const hasError = validationMessage.length > 0 || /invalid|invalide|email.*format|format.*email/i.test(body);
|
|
expect(hasError).toBeTruthy();
|
|
console.log(` Invalid email format: validation shown ("${validationMessage}")`);
|
|
});
|
|
|
|
test('05. Identifiants incorrects affiche erreur serveur sans crash', async ({ page }) => {
|
|
const emailInput = page.locator('input[type="email"]');
|
|
await emailInput.clear();
|
|
await emailInput.fill('nonexistent@example.com');
|
|
|
|
const passwordInput = page.locator('input[type="password"]').first();
|
|
await passwordInput.clear();
|
|
await passwordInput.fill('WrongPassword999!');
|
|
|
|
const submitBtn = page.getByTestId('login-submit');
|
|
await submitBtn.click();
|
|
|
|
await page.waitForTimeout(3_000);
|
|
|
|
// Should stay on login
|
|
await expect(page).toHaveURL(/login/);
|
|
|
|
// Should show an error alert
|
|
const body = await page.textContent('body') || '';
|
|
expect(body).not.toMatch(/crash|TypeError|Cannot read/i);
|
|
|
|
const errorAlert = page.getByRole('alert');
|
|
const hasAlert = await errorAlert.isVisible().catch(() => false);
|
|
const hasErrorText = /incorrect|invalid|erreur|error|unauthorized|identifiants/i.test(body);
|
|
console.log(` Wrong credentials: ${hasAlert ? 'alert shown' : hasErrorText ? 'error text shown' : 'handled'}`);
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// REGISTER FORM VALIDATION
|
|
// =============================================================================
|
|
|
|
test.describe('FORMS — Register form validation @feature-forms', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await navigateTo(page, '/register');
|
|
await page.getByTestId('register-form').waitFor({ state: 'visible', timeout: 10_000 });
|
|
});
|
|
|
|
test('06. Soumettre register vide affiche erreurs multiples @critical', async ({ page }) => {
|
|
const submitBtn = page.getByTestId('register-submit');
|
|
await submitBtn.click();
|
|
|
|
// Should stay on register page
|
|
await expect(page).toHaveURL(/register/);
|
|
|
|
// Check for validation errors
|
|
const body = await page.textContent('body') || '';
|
|
const hasCustomErrors = /required|obligatoire|invalid|invalide|trop court|too short/i.test(body);
|
|
|
|
const usernameInput = page.locator('#register-username');
|
|
const usernameValidation = await usernameInput.evaluate(
|
|
(el: HTMLInputElement) => el.validationMessage,
|
|
).catch(() => '');
|
|
|
|
const hasValidation = hasCustomErrors || usernameValidation.length > 0;
|
|
expect(hasValidation).toBeTruthy();
|
|
console.log(` Empty register: validation shown (${hasCustomErrors ? 'custom errors' : 'native validation'})`);
|
|
});
|
|
|
|
test('07. Username trop court (< 3 chars) affiche erreur', async ({ page }) => {
|
|
const usernameInput = page.locator('#register-username');
|
|
await usernameInput.fill('ab');
|
|
await usernameInput.blur();
|
|
|
|
await page.waitForTimeout(500);
|
|
|
|
const body = await page.textContent('body') || '';
|
|
const hasError = /trop court|too short|minimum|au moins|at least|3.*caract|3.*char/i.test(body);
|
|
|
|
const validationMessage = await usernameInput.evaluate(
|
|
(el: HTMLInputElement) => el.validationMessage,
|
|
).catch(() => '');
|
|
|
|
const validated = hasError || validationMessage.length > 0;
|
|
console.log(` Short username: ${validated ? 'error shown' : 'no explicit error (may validate on submit)'}`);
|
|
});
|
|
|
|
test('08. Mot de passe trop court (< 12 chars) affiche erreur', async ({ page }) => {
|
|
const passwordInput = page.locator('#register-password');
|
|
await passwordInput.fill('Short1!');
|
|
await passwordInput.blur();
|
|
|
|
await page.waitForTimeout(500);
|
|
|
|
const body = await page.textContent('body') || '';
|
|
const hasError = /trop court|too short|minimum|au moins|at least|caract|char|password/i.test(body);
|
|
|
|
const validationMessage = await passwordInput.evaluate(
|
|
(el: HTMLInputElement) => el.validationMessage,
|
|
).catch(() => '');
|
|
|
|
const validated = hasError || validationMessage.length > 0;
|
|
console.log(` Short password: ${validated ? 'error shown' : 'no explicit error (may validate on submit)'}`);
|
|
});
|
|
|
|
test('09. Mots de passe ne correspondent pas affiche erreur', async ({ page }) => {
|
|
const passwordInput = page.locator('#register-password');
|
|
await passwordInput.fill('SecurePassword123!@#');
|
|
|
|
const confirmInput = page.locator('#register-password_confirm');
|
|
await confirmInput.fill('DifferentPassword456!@#');
|
|
await confirmInput.blur();
|
|
|
|
await page.waitForTimeout(500);
|
|
|
|
const body = await page.textContent('body') || '';
|
|
const hasError = /ne correspondent pas|do not match|don't match|mismatch|identiques|match/i.test(body);
|
|
|
|
// Also try submitting to trigger validation
|
|
if (!hasError) {
|
|
// Fill other required fields first
|
|
await page.locator('#register-username').fill('testuser');
|
|
await page.locator('#register-email').fill('test@example.com');
|
|
|
|
const termsCheckbox = page.locator('#register-terms');
|
|
if (await termsCheckbox.isVisible().catch(() => false)) {
|
|
await termsCheckbox.check();
|
|
}
|
|
|
|
const submitBtn = page.getByTestId('register-submit');
|
|
await submitBtn.click();
|
|
await page.waitForTimeout(1_000);
|
|
|
|
const bodyAfterSubmit = await page.textContent('body') || '';
|
|
const hasErrorAfterSubmit = /ne correspondent pas|do not match|don't match|mismatch|identiques|match/i.test(bodyAfterSubmit);
|
|
console.log(` Mismatched passwords: ${hasErrorAfterSubmit ? 'error shown on submit' : 'check behavior'}`);
|
|
} else {
|
|
console.log(' Mismatched passwords: error shown on blur');
|
|
}
|
|
|
|
// Should stay on register regardless
|
|
await expect(page).toHaveURL(/register/);
|
|
});
|
|
|
|
test('10. Terms non cochees affiche erreur', async ({ page }) => {
|
|
// Fill all fields except terms
|
|
await page.locator('#register-username').fill('testuser123');
|
|
await page.locator('#register-email').fill(`terms-test-${Date.now()}@example.com`);
|
|
await page.locator('#register-password').fill('SecurePassword123!@#');
|
|
await page.locator('#register-password_confirm').fill('SecurePassword123!@#');
|
|
|
|
// Make sure terms is NOT checked
|
|
const termsCheckbox = page.locator('#register-terms');
|
|
if (await termsCheckbox.isVisible().catch(() => false)) {
|
|
if (await termsCheckbox.isChecked()) {
|
|
await termsCheckbox.uncheck();
|
|
}
|
|
}
|
|
|
|
const submitBtn = page.getByTestId('register-submit');
|
|
await submitBtn.click();
|
|
|
|
await page.waitForTimeout(1_000);
|
|
|
|
// Should stay on register page
|
|
await expect(page).toHaveURL(/register/);
|
|
|
|
const body = await page.textContent('body') || '';
|
|
const hasTermsError = /terms|conditions|accepter|accept|cgu|tos/i.test(body);
|
|
|
|
const termsValidation = await termsCheckbox.evaluate(
|
|
(el: HTMLInputElement) => el.validationMessage,
|
|
).catch(() => '');
|
|
|
|
console.log(` Terms unchecked: ${hasTermsError || termsValidation.length > 0 ? 'error shown' : 'form blocked (native or custom)'}`);
|
|
});
|
|
|
|
test('11. Email invalide dans le formulaire d\'inscription affiche erreur', async ({ page }) => {
|
|
await page.locator('#register-email').fill('invalid-email-format');
|
|
await page.locator('#register-email').blur();
|
|
|
|
await page.waitForTimeout(500);
|
|
|
|
const body = await page.textContent('body') || '';
|
|
const hasError = /email.*invalide|invalid.*email|format/i.test(body);
|
|
|
|
const emailInput = page.locator('#register-email');
|
|
const validationMessage = await emailInput.evaluate(
|
|
(el: HTMLInputElement) => el.validationMessage,
|
|
).catch(() => '');
|
|
|
|
const validated = hasError || validationMessage.length > 0;
|
|
expect(validated).toBeTruthy();
|
|
console.log(` Invalid register email: error shown ("${validationMessage}")`);
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// FORGOT PASSWORD FORM VALIDATION
|
|
// =============================================================================
|
|
|
|
test.describe('FORMS — Forgot password form validation @feature-forms', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await navigateTo(page, '/forgot-password');
|
|
});
|
|
|
|
test('12. Soumettre sans email affiche erreur', async ({ page }) => {
|
|
const submitBtn = page.getByRole('button', { name: /reset|réinitialiser|send|envoyer|submit/i });
|
|
if (!(await submitBtn.isVisible().catch(() => false))) {
|
|
console.log(' Forgot password form not found (skipping)');
|
|
return;
|
|
}
|
|
|
|
await submitBtn.click();
|
|
|
|
const body = await page.textContent('body') || '';
|
|
const hasError = /required|obligatoire|email|invalid|invalide/i.test(body);
|
|
|
|
const emailInput = page.locator('input[type="email"]').first();
|
|
const validationMessage = await emailInput.evaluate(
|
|
(el: HTMLInputElement) => el.validationMessage,
|
|
).catch(() => '');
|
|
|
|
const validated = hasError || validationMessage.length > 0;
|
|
expect(validated).toBeTruthy();
|
|
console.log(` Empty forgot password: validation shown ("${validationMessage}")`);
|
|
});
|
|
|
|
test('13. Email invalide affiche erreur', async ({ page }) => {
|
|
const emailInput = page.locator('input[type="email"]').first()
|
|
.or(page.getByLabel(/email/i).first());
|
|
|
|
if (!(await emailInput.isVisible().catch(() => false))) {
|
|
console.log(' Forgot password email input not found (skipping)');
|
|
return;
|
|
}
|
|
|
|
await emailInput.fill('not-an-email');
|
|
|
|
const submitBtn = page.getByRole('button', { name: /reset|réinitialiser|send|envoyer|submit/i });
|
|
await submitBtn.click();
|
|
|
|
const validationMessage = await emailInput.evaluate(
|
|
(el: HTMLInputElement) => el.validationMessage,
|
|
).catch(() => '');
|
|
|
|
const body = await page.textContent('body') || '';
|
|
const hasError = validationMessage.length > 0 || /invalid|invalide|format/i.test(body);
|
|
expect(hasError).toBeTruthy();
|
|
console.log(` Invalid email in forgot password: error shown ("${validationMessage}")`);
|
|
});
|
|
|
|
test('14. Email valide affiche message de succes', async ({ page }) => {
|
|
const emailInput = page.locator('input[type="email"]').first()
|
|
.or(page.getByLabel(/email/i).first());
|
|
|
|
if (!(await emailInput.isVisible().catch(() => false))) {
|
|
console.log(' Forgot password email input not found (skipping)');
|
|
return;
|
|
}
|
|
|
|
await emailInput.fill('test@example.com');
|
|
|
|
const submitBtn = page.getByRole('button', { name: /reset|réinitialiser|send|envoyer|submit/i });
|
|
await submitBtn.click();
|
|
|
|
await page.waitForTimeout(3_000);
|
|
|
|
const body = await page.textContent('body') || '';
|
|
// Should show a success message (email sent) or an error (email not found)
|
|
// Either way, should not crash
|
|
expect(body).not.toMatch(/crash|TypeError|Cannot read/i);
|
|
|
|
const hasSuccess = /envoyé|sent|check.*email|vérif.*email|lien.*envoyé|link.*sent|succès|success/i.test(body);
|
|
const hasError = /not found|introuvable|error|erreur/i.test(body);
|
|
console.log(` Valid email forgot password: ${hasSuccess ? 'success message' : hasError ? 'error (expected if email not in DB)' : 'response received'}`);
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// PLAYLIST CREATE FORM VALIDATION
|
|
// =============================================================================
|
|
|
|
test.describe('FORMS — Playlist create form validation @feature-forms', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
|
});
|
|
|
|
test('15. Creer playlist sans titre affiche erreur', async ({ page }) => {
|
|
await navigateTo(page, '/playlists');
|
|
|
|
const createBtn = page.getByRole('button', { name: /créer|create|nouvelle|new/i }).first()
|
|
.or(page.getByRole('link', { name: /créer|create|nouvelle|new/i }).first());
|
|
|
|
if (!(await createBtn.isVisible().catch(() => false))) {
|
|
console.log(' Create playlist button not found (skipping)');
|
|
return;
|
|
}
|
|
|
|
await createBtn.click();
|
|
await page.waitForTimeout(1_000);
|
|
|
|
// Try to submit without filling the title — scope to dialog to avoid strict mode violation
|
|
const dialog = page.locator('[role="dialog"]').first();
|
|
const dialogVisible = await dialog.isVisible().catch(() => false);
|
|
const saveBtn = dialogVisible
|
|
? dialog.getByRole('button', { name: /créer|create|sauvegarder|save|ok|ajouter|add/i }).first()
|
|
: page.getByRole('button', { name: /créer|create|sauvegarder|save|ok|ajouter|add/i }).first();
|
|
if (!(await saveBtn.isVisible().catch(() => false))) {
|
|
console.log(' Save button not found after clicking create (skipping)');
|
|
return;
|
|
}
|
|
|
|
await saveBtn.click();
|
|
await page.waitForTimeout(1_000);
|
|
|
|
const body = await page.textContent('body') || '';
|
|
expect(body).not.toMatch(/crash|TypeError|Cannot read/i);
|
|
|
|
const hasError = /required|obligatoire|titre|title|nom|name|vide|empty/i.test(body);
|
|
|
|
const nameInput = page.getByLabel(/nom|name|titre|title/i).first()
|
|
.or(page.getByPlaceholder(/nom|name|titre/i).first());
|
|
const validationMessage = await nameInput.evaluate(
|
|
(el: HTMLInputElement) => el.validationMessage,
|
|
).catch(() => '');
|
|
|
|
const validated = hasError || validationMessage.length > 0;
|
|
console.log(` Empty playlist title: ${validated ? 'error shown' : 'form blocked or handled'}`);
|
|
});
|
|
|
|
test('16. Creer playlist avec titre valide fonctionne', async ({ page }) => {
|
|
await navigateTo(page, '/playlists');
|
|
|
|
const createBtn = page.getByRole('button', { name: /créer|create|nouvelle|new/i }).first()
|
|
.or(page.getByRole('link', { name: /créer|create|nouvelle|new/i }).first());
|
|
|
|
if (!(await createBtn.isVisible().catch(() => false))) {
|
|
console.log(' Create playlist button not found (skipping)');
|
|
return;
|
|
}
|
|
|
|
await createBtn.click();
|
|
await page.waitForTimeout(1_000);
|
|
|
|
const nameInput = page.getByLabel(/nom|name|titre|title/i).first()
|
|
.or(page.getByPlaceholder(/nom|name|titre/i).first());
|
|
|
|
if (!(await nameInput.isVisible().catch(() => false))) {
|
|
console.log(' Playlist name input not found (skipping)');
|
|
return;
|
|
}
|
|
|
|
const playlistName = `E2E Validation Test ${Date.now()}`;
|
|
await nameInput.fill(playlistName);
|
|
|
|
// Scope to dialog to avoid strict mode violation (sidebar "Create" + dialog "Create")
|
|
const dialog = page.locator('[role="dialog"]').first();
|
|
const dialogVisible = await dialog.isVisible().catch(() => false);
|
|
let saveBtn: import('@playwright/test').Locator;
|
|
if (dialogVisible) {
|
|
saveBtn = dialog.getByRole('button', { name: /créer|create|sauvegarder|save|ok|ajouter|add/i }).first();
|
|
} else {
|
|
// Fallback: also try [data-state="open"] overlays
|
|
const overlay = page.locator('[data-state="open"]').first();
|
|
const overlayVisible = await overlay.isVisible().catch(() => false);
|
|
if (overlayVisible) {
|
|
saveBtn = overlay.getByRole('button', { name: /créer|create|sauvegarder|save|ok|ajouter|add/i }).first();
|
|
} else {
|
|
saveBtn = page.getByRole('button', { name: /créer|create|sauvegarder|save|ok|ajouter|add/i }).first();
|
|
}
|
|
}
|
|
await saveBtn.click();
|
|
|
|
await page.waitForTimeout(3_000);
|
|
|
|
const body = await page.textContent('body') || '';
|
|
expect(body).not.toMatch(/crash|TypeError|Cannot read/i);
|
|
|
|
// Should either show the new playlist or redirect to it
|
|
const success = body.includes(playlistName) || page.url().includes('/playlists/');
|
|
console.log(` Create playlist with title: ${success ? 'success' : 'check behavior'}`);
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// SETTINGS FORMS VALIDATION
|
|
// =============================================================================
|
|
|
|
test.describe('FORMS — Settings forms validation @feature-forms', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
|
await navigateTo(page, '/settings');
|
|
});
|
|
|
|
test('17. Changer mot de passe — champs vides affiche erreur', async ({ page }) => {
|
|
// Find the password change section
|
|
const passwordSection = page.getByText(/changer.*mot de passe|change.*password|modifier.*mot de passe/i);
|
|
if (!(await passwordSection.isVisible().catch(() => false))) {
|
|
console.log(' Password change section not found (skipping)');
|
|
return;
|
|
}
|
|
|
|
// Look for a submit button in the password section
|
|
const changeBtn = page.getByRole('button', { name: /changer|change|modifier|update|save|sauvegarder/i });
|
|
const allButtons = await changeBtn.all();
|
|
|
|
// Try clicking the button closest to password section
|
|
for (const btn of allButtons) {
|
|
const btnText = await btn.textContent().catch(() => '');
|
|
if (/password|mot de passe|changer|change|modifier/i.test(btnText || '')) {
|
|
await btn.click();
|
|
break;
|
|
}
|
|
}
|
|
// Fallback: click first matching button
|
|
if (allButtons.length > 0) {
|
|
await allButtons[0].click();
|
|
}
|
|
|
|
await page.waitForTimeout(1_000);
|
|
|
|
const body = await page.textContent('body') || '';
|
|
expect(body).not.toMatch(/crash|TypeError|Cannot read/i);
|
|
|
|
const hasError = /required|obligatoire|vide|empty|remplir|fill/i.test(body);
|
|
console.log(` Empty password change: ${hasError ? 'error shown' : 'handled'}`);
|
|
});
|
|
|
|
test('18. Changer mot de passe — nouveau != confirmation affiche erreur', async ({ page }) => {
|
|
// Find password fields
|
|
const currentPassword = page.getByLabel(/actuel|current/i).first()
|
|
.or(page.locator('input[name*="current_password"]').first());
|
|
const newPassword = page.getByLabel(/nouveau|new/i).first()
|
|
.or(page.locator('input[name*="new_password"]').first());
|
|
const confirmPassword = page.getByLabel(/confirm/i).first()
|
|
.or(page.locator('input[name*="confirm"]').first());
|
|
|
|
if (!(await currentPassword.isVisible().catch(() => false))) {
|
|
console.log(' Password change fields not found (skipping)');
|
|
return;
|
|
}
|
|
|
|
await currentPassword.fill('OldPassword123!');
|
|
await newPassword.fill('NewPassword123!@#');
|
|
await confirmPassword.fill('DifferentPassword456!@#');
|
|
|
|
const changeBtn = page.getByRole('button', { name: /changer|change|modifier|update|save|sauvegarder/i }).first();
|
|
await changeBtn.click();
|
|
|
|
await page.waitForTimeout(1_000);
|
|
|
|
// Should stay on settings and show error
|
|
const body = await page.textContent('body') || '';
|
|
expect(body).not.toMatch(/crash|TypeError|Cannot read/i);
|
|
|
|
const hasError = /ne correspondent pas|do not match|don't match|mismatch|identiques/i.test(body);
|
|
console.log(` Mismatched new passwords: ${hasError ? 'error shown' : 'handled'}`);
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// SEARCH FORM VALIDATION
|
|
// =============================================================================
|
|
|
|
test.describe('FORMS — Search form validation @feature-forms', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
|
});
|
|
|
|
test('19. Recherche vide ne crash pas, affiche etat initial', async ({ page }) => {
|
|
await navigateTo(page, '/search');
|
|
|
|
const searchInput = page.locator('input[role="combobox"][aria-label="Search"]')
|
|
.or(page.getByPlaceholder(/search for tracks/i))
|
|
.or(page.locator('[role="search"] input'));
|
|
|
|
if (!(await searchInput.first().isVisible().catch(() => false))) {
|
|
console.log(' Search input not found (skipping)');
|
|
return;
|
|
}
|
|
|
|
// Clear and press Enter
|
|
await searchInput.first().fill('');
|
|
await searchInput.first().press('Enter');
|
|
await page.waitForTimeout(1_000);
|
|
|
|
await assertNotBroken(page);
|
|
|
|
const body = await page.textContent('body') || '';
|
|
expect(body).not.toMatch(/crash|TypeError|Cannot read/i);
|
|
console.log(' Empty search: no crash, page stable');
|
|
});
|
|
|
|
test('20. Recherche avec caracteres speciaux ne crash pas', async ({ page }) => {
|
|
await navigateTo(page, '/search');
|
|
|
|
const searchInput = page.locator('input[role="combobox"][aria-label="Search"]')
|
|
.or(page.getByPlaceholder(/search for tracks/i))
|
|
.or(page.locator('[role="search"] input'));
|
|
|
|
if (!(await searchInput.first().isVisible().catch(() => false))) {
|
|
console.log(' Search input not found (skipping)');
|
|
return;
|
|
}
|
|
|
|
const specialInputs = [
|
|
'<script>alert(1)</script>',
|
|
"'; DROP TABLE tracks; --",
|
|
'../../etc/passwd',
|
|
'%00%0d%0a',
|
|
String.raw`\x00\x1f`,
|
|
];
|
|
|
|
for (const input of specialInputs) {
|
|
await searchInput.first().fill(input);
|
|
await page.waitForTimeout(500);
|
|
|
|
const body = await page.textContent('body') || '';
|
|
expect(body).not.toMatch(/crash|TypeError|Cannot read|Unexpected token/i);
|
|
}
|
|
|
|
console.log(' Special characters in search: no crash');
|
|
});
|
|
|
|
test('21. Recherche avec espaces seuls ne crash pas', async ({ page }) => {
|
|
await navigateTo(page, '/search');
|
|
|
|
const searchInput = page.locator('input[role="combobox"][aria-label="Search"]')
|
|
.or(page.getByPlaceholder(/search for tracks/i))
|
|
.or(page.locator('[role="search"] input'));
|
|
|
|
if (!(await searchInput.first().isVisible().catch(() => false))) {
|
|
console.log(' Search input not found (skipping)');
|
|
return;
|
|
}
|
|
|
|
await searchInput.first().fill(' ');
|
|
await searchInput.first().press('Enter');
|
|
await page.waitForTimeout(1_000);
|
|
|
|
await assertNotBroken(page);
|
|
console.log(' Whitespace-only search: no crash');
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// COMMENT FORM VALIDATION
|
|
// =============================================================================
|
|
|
|
test.describe('FORMS — Comment form validation @feature-forms', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
|
});
|
|
|
|
test('22. Soumettre commentaire vide ne l\'envoie pas', async ({ page }) => {
|
|
// Navigate to a track page or discover page where comments might be
|
|
await navigateTo(page, '/discover');
|
|
|
|
// Try to find a track link and navigate to its detail page
|
|
const trackLink = page.locator('a[href*="/tracks/"]').first();
|
|
if (await trackLink.isVisible().catch(() => false)) {
|
|
await trackLink.click();
|
|
await page.waitForLoadState('networkidle').catch(() => {});
|
|
}
|
|
|
|
// Look for comment input
|
|
const commentInput = page.getByPlaceholder(/comment|ajouter.*commentaire|écrire/i).first()
|
|
.or(page.locator('textarea[name*="comment"]').first())
|
|
.or(page.getByLabel(/comment/i).first());
|
|
|
|
if (!(await commentInput.isVisible().catch(() => false))) {
|
|
console.log(' Comment form not found on page (skipping)');
|
|
return;
|
|
}
|
|
|
|
// Leave comment empty and try to submit
|
|
await commentInput.fill('');
|
|
|
|
const submitBtn = page.getByRole('button', { name: /publier|post|envoyer|send|comment/i }).first();
|
|
if (!(await submitBtn.isVisible().catch(() => false))) {
|
|
console.log(' Comment submit button not found (skipping)');
|
|
return;
|
|
}
|
|
|
|
// Track if a request was sent
|
|
let commentRequestSent = false;
|
|
page.on('request', (req) => {
|
|
if (req.url().includes('/comment') && req.method() === 'POST') {
|
|
commentRequestSent = true;
|
|
}
|
|
});
|
|
|
|
await submitBtn.click();
|
|
await page.waitForTimeout(1_500);
|
|
|
|
const body = await page.textContent('body') || '';
|
|
expect(body).not.toMatch(/crash|TypeError|Cannot read/i);
|
|
|
|
// The button might be disabled or validation might prevent sending
|
|
console.log(` Empty comment: ${commentRequestSent ? 'request sent (check server validation)' : 'not sent (client validation)'}`);
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// CONTACT / SUPPORT FORM VALIDATION
|
|
// =============================================================================
|
|
|
|
test.describe('FORMS — Support/Contact form validation @feature-forms', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
|
});
|
|
|
|
test('23. Soumettre formulaire support vide affiche erreur', async ({ page }) => {
|
|
await navigateTo(page, '/support');
|
|
|
|
// Give the page a moment to settle (redirects, lazy loading)
|
|
await page.waitForTimeout(1_000);
|
|
|
|
const url = page.url();
|
|
const body = await page.textContent('body') || '';
|
|
|
|
// /support may not exist — if we landed on 404, a redirect, or unrelated page, skip gracefully
|
|
if (url.includes('/404') || url.includes('/login') || url.includes('/dashboard') || !/support|aide|help|ticket|contact/i.test(body)) {
|
|
console.log(` Support page not found (ended at ${url}) — skipping`);
|
|
return;
|
|
}
|
|
|
|
// Look for a submit button — if the support page has no form, skip
|
|
const submitBtn = page.getByRole('button', { name: /envoyer|send|soumettre|submit|créer|create|send message/i }).first();
|
|
const submitVisible = await submitBtn.isVisible({ timeout: 5_000 }).catch(() => false);
|
|
if (!submitVisible) {
|
|
console.log(' Support submit button not found — support form may not exist (skipping)');
|
|
return;
|
|
}
|
|
|
|
// The support form button is disabled when the form is empty (client validation).
|
|
// Check if button is disabled — that IS the expected validation behavior.
|
|
const isDisabled = await submitBtn.isDisabled().catch(() => false);
|
|
if (isDisabled) {
|
|
console.log(' Empty support form: submit button disabled (client validation works)');
|
|
return;
|
|
}
|
|
|
|
// If not disabled, try clicking (force: true to bypass actionability)
|
|
await submitBtn.click({ force: true, timeout: 5_000 });
|
|
await page.waitForTimeout(1_000);
|
|
|
|
const bodyAfter = await page.textContent('body') || '';
|
|
expect(bodyAfter).not.toMatch(/crash|TypeError|Cannot read/i);
|
|
|
|
const hasError = /required|obligatoire|vide|empty|remplir|fill|erreur|error/i.test(bodyAfter);
|
|
console.log(` Empty support form: ${hasError ? 'error shown' : 'handled'}`);
|
|
});
|
|
});
|
|
|
|
// =============================================================================
|
|
// PROFILE EDIT FORM VALIDATION
|
|
// =============================================================================
|
|
|
|
test.describe('FORMS — Profile edit form validation @feature-forms', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
|
|
});
|
|
|
|
test('24. Vider le champ username dans le profil affiche erreur', async ({ page }) => {
|
|
await navigateTo(page, '/settings');
|
|
|
|
const usernameInput = page.getByLabel(/username|nom d'utilisateur|pseudo/i).first()
|
|
.or(page.locator('input[name*="username"]').first());
|
|
|
|
if (!(await usernameInput.isVisible().catch(() => false))) {
|
|
// Try navigating to /profile/edit
|
|
await navigateTo(page, '/profile/edit');
|
|
const usernameInput2 = page.getByLabel(/username|nom d'utilisateur|pseudo/i).first()
|
|
.or(page.locator('input[name*="username"]').first());
|
|
|
|
if (!(await usernameInput2.isVisible().catch(() => false))) {
|
|
console.log(' Username field not found in settings or profile (skipping)');
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Clear the username field
|
|
const input = page.getByLabel(/username|nom d'utilisateur|pseudo/i).first()
|
|
.or(page.locator('input[name*="username"]').first());
|
|
await input.fill('');
|
|
|
|
const saveBtn = page.getByRole('button', { name: /save|sauvegarder|mettre à jour|update/i }).first();
|
|
if (await saveBtn.isVisible().catch(() => false)) {
|
|
await saveBtn.click();
|
|
await page.waitForTimeout(1_000);
|
|
}
|
|
|
|
const body = await page.textContent('body') || '';
|
|
expect(body).not.toMatch(/crash|TypeError|Cannot read/i);
|
|
|
|
const hasError = /required|obligatoire|vide|empty|username/i.test(body);
|
|
console.log(` Empty username: ${hasError ? 'error shown' : 'handled'}`);
|
|
});
|
|
});
|