diff --git a/tests/core/data-consistency.spec.ts b/tests/core/data-consistency.spec.ts index 42879a59..5f5e5645 100644 --- a/tests/core/data-consistency.spec.ts +++ b/tests/core/data-consistency.spec.ts @@ -1,5 +1,5 @@ import { test, expect, loginUser } from '../fixtures/auth-fixtures'; -import { waitForLoadingComplete } from '../utils/wait-helpers'; +import { waitForDialog, waitForLoadingComplete } from '../utils/wait-helpers'; async function getAuthToken(page: import('@playwright/test').Page): Promise { return await page.evaluate(() => { @@ -18,7 +18,7 @@ async function openInviteUserForm(page: import('@playwright/test').Page): Promis const inviteButton = page.getByRole('button', { name: /invite.*user|add user|create user/i }).first(); await expect(inviteButton).toBeVisible({ timeout: 15000 }); await inviteButton.click(); - await expect(page.getByRole('dialog')).toBeVisible({ timeout: 15000 }); + await waitForDialog(page, { timeout: 15000 }); } async function fillInviteForm( @@ -44,9 +44,25 @@ async function fillInviteForm( const sendButton = page.getByRole('dialog') .getByRole('button', { name: /send.*invite|create|submit/i }) .first(); + + const createResponse = page.waitForResponse( + (response) => response.url().includes('/api/v1/users') && response.request().method() === 'POST', + { timeout: 15000 } + ).catch(() => null); + await expect(sendButton).toBeVisible({ timeout: 15000 }); await sendButton.click(); + await createResponse; await waitForLoadingComplete(page, { timeout: 15000 }); + + const inviteDialog = page.getByRole('dialog').first(); + const doneButton = inviteDialog.getByRole('button', { name: /done|close|cancel/i }).first(); + if (await doneButton.isVisible().catch(() => false)) { + await doneButton.click(); + } + await inviteDialog.waitFor({ state: 'hidden', timeout: 10000 }).catch(() => { + // Some implementations auto-close on submit. + }); } async function openCreateProxyHostForm(page: import('@playwright/test').Page): Promise { @@ -55,6 +71,7 @@ async function openCreateProxyHostForm(page: import('@playwright/test').Page): P const addButton = page.getByRole('button', { name: /add.*proxy.*host/i }).first(); await expect(addButton).toBeVisible({ timeout: 15000 }); await addButton.click(); + await waitForDialog(page, { timeout: 15000 }); } /** @@ -136,7 +153,7 @@ test.describe('Data Consistency', () => { const token = await getAuthToken(page); const response = await page.request.get( - 'http://127.0.0.1:8080/api/users', + '/api/v1/users', { headers: { 'Authorization': `Bearer ${token || ''}` }, ignoreHTTPSErrors: true, @@ -173,7 +190,7 @@ test.describe('Data Consistency', () => { const token = await getAuthToken(page); const usersResponse = await page.request.get( - 'http://127.0.0.1:8080/api/users', + '/api/v1/users', { headers: { 'Authorization': `Bearer ${token || ''}` }, ignoreHTTPSErrors: true, @@ -187,7 +204,7 @@ test.describe('Data Consistency', () => { if (user) { const updateResponse = await page.request.patch( - `http://127.0.0.1:8080/api/users/${user.id}`, + `/api/v1/users/${user.id}`, { data: { name: updatedName }, headers: { 'Authorization': `Bearer ${token || ''}` }, @@ -240,7 +257,7 @@ test.describe('Data Consistency', () => { const token = await getAuthToken(page); const response = await page.request.get( - 'http://127.0.0.1:8080/api/users', + '/api/v1/users', { headers: { 'Authorization': `Bearer ${token || ''}` }, ignoreHTTPSErrors: true, @@ -269,7 +286,7 @@ test.describe('Data Consistency', () => { const token = await getAuthToken(page); const usersResponse = await page.request.get( - 'http://127.0.0.1:8080/api/users', + '/api/v1/users', { headers: { 'Authorization': `Bearer ${token || ''}` }, ignoreHTTPSErrors: true, @@ -284,7 +301,7 @@ test.describe('Data Consistency', () => { if (user) { // Send two concurrent updates const update1 = page.request.patch( - `http://127.0.0.1:8080/api/users/${user.id}`, + `/api/v1/users/${user.id}`, { data: { name: 'Update One' }, headers: { 'Authorization': `Bearer ${token || ''}` }, @@ -293,7 +310,7 @@ test.describe('Data Consistency', () => { ); const update2 = page.request.patch( - `http://127.0.0.1:8080/api/users/${user.id}`, + `/api/v1/users/${user.id}`, { data: { name: 'Update Two' }, headers: { 'Authorization': `Bearer ${token || ''}` }, @@ -327,8 +344,8 @@ test.describe('Data Consistency', () => { await test.step('Create proxy', async () => { await openCreateProxyHostForm(page); - await page.getByRole('textbox', { name: /domain names/i }).first().fill(testProxy.domain); - await page.getByRole('textbox', { name: /target|forward/i }).first().fill(testProxy.target); + await page.getByRole('textbox', { name: /domain names|domain/i }).first().fill(testProxy.domain); + await page.getByLabel(/forward host|target|forward/i).first().fill(testProxy.target); await page.getByRole('textbox', { name: /description/i }).first().fill(testProxy.description); const submitButton = page.getByRole('button', { name: /create|submit/i }).first(); @@ -341,7 +358,7 @@ test.describe('Data Consistency', () => { // Try to modify with invalid data that should fail validation const response = await page.request.patch( - `http://127.0.0.1:8080/api/proxy-hosts`, + `/api/v1/proxy-hosts`, { data: { domain: '' }, // Empty domain should fail headers: { 'Authorization': `Bearer ${token || ''}` }, @@ -400,7 +417,9 @@ test.describe('Data Consistency', () => { } // Try submitting anyway - const submitButton = page.getByRole('button', { name: /send.*invite|create|submit/i }).first(); + const submitButton = page.getByRole('dialog') + .getByRole('button', { name: /send.*invite|create|submit/i }) + .first(); if (!(await submitButton.isDisabled())) { await submitButton.click(); await waitForLoadingComplete(page, { timeout: 15000 }); @@ -433,7 +452,7 @@ test.describe('Data Consistency', () => { await waitForLoadingComplete(page, { timeout: 15000 }); // Get first page - const page1Items = await page.locator('[role="row"], [class*="user-item"]').count(); + const page1Items = await page.locator('table tbody tr').count(); expect(page1Items).toBeGreaterThan(0); // Navigate to page 2 if pagination exists @@ -442,7 +461,7 @@ test.describe('Data Consistency', () => { await nextButton.click(); await waitForLoadingComplete(page, { timeout: 15000 }); - const page2Items = await page.locator('[role="row"], [class*="user-item"]').count(); + const page2Items = await page.locator('table tbody tr').count(); expect(page2Items).toBeGreaterThanOrEqual(0); // Go back to page 1 @@ -451,7 +470,7 @@ test.describe('Data Consistency', () => { await prevButton.click(); await waitForLoadingComplete(page, { timeout: 15000 }); - const backPage1Items = await page.locator('[role="row"], [class*="user-item"]').count(); + const backPage1Items = await page.locator('table tbody tr').count(); expect(backPage1Items).toBe(page1Items); } } diff --git a/tests/core/multi-component-workflows.spec.ts b/tests/core/multi-component-workflows.spec.ts index c8896da3..6a1b0b22 100644 --- a/tests/core/multi-component-workflows.spec.ts +++ b/tests/core/multi-component-workflows.spec.ts @@ -1,4 +1,5 @@ -import { test, expect } from '@playwright/test'; +import { test, expect, loginUser } from '../fixtures/auth-fixtures'; +import { waitForLoadingComplete } from '../utils/wait-helpers'; /** * Integration: Multi-Component Workflows @@ -21,9 +22,10 @@ test.describe('Multi-Component Workflows', () => { password: 'MultiFlow123!', }; - test.beforeEach(async ({ page }) => { - await page.goto('/', { waitUntil: 'networkidle' }); - await page.waitForSelector('[role="main"]', { timeout: 5000 }); + test.beforeEach(async ({ page, adminUser }) => { + await loginUser(page, adminUser); + await waitForLoadingComplete(page, { timeout: 15000 }); + await expect(page.getByRole('main')).toBeVisible({ timeout: 15000 }); }); test.afterEach(async ({ page }) => { diff --git a/tests/settings/user-lifecycle.spec.ts b/tests/settings/user-lifecycle.spec.ts index 449769ab..bcbb4b23 100644 --- a/tests/settings/user-lifecycle.spec.ts +++ b/tests/settings/user-lifecycle.spec.ts @@ -1,4 +1,90 @@ import { test, expect, loginUser, TEST_PASSWORD } from '../fixtures/auth-fixtures'; +import { waitForDialog, waitForLoadingComplete } from '../utils/wait-helpers'; + +async function openInviteUserDialog(page: import('@playwright/test').Page): Promise { + await page.goto('/users', { waitUntil: 'networkidle' }); + await waitForLoadingComplete(page, { timeout: 15000 }); + + const inviteButton = page.getByRole('button', { name: /invite.*user|add user|create user/i }).first(); + await expect(inviteButton).toBeVisible({ timeout: 15000 }); + await inviteButton.click(); + await waitForDialog(page, { timeout: 15000 }); +} + +async function createUserFromInviteDialog( + page: import('@playwright/test').Page, + user: { email: string; name: string; password: string } +): Promise { + const dialog = page.getByRole('dialog').first(); + + const emailInput = dialog.getByPlaceholder(/user@example/i).or(dialog.getByLabel(/email/i)).first(); + await expect(emailInput).toBeVisible({ timeout: 15000 }); + await emailInput.fill(user.email); + + const nameInput = dialog.getByPlaceholder(/name/i).or(dialog.getByLabel(/name/i)).first(); + if (await nameInput.isVisible().catch(() => false)) { + await nameInput.fill(user.name); + } + + const passwordInput = dialog.getByLabel(/password/i).first(); + if (await passwordInput.isVisible().catch(() => false)) { + await passwordInput.fill(user.password); + } + + const createResponse = page.waitForResponse( + (response) => response.url().includes('/api/v1/users') && response.request().method() === 'POST', + { timeout: 15000 } + ).catch(() => null); + + const submitButton = dialog.getByRole('button', { name: /send.*invite|create.*user|create|submit/i }).first(); + await expect(submitButton).toBeVisible({ timeout: 15000 }); + await submitButton.click(); + await createResponse; + await waitForLoadingComplete(page, { timeout: 15000 }); + + const doneButton = dialog.getByRole('button', { name: /done|close|cancel/i }).first(); + if (await doneButton.isVisible().catch(() => false)) { + await doneButton.click(); + } + await dialog.waitFor({ state: 'hidden', timeout: 10000 }).catch(() => { + // Some implementations auto-close the dialog. + }); +} + +async function navigateToLogin(page: import('@playwright/test').Page): Promise { + const logoutButton = page.getByRole('button', { name: /logout/i }).first(); + if (await logoutButton.isVisible().catch(() => false)) { + await logoutButton.click(); + } else { + await page.goto('/login', { waitUntil: 'domcontentloaded' }); + } + + const emailInput = page.locator('input[type="email"]').or(page.getByLabel(/email/i)).first(); + await expect(emailInput).toBeVisible({ timeout: 15000 }); +} + +async function loginWithCredentials( + page: import('@playwright/test').Page, + email: string, + password: string +): Promise { + const emailInput = page.locator('input[type="email"]').or(page.getByLabel(/email/i)).first(); + const passwordInput = page.locator('input[type="password"]').or(page.getByLabel(/password/i)).first(); + + await expect(emailInput).toBeVisible({ timeout: 15000 }); + await expect(passwordInput).toBeVisible({ timeout: 15000 }); + await emailInput.fill(email); + await passwordInput.fill(password); + + const loginResponse = page.waitForResponse( + (response) => response.url().includes('/api/v1/auth/login') && response.request().method() === 'POST', + { timeout: 15000 } + ).catch(() => null); + + await page.getByRole('button', { name: /login|sign in/i }).first().click(); + await loginResponse; + await waitForLoadingComplete(page, { timeout: 15000 }); +} /** * Integration: Admin → User E2E Workflow @@ -30,18 +116,8 @@ test.describe('Admin-User E2E Workflow', () => { await test.step('STEP 1: Admin creates new user', async () => { const start = Date.now(); - await page.goto('/users', { waitUntil: 'networkidle' }); - - const addButton = page.getByRole('button', { name: /add|create/i }).first(); - await addButton.click(); - - await page.getByLabel(/email/i).fill(testUser.email); - await page.getByLabel(/name/i).fill(testUser.name); - await page.getByLabel(/password/i).first().fill(testUser.password); - - const submitButton = page.getByRole('button', { name: /create|submit/i }).first(); - await submitButton.click(); - await page.waitForLoadState('networkidle'); + await openInviteUserDialog(page); + await createUserFromInviteDialog(page, testUser); const duration = Date.now() - start; console.log(`✓ User created in ${duration}ms`); @@ -77,13 +153,7 @@ test.describe('Admin-User E2E Workflow', () => { await test.step('STEP 4: New user logs in', async () => { const start = Date.now(); - await page.getByLabel(/email/i).fill(testUser.email); - await page.getByLabel(/password/i).fill(testUser.password); - - const loginButton = page.getByRole('button', { name: /login/i }); - await loginButton.click(); - - await page.waitForLoadState('networkidle'); + await loginWithCredentials(page, testUser.email, testUser.password); const duration = Date.now() - start; console.log(`✓ User logged in in ${duration}ms`); @@ -119,10 +189,7 @@ test.describe('Admin-User E2E Workflow', () => { } // Login as admin - await page.getByLabel(/email/i).fill(adminEmail); - await page.getByLabel(/password/i).fill(TEST_PASSWORD); - await page.getByRole('button', { name: /login/i }).click(); - await page.waitForLoadState('networkidle'); + await loginWithCredentials(page, adminEmail, TEST_PASSWORD); // Check audit logs await page.goto('/audit', { waitUntil: 'networkidle' }).catch(() => { @@ -138,18 +205,8 @@ test.describe('Admin-User E2E Workflow', () => { // Admin modifies role → user gains new permissions immediately test('Role change takes effect immediately on user refresh', async ({ page }) => { await test.step('Create test user with default role', async () => { - await page.goto('/users', { waitUntil: 'networkidle' }); - - const addButton = page.getByRole('button', { name: /add|create/i }).first(); - await addButton.click(); - - await page.getByLabel(/email/i).fill(testUser.email); - await page.getByLabel(/name/i).fill(testUser.name); - await page.getByLabel(/password/i).first().fill(testUser.password); - - const submitButton = page.getByRole('button', { name: /create|submit/i }).first(); - await submitButton.click(); - await page.waitForLoadState('networkidle'); + await openInviteUserDialog(page); + await createUserFromInviteDialog(page, testUser); }); await test.step('User logs in and notes current permissions', async () => { @@ -157,10 +214,7 @@ test.describe('Admin-User E2E Workflow', () => { await logoutButton.click(); await page.waitForURL(/login/); - await page.getByLabel(/email/i).fill(testUser.email); - await page.getByLabel(/password/i).fill(testUser.password); - await page.getByRole('button', { name: /login/i }).click(); - await page.waitForLoadState('networkidle'); + await loginWithCredentials(page, testUser.email, testUser.password); }); await test.step('Admin upgrades user role (in parallel)', async () => { @@ -189,18 +243,8 @@ test.describe('Admin-User E2E Workflow', () => { }; await test.step('Create user to delete', async () => { - await page.goto('/users', { waitUntil: 'networkidle' }); - - const addButton = page.getByRole('button', { name: /add|create/i }).first(); - await addButton.click(); - - await page.getByLabel(/email/i).fill(deletableUser.email); - await page.getByLabel(/name/i).fill(deletableUser.name); - await page.getByLabel(/password/i).first().fill(deletableUser.password); - - const submitButton = page.getByRole('button', { name: /create|submit/i }).first(); - await submitButton.click(); - await page.waitForLoadState('networkidle'); + await openInviteUserDialog(page); + await createUserFromInviteDialog(page, deletableUser); }); await test.step('Admin deletes user', async () => { @@ -222,11 +266,7 @@ test.describe('Admin-User E2E Workflow', () => { await page.waitForURL(/login/); } - await page.getByLabel(/email/i).fill(deletableUser.email); - await page.getByLabel(/password/i).fill(deletableUser.password); - await page.getByRole('button', { name: /login/i }).click(); - - await page.waitForLoadState('networkidle'); + await loginWithCredentials(page, deletableUser.email, deletableUser.password); }); await test.step('Verify login failed with appropriate error', async () => { @@ -247,18 +287,8 @@ test.describe('Admin-User E2E Workflow', () => { test('Audit log records user lifecycle events', async ({ page }) => { await test.step('Perform workflow actions', async () => { // Create user - await page.goto('/users', { waitUntil: 'networkidle' }); - - const addButton = page.getByRole('button', { name: /add|create/i }).first(); - await addButton.click(); - - await page.getByLabel(/email/i).fill(testUser.email); - await page.getByLabel(/name/i).fill(testUser.name); - await page.getByLabel(/password/i).first().fill(testUser.password); - - const submit = page.getByRole('button', { name: /create|submit/i }).first(); - await submit.click(); - await page.waitForLoadState('networkidle'); + await openInviteUserDialog(page); + await createUserFromInviteDialog(page, testUser); }); await test.step('Check audit trail for user creation event', async () => { @@ -288,18 +318,8 @@ test.describe('Admin-User E2E Workflow', () => { // User cannot escalate own role test('User cannot promote self to admin', async ({ page }) => { await test.step('Create test user', async () => { - await page.goto('/users', { waitUntil: 'networkidle' }); - - const addButton = page.getByRole('button', { name: /add|create/i }).first(); - await addButton.click(); - - await page.getByLabel(/email/i).fill(testUser.email); - await page.getByLabel(/name/i).fill(testUser.name); - await page.getByLabel(/password/i).first().fill(testUser.password); - - const submitButton = page.getByRole('button', { name: /create|submit/i }).first(); - await submitButton.click(); - await page.waitForLoadState('networkidle'); + await openInviteUserDialog(page); + await createUserFromInviteDialog(page, testUser); }); await test.step('User attempts to modify own role', async () => { @@ -309,10 +329,7 @@ test.describe('Admin-User E2E Workflow', () => { await page.waitForURL(/login/); // Login as user - await page.getByLabel(/email/i).fill(testUser.email); - await page.getByLabel(/password/i).fill(testUser.password); - await page.getByRole('button', { name: /login/i }).click(); - await page.waitForLoadState('networkidle'); + await loginWithCredentials(page, testUser.email, testUser.password); // Try to access user management await page.goto('/users', { waitUntil: 'networkidle' }).catch(() => { @@ -349,31 +366,13 @@ test.describe('Admin-User E2E Workflow', () => { }; await test.step('Create first user', async () => { - await page.goto('/users', { waitUntil: 'networkidle' }); - - const addButton = page.getByRole('button', { name: /add|create/i }).first(); - await addButton.click(); - - await page.getByLabel(/email/i).fill(user1.email); - await page.getByLabel(/name/i).fill(user1.name); - await page.getByLabel(/password/i).first().fill(user1.password); - - const submitButton = page.getByRole('button', { name: /create|submit/i }).first(); - await submitButton.click(); - await page.waitForLoadState('networkidle'); + await openInviteUserDialog(page); + await createUserFromInviteDialog(page, user1); }); await test.step('Create second user', async () => { - const addButton = page.getByRole('button', { name: /add|create/i }).first(); - await addButton.click(); - - await page.getByLabel(/email/i).fill(user2.email); - await page.getByLabel(/name/i).fill(user2.name); - await page.getByLabel(/password/i).first().fill(user2.password); - - const submitButton = page.getByRole('button', { name: /create|submit/i }).first(); - await submitButton.click(); - await page.waitForLoadState('networkidle'); + await openInviteUserDialog(page); + await createUserFromInviteDialog(page, user2); }); await test.step('User1 logs in and verifies data isolation', async () => { @@ -381,10 +380,7 @@ test.describe('Admin-User E2E Workflow', () => { await logoutButton.click(); await page.waitForURL(/login/); - await page.getByLabel(/email/i).fill(user1.email); - await page.getByLabel(/password/i).fill(user1.password); - await page.getByRole('button', { name: /login/i }).click(); - await page.waitForLoadState('networkidle'); + await loginWithCredentials(page, user1.email, user1.password); // User1 should see their profile but not User2's const user1Profile = page.getByText(user1.name).first(); @@ -396,18 +392,14 @@ test.describe('Admin-User E2E Workflow', () => { // User logout → login as different user → resources isolated test('Session isolation after logout and re-login', async ({ page }) => { + await test.step('Create secondary user for session switch', async () => { + await openInviteUserDialog(page); + await createUserFromInviteDialog(page, testUser); + }); + await test.step('Login as first user', async () => { - await page.goto('/', { waitUntil: 'networkidle' }); - - const emailInput = page.getByLabel(/email/i); - const passwordInput = page.getByLabel(/password/i); - - await emailInput.fill(adminEmail); - await passwordInput.fill(TEST_PASSWORD); - - const loginButton = page.getByRole('button', { name: /login/i }); - await loginButton.click(); - await page.waitForLoadState('networkidle'); + await navigateToLogin(page); + await loginWithCredentials(page, adminEmail, TEST_PASSWORD); }); await test.step('Note session storage', async () => { @@ -429,15 +421,7 @@ test.describe('Admin-User E2E Workflow', () => { }); await test.step('Login as different user', async () => { - const emailInput = page.getByLabel(/email/i); - const passwordInput = page.getByLabel(/password/i); - - await emailInput.fill(testUser.email); - await passwordInput.fill(testUser.password); - - const loginButton = page.getByRole('button', { name: /login/i }); - await loginButton.click(); - await page.waitForLoadState('networkidle'); + await loginWithCredentials(page, testUser.email, testUser.password); }); await test.step('Verify new session established', async () => {