完善通用求解器回归与前端交互
- 引入因果坐标内核、热流体恢复和递进长时回归\n- 完善正交连线、线桥、视图保持与结果曲线缩放\n- 补充依赖约束、CI、测试基线和北京时间更新日志
This commit is contained in:
1 parent
143e8dd309
commit
b435daecf2
65 files changed
+172271
-701
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
import { expect, test, type Locator, type Page } from "@playwright/test";
|
||||
|
||||
import { prepareApp, resultSnapshot } from "./fixtures";
|
||||
|
||||
@@ -39,6 +39,86 @@ async function openResults(page: Page) {
|
||||
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) => {
|
||||
@@ -155,6 +235,553 @@ test("a pending window recovered from stale variable keys accepts a new first cu
|
||||
);
|
||||
await expect(pendingWindow).toHaveAttribute("data-pending", "true");
|
||||
|
||||
await page.locator(".results-variable-list button").first().dragTo(pendingWindow);
|
||||
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");
|
||||
});
|
||||
Reference in new issue
Block a user