mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-05-01 19:17:41 +03:00
Revert modal changes (#23001)
* revert modal changes from #22963 * add test and lint
This commit is contained in:
parent
ea246384bf
commit
819e8de172
@ -1,4 +1,114 @@
|
||||
import { test, expect } from "../fixtures/frigate-test";
|
||||
import {
|
||||
expectBodyInteractive,
|
||||
waitForBodyInteractive,
|
||||
} from "../helpers/overlay-interaction";
|
||||
|
||||
test.describe("Export Page - Delete race @high", () => {
|
||||
// Empirical guard for radix-ui/primitives#3445: when a modal DropdownMenu
|
||||
// opens an AlertDialog and the AlertDialog's confirm action causes the
|
||||
// parent's optimistic cache update to unmount the card, we want to know
|
||||
// whether the deduped react-dismissable-layer (1.1.11) handles the
|
||||
// pointer-events stack cleanup or whether `modal={false}` is still
|
||||
// required on the DropdownMenu. The classic "canonical" pattern, distinct
|
||||
// from the FaceSelectionDialog auto-unmount race already covered by
|
||||
// face-library.spec.ts.
|
||||
test("deleting an export via dropdown→alert→confirm leaves body interactive", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
if (frigateApp.isMobile) {
|
||||
test.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
const initialExports = [
|
||||
{
|
||||
id: "export-race-001",
|
||||
camera: "front_door",
|
||||
name: "Race - Test Export",
|
||||
date: 1775490731.3863528,
|
||||
video_path: "/exports/export-race-001.mp4",
|
||||
thumb_path: "/exports/export-race-001-thumb.jpg",
|
||||
in_progress: false,
|
||||
export_case_id: null,
|
||||
},
|
||||
];
|
||||
let deleted = false;
|
||||
|
||||
await frigateApp.installDefaults({
|
||||
exports: initialExports,
|
||||
});
|
||||
|
||||
// Flip /api/export to empty after the delete POST is observed so the
|
||||
// page's SWR mutate sees the export gone.
|
||||
await frigateApp.page.route("**/api/export**", async (route) => {
|
||||
const payload = deleted ? [] : initialExports;
|
||||
await route.fulfill({ json: payload });
|
||||
});
|
||||
await frigateApp.page.route("**/api/exports/delete", async (route) => {
|
||||
deleted = true;
|
||||
const delayMs = Number(
|
||||
(globalThis as { process?: { env?: Record<string, string> } }).process
|
||||
?.env?.DELETE_DELAY_MS ?? "100",
|
||||
);
|
||||
if (delayMs > 0) {
|
||||
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||
}
|
||||
await route.fulfill({ json: { success: true } });
|
||||
});
|
||||
|
||||
await frigateApp.goto("/export");
|
||||
await expect(frigateApp.page.getByText("Race - Test Export")).toBeVisible({
|
||||
timeout: 5_000,
|
||||
});
|
||||
|
||||
// Open the kebab menu on the export card. The kebab uses the
|
||||
// (misleading) aria-label "Edit name" from ExportCard's source — it
|
||||
// wraps the FiMoreVertical icon. There is exactly one such button on
|
||||
// the page once we have a single export rendered.
|
||||
const kebab = frigateApp.page
|
||||
.getByRole("button", { name: /edit name/i })
|
||||
.first();
|
||||
await expect(kebab).toBeVisible({ timeout: 5_000 });
|
||||
await kebab.click();
|
||||
|
||||
const menu = frigateApp.page
|
||||
.locator('[role="menu"], [data-radix-menu-content]')
|
||||
.first();
|
||||
await expect(menu).toBeVisible({ timeout: 3_000 });
|
||||
|
||||
// Delete Export
|
||||
await menu
|
||||
.getByRole("menuitem", { name: /delete export/i })
|
||||
.first()
|
||||
.click();
|
||||
|
||||
// AlertDialog at page level. The confirm button's accessible name is
|
||||
// "Delete Export" (its aria-label), the visible text is just "Delete".
|
||||
const confirm = frigateApp.page.getByRole("alertdialog");
|
||||
await expect(confirm).toBeVisible({ timeout: 3_000 });
|
||||
await confirm
|
||||
.getByRole("button", { name: /^delete export$/i })
|
||||
.first()
|
||||
.click();
|
||||
|
||||
// The card optimistically disappears, the dialog closes, and body
|
||||
// pointer-events must come unstuck.
|
||||
await expect(
|
||||
frigateApp.page.getByText("Race - Test Export"),
|
||||
).not.toBeVisible({ timeout: 5_000 });
|
||||
await waitForBodyInteractive(frigateApp.page, 5_000);
|
||||
await expectBodyInteractive(frigateApp.page);
|
||||
|
||||
// Sanity: another page-level button still responds.
|
||||
const newCase = frigateApp.page.getByRole("button", { name: /new case/i });
|
||||
await expect(newCase).toBeVisible({ timeout: 3_000 });
|
||||
await newCase.click();
|
||||
await expect(
|
||||
frigateApp.page.getByRole("dialog").filter({ hasText: /create case/i }),
|
||||
).toBeVisible({ timeout: 3_000 });
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Export Page - Overview @high", () => {
|
||||
test("renders uncategorized exports and case cards from mock data", async ({
|
||||
|
||||
@ -472,7 +472,9 @@ test.describe("FaceSelectionDialog @high", () => {
|
||||
await groupedImage.locator("xpath=..").click();
|
||||
const dialog = frigateApp.page
|
||||
.getByRole("dialog")
|
||||
.filter({ has: frigateApp.page.locator('img[src*="clips/faces/train/"]') })
|
||||
.filter({
|
||||
has: frigateApp.page.locator('img[src*="clips/faces/train/"]'),
|
||||
})
|
||||
.first();
|
||||
await expect(dialog).toBeVisible({ timeout: 5_000 });
|
||||
|
||||
|
||||
@ -266,7 +266,7 @@ export function ExportCard({
|
||||
)}
|
||||
{!exportedRecording.in_progress && !selectionMode && (
|
||||
<div className="absolute bottom-2 right-3 z-40">
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<BlurredIconButton
|
||||
aria-label={t("tooltip.editName")}
|
||||
|
||||
@ -275,7 +275,7 @@ export default function ReviewCard({
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<ContextMenu key={event.id} modal={false}>
|
||||
<ContextMenu key={event.id}>
|
||||
<ContextMenuTrigger asChild>{content}</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem>
|
||||
|
||||
@ -272,7 +272,7 @@ export default function LiveContextMenu({
|
||||
|
||||
return (
|
||||
<div className={cn("w-full", className)}>
|
||||
<ContextMenu key={camera} modal={false} onOpenChange={handleOpenChange}>
|
||||
<ContextMenu key={camera} onOpenChange={handleOpenChange}>
|
||||
<ContextMenuTrigger>{children}</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
<div className="flex flex-col items-start gap-1 py-1 pl-2">
|
||||
|
||||
@ -258,13 +258,13 @@ export default function SearchResultActions({
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
{isContextMenu ? (
|
||||
<ContextMenu modal={false}>
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger>{children}</ContextMenuTrigger>
|
||||
<ContextMenuContent>{menuItems}</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
) : (
|
||||
<>
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<BlurredIconButton aria-label={t("itemMenu.more.aria")}>
|
||||
<FiMoreVertical className="size-5" />
|
||||
|
||||
@ -22,7 +22,7 @@ export default function ActionsDropdown({
|
||||
const { t } = useTranslation(["components/dialog", "views/replay", "common"]);
|
||||
|
||||
return (
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
className="flex items-center gap-2"
|
||||
|
||||
@ -594,7 +594,7 @@ function LibrarySelector({
|
||||
forbiddenErrorMessage={t("description.nameCannotContainHash")}
|
||||
/>
|
||||
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button className="flex justify-between smart-capitalize">
|
||||
{pageTitle}
|
||||
|
||||
@ -342,7 +342,7 @@ function ModelCard({ config, onClick, onUpdate, onDelete }: ModelCardProps) {
|
||||
{config.name}
|
||||
</div>
|
||||
<div className="absolute bottom-2 right-2 z-40">
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild onClick={(e) => e.stopPropagation()}>
|
||||
<BlurredIconButton>
|
||||
<FiMoreVertical className="size-5 text-white" />
|
||||
|
||||
@ -698,7 +698,7 @@ function LibrarySelector({
|
||||
regexErrorMessage={t("description.invalidName")}
|
||||
/>
|
||||
|
||||
<DropdownMenu modal={false}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button className="flex justify-between smart-capitalize">
|
||||
{pageTitle}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user