diff --git a/web/e2e/specs/system.spec.ts b/web/e2e/specs/system.spec.ts index 87e4db51ea..bd90ee2bfe 100644 --- a/web/e2e/specs/system.spec.ts +++ b/web/e2e/specs/system.spec.ts @@ -72,7 +72,15 @@ test.describe("System — tabs @medium", () => { { timeout: 15_000 }, ); await expect(frigateApp.page.getByText("0.15.0-test")).toBeVisible(); - await expect(frigateApp.page.getByText(/Last refreshed/)).toBeVisible(); + + if (frigateApp.isMobile) { + // the "Last refreshed" label is dropped on mobile so the timestamp + // clears the centered logo + await expect(frigateApp.page.getByText(/Last refreshed/)).toHaveCount(0); + await expect(frigateApp.page.getByText(/Just now|ago/)).toBeVisible(); + } else { + await expect(frigateApp.page.getByText(/Last refreshed/)).toBeVisible(); + } }); test("storage tab renders content after switching", async ({ @@ -234,4 +242,45 @@ test.describe("System — mobile @medium @mobile", () => { { timeout: 5_000 }, ); }); + + test("header controls leave the logo uncovered on a narrow phone", async ({ + frigateApp, + }) => { + await frigateApp.goto("/system#general"); + await expect(frigateApp.page.getByLabel("Select general")).toHaveAttribute( + "data-state", + "on", + { timeout: 15_000 }, + ); + await frigateApp.page.setViewportSize({ width: 320, height: 740 }); + + const logo = frigateApp.page.locator("svg.fill-current").first(); + const tabs = frigateApp.page + .locator("[data-radix-scroll-area-viewport]") + .filter({ has: frigateApp.page.getByLabel("Select general") }); + const refreshed = frigateApp.page.getByText(/Just now|ago/); + + const logoBox = await logo.boundingBox(); + const tabsBox = await tabs.boundingBox(); + const refreshedBox = await refreshed.boundingBox(); + + expect(tabsBox!.x + tabsBox!.width).toBeLessThanOrEqual(logoBox!.x + 1); + expect(refreshedBox!.x).toBeGreaterThanOrEqual(logoBox!.x + logoBox!.width); + + // the clipped tabs stay reachable by scrolling + const overflow = await tabs.evaluate((el) => ({ + scroll: el.scrollWidth, + client: el.clientWidth, + })); + expect(overflow.scroll).toBeGreaterThan(overflow.client); + await tabs.evaluate((el) => { + el.scrollLeft = el.scrollWidth; + }); + await frigateApp.page.getByLabel("Select cameras").click(); + await expect(frigateApp.page.getByLabel("Select cameras")).toHaveAttribute( + "data-state", + "on", + { timeout: 5_000 }, + ); + }); }); diff --git a/web/src/pages/System.tsx b/web/src/pages/System.tsx index e07741e502..7823a10424 100644 --- a/web/src/pages/System.tsx +++ b/web/src/pages/System.tsx @@ -24,6 +24,8 @@ import HealthMetrics from "@/views/system/HealthMetrics"; import NoticeFilterButton from "@/components/health/NoticeFilterButton"; import { DEFAULT_NOTICE_FILTER, NoticeFilter } from "@/types/health"; import { useTranslation } from "react-i18next"; +import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; +import { cn } from "@/lib/utils"; const allMetrics = [ "health", @@ -96,35 +98,40 @@ function System() { {isMobile && ( )} - { - if (value) { - setPageToggle(value); - } - }} // don't allow the severity to be unselected - > - {Object.values(metrics).map((item) => ( - + + { + if (value) { + setPageToggle(value); + } + }} // don't allow the severity to be unselected > - {item == "health" && } - {item == "general" && } - {item == "enrichments" && } - {item == "storage" && } - {item == "cameras" && } - {isDesktop && ( - {t(item + ".title")} - )} - - ))} - + {Object.values(metrics).map((item) => ( + + {item == "health" && } + {item == "general" && } + {item == "enrichments" && } + {item == "storage" && } + {item == "cameras" && } + {isDesktop && ( + {t(item + ".title")} + )} + + ))} + + + + {pageToggle == "health" && ( @@ -135,7 +142,7 @@ function System() { )} {lastUpdated && pageToggle != "health" && ( - {t("lastRefreshed")} + {isDesktop && t("lastRefreshed")} )}