mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-07-14 16:01:13 +03:00
add helpers
This commit is contained in:
parent
8b3e21703d
commit
fbcf329a71
@ -113,11 +113,12 @@ export class ApiMocker {
|
||||
route.fulfill({ json: [] }),
|
||||
);
|
||||
|
||||
// Sub-labels and attributes (for explore filters)
|
||||
await this.page.route("**/api/sub_labels", (route) =>
|
||||
// Sub-labels and attributes (for explore filters).
|
||||
// Use trailing ** so query-string variants (e.g. ?split_joined=1) match.
|
||||
await this.page.route("**/api/sub_labels**", (route) =>
|
||||
route.fulfill({ json: [] }),
|
||||
);
|
||||
await this.page.route("**/api/labels", (route) =>
|
||||
await this.page.route("**/api/labels**", (route) =>
|
||||
route.fulfill({ json: ["person", "car"] }),
|
||||
);
|
||||
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({}));
|
||||
}
|
||||
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") {
|
||||
this.send("birdseye_layout", JSON.stringify(null));
|
||||
|
||||
Loading…
Reference in New Issue
Block a user