chore: enhance user lifecycle tests with invite dialog and loading handling

This commit is contained in:
GitHub Actions
2026-02-13 01:58:50 +00:00
parent 2904b7435e
commit 162750aacb
3 changed files with 155 additions and 150 deletions

View File

@@ -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<string> {
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<void> {
@@ -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);
}
}

View File

@@ -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 }) => {

View File

@@ -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<void> {
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<void> {
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<void> {
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<void> {
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 () => {