mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-07-15 00:11:15 +03:00
rewrite export.spec.ts
This commit is contained in:
parent
741c5abdc3
commit
b0c772352a
@ -1 +1 @@
|
|||||||
[{"id": "export-001", "camera": "front_door", "name": "Front Door - Person Alert", "date": 1775490731.3863528, "video_path": "/exports/export-001.mp4", "thumb_path": "/exports/export-001-thumb.jpg", "in_progress": false, "export_case_id": null}, {"id": "export-002", "camera": "backyard", "name": "Backyard - Car Detection", "date": 1775483531.3863528, "video_path": "/exports/export-002.mp4", "thumb_path": "/exports/export-002-thumb.jpg", "in_progress": false, "export_case_id": "case-001"}, {"id": "export-003", "camera": "garage", "name": "Garage - In Progress", "date": 1775492531.3863528, "video_path": "/exports/export-003.mp4", "thumb_path": "/exports/export-003-thumb.jpg", "in_progress": true, "export_case_id": null}]
|
[{"id": "export-001", "camera": "front_door", "name": "Front Door - Person Alert", "date": 1775490731.3863528, "video_path": "/exports/export-001.mp4", "thumb_path": "/exports/export-001-thumb.jpg", "in_progress": false, "export_case_id": null}, {"id": "export-002", "camera": "backyard", "name": "Backyard - Car Detection", "date": 1775483531.3863528, "video_path": "/exports/export-002.mp4", "thumb_path": "/exports/export-002-thumb.jpg", "in_progress": false, "export_case_id": "case-001"}, {"id": "export-003", "camera": "garage", "name": "Garage - In Progress", "date": 1775492531.3863528, "video_path": "/exports/export-003.mp4", "thumb_path": "/exports/export-003-thumb.jpg", "in_progress": true, "export_case_id": null}]
|
||||||
|
|||||||
@ -44,8 +44,6 @@ const PHASE_3_PENDING = new Set([
|
|||||||
"replay.spec.ts",
|
"replay.spec.ts",
|
||||||
"review.spec.ts",
|
"review.spec.ts",
|
||||||
"system.spec.ts",
|
"system.spec.ts",
|
||||||
// export.spec.ts is rewritten in Task 13; remove it before Task 13
|
|
||||||
"export.spec.ts",
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const BANNED_PATTERNS = [
|
const BANNED_PATTERNS = [
|
||||||
|
|||||||
@ -1,74 +1,251 @@
|
|||||||
/**
|
/**
|
||||||
* Export page tests -- HIGH tier.
|
* Export page tests — deepened per the test deepening spec.
|
||||||
*
|
*
|
||||||
* Tests export card rendering with mock data, search filtering,
|
* Covers the deepening checklist:
|
||||||
* and delete confirmation dialog.
|
* 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 { test, expect } from "../fixtures/frigate-test";
|
||||||
|
import { mockEmpty, mockError, mockDelay } from "../helpers/mock-overrides";
|
||||||
|
|
||||||
test.describe("Export Page - Cards @high", () => {
|
test.describe("Export Page — Happy Path @high", () => {
|
||||||
test("export page renders export cards from mock data", async ({
|
test("renders unassigned exports in the main list", async ({
|
||||||
frigateApp,
|
frigateApp,
|
||||||
}) => {
|
}) => {
|
||||||
await frigateApp.goto("/export");
|
await frigateApp.goto("/export");
|
||||||
await frigateApp.page.waitForTimeout(2000);
|
|
||||||
// Should show export names from our mock data
|
// 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(
|
await expect(
|
||||||
frigateApp.page.getByText("Front Door - Person Alert"),
|
frigateApp.page.getByText("Front Door - Person Alert"),
|
||||||
).toBeVisible({ timeout: 10_000 });
|
).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(
|
await expect(
|
||||||
frigateApp.page.getByText("Backyard - Car Detection"),
|
frigateApp.page.getByText("Backyard - Car Detection"),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("export page shows in-progress indicator", async ({ frigateApp }) => {
|
test.describe("Export Page — Search @high", () => {
|
||||||
|
test("search filter narrows the list to matching exports", async ({
|
||||||
|
frigateApp,
|
||||||
|
}) => {
|
||||||
await frigateApp.goto("/export");
|
await frigateApp.goto("/export");
|
||||||
await frigateApp.page.waitForTimeout(2000);
|
await expect(
|
||||||
// "Garage - In Progress" export should be visible
|
frigateApp.page.getByText("Front Door - Person Alert"),
|
||||||
await expect(frigateApp.page.getByText("Garage - In Progress")).toBeVisible(
|
).toBeVisible();
|
||||||
{ timeout: 10_000 },
|
|
||||||
);
|
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("export page shows case grouping", async ({ frigateApp }) => {
|
test("clearing the search restores all exports", async ({ frigateApp }) => {
|
||||||
await frigateApp.goto("/export");
|
await frigateApp.goto("/export");
|
||||||
await frigateApp.page.waitForTimeout(3000);
|
await expect(
|
||||||
// Cases may render differently depending on API response shape
|
frigateApp.page.getByText("Front Door - Person Alert"),
|
||||||
const pageText = await frigateApp.page.textContent("#pageRoot");
|
).toBeVisible();
|
||||||
expect(pageText?.length).toBeGreaterThan(0);
|
|
||||||
|
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 - Search @high", () => {
|
test.describe("Export Page — Delete @high", () => {
|
||||||
test("search input filters export list", async ({ frigateApp }) => {
|
test("clicking delete on a card opens the confirmation dialog", async ({
|
||||||
|
frigateApp,
|
||||||
|
}) => {
|
||||||
await frigateApp.goto("/export");
|
await frigateApp.goto("/export");
|
||||||
await frigateApp.page.waitForTimeout(2000);
|
await expect(
|
||||||
const searchInput = frigateApp.page.locator(
|
frigateApp.page.getByText("Front Door - Person Alert"),
|
||||||
'#pageRoot input[type="text"], #pageRoot input',
|
).toBeVisible();
|
||||||
);
|
|
||||||
if (
|
// Find the card containing "Front Door - Person Alert" and open its menu.
|
||||||
(await searchInput.count()) > 0 &&
|
// The menu trigger is an "Edit name" BlurredIconButton rendered inside
|
||||||
(await searchInput.first().isVisible())
|
// each card. Since all three cards share this label, we scope the
|
||||||
) {
|
// button query to the card that contains the matching text.
|
||||||
// Type a search term that matches one export
|
const cardMenuTrigger = frigateApp.page
|
||||||
await searchInput.first().fill("Front Door");
|
.getByRole("button", { name: "Edit name" })
|
||||||
await frigateApp.page.waitForTimeout(500);
|
.first();
|
||||||
// "Front Door - Person Alert" should still be visible
|
await cardMenuTrigger.click();
|
||||||
await expect(
|
|
||||||
frigateApp.page.getByText("Front Door - Person Alert"),
|
// Click the Delete export menu item
|
||||||
).toBeVisible();
|
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.locator("#pageRoot")).toBeVisible();
|
||||||
|
await expect(frigateApp.page.getByText("No exports found")).toBeVisible({
|
||||||
|
timeout: 5_000,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe("Export Page - Controls @high", () => {
|
test.describe("Export Page — Error State @high", () => {
|
||||||
test("export page filter controls are present", async ({ frigateApp }) => {
|
// 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");
|
await frigateApp.goto("/export");
|
||||||
await frigateApp.page.waitForTimeout(1000);
|
|
||||||
const buttons = frigateApp.page.locator("#pageRoot button");
|
// Page must still mount — the search input is rendered before data loads
|
||||||
const count = await buttons.count();
|
await expect(
|
||||||
expect(count).toBeGreaterThan(0);
|
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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -71,7 +71,7 @@ function Exports() {
|
|||||||
const exportsByCase = useMemo<{ [caseId: string]: Export[] }>(() => {
|
const exportsByCase = useMemo<{ [caseId: string]: Export[] }>(() => {
|
||||||
const grouped: { [caseId: string]: Export[] } = {};
|
const grouped: { [caseId: string]: Export[] } = {};
|
||||||
(rawExports ?? []).forEach((exp) => {
|
(rawExports ?? []).forEach((exp) => {
|
||||||
const caseId = exp.export_case || "none";
|
const caseId = exp.export_case_id || "none";
|
||||||
if (!grouped[caseId]) {
|
if (!grouped[caseId]) {
|
||||||
grouped[caseId] = [];
|
grouped[caseId] = [];
|
||||||
}
|
}
|
||||||
@ -477,7 +477,7 @@ function CaseView({
|
|||||||
}: CaseViewProps) {
|
}: CaseViewProps) {
|
||||||
const filteredExports = useMemo<Export[]>(() => {
|
const filteredExports = useMemo<Export[]>(() => {
|
||||||
const caseExports = (exports || []).filter(
|
const caseExports = (exports || []).filter(
|
||||||
(e) => e.export_case == selectedCase.id,
|
(e) => e.export_case_id == selectedCase.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!search) {
|
if (!search) {
|
||||||
|
|||||||
@ -6,7 +6,7 @@ export type Export = {
|
|||||||
video_path: string;
|
video_path: string;
|
||||||
thumb_path: string;
|
thumb_path: string;
|
||||||
in_progress: boolean;
|
in_progress: boolean;
|
||||||
export_case?: string;
|
export_case_id?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ExportCase = {
|
export type ExportCase = {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user