Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,17 @@ jobs:

- name: Build
run: npm run build

- name: Install Playwright Browsers
run: npx playwright install --with-deps chromium

- name: Run E2E tests
run: npm run test:e2e

- name: Upload test results
uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,8 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts

# playwright
/test-results/
/playwright-report/
/playwright/.cache/
24 changes: 22 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ Next.js application called "Map of AI Futures" - an interactive probability flow
- `npm run dev` - Start dev server (http://localhost:3000)
- `npm run build` - Build for production
- `npm run lint` - Run ESLint
- `npm run type-check` - Run TypeScript type checking
- `npm run test:e2e` - Run E2E tests with Playwright
- `npm run test:e2e:ui` - Run E2E tests with Playwright UI
- `npm run test:e2e:headed` - Run E2E tests with browser visible
- `npm run test:e2e:debug` - Debug E2E tests

**Process Management:**

Expand Down Expand Up @@ -66,6 +71,8 @@ Next.js application called "Map of AI Futures" - an interactive probability flow
```

2. **Create Pull Request:**
- **IMPORTANT:** Only create PRs when the user explicitly requests it
- Feature branches may be worked on for extended periods before PR creation
- Use GitHub UI or `gh pr create --repo lucbrinkman/ai-world-model` command
- **IMPORTANT:** Always create PRs to `lucbrinkman/ai-world-model` (NOT the upstream fork `swantescholz/aifutures`)
- CI automatically runs type-check, lint, and build
Expand All @@ -79,9 +86,21 @@ Next.js application called "Map of AI Futures" - an interactive probability flow
**CI/CD Pipeline:**

- GitHub Actions workflow runs on all pushes and PRs (`.github/workflows/ci.yml`)
- Checks: TypeScript type-check, ESLint, Next.js build
- Checks: TypeScript type-check, ESLint, Next.js build, E2E tests (Playwright)
- PRs blocked from merging if CI fails
- Local pre-commit hooks run same checks (via Husky) to catch issues early
- Local pre-commit hooks run type-check, lint, and format checks (via Husky)
- E2E tests run in CI to validate core functionality

**Testing:**

- **E2E Tests** (`tests/e2e/`): Playwright tests for critical user flows
- Canvas navigation (panning, zooming)
- Node creation (context menu, add arrow buttons)
- Node movement (drag-and-drop, grid snapping, free movement with Shift)
- Node deletion (delete button, START node protection)
- Tests run automatically in CI on every PR
- Run locally: `npm run test:e2e`
- Debug tests: `npm run test:e2e:ui` or `npm run test:e2e:debug`

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

Expand All @@ -93,6 +112,7 @@ Next.js application called "Map of AI Futures" - an interactive probability flow

- NEVER merge to main locally - always use GitHub PRs
- NEVER push main branch directly - only push feature branches
- NEVER create PRs automatically - only when the user explicitly requests it
- Let CI validate all changes before merging
- If CI fails, fix issues and push updates to the PR branch

Expand Down
64 changes: 64 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
"type-check": "tsc --noEmit",
"format": "prettier --write .",
"format:check": "prettier --check .",
"prepare": "husky"
"prepare": "husky",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:headed": "playwright test --headed",
"test:e2e:debug": "playwright test --debug"
},
"dependencies": {
"@supabase/ssr": "^0.7.0",
Expand All @@ -23,6 +27,7 @@
"resend": "^6.5.2"
},
"devDependencies": {
"@playwright/test": "^1.56.1",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
Expand Down
45 changes: 45 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { defineConfig, devices } from "@playwright/test";

/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./tests/e2e",
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: "http://localhost:3000",

/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",

/* Screenshot on failure */
screenshot: "only-on-failure",
},

/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],

/* Run your local dev server before starting the tests */
webServer: {
command: "npm run dev",
url: "http://localhost:3000",
reuseExistingServer: !process.env.CI,
timeout: 120000,
},
});
75 changes: 75 additions & 0 deletions tests/e2e/canvas-navigation.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { test, expect } from "@playwright/test";

test.describe("Canvas Navigation", () => {
test("should load the flowchart page", async ({ page }) => {
await page.goto("/");

// Wait for the canvas to be visible
await expect(page.locator('[data-node="true"]').first()).toBeVisible();

// Check that we have nodes on the canvas
const nodeCount = await page.locator('[data-node="true"]').count();
expect(nodeCount).toBeGreaterThan(0);
});

test("should be able to pan the canvas", async ({ page }) => {
await page.goto("/");

// Wait for canvas to load
await page.locator('[data-node="true"]').first().waitFor();

// Get initial scroll position
const scrollContainer = page
.locator(".w-full.h-full.overflow-auto")
.first();
const initialScrollLeft = await scrollContainer.evaluate(
(el) => el.scrollLeft
);
const initialScrollTop = await scrollContainer.evaluate(
(el) => el.scrollTop
);

// Pan by dragging on empty canvas space
await page.mouse.move(400, 400);
await page.mouse.down();
await page.mouse.move(300, 300);
await page.mouse.up();

// Check that scroll position changed
const newScrollLeft = await scrollContainer.evaluate((el) => el.scrollLeft);
const newScrollTop = await scrollContainer.evaluate((el) => el.scrollTop);

expect(
newScrollLeft !== initialScrollLeft || newScrollTop !== initialScrollTop
).toBeTruthy();
});

test("should zoom in and out with Ctrl+scroll", async ({ page }) => {
await page.goto("/");

// Wait for canvas to load
await page.locator('[data-node="true"]').first().waitFor();

// Get initial zoom (from the canvas container transform style)
const canvas = page.locator(".relative.bg-background").first();
const initialTransform = await canvas.evaluate(
(el) => window.getComputedStyle(el).transform
);

// Zoom in with Ctrl+Wheel
await page.mouse.move(500, 500);
await page.keyboard.down("Control");
await page.mouse.wheel(0, -100);
await page.keyboard.up("Control");

// Wait for zoom to apply
await page.waitForTimeout(100);

const newTransform = await canvas.evaluate(
(el) => window.getComputedStyle(el).transform
);

// Transform should have changed
expect(newTransform).not.toBe(initialTransform);
});
});
76 changes: 76 additions & 0 deletions tests/e2e/node-creation.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { test, expect } from "@playwright/test";

test.describe("Node Creation", () => {
test("should create a new node via context menu", async ({ page }) => {
await page.goto("/");

// Wait for canvas to load
await page.locator('[data-node="true"]').first().waitFor();

// Count initial nodes
const initialNodeCount = await page.locator('[data-node="true"]').count();

// Right-click on empty canvas space to open context menu
const canvas = page.locator(".relative.bg-background").first();
await canvas.click({ button: "right", position: { x: 600, y: 400 } });

// Wait for context menu to appear
await page.waitForTimeout(200);

// Click "Add Node" in context menu
const addNodeButton = page.getByText("Add Node");
await expect(addNodeButton).toBeVisible();
await addNodeButton.click();

// Wait for new node to be created
await page.waitForTimeout(500);

// Count nodes after creation
const newNodeCount = await page.locator('[data-node="true"]').count();

// Should have one more node
expect(newNodeCount).toBe(initialNodeCount + 1);
});

test("should create a node using add arrow buttons", async ({ page }) => {
await page.goto("/");

// Wait for canvas to load
await page.locator('[data-node="true"]').first().waitFor();

// Click on a node to select it
const firstNode = page.locator('[data-node="true"]').first();
await firstNode.click();

// Wait for node to be selected and add arrow buttons to appear
await page.waitForTimeout(300);

// Count initial nodes
const initialNodeCount = await page.locator('[data-node="true"]').count();

// Look for an add arrow button (they should be visible on selected nodes)
// Try to find one of the directional arrow buttons
const arrowButton = page
.locator('button:has(svg[class*="lucide-arrow"])')
.first();

if (await arrowButton.isVisible()) {
// Click and drag the arrow button to create a new connection
await arrowButton.hover();
await page.mouse.down();

// Drag to a new position
await page.mouse.move(700, 300);
await page.mouse.up();

// Wait for node creation
await page.waitForTimeout(500);

// Count nodes after creation
const newNodeCount = await page.locator('[data-node="true"]').count();

// Should have one more node
expect(newNodeCount).toBe(initialNodeCount + 1);
}
});
});
Loading
Loading