Skip to content

Commit 7240763

Browse files
lucbrinkmanclaude
andcommitted
Add Playwright E2E tests for canvas interactions and node management
- Install Playwright with Chromium browser - Create comprehensive E2E test suite: - Canvas navigation (load, pan, zoom) - Node creation (context menu, add arrow buttons) - Node movement (drag, grid snap, free movement with Shift) - Node deletion (delete button, START node protection, nodes with connections) - Add npm scripts for running tests in different modes (headless, UI, headed, debug) - Integrate E2E tests into CI workflow - Upload test artifacts on failure for debugging - Update CLAUDE.md with testing documentation - Add Playwright artifacts to .gitignore 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 951c6ba commit 7240763

File tree

10 files changed

+508
-3
lines changed

10 files changed

+508
-3
lines changed

.github/workflows/ci.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,17 @@ jobs:
3131

3232
- name: Build
3333
run: npm run build
34+
35+
- name: Install Playwright Browsers
36+
run: npx playwright install --with-deps chromium
37+
38+
- name: Run E2E tests
39+
run: npm run test:e2e
40+
41+
- name: Upload test results
42+
uses: actions/upload-artifact@v4
43+
if: failure()
44+
with:
45+
name: playwright-report
46+
path: playwright-report/
47+
retention-days: 30

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,8 @@ yarn-error.log*
3131
# typescript
3232
*.tsbuildinfo
3333
next-env.d.ts
34+
35+
# playwright
36+
/test-results/
37+
/playwright-report/
38+
/playwright/.cache/

