From 35da2fef1951e1d54975ae9ca3104c6b3c366d9e Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Sun, 22 Mar 2026 11:20:32 +0000 Subject: [PATCH 01/10] [#247] E2E smoke tests with Playwright Setup: - Install @playwright/test, Chromium browser - Create playwright.config.ts (localhost:3000, webServer npm run dev, Chromium only) - Add test:e2e script to package.json - Add e2e CI job (install Chromium, build, run tests) E2E test flows (11 tests): - Home page: grid renders, FilterBar visible, dropdowns work, sort dropdown shows options, genre dropdown shows options - Story detail: navigates from home, no console errors - Create page: form/connect prompt renders, graceful no-wallet state - Navigation: logo links home, Create link works, footer renders All tests pass without wallet connection or on-chain transactions. Unit tests (63) still pass. Fixes #247 Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/ci.yml | 13 ++++++++ e2e/create.spec.ts | 27 +++++++++++++++++ e2e/home.spec.ts | 60 +++++++++++++++++++++++++++++++++++++ e2e/navigation.spec.ts | 35 ++++++++++++++++++++++ e2e/story-detail.spec.ts | 48 ++++++++++++++++++++++++++++++ package-lock.json | 64 ++++++++++++++++++++++++++++++++++++++++ package.json | 4 ++- playwright.config.ts | 26 ++++++++++++++++ 8 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 e2e/create.spec.ts create mode 100644 e2e/home.spec.ts create mode 100644 e2e/navigation.spec.ts create mode 100644 e2e/story-detail.spec.ts create mode 100644 playwright.config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a0fe84b7..c7a78888 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,3 +18,16 @@ jobs: - run: npm run lint - run: npm run typecheck - run: npm test + + e2e: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + - run: npm ci + - run: npx playwright install --with-deps chromium + - run: npm run build + - run: npm run test:e2e diff --git a/e2e/create.spec.ts b/e2e/create.spec.ts new file mode 100644 index 00000000..2c36394e --- /dev/null +++ b/e2e/create.spec.ts @@ -0,0 +1,27 @@ +import { test, expect } from "@playwright/test"; + +test.describe("Create Storyline Page", () => { + test("form renders with all fields", async ({ page }) => { + await page.goto("/create"); + // Form should render with input fields + await expect(page.locator("body")).toBeVisible(); + + // Check for title input + const titleInput = page.getByPlaceholder(/title/i).or(page.locator("input[name='title']")); + // The page may require wallet connection — if so, it shows a connect prompt + // Accept either form fields or a connect prompt + const hasForm = await titleInput.isVisible().catch(() => false); + const hasConnectPrompt = await page.getByText(/connect/i).first().isVisible().catch(() => false); + + expect(hasForm || hasConnectPrompt).toBe(true); + }); + + test("wallet-not-connected state handled gracefully", async ({ page }) => { + await page.goto("/create"); + // Page should not crash — should either show form or a connect wallet prompt + await expect(page.locator("body")).toBeVisible(); + // No unhandled error overlay + const errorOverlay = page.locator("#__next-build-error, [data-nextjs-dialog]"); + await expect(errorOverlay).not.toBeVisible(); + }); +}); diff --git a/e2e/home.spec.ts b/e2e/home.spec.ts new file mode 100644 index 00000000..f82c165c --- /dev/null +++ b/e2e/home.spec.ts @@ -0,0 +1,60 @@ +import { test, expect } from "@playwright/test"; + +test.describe("Home Page", () => { + test("page loads and story grid renders", async ({ page }) => { + await page.goto("/"); + await expect(page).toHaveTitle(/PlotLink/i); + const grid = page.locator(".grid"); + await expect(grid.first()).toBeVisible({ timeout: 15000 }); + }); + + test("FilterBar is visible and dropdowns work", async ({ page }) => { + // Use desktop viewport so full labels show + await page.setViewportSize({ width: 1280, height: 720 }); + await page.goto("/"); + + // FilterBar should be visible + const filterBar = page.locator("div").filter({ hasText: /writer:/ }).first(); + await expect(filterBar).toBeVisible({ timeout: 10000 }); + + // Click writer filter to open dropdown + const writerButton = page.locator("button").filter({ hasText: /writer:/ }).first(); + await writerButton.click(); + + // Dropdown should show options + const dropdown = page.locator("[class*='absolute']").filter({ hasText: "Human" }); + await expect(dropdown.first()).toBeVisible(); + + // Close by clicking body + await page.locator("h1, h2, header").first().click(); + }); + + test("sort dropdown shows Recent and Trending options", async ({ page }) => { + await page.setViewportSize({ width: 1280, height: 720 }); + await page.goto("/"); + + // Click sort filter + const sortButton = page.locator("button").filter({ hasText: /sort:/ }).first(); + await expect(sortButton).toBeVisible({ timeout: 10000 }); + await sortButton.click(); + + // Dropdown should show both options + const recentOption = page.locator("[class*='absolute'] button").filter({ hasText: "Recent" }); + const trendingOption = page.locator("[class*='absolute'] button").filter({ hasText: "Trending" }); + await expect(recentOption.first()).toBeVisible({ timeout: 3000 }); + await expect(trendingOption.first()).toBeVisible(); + }); + + test("genre filter opens dropdown with options", async ({ page }) => { + await page.setViewportSize({ width: 1280, height: 720 }); + await page.goto("/"); + + const genreButton = page.locator("button").filter({ hasText: /genre:/ }).first(); + await expect(genreButton).toBeVisible({ timeout: 10000 }); + await genreButton.click(); + + // Dropdown should show "All genres" option + const allGenresOption = page.locator("button").filter({ hasText: "All genres" }); + await expect(allGenresOption.first()).toBeVisible({ timeout: 3000 }); + }); +}); diff --git a/e2e/navigation.spec.ts b/e2e/navigation.spec.ts new file mode 100644 index 00000000..5104d0f4 --- /dev/null +++ b/e2e/navigation.spec.ts @@ -0,0 +1,35 @@ +import { test, expect } from "@playwright/test"; + +test.describe("Navigation", () => { + test("NavBar logo links to home", async ({ page }) => { + await page.goto("/create"); + const logo = page.getByText("PlotLink").first(); + await expect(logo).toBeVisible({ timeout: 10000 }); + await logo.click(); + await expect(page).toHaveURL("/"); + }); + + test("NavBar Create link navigates to /create", async ({ page }) => { + await page.goto("/"); + // Find Create link in desktop nav (hidden on mobile, visible md+) + const createLink = page.locator("a[href='/create']").first(); + await expect(createLink).toBeVisible({ timeout: 10000 }); + await createLink.click(); + await expect(page).toHaveURL("/create"); + }); + + test("Footer renders", async ({ page }) => { + await page.goto("/"); + // Wait for page to load + await page.waitForTimeout(2000); + // Footer should be present in the DOM + const footer = page.locator("footer"); + if (await footer.count() > 0) { + await expect(footer.first()).toBeVisible(); + } else { + // Some layouts may not have a