Skip to content

feat: Playwright E2E tests — 5 critical flows, CI workflow#133

Merged
TerrifiedBug merged 12 commits intomainfrom
feat/e2e-tests
Apr 1, 2026
Merged

feat: Playwright E2E tests — 5 critical flows, CI workflow#133
TerrifiedBug merged 12 commits intomainfrom
feat/e2e-tests

Conversation

@TerrifiedBug
Copy link
Copy Markdown
Owner

Summary

  • Set up Playwright from scratch with Page Object Model architecture, custom fixtures, and storageState-based auth
  • 15 E2E tests across 5 critical user flows: auth (login/logout), pipeline CRUD (create/persist/delete), deploy (dialog flow), fleet (list/detail navigation), alerts (history/acknowledge)
  • Full-stack tests against real Postgres — Prisma seed script creates complete test data (user, team, environment, pipeline with 3 nodes, fleet node, alert rules + events)
  • Separate CI workflow (e2e.yml) with Postgres service container, nightly cron + PR triggers, artifact upload on failure

Files

  • playwright.config.ts — setup project with storageState auth, chromium-only, global teardown
  • e2e/helpers/ — constants, seed script, cleanup script
  • e2e/pages/ — 5 page objects + 3 shared components (sidebar, toast, deploy dialog)
  • e2e/fixtures/test.fixture.ts — extended test with all page object fixtures
  • e2e/global-setup.ts — DB seed + UI login + storageState save
  • e2e/tests/ — auth, pipeline-crud, deploy, fleet, alerts specs
  • .github/workflows/e2e.yml — CI with Postgres service, wait-on, artifact upload
  • e2e/docker-compose.e2e.yml — local Postgres for development

Test plan

  • npx playwright test --list shows 15 tests across 6 files
  • npx tsc --project e2e/tsconfig.json --noEmit compiles cleanly
  • Run docker compose -f e2e/docker-compose.e2e.yml up -d + pnpm dev + pnpm test:e2e locally
  • CI workflow triggers on PR and runs E2E suite

TerrifiedBug and others added 9 commits April 1, 2026 15:53
- Add e2e/tsconfig.json with moduleResolution: node so Playwright can
  resolve the generated Prisma client and path aliases at runtime
- Fix cleanup.ts: prisma.alertDelivery → prisma.deliveryAttempt to match
  the actual Prisma schema (model is DeliveryAttempt, not AlertDelivery)
- src/generated/prisma must be generated via `npx prisma generate` before
  running E2E tests (added to CI workflow already)
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 1, 2026

Greptile Summary

This PR introduces a solid Playwright E2E test suite with Page Object Model architecture, storageState-based auth, a Prisma seed/cleanup harness, and a CI workflow. The overall structure is well-thought-out — the project dependency chain, sequential execution, shared fixtures, and manual FK-ordered cleanup are all correct. Two bugs in global-setup.ts need fixing before CI will be reliable; one is a P1 crash and one is a P1 silent failure.

Key findings:

  • P1 — waitForURL("**/*") silently accepts a failed login (e2e/global-setup.ts:36): "**/*" matches every URL, including /login itself. If the login fails for any reason (credentials mismatch, server not ready, NextAuth error), Playwright resolves immediately and saves an unauthenticated storage state. Every subsequent test then runs without auth, failing with confusing "redirect to login" errors rather than a clear setup error. Should be (url) => !url.pathname.includes("/login").

  • P1 — missing mkdir for e2e/.auth/ before fs.writeFile (e2e/global-setup.ts:15): e2e/.auth/ is .gitignored and won't exist on a fresh CI checkout. fs.writeFile does not create missing parent directories, so the seed-result write throws ENOENT and the entire setup crashes before any test can run. A fs.mkdir("e2e/.auth", { recursive: true }) call is needed first.

  • P2 — deploy badge test never calls expectDeploymentBadge (e2e/tests/deploy.spec.ts:26): The test is titled "should show deployment badge on pipeline list after deploy" but only calls expectPipelineInList — the badge-specific assertion method exists on the page object but is never invoked.

  • P2 — hardcoded credentials in auth.spec.ts instead of the TEST_USER constants already defined in e2e/helpers/constants.ts.

