Skip to content

Commit dac4379

Browse files
Merge pull request #36 from vincentgrobler/feature/ticket-6.1-testing-ci
Feature/ticket 6.1 testing ci
2 parents 50b0bc1 + 6b28598 commit dac4379

17 files changed

Lines changed: 1645 additions & 9 deletions

.github/workflows/ci.yml

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# ─────────────────────────────────────────────────────────────────────────────
2+
# CrewForm — CI Pipeline
3+
# ─────────────────────────────────────────────────────────────────────────────
14
# SPDX-License-Identifier: AGPL-3.0-or-later
25
# Copyright (C) 2026 CrewForm
36

@@ -10,6 +13,7 @@ on:
1013
branches: [main]
1114

1215
jobs:
16+
# ── Frontend: Build, Lint, Test ─────────────────────────────────────────
1317
build-and-test:
1418
name: Build, Lint & Test
1519
runs-on: ubuntu-latest
@@ -25,22 +29,87 @@ jobs:
2529
cache: 'npm'
2630

2731
- name: Install dependencies
28-
run: |
29-
npm ci
30-
cd task-runner && npm ci
32+
run: npm ci
3133

3234
- name: Lint
3335
run: npm run lint
3436

3537
- name: Type check
3638
run: npx tsc --noEmit
3739

38-
- name: Test
40+
- name: Unit tests
3941
run: npm run test -- --run
4042

4143
- name: Build
4244
run: npm run build
4345

46+
# ── Task Runner: Lint & Test ────────────────────────────────────────────
47+
task-runner-test:
48+
name: Task Runner — Lint & Test
49+
runs-on: ubuntu-latest
50+
51+
defaults:
52+
run:
53+
working-directory: task-runner
54+
55+
steps:
56+
- name: Checkout
57+
uses: actions/checkout@v4
58+
59+
- name: Setup Node.js
60+
uses: actions/setup-node@v4
61+
with:
62+
node-version: '20'
63+
cache: 'npm'
64+
cache-dependency-path: task-runner/package-lock.json
65+
66+
- name: Install dependencies
67+
run: npm ci
68+
69+
- name: Type check
70+
run: npx tsc --noEmit
71+
72+
- name: Unit tests
73+
run: npx vitest run
74+
75+
# ── E2E: Playwright (manual trigger) ────────────────────────────────────
76+
e2e:
77+
name: E2E — Playwright
78+
runs-on: ubuntu-latest
79+
if: github.event_name == 'workflow_dispatch'
80+
81+
steps:
82+
- name: Checkout
83+
uses: actions/checkout@v4
84+
85+
- name: Setup Node.js
86+
uses: actions/setup-node@v4
87+
with:
88+
node-version: '20'
89+
cache: 'npm'
90+
91+
- name: Install dependencies
92+
run: npm ci
93+
94+
- name: Install Playwright browsers
95+
run: npx playwright install --with-deps chromium
96+
97+
- name: Run Playwright tests
98+
run: npx playwright test
99+
env:
100+
E2E_EMAIL: ${{ secrets.E2E_EMAIL }}
101+
E2E_PASSWORD: ${{ secrets.E2E_PASSWORD }}
102+
E2E_BASE_URL: ${{ secrets.E2E_BASE_URL }}
103+
104+
- name: Upload test results
105+
uses: actions/upload-artifact@v4
106+
if: always()
107+
with:
108+
name: playwright-report
109+
path: playwright-report/
110+
retention-days: 14
111+
112+
# ── Sensitive Doc Guard ─────────────────────────────────────────────────
44113
doc-guard:
45114
name: Sensitive Doc Guard
46115
runs-on: ubuntu-latest
@@ -64,6 +133,7 @@ jobs:
64133
fi
65134
echo "✅ No sensitive docs found — all clear"
66135
136+
# ── AGPL Licence Header Check ───────────────────────────────────────────
67137
licence-check:
68138
name: AGPL Licence Headers
69139
runs-on: ubuntu-latest

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ dist-ssr
1010
# Environment
1111
.env
1212
.env.local
13+
14+
# Playwright
15+
/test-results/
16+
/playwright-report/
17+
/blob-report/
18+
/playwright/.cache/
19+
e2e/.auth/
1320
.env.development.local
1421
.env.test.local
1522
.env.production.local

e2e/agents.spec.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
// Copyright (C) 2026 CrewForm
3+
4+
/**
5+
* E2E — Critical path 2: Agent CRUD
6+
*
7+
* Verifies: create agent → appears in list → edit → delete
8+
*/
9+
10+
import { test, expect } from '@playwright/test'
11+
12+
const AGENT_NAME = `E2E Test Agent ${Date.now()}`
13+
const AGENT_NAME_EDITED = `${AGENT_NAME} (edited)`
14+
15+
test.describe('Agent CRUD', () => {
16+
test.describe.configure({ mode: 'serial' })
17+
18+
test('create a new agent', async ({ page }) => {
19+
await page.goto('/agents')
20+
21+
// Click create button
22+
await page.getByRole('button', { name: /create|new|add/i }).click()
23+
24+
// Fill in agent form
25+
await page.getByLabel(/name/i).fill(AGENT_NAME)
26+
await page.getByLabel(/description/i).fill('E2E test agent — safe to delete')
27+
28+
// Submit
29+
await page.getByRole('button', { name: /save|create|submit/i }).click()
30+
31+
// Agent should appear in the list
32+
await expect(page.getByText(AGENT_NAME)).toBeVisible({ timeout: 10_000 })
33+
})
34+
35+
test('edit the agent', async ({ page }) => {
36+
await page.goto('/agents')
37+
38+
// Find and click on the agent
39+
await page.getByText(AGENT_NAME).click()
40+
41+
// Edit name
42+
const nameInput = page.getByLabel(/name/i)
43+
await nameInput.clear()
44+
await nameInput.fill(AGENT_NAME_EDITED)
45+
46+
// Save
47+
await page.getByRole('button', { name: /save|update/i }).click()
48+
49+
// Updated name should be visible
50+
await expect(page.getByText(AGENT_NAME_EDITED)).toBeVisible({ timeout: 10_000 })
51+
})
52+
53+
test('delete the agent', async ({ page }) => {
54+
await page.goto('/agents')
55+
56+
// Find the agent
57+
await page.getByText(AGENT_NAME_EDITED).click()
58+
59+
// Click delete
60+
await page.getByRole('button', { name: /delete/i }).click()
61+
62+
// Confirm deletion if there's a confirmation dialog
63+
const confirmBtn = page.getByRole('button', { name: /confirm|yes|delete/i })
64+
if (await confirmBtn.isVisible({ timeout: 2_000 }).catch(() => false)) {
65+
await confirmBtn.click()
66+
}
67+
68+
// Agent should no longer be in the list
69+
await expect(page.getByText(AGENT_NAME_EDITED)).not.toBeVisible({ timeout: 10_000 })
70+
})
71+
})

e2e/auth.setup.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
// Copyright (C) 2026 CrewForm
3+
4+
/**
5+
* Auth setup — creates and saves authenticated browser state.
6+
*
7+
* This runs once before all other tests. The storage state is reused
8+
* by all test projects so we don't log in for every spec.
9+
*
10+
* Requires E2E_EMAIL and E2E_PASSWORD env vars (or defaults for local dev).
11+
*/
12+
13+
import { test as setup, expect } from '@playwright/test'
14+
15+
const AUTH_FILE = 'e2e/.auth/user.json'
16+
17+
setup('authenticate', async ({ page }) => {
18+
const email = process.env.E2E_EMAIL ?? 'test@crewform.local'
19+
const password = process.env.E2E_PASSWORD ?? 'testpassword123'
20+
21+
// Navigate to login page
22+
await page.goto('/login')
23+
24+
// Fill in credentials
25+
await page.getByLabel('Email').fill(email)
26+
await page.getByLabel('Password').fill(password)
27+
await page.getByRole('button', { name: /sign in|log in/i }).click()
28+
29+
// Wait for redirect to dashboard
30+
await expect(page).toHaveURL(/\/(dashboard)?$/, { timeout: 10_000 })
31+
32+
// Save authenticated state
33+
await page.context().storageState({ path: AUTH_FILE })
34+
})

e2e/login.spec.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
// Copyright (C) 2026 CrewForm
3+
4+
/**
5+
* E2E — Critical path 1: Login flow
6+
*
7+
* Verifies: login → dashboard loads → sidebar navigation visible
8+
*/
9+
10+
import { test, expect } from '@playwright/test'
11+
12+
test.describe('Login & Dashboard', () => {
13+
test('dashboard loads with sidebar navigation', async ({ page }) => {
14+
await page.goto('/')
15+
16+
// Should be on dashboard (auth setup already logged us in)
17+
await expect(page).toHaveURL(/\/(dashboard)?$/)
18+
19+
// Sidebar should be visible with key navigation items
20+
const sidebar = page.locator('nav, [role="navigation"]').first()
21+
await expect(sidebar).toBeVisible()
22+
23+
// Key nav items should be present
24+
await expect(page.getByText('Dashboard')).toBeVisible()
25+
await expect(page.getByText('Agents')).toBeVisible()
26+
await expect(page.getByText('Tasks')).toBeVisible()
27+
})
28+
29+
test('dashboard shows analytics section', async ({ page }) => {
30+
await page.goto('/')
31+
32+
// Analytics cards or charts should load
33+
await expect(
34+
page.getByText(/total|agents|tasks|usage/i).first(),
35+
).toBeVisible({ timeout: 10_000 })
36+
})
37+
})

e2e/marketplace.spec.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
// Copyright (C) 2026 CrewForm
3+
4+
/**
5+
* E2E — Critical path 4: Marketplace
6+
*
7+
* Verifies: browse → search → filter tags → view detail modal → install
8+
*/
9+
10+
import { test, expect } from '@playwright/test'
11+
12+
test.describe('Marketplace', () => {
13+
test('browse marketplace and see agent cards', async ({ page }) => {
14+
await page.goto('/marketplace')
15+
16+
// Should see the marketplace heading
17+
await expect(page.getByRole('heading', { name: /marketplace/i })).toBeVisible()
18+
19+
// Agent cards should load
20+
await expect(
21+
page.locator('button').filter({ hasText: /installs/i }).first(),
22+
).toBeVisible({ timeout: 10_000 })
23+
})
24+
25+
test('search filters agents', async ({ page }) => {
26+
await page.goto('/marketplace')
27+
28+
// Wait for cards to load
29+
await expect(
30+
page.locator('button').filter({ hasText: /installs/i }).first(),
31+
).toBeVisible({ timeout: 10_000 })
32+
33+
// Count initial cards
34+
const initialCount = await page.locator('button').filter({ hasText: /installs/i }).count()
35+
36+
// Type in search
37+
await page.getByPlaceholder(/search/i).fill('code')
38+
39+
// Wait for results to update
40+
await page.waitForTimeout(500) // debounce
41+
42+
// Should have fewer or same cards (filtered)
43+
const filteredCount = await page.locator('button').filter({ hasText: /installs/i }).count()
44+
expect(filteredCount).toBeLessThanOrEqual(initialCount)
45+
})
46+
47+
test('click tag pill filters results', async ({ page }) => {
48+
await page.goto('/marketplace')
49+
50+
// Wait for tags to load
51+
await expect(
52+
page.locator('button').filter({ hasText: /installs/i }).first(),
53+
).toBeVisible({ timeout: 10_000 })
54+
55+
// Click a tag pill
56+
const tagPill = page.locator('button').filter({ hasText: /coding|research|writing/i }).first()
57+
if (await tagPill.isVisible({ timeout: 3_000 }).catch(() => false)) {
58+
await tagPill.click()
59+
// Results should update (at least some cards visible)
60+
await page.waitForTimeout(500)
61+
}
62+
})
63+
64+
test('open agent detail modal', async ({ page }) => {
65+
await page.goto('/marketplace')
66+
67+
// Wait for cards to load
68+
await expect(
69+
page.locator('button').filter({ hasText: /installs/i }).first(),
70+
).toBeVisible({ timeout: 10_000 })
71+
72+
// Click the first agent card
73+
await page.locator('button').filter({ hasText: /installs/i }).first().click()
74+
75+
// Modal should open with agent details
76+
await expect(page.getByText(/system prompt/i)).toBeVisible({ timeout: 5_000 })
77+
await expect(page.getByRole('button', { name: /install agent/i })).toBeVisible()
78+
})
79+
80+
test('close detail modal', async ({ page }) => {
81+
await page.goto('/marketplace')
82+
83+
// Open modal
84+
await expect(
85+
page.locator('button').filter({ hasText: /installs/i }).first(),
86+
).toBeVisible({ timeout: 10_000 })
87+
await page.locator('button').filter({ hasText: /installs/i }).first().click()
88+
89+
// Wait for modal
90+
await expect(page.getByText(/system prompt/i)).toBeVisible({ timeout: 5_000 })
91+
92+
// Close modal via X button
93+
await page.locator('button').filter({ has: page.locator('svg') }).first().click()
94+
95+
// Modal should close — system prompt should not be visible
96+
await expect(page.getByText(/system prompt/i)).not.toBeVisible({ timeout: 3_000 })
97+
})
98+
})

0 commit comments

Comments
 (0)