CLAUDE.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ Next.js application called "Map of AI Futures" - an interactive probability flow
2121
- `npm run dev` - Start dev server (http://localhost:3000)
2222
- `npm run build` - Build for production
2323
- `npm run lint` - Run ESLint
24+
- `npm run type-check` - Run TypeScript type checking
25+
- `npm run test:e2e` - Run E2E tests with Playwright
26+
- `npm run test:e2e:ui` - Run E2E tests with Playwright UI
27+
- `npm run test:e2e:headed` - Run E2E tests with browser visible
28+
- `npm run test:e2e:debug` - Debug E2E tests
2429

2530
**Process Management:**
2631

@@ -79,9 +84,21 @@ Next.js application called "Map of AI Futures" - an interactive probability flow
7984
**CI/CD Pipeline:**
8085

8186
- GitHub Actions workflow runs on all pushes and PRs (`.github/workflows/ci.yml`)
82-
- Checks: TypeScript type-check, ESLint, Next.js build
87+
- Checks: TypeScript type-check, ESLint, Next.js build, E2E tests (Playwright)
8388
- PRs blocked from merging if CI fails
84-
- Local pre-commit hooks run same checks (via Husky) to catch issues early
89+
- Local pre-commit hooks run type-check, lint, and format checks (via Husky)
90+
- E2E tests run in CI to validate core functionality
91+
92+
**Testing:**
93+
94+
- **E2E Tests** (`tests/e2e/`): Playwright tests for critical user flows
95+
- Canvas navigation (panning, zooming)
96+
- Node creation (context menu, add arrow buttons)
97+
- Node movement (drag-and-drop, grid snapping, free movement with Shift)
98+
- Node deletion (delete button, START node protection)
99+
- Tests run automatically in CI on every PR
100+
- Run locally: `npm run test:e2e`
101+
- Debug tests: `npm run test:e2e:ui` or `npm run test:e2e:debug`
85102

86103
**Branch Protection (configure in GitHub settings):**
87104

package-lock.json

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

package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@
1010
"type-check": "tsc --noEmit",
1111
"format": "prettier --write .",
1212
"format:check": "prettier --check .",
13-
"prepare": "husky"
13+
"prepare": "husky",
14+
"test:e2e": "playwright test",
15+
"test:e2e:ui": "playwright test --ui",
16+
"test:e2e:headed": "playwright test --headed",
17+
"test:e2e:debug": "playwright test --debug"
1418
},
1519
"dependencies": {
1620
"@supabase/ssr": "^0.7.0",
@@ -23,6 +27,7 @@
2327
"resend": "^6.5.2"
2428
},
2529
"devDependencies": {
30+
"@playwright/test": "^1.56.1",
2631
"@types/node": "^20",
2732
"@types/react": "^19",
2833
"@types/react-dom": "^19",

playwright.config.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { defineConfig, devices } from "@playwright/test";
2+
3+
/**
4+
* See https://playwright.dev/docs/test-configuration.
5+
*/
6+
export default defineConfig({
7+
testDir: "./tests/e2e",
8+
/* Run tests in files in parallel */
9+
fullyParallel: true,
10+
/* Fail the build on CI if you accidentally left test.only in the source code. */
11+
forbidOnly: !!process.env.CI,
12+
/* Retry on CI only */
13+
retries: process.env.CI ? 2 : 0,
14+
/* Opt out of parallel tests on CI. */
15+
workers: process.env.CI ? 1 : undefined,
16+
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
17+
reporter: "html",
18+
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
19+
use: {
20+
/* Base URL to use in actions like `await page.goto('/')`. */
21+
baseURL: "http://localhost:3000",
22+
23+
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
24+
trace: "on-first-retry",
25+
26+
/* Screenshot on failure */
27+
screenshot: "only-on-failure",
28+
},
29+
30+
/* Configure projects for major browsers */
31+
projects: [
32+
{
33+
name: "chromium",
34+
use: { ...devices["Desktop Chrome"] },
35+
},
36+
],
37+
38+
/* Run your local dev server before starting the tests */
39+
webServer: {
40+
command: "npm run dev",
41+
url: "http://localhost:3000",
42+
reuseExistingServer: !process.env.CI,
43+
timeout: 120000,
44+
},
45+
});
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { test, expect } from "@playwright/test";
2+
3+
test.describe("Canvas Navigation", () => {
4+
test("should load the flowchart page", async ({ page }) => {
5+
await page.goto("/");
6+
7+
// Wait for the canvas to be visible
8+
await expect(page.locator('[data-node="true"]').first()).toBeVisible();
9+
10+
// Check that we have nodes on the canvas
11+
const nodeCount = await page.locator('[data-node="true"]').count();
12+
expect(nodeCount).toBeGreaterThan(0);
13+
});
14+
15+
test("should be able to pan the canvas", async ({ page }) => {
16+
await page.goto("/");
17+
18+
// Wait for canvas to load
19+
await page.locator('[data-node="true"]').first().waitFor();
20+
21+
// Get initial scroll position
22+
const scrollContainer = page
23+
.locator(".w-full.h-full.overflow-auto")
24+
.first();
25+
const initialScrollLeft = await scrollContainer.evaluate(
26+
(el) => el.scrollLeft
27+
);
28+
const initialScrollTop = await scrollContainer.evaluate(
29+
(el) => el.scrollTop
30+
);
31+
32+
// Pan by dragging on empty canvas space
33+
await page.mouse.move(400, 400);
34+
await page.mouse.down();
35+
await page.mouse.move(300, 300);
36+
await page.mouse.up();
37+
38+
// Check that scroll position changed
39+
const newScrollLeft = await scrollContainer.evaluate((el) => el.scrollLeft);
40+
const newScrollTop = await scrollContainer.evaluate((el) => el.scrollTop);
41+
42+
expect(
43+
newScrollLeft !== initialScrollLeft || newScrollTop !== initialScrollTop
44+
).toBeTruthy();
45+
});
46+
47+
test("should zoom in and out with Ctrl+scroll", async ({ page }) => {
48+
await page.goto("/");
49+
50+
// Wait for canvas to load
51+
await page.locator('[data-node="true"]').first().waitFor();
52+
53+
// Get initial zoom (from the canvas container transform style)
54+
const canvas = page.locator(".relative.bg-background").first();
55+
const initialTransform = await canvas.evaluate(
56+
(el) => window.getComputedStyle(el).transform
57+
);
58+
59+
// Zoom in with Ctrl+Wheel
60+
await page.mouse.move(500, 500);
61+
await page.keyboard.down("Control");
62+
await page.mouse.wheel(0, -100);
63+
await page.keyboard.up("Control");
64+
65+
// Wait for zoom to apply
66+
await page.waitForTimeout(100);
67+
68+
const newTransform = await canvas.evaluate(
69+
(el) => window.getComputedStyle(el).transform
70+
);
71+
72+
// Transform should have changed
73+
expect(newTransform).not.toBe(initialTransform);
74+
});
75+
});

tests/e2e/node-creation.spec.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { test, expect } from "@playwright/test";
2+
3+
test.describe("Node Creation", () => {
4+
test("should create a new node via context menu", async ({ page }) => {
5+
await page.goto("/");
6+
7+
// Wait for canvas to load
8+
await page.locator('[data-node="true"]').first().waitFor();
9+
10+
// Count initial nodes
11+
const initialNodeCount = await page.locator('[data-node="true"]').count();
12+
13+
// Right-click on empty canvas space to open context menu
14+
const canvas = page.locator(".relative.bg-background").first();
15+
await canvas.click({ button: "right", position: { x: 600, y: 400 } });
16+
17+
// Wait for context menu to appear
18+
await page.waitForTimeout(200);
19+
20+
// Click "Add Node" in context menu
21+
const addNodeButton = page.getByText("Add Node");
22+
await expect(addNodeButton).toBeVisible();
23+
await addNodeButton.click();
24+
25+
// Wait for new node to be created
26+
await page.waitForTimeout(500);
27+
28+
// Count nodes after creation
29+
const newNodeCount = await page.locator('[data-node="true"]').count();
30+
31+
// Should have one more node
32+
expect(newNodeCount).toBe(initialNodeCount + 1);
33+
});
34+
35+
test("should create a node using add arrow buttons", async ({ page }) => {
36+
await page.goto("/");
37+
38+
// Wait for canvas to load
39+
await page.locator('[data-node="true"]').first().waitFor();
40+
41+
// Click on a node to select it
42+
const firstNode = page.locator('[data-node="true"]').first();
43+
await firstNode.click();
44+
45+
// Wait for node to be selected and add arrow buttons to appear
46+
await page.waitForTimeout(300);
47+
48+
// Count initial nodes
49+
const initialNodeCount = await page.locator('[data-node="true"]').count();
50+
51+
// Look for an add arrow button (they should be visible on selected nodes)
52+
// Try to find one of the directional arrow buttons
53+
const arrowButton = page
54+
.locator('button:has(svg[class*="lucide-arrow"])')
55+
.first();
56+
57+
if (await arrowButton.isVisible()) {
58+
// Click and drag the arrow button to create a new connection
59+
await arrowButton.hover();
60+
await page.mouse.down();
61+
62+
// Drag to a new position
63+
await page.mouse.move(700, 300);
64+
await page.mouse.up();
65+
66+
// Wait for node creation
67+
await page.waitForTimeout(500);
68+
69+
// Count nodes after creation
70+
const newNodeCount = await page.locator('[data-node="true"]').count();
71+
72+
// Should have one more node
73+
expect(newNodeCount).toBe(initialNodeCount + 1);
74+
}
75+
});
76+
});

0 commit comments

Comments
 (0)