From 036a7d9d265f4df4b12549c330aa44927409aa71 Mon Sep 17 00:00:00 2001 From: Test User Date: Mon, 22 Dec 2025 17:16:55 -0500 Subject: [PATCH] refactor: update e2e tests to use 'load' state for page navigation - Changed instances of `waitForLoadState('networkidle')` to `waitForLoadState('load')` across multiple test files and utility functions to improve test reliability in applications with persistent connections. - Added documentation to the e2e testing guide explaining the rationale behind using 'load' state instead of 'networkidle' to prevent timeouts and flaky tests. --- apps/ui/tests/e2e-testing-guide.md | 43 +++++++++++++++++-- .../projects/new-project-creation.spec.ts | 2 +- .../projects/open-existing-project.spec.ts | 2 +- apps/ui/tests/utils/core/waiting.ts | 8 ++-- apps/ui/tests/utils/git/worktree.ts | 2 +- apps/ui/tests/utils/navigation/views.ts | 14 +++--- 6 files changed, 55 insertions(+), 16 deletions(-) diff --git a/apps/ui/tests/e2e-testing-guide.md b/apps/ui/tests/e2e-testing-guide.md index 8b07dd53..6a48f32b 100644 --- a/apps/ui/tests/e2e-testing-guide.md +++ b/apps/ui/tests/e2e-testing-guide.md @@ -89,13 +89,26 @@ await expect(page.locator('[data-testid="welcome-view"]')).toBeVisible({ timeout await page.waitForSelector('[data-testid="welcome-view"]'); ``` -### Wait for network idle after navigation +### Wait for page load after navigation + +**Important:** Use `load` state, NOT `networkidle`. This app has persistent connections (websockets, polling) that prevent the network from ever becoming "idle", causing `networkidle` to timeout. ```typescript await page.goto('/'); -await page.waitForLoadState('networkidle'); +await page.waitForLoadState('load'); + +// Then wait for specific elements to verify the page is ready +await expect(page.locator('[data-testid="board-view"]')).toBeVisible({ timeout: 10000 }); ``` +**Why not `networkidle`?** + +- `networkidle` requires no network activity for 500ms +- Modern SPAs with real-time features (websockets, polling, SSE) never reach this state +- Using `networkidle` causes 30+ second timeouts and flaky tests +- The `load` state fires when the page finishes loading, which is sufficient +- Always follow up with element visibility checks for reliability + ### Use appropriate timeouts - Quick UI updates: 5000ms (default) @@ -267,6 +280,29 @@ npm run test -- project-creation.spec.ts --repeat-each=5 3. Run with `--headed` to watch the test 4. Add `await page.pause()` to pause execution at a specific point +## Common Pitfalls + +### Timeout on `waitForLoadState('networkidle')` + +If tests timeout waiting for network idle, the app likely has persistent connections. Use `load` state instead: + +```typescript +// Bad - will timeout with persistent connections +await page.waitForLoadState('networkidle'); + +// Good - completes when page loads +await page.waitForLoadState('load'); +await expect(page.locator('[data-testid="my-element"]')).toBeVisible(); +``` + +### Port conflicts + +If you see "Port 3008 is already in use", kill the process: + +```bash +lsof -ti:3008 | xargs kill -9 +``` + ## Available Test Utilities Import from `./utils`: @@ -290,8 +326,9 @@ Import from `./utils`: ### Waiting Utilities -- `waitForNetworkIdle(page)` - Wait for network to be idle +- `waitForNetworkIdle(page)` - Wait for page to load (uses `load` state, not `networkidle`) - `waitForElement(page, testId)` - Wait for element by test ID +- `waitForBoardView(page)` - Navigate to board and wait for it to be visible ### Async File Verification diff --git a/apps/ui/tests/projects/new-project-creation.spec.ts b/apps/ui/tests/projects/new-project-creation.spec.ts index d4c5e3d1..3feff75c 100644 --- a/apps/ui/tests/projects/new-project-creation.spec.ts +++ b/apps/ui/tests/projects/new-project-creation.spec.ts @@ -27,7 +27,7 @@ test.describe('Project Creation', () => { await setupWelcomeView(page, { workspaceDir: TEST_TEMP_DIR }); await page.goto('/'); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('load'); await expect(page.locator('[data-testid="welcome-view"]')).toBeVisible({ timeout: 10000 }); diff --git a/apps/ui/tests/projects/open-existing-project.spec.ts b/apps/ui/tests/projects/open-existing-project.spec.ts index 17b6c28c..c47cbcf7 100644 --- a/apps/ui/tests/projects/open-existing-project.spec.ts +++ b/apps/ui/tests/projects/open-existing-project.spec.ts @@ -75,7 +75,7 @@ test.describe('Open Project', () => { // Navigate to the app await page.goto('/'); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('load'); // Wait for welcome view to be visible await expect(page.locator('[data-testid="welcome-view"]')).toBeVisible({ timeout: 10000 }); diff --git a/apps/ui/tests/utils/core/waiting.ts b/apps/ui/tests/utils/core/waiting.ts index 3be64b76..09a073b0 100644 --- a/apps/ui/tests/utils/core/waiting.ts +++ b/apps/ui/tests/utils/core/waiting.ts @@ -1,11 +1,13 @@ import { Page, Locator } from '@playwright/test'; /** - * Wait for the page to reach network idle state - * This is commonly used after navigation or page reload to ensure all network requests have completed + * Wait for the page to load + * Uses 'load' state instead of 'networkidle' because the app has persistent + * connections (websockets/polling) that prevent network from ever being idle. + * Tests should wait for specific elements to verify page is ready. */ export async function waitForNetworkIdle(page: Page): Promise { - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('load'); } /** diff --git a/apps/ui/tests/utils/git/worktree.ts b/apps/ui/tests/utils/git/worktree.ts index 099123b4..e7c1b2f8 100644 --- a/apps/ui/tests/utils/git/worktree.ts +++ b/apps/ui/tests/utils/git/worktree.ts @@ -479,7 +479,7 @@ export async function waitForBoardView(page: Page): Promise { const currentUrl = page.url(); if (!currentUrl.includes('/board')) { await page.goto('/board'); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('load'); } // Wait for either board-view (success) or board-view-no-project (store not hydrated yet) diff --git a/apps/ui/tests/utils/navigation/views.ts b/apps/ui/tests/utils/navigation/views.ts index 459810d8..1abe0bb6 100644 --- a/apps/ui/tests/utils/navigation/views.ts +++ b/apps/ui/tests/utils/navigation/views.ts @@ -9,7 +9,7 @@ import { waitForElement } from '../core/waiting'; export async function navigateToBoard(page: Page): Promise { // Navigate directly to /board route await page.goto('/board'); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('load'); // Wait for the board view to be visible await waitForElement(page, 'board-view', { timeout: 10000 }); @@ -22,7 +22,7 @@ export async function navigateToBoard(page: Page): Promise { export async function navigateToContext(page: Page): Promise { // Navigate directly to /context route await page.goto('/context'); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('load'); // Wait for loading to complete (if present) const loadingElement = page.locator('[data-testid="context-view-loading"]'); @@ -47,7 +47,7 @@ export async function navigateToContext(page: Page): Promise { export async function navigateToSpec(page: Page): Promise { // Navigate directly to /spec route await page.goto('/spec'); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('load'); // Wait for loading state to complete first (if present) const loadingElement = page.locator('[data-testid="spec-view-loading"]'); @@ -77,7 +77,7 @@ export async function navigateToSpec(page: Page): Promise { export async function navigateToAgent(page: Page): Promise { // Navigate directly to /agent route await page.goto('/agent'); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('load'); // Wait for the agent view to be visible await waitForElement(page, 'agent-view', { timeout: 10000 }); @@ -90,7 +90,7 @@ export async function navigateToAgent(page: Page): Promise { export async function navigateToSettings(page: Page): Promise { // Navigate directly to /settings route await page.goto('/settings'); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('load'); // Wait for the settings view to be visible await waitForElement(page, 'settings-view', { timeout: 10000 }); @@ -105,7 +105,7 @@ export async function navigateToSetup(page: Page): Promise { const { setupFirstRun } = await import('../project/setup'); await setupFirstRun(page); await page.goto('/'); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('load'); await waitForElement(page, 'setup-view', { timeout: 10000 }); } @@ -114,7 +114,7 @@ export async function navigateToSetup(page: Page): Promise { */ export async function navigateToWelcome(page: Page): Promise { await page.goto('/'); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('load'); await waitForElement(page, 'welcome-view', { timeout: 10000 }); }