Confidence Score: 4/5

Two P1 bugs in global-setup.ts will reliably break CI; safe to merge once those two lines are fixed.

The two P1 issues in global-setup.ts are genuine, reproducible defects on the changed path: the ENOENT crash prevents CI from running at all on a fresh checkout, and the permissive waitForURL glob means a failed login silently poisons the auth state for the entire run. Both are one-or-two-line fixes. All remaining findings are P2 style/completeness issues that don't block the test suite from working once the P1s are resolved.

e2e/global-setup.ts requires two targeted fixes before the CI workflow will be reliable.

Important Files Changed

Filename Overview
e2e/global-setup.ts Two P1 bugs: waitForURL("**/*") silently accepts failed logins, and missing mkdir for e2e/.auth/ causes ENOENT on fresh CI checkouts
e2e/tests/deploy.spec.ts Second test asserts pipeline visibility but never calls expectDeploymentBadge, so the badge check stated in the test name is not actually performed
e2e/tests/auth.spec.ts Functional auth tests, but credentials are hardcoded as string literals instead of referencing TEST_USER constants — divergence risk if seed values change
e2e/helpers/seed.ts Complete seed covering all required entities (user, team, environment, pipeline with 3 nodes + edges + version, fleet node, alert rules and events); looks correct
e2e/helpers/cleanup.ts Manual ordered deletion handles all FK constraints correctly; no cascade issues observed
.github/workflows/e2e.yml Well-structured CI workflow with Postgres service, health checks, wait-on, and artifact upload on failure; no issues
playwright.config.ts Correct setup/chromium project dependency chain with storageState auth; single-worker sequential execution is appropriate for these tests
e2e/tests/pipeline-crud.spec.ts Tests are logically ordered (create → persist → delete), but all three nodes in the create test are dragged to the same canvas coordinates which may cause positioning issues

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    CI[CI Workflow / pnpm test:e2e] --> GS[global-setup.ts\nsetup project]
    GS --> DB[(Postgres\nService Container)]
    GS --> SEED[seed.ts\nCreate: user · team · env\npipeline · nodes · fleet node\nalert rules + events]
    SEED --> WRITE[fs.writeFile\ne2e/.auth/seed-result.json]
    GS --> LOGIN[UI Login\n/login → click Sign In]
    LOGIN --> WAIT["waitForURL(**/*)\n⚠ matches /login too"]
    WAIT --> AUTH[storageState → e2e/.auth/user.json]
    AUTH --> TESTS
    subgraph TESTS[Chromium Project — sequential, workers=1]
        direction TB
        A[alerts.spec.ts] --> B[auth.spec.ts]
        B --> C[deploy.spec.ts\nreads seed-result.json]
        C --> D[fleet.spec.ts]
        D --> E[pipeline-crud.spec.ts\ndeletes seeded pipeline]
    end
    TESTS --> GT[global-teardown.ts]
    GT --> CLEAN[cleanup.ts\ndelete all test data]
    CLEAN --> DB
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: e2e/global-setup.ts
Line: 36

Comment:
**`waitForURL("**/*")` matches the login page itself — failed auth is undetected**

`"**/*"` is a glob that matches every URL, including `http://localhost:3000/login`. Playwright's `waitForURL` resolves the moment the current URL matches the pattern. Because the page is already on `/login` when the click happens (and would stay there if credentials are wrong, the server returns an error, or NextAuth rejects the login), this line resolves immediately in those cases. The auth state is then saved to `e2e/.auth/user.json` with an unauthenticated session, causing every downstream test to fail with confusing "not authenticated" errors rather than a clear setup failure.

The fix is to wait for a URL that is specifically *not* the login page:

