From 60db8bd9deed28230ac0fdbae43243abb64b417b Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Fri, 3 Apr 2026 16:14:07 +0200 Subject: [PATCH] test: add E2E tests for dashboard and collection views Covers dashboard card rendering (item count, nav links, active thread/setup counts) and collection page (gear display, search, category filter, tab switching). Updates playwright config to serve production build with pre-seeded test DB. Co-Authored-By: Claude Sonnet 4.6 --- e2e/collection.spec.ts | 89 ++++++++++++++++++++++++++++++++++++++++ e2e/dashboard.spec.ts | 56 +++++++++++++++++++++++++ e2e/start-test-server.sh | 4 ++ playwright.config.ts | 5 +-- 4 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 e2e/collection.spec.ts create mode 100644 e2e/dashboard.spec.ts create mode 100755 e2e/start-test-server.sh diff --git a/e2e/collection.spec.ts b/e2e/collection.spec.ts new file mode 100644 index 0000000..831275a --- /dev/null +++ b/e2e/collection.spec.ts @@ -0,0 +1,89 @@ +import { expect, test } from "@playwright/test"; + +test.describe("Collection page", () => { + test.beforeEach(async ({ page }) => { + await page.goto("/collection"); + await page.waitForLoadState("networkidle"); + }); + + test.describe("Gear tab", () => { + test("shows seeded items", async ({ page }) => { + await expect(page.getByText("Zpacks Duplex")).toBeVisible(); + await expect(page.getByText("BRS-3000T Stove")).toBeVisible(); + }); + + test("search filters items by name", async ({ page }) => { + const searchInput = page.getByPlaceholder("Search items..."); + await searchInput.fill("Zpacks"); + await expect(page.getByText("Zpacks Duplex")).toBeVisible(); + // Other items should not be visible + await expect(page.getByText("BRS-3000T Stove")).not.toBeVisible(); + }); + + test("clearing search restores all items", async ({ page }) => { + const searchInput = page.getByPlaceholder("Search items..."); + await searchInput.fill("Zpacks"); + await expect(page.getByText("BRS-3000T Stove")).not.toBeVisible(); + // Clear the search + await searchInput.clear(); + await expect(page.getByText("BRS-3000T Stove")).toBeVisible(); + }); + + test("category filter dropdown opens and lists categories", async ({ + page, + }) => { + const filterButton = page.getByRole("button", { + name: /all categories/i, + }); + await filterButton.click(); + + // Dropdown list (ul) contains the category options + const dropdown = page.locator("ul"); + await expect( + dropdown.getByRole("button", { name: "Shelter" }), + ).toBeVisible(); + await expect( + dropdown.getByRole("button", { name: "Cook Kit" }), + ).toBeVisible(); + }); + + test("category filter shows only items in selected category", async ({ + page, + }) => { + // Open filter dropdown + const filterButton = page.getByRole("button", { + name: /all categories/i, + }); + await filterButton.click(); + + // Select "Shelter" from the dropdown list + const dropdown = page.locator("ul"); + await dropdown.getByRole("button", { name: "Shelter" }).click(); + + await expect(page.getByText("Zpacks Duplex")).toBeVisible(); + // Items from other categories should not be visible + await expect(page.getByText("BRS-3000T Stove")).not.toBeVisible(); + }); + }); + + test.describe("Tab switching", () => { + test("navigates to planning tab", async ({ page }) => { + await page.goto("/collection?tab=planning"); + await page.waitForLoadState("networkidle"); + // Planning tab shows the active thread + await expect(page.getByText("New Backpack")).toBeVisible(); + }); + + test("navigates to setups tab", async ({ page }) => { + await page.goto("/collection?tab=setups"); + await page.waitForLoadState("networkidle"); + // Setups tab shows the seeded setup + await expect(page.getByText("Weekend Overnighter")).toBeVisible(); + }); + + test("gear tab is default and shows items", async ({ page }) => { + // Default tab (no ?tab param) shows gear + await expect(page.getByText("Zpacks Duplex")).toBeVisible(); + }); + }); +}); diff --git a/e2e/dashboard.spec.ts b/e2e/dashboard.spec.ts new file mode 100644 index 0000000..ef51ee1 --- /dev/null +++ b/e2e/dashboard.spec.ts @@ -0,0 +1,56 @@ +import { expect, test } from "@playwright/test"; + +test.describe("Dashboard", () => { + test.beforeEach(async ({ page }) => { + await page.goto("/"); + await page.waitForLoadState("networkidle"); + }); + + test("shows GearBox heading", async ({ page }) => { + await expect(page.getByText("GearBox")).toBeVisible(); + }); + + test("shows collection card with item count of 6", async ({ page }) => { + // The Collection card link contains "Items" label and value "6" + const collectionCard = page + .getByRole("link", { name: /collection/i }) + .first(); + await expect(collectionCard).toBeVisible(); + await expect(collectionCard.getByText("6")).toBeVisible(); + }); + + test("shows Collection, Planning, and Setups card headings", async ({ + page, + }) => { + await expect( + page.getByRole("heading", { name: "Collection" }), + ).toBeVisible(); + await expect(page.getByRole("heading", { name: "Planning" })).toBeVisible(); + await expect(page.getByRole("heading", { name: "Setups" })).toBeVisible(); + }); + + test("Collection card links to /collection", async ({ page }) => { + const collectionLink = page + .getByRole("link", { name: /collection/i }) + .first(); + await collectionLink.click(); + await page.waitForLoadState("networkidle"); + await expect(page).toHaveURL(/\/collection/); + }); + + test("shows active thread count on Planning card", async ({ page }) => { + // The Planning card is a link containing "Active threads" + const planningCard = page.getByRole("link", { name: /planning/i }); + await expect(planningCard.getByText("Active threads")).toBeVisible(); + // Seed has 1 active thread + await expect(planningCard.getByText("1")).toBeVisible(); + }); + + test("shows setup count on Setups card", async ({ page }) => { + // The Setups card has a heading "Setups" + await expect(page.getByRole("heading", { name: "Setups" })).toBeVisible(); + // Seed has 1 setup + const setupsCard = page.getByRole("link", { name: /setups/i }).last(); + await expect(setupsCard.getByText("1")).toBeVisible(); + }); +}); diff --git a/e2e/start-test-server.sh b/e2e/start-test-server.sh new file mode 100755 index 0000000..15650da --- /dev/null +++ b/e2e/start-test-server.sh @@ -0,0 +1,4 @@ +#!/bin/sh +# Seed the test database, then start the production server +bun run e2e/global-setup.ts +NODE_ENV=production DATABASE_PATH=./e2e/test.db bun run src/server/index.ts diff --git a/playwright.config.ts b/playwright.config.ts index 3ff69e7..46215db 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -6,7 +6,6 @@ export default defineConfig({ retries: 0, workers: 1, reporter: "list", - globalSetup: "./e2e/global-setup.ts", use: { baseURL: "http://localhost:3000", trace: "on-first-retry", @@ -18,9 +17,9 @@ export default defineConfig({ }, ], webServer: { - command: "DATABASE_PATH=./e2e/test.db bun run dev:server", + command: "sh e2e/start-test-server.sh", port: 3000, reuseExistingServer: !process.env.CI, - timeout: 10000, + timeout: 30000, }, });