veza/tests/e2e/25-profile.spec.ts
senke 20a16f7cbe test: add comprehensive e2e test suite (34 spec files)
New tests/e2e/ suite covering:
- Auth, navigation, player, tracks, playlists
- Search, discover, social, marketplace, chat
- Accessibility, API, workflows, edge cases
- Routes coverage, forms validation, modals
- Empty states, responsive, network errors
- Error boundary, performance, visual regression
- Cross-browser, profile, smoke, upload
- Storybook, deep pages, visual bugs
- Includes fixtures, helpers, global setup/teardown
- Playwright config and coverage map

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 11:36:22 +01:00

597 lines
18 KiB
TypeScript

import { test, expect } from '@playwright/test';
import { loginViaAPI, CONFIG, navigateTo, waitForToast } from './helpers';
/**
* Profile E2E Test Suite
*
* Tests user profile management:
* - Display profile
* - Update username, bio
* - Change password
* - Upload avatar
* - Field validation
* - Account information display
*/
/**
* Check whether login succeeded (page is no longer on /login).
*/
function isLoggedIn(page: import('@playwright/test').Page): boolean {
return !page.url().includes('/login');
}
test.describe('USER PROFILE MANAGEMENT', () => {
test.describe.configure({ timeout: 60000 });
test.beforeEach(async ({ page }) => {
// Capture errors for diagnostics
page.on('console', (msg) => {
if (msg.type() === 'error') {
console.log(`[console.error] ${msg.text()}`);
}
});
page.on('response', (response) => {
if (response.status() >= 500) {
console.log(`[network error] ${response.request().method()} ${response.url()}: ${response.status()}`);
}
});
await loginViaAPI(page, CONFIG.users.listener.email, CONFIG.users.listener.password);
});
test('should display user profile information', async ({ page }) => {
if (!isLoggedIn(page)) {
test.skip(true, 'Login failed — still on /login');
return;
}
await navigateTo(page, '/profile');
// Try sidebar navigation first
const profileLinkSidebar = page
.locator(
'[role="menuitem"]:has-text("Profil"), [role="menuitem"]:has-text("Profile")',
)
.first();
const isSidebarLinkVisible = await profileLinkSidebar
.isVisible({ timeout: 3000 })
.catch(() => false);
if (isSidebarLinkVisible) {
await profileLinkSidebar.click();
} else {
// Try user menu
const userMenu = page
.locator(
'[data-testid="user-menu"], button[aria-label*="user" i], button[aria-label*="profile" i]',
)
.first();
const isUserMenuVisible = await userMenu.isVisible().catch(() => false);
if (isUserMenuVisible) {
await userMenu.click();
await page.waitForTimeout(500);
await page
.locator(
'[role="menuitem"]:has-text("Profil"), [role="menuitem"]:has-text("Profile")',
)
.first()
.click()
.catch(() => {});
} else {
await navigateTo(page, '/profile');
}
}
await page
.waitForURL(/\/profile|\/settings/, { timeout: 15000 })
.catch(() => {});
await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
const pageTitle = page
.locator(
'h1:has-text("Profil"), h1:has-text("Profile"), h2:has-text("Profil"), h2:has-text("Profile"), [class*="CardTitle"], [class*="card-title"]',
)
.first();
const titleVisible = await pageTitle
.isVisible({ timeout: 5000 })
.catch(() => false);
if (!titleVisible) {
const currentUrl = page.url();
expect(
currentUrl.includes('/profile') || currentUrl.includes('/settings'),
).toBeTruthy();
}
// Profile page may show username as text or input — verify page loaded with content
const body = await page.textContent('body') || '';
expect(body.length).toBeGreaterThan(50);
// Verify we're on a profile-related page
const currentUrl = page.url();
expect(
currentUrl.includes('/profile') || currentUrl.includes('/settings') || currentUrl.includes('/dashboard'),
).toBeTruthy();
});
test('should update username successfully', async ({ page }) => {
if (!isLoggedIn(page)) {
test.skip(true, 'Login failed — still on /login');
return;
}
await navigateTo(page, '/profile');
test.setTimeout(60000);
const usernameField = page
.locator('input#username, input[name="username"], input[placeholder*="username" i]')
.first();
const isUsernameVisible = await usernameField
.isVisible({ timeout: 15000 })
.catch(() => false);
if (!isUsernameVisible) {
test.skip(true, 'Username field not found on profile page');
return;
}
// Wait for field to be populated
await page
.waitForFunction(
(selector) => {
const input = document.querySelector(selector) as HTMLInputElement;
return input && input.value && input.value.trim().length > 0;
},
'input#username, input[name="username"]',
{ timeout: 15000 },
)
.catch(() => {});
// Enable edit mode if needed
const isDisabled = await usernameField.isDisabled().catch(() => false);
if (isDisabled) {
const editButton = page
.locator(
'button:has-text("Edit"), button:has-text("Modifier"), button:has-text("profile.edit")',
)
.first();
if (await editButton.isVisible({ timeout: 5000 }).catch(() => false)) {
await editButton.click();
await page.waitForTimeout(500);
await expect(usernameField).toBeEnabled({ timeout: 5000 });
}
}
const newUsername = `testuser_${Date.now()}`;
await usernameField.clear();
await usernameField.fill(newUsername);
const submitButton = page
.locator(
'button:has-text("Save"), button:has-text("Enregistrer"), button[type="submit"]',
)
.first();
const submitVisible = await submitButton.isVisible({ timeout: 5000 }).catch(() => false);
if (!submitVisible) {
test.skip(true, 'Submit button not found on profile page');
return;
}
const updatePromise = page.waitForResponse(
(response) =>
response.url().includes('/users') &&
response.request().method() === 'PUT' &&
response.status() < 500,
{ timeout: 15000 },
);
await submitButton.click();
try {
const response = await updatePromise;
const status = response.status();
if (status === 200 || status === 204) {
const toastText = await waitForToast(page);
console.log(`Toast: ${toastText}`);
}
} catch {
console.warn('Update request timeout');
}
if (page.isClosed()) return;
try {
await page.reload({ waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForLoadState('networkidle', { timeout: 30000 });
const updatedUsernameField = page
.locator('input[name="username"], input#username')
.first();
const updatedVisible = await updatedUsernameField
.isVisible({ timeout: 15000 })
.catch(() => false);
if (updatedVisible) {
await page
.waitForFunction(
(selector) => {
const input = document.querySelector(selector) as HTMLInputElement;
return input && input.value && input.value.trim().length > 0;
},
'input[name="username"], input#username',
{ timeout: 15000 },
)
.catch(() => {});
const currentValue = await updatedUsernameField.inputValue();
expect(currentValue).toBe(newUsername);
}
} catch {
console.warn('Reload failed or timeout');
}
});
test('should update bio successfully', async ({ page }) => {
if (!isLoggedIn(page)) {
test.skip(true, 'Login failed — still on /login');
return;
}
await navigateTo(page, '/profile');
const bioField = page.locator('input#bio, textarea#bio, [id="bio"]').first();
const bioExists = await bioField
.isVisible({ timeout: 5000 })
.catch(() => false);
if (!bioExists) {
test.skip(true, 'Bio field not found on profile page');
return;
}
const isDisabled = await bioField.isDisabled().catch(() => false);
if (isDisabled) {
const editButton = page
.locator('button:has-text("Edit"), button:has-text("Modifier")')
.first();
if (await editButton.isVisible({ timeout: 5000 }).catch(() => false)) {
await editButton.click();
await page.waitForTimeout(500);
await expect(bioField).toBeEnabled({ timeout: 5000 });
}
}
const newBio = `This is a test bio updated at ${new Date().toISOString()}`;
await bioField.clear();
await bioField.fill(newBio);
const submitButton = page
.locator(
'button:has-text("Save"), button:has-text("Enregistrer"), button[type="submit"]',
)
.first();
const submitVisible = await submitButton.isVisible({ timeout: 5000 }).catch(() => false);
if (!submitVisible) {
test.skip(true, 'Submit button not found on profile page');
return;
}
await submitButton.click();
await waitForToast(page);
await page.reload({ waitUntil: 'domcontentloaded' });
await page.waitForLoadState('networkidle').catch(() => {});
const updatedBioField = page
.locator('textarea[name="bio"], textarea#bio, input#bio')
.first();
const updatedVisible = await updatedBioField
.isVisible({ timeout: 15000 })
.catch(() => false);
if (updatedVisible) {
const currentValue = await updatedBioField.inputValue();
expect(currentValue).toBe(newBio);
}
});
test('should change password successfully', async ({ page }) => {
if (!isLoggedIn(page)) {
test.skip(true, 'Login failed — still on /login');
return;
}
await navigateTo(page, '/profile');
const changePasswordButton = page
.locator(
'button:has-text("Change password"), button:has-text("Changer"), a:has-text("Security"), a:has-text("Securite")',
)
.first();
const isChangePasswordVisible = await changePasswordButton
.isVisible({ timeout: 5000 })
.catch(() => false);
if (!isChangePasswordVisible) {
test.skip(true, 'Change password button not found on profile page');
return;
}
await changePasswordButton.click();
await page.waitForTimeout(500);
const currentPasswordField = page
.locator(
'input[name="currentPassword"], input[name="current_password"], input#currentPassword',
)
.first();
const newPasswordField = page
.locator(
'input[name="newPassword"], input[name="new_password"], input#newPassword',
)
.first();
const confirmPasswordField = page
.locator(
'input[name="confirmPassword"], input[name="confirm_password"], input#confirmPassword',
)
.first();
const areFieldsVisible = await currentPasswordField
.isVisible({ timeout: 5000 })
.catch(() => false);
if (!areFieldsVisible) {
test.skip(true, 'Password change fields not found');
return;
}
await currentPasswordField.fill('password123');
const newPassword = `NewPass${Date.now()}!`;
await newPasswordField.fill(newPassword);
await confirmPasswordField.fill(newPassword);
const submitButton = page
.locator(
'button:has-text("Change"), button:has-text("Update"), button[type="submit"]',
)
.first();
await submitButton.click();
try {
await waitForToast(page);
await page.waitForTimeout(1000);
// Restore old password
await currentPasswordField.fill(newPassword);
await newPasswordField.fill('password123');
await confirmPasswordField.fill('password123');
await submitButton.click();
await page.waitForTimeout(2000);
} catch {
console.warn('Password change failed or timed out');
}
});
test('should upload profile avatar', async ({ page }) => {
if (!isLoggedIn(page)) {
test.skip(true, 'Login failed — still on /login');
return;
}
await navigateTo(page, '/profile');
const avatarInput = page
.locator('input[type="file"][accept*="image"], input[name="avatar"]')
.first();
const isAvatarInputVisible = await avatarInput
.isVisible({ timeout: 5000 })
.catch(() => false);
if (!isAvatarInputVisible) {
const avatarContainer = page
.locator(
'[data-testid="avatar"], img[alt*="avatar" i], button:has-text("Upload")',
)
.first();
const isAvatarContainerVisible = await avatarContainer
.isVisible()
.catch(() => false);
if (isAvatarContainerVisible) {
await avatarContainer.click();
await page.waitForTimeout(500);
} else {
test.skip(true, 'Avatar upload not found on profile page');
return;
}
}
const imageBuffer = Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
'base64',
);
const fileInputFinal = page
.locator('input[type="file"][accept*="image"]')
.first();
const fileInputVisible = await fileInputFinal.count();
if (fileInputVisible === 0) {
test.skip(true, 'File input not found after clicking avatar');
return;
}
await fileInputFinal.setInputFiles({
name: 'avatar.png',
mimeType: 'image/png',
buffer: imageBuffer,
});
await page.waitForTimeout(2000);
const successVisible = await page
.locator('text=/uploaded|success|succes/i')
.isVisible({ timeout: 5000 })
.catch(() => false);
if (successVisible) {
console.log('Avatar uploaded successfully');
} else {
console.log('Avatar upload completed (no explicit success message)');
}
});
test('should validate username length', async ({ page }) => {
if (!isLoggedIn(page)) {
test.skip(true, 'Login failed — still on /login');
return;
}
await navigateTo(page, '/profile');
test.setTimeout(60000);
const usernameField = page
.locator('input#username, input[name="username"], input[placeholder*="username" i]')
.first();
const isUsernameVisible = await usernameField
.isVisible({ timeout: 15000 })
.catch(() => false);
if (!isUsernameVisible) {
test.skip(true, 'Username field not found on profile page');
return;
}
const isDisabled = await usernameField.isDisabled().catch(() => false);
if (isDisabled) {
const editButton = page
.locator('button:has-text("Edit"), button:has-text("Modifier")')
.first();
if (await editButton.isVisible({ timeout: 5000 }).catch(() => false)) {
await editButton.click();
await page.waitForTimeout(500);
await expect(usernameField).toBeEnabled({ timeout: 5000 });
}
}
await usernameField.clear();
await usernameField.fill('ab');
await usernameField.blur();
await page.waitForTimeout(500);
const errorMessageSelectors = [
'p.text-destructive',
'p.text-red-500',
'p.text-red-600',
'[role="alert"]',
'.text-error',
'.error-message',
'text=/trop court|too short|minimum|at least|caracteres|characters/i',
];
let validationDetected = false;
for (const selector of errorMessageSelectors) {
const errorElement = page.locator(selector).first();
const isVisible = await errorElement
.isVisible({ timeout: 2000 })
.catch(() => false);
if (isVisible) {
const errorText = (await errorElement.textContent().catch(() => '')) || '';
if (
errorText.toLowerCase().includes('short') ||
errorText.toLowerCase().includes('court') ||
errorText.toLowerCase().includes('minimum') ||
errorText.toLowerCase().includes('caractere')
) {
validationDetected = true;
break;
}
}
}
if (!validationDetected) {
const ariaInvalid = await usernameField.getAttribute('aria-invalid');
if (ariaInvalid === 'true') {
validationDetected = true;
}
}
if (!validationDetected) {
const submitButton = page
.locator(
'button:has-text("Save"), button:has-text("Enregistrer"), button[type="submit"]',
)
.first();
const isSubmitDisabled = await submitButton.isDisabled().catch(() => false);
if (isSubmitDisabled) {
validationDetected = true;
}
}
if (!validationDetected) {
const submitButton = page
.locator(
'button:has-text("Save"), button:has-text("Enregistrer"), button[type="submit"]',
)
.first();
if (await submitButton.isVisible({ timeout: 3000 }).catch(() => false)) {
await submitButton.click();
await page.waitForTimeout(500);
const errorAfterSubmit = page
.locator(
'text=/trop court|too short|minimum|at least|caracteres|characters|erreur|error/i, [role="alert"]',
)
.first();
const isErrorAfterSubmit = await errorAfterSubmit
.isVisible({ timeout: 3000 })
.catch(() => false);
if (isErrorAfterSubmit) {
validationDetected = true;
}
}
}
if (!validationDetected) {
const isInvalid = await usernameField.evaluate(
(el: HTMLInputElement) => !el.validity.valid,
);
if (isInvalid) {
validationDetected = true;
}
}
expect(validationDetected).toBeTruthy();
});
test('should display account information', async ({ page }) => {
if (!isLoggedIn(page)) {
test.skip(true, 'Login failed — still on /login');
return;
}
await navigateTo(page, '/profile');
const emailDisplay = page
.locator('input[name="email"], input[type="email"], text=/email/i')
.first();
const isEmailVisible = await emailDisplay
.isVisible({ timeout: 5000 })
.catch(() => false);
if (isEmailVisible) {
console.log('Email displayed');
}
const accountInfo = page
.locator('text=/member since|membre depuis|created|cree/i')
.first();
const isAccountInfoVisible = await accountInfo
.isVisible({ timeout: 5000 })
.catch(() => false);
if (isAccountInfoVisible) {
console.log('Account information displayed');
} else {
console.log('Additional account info not displayed');
}
});
});