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"); });