mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-07-15 00:11:15 +03:00
add helpers
This commit is contained in:
parent
8b3e21703d
commit
fbcf329a71
@ -113,11 +113,12 @@ export class ApiMocker {
|
|||||||
route.fulfill({ json: [] }),
|
route.fulfill({ json: [] }),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Sub-labels and attributes (for explore filters)
|
// Sub-labels and attributes (for explore filters).
|
||||||
await this.page.route("**/api/sub_labels", (route) =>
|
// Use trailing ** so query-string variants (e.g. ?split_joined=1) match.
|
||||||
|
await this.page.route("**/api/sub_labels**", (route) =>
|
||||||
route.fulfill({ json: [] }),
|
route.fulfill({ json: [] }),
|
||||||
);
|
);
|
||||||
await this.page.route("**/api/labels", (route) =>
|
await this.page.route("**/api/labels**", (route) =>
|
||||||
route.fulfill({ json: ["person", "car"] }),
|
route.fulfill({ json: ["person", "car"] }),
|
||||||
);
|
);
|
||||||
await this.page.route("**/api/*/attributes", (route) =>
|
await this.page.route("**/api/*/attributes", (route) =>
|
||||||
|
|||||||
25
web/e2e/helpers/clipboard.ts
Normal file
25
web/e2e/helpers/clipboard.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* Clipboard read helper for e2e tests.
|
||||||
|
*
|
||||||
|
* Clipboard API requires a browser permission in headless mode.
|
||||||
|
* grantClipboardPermissions() must be called before any readClipboard()
|
||||||
|
* attempt. Used by logs.spec.ts (Copy button) and config-editor.spec.ts
|
||||||
|
* (Copy button).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { BrowserContext, Page } from "@playwright/test";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grant clipboard-read + clipboard-write permissions on the context.
|
||||||
|
* Call in beforeEach or at the top of a test before the Copy action.
|
||||||
|
*/
|
||||||
|
export async function grantClipboardPermissions(
|
||||||
|
context: BrowserContext,
|
||||||
|
): Promise<void> {
|
||||||
|
await context.grantPermissions(["clipboard-read", "clipboard-write"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Read the current clipboard contents via the page's navigator.clipboard. */
|
||||||
|
export async function readClipboard(page: Page): Promise<string> {
|
||||||
|
return page.evaluate(async () => await navigator.clipboard.readText());
|
||||||
|
}
|
||||||
61
web/e2e/helpers/monaco.ts
Normal file
61
web/e2e/helpers/monaco.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* Monaco editor DOM helpers for e2e tests.
|
||||||
|
*
|
||||||
|
* Monaco is imported as a module-local object in the app and is NOT
|
||||||
|
* exposed on window; we drive + read through the rendered DOM and
|
||||||
|
* keyboard instead. Used by config-editor.spec.ts only.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { expect, type Page } from "@playwright/test";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current visible text of the first Monaco editor on the
|
||||||
|
* page. Monaco virtualizes long files — this reads only the rendered
|
||||||
|
* lines. For short configs (our mocks) that's the full content.
|
||||||
|
*/
|
||||||
|
export async function getMonacoVisibleText(page: Page): Promise<string> {
|
||||||
|
return page
|
||||||
|
.locator(".monaco-editor .view-lines")
|
||||||
|
.first()
|
||||||
|
.innerText();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Focus the editor and replace its full content with `value` via
|
||||||
|
* keyboard. Uses Ctrl+A (Cmd+A on macOS Playwright is equivalent)
|
||||||
|
* + Delete + type. Works cross-platform because Playwright normalizes.
|
||||||
|
*/
|
||||||
|
export async function replaceMonacoValue(
|
||||||
|
page: Page,
|
||||||
|
value: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const editor = page.locator(".monaco-editor").first();
|
||||||
|
await editor.click();
|
||||||
|
await page.keyboard.press("ControlOrMeta+A");
|
||||||
|
await page.keyboard.press("Delete");
|
||||||
|
// Use `type` with zero delay — Monaco handles each key.
|
||||||
|
await page.keyboard.type(value, { delay: 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true when the editor shows at least one error-severity
|
||||||
|
* marker. Monaco renders error underlines as `.squiggly-error` in
|
||||||
|
* the `.view-overlays` layer.
|
||||||
|
*/
|
||||||
|
export async function hasErrorMarkers(page: Page): Promise<boolean> {
|
||||||
|
const count = await page.locator(".monaco-editor .squiggly-error").count();
|
||||||
|
return count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Poll until an error marker appears. Monaco schedules marker updates
|
||||||
|
* asynchronously after content changes (debounce + schema validation).
|
||||||
|
*/
|
||||||
|
export async function waitForErrorMarker(
|
||||||
|
page: Page,
|
||||||
|
timeoutMs: number = 10_000,
|
||||||
|
): Promise<void> {
|
||||||
|
await expect
|
||||||
|
.poll(() => hasErrorMarkers(page), { timeout: timeoutMs })
|
||||||
|
.toBe(true);
|
||||||
|
}
|
||||||
65
web/e2e/helpers/ws-frames.ts
Normal file
65
web/e2e/helpers/ws-frames.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* WebSocket frame capture helper.
|
||||||
|
*
|
||||||
|
* The ws-mocker intercepts the /ws route, so Playwright's page-level
|
||||||
|
* `websocket` event never fires. This helper patches client-side
|
||||||
|
* WebSocket.prototype.send before any app code runs and mirrors every
|
||||||
|
* sent frame into a window-level array the test can read back.
|
||||||
|
*
|
||||||
|
* Used by live.spec.ts (feature toggles, PTZ preset commands) and
|
||||||
|
* config-editor.spec.ts (restart command via useRestart).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { expect, type Page } from "@playwright/test";
|
||||||
|
|
||||||
|
export type CapturedFrame = string;
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
__sentWsFrames: CapturedFrame[];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Patch WebSocket.prototype.send to capture every outbound frame into
|
||||||
|
* window.__sentWsFrames. Must be called BEFORE page.goto().
|
||||||
|
*/
|
||||||
|
export async function installWsFrameCapture(page: Page): Promise<void> {
|
||||||
|
await page.addInitScript(() => {
|
||||||
|
window.__sentWsFrames = [];
|
||||||
|
const origSend = WebSocket.prototype.send;
|
||||||
|
WebSocket.prototype.send = function (data) {
|
||||||
|
try {
|
||||||
|
window.__sentWsFrames.push(
|
||||||
|
typeof data === "string" ? data : "(binary)",
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// ignore — best-effort tracing
|
||||||
|
}
|
||||||
|
return origSend.call(this, data);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Read all captured frames at call time. */
|
||||||
|
export async function readWsFrames(page: Page): Promise<CapturedFrame[]> {
|
||||||
|
return page.evaluate(() => window.__sentWsFrames ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Poll until at least one captured frame matches the predicate.
|
||||||
|
* Throws via expect if the frame never arrives within timeout.
|
||||||
|
*/
|
||||||
|
export async function waitForWsFrame(
|
||||||
|
page: Page,
|
||||||
|
matcher: (frame: CapturedFrame) => boolean,
|
||||||
|
opts: { timeout?: number; message?: string } = {},
|
||||||
|
): Promise<void> {
|
||||||
|
const { timeout = 2_000, message } = opts;
|
||||||
|
await expect
|
||||||
|
.poll(async () => (await readWsFrames(page)).some(matcher), {
|
||||||
|
timeout,
|
||||||
|
message,
|
||||||
|
})
|
||||||
|
.toBe(true);
|
||||||
|
}
|
||||||
@ -79,7 +79,20 @@ export class WsMocker {
|
|||||||
this.send("model_state", JSON.stringify({}));
|
this.send("model_state", JSON.stringify({}));
|
||||||
}
|
}
|
||||||
if (data.topic === "embeddingsReindexProgress") {
|
if (data.topic === "embeddingsReindexProgress") {
|
||||||
this.send("embeddings_reindex_progress", JSON.stringify(null));
|
// Send a completed reindex state so Explore renders when
|
||||||
|
// semantic_search.enabled is true. A null payload leaves the page
|
||||||
|
// in a permanent loading spinner because !reindexState is truthy.
|
||||||
|
this.send(
|
||||||
|
"embeddings_reindex_progress",
|
||||||
|
JSON.stringify({
|
||||||
|
status: "completed",
|
||||||
|
processed_objects: 0,
|
||||||
|
total_objects: 0,
|
||||||
|
thumbnails: 0,
|
||||||
|
descriptions: 0,
|
||||||
|
time_remaining: null,
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (data.topic === "birdseyeLayout") {
|
if (data.topic === "birdseyeLayout") {
|
||||||
this.send("birdseye_layout", JSON.stringify(null));
|
this.send("birdseye_layout", JSON.stringify(null));
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user