1067 lines
35 KiB
TypeScript
1067 lines
35 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);
|
|
});
|
|
|
|
for (const quantity of ["pressure", "absolute_pressure"]) {
|
|
for (const kind of ["single", "multi", "mixed"] as const) {
|
|
test(`${quantity} 的 ${kind} 图默认 Pa,绝压换算及单位持久化不改变原始数据`, async ({ page }) => {
|
|
await page.addInitScript(({ snapshot, pressureQuantity }) => {
|
|
const pressure = snapshot.result.variables.find(
|
|
(variable) => variable.key === "generic_sensor_1.pressure",
|
|
);
|
|
if (pressure) pressure.quantity = pressureQuantity;
|
|
window.sessionStorage.setItem(
|
|
"system-simulation-flow:latest-result",
|
|
JSON.stringify(snapshot),
|
|
);
|
|
}, { snapshot: chartWindowSnapshot, pressureQuantity: quantity });
|
|
await page.goto("/");
|
|
await openResults(page);
|
|
|
|
const pressureVariable = page
|
|
.locator(".results-variable-list button")
|
|
.filter({ hasText: "压力" })
|
|
.first();
|
|
await expect(pressureVariable.locator("small")).toHaveText("Pa");
|
|
const chartWindow = page.locator(
|
|
`.result-chart-window[data-chart-kind="${kind}"]`,
|
|
);
|
|
if (kind === "single") {
|
|
await pressureVariable.click();
|
|
} else {
|
|
await page.getByRole("button", {
|
|
name: kind === "multi" ? "新建同单位多曲线对比窗口" : "新建异单位上下对比窗口",
|
|
exact: true,
|
|
}).click();
|
|
if (kind === "mixed") {
|
|
await page.locator(".results-variable-list button")
|
|
.filter({ hasText: "数值" }).first().dragTo(chartWindow);
|
|
}
|
|
await pressureVariable.dragTo(chartWindow);
|
|
}
|
|
const chart = chartWindow.locator('svg[data-result-chart="true"]');
|
|
const unitSelect = chartWindow.getByRole("combobox", {
|
|
name: kind === "mixed"
|
|
? "generic_sensor_1.pressure纵轴单位"
|
|
: kind === "multi" ? "多曲线纵轴单位" : "压力纵轴单位",
|
|
exact: true,
|
|
});
|
|
const showUnits = async () => {
|
|
if (kind === "mixed") {
|
|
await chartWindow.getByRole("button", {
|
|
name: "管理多曲线窗口中的曲线", exact: true,
|
|
}).click();
|
|
}
|
|
};
|
|
const readPressureRange = () => chart.evaluate((element) => {
|
|
const ranges = element.getAttribute("data-view-y-ranges");
|
|
return ranges
|
|
? JSON.parse(ranges)["generic_sensor_1.pressure"] as number[]
|
|
: [
|
|
Number(element.getAttribute("data-view-y-min")),
|
|
Number(element.getAttribute("data-view-y-max")),
|
|
];
|
|
});
|
|
await showUnits();
|
|
await expect(unitSelect).toHaveValue("Pa");
|
|
await expect(unitSelect.locator('option[value="barA"]')).toHaveCount(0);
|
|
await expect(chart.locator(".result-chart-unit")).toContainText(["Pa"]);
|
|
const pascalRange = await readPressureRange();
|
|
expect(pascalRange[0]).toBeCloseTo(98400, 6);
|
|
expect(pascalRange[1]).toBeCloseTo(121600, 6);
|
|
|
|
await unitSelect.selectOption("bar");
|
|
await expect(unitSelect).toHaveValue("bar");
|
|
const barRange = await readPressureRange();
|
|
expect(barRange[0]).toBeCloseTo(0.984, 6);
|
|
expect(barRange[1]).toBeCloseTo(1.216, 6);
|
|
if (kind === "multi") {
|
|
await expect(chartWindow.locator(".result-chart-window-title")).toContainText("bar");
|
|
await chartWindow.getByRole("button", {
|
|
name: "管理多曲线窗口中的曲线", exact: true,
|
|
}).click();
|
|
await expect(chartWindow.locator(".result-chart-multi-menu-label small")).toContainText("bar");
|
|
}
|
|
|
|
const storedPressure = await page.evaluate(() => {
|
|
const snapshot = JSON.parse(sessionStorage.getItem(
|
|
"system-simulation-flow:latest-result",
|
|
) ?? "null");
|
|
return {
|
|
values: snapshot.result.series["generic_sensor_1.pressure"],
|
|
final: snapshot.result.final["generic_sensor_1.pressure"],
|
|
unit: snapshot.result.variables.find(
|
|
(variable: { key: string }) => variable.key === "generic_sensor_1.pressure",
|
|
).unit,
|
|
};
|
|
});
|
|
expect(storedPressure).toEqual({
|
|
values: [100000, 110000, 120000], final: 120000, unit: "Pa",
|
|
});
|
|
|
|
await page.reload();
|
|
await openResults(page);
|
|
await showUnits();
|
|
await expect(unitSelect).toHaveValue("bar");
|
|
expect(await readPressureRange()).toEqual(barRange);
|
|
await unitSelect.selectOption("Pa");
|
|
expect(await readPressureRange()).toEqual(pascalRange);
|
|
});
|
|
}
|
|
}
|
|
|
|
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");
|
|
});
|