123 lines
6.5 KiB
TypeScript
123 lines
6.5 KiB
TypeScript
import { expect, test, type Page } from "@playwright/test";
|
|
import { execFileSync } from "node:child_process";
|
|
import { existsSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import {
|
|
normalizePortComputation, portConnectionIssue, referenceSupplyIssues,
|
|
} from "../../src/portComputation";
|
|
import { expandSimulationConsole, prepareApp } from "./fixtures";
|
|
|
|
const root = fileURLToPath(new URL("../../..", import.meta.url));
|
|
const localPython = resolve(root, ".venv-win/Scripts/python.exe");
|
|
const python = process.env.TEST_PYTHON ?? (existsSync(localPython) ? localPython : "python");
|
|
// Use real backend metadata: frontend/backend drift must fail this test.
|
|
const catalog = JSON.parse(execFileSync(python, ["-c",
|
|
"import json; from app.simulation.registry import build_component_catalog; print(json.dumps(build_component_catalog()))",
|
|
], { cwd: root, encoding: "utf8" }));
|
|
const definitions = new Map<string, any>(catalog.libraries.flatMap((library: any) =>
|
|
library.components.map((component: any) => [component.modelType, component])));
|
|
const port = (model: string, name: string) => definitions.get(model).ports.find((p: any) => p.name === name);
|
|
|
|
function node(id: string, model: string, x: number, y = 0) {
|
|
const definition = definitions.get(model);
|
|
return { id, type: "simulationComponent", position: { x, y }, data: {
|
|
label: id, componentType: model, modelType: model, modelVersion: definition.modelVersion,
|
|
symbol: definition.symbol, ports: definition.ports.map((p: any) => ({ ...p })),
|
|
parameters: Object.fromEntries(definition.parameters.map((p: any) => [p.name, p.name === "gi" ? 0 : p.default])),
|
|
parameterUnits: Object.fromEntries(definition.parameters.map((p: any) => [p.name, p.unit])),
|
|
rotation: 0, mirrored: false,
|
|
} };
|
|
}
|
|
|
|
function project(wrong = false) {
|
|
const nodes = [node("junction", "amesim_p4node2", 330),
|
|
node("valve", "amesim_pnvo001_fixed", 0), node("storage", "amesim_pnl0001", 660)];
|
|
if (wrong) {
|
|
// A forged/old snapshot must not replace the current catalog contract.
|
|
nodes[0].data.ports.forEach((p: any) => { delete p.computation; });
|
|
}
|
|
return { projectSchemaVersion: 1, name: "port-supply-e2e", nodes,
|
|
edges: wrong ? [
|
|
{ id: "wrong-reference", source: "junction", sourceHandle: "port_2",
|
|
target: "valve", targetHandle: "port_2", data: { isContactEdge: false } },
|
|
{ id: "wrong-branch", source: "junction", sourceHandle: "port_1",
|
|
target: "storage", targetHandle: "port_2", data: { isContactEdge: false } },
|
|
] : [],
|
|
simulation: { t_start: 0, t_stop: 0.01, step: 0.001, max_step: 0.001, method: "RK45" },
|
|
};
|
|
}
|
|
|
|
async function load(page: Page, value: object) {
|
|
await prepareApp(page);
|
|
await page.route("**/api/components/catalog", (route) => route.fulfill({
|
|
contentType: "application/json", body: JSON.stringify(catalog),
|
|
}));
|
|
await page.goto("/");
|
|
await expect(page.locator(".catalog-source.ready")).toBeVisible();
|
|
await page.locator('input[type="file"]').setInputFiles({
|
|
name: "port-supply.json", mimeType: "application/json", buffer: Buffer.from(JSON.stringify(value)),
|
|
});
|
|
await expect(page.locator(".flow-canvas .react-flow__node")).toHaveCount(3);
|
|
}
|
|
|
|
test("真实目录的参考口/支路口供需在两种绘线方向下一致", () => {
|
|
const reference = port("amesim_p4node2", "port_2");
|
|
const branch = port("amesim_p4node2", "port_1");
|
|
const state = port("amesim_pnl0001", "port_2");
|
|
const flow = port("amesim_pnvo001_fixed", "port_2");
|
|
for (const [a, b] of [[reference, state], [branch, flow]]) {
|
|
expect(portConnectionIssue(a, b)).toBeNull();
|
|
expect(portConnectionIssue(b, a)).toBeNull();
|
|
}
|
|
for (const [a, b] of [[reference, flow], [flow, reference]]) {
|
|
expect(portConnectionIssue(a, b)).toContain("温度");
|
|
expect(portConnectionIssue(a, b)).toContain("压力");
|
|
}
|
|
expect(portConnectionIssue(branch, state)).toContain("能量流率");
|
|
expect(portConnectionIssue(flow, port("amesim_pnl00r", "port_1"))).toBeNull();
|
|
expect(() => normalizePortComputation({ mode: "fixed", inputs: ["T"], outputs: ["T"] })).toThrow();
|
|
});
|
|
|
|
test("参考链能追溯储气状态,拒绝无来源循环", () => {
|
|
const nodes = [node("a", "amesim_p4node2", 0), node("b", "amesim_p4node2", 300),
|
|
node("state", "amesim_pnl0001", 600)];
|
|
const edges = [{ source: "a", sourceHandle: "port_2", target: "b", targetHandle: "port_1" },
|
|
{ source: "b", sourceHandle: "port_2", target: "state", targetHandle: "port_2" }];
|
|
expect(referenceSupplyIssues(nodes, edges)).toEqual([]);
|
|
edges[1] = { source: "b", sourceHandle: "port_2", target: "a", targetHandle: "port_1" };
|
|
expect(referenceSupplyIssues(nodes, edges)).toHaveLength(4);
|
|
expect(referenceSupplyIssues(nodes, edges)[0].message).toContain("没有实际提供者");
|
|
});
|
|
|
|
test("画布按变量供需过滤接线,并说明不兼容的具体原因", async ({ page }) => {
|
|
await load(page, project());
|
|
const handle = (id: string, name: string) => page.locator(
|
|
`.react-flow__node[data-id="${id}"] .port-handle[data-port-name="${name}"]`);
|
|
const reference = handle("junction", "port_2");
|
|
await expect(reference).toHaveAttribute("title", /温度、压力参考口/);
|
|
await reference.click();
|
|
await expect(handle("valve", "port_2")).toHaveAttribute("data-connection-state", "incompatible");
|
|
await expect(handle("valve", "port_2")).toHaveAttribute("title", /需要对端提供压力、温度/);
|
|
await expect(handle("storage", "port_2")).toHaveAttribute("data-connection-state", "compatible");
|
|
await handle("storage", "port_2").click();
|
|
await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(1);
|
|
});
|
|
|
|
test("导入时保留供需错误连线,模型检查阻止仿真且不信任旧端口快照", async ({ page }) => {
|
|
let simulations = 0;
|
|
await page.route("**/api/system-xml/simulate-stream", (route) => {
|
|
simulations += 1;
|
|
return route.fulfill({ status: 500, body: "unexpected simulation" });
|
|
});
|
|
await load(page, project(true));
|
|
await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(2);
|
|
await page.getByRole("button", { name: "运行仿真", exact: true }).click();
|
|
await expandSimulationConsole(page);
|
|
const panel = page.getByRole("complementary", { name: "仿真控制台", exact: true });
|
|
await expect(panel).toContainText("需要对端提供压力、温度");
|
|
await expect(panel).toContainText("质量流率、能量流率");
|
|
expect(simulations).toBe(0);
|
|
await expect(page.locator(".flow-canvas .react-flow__edge")).toHaveCount(2);
|
|
});
|