mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-22 15:07:41 +08:00
3b81416299
* Bump radix-ui packages to align react-dismissable-layer version and fix nested overlay pointer-events bug * remove workarounds for radix pointer events issues on dropdown and context menus * remove disablePortal from popover * remove modal on popovers * remove workarounds in restart dialog * keep onCloseAutoFocus for face, classification, and ptz these are necessary to prevent tooltips from re-showing and from the arrow keys from reopening the ptz presets menu * add tests
199 lines
6.1 KiB
TypeScript
199 lines
6.1 KiB
TypeScript
/**
|
|
* PTZ overlay regression tests -- MEDIUM tier.
|
|
*
|
|
* Guards two things on the PTZ preset dropdown:
|
|
*
|
|
* 1. After selecting a preset, the "Presets" tooltip must not re-pop
|
|
* (focus-restore side-effect that originally prompted the
|
|
* `onCloseAutoFocus preventDefault` workaround).
|
|
* 2. Keyboard shortcuts fired after the dropdown closes should not
|
|
* re-open the dropdown via Space/Enter/Arrow on the trigger
|
|
* (PR #12079 — "Prevent ptz keyboard shortcuts from reopening
|
|
* presets menu").
|
|
*
|
|
* Requires an onvif-configured camera and a mocked /ptz/info endpoint
|
|
* exposing presets.
|
|
*
|
|
* TODO: migrate these tests into live.spec.ts when it comes out of
|
|
* PENDING_REWRITE in e2e/scripts/lint-specs.mjs. They live in a dedicated
|
|
* file today so they stay lint-compliant (no waitForTimeout, no
|
|
* conditional isVisible) while live.spec.ts is still exempt.
|
|
*/
|
|
|
|
import { test, expect } from "../fixtures/frigate-test";
|
|
import {
|
|
expectBodyInteractive,
|
|
waitForBodyInteractive,
|
|
} from "../helpers/overlay-interaction";
|
|
|
|
const PTZ_CAMERA = "front_door";
|
|
const PRESET_NAMES = ["home", "driveway", "front_porch"];
|
|
|
|
test.describe("PTZ preset dropdown @medium", () => {
|
|
test("selecting a preset closes menu cleanly and does not re-open on keyboard", async ({
|
|
frigateApp,
|
|
}) => {
|
|
if (frigateApp.isMobile) {
|
|
test.skip();
|
|
return;
|
|
}
|
|
|
|
// 1. Give front_door an onvif host so the PtzControlPanel renders.
|
|
// 2. Mock the /ptz/info endpoint to expose features + presets.
|
|
await frigateApp.api.install({
|
|
config: {
|
|
cameras: {
|
|
[PTZ_CAMERA]: {
|
|
onvif: {
|
|
host: "10.0.0.50",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
await frigateApp.page.route(`**/api/${PTZ_CAMERA}/ptz/info`, (route) =>
|
|
route.fulfill({
|
|
json: {
|
|
name: PTZ_CAMERA,
|
|
features: ["pt", "zoom"],
|
|
presets: PRESET_NAMES,
|
|
profiles: [],
|
|
},
|
|
}),
|
|
);
|
|
|
|
// PTZ commands ride the WebSocket, not HTTP. The WsMocker intercepts
|
|
// the /ws route, so Playwright's page-level `websocket` event never
|
|
// fires — instead, patch the client WebSocket.prototype.send before
|
|
// any app code runs and mirror sends into a window-level array the
|
|
// test can read back.
|
|
await frigateApp.page.addInitScript(() => {
|
|
(window as unknown as { __sentWsFrames: string[] }).__sentWsFrames = [];
|
|
const origSend = WebSocket.prototype.send;
|
|
WebSocket.prototype.send = function (data) {
|
|
try {
|
|
(
|
|
window as unknown as { __sentWsFrames: string[] }
|
|
).__sentWsFrames.push(typeof data === "string" ? data : "(binary)");
|
|
} catch {
|
|
// ignore — best-effort tracing
|
|
}
|
|
return origSend.call(this, data);
|
|
};
|
|
});
|
|
|
|
await frigateApp.goto(`/#${PTZ_CAMERA}`);
|
|
|
|
// Locate the preset trigger — a button whose accessible name includes
|
|
// "presets" (set via aria-label={t("ptz.presets")}).
|
|
const presetTrigger = frigateApp.page.getByRole("button", {
|
|
name: /presets/i,
|
|
});
|
|
await expect(presetTrigger.first()).toBeVisible({ timeout: 5_000 });
|
|
|
|
await presetTrigger.first().click();
|
|
|
|
const menu = frigateApp.page
|
|
.locator('[role="menu"], [data-radix-menu-content]')
|
|
.first();
|
|
await expect(menu).toBeVisible({ timeout: 3_000 });
|
|
|
|
// Pick a preset.
|
|
const firstPreset = menu
|
|
.getByRole("menuitem", { name: PRESET_NAMES[0] })
|
|
.first();
|
|
await firstPreset.click();
|
|
|
|
// Menu closes.
|
|
await expect(menu).not.toBeVisible({ timeout: 3_000 });
|
|
|
|
// Preset command was dispatched over the WS.
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
const sentFrames = await frigateApp.page.evaluate(
|
|
() =>
|
|
(window as unknown as { __sentWsFrames: string[] })
|
|
.__sentWsFrames,
|
|
);
|
|
|
|
return sentFrames.some(
|
|
(frame) =>
|
|
frame.includes(`"${PTZ_CAMERA}/ptz"`) &&
|
|
frame.includes(`preset_${PRESET_NAMES[0]}`),
|
|
);
|
|
},
|
|
{ timeout: 2_000 },
|
|
)
|
|
.toBe(true);
|
|
|
|
// Body is interactive.
|
|
await waitForBodyInteractive(frigateApp.page);
|
|
await expectBodyInteractive(frigateApp.page);
|
|
|
|
// Presets tooltip should NOT be visible.
|
|
await expect
|
|
.poll(
|
|
async () =>
|
|
frigateApp.page
|
|
.locator('[role="tooltip"]')
|
|
.filter({ hasText: /presets/i })
|
|
.isVisible()
|
|
.catch(() => false),
|
|
{ timeout: 1_000 },
|
|
)
|
|
.toBe(false);
|
|
|
|
// Now press keyboard keys — none should reopen the menu.
|
|
await frigateApp.page.keyboard.press("ArrowUp");
|
|
await frigateApp.page.keyboard.press("Space");
|
|
await frigateApp.page.keyboard.press("Enter");
|
|
await expect
|
|
.poll(() => menu.isVisible().catch(() => false), { timeout: 1_000 })
|
|
.toBe(false);
|
|
});
|
|
});
|
|
|
|
test.describe("Mobile live camera overlay @medium @mobile", () => {
|
|
test("mobile single-camera view loads without freezing body", async ({
|
|
frigateApp,
|
|
}) => {
|
|
if (!frigateApp.isMobile) {
|
|
test.skip();
|
|
return;
|
|
}
|
|
|
|
// Same config override as the desktop spec so the mobile page exercises
|
|
// the onvif-enabled code path and its dismissable-layer consumers.
|
|
await frigateApp.api.install({
|
|
config: {
|
|
cameras: {
|
|
[PTZ_CAMERA]: {
|
|
onvif: { host: "10.0.0.50" },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
await frigateApp.page.route(`**/api/${PTZ_CAMERA}/ptz/info`, (route) =>
|
|
route.fulfill({
|
|
json: {
|
|
name: PTZ_CAMERA,
|
|
features: ["pt", "zoom"],
|
|
presets: PRESET_NAMES,
|
|
profiles: [],
|
|
},
|
|
}),
|
|
);
|
|
|
|
await frigateApp.goto(`/#${PTZ_CAMERA}`);
|
|
|
|
// Body must be interactive after navigation — this is the mobile-side
|
|
// smoke test for the dismissable-layer dedupe. A regression that
|
|
// stuck pointer-events: none on <body> would make the rest of the UI
|
|
// unclickable.
|
|
await expectBodyInteractive(frigateApp.page);
|
|
await expect(frigateApp.page.locator("body")).toBeVisible();
|
|
});
|
|
});
|