mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-10-01 20:36:51 +03:00
rework live dashboard grid layout and add natural mode
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* Live grid aspect modes.
|
||||
*
|
||||
* Bucketed mode (the default) snaps every camera to a wide, landscape or tall
|
||||
* tile, and converts layouts saved by pre-masonry versions instead of
|
||||
* discarding them. Natural mode sizes each tile to its own camera.
|
||||
*/
|
||||
|
||||
import { test, expect } from "../fixtures/frigate-test";
|
||||
import { LivePage } from "../pages/live.page";
|
||||
import {
|
||||
cameraBoxes,
|
||||
persistedLayoutKey,
|
||||
readLayout,
|
||||
seedLayout,
|
||||
type LayoutItem,
|
||||
} from "../helpers/grid-layout";
|
||||
|
||||
const GROUP = "outdoor";
|
||||
const GRID_COLS = 96;
|
||||
|
||||
test.describe("Live grid aspect modes @critical", () => {
|
||||
test.skip(
|
||||
({ frigateApp }) => frigateApp.isMobile,
|
||||
"Draggable grid is desktop-only",
|
||||
);
|
||||
|
||||
test("an ultra-wide camera gets a 32:9 tile in bucketed mode @mobile", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
await frigateApp.installDefaults({
|
||||
config: {
|
||||
cameras: { backyard: { detect: { width: 2560, height: 720 } } },
|
||||
},
|
||||
});
|
||||
await frigateApp.goto(`/?group=${GROUP}`);
|
||||
const live = new LivePage(frigateApp.page, true);
|
||||
await expect(live.cameraCard("backyard").first()).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
const { backyard: wide, front_door: normal } = await cameraBoxes(
|
||||
frigateApp.page,
|
||||
["backyard", "front_door"] as const,
|
||||
);
|
||||
|
||||
expect(wide.w / wide.h).toBeCloseTo(32 / 9, 1);
|
||||
expect(wide.w / normal.w).toBeCloseTo(2, 1);
|
||||
expect(wide.h).toBeCloseTo(normal.h, 0);
|
||||
});
|
||||
|
||||
test("a letterboxed still image rounds its own corners", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
// A portrait camera pillarboxes inside its 8:9 bucket, so the card's
|
||||
// overflow-hidden clip never reaches the picture's corners. The image has
|
||||
// to carry the radius itself or it renders with square edges on the tile.
|
||||
await frigateApp.installDefaults({
|
||||
config: {
|
||||
cameras: { backyard: { detect: { width: 720, height: 1280 } } },
|
||||
},
|
||||
});
|
||||
await frigateApp.goto(`/?group=${GROUP}`);
|
||||
const live = new LivePage(frigateApp.page, true);
|
||||
await expect(live.cameraCard("backyard").first()).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
const radii = await frigateApp.page.evaluate(() => {
|
||||
const card = document.querySelector("[data-camera='backyard']");
|
||||
const img = card?.querySelector("img");
|
||||
return {
|
||||
card: card ? getComputedStyle(card).borderTopLeftRadius : null,
|
||||
img: img ? getComputedStyle(img).borderTopLeftRadius : null,
|
||||
};
|
||||
});
|
||||
|
||||
expect(radii.card).not.toBe("0px");
|
||||
expect(radii.img).toBe(radii.card);
|
||||
});
|
||||
|
||||
test("a pre-masonry layout is converted, keeping resized tiles", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
await frigateApp.goto(`/?group=${GROUP}`);
|
||||
const live = new LivePage(frigateApp.page, true);
|
||||
await expect(live.cameraCard("front_door").first()).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
// 0.17/0.18 shape: bare array on a 12-column grid, 4x4 standard tiles.
|
||||
// backyard was manually resized to 8x8 and sits beside front_door's column,
|
||||
// front_door is a standard tile on the row below.
|
||||
const key = await persistedLayoutKey(frigateApp.page, GROUP);
|
||||
await seedLayout(frigateApp.page, key, [
|
||||
{ i: "backyard", x: 4, y: 0, w: 8, h: 8, moved: false, static: false },
|
||||
{ i: "front_door", x: 0, y: 8, w: 4, h: 4, moved: false, static: false },
|
||||
]);
|
||||
await frigateApp.page.reload();
|
||||
await frigateApp.page.waitForSelector("#pageRoot", { timeout: 10_000 });
|
||||
await expect(live.cameraCard("front_door").first()).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
// The conversion is written back on first load, replacing the legacy array
|
||||
// with an envelope. Poll for it: that write is an async idb put.
|
||||
await expect
|
||||
.poll(async () => (await readLayout(frigateApp.page, key))?.version, {
|
||||
timeout: 10_000,
|
||||
})
|
||||
.toBe(2);
|
||||
|
||||
const stored = (await readLayout(frigateApp.page, key))!;
|
||||
expect(stored).toMatchObject({ version: 2, naturalAspect: false });
|
||||
|
||||
// x and w scale 8x (12 -> 96 columns), y and h scale 18x (4 -> 72 rows per
|
||||
// standard tile), so the manual resize survives instead of snapping back.
|
||||
expect(
|
||||
stored.layout.find((i: LayoutItem) => i.i === "backyard"),
|
||||
).toMatchObject({
|
||||
x: 32,
|
||||
y: 0,
|
||||
w: 64,
|
||||
h: 144,
|
||||
});
|
||||
expect(
|
||||
stored.layout.find((i: LayoutItem) => i.i === "front_door"),
|
||||
).toMatchObject({
|
||||
x: 0,
|
||||
y: 144,
|
||||
w: 32,
|
||||
h: 72,
|
||||
});
|
||||
|
||||
// arrangement on screen: backyard indented, front_door below it
|
||||
const { backyard, front_door: frontDoor } = await cameraBoxes(
|
||||
frigateApp.page,
|
||||
["backyard", "front_door"] as const,
|
||||
);
|
||||
expect(backyard.x).toBeGreaterThan(frontDoor.x + frontDoor.w / 2);
|
||||
expect(frontDoor.y).toBeGreaterThan(backyard.y + backyard.h / 2);
|
||||
});
|
||||
|
||||
test("conversion is a pure scale, so odd sizes and positions survive", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
await frigateApp.goto(`/?group=${GROUP}`);
|
||||
const live = new LivePage(frigateApp.page, true);
|
||||
await expect(live.cameraCard("front_door").first()).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
// The old grid exposed all four resize corners with no aspect constraint,
|
||||
// so a stored tile can be any size. These two are adjacent and non-standard.
|
||||
const key = await persistedLayoutKey(frigateApp.page, GROUP);
|
||||
await seedLayout(frigateApp.page, key, [
|
||||
{ i: "front_door", x: 0, y: 3, w: 5, h: 5 },
|
||||
{ i: "backyard", x: 5, y: 3, w: 7, h: 5 },
|
||||
]);
|
||||
await frigateApp.page.reload();
|
||||
await frigateApp.page.waitForSelector("#pageRoot", { timeout: 10_000 });
|
||||
await expect(live.cameraCard("backyard").first()).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
await expect
|
||||
.poll(async () => (await readLayout(frigateApp.page, key))?.version, {
|
||||
timeout: 10_000,
|
||||
})
|
||||
.toBe(2);
|
||||
|
||||
const stored = (await readLayout(frigateApp.page, key))!;
|
||||
const frontDoor = stored.layout.find(
|
||||
(i: LayoutItem) => i.i === "front_door",
|
||||
)!;
|
||||
const backyard = stored.layout.find((i: LayoutItem) => i.i === "backyard")!;
|
||||
|
||||
expect(frontDoor).toMatchObject({ x: 0, y: 54, w: 40, h: 90 });
|
||||
expect(backyard).toMatchObject({ x: 40, y: 54, w: 56, h: 90 });
|
||||
|
||||
// still adjacent, still inside the grid, still not overlapping
|
||||
expect(frontDoor.x + frontDoor.w).toBe(backyard.x);
|
||||
expect(backyard.x + backyard.w).toBe(GRID_COLS);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* Masonry live grid -- custom-group draggable layout.
|
||||
*
|
||||
* Verifies natural-aspect tile sizing and that a saved layout the current
|
||||
* version cannot read is regenerated cleanly. The grid renders only for a
|
||||
* custom camera group (here: "outdoor") on desktop; mobile keeps the static
|
||||
* grid, which the @mobile block below guards.
|
||||
*/
|
||||
|
||||
import { test, expect } from "../fixtures/frigate-test";
|
||||
import { LivePage } from "../pages/live.page";
|
||||
import {
|
||||
cameraBoxes,
|
||||
persistedLayoutKey,
|
||||
readLayout,
|
||||
seedLayout,
|
||||
} from "../helpers/grid-layout";
|
||||
|
||||
const GROUP = "outdoor"; // custom group: front_door + backyard
|
||||
|
||||
test.describe("Masonry live grid @critical", () => {
|
||||
test.skip(
|
||||
({ frigateApp }) => frigateApp.isMobile,
|
||||
"Draggable masonry grid is desktop-only",
|
||||
);
|
||||
|
||||
test("custom group renders its cameras in the draggable grid", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
await frigateApp.goto(`/?group=${GROUP}`);
|
||||
const live = new LivePage(frigateApp.page, true);
|
||||
await expect(live.cameraCard("front_door").first()).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
await expect(live.cameraCard("backyard").first()).toBeVisible();
|
||||
});
|
||||
|
||||
test("tiles render at their camera's natural aspect ratio", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
// front_door stays 16:9; backyard is overridden to portrait so the two
|
||||
// tiles must render with opposite orientations.
|
||||
await frigateApp.installDefaults({
|
||||
config: {
|
||||
cameras: { backyard: { detect: { width: 720, height: 1280 } } },
|
||||
},
|
||||
});
|
||||
await frigateApp.goto(`/?group=${GROUP}`);
|
||||
const live = new LivePage(frigateApp.page, true);
|
||||
|
||||
await expect(live.cameraCard("front_door").first()).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
const { front_door: landscape, backyard: portrait } = await cameraBoxes(
|
||||
frigateApp.page,
|
||||
["front_door", "backyard"] as const,
|
||||
"card",
|
||||
);
|
||||
|
||||
// 16:9 tile is clearly wider than tall; portrait tile is taller than wide.
|
||||
expect(landscape.w / landscape.h).toBeGreaterThan(1.4);
|
||||
expect(portrait.w / portrait.h).toBeLessThan(1);
|
||||
});
|
||||
|
||||
test("dragging a tile does not shove other tiles far away", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
await frigateApp.goto(`/?group=${GROUP}`);
|
||||
const live = new LivePage(frigateApp.page, true);
|
||||
await expect(live.cameraCard("front_door").first()).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
await live.editLayoutButton.click();
|
||||
|
||||
const cameras = ["front_door", "backyard"] as const;
|
||||
const { front_door: fixedBefore, backyard: draggedBox } = await cameraBoxes(
|
||||
frigateApp.page,
|
||||
cameras,
|
||||
"card",
|
||||
);
|
||||
|
||||
// Drag backyard onto front_door's position (a deliberate collision). With
|
||||
// free-placement + prevent-collision, front_door must NOT be shoved down.
|
||||
const from = {
|
||||
x: draggedBox.x + draggedBox.w / 2,
|
||||
y: draggedBox.y + draggedBox.h / 2,
|
||||
};
|
||||
const to = {
|
||||
x: fixedBefore.x + fixedBefore.w / 2,
|
||||
y: fixedBefore.y + fixedBefore.h / 2,
|
||||
};
|
||||
await frigateApp.page.mouse.move(from.x, from.y);
|
||||
await frigateApp.page.mouse.down();
|
||||
await frigateApp.page.mouse.move(to.x, to.y, { steps: 15 });
|
||||
await frigateApp.page.mouse.up();
|
||||
|
||||
const { front_door: fixedAfter } = await cameraBoxes(
|
||||
frigateApp.page,
|
||||
cameras,
|
||||
"card",
|
||||
);
|
||||
// Allow a few px of snap; a collision-push would move it a whole tile down.
|
||||
expect(Math.abs(fixedAfter.y - fixedBefore.y)).toBeLessThan(40);
|
||||
});
|
||||
|
||||
test("resizing a top-row tile preserves its aspect ratio (no pillarboxing)", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
// A lone top tile has room to grow sideways, which is what exposed the bug:
|
||||
// a top-edge handle let width grow while height stayed clamped at y=0.
|
||||
await frigateApp.installDefaults({
|
||||
config: { camera_groups: { outdoor: { cameras: ["front_door"] } } },
|
||||
});
|
||||
await frigateApp.goto(`/?group=${GROUP}`);
|
||||
const live = new LivePage(frigateApp.page, true);
|
||||
await expect(live.cameraCard("front_door").first()).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
await live.editLayoutButton.click();
|
||||
|
||||
const tile = frigateApp.page.locator(".react-grid-item", {
|
||||
has: frigateApp.page.locator("[data-camera='front_door']"),
|
||||
});
|
||||
const only = ["front_door"] as const;
|
||||
const { front_door: before } = await cameraBoxes(
|
||||
frigateApp.page,
|
||||
only,
|
||||
"card",
|
||||
);
|
||||
const aspect = before.w / before.h;
|
||||
|
||||
// Regression: if a top-edge handle is exposed, dragging it up/out must NOT
|
||||
// distort the aspect (the old bug grew width while height stayed clamped).
|
||||
const ne = tile.locator(".react-resizable-handle-ne");
|
||||
if (await ne.count()) {
|
||||
await ne.dragTo(tile, {
|
||||
force: true,
|
||||
targetPosition: { x: 1000, y: -160 },
|
||||
});
|
||||
const { front_door: afterNe } = await cameraBoxes(
|
||||
frigateApp.page,
|
||||
only,
|
||||
"card",
|
||||
);
|
||||
// It must actually resize (not a silent no-op) AND keep its aspect.
|
||||
expect(afterNe.w).toBeGreaterThan(before.w);
|
||||
expect(Math.abs(afterNe.w / afterNe.h - aspect)).toBeLessThan(0.2);
|
||||
}
|
||||
|
||||
// Positive: growing from the bottom-right corner resizes and keeps aspect.
|
||||
const se = tile.locator(".react-resizable-handle-se");
|
||||
await se.dragTo(tile, { force: true, targetPosition: { x: 1000, y: 520 } });
|
||||
const { front_door: grown } = await cameraBoxes(
|
||||
frigateApp.page,
|
||||
only,
|
||||
"card",
|
||||
);
|
||||
expect(grown.w).toBeGreaterThan(before.w);
|
||||
expect(Math.abs(grown.w / grown.h - aspect)).toBeLessThan(0.2);
|
||||
});
|
||||
|
||||
test("the grid keeps its measured width after a back navigation", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
// The grid sizes itself from window.innerWidth until its container is
|
||||
// measured. On a warm back navigation nothing re-renders after that
|
||||
// container mounts, so an observer that never attaches leaves every tile
|
||||
// sized against the full window: the layout widens by the sidebar's width
|
||||
// and the rightmost column clips on a full row.
|
||||
await frigateApp.goto(`/?group=${GROUP}`);
|
||||
const live = new LivePage(frigateApp.page, true);
|
||||
await expect(live.cameraCard("front_door").first()).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
// total width the tiles span; tracks the width the grid laid out against
|
||||
const span = () =>
|
||||
frigateApp.page.evaluate(() => {
|
||||
const tiles = [...document.querySelectorAll(".react-grid-item")];
|
||||
|
||||
if (!tiles.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rects = tiles.map((tile) => tile.getBoundingClientRect());
|
||||
return +(
|
||||
Math.max(...rects.map((r) => r.right)) -
|
||||
Math.min(...rects.map((r) => r.left))
|
||||
).toFixed(1);
|
||||
});
|
||||
|
||||
let fresh: number | null = null;
|
||||
await expect
|
||||
.poll(async () => (fresh = await span()), { timeout: 10_000 })
|
||||
.not.toBeNull();
|
||||
|
||||
await live.cameraCard("front_door").first().click();
|
||||
await expect(frigateApp.page).toHaveURL(/#front_door/);
|
||||
|
||||
await frigateApp.page.goBack();
|
||||
await expect(live.cameraCard("front_door").first()).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
// the layout must settle back to the measured width, not window.innerWidth
|
||||
await expect
|
||||
.poll(span, { timeout: 10_000 })
|
||||
.toBeLessThanOrEqual(fresh! + 2);
|
||||
});
|
||||
|
||||
test("saved layout from an unreadable version regenerates without error", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
await frigateApp.goto(`/?group=${GROUP}`);
|
||||
const live = new LivePage(frigateApp.page, true);
|
||||
await expect(live.cameraCard("front_door").first()).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
|
||||
// A bare array is converted rather than discarded (covered in
|
||||
// live-grid-aspect-modes), so use a version the current grid cannot read.
|
||||
const key = await persistedLayoutKey(frigateApp.page, GROUP);
|
||||
await seedLayout(frigateApp.page, key, {
|
||||
version: 1,
|
||||
naturalAspect: false,
|
||||
layout: [{ i: "front_door", x: 0, y: 0, w: 4, h: 3 }],
|
||||
});
|
||||
|
||||
await frigateApp.page.reload();
|
||||
await frigateApp.page.waitForSelector("#pageRoot", { timeout: 10_000 });
|
||||
|
||||
// Grid regenerated; both cameras still render and the error collector
|
||||
// (frigate-test fixture) catches any crash.
|
||||
await expect(live.cameraCard("front_door").first()).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
await expect(live.cameraCard("backyard").first()).toBeVisible();
|
||||
|
||||
// The app must have replaced the value it could not read. Without this the
|
||||
// test would still pass against a key the app never touches.
|
||||
await expect
|
||||
.poll(async () => (await readLayout(frigateApp.page, key))?.version, {
|
||||
timeout: 10_000,
|
||||
})
|
||||
.toBeGreaterThan(1);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Masonry live grid on mobile @critical @mobile", () => {
|
||||
test("custom group keeps the static grid, with no draggable layout", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
test.skip(!frigateApp.isMobile, "Mobile-only");
|
||||
|
||||
await frigateApp.goto(`/?group=${GROUP}`);
|
||||
const live = new LivePage(frigateApp.page, false);
|
||||
await expect(live.cameraCard("front_door").first()).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
await expect(live.cameraCard("backyard").first()).toBeVisible();
|
||||
|
||||
// isMobileOnly routes around DraggableGridLayout entirely, so neither the
|
||||
// grid items nor the edit-layout toggle may appear.
|
||||
await expect(frigateApp.page.locator(".react-grid-item")).toHaveCount(0);
|
||||
await expect(live.editLayoutButton).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
@@ -13,7 +13,26 @@ import type { Page } from "@playwright/test";
|
||||
const OUTDOOR_LAYOUT_KEY = "outdoor-draggable-layout:admin";
|
||||
const STREAMING_KEY = "streaming-settings:admin";
|
||||
|
||||
const OUTDOOR_LAYOUT = [
|
||||
// the shape DraggableGridLayout writes
|
||||
const OUTDOOR_LAYOUT = {
|
||||
version: 2,
|
||||
naturalAspect: false,
|
||||
layout: [
|
||||
{ i: "front_door", x: 0, y: 0, w: 32, h: 72 },
|
||||
{ i: "backyard", x: 32, y: 0, w: 32, h: 72 },
|
||||
],
|
||||
};
|
||||
|
||||
const NATURAL_OUTDOOR_LAYOUT = {
|
||||
version: 2,
|
||||
naturalAspect: true,
|
||||
layout: [
|
||||
{ i: "front_door", x: 0, y: 0, w: 32, h: 72 },
|
||||
{ i: "backyard", x: 32, y: 0, w: 24, h: 96 },
|
||||
],
|
||||
};
|
||||
|
||||
const LEGACY_OUTDOOR_LAYOUT = [
|
||||
{ i: "front_door", x: 0, y: 0, w: 6, h: 4 },
|
||||
{ i: "backyard", x: 6, y: 0, w: 6, h: 4 },
|
||||
];
|
||||
@@ -159,6 +178,111 @@ test.describe("UI settings import/export @medium", () => {
|
||||
expect(payload.sections.preferences.playbackRate).toBe(2);
|
||||
});
|
||||
|
||||
test("round-trips a layout left unconverted by an upgrade", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
// DraggableGridLayout rewrites a pre-0.19 layout only when that group's
|
||||
// dashboard is opened, so exporting first carries the bare array into the
|
||||
// file. Import must accept it back rather than rejecting the whole file.
|
||||
await frigateApp.goto("/settings?page=uiSettings");
|
||||
|
||||
await writeIdb(frigateApp.page, {
|
||||
[OUTDOOR_LAYOUT_KEY]: LEGACY_OUTDOOR_LAYOUT,
|
||||
"playbackRate:admin": 2,
|
||||
});
|
||||
|
||||
const downloadPromise = frigateApp.page.waitForEvent("download");
|
||||
await frigateApp.page
|
||||
.getByRole("button", { name: "Export Settings" })
|
||||
.click();
|
||||
const download = await downloadPromise;
|
||||
const contents = readFileSync((await download.path())!, "utf-8");
|
||||
|
||||
expect(JSON.parse(contents).sections.layouts.outdoor).toEqual(
|
||||
LEGACY_OUTDOOR_LAYOUT,
|
||||
);
|
||||
|
||||
await clearIdb(frigateApp.page);
|
||||
await chooseImportText(frigateApp.page, contents);
|
||||
await confirmImport(frigateApp.page);
|
||||
|
||||
expect(await readIdb(frigateApp.page, OUTDOOR_LAYOUT_KEY)).toEqual(
|
||||
LEGACY_OUTDOOR_LAYOUT,
|
||||
);
|
||||
// the rest of the file must survive alongside it
|
||||
expect(await readIdb(frigateApp.page, "playbackRate:admin")).toBe(2);
|
||||
});
|
||||
|
||||
test("legacy layouts import turns natural aspect off so they display", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
// Bare-array layouts only render in bucketed mode; with natural aspect on
|
||||
// they would be discarded and regenerated on the next dashboard visit. The
|
||||
// import applies the mode the layouts were built for, and the file's own
|
||||
// naturalAspectLayout preference must not override that.
|
||||
await frigateApp.goto("/settings?page=uiSettings");
|
||||
await writeIdb(frigateApp.page, { "naturalAspectLayout:admin": true });
|
||||
|
||||
await chooseImportFile(
|
||||
frigateApp.page,
|
||||
importPayload({
|
||||
sections: {
|
||||
layouts: { outdoor: LEGACY_OUTDOOR_LAYOUT },
|
||||
streaming: {},
|
||||
preferences: { naturalAspectLayout: true },
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const note = frigateApp.page.getByText(/standard tile sizing/);
|
||||
await expect(note).toBeVisible();
|
||||
|
||||
// the note is about the layouts section, so it follows its switch
|
||||
await frigateApp.page.getByText("Camera group layouts (1 group)").click();
|
||||
await expect(note).toBeHidden();
|
||||
await frigateApp.page.getByText("Camera group layouts (1 group)").click();
|
||||
await expect(note).toBeVisible();
|
||||
|
||||
await confirmImport(frigateApp.page);
|
||||
|
||||
expect(await readIdb(frigateApp.page, OUTDOOR_LAYOUT_KEY)).toEqual(
|
||||
LEGACY_OUTDOOR_LAYOUT,
|
||||
);
|
||||
expect(await readIdb(frigateApp.page, "naturalAspectLayout:admin")).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("natural aspect layouts import turns the setting on", async ({
|
||||
frigateApp,
|
||||
}) => {
|
||||
await frigateApp.goto("/settings?page=uiSettings");
|
||||
|
||||
await chooseImportFile(
|
||||
frigateApp.page,
|
||||
importPayload({
|
||||
sections: {
|
||||
layouts: { outdoor: NATURAL_OUTDOOR_LAYOUT },
|
||||
streaming: {},
|
||||
preferences: {},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(
|
||||
frigateApp.page.getByText(/camera aspect ratio tile sizing/),
|
||||
).toBeVisible();
|
||||
|
||||
await confirmImport(frigateApp.page);
|
||||
|
||||
expect(await readIdb(frigateApp.page, OUTDOOR_LAYOUT_KEY)).toEqual(
|
||||
NATURAL_OUTDOOR_LAYOUT,
|
||||
);
|
||||
expect(await readIdb(frigateApp.page, "naturalAspectLayout:admin")).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("omits settings that were never stored", async ({ frigateApp }) => {
|
||||
await frigateApp.goto("/settings?page=uiSettings");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user