feat: refresh workbench visual layout
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import { expect, test } from "playwright/test";
|
||||
|
||||
test("Agent panel resizes by drag without exceeding half the viewport", async ({ page }) => {
|
||||
test("Agent panel resizes by drag without exceeding the 620px workspace limit", async ({
|
||||
page
|
||||
}) => {
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
|
||||
const panel = page.locator('aside[aria-label="Agent 命令面板"]');
|
||||
@@ -16,13 +18,15 @@ test("Agent panel resizes by drag without exceeding half the viewport", async ({
|
||||
await page.mouse.move(1_200, handleBox!.y + handleBox!.height / 2, { steps: 10 });
|
||||
await page.mouse.up();
|
||||
|
||||
await expect.poll(async () => (await panel.boundingBox())?.width).toBe(720);
|
||||
await expect.poll(async () => (await panel.boundingBox())?.width).toBe(620);
|
||||
|
||||
await resizeHandle.focus();
|
||||
await page.keyboard.press("Home");
|
||||
await expect.poll(async () => (await panel.boundingBox())?.width).toBe(460);
|
||||
await page.keyboard.press("ArrowRight");
|
||||
await expect.poll(async () => (await panel.boundingBox())?.width).toBe(476);
|
||||
await page.keyboard.press("End");
|
||||
await expect.poll(async () => (await panel.boundingBox())?.width).toBe(620);
|
||||
});
|
||||
|
||||
test("collapsed Agent rail is compact and expands as one clear action", async ({ page }) => {
|
||||
@@ -60,7 +64,7 @@ test.describe("mobile touch layout", () => {
|
||||
await page.touchscreen.tap(buttonBox!.x + buttonBox!.width / 2, buttonBox!.y + buttonBox!.height / 2);
|
||||
|
||||
await expect(page.locator('aside[aria-label="Agent 命令面板"]').filter({ visible: true })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "关闭 Agent 面板" })).toHaveCount(0);
|
||||
await expect(page.getByRole("button", { name: "关闭 Agent 面板" })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -73,5 +77,5 @@ test("mobile alert summary opens the Agent conversation panel", async ({ page })
|
||||
await page.getByRole("button", { name: "工况汇总" }).click();
|
||||
|
||||
await expect(page.locator('aside[aria-label="Agent 命令面板"]').filter({ visible: true })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "关闭 Agent 面板" })).toHaveCount(0);
|
||||
await expect(page.getByRole("button", { name: "关闭 Agent 面板" })).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { expect, test } from "playwright/test";
|
||||
|
||||
test.describe("mobile workbench sheet", () => {
|
||||
test.use({ viewport: { width: 390, height: 844 } });
|
||||
|
||||
test("supports all snap heights, keyboard control, and Escape close", async ({ page }) => {
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await page.getByRole("button", { name: "打开 Agent 面板" }).click();
|
||||
|
||||
const sheet = page.getByRole("region", { name: "Agent 工作台抽屉" });
|
||||
const handle = page.getByRole("button", { name: /Agent 工作台抽屉高度/ });
|
||||
|
||||
await expect(sheet).toBeVisible();
|
||||
await expect(sheet).toHaveAttribute("data-snap", "half");
|
||||
await expect.poll(async () => Math.round((await sheet.boundingBox())?.height ?? 0)).toBe(439);
|
||||
await expect
|
||||
.poll(async () =>
|
||||
sheet.evaluate((element) => {
|
||||
const style = getComputedStyle(element);
|
||||
const nestedFilters = Array.from(element.querySelectorAll("*")).filter(
|
||||
(descendant) => getComputedStyle(descendant).backdropFilter !== "none"
|
||||
);
|
||||
return {
|
||||
backdropFilter: style.backdropFilter,
|
||||
nestedFilterCount: nestedFilters.length,
|
||||
opacity: style.opacity,
|
||||
transform: style.transform
|
||||
};
|
||||
})
|
||||
)
|
||||
.toEqual({
|
||||
backdropFilter: "blur(24px) saturate(1.08)",
|
||||
nestedFilterCount: 0,
|
||||
opacity: "1",
|
||||
transform: "none"
|
||||
});
|
||||
|
||||
await handle.click();
|
||||
await expect(sheet).toHaveAttribute("data-snap", "full");
|
||||
await expect.poll(async () => Math.round((await sheet.boundingBox())?.height ?? 0)).toBe(776);
|
||||
await expect.poll(async () => Math.round((await sheet.boundingBox())?.y ?? 0)).toBe(68);
|
||||
|
||||
await handle.focus();
|
||||
await page.keyboard.press("Home");
|
||||
await expect(sheet).toHaveAttribute("data-snap", "peek");
|
||||
await expect.poll(async () => Math.round((await sheet.boundingBox())?.height ?? 0)).toBe(96);
|
||||
|
||||
await page.keyboard.press("ArrowUp");
|
||||
await expect(sheet).toHaveAttribute("data-snap", "half");
|
||||
await page.keyboard.press("End");
|
||||
await expect(sheet).toHaveAttribute("data-snap", "full");
|
||||
|
||||
await page.setViewportSize({ width: 390, height: 700 });
|
||||
await expect.poll(async () => Math.round((await sheet.boundingBox())?.height ?? 0)).toBe(632);
|
||||
await expect.poll(async () => Math.round((await sheet.boundingBox())?.y ?? 0)).toBe(68);
|
||||
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(sheet).toBeHidden();
|
||||
});
|
||||
|
||||
test("keeps Agent and condition sheets mutually exclusive", async ({ page }) => {
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
|
||||
await page.getByRole("button", { name: "打开 Agent 面板" }).click();
|
||||
await expect(page.getByRole("region", { name: "Agent 工作台抽屉" })).toBeVisible();
|
||||
await page.keyboard.press("Escape");
|
||||
|
||||
await page.getByRole("button", { name: "打开工况任务" }).click();
|
||||
await expect(page.getByRole("region", { name: "工况任务抽屉" })).toBeVisible();
|
||||
await expect(
|
||||
page.locator('aside[aria-label="Agent 命令面板"]').filter({ visible: true })
|
||||
).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("offers a touch close action for the condition sheet", async ({ page }) => {
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await page.getByRole("button", { name: "打开工况任务" }).click();
|
||||
|
||||
const sheet = page.getByRole("region", { name: "工况任务抽屉" });
|
||||
await expect(sheet).toBeVisible();
|
||||
await page.getByRole("button", { name: "关闭工况任务抽屉" }).click();
|
||||
await expect(sheet).toBeHidden();
|
||||
await expect(page.getByRole("button", { name: "打开工况任务" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("keeps condition detail expanded inside the mobile sheet", async ({ page }) => {
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await page.getByRole("button", { name: "打开工况任务" }).click();
|
||||
const sheet = page.getByRole("region", { name: "工况任务抽屉" });
|
||||
|
||||
await sheet.getByRole("button", { name: /SCADA 数据诊断/ }).first().click();
|
||||
await expect(sheet.getByRole("button", { name: "收起工况任务" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("clears mobile sheet state when crossing into the desktop layout", async ({ page }) => {
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await page.getByRole("button", { name: "打开 Agent 面板" }).click();
|
||||
await expect(page.getByRole("region", { name: "Agent 工作台抽屉" })).toBeVisible();
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 800 });
|
||||
await expect(page.getByRole("region", { name: "Agent 工作台抽屉" })).toHaveCount(0);
|
||||
await expect(page.locator('aside[aria-label="Agent 命令面板"]')).toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -9,12 +9,77 @@ test("scheduled conditions keep acrylic blur outside transformed ancestors", asy
|
||||
|
||||
await expectAcrylicComposition(panel);
|
||||
await expect(panel).toHaveCSS("width", "432px");
|
||||
const timeline = panel.getByTestId("scheduled-condition-timeline");
|
||||
const collapsedPanelBox = await panel.boundingBox();
|
||||
const collapsedTimelineBox = await timeline.boundingBox();
|
||||
expect(collapsedPanelBox).not.toBeNull();
|
||||
expect(collapsedTimelineBox).not.toBeNull();
|
||||
|
||||
await panel.getByRole("button", { name: "展开工况任务" }).click();
|
||||
await page.waitForTimeout(250);
|
||||
|
||||
await expectAcrylicComposition(panel);
|
||||
await expect(panel).toHaveCSS("width", "880px");
|
||||
const expandedPanelBox = await panel.boundingBox();
|
||||
const expandedTimelineBox = await timeline.boundingBox();
|
||||
expect(expandedPanelBox).not.toBeNull();
|
||||
expect(expandedTimelineBox).not.toBeNull();
|
||||
expect(Math.round(expandedPanelBox!.x + expandedPanelBox!.width)).toBe(
|
||||
Math.round(collapsedPanelBox!.x + collapsedPanelBox!.width)
|
||||
);
|
||||
expect(Math.round(expandedTimelineBox!.width)).toBe(
|
||||
Math.round(collapsedTimelineBox!.width)
|
||||
);
|
||||
expect(Math.round(expandedTimelineBox!.x + expandedTimelineBox!.width)).toBe(
|
||||
Math.round(collapsedTimelineBox!.x + collapsedTimelineBox!.width)
|
||||
);
|
||||
|
||||
await panel.getByRole("button", { name: "收起工况任务" }).click();
|
||||
await page.waitForTimeout(250);
|
||||
await expect(panel).toHaveCSS("width", "432px");
|
||||
const restoredTimelineBox = await timeline.boundingBox();
|
||||
expect(restoredTimelineBox).not.toBeNull();
|
||||
expect(Math.round(restoredTimelineBox!.width)).toBe(
|
||||
Math.round(collapsedTimelineBox!.width)
|
||||
);
|
||||
});
|
||||
|
||||
test("narrow desktop keeps Agent and expanded conditions from overlapping", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1024, height: 768 });
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await page.getByRole("button", { name: "工况任务", exact: true }).click();
|
||||
|
||||
const panel = page.getByRole("region", { name: "工况任务", exact: true });
|
||||
const agentPanel = page.locator('aside[aria-label="Agent 命令面板"]');
|
||||
const resizeHandle = page.getByRole("separator", { name: "调整 Agent 面板宽度" });
|
||||
await resizeHandle.focus();
|
||||
await page.keyboard.press("End");
|
||||
await expect(agentPanel).toHaveCSS("width", "484px");
|
||||
|
||||
const compactAgentBox = await agentPanel.boundingBox();
|
||||
const compactConditionBox = await panel.boundingBox();
|
||||
expect(compactAgentBox).not.toBeNull();
|
||||
expect(compactConditionBox).not.toBeNull();
|
||||
expect(compactConditionBox!.x - (compactAgentBox!.x + compactAgentBox!.width)).toBeGreaterThanOrEqual(
|
||||
24
|
||||
);
|
||||
|
||||
await panel.getByRole("button", { name: "展开工况任务" }).click();
|
||||
await expect(page.locator('aside[aria-label="Agent 折叠栏"]')).toBeVisible();
|
||||
await expect(panel).toHaveCSS("width", "852px");
|
||||
|
||||
const expandedConditionBox = await panel.boundingBox();
|
||||
const collapsedAgentBox = await page
|
||||
.locator('aside[aria-label="Agent 折叠栏"]')
|
||||
.boundingBox();
|
||||
expect(expandedConditionBox).not.toBeNull();
|
||||
expect(collapsedAgentBox).not.toBeNull();
|
||||
expect(
|
||||
expandedConditionBox!.x - (collapsedAgentBox!.x + collapsedAgentBox!.width)
|
||||
).toBeGreaterThanOrEqual(24);
|
||||
|
||||
await panel.getByRole("button", { name: "收起工况任务" }).click();
|
||||
await expect(agentPanel).toBeVisible();
|
||||
});
|
||||
|
||||
async function expectAcrylicComposition(panel: Locator) {
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { expect, test } from "playwright/test";
|
||||
|
||||
test("opening Agent and condition panels does not move the map camera", async ({ page }) => {
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await page.waitForFunction(() =>
|
||||
Boolean(window.__waterNetworkMap?.getLayer("scada-hit"))
|
||||
);
|
||||
await page.waitForTimeout(500);
|
||||
await page.evaluate(() => {
|
||||
const map = window.__waterNetworkMap;
|
||||
document.documentElement.dataset.panelCameraMoveCount = "0";
|
||||
map?.on("movestart", () => {
|
||||
const currentCount = Number(
|
||||
document.documentElement.dataset.panelCameraMoveCount ?? "0"
|
||||
);
|
||||
document.documentElement.dataset.panelCameraMoveCount = String(currentCount + 1);
|
||||
});
|
||||
});
|
||||
|
||||
await page.getByRole("button", { name: "折叠 Agent 面板" }).click();
|
||||
await page.waitForTimeout(250);
|
||||
await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click();
|
||||
await page.waitForTimeout(250);
|
||||
|
||||
const conditionPanel = page.getByRole("region", { name: "工况任务", exact: true });
|
||||
await conditionPanel.getByRole("button", { name: "展开工况任务" }).click();
|
||||
await page.waitForTimeout(250);
|
||||
|
||||
expect(
|
||||
await page.evaluate(
|
||||
() => document.documentElement.dataset.panelCameraMoveCount
|
||||
)
|
||||
).toBe("0");
|
||||
});
|
||||
@@ -0,0 +1,274 @@
|
||||
import { expect, test, type Locator, type Page } from "playwright/test";
|
||||
|
||||
const EMPTY_RASTER_TILE = Buffer.from(
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
|
||||
"base64"
|
||||
);
|
||||
|
||||
test.describe("neutral blue mist workbench", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.clock.setFixedTime(new Date("2026-07-21T10:40:00+08:00"));
|
||||
await page.emulateMedia({ reducedMotion: "reduce" });
|
||||
await prepareDeterministicWorkbench(page);
|
||||
});
|
||||
|
||||
test("desktop light basemap keeps both floating panels legible", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
await openWorkbench(page);
|
||||
|
||||
await expectDesktopFloatingGeometry(page);
|
||||
await expectAcrylicAlphas(page, {
|
||||
navigation: 0.64,
|
||||
panel: 0.56,
|
||||
control: 0.74
|
||||
});
|
||||
await expectLoadedChineseFont(page);
|
||||
await expectBodyContrast(page);
|
||||
await expect(page).toHaveScreenshot("workbench-desktop-light.png", {
|
||||
animations: "disabled",
|
||||
maxDiffPixelRatio: 0.01
|
||||
});
|
||||
});
|
||||
|
||||
test("desktop satellite basemap strengthens the same floating hierarchy", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
await openWorkbench(page);
|
||||
await selectBasemap(page, "影像");
|
||||
|
||||
await expect(page.locator("main")).toHaveAttribute("data-basemap-tone", "satellite");
|
||||
await expectDesktopFloatingGeometry(page);
|
||||
await expectAcrylicAlphas(page, {
|
||||
navigation: 0.8,
|
||||
panel: 0.72,
|
||||
control: 0.86
|
||||
});
|
||||
await expect(page).toHaveScreenshot("workbench-desktop-satellite.png", {
|
||||
animations: "disabled",
|
||||
maxDiffPixelRatio: 0.01
|
||||
});
|
||||
});
|
||||
|
||||
test("wide desktop keeps a map corridor at maximum Agent width", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1920, height: 1080 });
|
||||
await openWorkbench(page);
|
||||
|
||||
const agentPanel = page.locator('aside[aria-label="Agent 命令面板"]');
|
||||
const conditionPanel = page.getByRole("region", { name: "工况任务", exact: true });
|
||||
const resizeHandle = page.getByRole("separator", { name: "调整 Agent 面板宽度" });
|
||||
|
||||
await resizeHandle.focus();
|
||||
await page.keyboard.press("End");
|
||||
await expect(agentPanel).toHaveCSS("width", "620px");
|
||||
await expectMapCorridor(agentPanel, conditionPanel, 768);
|
||||
|
||||
await conditionPanel.getByRole("button", { name: "展开工况任务" }).click();
|
||||
await expect(conditionPanel).toHaveCSS("width", "960px");
|
||||
await expectMapCorridor(agentPanel, conditionPanel, 260);
|
||||
await expect(page).toHaveScreenshot("workbench-desktop-wide-max-agent.png", {
|
||||
animations: "disabled",
|
||||
maxDiffPixelRatio: 0.01
|
||||
});
|
||||
});
|
||||
|
||||
test("mobile light basemap shows the Agent half sheet", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await openWorkbench(page);
|
||||
await page.getByRole("button", { name: "打开 Agent 面板" }).click();
|
||||
|
||||
const sheet = page.getByRole("region", { name: "Agent 工作台抽屉" });
|
||||
await expect(sheet).toHaveAttribute("data-snap", "half");
|
||||
await expect.poll(async () => Math.round((await sheet.boundingBox())?.height ?? 0)).toBe(439);
|
||||
await expect(page).toHaveScreenshot("workbench-mobile-agent-light.png", {
|
||||
animations: "disabled",
|
||||
maxDiffPixelRatio: 0.01
|
||||
});
|
||||
});
|
||||
|
||||
test("mobile satellite basemap shows the condition half sheet", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await openWorkbench(page);
|
||||
await selectBasemap(page, "影像");
|
||||
await page.getByRole("button", { name: "打开工况任务" }).click();
|
||||
|
||||
const sheet = page.getByRole("region", { name: "工况任务抽屉" });
|
||||
await expect(sheet).toHaveAttribute("data-snap", "half");
|
||||
await expect(page.locator("main")).toHaveAttribute("data-basemap-tone", "satellite");
|
||||
await expect(page).toHaveScreenshot("workbench-mobile-condition-satellite.png", {
|
||||
animations: "disabled",
|
||||
maxDiffPixelRatio: 0.01
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function prepareDeterministicWorkbench(page: Page) {
|
||||
await page.route("**/runtime-config.js", async (route) => {
|
||||
await route.fulfill({
|
||||
contentType: "application/javascript",
|
||||
body: `globalThis.__TJWATER_CONFIG__ = {
|
||||
TJWATER_MAPBOX_ACCESS_TOKEN: "visual-test-token",
|
||||
TJWATER_MAP_URL: "https://visual-map.invalid/geoserver",
|
||||
TJWATER_GEOSERVER_WORKSPACE: "tjwater",
|
||||
TJWATER_AGENT_API_BASE_URL: "http://127.0.0.1:8787",
|
||||
TJWATER_ENABLE_DEV_PANEL: "false",
|
||||
TJWATER_ENABLE_MSW: "false"
|
||||
};`
|
||||
});
|
||||
});
|
||||
await page.route("https://api.mapbox.com/**", async (route) => {
|
||||
await route.fulfill({ contentType: "image/png", body: EMPTY_RASTER_TILE });
|
||||
});
|
||||
await page.route("https://visual-map.invalid/**", async (route) => {
|
||||
await route.fulfill({
|
||||
contentType: "application/vnd.mapbox-vector-tile",
|
||||
body: Buffer.alloc(0)
|
||||
});
|
||||
});
|
||||
await page.route("https://demotiles.maplibre.org/**", async (route) => {
|
||||
await route.fulfill({ status: 204, body: "" });
|
||||
});
|
||||
await page.route("**/*.riv", async (route) => {
|
||||
await route.abort();
|
||||
});
|
||||
}
|
||||
|
||||
async function openWorkbench(page: Page) {
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await page.locator("main").evaluate((element) => {
|
||||
element.setAttribute("data-visual-test", "true");
|
||||
});
|
||||
await page.addStyleTag({
|
||||
content: `
|
||||
[data-sonner-toaster] { display: none !important; }
|
||||
[data-visual-test] .maplibregl-canvas { opacity: 0.28; }
|
||||
[data-visual-test][data-basemap-tone="light"] .map-grid {
|
||||
background:
|
||||
radial-gradient(circle at 70% 32%, rgba(56, 120, 170, 0.18) 0 3px, transparent 4px),
|
||||
linear-gradient(118deg, transparent 0 46%, rgba(65, 121, 160, 0.12) 46.2% 46.7%, transparent 47%),
|
||||
linear-gradient(90deg, rgba(75, 124, 160, 0.06) 1px, transparent 1px),
|
||||
linear-gradient(rgba(75, 124, 160, 0.06) 1px, transparent 1px),
|
||||
#dce8ef;
|
||||
background-size: auto, auto, 48px 48px, 48px 48px, auto;
|
||||
}
|
||||
[data-visual-test][data-basemap-tone="satellite"] .map-grid {
|
||||
background:
|
||||
radial-gradient(circle at 68% 30%, rgba(183, 207, 159, 0.24) 0 4px, transparent 5px),
|
||||
linear-gradient(126deg, transparent 0 44%, rgba(178, 195, 153, 0.2) 44.2% 44.8%, transparent 45%),
|
||||
repeating-linear-gradient(24deg, rgba(34, 67, 58, 0.18) 0 1px, transparent 1px 22px),
|
||||
#526961;
|
||||
}
|
||||
`
|
||||
});
|
||||
await page.evaluate(async () => {
|
||||
await document.fonts.ready;
|
||||
});
|
||||
await page.waitForTimeout(350);
|
||||
}
|
||||
|
||||
async function selectBasemap(page: Page, label: "浅色" | "影像") {
|
||||
await page.getByRole("button", { name: /图层:管理地图图层/ }).click();
|
||||
await page.getByRole("button", { name: label, exact: true }).click();
|
||||
await page.getByRole("button", { name: /图层:管理地图图层/ }).click();
|
||||
await page.waitForTimeout(220);
|
||||
}
|
||||
|
||||
async function expectDesktopFloatingGeometry(page: Page) {
|
||||
const agentPanel = page.locator('aside[aria-label="Agent 命令面板"]');
|
||||
const conditionPanel = page.getByRole("region", { name: "工况任务", exact: true });
|
||||
|
||||
await expect(agentPanel).toHaveCSS("width", "460px");
|
||||
await expect(conditionPanel).toHaveCSS("width", "432px");
|
||||
await expect(agentPanel).toHaveCSS("border-radius", "16px");
|
||||
await expect(conditionPanel).toHaveCSS("border-radius", "16px");
|
||||
|
||||
const agentBox = await agentPanel.boundingBox();
|
||||
const conditionBox = await conditionPanel.boundingBox();
|
||||
expect(agentBox).not.toBeNull();
|
||||
expect(conditionBox).not.toBeNull();
|
||||
expect(agentBox!.x).toBe(12);
|
||||
expect(agentBox!.y).toBe(96);
|
||||
expect(agentBox!.y + agentBox!.height).toBe(884);
|
||||
expect(conditionBox!.x).toBe(944);
|
||||
expect(conditionBox!.y).toBe(96);
|
||||
}
|
||||
|
||||
async function expectMapCorridor(
|
||||
agentPanel: Locator,
|
||||
conditionPanel: Locator,
|
||||
minimumWidth: number
|
||||
) {
|
||||
const agentBox = await agentPanel.boundingBox();
|
||||
const conditionBox = await conditionPanel.boundingBox();
|
||||
expect(agentBox).not.toBeNull();
|
||||
expect(conditionBox).not.toBeNull();
|
||||
expect(conditionBox!.x - (agentBox!.x + agentBox!.width)).toBeGreaterThanOrEqual(
|
||||
minimumWidth
|
||||
);
|
||||
}
|
||||
|
||||
async function expectLoadedChineseFont(page: Page) {
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(() => document.fonts.check('16px "Noto Sans SC Variable"', "供水管网"))
|
||||
)
|
||||
.toBe(true);
|
||||
}
|
||||
|
||||
async function expectAcrylicAlphas(
|
||||
page: Page,
|
||||
expected: { navigation: number; panel: number; control: number }
|
||||
) {
|
||||
const alphas = await page.evaluate(() => {
|
||||
return {
|
||||
navigation: alphaOf(document.querySelector(".acrylic-navigation")),
|
||||
panel: alphaOf(document.querySelector(".acrylic-panel")),
|
||||
control: alphaOf(document.querySelector(".acrylic-control"))
|
||||
};
|
||||
|
||||
function alphaOf(element: Element | null) {
|
||||
if (!element) {
|
||||
return 1;
|
||||
}
|
||||
const color = getComputedStyle(element).backgroundColor;
|
||||
const channels = color.match(/\d+(?:\.\d+)?/g)?.map(Number) ?? [];
|
||||
return channels[3] ?? 1;
|
||||
}
|
||||
});
|
||||
|
||||
expect(alphas.navigation).toBeCloseTo(expected.navigation, 2);
|
||||
expect(alphas.panel).toBeCloseTo(expected.panel, 2);
|
||||
expect(alphas.control).toBeCloseTo(expected.control, 2);
|
||||
}
|
||||
|
||||
async function expectBodyContrast(page: Page) {
|
||||
const contrast = await page
|
||||
.getByText("直接说出你想调查的问题,我会整理监测与空间证据,并将分析结果带回地图。")
|
||||
.evaluate((element) => {
|
||||
const foreground = parseColor(getComputedStyle(element).color);
|
||||
const background = { r: 250, g: 252, b: 254 };
|
||||
return contrastRatio(foreground, background);
|
||||
|
||||
function parseColor(color: string) {
|
||||
const channels = color.match(/\d+(?:\.\d+)?/g)?.map(Number) ?? [0, 0, 0];
|
||||
return { r: channels[0] ?? 0, g: channels[1] ?? 0, b: channels[2] ?? 0 };
|
||||
}
|
||||
|
||||
function contrastRatio(
|
||||
first: { r: number; g: number; b: number },
|
||||
second: { r: number; g: number; b: number }
|
||||
) {
|
||||
const lighter = Math.max(luminance(first), luminance(second));
|
||||
const darker = Math.min(luminance(first), luminance(second));
|
||||
return (lighter + 0.05) / (darker + 0.05);
|
||||
}
|
||||
|
||||
function luminance(color: { r: number; g: number; b: number }) {
|
||||
const [r, g, b] = [color.r, color.g, color.b].map((channel) => {
|
||||
const normalized = channel / 255;
|
||||
return normalized <= 0.03928 ? normalized / 12.92 : ((normalized + 0.055) / 1.055) ** 2.4;
|
||||
});
|
||||
return 0.2126 * (r ?? 0) + 0.7152 * (g ?? 0) + 0.0722 * (b ?? 0);
|
||||
}
|
||||
});
|
||||
|
||||
expect(contrast).toBeGreaterThanOrEqual(4.5);
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 470 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 429 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 853 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 140 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 95 KiB |
Reference in New Issue
Block a user