From f8b77200f057dd574b4d374184f5430ac114d450 Mon Sep 17 00:00:00 2001 From: Felipe Cardoso Date: Fri, 7 Nov 2025 00:02:01 +0100 Subject: [PATCH] Refactor E2E tests and mock APIs for improved reliability and maintainability - Updated E2E tests to use specific role-based heading selectors for better robustness. - Enhanced mock routes in `auth.ts` to handle detailed organization endpoints more effectively. - Improved test flow by adding `waitUntil: 'networkidle'` to navigation steps. - Refined `admin-access.spec.ts` interactions to use optimized wait and click implementations for better performance. - Updated dialog texts and field labels to match latest UI changes. --- frontend/e2e/admin-access.spec.ts | 9 +++-- .../e2e/admin-organization-members.spec.ts | 6 ++-- frontend/e2e/helpers/auth.ts | 19 ++++++++--- frontend/e2e/settings-navigation.spec.ts | 34 +++++++++---------- frontend/e2e/settings-password.spec.ts | 14 +++----- 5 files changed, 45 insertions(+), 37 deletions(-) diff --git a/frontend/e2e/admin-access.spec.ts b/frontend/e2e/admin-access.spec.ts index d6ffb98..948fcee 100644 --- a/frontend/e2e/admin-access.spec.ts +++ b/frontend/e2e/admin-access.spec.ts @@ -172,7 +172,7 @@ test.describe('Admin Navigation', () => { await page.goto('/admin/organizations'); await expect(page).toHaveURL('/admin/organizations'); - await expect(page.locator('h2')).toContainText('All Organizations'); + await expect(page.getByRole('heading', { name: 'All Organizations' })).toBeVisible(); // Breadcrumbs should show Admin > Organizations await expect(page.getByTestId('breadcrumb-admin')).toBeVisible(); @@ -270,9 +270,12 @@ test.describe('Admin Breadcrumbs', () => { // Click 'Admin' breadcrumb to go back to dashboard const adminBreadcrumb = page.getByTestId('breadcrumb-admin'); - await adminBreadcrumb.click(); - await page.waitForURL('/admin', { timeout: 5000 }); + await Promise.all([ + page.waitForURL('/admin', { timeout: 10000 }), + adminBreadcrumb.click() + ]); + await expect(page).toHaveURL('/admin'); }); }); diff --git a/frontend/e2e/admin-organization-members.spec.ts b/frontend/e2e/admin-organization-members.spec.ts index fde2588..19a5044 100644 --- a/frontend/e2e/admin-organization-members.spec.ts +++ b/frontend/e2e/admin-organization-members.spec.ts @@ -150,17 +150,17 @@ test.describe('Admin Organization Members - AddMemberDialog E2E Tests', () => { }); test('should display dialog description', async ({ page }) => { - await expect(page.getByText(/Add a new member to this organization/i)).toBeVisible(); + await expect(page.getByText(/Add a user to this organization and assign them a role/i)).toBeVisible(); }); test('should display user email select field', async ({ page }) => { const dialog = page.locator('[role="dialog"]'); - await expect(dialog.getByText('User Email')).toBeVisible(); + await expect(dialog.getByText('User Email *')).toBeVisible(); }); test('should display role select field', async ({ page }) => { const dialog = page.locator('[role="dialog"]'); - await expect(dialog.getByText('Role')).toBeVisible(); + await expect(dialog.getByText('Role *')).toBeVisible(); }); test('should display add member and cancel buttons', async ({ page }) => { diff --git a/frontend/e2e/helpers/auth.ts b/frontend/e2e/helpers/auth.ts index 730b959..e01e727 100644 --- a/frontend/e2e/helpers/auth.ts +++ b/frontend/e2e/helpers/auth.ts @@ -293,7 +293,12 @@ export async function setupSuperuserMocks(page: Page): Promise { // Mock GET /api/v1/admin/organizations - Get all organizations (admin endpoint) await page.route(`${baseURL}/api/v1/admin/organizations*`, async (route: Route) => { - if (route.request().method() === 'GET') { + const url = route.request().url(); + + // Only handle list endpoint (no ID in path) - must have either end of URL or query params after /organizations + const isListEndpoint = url.match(/\/admin\/organizations(\?|$)/); + + if (route.request().method() === 'GET' && isListEndpoint) { await route.fulfill({ status: 200, contentType: 'application/json', @@ -362,11 +367,15 @@ export async function setupSuperuserMocks(page: Page): Promise { }); // Mock GET /api/v1/admin/organizations/:id - Get single organization - await page.route(`${baseURL}/api/v1/admin/organizations/*/`, async (route: Route) => { - if (route.request().method() === 'GET') { + await page.route(`${baseURL}/api/v1/admin/organizations/*`, async (route: Route) => { + const url = route.request().url(); + + // Only handle single org endpoint (has ID but no /members suffix) + const isSingleOrgEndpoint = url.match(/\/admin\/organizations\/[0-9a-f-]+\/?$/i); + + if (route.request().method() === 'GET' && isSingleOrgEndpoint) { // Extract org ID from URL - const url = route.request().url(); - const orgId = url.match(/organizations\/([^/]+)/)?.[1]; + const orgId = url.match(/organizations\/([^/]+)/)?.[1]?.replace(/\/$/, ''); // Remove trailing slash if any const org = MOCK_ORGANIZATIONS.find(o => o.id === orgId) || MOCK_ORGANIZATIONS[0]; await route.fulfill({ diff --git a/frontend/e2e/settings-navigation.spec.ts b/frontend/e2e/settings-navigation.spec.ts index 6e18a9c..838b025 100644 --- a/frontend/e2e/settings-navigation.spec.ts +++ b/frontend/e2e/settings-navigation.spec.ts @@ -20,13 +20,13 @@ test.describe('Settings Navigation', () => { await expect(page).toHaveURL('/'); // Navigate to settings/profile - await page.goto('/settings/profile'); + await page.goto('/settings/profile', { waitUntil: 'networkidle' }); // Verify navigation successful await expect(page).toHaveURL('/settings/profile'); - // Verify page loaded - await expect(page.locator('h2')).toContainText('Profile'); + // Verify page loaded - use specific heading selector + await expect(page.getByRole('heading', { name: 'Profile' })).toBeVisible(); }); test('should navigate from home to settings password', async ({ page }) => { @@ -34,49 +34,49 @@ test.describe('Settings Navigation', () => { await expect(page).toHaveURL('/'); // Navigate to settings/password - await page.goto('/settings/password'); + await page.goto('/settings/password', { waitUntil: 'networkidle' }); // Verify navigation successful await expect(page).toHaveURL('/settings/password'); - // Verify page loaded - await expect(page.locator('h2')).toContainText('Password'); + // Verify page loaded - use specific heading selector + await expect(page.getByRole('heading', { name: 'Password' })).toBeVisible(); }); test('should navigate between settings pages', async ({ page }) => { // Start at profile page - await page.goto('/settings/profile'); - await expect(page.locator('h2')).toContainText('Profile'); + await page.goto('/settings/profile', { waitUntil: 'networkidle' }); + await expect(page.getByRole('heading', { name: 'Profile' })).toBeVisible(); // Navigate to password page - await page.goto('/settings/password'); - await expect(page.locator('h2')).toContainText('Password'); + await page.goto('/settings/password', { waitUntil: 'networkidle' }); + await expect(page.getByRole('heading', { name: 'Password' })).toBeVisible(); // Navigate back to profile page - await page.goto('/settings/profile'); - await expect(page.locator('h2')).toContainText('Profile'); + await page.goto('/settings/profile', { waitUntil: 'networkidle' }); + await expect(page.getByRole('heading', { name: 'Profile' })).toBeVisible(); }); test('should redirect from /settings to /settings/profile', async ({ page }) => { // Navigate to base settings page - await page.goto('/settings'); + await page.goto('/settings', { waitUntil: 'networkidle' }); // Should redirect to profile page await expect(page).toHaveURL('/settings/profile'); - // Verify profile page loaded - await expect(page.locator('h2')).toContainText('Profile'); + // Verify profile page loaded - use specific heading selector + await expect(page.getByRole('heading', { name: 'Profile' })).toBeVisible(); }); test('should display preferences page placeholder', async ({ page }) => { // Navigate to preferences page - await page.goto('/settings/preferences'); + await page.goto('/settings/preferences', { waitUntil: 'networkidle' }); // Verify navigation successful await expect(page).toHaveURL('/settings/preferences'); // Verify page loaded with placeholder content - await expect(page.locator('h2')).toContainText('Preferences'); + await expect(page.getByRole('heading', { name: 'Preferences' })).toBeVisible(); await expect(page.getByText(/coming in task/i)).toBeVisible(); }); }); diff --git a/frontend/e2e/settings-password.spec.ts b/frontend/e2e/settings-password.spec.ts index 3fea990..2c86afa 100644 --- a/frontend/e2e/settings-password.spec.ts +++ b/frontend/e2e/settings-password.spec.ts @@ -15,22 +15,18 @@ test.describe('Password Change', () => { await loginViaUI(page); // Navigate to password page - await page.goto('/settings/password'); + await page.goto('/settings/password', { waitUntil: 'networkidle' }); - // Wait for page to render - await page.waitForTimeout(1000); + // Wait for form to be visible + await page.getByLabel(/current password/i).waitFor({ state: 'visible', timeout: 10000 }); }); test('should display password change form', async ({ page }) => { // Check page title - await expect(page.locator('h2')).toContainText('Password'); - - // Wait for form to be visible - const currentPasswordInput = page.getByLabel(/current password/i); - await currentPasswordInput.waitFor({ state: 'visible', timeout: 10000 }); + await expect(page.getByRole('heading', { name: 'Password' })).toBeVisible(); // Verify all password fields are present - await expect(currentPasswordInput).toBeVisible(); + await expect(page.getByLabel(/current password/i)).toBeVisible(); await expect(page.getByLabel(/^new password/i)).toBeVisible(); await expect(page.getByLabel(/confirm.*password/i)).toBeVisible();