```suggestion
  await page.waitForURL((url) => !url.pathname.includes("/login"), { timeout: 15_000 });
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: e2e/global-setup.ts
Line: 14-20

Comment:
**`e2e/.auth/` directory is never created — CI will throw ENOENT**

`e2e/.auth/` is listed in `.gitignore`, so it won't exist on a fresh clone or CI checkout. `fs.writeFile("e2e/.auth/seed-result.json", ...)` does **not** create missing parent directories; it throws `ENOENT: no such file or directory`. This crashes the global-setup before any test runs.

Playwright's own `storageState({ path })` call (line 38) handles directory creation internally, so that write is safe — but the manual `writeFile` here is not.

Add an `mkdir` before the write:

```suggestion
    const fs = await import("fs/promises");
    await fs.mkdir("e2e/.auth", { recursive: true });
    await fs.writeFile(
      "e2e/.auth/seed-result.json",
      JSON.stringify(result, null, 2),
    );
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: e2e/tests/deploy.spec.ts
Line: 26-37

Comment:
**Test name promises a deployment badge check that isn't performed**

`"should show deployment badge on pipeline list after deploy"` only asserts that the pipeline name appears in the list (`expectPipelineInList`). It never calls `pipelinesPage.expectDeploymentBadge("E2E Test Pipeline")`, which is the method that actually checks the badge (verifying the row no longer says "Draft"). The test as written will pass even if the pipeline never deployed successfully — it's just a duplicate of the visibility check already covered by the pipeline-crud tests.

Call `expectDeploymentBadge` to make the assertion match the test's stated intent:
```typescript
  await pipelinesPage.expectPipelineInList("E2E Test Pipeline");
  await pipelinesPage.expectDeploymentBadge("E2E Test Pipeline");
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: e2e/tests/auth.spec.ts
Line: 14-16

Comment:
**Credentials hardcoded instead of using `TEST_USER` constants**

`"e2e@test.local"` and `"TestPassword123!"` are repeated inline here (and again on lines 27, 40–41) rather than using the `TEST_USER` constants already defined in `e2e/helpers/constants.ts`. If the seed values ever change, these tests would silently diverge from the seed data.

```suggestion
    await loginPage.login(TEST_USER.email, TEST_USER.password);
```

Import at the top of the file:
```typescript
import { TEST_USER } from "../helpers/constants";
```

This also applies to the other three credential literals in this file (lines 27, 40, 41).

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "Merge branch 'main' into feat/e2e-tests" | Re-trigger Greptile

Comment thread e2e/global-setup.ts
await page.getByRole("button", { name: /sign in/i }).click();

await page.waitForURL("**/*", { timeout: 15_000 });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 waitForURL("**/*") matches the login page itself — failed auth is undetected

"**/*" is a glob that matches every URL, including http://localhost:3000/login. Playwright's waitForURL resolves the moment the current URL matches the pattern. Because the page is already on /login when the click happens (and would stay there if credentials are wrong, the server returns an error, or NextAuth rejects the login), this line resolves immediately in those cases. The auth state is then saved to e2e/.auth/user.json with an unauthenticated session, causing every downstream test to fail with confusing "not authenticated" errors rather than a clear setup failure.

The fix is to wait for a URL that is specifically not the login page:

Suggested change
await page.waitForURL((url) => !url.pathname.includes("/login"), { timeout: 15_000 });
Prompt To Fix With AI
This is a comment left during a code review.
Path: e2e/global-setup.ts
Line: 36

Comment:
**`waitForURL("**/*")` matches the login page itself — failed auth is undetected**

`"**/*"` is a glob that matches every URL, including `http://localhost:3000/login`. Playwright's `waitForURL` resolves the moment the current URL matches the pattern. Because the page is already on `/login` when the click happens (and would stay there if credentials are wrong, the server returns an error, or NextAuth rejects the login), this line resolves immediately in those cases. The auth state is then saved to `e2e/.auth/user.json` with an unauthenticated session, causing every downstream test to fail with confusing "not authenticated" errors rather than a clear setup failure.

The fix is to wait for a URL that is specifically *not* the login page:

```suggestion
  await page.waitForURL((url) => !url.pathname.includes("/login"), { timeout: 15_000 });
```

