feat: add Playwright e2e tests for onboarding wizard flow
Scaffolds end-to-end testing with Playwright for the onboarding wizard. Runs in skip_llm mode by default (UI-only, no LLM costs). Set PAPERCLIP_E2E_SKIP_LLM=false for full heartbeat verification. - tests/e2e/playwright.config.ts: Playwright config with webServer - tests/e2e/onboarding.spec.ts: 4-step wizard flow test - .github/workflows/e2e.yml: manual workflow_dispatch CI workflow - package.json: test:e2e and test:e2e:headed scripts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
44
.github/workflows/e2e.yml
vendored
Normal file
44
.github/workflows/e2e.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
name: E2E Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
skip_llm:
|
||||||
|
description: "Skip LLM-dependent assertions (default: true)"
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
e2e:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
env:
|
||||||
|
PAPERCLIP_E2E_SKIP_LLM: ${{ inputs.skip_llm && 'true' || 'false' }}
|
||||||
|
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 9
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
|
- run: pnpm install --frozen-lockfile
|
||||||
|
- run: pnpm build
|
||||||
|
- run: npx playwright install --with-deps chromium
|
||||||
|
|
||||||
|
- name: Run e2e tests
|
||||||
|
run: pnpm run test:e2e
|
||||||
|
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: playwright-report
|
||||||
|
path: |
|
||||||
|
tests/e2e/playwright-report/
|
||||||
|
tests/e2e/test-results/
|
||||||
|
retention-days: 14
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -36,4 +36,8 @@ tmp/
|
|||||||
*.tmp
|
*.tmp
|
||||||
.vscode/
|
.vscode/
|
||||||
.claude/settings.local.json
|
.claude/settings.local.json
|
||||||
.paperclip-local/
|
.paperclip-local/
|
||||||
|
|
||||||
|
# Playwright
|
||||||
|
tests/e2e/test-results/
|
||||||
|
tests/e2e/playwright-report/
|
||||||
@@ -25,10 +25,13 @@
|
|||||||
"docs:dev": "cd docs && npx mintlify dev",
|
"docs:dev": "cd docs && npx mintlify dev",
|
||||||
"smoke:openclaw-join": "./scripts/smoke/openclaw-join.sh",
|
"smoke:openclaw-join": "./scripts/smoke/openclaw-join.sh",
|
||||||
"smoke:openclaw-docker-ui": "./scripts/smoke/openclaw-docker-ui.sh",
|
"smoke:openclaw-docker-ui": "./scripts/smoke/openclaw-docker-ui.sh",
|
||||||
"smoke:openclaw-sse-standalone": "./scripts/smoke/openclaw-sse-standalone.sh"
|
"smoke:openclaw-sse-standalone": "./scripts/smoke/openclaw-sse-standalone.sh",
|
||||||
|
"test:e2e": "npx playwright test --config tests/e2e/playwright.config.ts",
|
||||||
|
"test:e2e:headed": "npx playwright test --config tests/e2e/playwright.config.ts --headed"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@changesets/cli": "^2.30.0",
|
"@changesets/cli": "^2.30.0",
|
||||||
|
"@playwright/test": "^1.58.2",
|
||||||
"esbuild": "^0.27.3",
|
"esbuild": "^0.27.3",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
"vitest": "^3.0.5"
|
"vitest": "^3.0.5"
|
||||||
|
|||||||
47
pnpm-lock.yaml
generated
47
pnpm-lock.yaml
generated
@@ -11,6 +11,9 @@ importers:
|
|||||||
'@changesets/cli':
|
'@changesets/cli':
|
||||||
specifier: ^2.30.0
|
specifier: ^2.30.0
|
||||||
version: 2.30.0(@types/node@25.2.3)
|
version: 2.30.0(@types/node@25.2.3)
|
||||||
|
'@playwright/test':
|
||||||
|
specifier: ^1.58.2
|
||||||
|
version: 1.58.2
|
||||||
esbuild:
|
esbuild:
|
||||||
specifier: ^0.27.3
|
specifier: ^0.27.3
|
||||||
version: 0.27.3
|
version: 0.27.3
|
||||||
@@ -35,9 +38,6 @@ importers:
|
|||||||
'@paperclipai/adapter-cursor-local':
|
'@paperclipai/adapter-cursor-local':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../packages/adapters/cursor-local
|
version: link:../packages/adapters/cursor-local
|
||||||
'@paperclipai/adapter-openclaw':
|
|
||||||
specifier: workspace:*
|
|
||||||
version: link:../packages/adapters/openclaw
|
|
||||||
'@paperclipai/adapter-openclaw-gateway':
|
'@paperclipai/adapter-openclaw-gateway':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../packages/adapters/openclaw-gateway
|
version: link:../packages/adapters/openclaw-gateway
|
||||||
@@ -261,9 +261,6 @@ importers:
|
|||||||
'@paperclipai/adapter-cursor-local':
|
'@paperclipai/adapter-cursor-local':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../packages/adapters/cursor-local
|
version: link:../packages/adapters/cursor-local
|
||||||
'@paperclipai/adapter-openclaw':
|
|
||||||
specifier: workspace:*
|
|
||||||
version: link:../packages/adapters/openclaw
|
|
||||||
'@paperclipai/adapter-openclaw-gateway':
|
'@paperclipai/adapter-openclaw-gateway':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../packages/adapters/openclaw-gateway
|
version: link:../packages/adapters/openclaw-gateway
|
||||||
@@ -379,9 +376,6 @@ importers:
|
|||||||
'@paperclipai/adapter-cursor-local':
|
'@paperclipai/adapter-cursor-local':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../packages/adapters/cursor-local
|
version: link:../packages/adapters/cursor-local
|
||||||
'@paperclipai/adapter-openclaw':
|
|
||||||
specifier: workspace:*
|
|
||||||
version: link:../packages/adapters/openclaw
|
|
||||||
'@paperclipai/adapter-openclaw-gateway':
|
'@paperclipai/adapter-openclaw-gateway':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../packages/adapters/openclaw-gateway
|
version: link:../packages/adapters/openclaw-gateway
|
||||||
@@ -1696,6 +1690,11 @@ packages:
|
|||||||
'@pinojs/redact@0.4.0':
|
'@pinojs/redact@0.4.0':
|
||||||
resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
|
resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
|
||||||
|
|
||||||
|
'@playwright/test@1.58.2':
|
||||||
|
resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
'@radix-ui/colors@3.0.0':
|
'@radix-ui/colors@3.0.0':
|
||||||
resolution: {integrity: sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==}
|
resolution: {integrity: sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==}
|
||||||
|
|
||||||
@@ -4012,6 +4011,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==}
|
resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==}
|
||||||
engines: {node: '>=6 <7 || >=8'}
|
engines: {node: '>=6 <7 || >=8'}
|
||||||
|
|
||||||
|
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}
|
||||||
@@ -4798,6 +4802,16 @@ packages:
|
|||||||
pkg-types@1.3.1:
|
pkg-types@1.3.1:
|
||||||
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
|
resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==}
|
||||||
|
|
||||||
|
playwright-core@1.58.2:
|
||||||
|
resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
playwright@1.58.2:
|
||||||
|
resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==}
|
||||||
|
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==}
|
||||||
|
|
||||||
@@ -7394,6 +7408,10 @@ snapshots:
|
|||||||
|
|
||||||
'@pinojs/redact@0.4.0': {}
|
'@pinojs/redact@0.4.0': {}
|
||||||
|
|
||||||
|
'@playwright/test@1.58.2':
|
||||||
|
dependencies:
|
||||||
|
playwright: 1.58.2
|
||||||
|
|
||||||
'@radix-ui/colors@3.0.0': {}
|
'@radix-ui/colors@3.0.0': {}
|
||||||
|
|
||||||
'@radix-ui/number@1.1.1': {}
|
'@radix-ui/number@1.1.1': {}
|
||||||
@@ -9872,6 +9890,9 @@ snapshots:
|
|||||||
jsonfile: 4.0.0
|
jsonfile: 4.0.0
|
||||||
universalify: 0.1.2
|
universalify: 0.1.2
|
||||||
|
|
||||||
|
fsevents@2.3.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -10923,6 +10944,14 @@ snapshots:
|
|||||||
mlly: 1.8.1
|
mlly: 1.8.1
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
|
|
||||||
|
playwright-core@1.58.2: {}
|
||||||
|
|
||||||
|
playwright@1.58.2:
|
||||||
|
dependencies:
|
||||||
|
playwright-core: 1.58.2
|
||||||
|
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:
|
||||||
|
|||||||
172
tests/e2e/onboarding.spec.ts
Normal file
172
tests/e2e/onboarding.spec.ts
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* E2E: Onboarding wizard flow (skip_llm mode).
|
||||||
|
*
|
||||||
|
* Walks through the 4-step OnboardingWizard:
|
||||||
|
* Step 1 — Name your company
|
||||||
|
* Step 2 — Create your first agent (adapter selection + config)
|
||||||
|
* Step 3 — Give it something to do (task creation)
|
||||||
|
* Step 4 — Ready to launch (summary + open issue)
|
||||||
|
*
|
||||||
|
* By default this runs in skip_llm mode: we do NOT assert that an LLM
|
||||||
|
* heartbeat fires. Set PAPERCLIP_E2E_SKIP_LLM=false to enable LLM-dependent
|
||||||
|
* assertions (requires a valid ANTHROPIC_API_KEY).
|
||||||
|
*/
|
||||||
|
|
||||||
|
const SKIP_LLM = process.env.PAPERCLIP_E2E_SKIP_LLM !== "false";
|
||||||
|
|
||||||
|
const COMPANY_NAME = `E2E-Test-${Date.now()}`;
|
||||||
|
const AGENT_NAME = "CEO";
|
||||||
|
const TASK_TITLE = "E2E test task";
|
||||||
|
|
||||||
|
test.describe("Onboarding wizard", () => {
|
||||||
|
test("completes full wizard flow", async ({ page }) => {
|
||||||
|
// Navigate to root — should auto-open onboarding when no companies exist
|
||||||
|
await page.goto("/");
|
||||||
|
|
||||||
|
// If the wizard didn't auto-open (company already exists), click the button
|
||||||
|
const wizardHeading = page.locator("h3", { hasText: "Name your company" });
|
||||||
|
const newCompanyBtn = page.getByRole("button", { name: "New Company" });
|
||||||
|
|
||||||
|
// Wait for either the wizard or the start page
|
||||||
|
await expect(
|
||||||
|
wizardHeading.or(newCompanyBtn)
|
||||||
|
).toBeVisible({ timeout: 15_000 });
|
||||||
|
|
||||||
|
if (await newCompanyBtn.isVisible()) {
|
||||||
|
await newCompanyBtn.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
// Step 1: Name your company
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
await expect(wizardHeading).toBeVisible({ timeout: 5_000 });
|
||||||
|
await expect(page.locator("text=Step 1 of 4")).toBeVisible();
|
||||||
|
|
||||||
|
const companyNameInput = page.locator('input[placeholder="Acme Corp"]');
|
||||||
|
await companyNameInput.fill(COMPANY_NAME);
|
||||||
|
|
||||||
|
// Click Next
|
||||||
|
const nextButton = page.getByRole("button", { name: "Next" });
|
||||||
|
await nextButton.click();
|
||||||
|
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
// Step 2: Create your first agent
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
await expect(
|
||||||
|
page.locator("h3", { hasText: "Create your first agent" })
|
||||||
|
).toBeVisible({ timeout: 10_000 });
|
||||||
|
await expect(page.locator("text=Step 2 of 4")).toBeVisible();
|
||||||
|
|
||||||
|
// Agent name should default to "CEO"
|
||||||
|
const agentNameInput = page.locator('input[placeholder="CEO"]');
|
||||||
|
await expect(agentNameInput).toHaveValue(AGENT_NAME);
|
||||||
|
|
||||||
|
// Claude Code adapter should be selected by default
|
||||||
|
await expect(
|
||||||
|
page.locator("button", { hasText: "Claude Code" }).locator("..")
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
// Select the "Process" adapter to avoid needing a real CLI tool installed
|
||||||
|
await page.locator("button", { hasText: "Process" }).click();
|
||||||
|
|
||||||
|
// Fill in process adapter fields
|
||||||
|
const commandInput = page.locator('input[placeholder="e.g. node, python"]');
|
||||||
|
await commandInput.fill("echo");
|
||||||
|
const argsInput = page.locator(
|
||||||
|
'input[placeholder="e.g. script.js, --flag"]'
|
||||||
|
);
|
||||||
|
await argsInput.fill("hello");
|
||||||
|
|
||||||
|
// Click Next (process adapter skips environment test)
|
||||||
|
await page.getByRole("button", { name: "Next" }).click();
|
||||||
|
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
// Step 3: Give it something to do
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
await expect(
|
||||||
|
page.locator("h3", { hasText: "Give it something to do" })
|
||||||
|
).toBeVisible({ timeout: 10_000 });
|
||||||
|
await expect(page.locator("text=Step 3 of 4")).toBeVisible();
|
||||||
|
|
||||||
|
// Clear default title and set our test title
|
||||||
|
const taskTitleInput = page.locator(
|
||||||
|
'input[placeholder="e.g. Research competitor pricing"]'
|
||||||
|
);
|
||||||
|
await taskTitleInput.clear();
|
||||||
|
await taskTitleInput.fill(TASK_TITLE);
|
||||||
|
|
||||||
|
// Click Next
|
||||||
|
await page.getByRole("button", { name: "Next" }).click();
|
||||||
|
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
// Step 4: Ready to launch
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
await expect(
|
||||||
|
page.locator("h3", { hasText: "Ready to launch" })
|
||||||
|
).toBeVisible({ timeout: 10_000 });
|
||||||
|
await expect(page.locator("text=Step 4 of 4")).toBeVisible();
|
||||||
|
|
||||||
|
// Verify summary displays our created entities
|
||||||
|
await expect(page.locator("text=" + COMPANY_NAME)).toBeVisible();
|
||||||
|
await expect(page.locator("text=" + AGENT_NAME)).toBeVisible();
|
||||||
|
await expect(page.locator("text=" + TASK_TITLE)).toBeVisible();
|
||||||
|
|
||||||
|
// Click "Open Issue"
|
||||||
|
await page.getByRole("button", { name: "Open Issue" }).click();
|
||||||
|
|
||||||
|
// Should navigate to the issue page
|
||||||
|
await expect(page).toHaveURL(/\/issues\//, { timeout: 10_000 });
|
||||||
|
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
// Verify via API that entities were created
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
const baseUrl = page.url().split("/").slice(0, 3).join("/");
|
||||||
|
|
||||||
|
// List companies and find ours
|
||||||
|
const companiesRes = await page.request.get(`${baseUrl}/api/companies`);
|
||||||
|
expect(companiesRes.ok()).toBe(true);
|
||||||
|
const companies = await companiesRes.json();
|
||||||
|
const company = companies.find(
|
||||||
|
(c: { name: string }) => c.name === COMPANY_NAME
|
||||||
|
);
|
||||||
|
expect(company).toBeTruthy();
|
||||||
|
|
||||||
|
// List agents for our company
|
||||||
|
const agentsRes = await page.request.get(
|
||||||
|
`${baseUrl}/api/companies/${company.id}/agents`
|
||||||
|
);
|
||||||
|
expect(agentsRes.ok()).toBe(true);
|
||||||
|
const agents = await agentsRes.json();
|
||||||
|
const ceoAgent = agents.find(
|
||||||
|
(a: { name: string }) => a.name === AGENT_NAME
|
||||||
|
);
|
||||||
|
expect(ceoAgent).toBeTruthy();
|
||||||
|
expect(ceoAgent.role).toBe("ceo");
|
||||||
|
expect(ceoAgent.adapterType).toBe("process");
|
||||||
|
|
||||||
|
// List issues for our company
|
||||||
|
const issuesRes = await page.request.get(
|
||||||
|
`${baseUrl}/api/companies/${company.id}/issues`
|
||||||
|
);
|
||||||
|
expect(issuesRes.ok()).toBe(true);
|
||||||
|
const issues = await issuesRes.json();
|
||||||
|
const task = issues.find(
|
||||||
|
(i: { title: string }) => i.title === TASK_TITLE
|
||||||
|
);
|
||||||
|
expect(task).toBeTruthy();
|
||||||
|
expect(task.assigneeAgentId).toBe(ceoAgent.id);
|
||||||
|
|
||||||
|
if (!SKIP_LLM) {
|
||||||
|
// LLM-dependent: wait for the heartbeat to transition the issue
|
||||||
|
await expect(async () => {
|
||||||
|
const res = await page.request.get(
|
||||||
|
`${baseUrl}/api/issues/${task.id}`
|
||||||
|
);
|
||||||
|
const issue = await res.json();
|
||||||
|
expect(["in_progress", "done"]).toContain(issue.status);
|
||||||
|
}).toPass({ timeout: 120_000, intervals: [5_000] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
35
tests/e2e/playwright.config.ts
Normal file
35
tests/e2e/playwright.config.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { defineConfig } from "@playwright/test";
|
||||||
|
|
||||||
|
const PORT = Number(process.env.PAPERCLIP_E2E_PORT ?? 3100);
|
||||||
|
const BASE_URL = `http://127.0.0.1:${PORT}`;
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: ".",
|
||||||
|
testMatch: "**/*.spec.ts",
|
||||||
|
timeout: 60_000,
|
||||||
|
retries: 0,
|
||||||
|
use: {
|
||||||
|
baseURL: BASE_URL,
|
||||||
|
headless: true,
|
||||||
|
screenshot: "only-on-failure",
|
||||||
|
trace: "on-first-retry",
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: "chromium",
|
||||||
|
use: { browserName: "chromium" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// The webServer directive starts `paperclipai run` before tests.
|
||||||
|
// Expects `pnpm paperclipai` to be runnable from repo root.
|
||||||
|
webServer: {
|
||||||
|
command: `pnpm paperclipai run --yes`,
|
||||||
|
url: `${BASE_URL}/api/health`,
|
||||||
|
reuseExistingServer: !!process.env.CI,
|
||||||
|
timeout: 120_000,
|
||||||
|
stdout: "pipe",
|
||||||
|
stderr: "pipe",
|
||||||
|
},
|
||||||
|
outputDir: "./test-results",
|
||||||
|
reporter: [["list"], ["html", { open: "never", outputFolder: "./playwright-report" }]],
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user