diff --git a/.github/workflows/client-e2e.yml b/.github/workflows/client-e2e.yml new file mode 100644 index 00000000..5aa1d397 --- /dev/null +++ b/.github/workflows/client-e2e.yml @@ -0,0 +1,91 @@ +# =============================================================== +# React Client E2E - Playwright (TypeScript) smoke suite +# =============================================================== +# - runs TypeScript Playwright tests for the React admin UI +# - Vite dev server is spawned by Playwright's webServer config +# - backend API is stubbed with page.route() (no gateway required) +# - triggered on PRs and pushes to main +# --------------------------------------------------------------- + +name: Client E2E (Mocked API) + +on: + push: + branches: ["main"] + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + branches: ["main"] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + playwright: + if: github.event_name != 'pull_request' || !github.event.pull_request.draft + name: Playwright mocked API tests + runs-on: ubuntu-latest + timeout-minutes: 30 + + env: + CI: "true" + PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.playwright-browsers + + steps: + - name: Checkout source + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 + with: + persist-credentials: false + fetch-depth: 1 + + - name: Setup Node + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version-file: ".nvmrc" + + - name: Install dependencies + run: npm ci --no-audit --no-fund + + - name: Generate API code + run: npm run api:generate + + - name: Cache Playwright browsers + id: playwright-cache + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 + with: + path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }} + key: ${{ runner.os }}-playwright-${{ hashFiles('package-lock.json') }} + restore-keys: | + ${{ runner.os }}-playwright- + + - name: Install Playwright browsers + if: steps.playwright-cache.outputs.cache-hit != 'true' + run: npx playwright install --with-deps chromium + + - name: Install Playwright system deps (cached browsers) + if: steps.playwright-cache.outputs.cache-hit == 'true' + run: npx playwright install-deps chromium + + - name: Run Playwright tests + run: npm run playwright + + - name: Upload Playwright HTML report + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: playwright-report + path: playwright-report + retention-days: 7 + if-no-files-found: ignore + + - name: Upload Playwright traces on failure + if: failure() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: playwright-traces + path: test-results + retention-days: 7 + if-no-files-found: ignore diff --git a/README.md b/README.md index 1c48bd4d..ad7abec1 100644 --- a/README.md +++ b/README.md @@ -337,11 +337,11 @@ Playwright. They run against the Vite dev server and stub backend API calls with `page.route()`, so no running ContextForge API is required. ```bash -npm run e2e:install # Install Playwright browsers (one-time) -npm run e2e # Headless run -npm run e2e:ui # Interactive UI mode -npm run e2e:debug # Playwright Inspector -npm run e2e:report # Open the last HTML report +npm run playwright:install # Install Playwright browsers (one-time) +npm run playwright # Headless run +npm run playwright:ui # Interactive UI mode +npm run playwright:debug # Playwright Inspector +npm run playwright:report # Open the last HTML report ``` See [`e2e/README.md`](./e2e/README.md) for layout, fixtures, and guidelines. @@ -440,11 +440,11 @@ dev setup. | `npm run test` | Run tests once (CI mode) | | `npm run test:ui` | Run tests with UI | | `npm run test:coverage` | Generate coverage report | -| `npm run e2e` | Run Playwright E2E tests | -| `npm run e2e:ui` | Playwright UI mode | -| `npm run e2e:debug` | Playwright Inspector | -| `npm run e2e:install` | Install Playwright browsers | -| `npm run e2e:report` | Open last Playwright report | +| `npm run playwright` | Run Playwright tests (mocked API) | +| `npm run playwright:ui` | Playwright UI mode | +| `npm run playwright:debug` | Playwright Inspector | +| `npm run playwright:install` | Install Playwright browsers | +| `npm run playwright:report` | Open last Playwright report | | `npm run secrets:scan` | Scan repo for secrets, update `.secrets.baseline` | | `npm run secrets:audit` | Interactively audit findings in `.secrets.baseline` | diff --git a/e2e/README.md b/e2e/README.md index 24bc3da7..09e2d791 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -30,24 +30,25 @@ e2e/ Install the Playwright browsers once per machine: ```bash -npm run e2e:install +npm run playwright:install ``` Then, from `client/`: ```bash -npm run e2e # Headless run against the Vite dev server -npm run e2e:ui # Interactive UI mode (great for authoring) -npm run e2e:debug # Step through with the Playwright Inspector -npm run e2e:report # Open the last HTML report +npm run playwright # Headless run against a production build of the app +npm run playwright:ui # Interactive UI mode (great for authoring) +npm run playwright:debug # Step through with the Playwright Inspector +npm run playwright:report # Open the last HTML report ``` -The config spawns `npm run dev:e2e` (Vite with `--base=/`) on port `5173` and -tears it down after the run. Set `PLAYWRIGHT_BASE_URL` to point tests at an -existing server instead: +The config spawns `npm run playwright:serve` (a production build served by +`vite preview`, with source maps) on port `5173` and tears it down after the +run. App changes need a fresh run to rebuild; the build takes a few seconds. +Set `PLAYWRIGHT_BASE_URL` to point tests at an existing server instead: ```bash -PLAYWRIGHT_BASE_URL=http://localhost:4444 PLAYWRIGHT_SKIP_WEBSERVER=1 npm run e2e +PLAYWRIGHT_BASE_URL=http://localhost:4444 PLAYWRIGHT_SKIP_WEBSERVER=1 npm run playwright ``` ### Real-backend mode (`e2e:docker`) @@ -134,7 +135,7 @@ API responses; real API Docker runs are available through the scripts above. ## Troubleshooting - **`Error: Timed out waiting for ... to be visible`** — check the locator in UI - mode (`npm run e2e:ui`) and confirm the mock returns what the UI expects. + mode (`npm run playwright:ui`) and confirm the mock returns what the UI expects. - **Tests that pass locally but flake in CI** — add a `page.waitForLoadState`, tighten the mock's payload, or widen the retry count in the config for the specific test. Do not add arbitrary `waitForTimeout` calls. diff --git a/e2e/global-search.spec.ts b/e2e/global-search.spec.ts index 4d7a7b36..24acc1b8 100644 --- a/e2e/global-search.spec.ts +++ b/e2e/global-search.spec.ts @@ -92,7 +92,6 @@ test.describe("Global search", () => { }); await page.goto(APP.SERVERS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Search" }).click(); const searchBox = page.getByRole("searchbox", { name: "Search" }); diff --git a/e2e/invitations.spec.ts b/e2e/invitations.spec.ts index c95857e5..3aa1f4c4 100644 --- a/e2e/invitations.spec.ts +++ b/e2e/invitations.spec.ts @@ -116,6 +116,7 @@ test.describe("Pending team invitations", () => { await mockInvitations(page, []); await page.goto(APP.TEAMS); + // networkidle: the absence check below is only meaningful once the page settles. await page.waitForLoadState("networkidle"); await expect(page.getByRole("button", { name: /invitation/ })).toHaveCount(0); @@ -126,7 +127,6 @@ test.describe("Pending team invitations", () => { await mockInvitations(page, [PLATFORM_INVITATION]); await page.goto(APP.TEAMS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "1 team invitation" }).click(); @@ -146,7 +146,6 @@ test.describe("Pending team invitations", () => { await mockInvitations(page, [PLATFORM_INVITATION, DESIGN_INVITATION]); await page.goto(APP.TEAMS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "2 team invitations" }).click(); @@ -161,7 +160,6 @@ test.describe("Pending team invitations", () => { await mockInvitations(page, [PLATFORM_INVITATION, DESIGN_INVITATION]); await page.goto(APP.TEAMS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "2 team invitations" }).click(); @@ -187,7 +185,6 @@ test.describe("Pending team invitations", () => { await mockInvitations(page, [PLATFORM_INVITATION, DESIGN_INVITATION]); await page.goto(APP.TEAMS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "2 team invitations" }).click(); @@ -211,7 +208,6 @@ test.describe("Pending team invitations", () => { }); await page.goto(APP.TEAMS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "2 team invitations" }).click(); @@ -243,6 +239,7 @@ test.describe("Pending team invitations", () => { await mockInvitations(page, [], { status: 500 }); await page.goto(APP.TEAMS); + // networkidle: the absence check below is only meaningful once the page settles. await page.waitForLoadState("networkidle"); // No count is known, so no chip renders and the failure is not toasted. @@ -255,7 +252,6 @@ test.describe("Pending team invitations", () => { await mockInvitations(page, [PLATFORM_INVITATION]); await page.goto(APP.TEAMS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "1 team invitation" }).click(); @@ -281,6 +277,7 @@ test.describe("Pending team invitations", () => { await mockInvitations(page, [PLATFORM_INVITATION]); await page.goto(APP.TEAMS); + // networkidle: the absence check below is only meaningful once the page settles. await page.waitForLoadState("networkidle"); await expect(page).not.toHaveURL(/settings\/teams/); diff --git a/e2e/oauth-authorization.spec.ts b/e2e/oauth-authorization.spec.ts index f74078be..310f48fc 100644 --- a/e2e/oauth-authorization.spec.ts +++ b/e2e/oauth-authorization.spec.ts @@ -103,7 +103,6 @@ test.describe("OAuth authorization-code popup flow", () => { }); await page.goto(APP.SERVERS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: /Connect/i }).click(); @@ -209,7 +208,6 @@ test.describe("OAuth authorization-code popup flow", () => { }); await page.goto(APP.SERVERS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: /Connect/i }).click(); await page.getByLabel("Name").fill(GATEWAY_NAME); @@ -261,7 +259,6 @@ test.describe("OAuth authorization-code popup flow", () => { }); await page.goto(APP.SERVERS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: /Connect/i }).click(); await page.getByLabel("Name").fill(GATEWAY_NAME); diff --git a/e2e/plugins.spec.ts b/e2e/plugins.spec.ts index e8bda5ff..c3c2c087 100644 --- a/e2e/plugins.spec.ts +++ b/e2e/plugins.spec.ts @@ -60,7 +60,6 @@ test.describe("Plugins page", () => { await mockPlugins(page, [GUARDRAILS, LOGGING]); await page.goto(APP.PLUGINS); - await page.waitForLoadState("networkidle"); await expect(page.getByRole("heading", { name: "Plugins" })).toBeVisible(); const list = page.getByRole("list", { name: "Catalog plugins" }); @@ -74,7 +73,6 @@ test.describe("Plugins page", () => { await mockPlugins(page, [GUARDRAILS, LOGGING]); await page.goto(APP.PLUGINS); - await page.waitForLoadState("networkidle"); // ListSearch collapses to an icon; the button shares the input's aria-label // and its click handler focuses (and thereby expands) the input. @@ -90,7 +88,6 @@ test.describe("Plugins page", () => { await mockPlugins(page, [GUARDRAILS, LOGGING]); await page.goto(APP.PLUGINS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: /^Filters(, \d+ active)?$/ }).click(); await page.getByRole("combobox", { name: "Hook" }).click(); @@ -116,7 +113,6 @@ test.describe("Plugins page", () => { await mockPlugins(page, [GUARDRAILS, LOGGING]); await page.goto(APP.PLUGINS); - await page.waitForLoadState("networkidle"); const viewToggle = page.getByRole("group", { name: "Plugins view" }); await viewToggle.getByRole("button", { name: "Enabled" }).click(); @@ -133,7 +129,6 @@ test.describe("Plugins page", () => { await mockPlugins(page, [GUARDRAILS, LOGGING]); await page.goto(APP.PLUGINS); - await page.waitForLoadState("networkidle"); const viewButton = page.getByRole("button", { name: "View PII Guardrails" }); await viewButton.click(); @@ -157,7 +152,6 @@ test.describe("Plugins page", () => { await mockPlugins(page, [], false); await page.goto(APP.PLUGINS); - await page.waitForLoadState("networkidle"); await expect( page.getByText( @@ -197,7 +191,6 @@ test.describe("Plugins page", () => { }); await page.goto(APP.PLUGINS); - await page.waitForLoadState("networkidle"); await expect(page.getByRole("alert")).toHaveText("Unable to load plugins. Try again."); diff --git a/e2e/prompts.spec.ts b/e2e/prompts.spec.ts index e77ebd36..b5f147f1 100644 --- a/e2e/prompts.spec.ts +++ b/e2e/prompts.spec.ts @@ -60,7 +60,6 @@ test.describe("Prompts page", () => { }); await page.goto(APP.PROMPTS); - await page.waitForLoadState("networkidle"); await expect(page.getByRole("heading", { name: "Prompts" })).toBeVisible(); await expect(page.getByRole("button", { name: "Add prompts" })).toBeVisible(); @@ -81,7 +80,6 @@ test.describe("Prompts page", () => { }); await page.goto(APP.PROMPTS); - await page.waitForLoadState("networkidle"); await expect(page.getByRole("heading", { name: "Prompts" })).toBeVisible(); await expect(page.getByRole("button", { name: "Add prompts" })).not.toBeVisible(); @@ -97,7 +95,6 @@ test.describe("Prompts page", () => { }); await page.goto(APP.PROMPTS); - await page.waitForLoadState("networkidle"); const alert = page.getByRole("alert"); await expect(alert).toBeVisible(); @@ -123,7 +120,6 @@ test.describe("Prompts page", () => { }); await page.goto(APP.PROMPTS); - await page.waitForLoadState("networkidle"); await expect(page.getByText("github-server")).toBeVisible(); await expect(page.getByText("REST prompts")).toBeVisible(); @@ -146,7 +142,6 @@ test.describe("Prompts page", () => { }); await page.goto(APP.PROMPTS); - await page.waitForLoadState("networkidle"); await expect(page.getByText("prompt_1")).toBeVisible(); await expect(page.getByText("prompt_8")).toBeVisible(); @@ -165,7 +160,6 @@ test.describe("Prompts page", () => { }); await page.goto(APP.PROMPTS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "More options for github-server" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -193,7 +187,6 @@ test.describe("Prompts page", () => { }); await page.goto(APP.PROMPTS); - await page.waitForLoadState("networkidle"); const kebab = page.getByRole("button", { name: `More options for ${longSlug}` }); await expect(kebab).toBeVisible(); @@ -254,7 +247,6 @@ test.describe("Prompts page", () => { }); await page.goto(APP.PROMPTS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Add prompts" }).click(); await expect(page.getByRole("heading", { name: "Add prompt" })).toBeVisible(); @@ -308,7 +300,6 @@ test.describe("Prompts page", () => { }); await page.goto(APP.PROMPTS); - await page.waitForLoadState("networkidle"); // Open the details panel for the REST prompts group. await page.getByRole("button", { name: "More options for REST prompts" }).click(); @@ -374,7 +365,6 @@ test.describe("Prompts page", () => { }); await page.goto(APP.PROMPTS); - await page.waitForLoadState("networkidle"); // Open the federated group's panel and launch the edit form. await page.getByRole("button", { name: "More options for hugging-face" }).click(); @@ -443,7 +433,6 @@ test.describe("Prompts page", () => { }); await page.goto(APP.PROMPTS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "More options for REST prompts" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -497,7 +486,6 @@ test.describe("Prompts page", () => { }); await page.goto(APP.PROMPTS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "More options for REST prompts" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -547,7 +535,6 @@ test.describe("Prompts page", () => { }); await page.goto(APP.PROMPTS); - await page.waitForLoadState("networkidle"); const { panel, dialog } = await openDeleteDialog(page); await expect(dialog).toBeVisible(); @@ -582,7 +569,6 @@ test.describe("Prompts page", () => { }); await page.goto(APP.PROMPTS); - await page.waitForLoadState("networkidle"); const { panel, dialog } = await openDeleteDialog(page); await dialog.getByRole("button", { name: "Delete" }).click(); @@ -627,7 +613,6 @@ test.describe("Prompts page", () => { }); await page.goto(APP.PROMPTS); - await page.waitForLoadState("networkidle"); const { panel, dialog } = await openDeleteDialog(page); await dialog.getByRole("button", { name: "Delete" }).click(); diff --git a/e2e/quick-add-server.spec.ts b/e2e/quick-add-server.spec.ts index ad6cc587..b42bb941 100644 --- a/e2e/quick-add-server.spec.ts +++ b/e2e/quick-add-server.spec.ts @@ -51,7 +51,6 @@ async function openQuickAddDialog(page: import("@playwright/test").Page) { }); await page.goto(APP.SERVERS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: /Connect/i }).click(); await page.getByRole("button", { name: /mcp server catalog/i }).click(); diff --git a/e2e/resources.spec.ts b/e2e/resources.spec.ts index d0f101f2..74ff8b05 100644 --- a/e2e/resources.spec.ts +++ b/e2e/resources.spec.ts @@ -49,7 +49,6 @@ test.describe("Resources page", () => { }); await page.goto(APP.RESOURCES); - await page.waitForLoadState("networkidle"); await expect(page.getByText("Add resources")).toBeVisible(); await expect( @@ -71,7 +70,6 @@ test.describe("Resources page", () => { }); await page.goto(APP.RESOURCES); - await page.waitForLoadState("networkidle"); await expect(page.getByRole("heading", { name: "Resources" })).toBeVisible(); await expect(page.getByText("Add resources")).not.toBeVisible(); @@ -87,7 +85,6 @@ test.describe("Resources page", () => { }); await page.goto(APP.RESOURCES); - await page.waitForLoadState("networkidle"); await page.getByText("Add resources").click(); @@ -105,7 +102,6 @@ test.describe("Resources page", () => { }); await page.goto(APP.RESOURCES); - await page.waitForLoadState("networkidle"); await expect(page.getByRole("alert")).toBeVisible(); await expect(page.getByText("Error loading resources")).toBeVisible(); @@ -121,7 +117,6 @@ test.describe("Resources page", () => { }); await page.goto(APP.RESOURCES); - await page.waitForLoadState("networkidle"); await expect(page.getByRole("heading", { name: "Resources" })).toBeVisible(); @@ -150,7 +145,6 @@ test.describe("Resources page", () => { }); await page.goto(APP.RESOURCES); - await page.waitForLoadState("networkidle"); await expect(page.getByText("big-gateway")).toBeVisible(); await expect(page.getByText("10 resources")).toBeVisible(); @@ -173,7 +167,6 @@ test.describe("Resources page", () => { }); await page.goto(APP.RESOURCES); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "More options for github-server" }).click(); @@ -199,7 +192,6 @@ test.describe("Resources page", () => { }); await page.goto(APP.RESOURCES); - await page.waitForLoadState("networkidle"); const kebab = page.getByRole("button", { name: `More options for ${longSlug}` }); await expect(kebab).toBeVisible(); @@ -231,7 +223,6 @@ test.describe("Resources page", () => { }); await page.goto(APP.RESOURCES); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "More options for github-server" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -253,7 +244,6 @@ test.describe("Resources page", () => { }); await page.goto(APP.RESOURCES); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "More options for github-server" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -289,7 +279,6 @@ test.describe("Resources page", () => { }); await page.goto(APP.RESOURCES); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "More options for github-server" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -333,7 +322,6 @@ test.describe("Resources page", () => { }); await page.goto(APP.RESOURCES); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "More options for github-server" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -369,7 +357,6 @@ test.describe("Resources page", () => { }); await page.goto(APP.RESOURCES); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "More options for github-server" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -413,7 +400,6 @@ test.describe("Resources page", () => { }); await page.goto(APP.RESOURCES); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "More options for github-server" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -462,7 +448,6 @@ test.describe("Resources page", () => { }); await page.goto(APP.RESOURCES); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "More options for github-server" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -507,7 +492,6 @@ test.describe("Resources page", () => { }); await page.goto(APP.RESOURCES); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "More options for solo-gateway" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -555,7 +539,6 @@ test.describe("Resources page", () => { }); await page.goto(APP.RESOURCES); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "More options for multi-gw" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -603,7 +586,6 @@ test.describe("Resources page", () => { }); await page.goto(APP.RESOURCES); - await page.waitForLoadState("networkidle"); await expect(page.getByText("lone-gateway")).toBeVisible(); await expect(page.getByText("github-server")).toBeVisible(); @@ -662,7 +644,6 @@ test.describe("Resources page", () => { }); await page.goto(APP.RESOURCES); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "More options for github-server" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -719,7 +700,6 @@ test.describe("Resources page", () => { }); await page.goto(APP.RESOURCES); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "More options for github-server" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); diff --git a/e2e/server-catalog.spec.ts b/e2e/server-catalog.spec.ts index f6ec9235..e565b2cf 100644 --- a/e2e/server-catalog.spec.ts +++ b/e2e/server-catalog.spec.ts @@ -146,7 +146,6 @@ test.describe("Server catalog page", () => { await mockCatalog(page, [OPEN_CONNECTED, OPEN_AVAILABLE, API_KEY_SERVER]); await page.goto(APP.SERVER_CATALOG); - await page.waitForLoadState("networkidle"); await expect(page.getByRole("heading", { name: "Server catalog" })).toBeVisible(); const catalogList = page.getByRole("list", { name: "Catalog servers" }); @@ -162,7 +161,6 @@ test.describe("Server catalog page", () => { await mockCatalog(page, [OPEN_CONNECTED, OPEN_AVAILABLE]); await page.goto(APP.SERVER_CATALOG); - await page.waitForLoadState("networkidle"); // ListSearch collapses to an icon; the button shares the input's aria-label // and its click handler focuses (and thereby expands) the input. @@ -178,7 +176,6 @@ test.describe("Server catalog page", () => { await mockCatalog(page, [OPEN_CONNECTED, OPEN_AVAILABLE]); await page.goto(APP.SERVER_CATALOG); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: /^Filters(, \d+ active)?$/ }).click(); const popover = page.getByRole("dialog", { name: "Filters" }); @@ -201,7 +198,6 @@ test.describe("Server catalog page", () => { await mockCatalog(page, [OPEN_CONNECTED, OPEN_AVAILABLE]); await page.goto(APP.SERVER_CATALOG); - await page.waitForLoadState("networkidle"); const viewToggle = page.getByRole("group", { name: "Catalog view" }); await viewToggle.getByRole("button", { name: "Connected" }).click(); @@ -220,7 +216,6 @@ test.describe("Server catalog page", () => { await mockCatalog(page, [OPEN_CONNECTED, OPEN_AVAILABLE]); await page.goto(APP.SERVER_CATALOG); - await page.waitForLoadState("networkidle"); const actionsButton = page.getByRole("button", { name: "Actions for Globalping" }); await actionsButton.click(); @@ -253,7 +248,6 @@ test.describe("Server catalog page", () => { }); await page.goto(APP.SERVER_CATALOG); - await page.waitForLoadState("networkidle"); await expect(page.getByText("Server catalog is disabled for this gateway.")).toBeVisible(); }); @@ -280,7 +274,6 @@ test.describe("Server catalog page", () => { }); await page.goto(APP.SERVER_CATALOG); - await page.waitForLoadState("networkidle"); await expect(page.getByRole("alert")).toHaveText("Unable to load server catalog. Try again."); @@ -403,7 +396,6 @@ test.describe("Server catalog page", () => { ); await page.goto(APP.SERVER_CATALOG); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: /^Filters(, \d+ active)?$/ }).click(); await page.getByRole("group", { name: "Providers" }).waitFor(); diff --git a/e2e/servers.spec.ts b/e2e/servers.spec.ts index c960d222..7050a766 100644 --- a/e2e/servers.spec.ts +++ b/e2e/servers.spec.ts @@ -56,7 +56,6 @@ test.describe("MCP Servers page", () => { }); await page.goto(APP.SERVERS); - await page.waitForLoadState("networkidle"); await expect(page.getByText("Connect MCP server")).toBeVisible(); await expect( @@ -76,6 +75,7 @@ test.describe("MCP Servers page", () => { }); await page.goto(APP.SERVERS); + // networkidle: the absence check below is only meaningful once the page settles. await page.waitForLoadState("networkidle"); await expect(page.getByText("Connect MCP server")).not.toBeVisible(); @@ -96,7 +96,6 @@ test.describe("MCP Servers page", () => { }); await page.goto(APP.SERVERS); - await page.waitForLoadState("networkidle"); await expect(page.getByRole("heading", { name: "MCP Servers" })).toBeVisible(); await expect(page.getByRole("button", { name: /Connect/i })).not.toBeVisible(); @@ -112,7 +111,6 @@ test.describe("MCP Servers page", () => { }); await page.goto(APP.SERVERS); - await page.waitForLoadState("networkidle"); await expect(page.getByRole("heading", { name: "MCP Servers" })).toBeVisible(); await expect(page.getByRole("row").filter({ hasText: "GitHub MCP Server" })).toBeVisible(); @@ -129,7 +127,6 @@ test.describe("MCP Servers page", () => { }); await page.goto(APP.SERVERS); - await page.waitForLoadState("networkidle"); await expect(page.getByRole("alert")).toBeVisible(); await expect(page.getByText("Error loading servers")).toBeVisible(); @@ -145,7 +142,6 @@ test.describe("MCP Servers page", () => { }); await page.goto(APP.SERVERS); - await page.waitForLoadState("networkidle"); await expect(page.getByRole("row").filter({ hasText: "GitHub MCP Server" })).toBeVisible(); await expect(page.getByRole("row").filter({ hasText: "Slack MCP Server" })).toBeVisible(); @@ -168,7 +164,6 @@ test.describe("MCP Servers page", () => { }); await page.goto(APP.SERVERS); - await page.waitForLoadState("networkidle"); await expect(page.getByRole("row").filter({ hasText: "GitHub MCP Server" })).toBeVisible(); await expect(page.getByRole("row").filter({ hasText: "Slack MCP Server" })).not.toBeVisible(); @@ -192,6 +187,7 @@ test.describe("MCP Servers page", () => { }); await page.goto(APP.SERVERS); + // networkidle: the absence check below is only meaningful once the page settles. await page.waitForLoadState("networkidle"); await expect(page.getByRole("button", { name: /Load More/i })).not.toBeVisible(); @@ -207,7 +203,6 @@ test.describe("MCP Servers page", () => { }); await page.goto(APP.SERVERS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for GitHub MCP Server" }).click(); @@ -232,7 +227,6 @@ test.describe("MCP Servers page", () => { }); await page.goto(APP.SERVERS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for GitHub MCP Server" }).click(); await expect(page.getByRole("menuitem", { name: "Refresh" })).toHaveCount(0); @@ -267,7 +261,6 @@ test.describe("MCP Servers page", () => { }); await page.goto(APP.SERVERS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for GitHub MCP Server" }).click(); await page.getByRole("menuitem", { name: "Refresh" }).click(); @@ -301,7 +294,6 @@ test.describe("MCP Servers page", () => { }); await page.goto(APP.SERVERS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for GitHub MCP Server" }).click(); await page.getByRole("menuitem", { name: "Refresh" }).click(); @@ -328,7 +320,6 @@ test.describe("MCP Servers page", () => { }); await page.goto(APP.SERVERS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for GitHub MCP Server" }).click(); await page.getByRole("menuitem", { name: "Refresh" }).click(); @@ -356,7 +347,6 @@ test.describe("MCP Servers page", () => { }); await page.goto(APP.SERVERS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for GitHub MCP Server" }).click(); await page.getByRole("menuitem", { name: "Refresh" }).click(); @@ -387,7 +377,6 @@ test.describe("MCP Servers page", () => { }); await page.goto(APP.SERVERS); - await page.waitForLoadState("networkidle"); await expect(page.getByRole("row").filter({ hasText: "GitHub MCP Server" })).toBeVisible(); await expect(page.getByRole("row").filter({ hasText: "Slack MCP Server" })).toBeVisible(); @@ -424,7 +413,6 @@ test.describe("MCP Servers page", () => { }); await page.goto(APP.SERVERS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for GitHub MCP Server" }).click(); await page.getByRole("menuitem", { name: "Delete" }).click(); @@ -459,7 +447,6 @@ test.describe("MCP Servers page", () => { }); await page.goto(APP.SERVERS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for GitHub MCP Server" }).click(); await page.getByRole("menuitem", { name: "Delete" }).click(); @@ -520,7 +507,6 @@ test.describe("MCP Servers page", () => { ); await page.goto(APP.SERVERS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for GitHub MCP Server" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -586,7 +572,6 @@ test.describe("MCP Servers page", () => { ); await page.goto(APP.SERVERS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for GitHub MCP Server" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -653,7 +638,6 @@ test.describe("MCP Servers page", () => { ); await page.goto(APP.SERVERS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for GitHub MCP Server" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -715,7 +699,6 @@ test.describe("MCP Servers page", () => { }); await page.goto(APP.SERVERS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for GitHub MCP Server" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -735,7 +718,6 @@ test.describe("MCP Servers page", () => { }); await page.goto(APP.SERVERS); - await page.waitForLoadState("networkidle"); await expect(page.locator("#limit-select")).toBeVisible(); await expect(page.getByText(/Per page:/i)).toBeVisible(); diff --git a/e2e/smoke/team-switcher.spec.ts b/e2e/smoke/team-switcher.spec.ts index d2cecb24..f1f65cf5 100644 --- a/e2e/smoke/team-switcher.spec.ts +++ b/e2e/smoke/team-switcher.spec.ts @@ -22,7 +22,6 @@ test.describe("TeamSwitcher component (smoke)", () => { // Navigate to a page with sidebar await page.goto(APP.GATEWAYS); - await page.waitForLoadState("networkidle"); // Wait for TeamSwitcher to be visible const trigger = page.getByRole("button", { name: /all teams/i }); @@ -53,7 +52,6 @@ test.describe("TeamSwitcher component (smoke)", () => { }); await page.goto(APP.GATEWAYS); - await page.waitForLoadState("networkidle"); // TeamSwitcher should still render with "All teams" const trigger = page.getByRole("button", { name: /all teams/i }); diff --git a/e2e/teams.spec.ts b/e2e/teams.spec.ts index 4a8a32fc..6e008b0e 100644 --- a/e2e/teams.spec.ts +++ b/e2e/teams.spec.ts @@ -121,7 +121,6 @@ test.describe("Teams page", () => { }); await page.goto(APP.TEAMS); - await page.waitForLoadState("networkidle"); const main = page.getByRole("main"); await expect(page.getByRole("heading", { name: "Teams" })).toBeVisible(); @@ -146,7 +145,6 @@ test.describe("Teams page", () => { }); await page.goto(APP.TEAMS); - await page.waitForLoadState("networkidle"); await expect(page.getByText("No teams yet")).toBeVisible(); await expect( @@ -173,7 +171,6 @@ test.describe("Teams page", () => { }); await page.goto(APP.TEAMS); - await page.waitForLoadState("networkidle"); await expect(page.getByText("Engineering")).toBeVisible(); await expect(page.getByText("Public Team")).toBeVisible(); @@ -232,7 +229,6 @@ test.describe("Teams page", () => { }); await page.goto(APP.TEAMS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Create Team" }).click(); await expect(page.getByRole("heading", { name: "Create Team" })).toBeVisible(); @@ -309,7 +305,6 @@ test.describe("Teams page", () => { }); await page.goto(APP.TEAMS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Create Team" }).click(); @@ -391,7 +386,6 @@ test.describe("Teams page", () => { }); await page.goto(APP.TEAMS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Create Team" }).click(); @@ -475,8 +469,16 @@ test.describe("Teams page", () => { } }); + // Stub the directory search so typing never sends a real request to the preview server. + await page.route("**/auth/email/admin/users?*", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ users: [] }), + }); + }); + await page.goto(APP.TEAMS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Create Team" }).click(); await page.locator("#team-name").fill("Team With Invite"); @@ -508,7 +510,6 @@ test.describe("Teams page", () => { }); await page.goto(APP.TEAMS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Create Team" }).click(); @@ -574,7 +575,6 @@ test.describe("Teams page", () => { }); await page.goto(APP.TEAMS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Create Team" }).click(); @@ -637,7 +637,6 @@ test.describe("Teams page", () => { }); await page.goto(APP.TEAMS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for Engineering" }).click(); await page.getByRole("menuitem", { name: "Edit" }).click(); @@ -671,7 +670,6 @@ test.describe("Teams page", () => { }); await page.goto(APP.TEAMS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for Engineering" }).click(); await page.getByRole("menuitem", { name: "Edit" }).click(); @@ -694,7 +692,6 @@ test.describe("Teams page", () => { }); await page.goto(APP.TEAMS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for Engineering" }).click(); await page.getByRole("menuitem", { name: "Edit" }).click(); @@ -723,7 +720,6 @@ test.describe("Teams page", () => { }); await page.goto(APP.TEAMS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for Engineering" }).click(); await page.getByRole("menuitem", { name: "Manage Members" }).click(); @@ -784,11 +780,9 @@ test.describe("Teams page", () => { }); await page.goto(APP.TEAMS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for Engineering" }).click(); await page.getByRole("menuitem", { name: "Manage Members" }).click(); - await page.getByRole("button", { name: "Add member" }).click(); await pickComboboxOption( page, @@ -862,12 +856,19 @@ test.describe("Teams page", () => { } }); + // Stub the directory search so typing never sends a real request to the preview server. + await page.route("**/auth/email/admin/users?*", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ users: [] }), + }); + }); + await page.goto(APP.TEAMS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for Engineering" }).click(); await page.getByRole("menuitem", { name: "Manage Members" }).click(); - await page.getByRole("button", { name: "Add member" }).click(); // No matching directory entry — commit the raw email via "Use ...". await enterCustomComboboxEmail( @@ -939,12 +940,19 @@ test.describe("Teams page", () => { } }); + // Stub the directory search so typing never sends a real request to the preview server. + await page.route("**/auth/email/admin/users?*", async (route) => { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ users: [] }), + }); + }); + await page.goto(APP.TEAMS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for Engineering" }).click(); await page.getByRole("menuitem", { name: "Manage Members" }).click(); - await page.getByRole("button", { name: "Add member" }).click(); await enterCustomComboboxEmail( page, @@ -1001,7 +1009,6 @@ test.describe("Teams page", () => { ); await page.goto(APP.TEAMS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for Engineering" }).click(); await page.getByRole("menuitem", { name: "Manage Members" }).click(); @@ -1049,7 +1056,6 @@ test.describe("Teams page", () => { ); await page.goto(APP.TEAMS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for Engineering" }).click(); await page.getByRole("menuitem", { name: "Manage Members" }).click(); @@ -1112,7 +1118,6 @@ test.describe("Teams page", () => { }); await page.goto(APP.TEAMS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for Engineering" }).click(); await page.getByRole("menuitem", { name: "Manage Members" }).click(); @@ -1163,7 +1168,6 @@ test.describe("Teams page", () => { }); await page.goto(APP.TEAMS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for Engineering" }).click(); await page.getByRole("menuitem", { name: "Manage Members" }).click(); @@ -1210,7 +1214,6 @@ test.describe("Teams page", () => { }); await page.goto(APP.TEAMS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for Engineering" }).click(); await page.getByRole("menuitem", { name: "Manage Members" }).click(); @@ -1253,7 +1256,6 @@ test.describe("Teams page", () => { }); await page.goto(APP.TEAMS); - await page.waitForLoadState("networkidle"); await expect(page.getByText("Engineering")).toBeVisible(); await expect(page.getByText("Public Team")).toBeVisible(); @@ -1287,7 +1289,6 @@ test.describe("Teams page", () => { }); await page.goto(APP.TEAMS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for Engineering" }).click(); await page.getByRole("menuitem", { name: "Delete" }).click(); @@ -1324,7 +1325,6 @@ test.describe("Teams page", () => { }); await page.goto(APP.TEAMS); - await page.waitForLoadState("networkidle"); await expect(page.getByText("Engineering")).toBeVisible(); @@ -1353,7 +1353,6 @@ test.describe("Teams page", () => { }); await page.goto(APP.TEAMS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for Engineering" }).click(); await page.getByRole("menuitem", { name: "Delete" }).click(); @@ -1390,7 +1389,6 @@ test.describe("Teams page", () => { }); await page.goto(APP.TEAMS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Create Team" }).click(); await page.locator("#team-name").fill("Duplicate Team"); @@ -1421,7 +1419,6 @@ test.describe("Teams page", () => { }); await page.goto(APP.TEAMS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for Engineering" }).click(); await page.getByRole("menuitem", { name: "Edit" }).click(); @@ -1469,7 +1466,6 @@ test.describe("Teams page", () => { }); await page.goto(APP.TEAMS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Create Team" }).click(); await page.locator("#team-name").fill("Slow Team"); @@ -1499,7 +1495,6 @@ test.describe("Teams page", () => { }); await page.goto(APP.TEAMS); - await page.waitForLoadState("networkidle"); // aria-labels on the row's action buttons also contain the long name, so // scope to visible text (getByText) rather than accessible name (getByRole) @@ -1532,7 +1527,6 @@ test.describe("Teams page", () => { }); await page.goto(APP.TEAMS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for Engineering" }).click(); await page.getByRole("menuitem", { name: "Manage Members" }).click(); @@ -1567,7 +1561,6 @@ test.describe("Teams page", () => { }); await page.goto(APP.TEAMS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for Engineering" }).click(); await page.getByRole("menuitem", { name: "Manage Members" }).click(); diff --git a/e2e/tokens.spec.ts b/e2e/tokens.spec.ts index d34cf988..9eb51990 100644 --- a/e2e/tokens.spec.ts +++ b/e2e/tokens.spec.ts @@ -67,7 +67,6 @@ test.describe("API Tokens", () => { }); await page.goto(TOKENS_TAB); - await page.waitForLoadState("networkidle"); await expect(page.getByRole("tab", { name: "API tokens" })).toBeVisible(); await expect(page.getByText("CI token")).toBeVisible(); @@ -85,7 +84,6 @@ test.describe("API Tokens", () => { }); await page.goto(TOKENS_TAB); - await page.waitForLoadState("networkidle"); await expect(page.getByRole("heading", { name: "Generate API token" })).toBeVisible(); await expect(page.getByRole("button", { name: "Generate token" })).toBeVisible(); @@ -118,7 +116,6 @@ test.describe("API Tokens", () => { }); await page.goto(TOKENS_TAB); - await page.waitForLoadState("networkidle"); // Empty state -> open the create form. await page.getByRole("button", { name: "Generate token" }).click(); @@ -160,7 +157,6 @@ test.describe("API Tokens", () => { }); await page.goto(TOKENS_TAB); - await page.waitForLoadState("networkidle"); await expect(page.getByText("CI token")).toBeVisible(); diff --git a/e2e/tools.spec.ts b/e2e/tools.spec.ts index 61f9a49c..1ec6c06d 100644 --- a/e2e/tools.spec.ts +++ b/e2e/tools.spec.ts @@ -113,7 +113,6 @@ test.describe("Tools page", () => { }); await page.goto(APP.TOOLS); - await page.waitForLoadState("networkidle"); await expect(page.getByText("Add tools")).toBeVisible(); await expect( @@ -132,7 +131,6 @@ test.describe("Tools page", () => { }); await page.goto(APP.TOOLS); - await page.waitForLoadState("networkidle"); await expect(page.getByRole("heading", { name: "Tools" })).toBeVisible(); await expect(page.getByText("Add tools")).not.toBeVisible(); @@ -148,7 +146,6 @@ test.describe("Tools page", () => { }); await page.goto(APP.TOOLS); - await page.waitForLoadState("networkidle"); // Click the Add tools card await page.getByText("Add tools").click(); @@ -166,7 +163,6 @@ test.describe("Tools page", () => { }); await page.goto(APP.TOOLS); - await page.waitForLoadState("networkidle"); await expect(page.getByRole("heading", { name: "Tools" })).toBeVisible(); @@ -191,7 +187,6 @@ test.describe("Tools page", () => { }); await page.goto(APP.TOOLS); - await page.waitForLoadState("networkidle"); await expect(page.getByRole("alert")).toBeVisible(); await expect(page.getByText("Error loading tools")).toBeVisible(); @@ -211,7 +206,6 @@ test.describe("Tools page", () => { }); await page.goto(APP.TOOLS); - await page.waitForLoadState("networkidle"); await expect(page.getByText("big-gateway")).toBeVisible(); await expect(page.getByText("10 tools")).toBeVisible(); @@ -239,7 +233,6 @@ test.describe("Tools page", () => { }); await page.goto(APP.TOOLS); - await page.waitForLoadState("networkidle"); // The kebab menu for the long-named group must stay visible. const kebab = page.getByRole("button", { name: `More options for ${longSlug}` }); @@ -276,7 +269,6 @@ test.describe("Tools page", () => { }); await page.goto(APP.TOOLS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "More options for github-server" }).click(); @@ -293,7 +285,6 @@ test.describe("Tools page", () => { }); await page.goto(APP.TOOLS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "More options for github-server" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -349,7 +340,6 @@ test.describe("Tools page", () => { }); await page.goto(APP.TOOLS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "More options for github-server" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -429,7 +419,6 @@ test.describe("Tools page", () => { }); await page.goto(APP.TOOLS); - await page.waitForLoadState("networkidle"); const panel = await openToolDetails(page, "github-server"); await expect(panel.getByText("MCP version 2025-11-25")).toBeVisible(); @@ -490,7 +479,6 @@ test.describe("Tools page", () => { }); await page.goto(APP.TOOLS); - await page.waitForLoadState("networkidle"); const panel = await openToolDetails(page, "local-gateway"); await panel.getByRole("switch", { name: "Live invocation" }).click(); @@ -555,7 +543,6 @@ test.describe("Tools page", () => { }); await page.goto(APP.TOOLS); - await page.waitForLoadState("networkidle"); const panel = await openToolDetails(page, "github-server"); await panel.getByRole("switch", { name: "Live invocation" }).click(); @@ -590,7 +577,6 @@ test.describe("Tools page", () => { await routeToolsList(page, [liveTool]); await page.goto(APP.TOOLS); - await page.waitForLoadState("networkidle"); const panel = await openToolDetails(page, "github-server"); await expect(panel.getByRole("button", { name: "Preview", exact: true })).toBeVisible(); @@ -607,7 +593,6 @@ test.describe("Tools page", () => { await routeToolsList(page, [liveTool]); await page.goto(APP.TOOLS); - await page.waitForLoadState("networkidle"); const panel = await openToolDetails(page, "github-server"); await expect(panel.getByRole("button", { name: "Preview", exact: true })).toBeVisible(); @@ -623,7 +608,6 @@ test.describe("Tools page", () => { await routeToolsList(page, [federatedTool]); await page.goto(APP.TOOLS); - await page.waitForLoadState("networkidle"); const panel = await openToolDetails(page, "github-server"); await expect( @@ -659,7 +643,6 @@ test.describe("Tools page", () => { }); await page.goto(APP.TOOLS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "More options for github-server" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -691,7 +674,6 @@ test.describe("Tools page", () => { }); await page.goto(APP.TOOLS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "More options for github-server" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -726,7 +708,6 @@ test.describe("Tools page", () => { }); await page.goto(APP.TOOLS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "More options for github-server" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -778,7 +759,6 @@ test.describe("Tools page", () => { }); await page.goto(APP.TOOLS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "More options for github-server" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -812,7 +792,6 @@ test.describe("Tools page", () => { }); await page.goto(APP.TOOLS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "More options for github-server" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -852,7 +831,6 @@ test.describe("Tools page", () => { }); await page.goto(APP.TOOLS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "More options for solo-gateway" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -900,7 +878,6 @@ test.describe("Tools page", () => { }); await page.goto(APP.TOOLS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "More options for multi-gw" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -951,7 +928,6 @@ test.describe("Tools page", () => { }); await page.goto(APP.TOOLS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "More options for rb-gateway" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -997,7 +973,6 @@ test.describe("Tools page", () => { }); await page.goto(APP.TOOLS); - await page.waitForLoadState("networkidle"); await expect(page.getByText("lone-gateway")).toBeVisible(); await expect(page.getByText("github-server")).toBeVisible(); @@ -1031,7 +1006,6 @@ test.describe("Tools page", () => { }); await page.goto(APP.TOOLS); - await page.waitForLoadState("networkidle"); const addToolsCard = page.getByRole("button").filter({ hasText: "Add tools" }); await addToolsCard.focus(); @@ -1062,7 +1036,6 @@ test.describe("Tools page", () => { }); await page.goto(APP.TOOLS); - await page.waitForLoadState("networkidle"); await openAddToolForm(page); await fillToolBasics(page, "list_users", "https://api.example.com/users"); @@ -1096,7 +1069,6 @@ test.describe("Tools page", () => { }); await page.goto(APP.TOOLS); - await page.waitForLoadState("networkidle"); await openAddToolForm(page); await fillToolBasics(page, "get_user", "https://api.example.com/users/1"); @@ -1124,7 +1096,6 @@ test.describe("Tools page", () => { }); await page.goto(APP.TOOLS); - await page.waitForLoadState("networkidle"); await openAddToolForm(page); await fillToolBasics(page, "denied_tool", "https://api.example.com/denied"); @@ -1211,7 +1182,6 @@ test.describe("Tools page", () => { }); await page.goto(APP.TOOLS); - await page.waitForLoadState("networkidle"); await openAddToolForm(page); await fillToolBasics(page, "auth_tool", "https://api.example.com/secure"); @@ -1263,7 +1233,6 @@ test.describe("Tools page", () => { }); await page.goto(APP.TOOLS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "More options for edit-gw" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -1304,7 +1273,6 @@ test.describe("Tools page", () => { await routeToolsList(page, [TOOL]); await page.goto(APP.TOOLS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "More options for schema-gw" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -1356,7 +1324,6 @@ test.describe("Tools page", () => { }); await page.goto(APP.TOOLS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "More options for toggle-gw" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -1406,7 +1373,6 @@ test.describe("Tools page", () => { }); await page.goto(APP.TOOLS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "More options for activate-gw" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -1456,7 +1422,6 @@ test.describe("Tools page", () => { }); await page.goto(APP.TOOLS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "More options for fail-gw" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); diff --git a/e2e/users.spec.ts b/e2e/users.spec.ts index 1b815da5..8b845c5d 100644 --- a/e2e/users.spec.ts +++ b/e2e/users.spec.ts @@ -58,7 +58,6 @@ test.describe("Users page", () => { }); await page.goto(APP.USERS); - await page.waitForLoadState("networkidle"); const main = page.getByRole("main"); await expect(page.getByRole("heading", { name: "Users", exact: true })).toBeVisible(); @@ -114,7 +113,6 @@ test.describe("Users page", () => { }); await page.goto(APP.USERS); - await page.waitForLoadState("networkidle"); await expect(page.getByText("John Doe")).toBeVisible(); await expect(page.getByText("Jane Smith")).not.toBeVisible(); @@ -160,7 +158,6 @@ test.describe("Users page", () => { }); await page.goto(APP.USERS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Create User" }).click(); @@ -188,7 +185,6 @@ test.describe("Users page", () => { }); await page.goto(APP.USERS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Create User" }).click(); await expect(page.getByRole("heading", { name: "Create User" })).toBeVisible(); }); @@ -280,7 +276,6 @@ test.describe("Users page", () => { }); await page.goto(APP.USERS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for John Doe" }).click(); await page.getByRole("menuitem", { name: "Edit" }).click(); @@ -311,7 +306,6 @@ test.describe("Users page", () => { }); await page.goto(APP.USERS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for John Doe" }).click(); await page.getByRole("menuitem", { name: "Edit" }).click(); @@ -380,7 +374,6 @@ test.describe("Users page", () => { }); await page.goto(APP.USERS); - await page.waitForLoadState("networkidle"); await expect(page.getByText("John Doe")).toBeVisible(); await expect(page.getByText("Jane Smith")).toBeVisible(); @@ -419,7 +412,6 @@ test.describe("Users page", () => { }); await page.goto(APP.USERS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for John Doe" }).click(); await page.getByRole("menuitem", { name: "Delete" }).click(); @@ -456,7 +448,6 @@ test.describe("Users page", () => { }); await page.goto(APP.USERS); - await page.waitForLoadState("networkidle"); await expect(page.getByText("John Doe")).toBeVisible(); @@ -523,7 +514,6 @@ test.describe("Users page", () => { ); await page.goto(APP.USERS); - await page.waitForLoadState("networkidle"); // Located by email, not by selfName -- an empty/whitespace-only real // full_name renders as "Unnamed User" (see above), which would make a diff --git a/e2e/virtual-servers.spec.ts b/e2e/virtual-servers.spec.ts index 2d65d95d..75923b69 100644 --- a/e2e/virtual-servers.spec.ts +++ b/e2e/virtual-servers.spec.ts @@ -216,7 +216,6 @@ test.describe("Virtual Servers page", () => { }); await page.goto(APP.GATEWAYS); - await page.waitForLoadState("networkidle"); await expect(page.getByRole("heading", { name: "Virtual servers" })).toBeVisible(); const mainContent = page.getByRole("main"); @@ -243,7 +242,6 @@ test.describe("Virtual Servers page", () => { }); await page.goto(APP.GATEWAYS); - await page.waitForLoadState("networkidle"); await expect(page.getByRole("heading", { name: "Virtual servers" })).toBeVisible(); await expect( @@ -261,7 +259,6 @@ test.describe("Virtual Servers page", () => { }); await page.goto(APP.GATEWAYS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: /Create server Make external sources/i }).click(); @@ -459,7 +456,6 @@ test.describe("Virtual Servers page", () => { }); await page.goto(APP.GATEWAYS); - await page.waitForLoadState("networkidle"); // Check for virtual servers heading await expect(page.getByRole("heading", { name: "Virtual servers" })).toBeVisible(); @@ -483,7 +479,6 @@ test.describe("Virtual Servers page", () => { }); await page.goto(APP.GATEWAYS); - await page.waitForLoadState("networkidle"); await expect(page.getByText("testVS")).toBeVisible(); @@ -511,7 +506,6 @@ test.describe("Virtual Servers page", () => { }); await page.goto(APP.GATEWAYS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for testVS" }).click(); @@ -553,7 +547,6 @@ test.describe("Virtual Servers page", () => { }); await page.goto(APP.GATEWAYS); - await page.waitForLoadState("networkidle"); const card = page.getByTestId("virtual-server-card").filter({ hasText: "testVS" }); await expect(card.getByTestId("status-indicator")).toHaveClass(/bg-tool-status-inactive/); @@ -593,7 +586,6 @@ test.describe("Virtual Servers page", () => { }); await page.goto(APP.GATEWAYS); - await page.waitForLoadState("networkidle"); const card = page.getByTestId("virtual-server-card").filter({ hasText: "testVS" }); await page.getByRole("button", { name: "Actions for testVS" }).click(); @@ -646,7 +638,6 @@ test.describe("Virtual Servers page", () => { }); await page.goto(APP.GATEWAYS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for testVS" }).click(); await page.getByRole("menuitem", { name: "Delete" }).click(); @@ -688,7 +679,6 @@ test.describe("Virtual Servers page", () => { }); await page.goto(APP.GATEWAYS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for testVS" }).click(); await page.getByRole("menuitem", { name: "Delete" }).click(); @@ -724,7 +714,6 @@ test.describe("Virtual Servers page", () => { }); await page.goto(APP.GATEWAYS); - await page.waitForLoadState("networkidle"); await expect(page.getByText("testVS")).toBeVisible(); @@ -769,7 +758,6 @@ test.describe("Virtual Servers page", () => { }); await page.goto(APP.GATEWAYS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for testVS" }).click(); await page.getByRole("menuitem", { name: "Delete" }).click(); @@ -820,7 +808,6 @@ test.describe("Virtual Servers page", () => { }); await page.goto(APP.GATEWAYS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for testVS" }).click(); await page.getByRole("menuitem", { name: "Delete" }).click(); @@ -865,7 +852,6 @@ test.describe("Virtual Servers page", () => { }); await page.goto(APP.GATEWAYS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for testVS" }).click(); await page.getByRole("menuitem", { name: "Delete" }).click(); @@ -892,7 +878,6 @@ test.describe("Virtual Servers page", () => { }); await page.goto(APP.GATEWAYS); - await page.waitForLoadState("networkidle"); await expect( page.getByTestId("virtual-server-card").filter({ hasText: "testVS" }), @@ -934,7 +919,6 @@ test.describe("Virtual Servers page", () => { }); await page.goto(APP.GATEWAYS); - await page.waitForLoadState("networkidle"); await expect( page.getByTestId("virtual-server-card").filter({ hasText: "testVS" }), @@ -994,7 +978,6 @@ test.describe("Virtual Servers page", () => { }); await page.goto(APP.GATEWAYS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for testVS" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -1004,7 +987,6 @@ test.describe("Virtual Servers page", () => { await page.keyboard.press("Escape"); // drawer is a full-height overlay; must close it before clicking table row actions await expect(detailsPanel).toHaveCount(0); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for testVS" }).click(); await page.getByRole("menuitem", { name: "Delete" }).click(); @@ -1057,7 +1039,6 @@ test.describe("Virtual Servers page", () => { }); await page.goto(APP.GATEWAYS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for testVS" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -1067,7 +1048,6 @@ test.describe("Virtual Servers page", () => { await page.keyboard.press("Escape"); // drawer is a full-height overlay; must close it before clicking table row actions await expect(detailsPanel).toHaveCount(0); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for testVS" }).click(); await page.getByRole("menuitem", { name: "Delete" }).click(); @@ -1113,7 +1093,6 @@ test.describe("Virtual Servers page", () => { }); await page.goto(APP.GATEWAYS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for testVS" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -1125,8 +1104,6 @@ test.describe("Virtual Servers page", () => { await expect(detailsPanel).toHaveCount(0); - await page.waitForLoadState("networkidle"); - await page.getByRole("button", { name: "Actions for testVS" }).click(); await page.getByRole("menuitem", { name: "Delete" }).click(); @@ -1160,7 +1137,6 @@ test.describe("Virtual Servers page", () => { }); await page.goto(APP.GATEWAYS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for testVS" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -1271,7 +1247,6 @@ test.describe("Virtual Servers page", () => { }); await page.goto(APP.GATEWAYS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for testVS" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -1324,7 +1299,6 @@ test.describe("Virtual Servers page", () => { }); await page.goto(APP.GATEWAYS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for testVS" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -1370,7 +1344,6 @@ test.describe("Virtual Servers page", () => { }); await page.goto(APP.GATEWAYS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for testVS" }).click(); await page.getByRole("menuitem", { name: "View details" }).click(); @@ -1436,7 +1409,6 @@ test.describe("Virtual Servers page", () => { }); await page.goto(APP.GATEWAYS); - await page.waitForLoadState("networkidle"); const panel = await openVirtualServerToolTest(page); await expect(panel.getByRole("button", { name: "Preview" })).toBeVisible(); @@ -1508,7 +1480,6 @@ test.describe("Virtual Servers page", () => { await routeVirtualServerTryIt(page, [makeTryItTool()]); await page.goto(APP.GATEWAYS); - await page.waitForLoadState("networkidle"); const panel = await openVirtualServerToolTest(page); await expect(panel.getByRole("switch", { name: "Live invocation" })).toHaveCount(0); @@ -1520,7 +1491,6 @@ test.describe("Virtual Servers page", () => { test("restores focus after keyboard navigation through tool testing", async ({ page }) => { await routeVirtualServerTryIt(page, [makeTryItTool()]); await page.goto(APP.GATEWAYS); - await page.waitForLoadState("networkidle"); await page.getByRole("button", { name: "Actions for testVS" }).focus(); await page.keyboard.press("Enter"); @@ -1546,7 +1516,6 @@ test.describe("Virtual Servers page", () => { await routeVirtualServerTryIt(page, [makeTryItTool()]); await page.goto(APP.GATEWAYS); - await page.waitForLoadState("networkidle"); const panel = await openVirtualServerToolTest(page); await expect(panel.getByRole("switch", { name: "Live invocation" })).toHaveCount(0); @@ -1566,6 +1535,7 @@ test.describe("Virtual Servers page", () => { }); await page.goto(APP.GATEWAYS); + // networkidle: the absence check below is only meaningful once the page settles. await page.waitForLoadState("networkidle"); await expect(page.getByRole("button", { name: /Open testVS \(coming soon\)/ })).toHaveCount(0); @@ -1582,6 +1552,7 @@ test.describe("Virtual Servers page", () => { }); await page.goto(APP.GATEWAYS); + // networkidle: the absence check below is only meaningful once the page settles. await page.waitForLoadState("networkidle"); await expect(page.getByRole("button", { name: "Virtual server actions" })).toHaveCount(0); @@ -1601,7 +1572,6 @@ test.describe("Virtual Servers page", () => { }); await page.goto(APP.GATEWAYS); - await page.waitForLoadState("networkidle"); // Check for error message await expect(page.getByRole("alert")).toBeVisible(); @@ -1620,7 +1590,6 @@ test.describe("Virtual Servers page", () => { }); await page.goto(APP.GATEWAYS); - await page.waitForLoadState("networkidle"); const card = page.getByTestId("virtual-server-card").filter({ hasText: "testVS" }); @@ -1649,7 +1618,6 @@ test.describe("Virtual Servers page", () => { }); await page.goto(APP.GATEWAYS); - await page.waitForLoadState("networkidle"); await expect(page.getByText("testVS")).toBeVisible(); await expect(page.getByText("Production Server")).toBeVisible(); @@ -1683,7 +1651,6 @@ test.describe("Virtual Servers page", () => { }); await page.goto(APP.GATEWAYS); - await page.waitForLoadState("networkidle"); // Cards are uniform now, so empty servers are no longer sorted to the end // to keep a full-width row from splitting the grid. @@ -1707,7 +1674,6 @@ test.describe("Virtual Servers page", () => { }); await page.goto(APP.GATEWAYS); - await page.waitForLoadState("networkidle"); // Focus the create server card const connectCard = page.getByRole("button", { @@ -1737,7 +1703,6 @@ test.describe("Virtual Servers page", () => { }); await page.goto(APP.GATEWAYS); - await page.waitForLoadState("networkidle"); const card = page.getByTestId("virtual-server-card").filter({ hasText: "testVS" }); await expect(card.getByTestId("last-updated")).toBeVisible(); @@ -1761,7 +1726,6 @@ test.describe("Virtual Servers page", () => { }); await page.goto(APP.GATEWAYS); - await page.waitForLoadState("networkidle"); const card = page.getByTestId("virtual-server-card").filter({ hasText: "testVS" }); @@ -1791,7 +1755,6 @@ test.describe("Virtual Servers page", () => { }); await page.goto(APP.GATEWAYS); - await page.waitForLoadState("networkidle"); await page .getByTestId("virtual-server-card") @@ -1872,7 +1835,6 @@ test.describe("Virtual Servers page", () => { }); await page.goto("/app/gateways/create-server"); - await page.waitForLoadState("networkidle"); await expect(page.getByRole("radio", { name: "Internal" })).toBeChecked(); await page.getByLabel("Name").fill("MCP backed server"); diff --git a/package.json b/package.json index 12b9a3ce..6e3f80a3 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "scripts": { "predev": "npm ls --depth=0 >/dev/null 2>&1 || (echo 'Dependencies missing or out of date at repo root — run: npm install' >&2 && exit 1)", "dev": "vite", - "dev:e2e": "vite --base=/ --port 5173 --strictPort", + "playwright:serve": "vite build --base=/ --sourcemap && vite preview --base=/ --port 5173 --strictPort", "predev:bff": "npm ls --prefix server --depth=0 >/dev/null 2>&1 || (echo 'Dependencies missing or out of date in server/ — run: npm install --prefix server' >&2 && exit 1)", "dev:bff": "VITE_DEV_SERVER_URL=http://localhost:5173 npm run dev --prefix server", "predev:all": "npm run predev --silent && ([ -f openapi.json ] || (echo 'openapi.json is missing at repo root — api:generate reads from it and will fail with a less helpful error. Restore it (e.g. git checkout main -- openapi.json), see README.md#api-types' >&2 && exit 1)) && npm run api:generate", @@ -27,7 +27,7 @@ "test:ui": "vitest --ui", "test:watch": "vitest", "test:coverage": "vitest run --coverage", - "e2e": "playwright test", + "playwright": "playwright test", "e2e:docker:up": "docker compose -f docker-compose.e2e.yml up -d --wait --build", "e2e:docker:seed": "node --env-file-if-exists=.env --experimental-strip-types e2e/seed/seed.ts", "e2e:docker:down": "docker compose -f docker-compose.e2e.yml down -v", @@ -35,11 +35,11 @@ "e2e:docker:tls:up": "docker compose -f docker-compose.e2e.yml -f docker-compose.tls.yml -f docker-compose.e2e-tls.yml up -d --wait --build", "e2e:docker:tls:down": "docker compose -f docker-compose.e2e.yml -f docker-compose.tls.yml -f docker-compose.e2e-tls.yml down -v", "e2e:docker:tls": "(npm run e2e:docker:tls:up && E2E_GATEWAY_URL=https://localhost:4444 NODE_EXTRA_CA_CERTS=$PWD/certs/cert.pem node --env-file-if-exists=.env --experimental-strip-types e2e/seed/seed.ts && E2E_REAL_API=true PLAYWRIGHT_BASE_URL=https://localhost:8443 PLAYWRIGHT_SKIP_WEBSERVER=1 node --env-file-if-exists=.env node_modules/.bin/playwright test --reporter=list; s=$?; npm run e2e:docker:tls:down; exit $s)", - "e2e:html": "playwright test --reporter=html", - "e2e:ui": "playwright test --ui", - "e2e:debug": "playwright test --debug", - "e2e:install": "playwright install --with-deps chromium", - "e2e:report": "playwright show-report", + "playwright:html": "playwright test --reporter=html", + "playwright:ui": "playwright test --ui", + "playwright:debug": "playwright test --debug", + "playwright:install": "playwright install --with-deps chromium", + "playwright:report": "playwright show-report", "i18n:extract": "formatjs extract 'src/**/*.{ts,tsx}' --out-file src/i18n/extracted.json --id-interpolation-pattern '[sha512:contenthash:base64:6]'", "i18n:compile": "formatjs compile-folder --ast src/i18n/locales src/i18n/compiled", "openapi:refresh": "bash scripts/refresh-openapi.sh", diff --git a/playwright.config.ts b/playwright.config.ts index 4ed27bbb..bd0d0b88 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -36,7 +36,7 @@ export default defineConfig({ ignoreHTTPSErrors: BASE_URL.startsWith("https"), trace: "on-first-retry", screenshot: "only-on-failure", - video: "retain-on-failure", + video: IS_CI ? "off" : "retain-on-failure", actionTimeout: 10_000, navigationTimeout: 15_000, extraHTTPHeaders: { @@ -56,7 +56,8 @@ export default defineConfig({ webServer: process.env.PLAYWRIGHT_SKIP_WEBSERVER ? undefined : { - command: "npm run dev:e2e", + // Built bundle, not the dev server: ~40% faster, half the CPU (no per-module transforms). + command: "npm run playwright:serve", url: BASE_URL, reuseExistingServer: REUSE_EXISTING_SERVER, timeout: 120_000, diff --git a/src/components/ui/combobox.test.tsx b/src/components/ui/combobox.test.tsx index 845c6e9e..5fd9330d 100644 --- a/src/components/ui/combobox.test.tsx +++ b/src/components/ui/combobox.test.tsx @@ -91,6 +91,21 @@ describe("Combobox", () => { expect(onValueChange).toHaveBeenCalledWith("new@example.com"); }); + it("keeps the typed search when focus bounces away and back while open", async () => { + const user = userEvent.setup(); + render(); + const input = screen.getByRole("combobox"); + + await user.click(input); + await user.keyboard("new@example.com"); + // A closing menu returning focus to its trigger, then a dialog's focus trap pulling it back. + fireEvent.blur(input); + fireEvent.focus(input); + + expect(input).toHaveValue("new@example.com"); + expect(screen.getByRole("option", { name: 'Use "new@example.com"' })).toBeInTheDocument(); + }); + it("shows the empty text and no custom entry when allowCustomValue is false", async () => { const user = userEvent.setup(); render(); diff --git a/src/components/ui/combobox.tsx b/src/components/ui/combobox.tsx index 16af90e8..bad87e6a 100644 --- a/src/components/ui/combobox.tsx +++ b/src/components/ui/combobox.tsx @@ -98,7 +98,7 @@ export function Combobox({ const handleOpen = () => { clearBlurTimeout(); - if (!disabled) { + if (!disabled && !open) { setOpen(true); setSearchValue(""); }