-
Notifications
You must be signed in to change notification settings - Fork 651
feat(ui-test): Playwright ui tests foundation stage 0 #2180
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
b83445b
6f3c96f
6f1fd4b
be5ee4e
5d1b617
6d5fdb4
8181910
671024c
4f6b52c
081fdd1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| name: UI Playwright E2E | ||
|
|
||
| # Page-level browser E2E for the UI against a mocked backend. | ||
| # Kept separate from CI so the main workflow does not need paths-filter or | ||
| # conditional steps. No cluster required — the stub backend is booted by Playwright. | ||
|
|
||
| on: | ||
| push: | ||
| branches: [main, "release/**"] | ||
| paths: | ||
| - "ui/**" | ||
| pull_request: | ||
| branches: [main, "release/**"] | ||
| paths: | ||
| - "ui/**" | ||
| workflow_dispatch: | ||
|
|
||
| jobs: | ||
| playwright: | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: read | ||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@v6 | ||
|
|
||
| - name: Setup Node.js | ||
| uses: actions/setup-node@v6 | ||
| with: | ||
| node-version: "24" | ||
| cache: "npm" | ||
| cache-dependency-path: ui/package-lock.json | ||
|
|
||
| - name: Install dependencies | ||
| working-directory: ./ui | ||
| env: | ||
| CYPRESS_INSTALL_BINARY: 0 | ||
| run: npm ci | ||
|
|
||
| - name: Install Playwright browser | ||
| working-directory: ./ui | ||
| run: npx playwright install --with-deps chromium | ||
|
|
||
| - name: Run Playwright tests | ||
| working-directory: ./ui | ||
| run: npm run test:pw | ||
|
|
||
| - name: Upload Playwright report | ||
| if: always() | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: playwright-report | ||
| path: | | ||
| ui/playwright-report | ||
| ui/playwright/test-results | ||
| retention-days: 14 |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| import { defineConfig, devices } from "@playwright/test"; | ||
|
|
||
| /** | ||
| * Playwright E2E config for the kagent UI. | ||
| * | ||
| * Data is fetched server-side (Next.js server actions), so browser-level | ||
| * `page.route` cannot intercept `/api/**`. Instead we boot a standalone stub | ||
| * backend (playwright/mocks/server.mjs) and point Next at it via | ||
| * BACKEND_INTERNAL_URL — `getBackendUrl()` (src/lib/utils.ts) checks that env | ||
| * var first. Both servers are started by the `webServer` block below. | ||
| * | ||
| * See playwright/README.md for the full test strategy. | ||
| */ | ||
|
|
||
| const CI = !!process.env.CI; | ||
|
|
||
| const STUB_PORT = 8899; | ||
| const STUB_URL = `http://127.0.0.1:${STUB_PORT}`; | ||
| const APP_URL = "http://localhost:8001"; | ||
|
|
||
| export default defineConfig({ | ||
| testDir: "./playwright/tests", | ||
| outputDir: "./playwright/test-results", | ||
| // Parallelism stays off until Stage 1 per-test data isolation lands: one | ||
| // shared stub backend + one Next server means concurrent tests would race | ||
| // against shared state (see README). Flip both `fullyParallel` and `workers` | ||
| // together when isolation is in place. | ||
| fullyParallel: false, | ||
| forbidOnly: CI, | ||
| retries: CI ? 1 : 0, | ||
| workers: 1, | ||
| timeout: 30_000, | ||
| expect: { timeout: 10_000 }, | ||
| reporter: [["html", { open: "never" }], ["list"]], | ||
| use: { | ||
| baseURL: APP_URL, | ||
| screenshot: "only-on-failure", | ||
| trace: "retain-on-failure", | ||
| video: "off", | ||
| }, | ||
| projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }], | ||
| webServer: [ | ||
| { | ||
| command: "node playwright/mocks/server.mjs", | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. server.mjs reads STUB_PORT from env but this block doesn't pin it, so a shell-set STUB_PORT makes the stub bind to the wrong port and the health check times out
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I pinned STUB_PORT in the stub's webServer env so it always goes to the health endpoint or whatever BACKEND_INTERNAL_URL is set to |
||
| url: `${STUB_URL}/__mock/health`, | ||
| reuseExistingServer: !CI, | ||
| timeout: 30_000, | ||
| stdout: "pipe", | ||
| stderr: "pipe", | ||
| // Pin the port so a shell-exported STUB_PORT can't make the stub bind | ||
| // somewhere other than the health-check / BACKEND_INTERNAL_URL address. | ||
| env: { STUB_PORT: String(STUB_PORT) }, | ||
| }, | ||
| { | ||
| command: "npm run dev", | ||
| url: APP_URL, | ||
| // Never reuse an existing dev server: the BACKEND_INTERNAL_URL below is | ||
| // only applied to a server Playwright starts. A reused server (e.g. a | ||
| // hand-started `npm run dev`) would silently bypass the stub. Always | ||
| // boot our own so the redirect is guaranteed; a busy port fails loudly. | ||
| reuseExistingServer: false, | ||
| timeout: 120_000, | ||
| env: { | ||
| // Redirect the server-side backend fetch to our stub. | ||
| BACKEND_INTERNAL_URL: `${STUB_URL}/api`, | ||
| }, | ||
| }, | ||
| ], | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| # Playwright E2E tests | ||
|
|
||
| Page-level browser end-to-end tests for the kagent UI, run against a **mocked | ||
| backend**. This suite fills the one gap nothing else covers: **multi-step user | ||
| flows across components**. | ||
|
|
||
| ## What this suite does and does not cover | ||
|
|
||
| | Layer | Tool | Not Playwright's job | | ||
| |---|---|---| | ||
| | Atoms (`src/components/ui/*`) | shadcn primitives | skip | | ||
| | Visual / render states | Storybook + Vitest-browser + Chromatic | skip | | ||
| | Unit / logic | Jest (`*.test.ts(x)`) | skip | | ||
| | Page-load smoke (`h1` renders, page reachable, onboarding steps 1–2, model-edit page opens) | Cypress (`cypress/e2e/smoke.cy.ts`) | skip | | ||
| | **Multi-step flows: form submission, payload correctness, streaming, wizard completion, error/edge states** | **Playwright (this suite)** | — | | ||
|
|
||
| Rule: if Cypress / Chromatic / Jest already assert it, Playwright does not. | ||
|
|
||
| ## How mocking works (important) | ||
|
|
||
| Nearly every `/api/**` call runs **server-side** inside Next.js server actions | ||
| (`src/app/actions/*.ts`, all `"use server"`). The browser sends an opaque RPC | ||
| POST to the route; the Node server does the actual backend `fetch`. So browser | ||
| `page.route("**/api/**")` intercepts **nothing** on load. | ||
|
|
||
| Instead we run a standalone **stub backend** (`mocks/server.mjs`) and point Next | ||
| at it with `BACKEND_INTERNAL_URL` — `getBackendUrl()` (`src/lib/utils.ts`) checks | ||
| that env var first. Playwright's `webServer` (in `../playwright.config.ts`) boots | ||
| both the stub (`:8899`) and `next dev` (`:8001`). | ||
|
|
||
| The one exception is A2A chat streaming (`POST /a2a/**`, SSE), which **is** | ||
| browser-originated and `page.route`-able — used in the chat spec (Stage 2). | ||
|
|
||
| ## Layout | ||
|
|
||
| ``` | ||
| playwright/ | ||
| tests/ # *.spec.ts, one per feature area | ||
| helpers/ # (Stage 1) reusable drivers: page, forms, select, dialog, nav | ||
| mocks/ | ||
| server.mjs # stub backend (runtime source of happy-path data) | ||
| data.ts # typed spec-side builders (mirror server.mjs shapes) | ||
| fixtures/ | ||
| test.ts # import { test, expect } from here in every spec | ||
| tsconfig.json | ||
| ``` | ||
|
|
||
| ## Running | ||
|
|
||
| ```bash | ||
| cd ui | ||
| npm install | ||
| npx playwright install chromium # first time only | ||
| npm run test:pw # headless | ||
| npm run test:pw:ui # interactive UI mode | ||
| npm run test:pw:debug # step-through debugger | ||
| ``` | ||
|
|
||
| Playwright starts the stub + dev server automatically. To drive the app manually | ||
| against the stub: | ||
|
|
||
| ```bash | ||
| node playwright/mocks/server.mjs & | ||
| BACKEND_INTERNAL_URL=http://127.0.0.1:8899/api npm run dev | ||
| # open http://localhost:8001 | ||
| ``` | ||
|
|
||
| ## Conventions | ||
|
|
||
| - Import `{ test, expect }` from `../fixtures/test`, never `@playwright/test`. | ||
| - One spec file per feature area (`tests/agents/`, `tests/chat/`, …); use | ||
| `test.step()` for sub-phases of a multi-step flow. | ||
| - **`data-testid` policy:** prefer `getByRole` / `getByLabel`. Add `data-testid` | ||
| only where role/text is ambiguous or unstable (list rows, per-item action | ||
| buttons, wizard steps, combobox options). Add incrementally — no upfront sweep. | ||
| Do **not** remove/rename the existing `data-test` model-edit hooks; Cypress | ||
| depends on them. | ||
|
|
||
| ## Roadmap | ||
|
|
||
| - **Stage 0 (done):** foundation — config, stub backend, CI, one smoke test. | ||
| - **Stage 1:** helper/driver library (`helpers/*`) + per-test scenario overrides | ||
| via the stub's `/__mock/scenario` endpoint. Prefer stateless scenario selection | ||
| keyed by request content (e.g. `?namespace=empty-ns` → `[]`); fall back to the | ||
| control endpoint with `workers: 1` for endpoints lacking a discriminator. | ||
| - **Stage 2:** feature flows (gap-scoped), ordered by importance — | ||
| Create Agent → Chat/session (A2A SSE mock) → Models → MCP → Onboarding completion. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| // Shared test fixture. Import { test, expect } from here in every spec (not | ||
| // directly from @playwright/test) so all specs get the same setup. | ||
| // | ||
| // For now it resets the stub backend before each test. Reset is a no-op until | ||
| // Stage 1 adds per-test scenarios, but establishing the pattern now means specs | ||
| // don't change when scenarios land. | ||
|
|
||
| import { test as base, expect } from "@playwright/test"; | ||
|
|
||
| const STUB_URL = "http://127.0.0.1:8899"; | ||
|
|
||
| export const test = base.extend({ | ||
| page: async ({ page, request }, use) => { | ||
| try { | ||
| await request.post(`${STUB_URL}/__mock/reset`); | ||
| } catch (err) { | ||
| // Stub should always be reachable in CI (managed via webServer). Fail fast. | ||
| if (process.env.CI) throw err; | ||
| } | ||
| // Bypass the first-run onboarding wizard (AppInitializer redirects to it when | ||
| // this key is unset). Runs before any page script, on every navigation. | ||
| await page.addInitScript(() => { | ||
| window.localStorage.setItem("kagent-onboarding", "true"); | ||
| }); | ||
| await use(page); | ||
| }, | ||
| }); | ||
|
|
||
| export { expect }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With workers: 1 this is a no-op, but raising workers before Stage 1 isolation lands will cause tests to race against the shared stub. Consider setting this to false until then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call — Fixed in 08b3cfc — set fullyParallel: false to match workers: 1, so both stay off until Stage 1 isolation lands. Thanks @mesutoezdil !
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pls write your own sentences when interacting with me, not llm outputs. thx!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
my bad! I will keep it human moving forward 🤝