Files
GearBox/e2e/threads.spec.ts
Jean-Luc Makiola c4ce96ce4f test: add E2E tests for threads, auth, and error handling
Also fix CandidateListItem to not use Reorder.Item when isActive=false,
which caused a framer-motion crash on resolved thread detail pages.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 16:23:26 +02:00

114 lines
4.4 KiB
TypeScript

import { expect, test } from "@playwright/test";
test.describe("Thread detail page", () => {
test("loads with candidates visible", async ({ page }) => {
await page.goto("/collection?tab=planning");
await page.waitForLoadState("networkidle");
// Click the "New Backpack" thread card
await page.getByText("New Backpack").click();
await page.waitForLoadState("networkidle");
// Thread detail page should show the thread name
await expect(
page.getByRole("heading", { name: "New Backpack" }),
).toBeVisible({ timeout: 5000 });
// Candidates should be visible
await expect(page.getByText("ULA Circuit")).toBeVisible({ timeout: 5000 });
await expect(page.getByText("Gossamer Gear Mariposa")).toBeVisible({
timeout: 5000,
});
await expect(page.getByText("Granite Gear Crown2 38")).toBeVisible({
timeout: 5000,
});
});
test("rank badges are visible for top 3 candidates", async ({ page }) => {
await page.goto("/collection?tab=planning");
await page.waitForLoadState("networkidle");
await page.getByText("New Backpack").click();
await page.waitForLoadState("networkidle");
// Rank badges are medal icons — the component renders LucideIcon with name="medal"
// for ranks 1, 2, 3. We can verify via SVG elements or by checking the page has
// the expected number of medal icons (one per top candidate).
// The list view is default, which renders CandidateListItem with RankBadge.
// With 3 candidates all in top 3, all 3 get medal icons.
await expect(page.locator("text=ULA Circuit")).toBeVisible({
timeout: 5000,
});
await expect(page.locator("text=Gossamer Gear Mariposa")).toBeVisible({
timeout: 5000,
});
await expect(page.locator("text=Granite Gear Crown2 38")).toBeVisible({
timeout: 5000,
});
// Verify candidates are rendered (rank badges are SVG medals, not text)
// Check that at least the 3 candidates are present in the list
const candidateRows = page.locator(".bg-white.rounded-xl.border");
await expect(candidateRows).toHaveCount(3, { timeout: 5000 });
});
test("comparison view toggle works", async ({ page }) => {
await page.goto("/collection?tab=planning");
await page.waitForLoadState("networkidle");
await page.getByText("New Backpack").click();
await page.waitForLoadState("networkidle");
await expect(page.getByText("ULA Circuit")).toBeVisible({ timeout: 5000 });
// The compare button is a button with title="Compare view" (icon button with columns-3 icon)
const compareButton = page.getByRole("button", { name: "Compare view" });
await expect(compareButton).toBeVisible({ timeout: 5000 });
await compareButton.click();
// After clicking, a table should appear with Weight and Price row labels
await expect(page.locator("table")).toBeVisible({ timeout: 5000 });
// The comparison table renders row labels in sticky <td> cells (exact match to avoid
// matching candidate notes that contain the word "weight" or "price")
await expect(
page.getByRole("cell", { name: "Weight", exact: true }),
).toBeVisible({ timeout: 5000 });
await expect(
page.getByRole("cell", { name: "Price", exact: true }),
).toBeVisible({ timeout: 5000 });
// All 3 candidates should appear as table column headers (in <thead>)
await expect(page.locator("thead").getByText("ULA Circuit")).toBeVisible({
timeout: 5000,
});
await expect(
page.locator("thead").getByText("Gossamer Gear Mariposa"),
).toBeVisible({ timeout: 5000 });
});
test("resolved thread shows winner banner", async ({ page }) => {
await page.goto("/collection?tab=planning");
await page.waitForLoadState("networkidle");
// Click the "Resolved" tab pill button in the planning view
await page.getByRole("button", { name: "Resolved" }).click();
await page.waitForLoadState("networkidle");
// Camp Stove resolved thread should appear
await expect(page.getByText("Camp Stove")).toBeVisible({ timeout: 5000 });
// Click the Camp Stove thread card and wait for URL to change to thread detail
await page.getByText("Camp Stove").click();
await page.waitForURL(/\/threads\/\d+/, { timeout: 5000 });
await page.waitForLoadState("networkidle");
// Should show the resolved thread heading
await expect(page.getByRole("heading", { name: "Camp Stove" })).toBeVisible(
{ timeout: 5000 },
);
// The winner candidate (BRS-3000T) should be visible in the candidate list
await expect(page.getByText("BRS-3000T")).toBeVisible({ timeout: 5000 });
});
});