How can I resolve this? If you propose a fix, please make it concise.

Comment thread e2e/global-setup.ts
Comment on lines +14 to +20
const result = await seed(prisma);

const fs = await import("fs/promises");
await fs.writeFile(
"e2e/.auth/seed-result.json",
JSON.stringify(result, null, 2),
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 e2e/.auth/ directory is never created — CI will throw ENOENT

e2e/.auth/ is listed in .gitignore, so it won't exist on a fresh clone or CI checkout. fs.writeFile("e2e/.auth/seed-result.json", ...) does not create missing parent directories; it throws ENOENT: no such file or directory. This crashes the global-setup before any test runs.

Playwright's own storageState({ path }) call (line 38) handles directory creation internally, so that write is safe — but the manual writeFile here is not.

Add an mkdir before the write:

Suggested change
const result = await seed(prisma);
const fs = await import("fs/promises");
await fs.writeFile(
"e2e/.auth/seed-result.json",
JSON.stringify(result, null, 2),
);
const fs = await import("fs/promises");
await fs.mkdir("e2e/.auth", { recursive: true });
await fs.writeFile(
"e2e/.auth/seed-result.json",
JSON.stringify(result, null, 2),
);
Prompt To Fix With AI
This is a comment left during a code review.
Path: e2e/global-setup.ts
Line: 14-20

Comment:
**`e2e/.auth/` directory is never created — CI will throw ENOENT**

`e2e/.auth/` is listed in `.gitignore`, so it won't exist on a fresh clone or CI checkout. `fs.writeFile("e2e/.auth/seed-result.json", ...)` does **not** create missing parent directories; it throws `ENOENT: no such file or directory`. This crashes the global-setup before any test runs.

Playwright's own `storageState({ path })` call (line 38) handles directory creation internally, so that write is safe — but the manual `writeFile` here is not.

Add an `mkdir` before the write:

```suggestion
    const fs = await import("fs/promises");
    await fs.mkdir("e2e/.auth", { recursive: true });
    await fs.writeFile(
      "e2e/.auth/seed-result.json",
      JSON.stringify(result, null, 2),
    );
```

How can I resolve this? If you propose a fix, please make it concise.

Comment thread e2e/tests/deploy.spec.ts
Comment on lines +26 to +37

test("should show deployment badge on pipeline list after deploy", async ({
page,
pipelinesPage,
sidebar,
}) => {
await sidebar.navigateTo("Pipelines");
await page.waitForLoadState("networkidle");

await pipelinesPage.expectPipelineInList("E2E Test Pipeline");
});
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Test name promises a deployment badge check that isn't performed

"should show deployment badge on pipeline list after deploy" only asserts that the pipeline name appears in the list (expectPipelineInList). It never calls pipelinesPage.expectDeploymentBadge("E2E Test Pipeline"), which is the method that actually checks the badge (verifying the row no longer says "Draft"). The test as written will pass even if the pipeline never deployed successfully — it's just a duplicate of the visibility check already covered by the pipeline-crud tests.

Call expectDeploymentBadge to make the assertion match the test's stated intent:

  await pipelinesPage.expectPipelineInList("E2E Test Pipeline");
  await pipelinesPage.expectDeploymentBadge("E2E Test Pipeline");
Prompt To Fix With AI
This is a comment left during a code review.
Path: e2e/tests/deploy.spec.ts
Line: 26-37

Comment:
**Test name promises a deployment badge check that isn't performed**

`"should show deployment badge on pipeline list after deploy"` only asserts that the pipeline name appears in the list (`expectPipelineInList`). It never calls `pipelinesPage.expectDeploymentBadge("E2E Test Pipeline")`, which is the method that actually checks the badge (verifying the row no longer says "Draft"). The test as written will pass even if the pipeline never deployed successfully — it's just a duplicate of the visibility check already covered by the pipeline-crud tests.

Call `expectDeploymentBadge` to make the assertion match the test's stated intent:
```typescript
  await pipelinesPage.expectPipelineInList("E2E Test Pipeline");
  await pipelinesPage.expectDeploymentBadge("E2E Test Pipeline");
```

How can I resolve this? If you propose a fix, please make it concise.

Comment thread e2e/tests/auth.spec.ts
Comment on lines +14 to +16
await loginPage.goto();
await loginPage.login("e2e@test.local", "TestPassword123!");
await loginPage.expectRedirectedToDashboard();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Credentials hardcoded instead of using TEST_USER constants

"e2e@test.local" and "TestPassword123!" are repeated inline here (and again on lines 27, 40–41) rather than using the TEST_USER constants already defined in e2e/helpers/constants.ts. If the seed values ever change, these tests would silently diverge from the seed data.

Suggested change
await loginPage.goto();
await loginPage.login("e2e@test.local", "TestPassword123!");
await loginPage.expectRedirectedToDashboard();
await loginPage.login(TEST_USER.email, TEST_USER.password);

Import at the top of the file:

import { TEST_USER } from "../helpers/constants";

This also applies to the other three credential literals in this file (lines 27, 40, 41).

Prompt To Fix With AI
This is a comment left during a code review.
Path: e2e/tests/auth.spec.ts
Line: 14-16

Comment:
**Credentials hardcoded instead of using `TEST_USER` constants**

`"e2e@test.local"` and `"TestPassword123!"` are repeated inline here (and again on lines 27, 40–41) rather than using the `TEST_USER` constants already defined in `e2e/helpers/constants.ts`. If the seed values ever change, these tests would silently diverge from the seed data.

```suggestion
    await loginPage.login(TEST_USER.email, TEST_USER.password);
```

Import at the top of the file:
```typescript
import { TEST_USER } from "../helpers/constants";
```

This also applies to the other three credential literals in this file (lines 27, 40, 41).

How can I resolve this? If you propose a fix, please make it concise.

- Fix react-hooks/rules-of-hooks false positive in Playwright fixtures
- Fix waitForURL glob matching login page (P1)
- Add mkdir for e2e/.auth directory on fresh clone (P1)
- Add expectDeploymentBadge call in deploy test (P2)
- Use TEST_USER constants instead of hardcoded credentials (P2)
- Remove unused imports (Locator, expect, page, toast)
@TerrifiedBug TerrifiedBug merged commit 92a7aa9 into main Apr 1, 2026
9 checks passed
@TerrifiedBug TerrifiedBug deleted the feat/e2e-tests branch April 1, 2026 15:43
TerrifiedBug added a commit that referenced this pull request Apr 1, 2026
* feat: install Playwright and configure E2E test infrastructure

* feat: add E2E test helpers — constants, seed, cleanup

* feat: add E2E page objects — login, pipelines, editor, fleet, alerts, shared components

* feat: add Playwright fixtures, global setup with auth, and teardown

* feat: add 5 E2E test specs — auth, pipeline CRUD, deploy, fleet, alerts

* feat: add E2E CI workflow and Docker Compose for local Postgres

* chore: add wait-on for E2E CI server readiness check

* fix: resolve E2E setup issues found during verification

- Add e2e/tsconfig.json with moduleResolution: node so Playwright can
  resolve the generated Prisma client and path aliases at runtime
- Fix cleanup.ts: prisma.alertDelivery → prisma.deliveryAttempt to match
  the actual Prisma schema (model is DeliveryAttempt, not AlertDelivery)
- src/generated/prisma must be generated via `npx prisma generate` before
  running E2E tests (added to CI workflow already)

* chore: run E2E tests only on release tags and manual dispatch

* fix: resolve CI lint errors and Greptile P1/P2 findings

- Fix react-hooks/rules-of-hooks false positive in Playwright fixtures
- Fix waitForURL glob matching login page (P1)
- Add mkdir for e2e/.auth directory on fresh clone (P1)
- Add expectDeploymentBadge call in deploy test (P2)
- Use TEST_USER constants instead of hardcoded credentials (P2)
- Remove unused imports (Locator, expect, page, toast)

* fix: restore page param in pipeline delete test
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant