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 <noreply@anthropic.com>
This commit is contained in:
89
e2e/collection.spec.ts
Normal file
89
e2e/collection.spec.ts
Normal file
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
56
e2e/dashboard.spec.ts
Normal file
56
e2e/dashboard.spec.ts
Normal file
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
4
e2e/start-test-server.sh
Executable file
4
e2e/start-test-server.sh
Executable file
@@ -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
|
||||||
@@ -6,7 +6,6 @@ export default defineConfig({
|
|||||||
retries: 0,
|
retries: 0,
|
||||||
workers: 1,
|
workers: 1,
|
||||||
reporter: "list",
|
reporter: "list",
|
||||||
globalSetup: "./e2e/global-setup.ts",
|
|
||||||
use: {
|
use: {
|
||||||
baseURL: "http://localhost:3000",
|
baseURL: "http://localhost:3000",
|
||||||
trace: "on-first-retry",
|
trace: "on-first-retry",
|
||||||
@@ -18,9 +17,9 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
webServer: {
|
webServer: {
|
||||||
command: "DATABASE_PATH=./e2e/test.db bun run dev:server",
|
command: "sh e2e/start-test-server.sh",
|
||||||
port: 3000,
|
port: 3000,
|
||||||
reuseExistingServer: !process.env.CI,
|
reuseExistingServer: !process.env.CI,
|
||||||
timeout: 10000,
|
timeout: 30000,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user