feat(frontend): add Playwright E2E tests with CI workflow (#2279)
* feat(frontend): add Playwright E2E tests with CI workflow
Add end-to-end testing infrastructure using Playwright (Chromium only).
14 tests across 5 spec files cover landing page, chat workspace,
thread history, sidebar navigation, and agent chat — all with mocked
LangGraph/Backend APIs via network interception (zero backend dependency).
New files:
- playwright.config.ts — Chromium, 30s timeout, auto-start Next.js
- tests/e2e/utils/mock-api.ts — shared API mocks & SSE stream helpers
- tests/e2e/{landing,chat,thread-history,sidebar,agent-chat}.spec.ts
- .github/workflows/e2e-tests.yml — push main + PR trigger, paths filter
Updated: package.json, Makefile, .gitignore, CONTRIBUTING.md,
frontend/CLAUDE.md, frontend/AGENTS.md, frontend/README.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: apply Copilot suggestions
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
@@ -0,0 +1,63 @@
|
|||||||
|
name: E2E Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ 'main' ]
|
||||||
|
paths:
|
||||||
|
- 'frontend/**'
|
||||||
|
- '.github/workflows/e2e-tests.yml'
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
paths:
|
||||||
|
- 'frontend/**'
|
||||||
|
- '.github/workflows/e2e-tests.yml'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: e2e-tests-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
e2e-tests:
|
||||||
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '22'
|
||||||
|
|
||||||
|
- name: Enable Corepack
|
||||||
|
run: corepack enable
|
||||||
|
|
||||||
|
- name: Use pinned pnpm version
|
||||||
|
run: corepack prepare pnpm@10.26.2 --activate
|
||||||
|
|
||||||
|
- name: Install frontend dependencies
|
||||||
|
working-directory: frontend
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Install Playwright Chromium
|
||||||
|
working-directory: frontend
|
||||||
|
run: npx playwright install chromium --with-deps
|
||||||
|
|
||||||
|
- name: Run E2E tests
|
||||||
|
working-directory: frontend
|
||||||
|
run: pnpm exec playwright test
|
||||||
|
env:
|
||||||
|
SKIP_ENV_VALIDATION: '1'
|
||||||
|
|
||||||
|
- name: Upload Playwright report
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
with:
|
||||||
|
name: playwright-report
|
||||||
|
path: frontend/playwright-report/
|
||||||
|
retention-days: 7
|
||||||
@@ -55,5 +55,7 @@ web/
|
|||||||
backend/Dockerfile.langgraph
|
backend/Dockerfile.langgraph
|
||||||
config.yaml.bak
|
config.yaml.bak
|
||||||
.playwright-mcp
|
.playwright-mcp
|
||||||
|
/frontend/test-results/
|
||||||
|
/frontend/playwright-report/
|
||||||
.gstack/
|
.gstack/
|
||||||
.worktrees
|
.worktrees
|
||||||
|
|||||||
+6
-1
@@ -300,9 +300,13 @@ Nginx (port 2026) ← Unified entry point
|
|||||||
cd backend
|
cd backend
|
||||||
make test
|
make test
|
||||||
|
|
||||||
# Frontend tests
|
# Frontend unit tests
|
||||||
cd frontend
|
cd frontend
|
||||||
make test
|
make test
|
||||||
|
|
||||||
|
# Frontend E2E tests (requires Chromium; builds and auto-starts the Next.js production server)
|
||||||
|
cd frontend
|
||||||
|
make test-e2e
|
||||||
```
|
```
|
||||||
|
|
||||||
### PR Regression Checks
|
### PR Regression Checks
|
||||||
@@ -311,6 +315,7 @@ Every pull request triggers the following CI workflows:
|
|||||||
|
|
||||||
- **Backend unit tests** — [.github/workflows/backend-unit-tests.yml](.github/workflows/backend-unit-tests.yml)
|
- **Backend unit tests** — [.github/workflows/backend-unit-tests.yml](.github/workflows/backend-unit-tests.yml)
|
||||||
- **Frontend unit tests** — [.github/workflows/frontend-unit-tests.yml](.github/workflows/frontend-unit-tests.yml)
|
- **Frontend unit tests** — [.github/workflows/frontend-unit-tests.yml](.github/workflows/frontend-unit-tests.yml)
|
||||||
|
- **Frontend E2E tests** — [.github/workflows/e2e-tests.yml](.github/workflows/e2e-tests.yml) (triggered only when `frontend/` files change)
|
||||||
|
|
||||||
## Code Style
|
## Code Style
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -37,6 +37,7 @@ DeerFlow is built on a sophisticated agent-based architecture using the [LangGra
|
|||||||
|
|
||||||
```
|
```
|
||||||
tests/
|
tests/
|
||||||
|
├── e2e/ # E2E tests (Playwright, Chromium, mocked backend)
|
||||||
└── unit/ # Unit tests (mirrors src/ layout, powered by Vitest)
|
└── unit/ # Unit tests (mirrors src/ layout, powered by Vitest)
|
||||||
src/
|
src/
|
||||||
├── app/ # Next.js App Router pages
|
├── app/ # Next.js App Router pages
|
||||||
@@ -98,7 +99,7 @@ When adding new agent features:
|
|||||||
1. Follow the established project structure
|
1. Follow the established project structure
|
||||||
2. Add comprehensive TypeScript types
|
2. Add comprehensive TypeScript types
|
||||||
3. Implement proper error handling
|
3. Implement proper error handling
|
||||||
4. Write unit tests under `tests/unit/` (run with `pnpm test`)
|
4. Write unit tests under `tests/unit/` (run with `pnpm test`) and E2E tests under `tests/e2e/` (run with `pnpm test:e2e`)
|
||||||
5. Update this documentation
|
5. Update this documentation
|
||||||
6. Follow the code style guide (ESLint + Prettier)
|
6. Follow the code style guide (ESLint + Prettier)
|
||||||
|
|
||||||
|
|||||||
@@ -18,11 +18,14 @@ DeerFlow Frontend is a Next.js 16 web interface for an AI agent system. It commu
|
|||||||
| `pnpm lint` | ESLint only |
|
| `pnpm lint` | ESLint only |
|
||||||
| `pnpm lint:fix` | ESLint with auto-fix |
|
| `pnpm lint:fix` | ESLint with auto-fix |
|
||||||
| `pnpm test` | Run unit tests with Vitest |
|
| `pnpm test` | Run unit tests with Vitest |
|
||||||
|
| `pnpm test:e2e` | Run E2E tests with Playwright (Chromium) |
|
||||||
| `pnpm typecheck` | TypeScript type check (`tsc --noEmit`) |
|
| `pnpm typecheck` | TypeScript type check (`tsc --noEmit`) |
|
||||||
| `pnpm start` | Start production server |
|
| `pnpm start` | Start production server |
|
||||||
|
|
||||||
Unit tests live under `tests/unit/` and mirror the `src/` layout (e.g., `tests/unit/core/api/stream-mode.test.ts` tests `src/core/api/stream-mode.ts`). Powered by Vitest; import source modules via the `@/` path alias.
|
Unit tests live under `tests/unit/` and mirror the `src/` layout (e.g., `tests/unit/core/api/stream-mode.test.ts` tests `src/core/api/stream-mode.ts`). Powered by Vitest; import source modules via the `@/` path alias.
|
||||||
|
|
||||||
|
E2E tests live under `tests/e2e/` and use Playwright with Chromium. They mock all backend APIs via `page.route()` network interception and test real page interactions (navigation, chat input, streaming responses). Config: `playwright.config.ts`.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ dev:
|
|||||||
test:
|
test:
|
||||||
pnpm test
|
pnpm test
|
||||||
|
|
||||||
|
test-e2e:
|
||||||
|
pnpm test:e2e
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
pnpm lint
|
pnpm lint
|
||||||
|
|
||||||
|
|||||||
@@ -53,6 +53,12 @@ pnpm lint
|
|||||||
# Run unit tests
|
# Run unit tests
|
||||||
pnpm test
|
pnpm test
|
||||||
|
|
||||||
|
# One-time setup: install Playwright Chromium browser
|
||||||
|
pnpm exec playwright install chromium
|
||||||
|
|
||||||
|
# Run E2E tests (builds and starts production server automatically)
|
||||||
|
pnpm test:e2e
|
||||||
|
|
||||||
# Build for production
|
# Build for production
|
||||||
pnpm build
|
pnpm build
|
||||||
|
|
||||||
@@ -86,6 +92,7 @@ NEXT_PUBLIC_LANGGRAPH_BASE_URL="http://localhost:2024"
|
|||||||
|
|
||||||
```
|
```
|
||||||
tests/
|
tests/
|
||||||
|
├── e2e/ # E2E tests (Playwright, Chromium, mocked backend)
|
||||||
└── unit/ # Unit tests (mirrors src/ layout)
|
└── unit/ # Unit tests (mirrors src/ layout)
|
||||||
src/
|
src/
|
||||||
├── app/ # Next.js App Router pages
|
├── app/ # Next.js App Router pages
|
||||||
@@ -125,6 +132,7 @@ src/
|
|||||||
| `pnpm build` | Build for production |
|
| `pnpm build` | Build for production |
|
||||||
| `pnpm start` | Start production server |
|
| `pnpm start` | Start production server |
|
||||||
| `pnpm test` | Run unit tests with Vitest |
|
| `pnpm test` | Run unit tests with Vitest |
|
||||||
|
| `pnpm test:e2e` | Run E2E tests with Playwright |
|
||||||
| `pnpm format` | Check formatting with Prettier |
|
| `pnpm format` | Check formatting with Prettier |
|
||||||
| `pnpm format:write` | Apply formatting with Prettier |
|
| `pnpm format:write` | Apply formatting with Prettier |
|
||||||
| `pnpm lint` | Run ESLint |
|
| `pnpm lint` | Run ESLint |
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"preview": "next build && next start",
|
"preview": "next build && next start",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
|
"test:e2e": "playwright test",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -93,6 +94,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
|
"@playwright/test": "^1.59.1",
|
||||||
"@tailwindcss/postcss": "^4.0.15",
|
"@tailwindcss/postcss": "^4.0.15",
|
||||||
"@types/gsap": "^3.0.0",
|
"@types/gsap": "^3.0.0",
|
||||||
"@types/node": "^20.14.10",
|
"@types/node": "^20.14.10",
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { defineConfig, devices } from "@playwright/test";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: "./tests/e2e",
|
||||||
|
fullyParallel: true,
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
reporter: process.env.CI ? "github" : "html",
|
||||||
|
timeout: 30_000,
|
||||||
|
|
||||||
|
use: {
|
||||||
|
baseURL: "http://localhost:3000",
|
||||||
|
trace: "on-first-retry",
|
||||||
|
},
|
||||||
|
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: "chromium",
|
||||||
|
use: { ...devices["Desktop Chrome"] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
webServer: {
|
||||||
|
command: "pnpm build && pnpm start",
|
||||||
|
url: "http://localhost:3000",
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
timeout: 120_000,
|
||||||
|
env: {
|
||||||
|
SKIP_ENV_VALIDATION: "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Generated
+51
-12
@@ -115,7 +115,7 @@ importers:
|
|||||||
version: 1.2.1
|
version: 1.2.1
|
||||||
better-auth:
|
better-auth:
|
||||||
specifier: ^1.3
|
specifier: ^1.3
|
||||||
version: 1.4.18(next@16.1.7(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.4(@opentelemetry/api@1.9.0)(@types/node@20.19.33)(vite@7.3.1(@types/node@20.19.33)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3)))(vue@3.5.28(typescript@5.9.3))
|
version: 1.4.18(next@16.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.4(@opentelemetry/api@1.9.0)(@types/node@20.19.33)(vite@7.3.1(@types/node@20.19.33)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3)))(vue@3.5.28(typescript@5.9.3))
|
||||||
canvas-confetti:
|
canvas-confetti:
|
||||||
specifier: ^1.9.4
|
specifier: ^1.9.4
|
||||||
version: 1.9.4
|
version: 1.9.4
|
||||||
@@ -160,16 +160,16 @@ importers:
|
|||||||
version: 5.1.6
|
version: 5.1.6
|
||||||
next:
|
next:
|
||||||
specifier: ^16.1.7
|
specifier: ^16.1.7
|
||||||
version: 16.1.7(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 16.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
next-themes:
|
next-themes:
|
||||||
specifier: ^0.4.6
|
specifier: ^0.4.6
|
||||||
version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
nextra:
|
nextra:
|
||||||
specifier: ^4.6.1
|
specifier: ^4.6.1
|
||||||
version: 4.6.1(next@16.1.7(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)
|
version: 4.6.1(next@16.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)
|
||||||
nextra-theme-docs:
|
nextra-theme-docs:
|
||||||
specifier: ^4.6.1
|
specifier: ^4.6.1
|
||||||
version: 4.6.1(@types/react@19.2.13)(next@16.1.7(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(nextra@4.6.1(next@16.1.7(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))
|
version: 4.6.1(@types/react@19.2.13)(next@16.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(nextra@4.6.1(next@16.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))
|
||||||
nuxt-og-image:
|
nuxt-og-image:
|
||||||
specifier: ^5.1.13
|
specifier: ^5.1.13
|
||||||
version: 5.1.13(@unhead/vue@2.1.4(vue@3.5.28(typescript@5.9.3)))(unstorage@1.17.4)(vite@7.3.1(@types/node@20.19.33)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3))(vue@3.5.28(typescript@5.9.3))
|
version: 5.1.13(@unhead/vue@2.1.4(vue@3.5.28(typescript@5.9.3)))(unstorage@1.17.4)(vite@7.3.1(@types/node@20.19.33)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3))(vue@3.5.28(typescript@5.9.3))
|
||||||
@@ -228,6 +228,9 @@ importers:
|
|||||||
'@eslint/eslintrc':
|
'@eslint/eslintrc':
|
||||||
specifier: ^3.3.1
|
specifier: ^3.3.1
|
||||||
version: 3.3.3
|
version: 3.3.3
|
||||||
|
'@playwright/test':
|
||||||
|
specifier: ^1.59.1
|
||||||
|
version: 1.59.1
|
||||||
'@tailwindcss/postcss':
|
'@tailwindcss/postcss':
|
||||||
specifier: ^4.0.15
|
specifier: ^4.0.15
|
||||||
version: 4.1.18
|
version: 4.1.18
|
||||||
@@ -1156,6 +1159,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
|
resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
|
||||||
engines: {node: '>=8.0.0'}
|
engines: {node: '>=8.0.0'}
|
||||||
|
|
||||||
|
'@playwright/test@1.59.1':
|
||||||
|
resolution: {integrity: sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
'@polka/url@1.0.0-next.29':
|
'@polka/url@1.0.0-next.29':
|
||||||
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
|
||||||
|
|
||||||
@@ -3632,6 +3640,11 @@ packages:
|
|||||||
react-dom:
|
react-dom:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
fsevents@2.3.2:
|
||||||
|
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
||||||
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
@@ -4794,6 +4807,16 @@ packages:
|
|||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
playwright-core@1.59.1:
|
||||||
|
resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
playwright@1.59.1:
|
||||||
|
resolution: {integrity: sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
points-on-curve@0.2.0:
|
points-on-curve@0.2.0:
|
||||||
resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==}
|
resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==}
|
||||||
|
|
||||||
@@ -6847,6 +6870,10 @@ snapshots:
|
|||||||
|
|
||||||
'@opentelemetry/api@1.9.0': {}
|
'@opentelemetry/api@1.9.0': {}
|
||||||
|
|
||||||
|
'@playwright/test@1.59.1':
|
||||||
|
dependencies:
|
||||||
|
playwright: 1.59.1
|
||||||
|
|
||||||
'@polka/url@1.0.0-next.29': {}
|
'@polka/url@1.0.0-next.29': {}
|
||||||
|
|
||||||
'@radix-ui/number@1.1.1': {}
|
'@radix-ui/number@1.1.1': {}
|
||||||
@@ -8415,7 +8442,7 @@ snapshots:
|
|||||||
|
|
||||||
best-effort-json-parser@1.2.1: {}
|
best-effort-json-parser@1.2.1: {}
|
||||||
|
|
||||||
better-auth@1.4.18(next@16.1.7(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.4(@opentelemetry/api@1.9.0)(@types/node@20.19.33)(vite@7.3.1(@types/node@20.19.33)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3)))(vue@3.5.28(typescript@5.9.3)):
|
better-auth@1.4.18(next@16.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(vitest@4.1.4(@opentelemetry/api@1.9.0)(@types/node@20.19.33)(vite@7.3.1(@types/node@20.19.33)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3)))(vue@3.5.28(typescript@5.9.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@better-auth/core': 1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)
|
'@better-auth/core': 1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)
|
||||||
'@better-auth/telemetry': 1.4.18(@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))
|
'@better-auth/telemetry': 1.4.18(@better-auth/core@1.4.18(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@3.25.76))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0))
|
||||||
@@ -8430,7 +8457,7 @@ snapshots:
|
|||||||
nanostores: 1.1.0
|
nanostores: 1.1.0
|
||||||
zod: 4.3.6
|
zod: 4.3.6
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
next: 16.1.7(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
next: 16.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
react: 19.2.4
|
react: 19.2.4
|
||||||
react-dom: 19.2.4(react@19.2.4)
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
vitest: 4.1.4(@opentelemetry/api@1.9.0)(@types/node@20.19.33)(vite@7.3.1(@types/node@20.19.33)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3))
|
vitest: 4.1.4(@opentelemetry/api@1.9.0)(@types/node@20.19.33)(vite@7.3.1(@types/node@20.19.33)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.3))
|
||||||
@@ -9475,6 +9502,9 @@ snapshots:
|
|||||||
react: 19.2.4
|
react: 19.2.4
|
||||||
react-dom: 19.2.4(react@19.2.4)
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
|
|
||||||
|
fsevents@2.3.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -10724,7 +10754,7 @@ snapshots:
|
|||||||
react: 19.2.4
|
react: 19.2.4
|
||||||
react-dom: 19.2.4(react@19.2.4)
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
|
|
||||||
next@16.1.7(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
next@16.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@next/env': 16.1.7
|
'@next/env': 16.1.7
|
||||||
'@swc/helpers': 0.5.15
|
'@swc/helpers': 0.5.15
|
||||||
@@ -10744,18 +10774,19 @@ snapshots:
|
|||||||
'@next/swc-win32-arm64-msvc': 16.1.7
|
'@next/swc-win32-arm64-msvc': 16.1.7
|
||||||
'@next/swc-win32-x64-msvc': 16.1.7
|
'@next/swc-win32-x64-msvc': 16.1.7
|
||||||
'@opentelemetry/api': 1.9.0
|
'@opentelemetry/api': 1.9.0
|
||||||
|
'@playwright/test': 1.59.1
|
||||||
sharp: 0.34.5
|
sharp: 0.34.5
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
- babel-plugin-macros
|
- babel-plugin-macros
|
||||||
|
|
||||||
nextra-theme-docs@4.6.1(@types/react@19.2.13)(next@16.1.7(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(nextra@4.6.1(next@16.1.7(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)):
|
nextra-theme-docs@4.6.1(@types/react@19.2.13)(next@16.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(nextra@4.6.1(next@16.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@headlessui/react': 2.2.9(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
'@headlessui/react': 2.2.9(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
clsx: 2.1.1
|
clsx: 2.1.1
|
||||||
next: 16.1.7(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
next: 16.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
next-themes: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
next-themes: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
nextra: 4.6.1(next@16.1.7(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)
|
nextra: 4.6.1(next@16.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3)
|
||||||
react: 19.2.4
|
react: 19.2.4
|
||||||
react-compiler-runtime: 19.1.0-rc.3(react@19.2.4)
|
react-compiler-runtime: 19.1.0-rc.3(react@19.2.4)
|
||||||
react-dom: 19.2.4(react@19.2.4)
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
@@ -10767,7 +10798,7 @@ snapshots:
|
|||||||
- immer
|
- immer
|
||||||
- use-sync-external-store
|
- use-sync-external-store
|
||||||
|
|
||||||
nextra@4.6.1(next@16.1.7(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3):
|
nextra@4.6.1(next@16.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@formatjs/intl-localematcher': 0.6.2
|
'@formatjs/intl-localematcher': 0.6.2
|
||||||
'@headlessui/react': 2.2.9(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
'@headlessui/react': 2.2.9(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
@@ -10788,7 +10819,7 @@ snapshots:
|
|||||||
mdast-util-gfm: 3.1.0
|
mdast-util-gfm: 3.1.0
|
||||||
mdast-util-to-hast: 13.2.1
|
mdast-util-to-hast: 13.2.1
|
||||||
negotiator: 1.0.0
|
negotiator: 1.0.0
|
||||||
next: 16.1.7(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
next: 16.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||||
react: 19.2.4
|
react: 19.2.4
|
||||||
react-compiler-runtime: 19.1.0-rc.3(react@19.2.4)
|
react-compiler-runtime: 19.1.0-rc.3(react@19.2.4)
|
||||||
react-dom: 19.2.4(react@19.2.4)
|
react-dom: 19.2.4(react@19.2.4)
|
||||||
@@ -11096,6 +11127,14 @@ snapshots:
|
|||||||
|
|
||||||
playwright-core@1.58.2: {}
|
playwright-core@1.58.2: {}
|
||||||
|
|
||||||
|
playwright-core@1.59.1: {}
|
||||||
|
|
||||||
|
playwright@1.59.1:
|
||||||
|
dependencies:
|
||||||
|
playwright-core: 1.59.1
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents: 2.3.2
|
||||||
|
|
||||||
points-on-curve@0.2.0: {}
|
points-on-curve@0.2.0: {}
|
||||||
|
|
||||||
points-on-path@0.2.1:
|
points-on-path@0.2.1:
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
import { mockLangGraphAPI } from "./utils/mock-api";
|
||||||
|
|
||||||
|
const MOCK_AGENTS = [
|
||||||
|
{
|
||||||
|
name: "test-agent",
|
||||||
|
description: "A test agent for E2E tests",
|
||||||
|
system_prompt: "You are a test agent.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
test.describe("Agent chat", () => {
|
||||||
|
test("agent gallery page loads and shows agents", async ({ page }) => {
|
||||||
|
mockLangGraphAPI(page, { agents: MOCK_AGENTS });
|
||||||
|
|
||||||
|
await page.goto("/workspace/agents");
|
||||||
|
|
||||||
|
// The agent card should appear with the agent name
|
||||||
|
await expect(page.getByText("test-agent")).toBeVisible({
|
||||||
|
timeout: 15_000,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("agent chat page loads with input box", async ({ page }) => {
|
||||||
|
mockLangGraphAPI(page, { agents: MOCK_AGENTS });
|
||||||
|
|
||||||
|
await page.goto("/workspace/agents/test-agent/chats/new");
|
||||||
|
|
||||||
|
// The prompt input textarea should be visible
|
||||||
|
const textarea = page.getByPlaceholder(/how can i assist you/i);
|
||||||
|
await expect(textarea).toBeVisible({ timeout: 15_000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("agent chat page shows agent badge", async ({ page }) => {
|
||||||
|
mockLangGraphAPI(page, { agents: MOCK_AGENTS });
|
||||||
|
|
||||||
|
await page.goto("/workspace/agents/test-agent/chats/new");
|
||||||
|
|
||||||
|
// The agent badge should display in the header (scoped to header to avoid
|
||||||
|
// matching the welcome area which also shows the agent name)
|
||||||
|
await expect(
|
||||||
|
page.locator("header span", { hasText: "test-agent" }),
|
||||||
|
).toBeVisible({ timeout: 15_000 });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
import { handleRunStream, mockLangGraphAPI } from "./utils/mock-api";
|
||||||
|
|
||||||
|
test.describe("Chat workspace", () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
mockLangGraphAPI(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("new chat page loads with input box", async ({ page }) => {
|
||||||
|
await page.goto("/workspace/chats/new");
|
||||||
|
|
||||||
|
const textarea = page.getByPlaceholder(/how can i assist you/i);
|
||||||
|
await expect(textarea).toBeVisible({ timeout: 15_000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can type a message in the input box", async ({ page }) => {
|
||||||
|
await page.goto("/workspace/chats/new");
|
||||||
|
|
||||||
|
const textarea = page.getByPlaceholder(/how can i assist you/i);
|
||||||
|
await expect(textarea).toBeVisible({ timeout: 15_000 });
|
||||||
|
|
||||||
|
await textarea.fill("Hello, DeerFlow!");
|
||||||
|
await expect(textarea).toHaveValue("Hello, DeerFlow!");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("sending a message triggers API call and shows response", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
let streamCalled = false;
|
||||||
|
await page.route("**/runs/stream", (route) => {
|
||||||
|
streamCalled = true;
|
||||||
|
return handleRunStream(route);
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto("/workspace/chats/new");
|
||||||
|
|
||||||
|
const textarea = page.getByPlaceholder(/how can i assist you/i);
|
||||||
|
await expect(textarea).toBeVisible({ timeout: 15_000 });
|
||||||
|
|
||||||
|
await textarea.fill("Hello");
|
||||||
|
await textarea.press("Enter");
|
||||||
|
|
||||||
|
await expect.poll(() => streamCalled, { timeout: 10_000 }).toBeTruthy();
|
||||||
|
|
||||||
|
// The AI response should appear in the chat
|
||||||
|
await expect(page.getByText("Hello from DeerFlow!")).toBeVisible({
|
||||||
|
timeout: 10_000,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
import { mockLangGraphAPI } from "./utils/mock-api";
|
||||||
|
|
||||||
|
test.describe("Landing page", () => {
|
||||||
|
test("renders the header and hero section", async ({ page }) => {
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
// Header brand name
|
||||||
|
await expect(
|
||||||
|
page.locator("header h1", { hasText: "DeerFlow" }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
// "Get Started" call-to-action button in hero
|
||||||
|
await expect(
|
||||||
|
page.getByRole("link", { name: /get started/i }),
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Get Started link navigates to workspace", async ({ page }) => {
|
||||||
|
mockLangGraphAPI(page);
|
||||||
|
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
const getStarted = page.getByRole("link", { name: /get started/i });
|
||||||
|
await getStarted.click();
|
||||||
|
|
||||||
|
// Should redirect to /workspace/chats/new
|
||||||
|
await page.waitForURL("**/workspace/chats/new");
|
||||||
|
await expect(page).toHaveURL(/\/workspace\/chats\/new/);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
import { mockLangGraphAPI } from "./utils/mock-api";
|
||||||
|
|
||||||
|
test.describe("Sidebar navigation", () => {
|
||||||
|
test("sidebar contains Chats and Agents nav links", async ({ page }) => {
|
||||||
|
mockLangGraphAPI(page);
|
||||||
|
|
||||||
|
await page.goto("/workspace/chats/new");
|
||||||
|
|
||||||
|
// Sidebar uses data-sidebar="menu-button" with asChild rendering on <Link>
|
||||||
|
const sidebar = page.locator("[data-sidebar='sidebar']");
|
||||||
|
await expect(sidebar.locator("a[href='/workspace/chats']")).toBeVisible({
|
||||||
|
timeout: 15_000,
|
||||||
|
});
|
||||||
|
await expect(sidebar.locator("a[href='/workspace/agents']")).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Agents link navigates to agents page", async ({ page }) => {
|
||||||
|
mockLangGraphAPI(page);
|
||||||
|
|
||||||
|
await page.goto("/workspace/chats/new");
|
||||||
|
|
||||||
|
const sidebar = page.locator("[data-sidebar='sidebar']");
|
||||||
|
const agentsLink = sidebar.locator("a[href='/workspace/agents']");
|
||||||
|
await expect(agentsLink).toBeVisible({ timeout: 15_000 });
|
||||||
|
await agentsLink.click();
|
||||||
|
|
||||||
|
await page.waitForURL("**/workspace/agents");
|
||||||
|
await expect(page).toHaveURL(/\/workspace\/agents/);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
import {
|
||||||
|
mockLangGraphAPI,
|
||||||
|
MOCK_THREAD_ID,
|
||||||
|
MOCK_THREAD_ID_2,
|
||||||
|
} from "./utils/mock-api";
|
||||||
|
|
||||||
|
const THREADS = [
|
||||||
|
{
|
||||||
|
thread_id: MOCK_THREAD_ID,
|
||||||
|
title: "First conversation",
|
||||||
|
updated_at: "2025-06-01T12:00:00Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
thread_id: MOCK_THREAD_ID_2,
|
||||||
|
title: "Second conversation",
|
||||||
|
updated_at: "2025-06-02T12:00:00Z",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
test.describe("Thread history", () => {
|
||||||
|
test("sidebar shows existing threads", async ({ page }) => {
|
||||||
|
mockLangGraphAPI(page, { threads: THREADS });
|
||||||
|
|
||||||
|
await page.goto("/workspace/chats/new");
|
||||||
|
|
||||||
|
// Both thread titles should appear in the sidebar
|
||||||
|
await expect(page.getByText("First conversation")).toBeVisible({
|
||||||
|
timeout: 15_000,
|
||||||
|
});
|
||||||
|
await expect(page.getByText("Second conversation")).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("clicking a thread in sidebar navigates to it", async ({ page }) => {
|
||||||
|
mockLangGraphAPI(page, { threads: THREADS });
|
||||||
|
|
||||||
|
await page.goto("/workspace/chats/new");
|
||||||
|
|
||||||
|
// Wait for sidebar to populate
|
||||||
|
const firstThread = page.getByText("First conversation");
|
||||||
|
await expect(firstThread).toBeVisible({ timeout: 15_000 });
|
||||||
|
|
||||||
|
// Click on the first thread
|
||||||
|
await firstThread.click();
|
||||||
|
|
||||||
|
// Should navigate to that thread's URL
|
||||||
|
await page.waitForURL(`**/workspace/chats/${MOCK_THREAD_ID}`);
|
||||||
|
await expect(page).toHaveURL(new RegExp(MOCK_THREAD_ID));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("existing thread loads historical messages", async ({ page }) => {
|
||||||
|
mockLangGraphAPI(page, { threads: THREADS });
|
||||||
|
|
||||||
|
// Navigate directly to an existing thread
|
||||||
|
await page.goto(`/workspace/chats/${MOCK_THREAD_ID}`);
|
||||||
|
|
||||||
|
// The historical AI response should be displayed
|
||||||
|
await expect(
|
||||||
|
page.getByText("Response in thread First conversation"),
|
||||||
|
).toBeVisible({ timeout: 15_000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test("chats list page shows all threads", async ({ page }) => {
|
||||||
|
mockLangGraphAPI(page, { threads: THREADS });
|
||||||
|
|
||||||
|
await page.goto("/workspace/chats");
|
||||||
|
|
||||||
|
// Both threads should be listed in the main content area
|
||||||
|
const main = page.locator("main");
|
||||||
|
await expect(main.getByText("First conversation")).toBeVisible({
|
||||||
|
timeout: 15_000,
|
||||||
|
});
|
||||||
|
await expect(main.getByText("Second conversation")).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
/**
|
||||||
|
* Shared mock helpers for E2E tests.
|
||||||
|
*
|
||||||
|
* Intercepts all LangGraph / Backend API endpoints so tests can run without
|
||||||
|
* a real backend. Each test file imports `mockLangGraphAPI` and
|
||||||
|
* `handleRunStream` from here.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Page, Route } from "@playwright/test";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Constants — deterministic IDs used across tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export const MOCK_THREAD_ID = "00000000-0000-0000-0000-000000000001";
|
||||||
|
export const MOCK_THREAD_ID_2 = "00000000-0000-0000-0000-000000000002";
|
||||||
|
export const MOCK_RUN_ID = "00000000-0000-0000-0000-000000000099";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Types
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export type MockThread = {
|
||||||
|
thread_id: string;
|
||||||
|
title?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
agent_name?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MockAgent = {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
system_prompt?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MockAPIOptions = {
|
||||||
|
threads?: MockThread[];
|
||||||
|
agents?: MockAgent[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// mockLangGraphAPI
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock all LangGraph API endpoints that the frontend calls on page load and
|
||||||
|
* during message sending. Without these mocks the pages would hang waiting
|
||||||
|
* for a real backend.
|
||||||
|
*/
|
||||||
|
export function mockLangGraphAPI(page: Page, options?: MockAPIOptions) {
|
||||||
|
const threads = options?.threads ?? [];
|
||||||
|
const agents = options?.agents ?? [];
|
||||||
|
|
||||||
|
// Thread search — sidebar thread list & chats list page
|
||||||
|
void page.route("**/api/langgraph/threads/search", (route) => {
|
||||||
|
const body = threads.map((t) => ({
|
||||||
|
thread_id: t.thread_id,
|
||||||
|
created_at: "2025-01-01T00:00:00Z",
|
||||||
|
updated_at: t.updated_at ?? "2025-01-01T00:00:00Z",
|
||||||
|
metadata: t.agent_name ? { agent_name: t.agent_name } : {},
|
||||||
|
status: "idle",
|
||||||
|
values: { title: t.title ?? "Untitled" },
|
||||||
|
}));
|
||||||
|
return route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Thread create — called when user sends first message in a new chat
|
||||||
|
void page.route("**/api/langgraph/threads", (route) => {
|
||||||
|
if (route.request().method() === "POST") {
|
||||||
|
return route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({
|
||||||
|
thread_id: MOCK_THREAD_ID,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
metadata: {},
|
||||||
|
status: "idle",
|
||||||
|
values: {},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return route.fallback();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Thread update (PATCH) — metadata update after creation
|
||||||
|
void page.route("**/api/langgraph/threads/*", (route) => {
|
||||||
|
if (route.request().method() === "PATCH") {
|
||||||
|
return route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({ thread_id: MOCK_THREAD_ID }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return route.fallback();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Thread history — useStream fetches state history on mount
|
||||||
|
void page.route("**/api/langgraph/threads/*/history", (route) => {
|
||||||
|
const url = route.request().url();
|
||||||
|
|
||||||
|
// For threads that exist in our mock data, return history with messages
|
||||||
|
const matchingThread = threads.find((t) => url.includes(t.thread_id));
|
||||||
|
if (matchingThread) {
|
||||||
|
return route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify([
|
||||||
|
{
|
||||||
|
values: {
|
||||||
|
title: matchingThread.title ?? "Untitled",
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
type: "human",
|
||||||
|
id: `msg-human-${matchingThread.thread_id}`,
|
||||||
|
content: [{ type: "text", text: "Previous question" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "ai",
|
||||||
|
id: `msg-ai-${matchingThread.thread_id}`,
|
||||||
|
content: `Response in thread ${matchingThread.title ?? matchingThread.thread_id}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
next: [],
|
||||||
|
metadata: {},
|
||||||
|
created_at: "2025-01-01T00:00:00Z",
|
||||||
|
parent_config: null,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// New threads — empty history
|
||||||
|
return route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: "[]",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Thread state — getState for individual thread
|
||||||
|
void page.route("**/api/langgraph/threads/*/state", (route) => {
|
||||||
|
if (route.request().method() === "GET") {
|
||||||
|
const url = route.request().url();
|
||||||
|
const matchingThread = threads.find((t) => url.includes(t.thread_id));
|
||||||
|
return route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({
|
||||||
|
values: {
|
||||||
|
title: matchingThread?.title ?? "Untitled",
|
||||||
|
messages: matchingThread
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
type: "human",
|
||||||
|
id: `msg-human-${matchingThread.thread_id}`,
|
||||||
|
content: [{ type: "text", text: "Previous question" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "ai",
|
||||||
|
id: `msg-ai-${matchingThread.thread_id}`,
|
||||||
|
content: `Response in thread ${matchingThread.title ?? matchingThread.thread_id}`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
},
|
||||||
|
next: [],
|
||||||
|
metadata: {},
|
||||||
|
created_at: "2025-01-01T00:00:00Z",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return route.fallback();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Run stream — returns a minimal SSE response with an AI message
|
||||||
|
void page.route("**/api/langgraph/runs/stream", handleRunStream);
|
||||||
|
void page.route("**/api/langgraph/threads/*/runs/stream", handleRunStream);
|
||||||
|
|
||||||
|
// Agents list — sidebar & gallery page
|
||||||
|
void page.route("**/api/agents", (route) => {
|
||||||
|
if (route.request().method() === "GET") {
|
||||||
|
return route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({ agents }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return route.fallback();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Individual agent — agent chat page
|
||||||
|
void page.route("**/api/agents/*", (route) => {
|
||||||
|
if (route.request().method() === "GET") {
|
||||||
|
const url = route.request().url();
|
||||||
|
const agent = agents.find((a) => url.endsWith(`/api/agents/${a.name}`));
|
||||||
|
if (agent) {
|
||||||
|
return route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify(agent),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return route.fulfill({
|
||||||
|
status: 404,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({ detail: "Agent not found" }),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// handleRunStream
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a minimal SSE stream that the LangGraph SDK can parse.
|
||||||
|
* The stream returns a single AI message: "Hello from DeerFlow!".
|
||||||
|
*/
|
||||||
|
export function handleRunStream(route: Route) {
|
||||||
|
const events = [
|
||||||
|
{
|
||||||
|
event: "metadata",
|
||||||
|
data: { run_id: MOCK_RUN_ID, thread_id: MOCK_THREAD_ID },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
event: "values",
|
||||||
|
data: {
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
type: "human",
|
||||||
|
id: "msg-human-1",
|
||||||
|
content: [{ type: "text", text: "Hello" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "ai",
|
||||||
|
id: "msg-ai-1",
|
||||||
|
content: "Hello from DeerFlow!",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ event: "end", data: {} },
|
||||||
|
];
|
||||||
|
|
||||||
|
const body = events
|
||||||
|
.map((e) => `event: ${e.event}\ndata: ${JSON.stringify(e.data)}\n\n`)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
return route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "text/event-stream",
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user