356 lines
13 KiB
TypeScript
356 lines
13 KiB
TypeScript
|
|
import { test, expect, Page } from '@playwright/test';
|
||
|
|
import * as path from 'path';
|
||
|
|
|
||
|
|
// Test data
|
||
|
|
const testEmail = `user+profile-${Date.now()}@lab.veza`;
|
||
|
|
const testPassword = `V3za!profile-${Date.now()}`;
|
||
|
|
const avatarPath = path.join(__dirname, '../../data/images/avatar.jpg');
|
||
|
|
|
||
|
|
// Helper to register and login
|
||
|
|
async function registerAndLogin(page: Page) {
|
||
|
|
await page.goto('/register');
|
||
|
|
await page.fill('input[type="email"]', testEmail);
|
||
|
|
await page.fill('input[type="password"]', testPassword);
|
||
|
|
|
||
|
|
const confirmField = page.locator('input[name="confirmPassword"]');
|
||
|
|
if (await confirmField.isVisible()) {
|
||
|
|
await confirmField.fill(testPassword);
|
||
|
|
}
|
||
|
|
|
||
|
|
await page.locator('button[type="submit"]').click();
|
||
|
|
await expect(page).toHaveURL(/\/(dashboard|home|app|profile)/, { timeout: 10000 });
|
||
|
|
}
|
||
|
|
|
||
|
|
test.describe('User Profile Management', () => {
|
||
|
|
test.beforeEach(async ({ page }) => {
|
||
|
|
await registerAndLogin(page);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should load profile page', async ({ page }) => {
|
||
|
|
await page.goto('/profile');
|
||
|
|
|
||
|
|
// Check main elements
|
||
|
|
await expect(page.locator('[data-testid="profile-container"], .profile-container, #profile')).toBeVisible();
|
||
|
|
|
||
|
|
// Should show user email
|
||
|
|
await expect(page.locator(`text="${testEmail}"`)).toBeVisible();
|
||
|
|
|
||
|
|
// Should have edit button
|
||
|
|
await expect(page.locator('button:has-text("Edit"), [data-testid="edit-profile"]')).toBeVisible();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should display user information', async ({ page }) => {
|
||
|
|
await page.goto('/profile');
|
||
|
|
|
||
|
|
// Check for user fields
|
||
|
|
await expect(page.locator('text=/email/i').locator('xpath=following-sibling::*')).toContainText(testEmail);
|
||
|
|
|
||
|
|
// Check for other profile fields
|
||
|
|
const fields = ['nickname', 'username', 'display name', 'bio', 'about'];
|
||
|
|
for (const field of fields) {
|
||
|
|
const fieldElement = page.locator(`text=/${field}/i`);
|
||
|
|
if (await fieldElement.isVisible({ timeout: 1000 }).catch(() => false)) {
|
||
|
|
// Field exists in profile
|
||
|
|
expect(fieldElement).toBeTruthy();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should edit profile nickname', async ({ page }) => {
|
||
|
|
await page.goto('/profile');
|
||
|
|
|
||
|
|
// Click edit button
|
||
|
|
await page.locator('button:has-text("Edit"), [data-testid="edit-profile"]').click();
|
||
|
|
|
||
|
|
// Find nickname field
|
||
|
|
const nicknameInput = page.locator('input[name="nickname"], input[placeholder*="nickname"], input[aria-label*="nickname"]');
|
||
|
|
|
||
|
|
if (await nicknameInput.isVisible()) {
|
||
|
|
const newNickname = `TestUser${Date.now()}`;
|
||
|
|
await nicknameInput.fill(newNickname);
|
||
|
|
|
||
|
|
// Save changes
|
||
|
|
await page.locator('button:has-text("Save"), button[type="submit"]').click();
|
||
|
|
|
||
|
|
// Should show success message
|
||
|
|
await expect(page.locator('text=/success|updated|saved/i')).toBeVisible({ timeout: 5000 });
|
||
|
|
|
||
|
|
// Nickname should be updated
|
||
|
|
await expect(page.locator(`text="${newNickname}"`)).toBeVisible();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should upload avatar image', async ({ page }) => {
|
||
|
|
await page.goto('/profile');
|
||
|
|
|
||
|
|
// Find avatar upload
|
||
|
|
const avatarSection = page.locator('[data-testid="avatar-section"], .avatar-section');
|
||
|
|
const uploadButton = avatarSection.locator('button:has-text("Upload"), button:has-text("Change"), input[type="file"]');
|
||
|
|
|
||
|
|
if (await uploadButton.isVisible()) {
|
||
|
|
// Upload avatar
|
||
|
|
if (uploadButton.locator('input[type="file"]').isVisible()) {
|
||
|
|
await uploadButton.setInputFiles(avatarPath);
|
||
|
|
} else {
|
||
|
|
await uploadButton.click();
|
||
|
|
const fileInput = page.locator('input[type="file"][accept*="image"]');
|
||
|
|
await fileInput.setInputFiles(avatarPath);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Wait for upload
|
||
|
|
await expect(page.locator('text=/upload.*success|avatar.*updated/i')).toBeVisible({ timeout: 10000 });
|
||
|
|
|
||
|
|
// Avatar should be visible
|
||
|
|
const avatarImg = page.locator('img[alt*="avatar"], img[alt*="profile"], .avatar img');
|
||
|
|
await expect(avatarImg).toBeVisible();
|
||
|
|
|
||
|
|
const src = await avatarImg.getAttribute('src');
|
||
|
|
expect(src).not.toContain('default');
|
||
|
|
expect(src).not.toContain('placeholder');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should validate profile form inputs', async ({ page }) => {
|
||
|
|
await page.goto('/profile');
|
||
|
|
|
||
|
|
// Enter edit mode
|
||
|
|
await page.locator('button:has-text("Edit")').click();
|
||
|
|
|
||
|
|
// Test bio/about character limit
|
||
|
|
const bioInput = page.locator('textarea[name="bio"], textarea[name="about"]');
|
||
|
|
|
||
|
|
if (await bioInput.isVisible()) {
|
||
|
|
// Try to enter very long text
|
||
|
|
const longText = 'a'.repeat(1000);
|
||
|
|
await bioInput.fill(longText);
|
||
|
|
|
||
|
|
// Should show character count or limit
|
||
|
|
const charCount = page.locator('.char-count, .character-count, text=/\\d+.*\\/.*\\d+/');
|
||
|
|
if (await charCount.isVisible()) {
|
||
|
|
const count = await charCount.textContent();
|
||
|
|
expect(count).toMatch(/\d+/);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Test URL field validation
|
||
|
|
const websiteInput = page.locator('input[name="website"], input[type="url"]');
|
||
|
|
|
||
|
|
if (await websiteInput.isVisible()) {
|
||
|
|
await websiteInput.fill('not-a-url');
|
||
|
|
await page.locator('button:has-text("Save")').click();
|
||
|
|
|
||
|
|
// Should show validation error
|
||
|
|
await expect(page.locator('text=/invalid.*url|valid.*url/i')).toBeVisible();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should show account creation date', async ({ page }) => {
|
||
|
|
await page.goto('/profile');
|
||
|
|
|
||
|
|
// Look for join date/created date
|
||
|
|
const datePattern = page.locator('text=/joined|created|member since/i');
|
||
|
|
|
||
|
|
if (await datePattern.isVisible()) {
|
||
|
|
const dateText = await datePattern.locator('xpath=following-sibling::*').textContent();
|
||
|
|
expect(dateText).toMatch(/\d{4}|\d{1,2}.*\d{1,2}/); // Year or date pattern
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should handle social media links', async ({ page }) => {
|
||
|
|
await page.goto('/profile');
|
||
|
|
|
||
|
|
// Enter edit mode
|
||
|
|
await page.locator('button:has-text("Edit")').click();
|
||
|
|
|
||
|
|
// Check for social media fields
|
||
|
|
const socialFields = ['twitter', 'github', 'linkedin', 'facebook'];
|
||
|
|
|
||
|
|
for (const platform of socialFields) {
|
||
|
|
const input = page.locator(`input[name="${platform}"], input[placeholder*="${platform}"]`);
|
||
|
|
|
||
|
|
if (await input.isVisible({ timeout: 1000 }).catch(() => false)) {
|
||
|
|
await input.fill(`https://${platform}.com/testuser`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Save
|
||
|
|
await page.locator('button:has-text("Save")').click();
|
||
|
|
|
||
|
|
// Links should be saved
|
||
|
|
for (const platform of socialFields) {
|
||
|
|
const link = page.locator(`a[href*="${platform}.com"]`);
|
||
|
|
if (await link.isVisible({ timeout: 1000 }).catch(() => false)) {
|
||
|
|
const href = await link.getAttribute('href');
|
||
|
|
expect(href).toContain(`${platform}.com/testuser`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should change password', async ({ page }) => {
|
||
|
|
await page.goto('/profile');
|
||
|
|
|
||
|
|
// Look for password change section
|
||
|
|
const changePasswordButton = page.locator('button:has-text("Change Password"), a:has-text("Change Password")');
|
||
|
|
|
||
|
|
if (await changePasswordButton.isVisible()) {
|
||
|
|
await changePasswordButton.click();
|
||
|
|
|
||
|
|
// Fill password change form
|
||
|
|
const currentPasswordInput = page.locator('input[name="currentPassword"], input[name="current_password"]');
|
||
|
|
const newPasswordInput = page.locator('input[name="newPassword"], input[name="new_password"]');
|
||
|
|
const confirmPasswordInput = page.locator('input[name="confirmPassword"], input[name="confirm_password"]');
|
||
|
|
|
||
|
|
await currentPasswordInput.fill(testPassword);
|
||
|
|
const newPassword = `V3za!new-${Date.now()}`;
|
||
|
|
await newPasswordInput.fill(newPassword);
|
||
|
|
await confirmPasswordInput.fill(newPassword);
|
||
|
|
|
||
|
|
// Submit
|
||
|
|
await page.locator('button:has-text("Update Password"), button:has-text("Change")').click();
|
||
|
|
|
||
|
|
// Should show success
|
||
|
|
await expect(page.locator('text=/password.*changed|password.*updated/i')).toBeVisible({ timeout: 5000 });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should show email preferences', async ({ page }) => {
|
||
|
|
await page.goto('/profile');
|
||
|
|
|
||
|
|
// Look for email preferences or notifications
|
||
|
|
const preferencesSection = page.locator('text=/email.*preferences|notifications|subscribe/i');
|
||
|
|
|
||
|
|
if (await preferencesSection.isVisible()) {
|
||
|
|
// Check for toggles
|
||
|
|
const toggles = page.locator('input[type="checkbox"], [role="switch"]');
|
||
|
|
const toggleCount = await toggles.count();
|
||
|
|
|
||
|
|
if (toggleCount > 0) {
|
||
|
|
// Toggle first preference
|
||
|
|
const firstToggle = toggles.first();
|
||
|
|
const wasChecked = await firstToggle.isChecked();
|
||
|
|
await firstToggle.click();
|
||
|
|
|
||
|
|
// State should change
|
||
|
|
expect(await firstToggle.isChecked()).toBe(!wasChecked);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should handle privacy settings', async ({ page }) => {
|
||
|
|
await page.goto('/profile');
|
||
|
|
|
||
|
|
// Look for privacy settings
|
||
|
|
const privacySection = page.locator('text=/privacy|visibility|public.*profile/i');
|
||
|
|
|
||
|
|
if (await privacySection.isVisible()) {
|
||
|
|
// Find privacy toggles
|
||
|
|
const privacyToggles = privacySection.locator('xpath=following-sibling::*').locator('input[type="checkbox"], select');
|
||
|
|
|
||
|
|
if (await privacyToggles.first().isVisible()) {
|
||
|
|
// Change a privacy setting
|
||
|
|
const firstToggle = privacyToggles.first();
|
||
|
|
|
||
|
|
if (await firstToggle.getAttribute('type') === 'checkbox') {
|
||
|
|
await firstToggle.click();
|
||
|
|
} else {
|
||
|
|
// It's a select
|
||
|
|
await firstToggle.selectOption({ index: 1 });
|
||
|
|
}
|
||
|
|
|
||
|
|
// Save if needed
|
||
|
|
const saveButton = page.locator('button:has-text("Save")');
|
||
|
|
if (await saveButton.isVisible()) {
|
||
|
|
await saveButton.click();
|
||
|
|
await expect(page.locator('text=/saved|updated/i')).toBeVisible();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should delete avatar', async ({ page }) => {
|
||
|
|
await page.goto('/profile');
|
||
|
|
|
||
|
|
// First upload an avatar
|
||
|
|
const uploadButton = page.locator('button:has-text("Upload"), input[type="file"][accept*="image"]');
|
||
|
|
if (await uploadButton.isVisible()) {
|
||
|
|
await uploadButton.setInputFiles(avatarPath);
|
||
|
|
await page.waitForTimeout(2000);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Look for delete avatar option
|
||
|
|
const deleteAvatarButton = page.locator('button:has-text("Remove Avatar"), button:has-text("Delete Avatar")');
|
||
|
|
|
||
|
|
if (await deleteAvatarButton.isVisible()) {
|
||
|
|
await deleteAvatarButton.click();
|
||
|
|
|
||
|
|
// Confirm if needed
|
||
|
|
const confirmButton = page.locator('button:has-text("Confirm"), button:has-text("Yes")');
|
||
|
|
if (await confirmButton.isVisible({ timeout: 1000 }).catch(() => false)) {
|
||
|
|
await confirmButton.click();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Avatar should be removed
|
||
|
|
await expect(page.locator('text=/avatar.*removed|avatar.*deleted/i')).toBeVisible();
|
||
|
|
|
||
|
|
// Should show default avatar
|
||
|
|
const avatarImg = page.locator('.avatar img');
|
||
|
|
const src = await avatarImg.getAttribute('src');
|
||
|
|
expect(src).toMatch(/default|placeholder|gravatar/);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should export user data', async ({ page }) => {
|
||
|
|
await page.goto('/profile');
|
||
|
|
|
||
|
|
// Look for export data option
|
||
|
|
const exportButton = page.locator('button:has-text("Export"), a:has-text("Download.*Data")');
|
||
|
|
|
||
|
|
if (await exportButton.isVisible()) {
|
||
|
|
// Start download
|
||
|
|
const downloadPromise = page.waitForEvent('download');
|
||
|
|
await exportButton.click();
|
||
|
|
|
||
|
|
try {
|
||
|
|
const download = await downloadPromise;
|
||
|
|
expect(download.suggestedFilename()).toMatch(/export|data|backup/);
|
||
|
|
} catch {
|
||
|
|
// Might need confirmation
|
||
|
|
const confirmExport = page.locator('button:has-text("Download"), button:has-text("Export")').last();
|
||
|
|
if (await confirmExport.isVisible()) {
|
||
|
|
const downloadPromise = page.waitForEvent('download');
|
||
|
|
await confirmExport.click();
|
||
|
|
const download = await downloadPromise;
|
||
|
|
expect(download.suggestedFilename()).toMatch(/export|data|backup/);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should handle account deletion', async ({ page }) => {
|
||
|
|
await page.goto('/profile');
|
||
|
|
|
||
|
|
// Look for delete account option (usually in danger zone)
|
||
|
|
const deleteAccountButton = page.locator('button:has-text("Delete Account"), a:has-text("Delete Account")');
|
||
|
|
|
||
|
|
if (await deleteAccountButton.isVisible()) {
|
||
|
|
await deleteAccountButton.click();
|
||
|
|
|
||
|
|
// Should show confirmation dialog
|
||
|
|
const confirmDialog = page.locator('[role="dialog"], .modal');
|
||
|
|
await expect(confirmDialog).toBeVisible();
|
||
|
|
|
||
|
|
// Should require typing confirmation
|
||
|
|
const confirmInput = confirmDialog.locator('input[placeholder*="DELETE"], input[placeholder*="confirm"]');
|
||
|
|
if (await confirmInput.isVisible()) {
|
||
|
|
await confirmInput.fill('DELETE');
|
||
|
|
}
|
||
|
|
|
||
|
|
// We won't actually delete in test
|
||
|
|
const cancelButton = confirmDialog.locator('button:has-text("Cancel")');
|
||
|
|
await cancelButton.click();
|
||
|
|
|
||
|
|
// Dialog should close
|
||
|
|
await expect(confirmDialog).not.toBeVisible();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|