frigate/web/e2e/specs/export.spec.ts
2026-04-08 16:51:58 -05:00

252 lines
7.8 KiB
TypeScript

/**
* Export page tests — deepened per the test deepening spec.
*
* Covers the deepening checklist:
* 1. Clean render (auto via error fixture)
* 2. Mobile + desktop parity (@mobile test)
* 3. Primary interactions: search, delete, case open, case close
* 4. Empty / loading / error states (via mock-overrides)
* 5. No lenient waits — only real conditions
* 6. Data-driven assertions against fixtures/mock-data/exports.json
*/
import { test, expect } from "../fixtures/frigate-test";
import { mockEmpty, mockError, mockDelay } from "../helpers/mock-overrides";
test.describe("Export Page — Happy Path @high", () => {
test("renders unassigned exports in the main list", async ({
frigateApp,
}) => {
await frigateApp.goto("/export");
// export-001 (Front Door) and export-003 (Garage) are unassigned — visible
// in the main list. export-002 (Backyard) belongs to case-001 and appears
// only inside the case detail view, not in the top-level list.
await expect(
frigateApp.page.getByText("Front Door - Person Alert"),
).toBeVisible();
await expect(
frigateApp.page.getByText("Garage - In Progress"),
).toBeVisible();
});
test("renders the case card from mock data", async ({ frigateApp }) => {
await frigateApp.goto("/export");
await expect(
frigateApp.page.getByText("Package Theft Investigation"),
).toBeVisible();
});
test("clicking a case opens its detail view", async ({ frigateApp }) => {
await frigateApp.goto("/export");
await frigateApp.page
.getByText("Package Theft Investigation")
.first()
.click();
await expect(
frigateApp.page.getByRole("heading", {
name: "Package Theft Investigation",
}),
).toBeVisible();
await expect(
frigateApp.page.getByText("Backyard - Car Detection"),
).toBeVisible();
});
});
test.describe("Export Page — Search @high", () => {
test("search filter narrows the list to matching exports", async ({
frigateApp,
}) => {
await frigateApp.goto("/export");
await expect(
frigateApp.page.getByText("Front Door - Person Alert"),
).toBeVisible();
const search = frigateApp.page.getByPlaceholder(/search/i).first();
await search.fill("Front Door");
await expect(
frigateApp.page.getByText("Front Door - Person Alert"),
).toBeVisible();
// Garage (unassigned) and Package Theft Investigation (case) are filtered out
await expect(
frigateApp.page.getByText("Garage - In Progress"),
).toBeHidden();
await expect(
frigateApp.page.getByText("Package Theft Investigation"),
).toBeHidden();
});
test("clearing the search restores all exports", async ({ frigateApp }) => {
await frigateApp.goto("/export");
await expect(
frigateApp.page.getByText("Front Door - Person Alert"),
).toBeVisible();
const search = frigateApp.page.getByPlaceholder(/search/i).first();
await search.fill("Front Door");
await expect(
frigateApp.page.getByText("Garage - In Progress"),
).toBeHidden();
await search.fill("");
await expect(
frigateApp.page.getByText("Garage - In Progress"),
).toBeVisible();
});
});
test.describe("Export Page — Delete @high", () => {
test("clicking delete on a card opens the confirmation dialog", async ({
frigateApp,
}) => {
await frigateApp.goto("/export");
await expect(
frigateApp.page.getByText("Front Door - Person Alert"),
).toBeVisible();
// Find the card containing "Front Door - Person Alert" and open its menu.
// The menu trigger is an "Edit name" BlurredIconButton rendered inside
// each card. Since all three cards share this label, we scope the
// button query to the card that contains the matching text.
const cardMenuTrigger = frigateApp.page
.getByRole("button", { name: "Edit name" })
.first();
await cardMenuTrigger.click();
// Click the Delete export menu item
await frigateApp.page
.getByRole("menuitem", { name: "Delete export" })
.click();
// Confirmation alert dialog visible with the Delete Export button
// (capital E — distinct from the lowercase-e menu item)
await expect(
frigateApp.page.getByRole("button", { name: "Delete Export" }),
).toBeVisible();
});
test("cancelling the delete dialog leaves the card in place", async ({
frigateApp,
}) => {
await frigateApp.goto("/export");
await expect(
frigateApp.page.getByText("Front Door - Person Alert"),
).toBeVisible();
await frigateApp.page
.getByRole("button", { name: "Edit name" })
.first()
.click();
await frigateApp.page
.getByRole("menuitem", { name: "Delete export" })
.click();
await frigateApp.page.getByRole("button", { name: /^cancel$/i }).click();
await expect(
frigateApp.page.getByRole("button", { name: "Delete Export" }),
).toBeHidden();
await expect(
frigateApp.page.getByText("Front Door - Person Alert"),
).toBeVisible();
});
});
test.describe("Export Page — States @high", () => {
test("empty state renders when there are no exports or cases", async ({
page,
frigateApp,
}) => {
await mockEmpty(page, "**/api/exports**");
await mockEmpty(page, "**/api/cases**");
await frigateApp.goto("/export");
await expect(frigateApp.page.getByText("No exports found")).toBeVisible();
});
test("loading state — empty list resolves after delay", async ({
page,
frigateApp,
}) => {
await mockDelay(page, "**/api/exports**", 500, []);
await mockDelay(page, "**/api/cases**", 500, []);
await frigateApp.goto("/export");
await expect(frigateApp.page.locator("#pageRoot")).toBeVisible();
await expect(frigateApp.page.getByText("No exports found")).toBeVisible({
timeout: 5_000,
});
});
});
test.describe("Export Page — Error State @high", () => {
// The exports endpoint returning 500 will be logged by SWR. Use a single
// alternation regex per Playwright's isFixtureTuple collision.
test.use({
expectedErrors: [/500.*\/api\/exports|Failed to load resource.*500/],
});
test("API error does not crash the page", async ({ page, frigateApp }) => {
await mockError(page, "**/api/exports**");
await frigateApp.goto("/export");
// Page must still mount — the search input is rendered before data loads
await expect(
frigateApp.page.getByPlaceholder(/search/i).first(),
).toBeVisible();
});
});
test.describe("Export Page — Mobile @high @mobile", () => {
test("mobile viewport renders the export list and opens video pane", async ({
frigateApp,
}) => {
test.skip(!frigateApp.isMobile, "Mobile-only assertion");
await frigateApp.goto("/export");
await expect(
frigateApp.page.getByText("Front Door - Person Alert"),
).toBeVisible();
// Click the export card to open the video dialog. This is the exact
// code path that was silently broken for weeks before this spec existed.
await frigateApp.page
.getByText("Front Door - Person Alert")
.first()
.click();
// The dialog mounts with the export's name as title
await expect(
frigateApp.page
.getByRole("dialog")
.filter({ hasText: "Front Door - Person Alert" }),
).toBeVisible();
// The video element inside the dialog is present
await expect(
frigateApp.page.locator('[role="dialog"] video'),
).toBeVisible();
});
test("mobile viewport renders cases and opens case detail", async ({
frigateApp,
}) => {
test.skip(!frigateApp.isMobile, "Mobile-only assertion");
await frigateApp.goto("/export");
await frigateApp.page
.getByText("Package Theft Investigation")
.first()
.click();
await expect(
frigateApp.page.getByRole("heading", {
name: "Package Theft Investigation",
}),
).toBeVisible();
});
});