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

585 lines
17 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 基础单位输出的参考压力。",
},
{
name: "reference_temperature",
label: "参考温度",
quantity: "temperature",
unit: "K",
default: 300,
minimum: 0,
minimumExclusive: false,
description: "用于验证带偏移量单位的数值换算和显示精度。",
},
],
},
{
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: "传感器输入值的无量纲放大倍数。",
},
],
},
{
type: "choice_visibility_test",
label: "离散参数测试元件",
modelType: "choice_visibility_test",
modelVersion: "1.0.0",
symbol: "symbol-not-registered",
order: 5,
category: {
id: "generic",
label: "通用组件",
order: 1,
},
ports: [],
parameters: [
{
name: "mode",
label: "工作模式",
quantity: "dimensionless",
unit: "",
default: 1,
minimum: 1,
maximum: 3,
minimumExclusive: false,
editor: "choice",
options: [
{ value: 1, label: "基础模式" },
{ value: 2, label: "高级模式" },
{ value: 3, label: "专家模式" },
],
},
{
name: "variant",
label: "高级变体",
quantity: "dimensionless",
unit: "",
default: 10,
minimum: 10,
maximum: 20,
minimumExclusive: false,
editor: "choice",
options: [
{ value: 10, label: "变体 A" },
{ value: 20, label: "变体 B" },
],
},
{
name: "basic_value",
label: "基础参数",
quantity: "dimensionless",
unit: "",
default: 1,
minimumExclusive: false,
visibleWhen: [{ parameter: "mode", values: [1] }],
},
{
name: "advanced_value",
label: "高级参数",
quantity: "dimensionless",
unit: "",
default: 1,
minimumExclusive: false,
visibleWhen: [{ parameter: "mode", values: [2, 3] }],
},
{
name: "combined_value",
label: "组合参数",
quantity: "dimensionless",
unit: "",
default: 3,
minimumExclusive: false,
visibleWhen: [
{ parameter: "mode", values: [2, 3] },
{ parameter: "variant", values: [20] },
],
},
],
},
{
type: "amesim_mecmas21",
label: "MECMAS21 编码测试元件",
modelType: "amesim_mecmas21",
modelVersion: "1.0.0",
symbol: "symbol-not-registered",
order: 6,
category: {
id: "generic",
label: "通用组件",
order: 1,
},
ports: [],
parameterGroups: [
{
id: "friction",
label: "摩擦",
order: 10,
parameters: ["frictionType", "strib", "rvisc"],
defaultExpanded: false,
},
{
id: "endstops",
label: "限位",
order: 20,
parameters: ["discContactOption", "xmin"],
defaultExpanded: false,
},
],
parameters: [
{
name: "mass",
label: "质量",
quantity: "mass",
unit: "kg",
default: 1,
minimum: 0,
minimumExclusive: true,
},
{
name: "rvisc",
label: "黏性摩擦系数",
quantity: "translational_damping",
unit: "N/(m/s)",
default: 0,
minimum: 0,
minimumExclusive: false,
description: "用于验证折叠摩擦参数仍支持详情提示和值保留。",
visibleWhen: [{ parameter: "useFriction", values: [2] }],
},
{
name: "xmin",
label: "下位移限位",
quantity: "length",
unit: "m",
default: -1,
minimumExclusive: false,
visibleWhen: [{ parameter: "stoptype", values: [1, 2, 3] }],
},
{
name: "useFriction",
label: "启用摩擦",
quantity: "dimensionless",
unit: "",
default: 1,
minimum: 1,
maximum: 2,
minimumExclusive: false,
editor: "choice",
options: [
{ value: 1, label: "禁用" },
{ value: 2, label: "启用" },
],
},
{
name: "stoptype",
label: "限位类型",
quantity: "dimensionless",
unit: "",
default: 4,
minimum: 1,
maximum: 4,
minimumExclusive: false,
editor: "choice",
options: [
{ value: 1, label: "理想限位" },
{ value: 2, label: "弹性限位" },
{ value: 3, label: "恢复碰撞" },
{ value: 4, label: "无限位" },
],
},
{
name: "discContactOption",
label: "允许负接触力",
quantity: "dimensionless",
unit: "",
default: 1,
minimum: 1,
maximum: 2,
minimumExclusive: false,
editor: "choice",
options: [
{ value: 1, label: "允许负接触力" },
{ value: 2, label: "不允许负接触力" },
],
visibleWhen: [{ parameter: "stoptype", values: [2] }],
},
{
name: "strib",
label: "Stribeck 选项",
quantity: "dimensionless",
unit: "",
default: 1,
minimum: 1,
maximum: 2,
minimumExclusive: false,
editor: "choice",
options: [
{ value: 1, label: "禁用" },
{ value: 2, label: "启用" },
],
visibleWhen: [
{ parameter: "useFriction", values: [2] },
{ parameter: "stoptype", values: [1, 2, 4] },
{ parameter: "frictionType", values: [2] },
],
},
{
name: "frictionType",
label: "摩擦类型",
quantity: "dimensionless",
unit: "",
default: 1,
minimum: 1,
maximum: 2,
minimumExclusive: false,
editor: "choice",
options: [
{ value: 1, label: "简单" },
{ value: 2, label: "高级" },
],
visibleWhen: [
{ parameter: "useFriction", values: [2] },
{ parameter: "stoptype", values: [1, 2, 4] },
],
},
{
name: "unrelatedCode",
label: "无关编码",
quantity: "dimensionless",
unit: "",
default: 7,
minimumExclusive: false,
},
],
},
],
},
],
};
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);
}