788 lines
26 KiB
TypeScript
788 lines
26 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],
|
|
},
|
|
},
|
|
};
|
|
|
|
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.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");
|
|
});
|