Skip to content

Commit 0940e59

Browse files
committed
feat: implement Playwright e2e tests infra
1 parent 380119c commit 0940e59

23 files changed

+848
-13
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,14 @@ jobs:
2020
name: Plugins
2121
uses: ./.github/workflows/plugins.yml
2222

23+
e2e:
24+
name: E2E
25+
uses: ./.github/workflows/e2e.yml
26+
2327
all-checks:
2428
name: All Checks Passed
2529
runs-on: ubuntu-22.04
26-
needs: [skit, ui, plugins]
30+
needs: [skit, ui, plugins, e2e]
2731
steps:
2832
- name: All checks passed
2933
run: echo "All CI checks passed successfully!"

.github/workflows/e2e.yml

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# SPDX-FileCopyrightText: © 2025 StreamKit Contributors
2+
#
3+
# SPDX-License-Identifier: MPL-2.0
4+
5+
name: E2E Tests
6+
7+
on:
8+
workflow_call:
9+
10+
env:
11+
CARGO_TERM_COLOR: always
12+
13+
jobs:
14+
e2e:
15+
name: Playwright E2E
16+
runs-on: ubuntu-22.04
17+
steps:
18+
- name: Free disk space
19+
uses: jlumbroso/free-disk-space@main
20+
with:
21+
tool-cache: false
22+
android: true
23+
dotnet: true
24+
haskell: true
25+
large-packages: false
26+
docker-images: true
27+
swap-storage: false
28+
29+
- uses: actions/checkout@v5
30+
31+
- name: Setup Bun
32+
uses: oven-sh/setup-bun@v2
33+
with:
34+
bun-version: "1.3.5"
35+
36+
- name: Build UI
37+
working-directory: ./ui
38+
run: |
39+
bun install --frozen-lockfile
40+
bun run build
41+
42+
- name: Install Rust toolchain
43+
uses: dtolnay/rust-toolchain@master
44+
with:
45+
toolchain: "1.92.0"
46+
47+
- uses: Swatinem/rust-cache@v2
48+
49+
- name: Build skit (debug)
50+
run: cargo build -p streamkit-server --bin skit
51+
52+
- name: Install E2E dependencies
53+
working-directory: ./e2e
54+
run: bun install --frozen-lockfile
55+
56+
- name: Install Playwright browsers
57+
working-directory: ./e2e
58+
run: bunx playwright install chromium --with-deps
59+
60+
- name: Lint E2E (typecheck + prettier)
61+
working-directory: ./e2e
62+
run: bun run lint
63+
64+
- name: Run E2E tests
65+
working-directory: ./e2e
66+
run: bun run test
67+
68+
- name: Upload Playwright report
69+
uses: actions/upload-artifact@v4
70+
if: failure()
71+
with:
72+
name: playwright-report
73+
path: e2e/playwright-report/
74+
retention-days: 7
75+
76+
- name: Upload test artifacts
77+
uses: actions/upload-artifact@v4
78+
if: failure()
79+
with:
80+
name: playwright-results
81+
path: e2e/test-results/
82+
retention-days: 7

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,8 @@ models
6161
/examples/plugins/*/build/
6262

6363
demo/
64+
65+
# E2E test artifacts
66+
e2e/test-results
67+
e2e/playwright-report
68+
e2e/.playwright

e2e/.prettierignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# SPDX-FileCopyrightText: © 2025 StreamKit Contributors
2+
#
3+
# SPDX-License-Identifier: MPL-2.0
4+
5+
node_modules
6+
bun.lock
7+
.playwright
8+
playwright-report
9+
test-results
10+

e2e/README.md

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
<!-- SPDX-FileCopyrightText: © 2025 StreamKit Contributors -->
2+
<!-- SPDX-License-Identifier: MPL-2.0 -->
3+
4+
# StreamKit E2E Tests
5+
6+
End-to-end tests for StreamKit using Playwright.
7+
8+
## Prerequisites
9+
10+
- Bun 1.3.5+
11+
- Rust 1.92.0+ (for building skit)
12+
- Built UI (`cd ui && bun install && bun run build` or `just build-ui`)
13+
- Built skit binary (`cargo build -p streamkit-server --bin skit`)
14+
15+
## Quick Start
16+
17+
```bash
18+
# Install dependencies and Playwright browsers
19+
just install-e2e
20+
just install-playwright
21+
22+
# Run tests (automatically starts server)
23+
just e2e
24+
25+
# Or run directly from e2e directory
26+
cd e2e
27+
bun install
28+
bunx playwright install chromium
29+
bun run test
30+
```
31+
32+
## Running Against External Server
33+
34+
If you already have a StreamKit server running:
35+
36+
```bash
37+
E2E_BASE_URL=http://localhost:4545 bun run test:only
38+
39+
# Or via justfile
40+
just e2e-external http://localhost:4545
41+
```
42+
43+
## Running Against Vite Dev Server
44+
45+
To test against the Vite development server (useful for debugging UI changes):
46+
47+
```bash
48+
# Terminal 1: Start skit backend
49+
cargo run -p streamkit-server --bin skit -- serve
50+
51+
# Terminal 2: Start Vite dev server
52+
cd ui && bun run dev
53+
54+
# Terminal 3: Run E2E tests against Vite
55+
just e2e-external http://localhost:3045
56+
```
57+
58+
The Vite dev server proxies `/api/*` and `/healthz` requests to the skit backend
59+
(default `127.0.0.1:4545`). This is primarily for Playwright’s direct API calls when
60+
`E2E_BASE_URL` points at the Vite server; the UI itself still talks directly to the backend
61+
in development (via `import.meta.env.VITE_API_BASE`).
62+
63+
Both servers must be running for tests to pass.
64+
65+
## Test Structure
66+
67+
- `tests/design.spec.ts` - Design view tests (canvas, samples, YAML editor)
68+
- `tests/monitor.spec.ts` - Monitor view tests (session lifecycle)
69+
70+
## Server Harness
71+
72+
When `E2E_BASE_URL` is not set, the test harness (`src/harness/run.ts`):
73+
74+
1. Finds a free port
75+
2. Starts `target/debug/skit serve` with `SK_SERVER__ADDRESS=127.0.0.1:<port>`
76+
3. Polls `/healthz` until server is ready (30s timeout)
77+
4. Runs all Playwright tests
78+
5. Stops the server
79+
80+
Environment variables set by harness:
81+
82+
- `SK_SERVER__ADDRESS` - Bind address
83+
- `SK_LOG__FILE_ENABLE=false` - Disable file logging
84+
- `RUST_LOG=warn` - Reduce log noise
85+
86+
## Scripts
87+
88+
| Script | Description |
89+
| --------------------- | -------------------------------------------- |
90+
| `bun run test` | Run tests with auto server management |
91+
| `bun run test:only` | Run tests directly (requires `E2E_BASE_URL`) |
92+
| `bun run test:headed` | Run tests with visible browser |
93+
| `bun run test:ui` | Run tests with Playwright UI |
94+
| `bun run report` | Show HTML test report |
95+
96+
## Debugging
97+
98+
```bash
99+
# Run with debug mode (shows server output)
100+
DEBUG=1 bun run test
101+
102+
# Run single test file
103+
bun run test -- tests/design.spec.ts
104+
105+
# Run with trace viewer on failure
106+
bun run test -- --trace on
107+
108+
# Run specific test by name
109+
bun run test -- -g "loads with all main panes"
110+
```
111+
112+
## CI
113+
114+
Tests run automatically in CI via `.github/workflows/e2e.yml`.
115+
On failure, `playwright-report/` and `test-results/` are uploaded as artifacts.
116+
117+
## Adding New Tests
118+
119+
1. Create a new spec file in `tests/` directory
120+
2. Use `data-testid` attributes for stable element selection
121+
3. Prefer role/name selectors for accessible elements
122+
4. Avoid arbitrary waits; use Playwright's built-in assertions
123+
124+
Example:
125+
126+
```typescript
127+
import { test, expect } from '@playwright/test';
128+
129+
test('my new test', async ({ page }) => {
130+
await page.goto('/my-route');
131+
await expect(page.getByTestId('my-element')).toBeVisible();
132+
});
133+
```

e2e/bun.lock

Lines changed: 29 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

e2e/package.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "streamkit-e2e",
3+
"version": "1.0.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": {
7+
"typecheck": "tsc --noEmit",
8+
"format": "../ui/node_modules/.bin/prettier --write . --config ../ui/.prettierrc.json --ignore-path ./.prettierignore",
9+
"format:check": "../ui/node_modules/.bin/prettier --check . --config ../ui/.prettierrc.json --ignore-path ./.prettierignore",
10+
"lint": "bun run typecheck && bun run format:check",
11+
"test": "bun run src/harness/run.ts",
12+
"test:only": "playwright test",
13+
"test:ui": "playwright test --ui",
14+
"test:headed": "bun run src/harness/run.ts -- --headed",
15+
"report": "playwright show-report"
16+
},
17+
"devDependencies": {
18+
"@playwright/test": "^1.49.0",
19+
"@types/node": "^22.0.0",
20+
"typescript": "~5.9.3"
21+
}
22+
}

e2e/playwright.config.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// SPDX-FileCopyrightText: © 2025 StreamKit Contributors
2+
//
3+
// SPDX-License-Identifier: MPL-2.0
4+
5+
import { defineConfig, devices } from '@playwright/test';
6+
7+
// E2E_BASE_URL is set by the harness runner (run.ts) or passed externally
8+
const baseURL = process.env.E2E_BASE_URL;
9+
10+
if (!baseURL) {
11+
throw new Error(
12+
'E2E_BASE_URL environment variable is required. ' +
13+
'Run tests via "bun run test" to auto-start the server, ' +
14+
'or set E2E_BASE_URL manually for an external server.'
15+
);
16+
}
17+
18+
export default defineConfig({
19+
testDir: './tests',
20+
fullyParallel: false, // Run tests serially for shared server
21+
forbidOnly: !!process.env.CI,
22+
retries: process.env.CI ? 2 : 0,
23+
workers: 1, // Single worker for shared server state
24+
reporter: process.env.CI ? [['html'], ['github']] : [['html']],
25+
timeout: 30000,
26+
expect: {
27+
timeout: 10000,
28+
},
29+
30+
use: {
31+
baseURL,
32+
trace: 'on-first-retry',
33+
screenshot: 'only-on-failure',
34+
video: 'retain-on-failure',
35+
},
36+
37+
projects: [
38+
{
39+
name: 'chromium',
40+
use: { ...devices['Desktop Chrome'] },
41+
},
42+
],
43+
});

e2e/src/harness/health.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// SPDX-FileCopyrightText: © 2025 StreamKit Contributors
2+
//
3+
// SPDX-License-Identifier: MPL-2.0
4+
5+
/**
6+
* Wait for the server to become healthy by polling the /healthz endpoint.
7+
*/
8+
export async function waitForHealth(
9+
baseUrl: string,
10+
timeoutMs: number = 30000,
11+
intervalMs: number = 500
12+
): Promise<void> {
13+
const deadline = Date.now() + timeoutMs;
14+
const healthUrl = `${baseUrl}/healthz`;
15+
16+
while (Date.now() < deadline) {
17+
try {
18+
const response = await fetch(healthUrl);
19+
if (response.ok) {
20+
const data = (await response.json()) as { status?: string };
21+
if (data.status === 'ok') {
22+
return;
23+
}
24+
}
25+
} catch {
26+
// Server not ready yet, continue polling
27+
}
28+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
29+
}
30+
31+
throw new Error(`Server health check timed out after ${timeoutMs}ms`);
32+
}

0 commit comments

Comments
 (0)