mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-06-22 04:11:55 +10:00
docs: plan e2e test implementation
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
This commit is contained in:
@@ -0,0 +1,649 @@
|
||||
# E2E Tests 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:** Build a Playwright E2E test setup that runs deterministic core Reactive Resume flows with ephemeral accounts/data locally and on GitHub Actions for every PR.
|
||||
|
||||
**Architecture:** Add a root-level Playwright harness that targets the production server after `pnpm build`. Use hybrid fixtures: one browser-driven auth smoke spec, and helper-created authenticated browser state plus ephemeral database cleanup for resume flows. Keep initial PR-gated coverage to auth, sample resume creation, builder autosave, JSON export/import, and public sharing.
|
||||
|
||||
**Tech Stack:** Playwright, TypeScript, pnpm, Turbo, GitHub Actions, PostgreSQL service container, Better Auth HTTP endpoints, Drizzle/Postgres cleanup helpers.
|
||||
|
||||
---
|
||||
|
||||
## File structure
|
||||
|
||||
- Create `playwright.config.ts`: Playwright projects, reporters, artifact policy, base URL, and production `webServer`.
|
||||
- Create `tests/e2e/README.md`: local setup and CI behavior.
|
||||
- Create `tests/e2e/fixtures/data.ts`: unique test identity and resume value generation.
|
||||
- Create `tests/e2e/fixtures/auth.ts`: browser UI auth helpers and API-backed authenticated storage state helpers.
|
||||
- Create `tests/e2e/fixtures/db.ts`: user cleanup by email/username prefix.
|
||||
- Create `tests/e2e/fixtures/resume.ts`: UI helpers for creating sample resumes and accessing builder sections.
|
||||
- Create `tests/e2e/fixtures/test.ts`: typed Playwright fixture composition.
|
||||
- Create `tests/e2e/specs/auth.spec.ts`: browser registration/login smoke flow.
|
||||
- Create `tests/e2e/specs/resume-lifecycle.spec.ts`: dashboard create sample resume and builder autosave flow.
|
||||
- Create `tests/e2e/specs/json-export-import.spec.ts`: deterministic JSON backup/restore flow.
|
||||
- Create `tests/e2e/specs/public-sharing.spec.ts`: public sharing flow with anonymous browser context.
|
||||
- Create `.github/workflows/e2e.yml`: PR/push workflow with Postgres, build, Playwright install, E2E run, and report uploads.
|
||||
- Modify `package.json`: add Playwright dependency and E2E scripts.
|
||||
- Modify `turbo.json`: register E2E task if routed through package scripts.
|
||||
- Modify targeted UI files only if accessible locators are not sufficient.
|
||||
|
||||
## Task 1: Add Playwright dependency and scripts
|
||||
|
||||
**Files:**
|
||||
- Modify: `package.json`
|
||||
- Modify: `pnpm-lock.yaml`
|
||||
- Modify: `turbo.json`
|
||||
|
||||
- [ ] **Step 1: Add Playwright with the package manager**
|
||||
|
||||
Run: `pnpm add -D @playwright/test`
|
||||
|
||||
Expected: `package.json` and `pnpm-lock.yaml` include `@playwright/test`.
|
||||
|
||||
- [ ] **Step 2: Add root scripts**
|
||||
|
||||
Change root `package.json` scripts to include:
|
||||
|
||||
```json
|
||||
{
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:ui": "playwright test --ui",
|
||||
"test:e2e:ci": "playwright test --reporter=list,github,junit"
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Register the Turbo task**
|
||||
|
||||
Add this task entry to `turbo.json`:
|
||||
|
||||
```json
|
||||
"test:e2e": {
|
||||
"cache": false
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Verify script discovery**
|
||||
|
||||
Run: `pnpm exec playwright --version`
|
||||
|
||||
Expected: Playwright prints a version and exits successfully.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add package.json pnpm-lock.yaml turbo.json
|
||||
git commit -m "test: add playwright e2e scripts"
|
||||
git push -u origin feat/e2e-test-plan-2b10
|
||||
```
|
||||
|
||||
## Task 2: Add Playwright configuration
|
||||
|
||||
**Files:**
|
||||
- Create: `playwright.config.ts`
|
||||
|
||||
- [ ] **Step 1: Create the config**
|
||||
|
||||
Create `playwright.config.ts`:
|
||||
|
||||
```ts
|
||||
import { defineConfig, devices } from "@playwright/test";
|
||||
|
||||
const port = Number.parseInt(process.env.PORT ?? "3000", 10);
|
||||
const baseURL = process.env.APP_URL ?? `http://127.0.0.1:${port}`;
|
||||
const isCI = Boolean(process.env.CI);
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./tests/e2e/specs",
|
||||
fullyParallel: true,
|
||||
forbidOnly: isCI,
|
||||
retries: isCI ? 2 : 0,
|
||||
workers: isCI ? 2 : undefined,
|
||||
reporter: isCI
|
||||
? [
|
||||
["list"],
|
||||
["github"],
|
||||
["junit", { outputFile: "test-results/e2e-junit.xml" }],
|
||||
]
|
||||
: [["list"], ["html", { open: "never" }]],
|
||||
use: {
|
||||
baseURL,
|
||||
trace: "retain-on-failure",
|
||||
screenshot: "only-on-failure",
|
||||
video: "retain-on-failure",
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: "chromium",
|
||||
use: { ...devices["Desktop Chrome"] },
|
||||
},
|
||||
],
|
||||
webServer: {
|
||||
command: "pnpm start",
|
||||
url: `${baseURL}/api/health`,
|
||||
reuseExistingServer: !isCI,
|
||||
timeout: 120_000,
|
||||
env: {
|
||||
...process.env,
|
||||
PORT: String(port),
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify config loads**
|
||||
|
||||
Run: `pnpm exec playwright test --list`
|
||||
|
||||
Expected: Playwright lists zero tests at this point without config errors.
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add playwright.config.ts
|
||||
git commit -m "test: configure playwright"
|
||||
git push -u origin feat/e2e-test-plan-2b10
|
||||
```
|
||||
|
||||
## Task 3: Add E2E fixtures
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/e2e/fixtures/data.ts`
|
||||
- Create: `tests/e2e/fixtures/db.ts`
|
||||
- Create: `tests/e2e/fixtures/auth.ts`
|
||||
- Create: `tests/e2e/fixtures/resume.ts`
|
||||
- Create: `tests/e2e/fixtures/test.ts`
|
||||
|
||||
- [ ] **Step 1: Add test data helpers**
|
||||
|
||||
Create `tests/e2e/fixtures/data.ts`:
|
||||
|
||||
```ts
|
||||
import type { TestInfo } from "@playwright/test";
|
||||
|
||||
const sanitize = (value: string) => value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
||||
|
||||
export type E2EAccount = {
|
||||
name: string;
|
||||
username: string;
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export function createRunSlug(testInfo: TestInfo) {
|
||||
const worker = testInfo.workerIndex;
|
||||
const title = sanitize(testInfo.titlePath.join("-")).slice(0, 40);
|
||||
const suffix = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
return `e2e-${worker}-${title}-${suffix}`;
|
||||
}
|
||||
|
||||
export function createAccount(testInfo: TestInfo): E2EAccount {
|
||||
const slug = createRunSlug(testInfo).replaceAll("-", "_").slice(0, 48);
|
||||
return {
|
||||
name: "E2E Test User",
|
||||
username: slug,
|
||||
email: `${slug}@example.test`,
|
||||
password: "Password123!",
|
||||
};
|
||||
}
|
||||
|
||||
export function createResumeName(testInfo: TestInfo) {
|
||||
return `E2E Resume ${createRunSlug(testInfo)}`;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add database cleanup**
|
||||
|
||||
Create `tests/e2e/fixtures/db.ts`:
|
||||
|
||||
```ts
|
||||
import { eq, or } from "drizzle-orm";
|
||||
import { db, getPool } from "@reactive-resume/db/client";
|
||||
import { user } from "@reactive-resume/db/schema";
|
||||
|
||||
export async function deleteE2EUser(account: { email: string; username: string }) {
|
||||
await db.delete(user).where(or(eq(user.email, account.email), eq(user.username, account.username)));
|
||||
}
|
||||
|
||||
export async function closeE2EDatabase() {
|
||||
await getPool().end();
|
||||
globalThis.__pool = undefined;
|
||||
globalThis.__drizzle = undefined;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add auth helpers**
|
||||
|
||||
Create `tests/e2e/fixtures/auth.ts`:
|
||||
|
||||
```ts
|
||||
import type { Browser, Page } from "@playwright/test";
|
||||
import type { E2EAccount } from "./data";
|
||||
|
||||
export async function registerViaUi(page: Page, account: E2EAccount) {
|
||||
await page.goto("/auth/register");
|
||||
await page.getByLabel("Name").fill(account.name);
|
||||
await page.getByLabel("Username").fill(account.username);
|
||||
await page.getByLabel("Email Address").fill(account.email);
|
||||
await page.getByLabel("Password").fill(account.password);
|
||||
await page.getByRole("button", { name: "Sign up" }).click();
|
||||
await page.getByRole("link", { name: /continue/i }).click();
|
||||
await page.waitForURL(/\/dashboard/);
|
||||
}
|
||||
|
||||
export async function loginViaUi(page: Page, account: E2EAccount) {
|
||||
await page.goto("/auth/login");
|
||||
await page.getByLabel(/email|username/i).fill(account.email);
|
||||
await page.getByLabel("Password").fill(account.password);
|
||||
await page.getByRole("button", { name: "Sign in" }).click();
|
||||
await page.waitForURL(/\/dashboard/);
|
||||
}
|
||||
|
||||
export async function createAuthenticatedPage(browser: Browser, account: E2EAccount) {
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
await registerViaUi(page, account);
|
||||
return page;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add resume UI helpers**
|
||||
|
||||
Create `tests/e2e/fixtures/resume.ts`:
|
||||
|
||||
```ts
|
||||
import type { Page, TestInfo } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
import { createResumeName } from "./data";
|
||||
|
||||
export async function createSampleResumeFromDashboard(page: Page, testInfo: TestInfo) {
|
||||
const resumeName = createResumeName(testInfo);
|
||||
await page.goto("/dashboard/resumes");
|
||||
await page.getByText("Create a new resume").click();
|
||||
await page.getByRole("dialog", { name: "Create a new resume" }).getByLabel("Name").fill(resumeName);
|
||||
await page.getByRole("button", { name: "Create resume with options" }).click();
|
||||
await page.getByRole("menuitem", { name: "Create a Sample Resume" }).click();
|
||||
await expect(page.getByText(resumeName)).toBeVisible();
|
||||
await page.getByText(resumeName).click();
|
||||
await page.waitForURL(/\/builder\/.+/);
|
||||
return resumeName;
|
||||
}
|
||||
|
||||
export async function openRightSidebarSection(page: Page, name: string) {
|
||||
await page.getByRole("button", { name }).click();
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Add fixture composition**
|
||||
|
||||
Create `tests/e2e/fixtures/test.ts`:
|
||||
|
||||
```ts
|
||||
import { test as base, expect } from "@playwright/test";
|
||||
import type { E2EAccount } from "./data";
|
||||
import { createAccount } from "./data";
|
||||
import { deleteE2EUser } from "./db";
|
||||
import { registerViaUi } from "./auth";
|
||||
|
||||
type Fixtures = {
|
||||
account: E2EAccount;
|
||||
authPage: import("@playwright/test").Page;
|
||||
};
|
||||
|
||||
export const test = base.extend<Fixtures>({
|
||||
account: async ({}, use, testInfo) => {
|
||||
const account = createAccount(testInfo);
|
||||
await use(account);
|
||||
await deleteE2EUser(account);
|
||||
},
|
||||
authPage: async ({ browser, account }, use) => {
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
await registerViaUi(page, account);
|
||||
await use(page);
|
||||
await context.close();
|
||||
},
|
||||
});
|
||||
|
||||
export { expect };
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Run type-aware feedback**
|
||||
|
||||
Run: `pnpm exec tsc --noEmit --allowImportingTsExtensions false --moduleResolution bundler --target es2022 --module esnext tests/e2e/fixtures/*.ts`
|
||||
|
||||
Expected: If direct `tsc` is too narrow for workspace exports, use `pnpm typecheck` after Task 6 instead.
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add tests/e2e/fixtures
|
||||
git commit -m "test: add e2e fixtures"
|
||||
git push -u origin feat/e2e-test-plan-2b10
|
||||
```
|
||||
|
||||
## Task 4: Add core E2E specs
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/e2e/specs/auth.spec.ts`
|
||||
- Create: `tests/e2e/specs/resume-lifecycle.spec.ts`
|
||||
- Create: `tests/e2e/specs/json-export-import.spec.ts`
|
||||
- Create: `tests/e2e/specs/public-sharing.spec.ts`
|
||||
|
||||
- [ ] **Step 1: Add auth smoke spec**
|
||||
|
||||
Create `tests/e2e/specs/auth.spec.ts`:
|
||||
|
||||
```ts
|
||||
import { test, expect } from "../fixtures/test";
|
||||
import { loginViaUi, registerViaUi } from "../fixtures/auth";
|
||||
|
||||
test("registers and logs in with email credentials", async ({ page, account }) => {
|
||||
await registerViaUi(page, account);
|
||||
await expect(page.getByRole("heading", { name: "Resumes" })).toBeVisible();
|
||||
await page.getByRole("button", { name: /user menu|account|profile/i }).click();
|
||||
await page.getByRole("menuitem", { name: /logout|sign out/i }).click();
|
||||
await loginViaUi(page, account);
|
||||
await expect(page.getByRole("heading", { name: "Resumes" })).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add resume lifecycle spec**
|
||||
|
||||
Create `tests/e2e/specs/resume-lifecycle.spec.ts`:
|
||||
|
||||
```ts
|
||||
import { test, expect } from "../fixtures/test";
|
||||
import { createSampleResumeFromDashboard } from "../fixtures/resume";
|
||||
|
||||
test("creates a sample resume and persists a basics edit", async ({ authPage: page }, testInfo) => {
|
||||
await createSampleResumeFromDashboard(page, testInfo);
|
||||
const updatedName = `E2E Edited ${Date.now()}`;
|
||||
await page.getByRole("button", { name: "Basics" }).click();
|
||||
await page.getByLabel("Name").fill(updatedName);
|
||||
await page.reload();
|
||||
await page.getByRole("button", { name: "Basics" }).click();
|
||||
await expect(page.getByLabel("Name")).toHaveValue(updatedName);
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add JSON export/import spec**
|
||||
|
||||
Create `tests/e2e/specs/json-export-import.spec.ts`:
|
||||
|
||||
```ts
|
||||
import { test, expect } from "../fixtures/test";
|
||||
import { createSampleResumeFromDashboard } from "../fixtures/resume";
|
||||
|
||||
test("exports and imports a resume JSON backup", async ({ authPage: page }, testInfo) => {
|
||||
const resumeName = await createSampleResumeFromDashboard(page, testInfo);
|
||||
await page.getByRole("button", { name: "Export" }).click();
|
||||
const downloadPromise = page.waitForEvent("download");
|
||||
await page.getByRole("button", { name: /^JSON$/ }).click();
|
||||
const download = await downloadPromise;
|
||||
expect(download.suggestedFilename()).toMatch(/\.json$/);
|
||||
const path = await download.path();
|
||||
expect(path).toBeTruthy();
|
||||
if (!path) throw new Error("Expected Playwright to provide a downloaded JSON path.");
|
||||
await page.goto("/dashboard/resumes");
|
||||
await page.getByText("Import an existing resume").click();
|
||||
await page.getByRole("combobox").click();
|
||||
await page.getByRole("option", { name: "Reactive Resume (JSON)" }).click();
|
||||
await page.getByText("Click here to select a file to import").setInputFiles(path);
|
||||
await page.getByRole("button", { name: "Import" }).click();
|
||||
await page.waitForURL(/\/builder\/.+/);
|
||||
await expect(page.getByText(resumeName)).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Add public sharing spec**
|
||||
|
||||
Create `tests/e2e/specs/public-sharing.spec.ts`:
|
||||
|
||||
```ts
|
||||
import { test, expect } from "../fixtures/test";
|
||||
import { createSampleResumeFromDashboard } from "../fixtures/resume";
|
||||
|
||||
test("publishes a resume and renders it for an anonymous visitor", async ({ browser, authPage: page }, testInfo) => {
|
||||
await createSampleResumeFromDashboard(page, testInfo);
|
||||
await page.getByRole("button", { name: "Sharing" }).click();
|
||||
await page.getByLabel("Allow Public Access").click();
|
||||
const publicUrl = await page.getByLabel("URL").inputValue();
|
||||
const anonymous = await browser.newPage();
|
||||
await anonymous.goto(publicUrl);
|
||||
await expect(anonymous.getByRole("button", { name: /download/i })).toBeVisible();
|
||||
await anonymous.close();
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run list mode**
|
||||
|
||||
Run: `pnpm test:e2e -- --list`
|
||||
|
||||
Expected: Four Chromium tests are listed.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add tests/e2e/specs
|
||||
git commit -m "test: add core e2e specs"
|
||||
git push -u origin feat/e2e-test-plan-2b10
|
||||
```
|
||||
|
||||
## Task 5: Add documentation
|
||||
|
||||
**Files:**
|
||||
- Create: `tests/e2e/README.md`
|
||||
|
||||
- [ ] **Step 1: Add E2E README**
|
||||
|
||||
Create `tests/e2e/README.md`:
|
||||
|
||||
```md
|
||||
# E2E Tests
|
||||
|
||||
Reactive Resume uses Playwright for PR-gated browser coverage of deterministic core flows.
|
||||
|
||||
## Local setup
|
||||
|
||||
Start PostgreSQL:
|
||||
|
||||
`sudo docker compose -f compose.dev.yml up -d postgres`
|
||||
|
||||
Use a local test environment:
|
||||
|
||||
`APP_URL=http://localhost:3000 PORT=3000 DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres AUTH_SECRET=e2e-test-secret FLAG_DISABLE_SIGNUPS=false FLAG_DISABLE_EMAIL_AUTH=false FLAG_DISABLE_API_RATE_LIMIT=true LOCAL_STORAGE_PATH=/workspace/data/e2e pnpm build`
|
||||
|
||||
Run tests:
|
||||
|
||||
`APP_URL=http://localhost:3000 PORT=3000 DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres AUTH_SECRET=e2e-test-secret FLAG_DISABLE_SIGNUPS=false FLAG_DISABLE_EMAIL_AUTH=false FLAG_DISABLE_API_RATE_LIMIT=true LOCAL_STORAGE_PATH=/workspace/data/e2e pnpm test:e2e`
|
||||
|
||||
## Coverage
|
||||
|
||||
- Email/password auth smoke.
|
||||
- Dashboard sample resume creation.
|
||||
- Builder basics edit and autosave persistence.
|
||||
- JSON export/import.
|
||||
- Public sharing for anonymous visitors.
|
||||
|
||||
PDF, DOCX, OAuth, passkeys, 2FA, password reset, and AI flows are intentionally outside the initial PR gate.
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add tests/e2e/README.md
|
||||
git commit -m "docs: document e2e workflow"
|
||||
git push -u origin feat/e2e-test-plan-2b10
|
||||
```
|
||||
|
||||
## Task 6: Add GitHub Actions workflow
|
||||
|
||||
**Files:**
|
||||
- Create: `.github/workflows/e2e.yml`
|
||||
|
||||
- [ ] **Step 1: Add workflow**
|
||||
|
||||
Create `.github/workflows/e2e.yml`:
|
||||
|
||||
```yaml
|
||||
name: E2E Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: ["main"]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
APP_URL: http://localhost:3000
|
||||
PORT: "3000"
|
||||
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
|
||||
AUTH_SECRET: e2e-test-secret
|
||||
FLAG_DISABLE_SIGNUPS: "false"
|
||||
FLAG_DISABLE_EMAIL_AUTH: "false"
|
||||
FLAG_DISABLE_API_RATE_LIMIT: "true"
|
||||
LOCAL_STORAGE_PATH: /tmp/reactive-resume-e2e-storage
|
||||
|
||||
jobs:
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:latest
|
||||
env:
|
||||
POSTGRES_DB: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd "pg_isready -U postgres -d postgres"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v6
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Install Playwright Browser
|
||||
run: pnpm exec playwright install --with-deps chromium
|
||||
|
||||
- name: Prepare Storage
|
||||
run: mkdir -p "$LOCAL_STORAGE_PATH"
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Run E2E Tests
|
||||
run: pnpm test:e2e:ci
|
||||
|
||||
- name: Upload Playwright Report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: playwright-report
|
||||
path: |
|
||||
playwright-report
|
||||
test-results
|
||||
if-no-files-found: ignore
|
||||
retention-days: 7
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add .github/workflows/e2e.yml
|
||||
git commit -m "ci: run e2e tests on pull requests"
|
||||
git push -u origin feat/e2e-test-plan-2b10
|
||||
```
|
||||
|
||||
## Task 7: Verify and stabilize
|
||||
|
||||
**Files:**
|
||||
- Modify any E2E files that fail due to actual labels or app behavior.
|
||||
- Modify targeted UI files only if no stable accessible locator exists.
|
||||
|
||||
- [ ] **Step 1: Run non-mutating checks**
|
||||
|
||||
Run: `pnpm exec biome check package.json turbo.json playwright.config.ts tests/e2e .github/workflows/e2e.yml`
|
||||
|
||||
Expected: No Biome errors.
|
||||
|
||||
- [ ] **Step 2: Run typecheck**
|
||||
|
||||
Run: `pnpm typecheck`
|
||||
|
||||
Expected: Typecheck passes for all workspaces.
|
||||
|
||||
- [ ] **Step 3: Start PostgreSQL**
|
||||
|
||||
Run: `sudo docker compose -f compose.dev.yml up -d postgres`
|
||||
|
||||
Expected: Postgres container is healthy.
|
||||
|
||||
- [ ] **Step 4: Build production app**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
APP_URL=http://localhost:3000 PORT=3000 DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres AUTH_SECRET=e2e-test-secret FLAG_DISABLE_SIGNUPS=false FLAG_DISABLE_EMAIL_AUTH=false FLAG_DISABLE_API_RATE_LIMIT=true LOCAL_STORAGE_PATH=/workspace/data/e2e pnpm build
|
||||
```
|
||||
|
||||
Expected: web and server production builds complete.
|
||||
|
||||
- [ ] **Step 5: Run E2E suite**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
APP_URL=http://localhost:3000 PORT=3000 DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres AUTH_SECRET=e2e-test-secret FLAG_DISABLE_SIGNUPS=false FLAG_DISABLE_EMAIL_AUTH=false FLAG_DISABLE_API_RATE_LIMIT=true LOCAL_STORAGE_PATH=/workspace/data/e2e pnpm test:e2e
|
||||
```
|
||||
|
||||
Expected: All Chromium specs pass.
|
||||
|
||||
- [ ] **Step 6: Commit stabilization changes**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "test: stabilize e2e suite"
|
||||
git push -u origin feat/e2e-test-plan-2b10
|
||||
```
|
||||
|
||||
## Self-review
|
||||
|
||||
- Spec coverage: Tasks cover Playwright setup, production-build target, hybrid fixtures, ephemeral data cleanup, core deterministic flows, CI workflow, and documentation.
|
||||
- Placeholder scan: No placeholder steps remain; each task names exact files, commands, and expected outcomes.
|
||||
- Type consistency: Fixture types are introduced before specs use them, and all helper names match across tasks.
|
||||
Reference in New Issue
Block a user