Skip to content
Merged
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
56 changes: 53 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,61 @@ jobs:
- name: Build
run: npm run build

e2e:
name: E2E smoke
runs-on: ubuntu-latest
timeout-minutes: 15
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: ci
POSTGRES_PASSWORD: ci
POSTGRES_DB: datashield
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U ci -d datashield"
--health-interval 3s
--health-timeout 3s
--health-retries 20
env:
DATABASE_URL: postgresql://ci:ci@localhost:5432/datashield
AUTH_SECRET: ci-placeholder-auth-secret
AUTH_URL: http://localhost:3000
DIRECTORY_ENCRYPTION_KEY: ci-placeholder-encryption-key-32-chars-minimum
steps:
- uses: actions/checkout@v7

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 22
cache: npm

- name: Install dependencies
run: npm ci

- name: Apply migrations
run: npx prisma migrate deploy

- name: Seed admin account
run: npx tsx prisma/seed.ts

- name: Build
run: npm run build

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

- name: Smoke test
run: npx playwright test

# Aggregate gate required by the main branch protection rule.
# Reports the "ci" status check, green only when quality and build pass.
# Reports the "ci" status check, green only when all gates pass.
ci:
name: ci
runs-on: ubuntu-latest
needs: [quality, test, build]
needs: [quality, test, build, e2e]
steps:
- run: echo "Quality, test and build gates passed."
- run: echo "Quality, test, build and e2e gates passed."
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ next-env.d.ts
# database dumps (contain PII, never commit)
backups/

# playwright
playwright-report/
test-results/

# private
ROADMAP.md
prisma/seed.dev.ts
Expand Down
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ lint-fix: ## Lint and auto-fix
test: ## Run the test suite
$(N) npm test

.PHONY: e2e
e2e: ## Run the Playwright smoke test (app must be reachable on :3000)
$(N) npm run test:e2e

.PHONY: check
check: ## Run the same gates CI enforces (lint, types, schema, build)
$(N) npm run lint -- --max-warnings 0
Expand Down
215 changes: 215 additions & 0 deletions docs/superpowers/plans/2026-07-11-e2e-smoke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
# E2E Smoke Test Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** One Playwright smoke spec (login -> dashboard -> alerts, CSP console guard) running locally against the compose stack and as a blocking CI job.

**Architecture:** `@playwright/test` with a single chromium project; `playwright.config.ts` reuses the local server, starts `next start` in CI; new `e2e` CI job with a postgres service; aggregate `ci` job gains the dependency.

**Tech Stack:** Playwright, GitHub Actions, postgres:16 service container.

## Global Constraints

- Spec: `docs/superpowers/specs/2026-07-11-e2e-smoke-design.md`. Branch: `feat/e2e-smoke`.
- ASCII only. Seeded admin: `admin@datashield.local` / `ChangeMe123!` (defaults in `prisma/seed.ts`).
- CI placeholders identical to the existing `build` job (`DATABASE_URL`, `AUTH_SECRET`, `DIRECTORY_ENCRYPTION_KEY`).
- Gates: `npm run lint -- --max-warnings 0`, `npx tsc --noEmit`, `npm test` (still 148 on this branch).

---

### Task 1: Playwright setup and smoke spec

**Files:**
- Create: `playwright.config.ts`
- Create: `e2e/smoke.spec.ts`
- Modify: `package.json` (devDependency + `test:e2e` script)
- Modify: `.gitignore` (playwright artifacts)
- Modify: `Makefile` (after the `test` target)

**Interfaces:**
- Produces: `npm run test:e2e` / `make e2e`; Task 2's CI job runs `npx playwright test`.

- [ ] **Step 1: Install Playwright**

Run: `npm install -D @playwright/test && npx playwright install chromium`
Expected: devDependency added, chromium downloaded.

- [ ] **Step 2: Write `playwright.config.ts`**

```ts
import { defineConfig, devices } from "@playwright/test"

export default defineConfig({
testDir: "e2e",
fullyParallel: false,
retries: process.env.CI ? 1 : 0,
use: {
baseURL: "http://localhost:3000",
trace: "on-first-retry",
},
projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }],
webServer: {
command: "npm run start",
url: "http://localhost:3000/api/health",
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
})
```

- [ ] **Step 3: Write `e2e/smoke.spec.ts`**

```ts
import { test, expect, type Page } from "@playwright/test"

const cspViolations: string[] = []

function watchConsole(page: Page) {
page.on("console", (msg) => {
const text = msg.text()
if (text.includes("Content Security Policy") || text.includes("Refused to")) {
cspViolations.push(text)
}
})
}

test("admin signs in, sees dashboard and alerts", async ({ page }) => {
watchConsole(page)

await page.goto("/login")
await page.getByLabel("Email").fill("admin@datashield.local")
await page.getByLabel("Password").fill("ChangeMe123!")
await page.getByRole("button", { name: "Sign in" }).click()

await page.waitForURL("**/dashboard")
await expect(page.getByRole("main")).toBeVisible()

await page.goto("/alerts")
await expect(page.getByRole("main")).toBeVisible()

expect(cspViolations).toEqual([])
})
```

If `getByRole("main")` does not exist on those pages, use a heading or a
`data-slot` the pages already render; check the DOM rather than adding
test-ids.

- [ ] **Step 4: Wire package.json, Makefile, .gitignore**

`package.json` scripts, after `"test:watch"`:

```json
"test:e2e": "playwright test",
```

`Makefile`, after the `test` target:

```make
.PHONY: e2e
e2e: ## Run the Playwright smoke test (app must be reachable on :3000)
$(N) npm run test:e2e
```

`.gitignore`, after the backups block:

```
# playwright
playwright-report/
test-results/
```

- [ ] **Step 5: Run locally against the compose stack**

Run: `npx playwright test`
Expected: 1 passed. The local dev server on :3000 is reused.

- [ ] **Step 6: Gates and commit**

Run: `npm run lint -- --max-warnings 0 && npx tsc --noEmit && npm test`
Expected: clean, 148 tests.

```bash
git add playwright.config.ts e2e/smoke.spec.ts package.json package-lock.json Makefile .gitignore
git commit -m "feat: add Playwright smoke test"
```

### Task 2: CI job

**Files:**
- Modify: `.github/workflows/ci.yml` (new `e2e` job before the aggregate; aggregate `needs` gains `e2e`)

**Interfaces:**
- Consumes: `npx playwright test` from Task 1.

- [ ] **Step 1: Add the `e2e` job**

Insert after the `build` job:

```yaml
e2e:
name: E2E smoke
runs-on: ubuntu-latest
timeout-minutes: 15
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: ci
POSTGRES_PASSWORD: ci
POSTGRES_DB: datashield
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U ci -d datashield"
--health-interval 3s
--health-timeout 3s
--health-retries 20
env:
DATABASE_URL: postgresql://ci:ci@localhost:5432/datashield
AUTH_SECRET: ci-placeholder-auth-secret
AUTH_URL: http://localhost:3000
DIRECTORY_ENCRYPTION_KEY: ci-placeholder-encryption-key-32-chars-minimum
steps:
- uses: actions/checkout@v7

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 22
cache: npm

- name: Install dependencies
run: npm ci

- name: Apply migrations
run: npx prisma migrate deploy

- name: Seed admin account
run: npx tsx prisma/seed.ts

- name: Build
run: npm run build

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

- name: Smoke test
run: npx playwright test
```

- [ ] **Step 2: Gate the aggregate**

Replace `needs: [quality, test, build]` with
`needs: [quality, test, build, e2e]` and update the final echo to mention
e2e.

- [ ] **Step 3: Commit**

```bash
git add .github/workflows/ci.yml
git commit -m "ci: run e2e smoke test as blocking gate"
```

CI verification happens at the final push (nothing is pushed before the
roadmap ends).
58 changes: 58 additions & 0 deletions docs/superpowers/specs/2026-07-11-e2e-smoke-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# E2E smoke test - design

Date: 2026-07-11. Roadmap V1 step 4: one minimal end-to-end test that proves
login, dashboard, and alerts work in a real browser, wired as a blocking CI
gate.

## Goal

Catch the regressions unit tests cannot see (auth flow, middleware, CSP,
hydration) with a single stable spec. Deliberately minimal: one spec file,
one browser (chromium). Broader coverage is v1.1 territory.

## Decisions

- Tool: `@playwright/test` (devDependency), chromium only.
- One spec, `e2e/smoke.spec.ts`: sign in with the seeded admin
(`admin@datashield.local` / `ChangeMe123!`), assert the dashboard renders,
navigate to the alerts page, assert it renders.
- The spec collects browser console messages and fails on any CSP violation
(`Refused to` / `Content Security Policy`), so the strict CSP from step 2
stays continuously verified.
- `playwright.config.ts`: `testDir: "e2e"`, `baseURL`
`http://localhost:3000`, `webServer` runs `npm run start` with
`reuseExistingServer: !process.env.CI` (locally it reuses the running
compose stack; in CI it starts the built app).
- CI: new `e2e` job (postgres:16 service, migrate, seed, build, playwright
install chromium, run tests), added to the aggregate `ci` job's `needs`,
so it blocks merges like the other gates.
- Vitest is untouched: its include is `src/**/*.test.ts`, the e2e dir uses
`.spec.ts`.
- Stable selectors: `getByLabel("Email")`, `getByLabel("Password")`,
`getByRole("button", { name: "Sign in" })` (labels exist in
`src/app/(auth)/login/page.tsx`).

## Files

| File | Change |
| --- | --- |
| `playwright.config.ts` | New. testDir, baseURL, webServer, chromium project. |
| `e2e/smoke.spec.ts` | New. The smoke spec with CSP console guard. |
| `package.json` | devDependency `@playwright/test`; script `test:e2e`. |
| `Makefile` | `e2e` target. |
| `.gitignore` | `playwright-report/`, `test-results/`. |
| `.github/workflows/ci.yml` | New `e2e` job; aggregate `ci` needs it. |

## Error handling

None custom; Playwright reports failures with traces
(`trace: "on-first-retry"`).

## Verification

- Local: `npx playwright test` against the running compose stack, 1 spec
green.
- Gates: lint, tsc (playwright config and spec are type-checked), vitest
count unchanged.
- CI: the `e2e` job goes green on the final push (verified at the end of
the roadmap since nothing is pushed before then).
30 changes: 30 additions & 0 deletions e2e/smoke.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { test, expect } from "@playwright/test"

let cspViolations: string[]

test.beforeEach(({ page }) => {
cspViolations = []
page.on("console", (msg) => {
const text = msg.text()
if (text.includes("Content Security Policy") || text.includes("Refused to")) {
cspViolations.push(text)
}
})
})

test.afterEach(() => {
expect(cspViolations).toEqual([])
})

test("admin signs in, sees dashboard and alerts", async ({ page }) => {
await page.goto("/login")
await page.getByLabel("Email").fill("admin@datashield.local")
await page.getByLabel("Password").fill("ChangeMe123!")
await page.getByRole("button", { name: "Sign in" }).click()

await page.waitForURL("**/dashboard")
await expect(page.getByRole("main")).toBeVisible()

await page.goto("/alerts")
await expect(page.getByRole("main")).toBeVisible()
})
Loading