Files
SystemSimulationApp/frontend/tests/e2e/fixtures.ts
T

342 lines
9.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { expect, type Page } from "@playwright/test";
const physicalPort = (
name: string,
side: "left" | "right",
nominalRole: "inlet" | "outlet" | "bidirectional",
) => ({
name,
kind: "physical",
domain: "pneumatic",
nominalRole,
positiveFlowDirection: "intoComponent",
side,
order: side === "left" ? 0 : 1,
});
export const componentCatalog = {
schemaVersion: 1,
libraries: [
{
id: "e2e-library",
label: "自动化测试组件库",
version: "1.0.0",
sourcePackage: "tests.e2e",
temporary: true,
order: 1,
components: [
{
type: "amesim_ideal_air_medium",
label: "空气介质定义",
modelType: "amesim_ideal_air_medium",
modelVersion: "0.2.0",
symbol: "amesim_ideal_air_medium",
order: 1,
category: {
id: "media",
label: "介质物性",
order: 1,
},
role: "amesimGasMediumDefinition",
ports: [],
parameters: [
{
name: "gi",
label: "介质定义索引(gi)",
quantity: "dimensionless",
unit: "",
default: 1,
minimum: 1,
maximum: 99,
minimumExclusive: false,
description: "介质定义索引由画布自动分配;索引 0 保留给内置理想空气。",
},
{
name: "property_model",
label: "物性计算模型",
quantity: "dimensionless",
unit: "",
default: 0,
minimum: 0,
maximum: 0,
minimumExclusive: false,
editor: "amesimGasPropertyModel",
options: [{ value: 0, label: "理想气体" }],
description: "选择空气介质采用的物性计算模型。",
},
],
},
{
type: "amesim_helium_medium",
label: "氦气介质定义",
modelType: "amesim_helium_medium",
modelVersion: "0.1.0",
symbol: "amesim_helium_medium",
order: 2,
category: {
id: "media",
label: "介质物性",
order: 1,
},
role: "amesimGasMediumDefinition",
ports: [],
parameters: [
{
name: "gi",
label: "介质定义索引(gi)",
quantity: "dimensionless",
unit: "",
default: 1,
minimum: 1,
maximum: 99,
minimumExclusive: false,
description: "介质定义索引由画布自动分配;索引 0 保留给内置理想空气。",
},
{
name: "property_model",
label: "物性计算模型",
quantity: "dimensionless",
unit: "",
default: 0,
minimum: 0,
maximum: 0,
minimumExclusive: false,
editor: "amesimGasPropertyModel",
options: [{ value: 0, label: "Peng–Robinson" }],
description: "氦气当前使用 Peng–Robinson 状态方程计算物性。",
},
],
},
{
type: "amesim_pneumatic_test",
label: "AMESim 气动测试元件",
modelType: "amesim_pneumatic_test",
modelVersion: "1.0.0",
symbol: "symbol-not-registered",
order: 3,
category: {
id: "pneumatic",
label: "气动元件",
order: 2,
},
ports: [
physicalPort("port_a", "left", "bidirectional"),
physicalPort("port_b", "right", "bidirectional"),
],
parameters: [
{
name: "gi",
label: "介质物性模型(gi)",
quantity: "dimensionless",
unit: "",
default: 0,
minimum: 0,
maximum: 99,
minimumExclusive: false,
editor: "amesimGasReference",
description: "选择该气动元件引用的画布介质定义索引;0 表示内置理想空气。",
},
{
name: "reference_pressure",
label: "参考压力",
quantity: "pressure",
unit: "Pa",
default: 100000,
minimum: 0,
minimumExclusive: true,
description: "用于验证参数表单位换算和 XML 基础单位输出的参考压力。",
},
],
},
{
type: "generic_sensor",
label: "通用测试元件",
modelType: "generic_sensor",
modelVersion: "1.0.0",
symbol: "symbol-not-registered",
order: 4,
category: {
id: "generic",
label: "通用组件",
order: 1,
},
ports: [
physicalPort("port_a", "left", "bidirectional"),
physicalPort("port_b", "right", "bidirectional"),
],
parameters: [
{
name: "gain",
label: "增益",
quantity: "dimensionless",
unit: "",
default: 1,
minimumExclusive: false,
description: "传感器输入值的无量纲放大倍数。",
},
],
},
],
},
],
};
const projectPort = (
name: string,
side: "left" | "right",
nominalRole: "inlet" | "outlet" | "bidirectional",
) => ({
name,
kind: "physical",
domain: "pneumatic",
nominalRole,
positiveFlowDirection: "intoComponent",
side,
});
export const wideProject = {
name: "demo-system",
nodes: Array.from({ length: 4 }, (_, index) => ({
id: `generic_sensor_${index + 1}`,
type: "simulationComponent",
position: {
x: index * 620,
y: index % 2 === 0 ? 0 : 260,
},
data: {
label: `generic_sensor_${index + 1}`,
componentType: "generic_sensor",
modelType: "generic_sensor",
symbol: "symbol-not-registered",
ports: [
projectPort("port_a", "left", "bidirectional"),
projectPort("port_b", "right", "bidirectional"),
],
parameters: { gain: 1 },
parameterUnits: { gain: "" },
rotation: 0,
mirrored: false,
},
})),
edges: Array.from({ length: 3 }, (_, index) => ({
id: `edge-${index + 1}`,
source: `generic_sensor_${index + 1}`,
target: `generic_sensor_${index + 2}`,
sourceHandle: "port_b",
targetHandle: "port_a",
})),
simulation: {
t_start: 0,
t_stop: 10,
step: 0.1,
max_step: 0.05,
method: "RK45",
},
};
export const resultSnapshot = {
id: "e2e-result",
createdAt: "2026-07-28T10:00:00.000Z",
project: wideProject,
result: {
success: true,
status: "completed",
partial: false,
message: "Simulation completed",
simulatedUntil: 10,
requestedStopTime: 10,
variables: [
{
key: "generic_sensor_1.value",
componentId: "generic_sensor_1",
componentType: "generic_sensor",
scope: "component",
portName: null,
name: "value",
label: "数值",
quantity: "dimensionless",
unit: "",
category: "state",
order: 1,
},
],
final: { "generic_sensor_1.value": 1 },
series: {
time: [0, 5, 10],
"generic_sensor_1.value": [0, 0.5, 1],
},
diagnostics: {
pressureFlow: {
solveCount: 3,
maxScaledResidual: 0,
maxEvaluationsPerSolve: 1,
},
stream: {
maxIterationsPerSolve: 1,
},
stateCount: 1,
sampleCount: 3,
},
},
};
export async function prepareApp(page: Page) {
await page.addInitScript(() => {
const preparedKey = "system-simulation-flow:e2e-storage-prepared";
if (window.sessionStorage.getItem(preparedKey) !== "1") {
window.localStorage.clear();
window.sessionStorage.clear();
window.sessionStorage.setItem(preparedKey, "1");
}
window.location.hash = "#/modeling";
});
await page.route("**/api/components/catalog", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(componentCatalog),
});
});
}
export async function expectAllNodesInsideCanvas(
page: Page,
canvasSelector: string,
expectedCount = wideProject.nodes.length,
) {
const canvas = page.locator(canvasSelector);
await expect(canvas).toBeVisible();
const nodes = canvas.locator(".react-flow__node");
await expect(nodes).toHaveCount(expectedCount);
await expect
.poll(
async () =>
page.evaluate(
({ selector, count }) => {
const canvasElement = document.querySelector<HTMLElement>(selector);
const nodeElements = Array.from(
canvasElement?.querySelectorAll<HTMLElement>(".react-flow__node") ?? [],
);
if (!canvasElement || nodeElements.length !== count) {
return false;
}
const canvasBounds = canvasElement.getBoundingClientRect();
const tolerance = 3;
return nodeElements.every((node) => {
const bounds = node.getBoundingClientRect();
return (
bounds.left >= canvasBounds.left - tolerance &&
bounds.top >= canvasBounds.top - tolerance &&
bounds.right <= canvasBounds.right + tolerance &&
bounds.bottom <= canvasBounds.bottom + tolerance
);
});
},
{ selector: canvasSelector, count: expectedCount },
),
{ message: `所有节点应完整显示在 ${canvasSelector} 内` },
)
.toBe(true);
}