Files
SystemSimulationApp/frontend/tests/e2e/results-chart-windows.spec.ts

959 lines
31 KiB
TypeScript

import { expect, test, type Locator, type Page } from "@playwright/test";
import { prepareApp, resultSnapshot } from "./fixtures";
const chartWindowSnapshot = {
...resultSnapshot,
id: "e2e-pending-chart-windows",
result: {
...resultSnapshot.result,
variables: [
...resultSnapshot.result.variables,
{
key: "generic_sensor_1.pressure",
componentId: "generic_sensor_1",
componentType: "generic_sensor",
scope: "component",
portName: null,
name: "pressure",
label: "压力",
quantity: "pressure",
unit: "Pa",
category: "state",
order: 2,
},
],
final: {
...resultSnapshot.result.final,
"generic_sensor_1.pressure": 120000,
},
series: {
...resultSnapshot.result.series,
"generic_sensor_1.pressure": [100000, 110000, 120000],
},
},
};
const eventForceSnapshot = {
...chartWindowSnapshot,
id: "e2e-mechanical-event-force-chart",
project: {
...chartWindowSnapshot.project,
nodes: chartWindowSnapshot.project.nodes.map((node, nodeIndex) =>
nodeIndex === 0
? {
...node,
data: {
...node.data,
ports: node.data.ports.map((port, portIndex) =>
portIndex === 0
? { ...port, domain: "mechanical" }
: port,
),
},
}
: node,
),
simulation: {
...chartWindowSnapshot.project.simulation,
t_stop: 2,
step: 0.5,
},
},
result: {
...chartWindowSnapshot.result,
simulatedUntil: 2,
requestedStopTime: 2,
variables: [
...chartWindowSnapshot.result.variables,
{
key: "generic_sensor_1.port_a.f",
componentId: "generic_sensor_1",
componentType: "generic_sensor",
scope: "port",
portName: "port_a",
name: "f",
label: "力",
quantity: "force",
unit: "N",
category: "flow",
order: 30,
},
{
key: "generic_sensor_1.force",
componentId: "generic_sensor_1",
componentType: "generic_sensor",
scope: "component",
portName: null,
name: "force",
label: "接触力",
quantity: "force",
unit: "N",
category: "derived",
order: 31,
},
],
final: {
...chartWindowSnapshot.result.final,
"generic_sensor_1.value": 2,
"generic_sensor_1.port_a.f": 5,
"generic_sensor_1.force": 5,
},
series: {
...chartWindowSnapshot.result.series,
time: [0, 0.5, 0.75, 1, 1.5, 2],
"generic_sensor_1.value": [0, 0.5, 1e9, 1, 1.5, 2],
"generic_sensor_1.port_a.f": [1, 2, 1e12, 3, 4, 5],
"generic_sensor_1.force": [1, 2, -1e12, 3, 4, 5],
},
diagnostics: {
...chartWindowSnapshot.result.diagnostics,
integration: {
segments: [{ stateTransitionTimes: [0.75] }],
},
sampleCount: 6,
},
},
};
async function openResults(page: Page) {
await page.getByRole("tab", { name: "结果", exact: true }).click();
await expect(page.locator(".results-chart-workspace")).toBeVisible();
}
async function readChartDomain(chart: Locator) {
return chart.evaluate((element) => ({
xMin: Number(element.getAttribute("data-view-x-min")),
xMax: Number(element.getAttribute("data-view-x-max")),
yMin: Number(element.getAttribute("data-view-y-min")),
yMax: Number(element.getAttribute("data-view-y-max")),
}));
}
async function dragChartSelection(
page: Page,
chart: Locator,
start: { x: number; y: number },
end: { x: number; y: number },
) {
const bounds = await chart.boundingBox();
expect(bounds).not.toBeNull();
await page.mouse.move(
bounds!.x + bounds!.width * start.x,
bounds!.y + bounds!.height * start.y,
);
await page.mouse.down();
await page.mouse.move(
bounds!.x + bounds!.width * end.x,
bounds!.y + bounds!.height * end.y,
{ steps: 5 },
);
await page.mouse.up();
}
async function wheelAtLocator(
page: Page,
target: Locator,
deltaY = -240,
position = { x: 0.5, y: 0.5 },
) {
const bounds = await target.boundingBox();
expect(bounds).not.toBeNull();
await page.mouse.move(
bounds!.x + bounds!.width * position.x,
bounds!.y + bounds!.height * position.y,
);
await page.mouse.wheel(0, deltaY);
}
async function dragMiddleAtLocator(
page: Page,
target: Locator,
delta = { x: 0.08, y: 0.08 },
) {
const bounds = await target.boundingBox();
expect(bounds).not.toBeNull();
const startX = bounds!.x + bounds!.width * 0.5;
const startY = bounds!.y + bounds!.height * 0.5;
await page.mouse.move(startX, startY);
await page.mouse.down({ button: "middle" });
await page.mouse.move(
startX + bounds!.width * delta.x,
startY + bounds!.height * delta.y,
{ steps: 5 },
);
await page.mouse.up({ button: "middle" });
}
async function readFlowViewport(page: Page) {
return page
.locator(".results-system-canvas .react-flow__viewport")
.evaluate((viewport) => {
const values =
getComputedStyle(viewport)
.transform.match(/-?\d*\.?\d+(?:e[-+]?\d+)?/gi)
?.map(Number) ?? [];
return {
x: values[4] ?? 0,
y: values[5] ?? 0,
zoom: values[0] ?? 1,
};
});
}
test.beforeEach(async ({ page }) => {
await prepareApp(page);
await page.addInitScript((snapshot) => {
window.sessionStorage.setItem(
"system-simulation-flow:latest-result",
JSON.stringify(snapshot),
);
}, chartWindowSnapshot);
});
test("机械状态事件力点不参与连续曲线纵轴,但原始数据与非力状态保持不变", async ({
page,
}) => {
await page.addInitScript((snapshot) => {
window.sessionStorage.setItem(
"system-simulation-flow:latest-result",
JSON.stringify(snapshot),
);
}, eventForceSnapshot);
await page.goto("/");
await openResults(page);
await page
.locator(".results-variable-list button")
.filter({ hasText: "port_a · 力" })
.first()
.click();
const forceWindow = page
.locator('.result-chart-window[data-chart-kind="single"]')
.filter({ hasText: "port_a.f" });
await expect(forceWindow).toHaveAttribute(
"data-separated-event-samples",
"1",
);
await expect(forceWindow).toContainText("已隐藏 1 个孤立事件力尖峰");
const forceChart = forceWindow.locator('svg[data-result-chart="true"]');
const forceDomain = await readChartDomain(forceChart);
expect(forceDomain.yMax).toBeLessThan(10);
const forcePath = await forceChart
.locator('path[data-chart-clipped-series="true"]')
.getAttribute("d");
expect(forcePath?.match(/\bM /g)).toHaveLength(1);
const storedEventForce = await page.evaluate(() => {
const raw = sessionStorage.getItem(
"system-simulation-flow:latest-result",
);
return raw
? JSON.parse(raw).result.series["generic_sensor_1.port_a.f"][2]
: null;
});
expect(storedEventForce).toBe(1e12);
await page
.locator(".results-variable-list button")
.filter({ hasText: "接触力" })
.click();
const componentForceWindow = page
.locator('.result-chart-window[data-chart-kind="single"]')
.filter({ hasText: "generic_sensor_1.force" });
await expect(componentForceWindow).toHaveAttribute(
"data-separated-event-samples",
"1",
);
const componentForceDomain = await readChartDomain(
componentForceWindow.locator('svg[data-result-chart="true"]'),
);
expect(componentForceDomain.yMin).toBeGreaterThan(-10);
const storedComponentForce = await page.evaluate(() => {
const raw = sessionStorage.getItem(
"system-simulation-flow:latest-result",
);
return raw
? JSON.parse(raw).result.series["generic_sensor_1.force"][2]
: null;
});
expect(storedComponentForce).toBe(-1e12);
await page
.locator(".results-variable-list button")
.filter({ hasText: "数值" })
.first()
.click();
const stateWindow = page
.locator('.result-chart-window[data-chart-kind="single"]')
.filter({ hasText: "数值" });
await expect(stateWindow).toHaveAttribute(
"data-separated-event-samples",
"0",
);
const stateDomain = await readChartDomain(
stateWindow.locator('svg[data-result-chart="true"]'),
);
expect(stateDomain.yMax).toBeGreaterThan(1e8);
});
test("组合曲线窗口先创建为空窗,并在删除最后一条曲线后恢复待定", async ({
page,
}) => {
await page.goto("/");
await openResults(page);
await page
.getByRole("button", { name: "新建同单位多曲线对比窗口", exact: true })
.click();
let sameUnitWindow = page.locator(
'.result-chart-window[data-chart-kind="multi"]',
);
await expect(sameUnitWindow).toHaveCount(1);
await expect(sameUnitWindow).toHaveAttribute("data-pending", "true");
await expect(sameUnitWindow).toContainText("等待第一条曲线");
const valueVariable = page
.locator(".results-variable-list button")
.filter({ hasText: "数值" })
.first();
await valueVariable.dragTo(sameUnitWindow);
await expect(sameUnitWindow).toHaveAttribute("data-pending", "false");
await expect(sameUnitWindow).toContainText("1/1 条显示");
await expect(sameUnitWindow).toContainText("无量纲");
await sameUnitWindow
.getByRole("button", {
name: "管理多曲线窗口中的曲线",
exact: true,
})
.click();
await sameUnitWindow.locator(".result-chart-multi-menu-row .remove").click();
await expect(sameUnitWindow).toHaveCount(1);
await expect(sameUnitWindow).toHaveAttribute("data-pending", "true");
await expect(sameUnitWindow).toContainText("等待第一条曲线");
await page.reload();
await openResults(page);
sameUnitWindow = page.locator(
'.result-chart-window[data-chart-kind="multi"]',
);
await expect(sameUnitWindow).toHaveCount(1);
await expect(sameUnitWindow).toHaveAttribute("data-pending", "true");
await page
.getByRole("button", { name: "新建异单位上下对比窗口", exact: true })
.click();
const mixedWindow = page.locator(
'.result-chart-window[data-chart-kind="mixed"]',
);
await expect(mixedWindow).toHaveCount(1);
await expect(mixedWindow).toHaveAttribute("data-pending", "true");
const pressureVariable = page
.locator(".results-variable-list button")
.filter({ hasText: "压力" })
.first();
await pressureVariable.dragTo(mixedWindow);
await expect(mixedWindow).toHaveAttribute("data-pending", "false");
await expect(mixedWindow).toContainText("Pa");
await mixedWindow
.getByRole("button", {
name: "管理多曲线窗口中的曲线",
exact: true,
})
.click();
await mixedWindow.locator(".result-chart-multi-menu-row .remove").click();
await expect(mixedWindow).toHaveCount(1);
await expect(mixedWindow).toHaveAttribute("data-pending", "true");
});
test("a pending window recovered from stale variable keys accepts a new first curve", async ({
page,
}) => {
await page.addInitScript((snapshotId) => {
window.sessionStorage.setItem(
`system-simulation-flow:result-layout:${snapshotId}`,
JSON.stringify([
{
id: "chart-1",
kind: "multi",
variableKey: "removed.variable",
variableKeys: ["removed.variable"],
hiddenVariableKeys: [],
variableUnits: { "removed.variable": "Pa" },
x: 40,
y: 40,
width: 620,
height: 330,
unit: "Pa",
cursorEnabled: false,
zIndex: 1,
},
]),
);
}, chartWindowSnapshot.id);
await page.goto("/");
await openResults(page);
const pendingWindow = page.locator(
'.result-chart-window[data-chart-kind="multi"]',
);
await expect(pendingWindow).toHaveAttribute("data-pending", "true");
await page
.locator(".results-variable-list button")
.first()
.dragTo(pendingWindow);
await expect(pendingWindow).toHaveAttribute("data-pending", "false");
});
test("single curve supports box zoom, original-size restore, and persisted interaction mode", async ({
page,
}) => {
await page.goto("/");
await openResults(page);
await page
.locator(".results-variable-list button")
.filter({ hasText: "数值" })
.first()
.click();
let chartWindow = page.locator(
'.result-chart-window[data-chart-kind="single"]',
);
let chart = chartWindow.locator('svg[data-result-chart="true"]');
await expect(chart).toBeVisible();
const initial = await readChartDomain(chart);
const zoomButton = chartWindow.getByRole("button", {
name: "打开 数值 曲线缩放",
exact: true,
});
await zoomButton.click();
await expect(
chartWindow.getByRole("button", {
name: "关闭 数值 曲线缩放",
exact: true,
}),
).toHaveAttribute("aria-pressed", "true");
await dragChartSelection(
page,
chart,
{ x: 0.32, y: 0.3 },
{ x: 0.76, y: 0.72 },
);
await expect(chart).toHaveAttribute("data-zoomed", "true");
await expect(chart.locator(".result-chart-zoom-selection")).toHaveCount(0);
await expect(chart.locator('[data-chart-clipped-series="true"]')).toHaveCount(
1,
);
const zoomed = await readChartDomain(chart);
expect(zoomed.xMin).toBeGreaterThan(initial.xMin);
expect(zoomed.xMax).toBeLessThan(initial.xMax);
expect(zoomed.yMin).toBeGreaterThan(initial.yMin);
expect(zoomed.yMax).toBeLessThan(initial.yMax);
await expect
.poll(() =>
page.evaluate((snapshotId) => {
const raw = sessionStorage.getItem(
`system-simulation-flow:result-layout:${snapshotId}`,
);
const windows = raw ? JSON.parse(raw) : [];
return windows[0]?.viewport?.x?.start ?? 0;
}, chartWindowSnapshot.id),
)
.toBeGreaterThan(0);
await page.getByRole("tab", { name: "建模", exact: true }).click();
await openResults(page);
chartWindow = page.locator('.result-chart-window[data-chart-kind="single"]');
chart = chartWindow.locator('svg[data-result-chart="true"]');
await expect(chart).toHaveAttribute("data-zoomed", "true");
const restored = await readChartDomain(chart);
expect(restored.xMin).toBeCloseTo(zoomed.xMin, 8);
expect(restored.xMax).toBeCloseTo(zoomed.xMax, 8);
await expect(
chartWindow.getByRole("button", {
name: "关闭 数值 曲线缩放",
exact: true,
}),
).toHaveAttribute("aria-pressed", "true");
await chartWindow
.getByRole("button", { name: "打开 数值 曲线游标", exact: true })
.click();
await expect(
chartWindow.getByRole("button", {
name: "打开 数值 曲线缩放",
exact: true,
}),
).toHaveAttribute("aria-pressed", "false");
await chartWindow
.getByRole("button", {
name: "恢复 数值 原始尺寸",
exact: true,
})
.click();
await expect(chart).toHaveAttribute("data-zoomed", "false");
const undone = await readChartDomain(chart);
expect(undone.xMin).toBeCloseTo(initial.xMin, 8);
expect(undone.xMax).toBeCloseTo(initial.xMax, 8);
await chartWindow
.getByRole("button", { name: "打开 数值 曲线缩放", exact: true })
.click();
await dragChartSelection(
page,
chart,
{ x: 0.3, y: 0.28 },
{ x: 0.7, y: 0.68 },
);
await chartWindow
.getByRole("button", { name: "恢复 数值 原始尺寸", exact: true })
.click();
await expect(chart).toHaveAttribute("data-zoomed", "false");
});
test("combined curves share X zoom while stacked Y zoom stays in the selected band", async ({
page,
}) => {
await page.goto("/");
await openResults(page);
const valueVariable = page
.locator(".results-variable-list button")
.filter({ hasText: "数值" })
.first();
const pressureVariable = page
.locator(".results-variable-list button")
.filter({ hasText: "压力" })
.first();
await page
.getByRole("button", { name: "新建同单位多曲线对比窗口", exact: true })
.click();
const multiWindow = page.locator(
'.result-chart-window[data-chart-kind="multi"]',
);
await valueVariable.dragTo(multiWindow);
const multiChart = multiWindow.locator('svg[data-result-chart="true"]');
await multiWindow
.getByRole("button", { name: "打开 多曲线 曲线缩放", exact: true })
.click();
await dragChartSelection(
page,
multiChart,
{ x: 0.31, y: 0.34 },
{ x: 0.73, y: 0.72 },
);
await expect(multiChart).toHaveAttribute("data-zoomed", "true");
await expect(
multiChart.locator('[data-chart-clipped-series="true"] path'),
).toHaveCount(1);
const multiCursor = multiWindow.locator("button.cursor");
await multiCursor.click();
await expect(multiWindow.locator(".result-chart-cursor-panel")).toBeVisible();
await multiWindow
.getByRole("button", { name: "管理多曲线窗口中的曲线", exact: true })
.click();
const visibilityToggle = multiWindow.locator(
'.result-chart-multi-menu-row input[type="checkbox"]',
);
await visibilityToggle.click();
await expect(multiCursor).toBeDisabled();
await expect(multiWindow.locator(".result-chart-cursor-panel")).toHaveCount(
0,
);
await visibilityToggle.click();
await expect(multiCursor).toBeEnabled();
await expect(multiCursor).toHaveAttribute("aria-pressed", "false");
await expect(multiWindow.locator(".result-chart-cursor-panel")).toHaveCount(
0,
);
await page
.getByRole("button", { name: "新建异单位上下对比窗口", exact: true })
.click();
const mixedWindow = page.locator(
'.result-chart-window[data-chart-kind="mixed"]',
);
await valueVariable.dragTo(mixedWindow);
await pressureVariable.dragTo(mixedWindow);
const mixedChart = mixedWindow.locator('svg[data-result-chart="true"]');
const beforeXRanges = await mixedChart.evaluate((element) => ({
xMin: Number(element.getAttribute("data-view-x-min")),
xMax: Number(element.getAttribute("data-view-x-max")),
y: JSON.parse(element.getAttribute("data-view-y-ranges") ?? "{}"),
}));
await mixedWindow
.getByRole("button", { name: "打开 多曲线 曲线缩放", exact: true })
.click();
await dragChartSelection(
page,
mixedChart,
{ x: 0.31, y: 0.13 },
{ x: 0.72, y: 0.42 },
);
await expect(mixedChart).toHaveAttribute("data-zoomed", "true");
const afterRanges = await mixedChart.evaluate((element) => ({
xMin: Number(element.getAttribute("data-view-x-min")),
xMax: Number(element.getAttribute("data-view-x-max")),
y: JSON.parse(element.getAttribute("data-view-y-ranges") ?? "{}"),
}));
expect(afterRanges.xMin).toBeGreaterThan(beforeXRanges.xMin);
expect(afterRanges.xMax).toBeLessThan(beforeXRanges.xMax);
expect(afterRanges.y["generic_sensor_1.value"]).not.toEqual(
beforeXRanges.y["generic_sensor_1.value"],
);
expect(afterRanges.y["generic_sensor_1.pressure"]).toEqual(
beforeXRanges.y["generic_sensor_1.pressure"],
);
const secondYAxis = mixedChart.locator('[data-chart-zoom-axis="y"]').nth(1);
await expect(secondYAxis).toBeVisible();
await wheelAtLocator(page, secondYAxis);
await expect
.poll(async () =>
mixedChart.evaluate((element) => {
const ranges = JSON.parse(
element.getAttribute("data-view-y-ranges") ?? "{}",
);
return JSON.stringify(ranges["generic_sensor_1.pressure"]);
}),
)
.not.toBe(JSON.stringify(afterRanges.y["generic_sensor_1.pressure"]));
const axisWheelRanges = await mixedChart.evaluate((element) => ({
xMin: Number(element.getAttribute("data-view-x-min")),
xMax: Number(element.getAttribute("data-view-x-max")),
y: JSON.parse(element.getAttribute("data-view-y-ranges") ?? "{}"),
}));
expect(axisWheelRanges.xMin).toBeCloseTo(afterRanges.xMin, 8);
expect(axisWheelRanges.xMax).toBeCloseTo(afterRanges.xMax, 8);
expect(axisWheelRanges.y["generic_sensor_1.value"]).toEqual(
afterRanges.y["generic_sensor_1.value"],
);
expect(axisWheelRanges.y["generic_sensor_1.pressure"]).not.toEqual(
afterRanges.y["generic_sensor_1.pressure"],
);
await dragMiddleAtLocator(page, secondYAxis, { x: 0, y: 0.08 });
const bandPannedRanges = await mixedChart.evaluate((element) => ({
xMin: Number(element.getAttribute("data-view-x-min")),
xMax: Number(element.getAttribute("data-view-x-max")),
y: JSON.parse(element.getAttribute("data-view-y-ranges") ?? "{}"),
}));
expect(bandPannedRanges.xMin).toBeCloseTo(axisWheelRanges.xMin, 8);
expect(bandPannedRanges.xMax).toBeCloseTo(axisWheelRanges.xMax, 8);
expect(bandPannedRanges.y["generic_sensor_1.value"]).toEqual(
axisWheelRanges.y["generic_sensor_1.value"],
);
const beforePressure = axisWheelRanges.y["generic_sensor_1.pressure"];
const afterPressure = bandPannedRanges.y["generic_sensor_1.pressure"];
expect(afterPressure[1] - afterPressure[0]).toBeCloseTo(
beforePressure[1] - beforePressure[0],
8,
);
expect(afterPressure[0]).toBeGreaterThan(beforePressure[0]);
await expect(
mixedChart.locator('[data-chart-clipped-series="true"]'),
).toHaveCount(2);
});
test("result system viewport survives modeling and results tab switches", async ({
page,
}) => {
await page.goto("/");
await openResults(page);
const pane = page.locator(".results-system-canvas .react-flow__pane");
const paneBounds = await pane.boundingBox();
expect(paneBounds).not.toBeNull();
await page.mouse.move(
paneBounds!.x + paneBounds!.width * 0.55,
paneBounds!.y + paneBounds!.height * 0.5,
);
const fitted = await readFlowViewport(page);
await page.mouse.wheel(0, -260);
await expect
.poll(async () => (await readFlowViewport(page)).zoom)
.not.toBeCloseTo(fitted.zoom, 3);
const adjusted = await readFlowViewport(page);
await expect
.poll(() =>
page.evaluate((snapshotId) => {
const raw = sessionStorage.getItem(
`system-simulation-flow:result-system-viewport:${snapshotId}`,
);
return raw ? JSON.parse(raw).zoom : 0;
}, chartWindowSnapshot.id),
)
.toBeCloseTo(adjusted.zoom, 3);
await page.getByRole("tab", { name: "建模", exact: true }).click();
await openResults(page);
const restored = await readFlowViewport(page);
expect(restored.x).toBeCloseTo(adjusted.x, 1);
expect(restored.y).toBeCloseTo(adjusted.y, 1);
expect(restored.zoom).toBeCloseTo(adjusted.zoom, 3);
await page
.getByRole("button", { name: "适应系统图窗口", exact: true })
.click();
await expect
.poll(async () => (await readFlowViewport(page)).zoom)
.not.toBeCloseTo(restored.zoom, 3);
});
test("zoom wheel follows the pointer and axis hit areas while cursor keeps the viewport", async ({
page,
}) => {
await page.goto("/");
await openResults(page);
await page
.locator(".results-variable-list button")
.filter({ hasText: "数值" })
.first()
.click();
const chartWindow = page.locator(
'.result-chart-window[data-chart-kind="single"]',
);
const chart = chartWindow.locator('svg[data-result-chart="true"]');
await expect(chart).toBeVisible();
const cursorButton = chartWindow.getByRole("button", {
name: "打开 数值 曲线游标",
exact: true,
});
await cursorButton.click();
await expect(chartWindow.locator(".result-chart-cursor-panel")).toBeVisible();
const zoomButton = chartWindow.getByRole("button", {
name: "打开 数值 曲线缩放",
exact: true,
});
await zoomButton.click();
await expect(cursorButton).toHaveAttribute("aria-pressed", "false");
await expect(chartWindow.locator(".result-chart-cursor-panel")).toHaveCount(
0,
);
const initial = await readChartDomain(chart);
const xAxis = chart.locator('[data-chart-zoom-axis="x"]');
const yAxis = chart.locator('[data-chart-zoom-axis="y"]');
await expect(xAxis).toBeVisible();
await expect(yAxis).toBeVisible();
const xAxisBounds = await xAxis.boundingBox();
const yAxisBounds = await yAxis.boundingBox();
expect(xAxisBounds).not.toBeNull();
expect(yAxisBounds).not.toBeNull();
const plotPointer = { x: 0.25, y: 0.35 };
const initialXAnchor =
initial.xMin + (initial.xMax - initial.xMin) * plotPointer.x;
const initialYAnchor =
initial.yMax - (initial.yMax - initial.yMin) * plotPointer.y;
await page.mouse.move(
xAxisBounds!.x + xAxisBounds!.width * plotPointer.x,
yAxisBounds!.y + yAxisBounds!.height * plotPointer.y,
);
await page.mouse.wheel(0, -240);
await expect
.poll(async () => {
const current = await readChartDomain(chart);
return current.xMax - current.xMin;
})
.toBeLessThan(initial.xMax - initial.xMin);
const plotZoomed = await readChartDomain(chart);
expect(plotZoomed.yMax - plotZoomed.yMin).toBeLessThan(
initial.yMax - initial.yMin,
);
const zoomedXAnchor =
plotZoomed.xMin + (plotZoomed.xMax - plotZoomed.xMin) * plotPointer.x;
const zoomedYAnchor =
plotZoomed.yMax - (plotZoomed.yMax - plotZoomed.yMin) * plotPointer.y;
expect(
Math.abs(zoomedXAnchor - initialXAnchor) / (initial.xMax - initial.xMin),
).toBeLessThan(0.002);
expect(
Math.abs(zoomedYAnchor - initialYAnchor) / (initial.yMax - initial.yMin),
).toBeLessThan(0.002);
await wheelAtLocator(page, xAxis);
await expect
.poll(async () => {
const current = await readChartDomain(chart);
return current.xMax - current.xMin;
})
.toBeLessThan(plotZoomed.xMax - plotZoomed.xMin);
const xOnlyZoomed = await readChartDomain(chart);
expect(xOnlyZoomed.yMin).toBeCloseTo(plotZoomed.yMin, 8);
expect(xOnlyZoomed.yMax).toBeCloseTo(plotZoomed.yMax, 8);
await wheelAtLocator(page, yAxis);
await expect
.poll(async () => {
const current = await readChartDomain(chart);
return current.yMax - current.yMin;
})
.toBeLessThan(xOnlyZoomed.yMax - xOnlyZoomed.yMin);
const yOnlyZoomed = await readChartDomain(chart);
expect(yOnlyZoomed.xMin).toBeCloseTo(xOnlyZoomed.xMin, 8);
expect(yOnlyZoomed.xMax).toBeCloseTo(xOnlyZoomed.xMax, 8);
const beforePlotPan = yOnlyZoomed;
await dragMiddleAtLocator(page, chart, { x: 0.05, y: 0.04 });
const plotPanned = await readChartDomain(chart);
expect(plotPanned.xMax - plotPanned.xMin).toBeCloseTo(
beforePlotPan.xMax - beforePlotPan.xMin,
8,
);
expect(plotPanned.yMax - plotPanned.yMin).toBeCloseTo(
beforePlotPan.yMax - beforePlotPan.yMin,
8,
);
expect(plotPanned.xMin).toBeLessThan(beforePlotPan.xMin);
expect(plotPanned.yMin).toBeGreaterThan(beforePlotPan.yMin);
const beforeXAxisPan = plotPanned;
await dragMiddleAtLocator(page, xAxis, { x: 0.06, y: 0 });
const xAxisPanned = await readChartDomain(chart);
expect(xAxisPanned.xMax - xAxisPanned.xMin).toBeCloseTo(
beforeXAxisPan.xMax - beforeXAxisPan.xMin,
8,
);
expect(xAxisPanned.xMin).toBeLessThan(beforeXAxisPan.xMin);
expect(xAxisPanned.yMin).toBeCloseTo(beforeXAxisPan.yMin, 8);
expect(xAxisPanned.yMax).toBeCloseTo(beforeXAxisPan.yMax, 8);
const beforeYAxisPan = xAxisPanned;
await dragMiddleAtLocator(page, yAxis, { x: 0, y: 0.06 });
const yAxisPanned = await readChartDomain(chart);
expect(yAxisPanned.yMax - yAxisPanned.yMin).toBeCloseTo(
beforeYAxisPan.yMax - beforeYAxisPan.yMin,
8,
);
expect(yAxisPanned.yMin).toBeGreaterThan(beforeYAxisPan.yMin);
expect(yAxisPanned.xMin).toBeCloseTo(beforeYAxisPan.xMin, 8);
expect(yAxisPanned.xMax).toBeCloseTo(beforeYAxisPan.xMax, 8);
await chartWindow
.getByRole("button", { name: "恢复 数值 原始尺寸", exact: true })
.click();
await page.mouse.move(
xAxisBounds!.x + xAxisBounds!.width * 0.5,
yAxisBounds!.y + yAxisBounds!.height * 0.5,
);
await page.mouse.wheel(0, -180);
await expect(chart).toHaveAttribute("data-zoomed", "true");
const beforeCursor = await readChartDomain(chart);
await cursorButton.click();
await expect(
chartWindow.getByRole("button", {
name: "打开 数值 曲线缩放",
exact: true,
}),
).toHaveAttribute("aria-pressed", "false");
await expect(chartWindow.locator(".result-chart-cursor-panel")).toBeVisible();
const withCursor = await readChartDomain(chart);
expect(withCursor).toEqual(beforeCursor);
await chartWindow
.getByRole("button", { name: "打开 数值 曲线缩放", exact: true })
.click();
await expect(chartWindow.locator(".result-chart-cursor-panel")).toHaveCount(
0,
);
await chartWindow
.getByRole("button", { name: "恢复 数值 原始尺寸", exact: true })
.click();
await page.mouse.move(
xAxisBounds!.x + xAxisBounds!.width * 0.5,
yAxisBounds!.y + yAxisBounds!.height * 0.5,
);
await page.mouse.wheel(0, 900);
await expect
.poll(async () => (await readChartDomain(chart)).xMin)
.toBeLessThan(initial.xMin);
const expandedDomain = await readChartDomain(chart);
expect(expandedDomain.xMax).toBeGreaterThan(initial.xMax);
expect(expandedDomain.yMin).toBeLessThan(initial.yMin);
expect(expandedDomain.yMax).toBeGreaterThan(initial.yMax);
const outline = await chart.evaluate((element) => {
element.focus();
return getComputedStyle(element).outlineStyle;
});
expect(outline).toBe("none");
await expect
.poll(() =>
page.evaluate((snapshotId) => {
const raw = sessionStorage.getItem(
`system-simulation-flow:result-layout:${snapshotId}`,
);
const windows = raw ? JSON.parse(raw) : [];
const single = windows.find(
(item: { kind?: string }) => item.kind === "single",
);
return single?.viewport?.x?.start ?? 0;
}, chartWindowSnapshot.id),
)
.toBeLessThan(0);
await chartWindow
.getByRole("button", { name: "恢复 数值 原始尺寸", exact: true })
.click();
await expect(chart).toHaveAttribute("data-zoomed", "false");
const restoredDomain = await readChartDomain(chart);
expect(restoredDomain).toEqual(initial);
});
test("cursor is unavailable when a zoomed viewport contains no curve", async ({
page,
}) => {
await page.goto("/");
await openResults(page);
await page
.locator(".results-variable-list button")
.filter({ hasText: "数值" })
.first()
.click();
const chartWindow = page.locator(
'.result-chart-window[data-chart-kind="single"]',
);
const chart = chartWindow.locator('svg[data-result-chart="true"]');
await chartWindow
.getByRole("button", { name: "打开 数值 曲线缩放", exact: true })
.click();
const xAxis = chart.locator('[data-chart-zoom-axis="x"]');
const yAxis = chart.locator('[data-chart-zoom-axis="y"]');
const xBounds = await xAxis.boundingBox();
const yBounds = await yAxis.boundingBox();
expect(xBounds).not.toBeNull();
expect(yBounds).not.toBeNull();
await page.mouse.move(
xBounds!.x + xBounds!.width * 0.08,
yBounds!.y + yBounds!.height * 0.08,
);
await page.mouse.down();
await page.mouse.move(
xBounds!.x + xBounds!.width * 0.32,
yBounds!.y + yBounds!.height * 0.28,
{ steps: 5 },
);
await page.mouse.up();
await expect(chart).toHaveAttribute("data-zoomed", "true");
const cursorButton = chartWindow.locator("button.cursor");
await expect(cursorButton).toBeDisabled();
await chartWindow
.getByRole("button", { name: "恢复 数值 原始尺寸", exact: true })
.click();
await expect(cursorButton).toBeEnabled();
await expect(cursorButton).toHaveAttribute("aria-pressed", "false");
});