feat: integrate AMESim media models and editor UI
This commit is contained in:
1 parent
3e4f466114
commit
e7177ab03e
181 files changed
+7042
-25270
No files matched your search
@@ -0,0 +1,202 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { prepareApp } from "./fixtures";
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await prepareApp(page);
|
||||
});
|
||||
|
||||
test("拖入介质定义后可供气动元件选择,删除定义会回退到内置空气", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
|
||||
const canvas = page.locator(".flow-canvas .react-flow__pane");
|
||||
await page
|
||||
.getByRole("button", { name: /空气介质定义/ })
|
||||
.dragTo(canvas, { targetPosition: { x: 280, y: 220 } });
|
||||
|
||||
const mediumNode = page.locator(
|
||||
'.flow-canvas .react-flow__node[data-id="amesim_ideal_air_medium_1"]',
|
||||
);
|
||||
await expect(mediumNode).toBeVisible();
|
||||
await mediumNode.click();
|
||||
await expect(page.getByLabel("介质定义索引(自动分配)")).toHaveValue("1");
|
||||
const propertyModelSelect = page.getByLabel("物性计算模型");
|
||||
await expect(propertyModelSelect).toHaveValue("0");
|
||||
await expect(propertyModelSelect.locator('option[value="0"]')).toHaveText(
|
||||
"理想气体",
|
||||
);
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: /AMESim 气动测试元件/ })
|
||||
.dragTo(canvas, { targetPosition: { x: 650, y: 360 } });
|
||||
|
||||
const pneumaticNode = page.locator(
|
||||
'.flow-canvas .react-flow__node[data-id="amesim_pneumatic_test_1"]',
|
||||
);
|
||||
await expect(pneumaticNode).toBeVisible();
|
||||
await pneumaticNode.click();
|
||||
|
||||
const mediumSelect = page.getByLabel("介质物性索引");
|
||||
await expect(mediumSelect).toHaveValue("0");
|
||||
await expect(mediumSelect.locator('option[value="0"]')).toHaveText("0");
|
||||
await expect(mediumSelect.locator('option[value="1"]')).toHaveText("1");
|
||||
|
||||
await mediumSelect.selectOption("1");
|
||||
await expect(mediumSelect).toHaveValue("1");
|
||||
|
||||
await mediumNode.click();
|
||||
await page.getByRole("button", { name: "删除 (Delete)" }).click();
|
||||
await expect(mediumNode).toHaveCount(0);
|
||||
|
||||
await pneumaticNode.click();
|
||||
await expect(page.getByLabel("介质物性索引")).toHaveValue("0");
|
||||
|
||||
const consolePanel = page.getByRole("complementary", {
|
||||
name: "仿真控制台",
|
||||
exact: true,
|
||||
});
|
||||
await page.getByRole("button", { name: "展开仿真控制台" }).click();
|
||||
await expect(consolePanel).toContainText(
|
||||
"介质定义已删除,1 个介质物性引用已切回 0 — 空气(理想气体,内置默认)",
|
||||
);
|
||||
|
||||
await page.getByRole("button", { name: "生成系统 XML" }).click();
|
||||
const xmlBlock = consolePanel.getByLabel("生成的系统 XML");
|
||||
await expect(xmlBlock).toContainText('mediumReferenceVersion="1"');
|
||||
await expect(xmlBlock).toContainText('<Parameter name="gi" value="0"');
|
||||
});
|
||||
|
||||
test("介质定义的物性计算模型会写入 XML", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const canvas = page.locator(".flow-canvas .react-flow__pane");
|
||||
await page
|
||||
.getByRole("button", { name: /空气介质定义/ })
|
||||
.dragTo(canvas, { targetPosition: { x: 280, y: 220 } });
|
||||
|
||||
const mediumNode = page.locator(
|
||||
'.flow-canvas .react-flow__node[data-id="amesim_ideal_air_medium_1"]',
|
||||
);
|
||||
await mediumNode.click();
|
||||
await expect(page.getByLabel("物性计算模型")).toHaveValue("0");
|
||||
|
||||
await page.getByRole("button", { name: "生成系统 XML" }).click();
|
||||
const consolePanel = page.getByRole("complementary", {
|
||||
name: "仿真控制台",
|
||||
exact: true,
|
||||
});
|
||||
await expect(consolePanel.getByLabel("生成的系统 XML")).toContainText(
|
||||
'<Parameter name="property_model" value="0"',
|
||||
);
|
||||
});
|
||||
|
||||
test("氦气介质自动分配 gi,并以 Peng–Robinson 模型写入 XML", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
|
||||
const canvas = page.locator(".flow-canvas .react-flow__pane");
|
||||
await page
|
||||
.getByRole("button", { name: /空气介质定义/ })
|
||||
.dragTo(canvas, { targetPosition: { x: 240, y: 180 } });
|
||||
await page
|
||||
.getByRole("button", { name: /氦气介质定义/ })
|
||||
.dragTo(canvas, { targetPosition: { x: 520, y: 180 } });
|
||||
|
||||
const heliumNode = page.locator(
|
||||
'.flow-canvas .react-flow__node[data-id="amesim_helium_medium_1"]',
|
||||
);
|
||||
await expect(heliumNode).toBeVisible();
|
||||
await heliumNode.click();
|
||||
await expect(page.getByLabel("介质定义索引(自动分配)")).toHaveValue("2");
|
||||
|
||||
const propertyModelSelect = page.getByLabel("物性计算模型");
|
||||
await expect(propertyModelSelect).toHaveValue("0");
|
||||
await expect(propertyModelSelect.locator('option[value="0"]')).toHaveText(
|
||||
"Peng–Robinson",
|
||||
);
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: /AMESim 气动测试元件/ })
|
||||
.dragTo(canvas, { targetPosition: { x: 760, y: 360 } });
|
||||
|
||||
const pneumaticNode = page.locator(
|
||||
'.flow-canvas .react-flow__node[data-id="amesim_pneumatic_test_1"]',
|
||||
);
|
||||
await expect(pneumaticNode).toBeVisible();
|
||||
await pneumaticNode.click();
|
||||
|
||||
const mediumSelect = page.getByLabel("介质物性索引");
|
||||
await expect(mediumSelect.locator("option")).toHaveText(["0", "1", "2"]);
|
||||
await mediumSelect.selectOption("2");
|
||||
await expect(mediumSelect).toHaveValue("2");
|
||||
|
||||
await page.getByRole("button", { name: "生成系统 XML" }).click();
|
||||
const consolePanel = page.getByRole("complementary", {
|
||||
name: "仿真控制台",
|
||||
exact: true,
|
||||
});
|
||||
const xmlBlock = consolePanel.getByLabel("生成的系统 XML");
|
||||
await expect(xmlBlock).toContainText('type="amesim_helium_medium"');
|
||||
await expect(xmlBlock).toContainText('<Parameter name="gi" value="2"');
|
||||
await expect(xmlBlock).toContainText(
|
||||
'<Parameter name="property_model" value="0"',
|
||||
);
|
||||
|
||||
const xml = await xmlBlock.textContent();
|
||||
expect(xml).toMatch(
|
||||
/<Component[^>]+type="amesim_helium_medium"[\s\S]*?<Parameter name="gi" value="2"[\s\S]*?<Parameter name="property_model" value="0"[\s\S]*?<\/Component>/,
|
||||
);
|
||||
expect(xml).toMatch(
|
||||
/<Component[^>]+type="amesim_pneumatic_test"[\s\S]*?<Parameter name="gi" value="2"[\s\S]*?<\/Component>/,
|
||||
);
|
||||
});
|
||||
|
||||
test("导入工程中的未知物性计算模型会被检查出来", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
const project = {
|
||||
name: "invalid-property-model",
|
||||
mediumReferenceVersion: 1,
|
||||
nodes: [
|
||||
{
|
||||
id: "amesim_ideal_air_medium_1",
|
||||
type: "simulationComponent",
|
||||
position: { x: 280, y: 220 },
|
||||
data: {
|
||||
label: "空气介质_1",
|
||||
componentType: "amesim_ideal_air_medium",
|
||||
modelType: "amesim_ideal_air_medium",
|
||||
ports: [],
|
||||
parameters: { gi: 1, property_model: 99 },
|
||||
parameterUnits: {},
|
||||
rotation: 0,
|
||||
mirrored: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
edges: [],
|
||||
simulation: {
|
||||
t_start: 0,
|
||||
t_stop: 1,
|
||||
step: 0.1,
|
||||
max_step: 0.05,
|
||||
method: "RK45",
|
||||
},
|
||||
};
|
||||
|
||||
await page.locator('input[type="file"]').setInputFiles({
|
||||
name: "invalid-property-model.json",
|
||||
mimeType: "application/json",
|
||||
buffer: Buffer.from(JSON.stringify(project)),
|
||||
});
|
||||
await page.getByRole("button", { name: "检查模型" }).click();
|
||||
|
||||
const consolePanel = page.getByRole("complementary", {
|
||||
name: "仿真控制台",
|
||||
exact: true,
|
||||
});
|
||||
await expect(consolePanel).toContainText(
|
||||
"空气介质_1:物性计算模型不在支持的可选项中",
|
||||
);
|
||||
});
|
||||
@@ -25,13 +25,136 @@ export const componentCatalog = {
|
||||
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: 1,
|
||||
order: 4,
|
||||
category: {
|
||||
id: "generic",
|
||||
label: "通用组件",
|
||||
@@ -49,6 +172,7 @@ export const componentCatalog = {
|
||||
unit: "",
|
||||
default: 1,
|
||||
minimumExclusive: false,
|
||||
description: "传感器输入值的无量纲放大倍数。",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -158,8 +282,12 @@ export const resultSnapshot = {
|
||||
|
||||
export async function prepareApp(page: Page) {
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.clear();
|
||||
window.sessionStorage.clear();
|
||||
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) => {
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
import { expect, test, type Locator } from "@playwright/test";
|
||||
import { prepareApp } from "./fixtures";
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await prepareApp(page);
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.removeItem(
|
||||
"system-simulation-flow:palette-collapsed-sections",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
async function expectFixedIconCardSize(
|
||||
item: Locator,
|
||||
) {
|
||||
const itemBox = await item.boundingBox();
|
||||
const iconBox = await item.locator(".palette-icon").boundingBox();
|
||||
|
||||
expect(itemBox).not.toBeNull();
|
||||
expect(iconBox).not.toBeNull();
|
||||
expect(itemBox!.width).toBeGreaterThanOrEqual(75);
|
||||
expect(itemBox!.width).toBeLessThanOrEqual(77);
|
||||
expect(itemBox!.height).toBeGreaterThanOrEqual(81);
|
||||
expect(itemBox!.height).toBeLessThanOrEqual(83);
|
||||
expect(iconBox!.width).toBeGreaterThanOrEqual(47);
|
||||
expect(iconBox!.width).toBeLessThanOrEqual(49);
|
||||
expect(iconBox!.height).toBeGreaterThanOrEqual(47);
|
||||
expect(iconBox!.height).toBeLessThanOrEqual(49);
|
||||
}
|
||||
|
||||
test("组件库可切换详细与仅图标显示,并在刷新后保留选择", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const palette = page.locator("aside.palette");
|
||||
const detailedButton = page.getByRole("button", {
|
||||
name: "详细显示",
|
||||
exact: true,
|
||||
});
|
||||
const iconsButton = page.getByRole("button", {
|
||||
name: "仅图标显示",
|
||||
exact: true,
|
||||
});
|
||||
const airItem = palette.getByRole("button", {
|
||||
name: "空气介质定义",
|
||||
exact: true,
|
||||
});
|
||||
const heliumItem = palette.getByRole("button", {
|
||||
name: "氦气介质定义",
|
||||
exact: true,
|
||||
});
|
||||
|
||||
await expect(palette).toHaveAttribute("data-view-mode", "detailed");
|
||||
await expect(detailedButton).toHaveAttribute("aria-pressed", "true");
|
||||
await expect(iconsButton).toHaveAttribute("aria-pressed", "false");
|
||||
await expect(airItem.locator(".palette-item-copy")).toBeVisible();
|
||||
|
||||
const airSymbol = airItem.locator(
|
||||
'[data-component-symbol="amesim_ideal_air_medium"]',
|
||||
);
|
||||
const heliumSymbol = heliumItem.locator(
|
||||
'[data-component-symbol="amesim_helium_medium"]',
|
||||
);
|
||||
await expect(airSymbol.locator("svg")).toBeVisible();
|
||||
await expect(
|
||||
airSymbol.locator('[data-symbol-label="Air"]'),
|
||||
).toHaveText("Air");
|
||||
await expect(airSymbol.locator(".component-symbol-medium-svg path")).toHaveCount(0);
|
||||
await expect(heliumSymbol.locator("svg")).toBeVisible();
|
||||
await expect(
|
||||
heliumSymbol.locator('[data-symbol-label="He"]'),
|
||||
).toHaveText("He");
|
||||
await expect(heliumSymbol.locator(".component-symbol-medium-svg path")).toHaveCount(0);
|
||||
|
||||
const unknownSymbol = palette
|
||||
.getByRole("button", { name: "通用测试元件", exact: true })
|
||||
.locator('[data-component-symbol="generic"]');
|
||||
await expect(unknownSymbol.locator("svg")).toBeVisible();
|
||||
|
||||
await iconsButton.click();
|
||||
await expect(palette).toHaveAttribute("data-view-mode", "icons");
|
||||
await expect(detailedButton).toHaveAttribute("aria-pressed", "false");
|
||||
await expect(iconsButton).toHaveAttribute("aria-pressed", "true");
|
||||
await expect(airItem).toBeVisible();
|
||||
await expect(airItem.locator(".palette-item-name")).toBeVisible();
|
||||
await expect(airItem.locator(".palette-item-name")).toHaveText("空气介质定义");
|
||||
await expect(airItem.locator(".palette-item-meta")).toBeHidden();
|
||||
await expect(airItem).not.toHaveAttribute("title");
|
||||
|
||||
await page.reload();
|
||||
|
||||
await expect(palette).toHaveAttribute("data-view-mode", "icons");
|
||||
await expect(iconsButton).toHaveAttribute("aria-pressed", "true");
|
||||
await expect(airItem.locator(".palette-item-name")).toBeVisible();
|
||||
await expect(airItem.locator(".palette-item-meta")).toBeHidden();
|
||||
});
|
||||
|
||||
test("组件库和分类支持独立折叠并同步可访问状态", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const palette = page.locator("aside.palette");
|
||||
const libraryToggle = palette.getByRole("button", {
|
||||
name: "自动化测试组件库",
|
||||
exact: true,
|
||||
});
|
||||
const categoryToggle = palette.getByRole("button", {
|
||||
name: "介质物性",
|
||||
exact: true,
|
||||
});
|
||||
|
||||
await expect(libraryToggle).toHaveAttribute("aria-expanded", "true");
|
||||
const libraryContentId = await libraryToggle.getAttribute("aria-controls");
|
||||
expect(libraryContentId).toBeTruthy();
|
||||
await expect(page.locator(`[id="${libraryContentId}"]`)).toBeVisible();
|
||||
|
||||
await expect(categoryToggle).toHaveAttribute("aria-expanded", "true");
|
||||
const categoryItemsId = await categoryToggle.getAttribute("aria-controls");
|
||||
expect(categoryItemsId).toBeTruthy();
|
||||
await expect(page.locator(`[id="${categoryItemsId}"]`)).toBeVisible();
|
||||
await expect(
|
||||
palette.getByRole("button", { name: "空气介质定义", exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
await categoryToggle.click();
|
||||
await expect(categoryToggle).toHaveAttribute("aria-expanded", "false");
|
||||
await expect(page.locator(`[id="${categoryItemsId}"]`)).toHaveCount(0);
|
||||
await expect(
|
||||
palette.getByRole("button", { name: "空气介质定义", exact: true }),
|
||||
).toHaveCount(0);
|
||||
|
||||
await categoryToggle.click();
|
||||
await expect(categoryToggle).toHaveAttribute("aria-expanded", "true");
|
||||
await expect(page.locator(`[id="${categoryItemsId}"]`)).toBeVisible();
|
||||
|
||||
await libraryToggle.click();
|
||||
await expect(libraryToggle).toHaveAttribute("aria-expanded", "false");
|
||||
await expect(page.locator(`[id="${libraryContentId}"]`)).toHaveCount(0);
|
||||
await expect(
|
||||
palette.getByRole("button", { name: "介质物性", exact: true }),
|
||||
).toHaveCount(0);
|
||||
|
||||
await libraryToggle.click();
|
||||
await expect(libraryToggle).toHaveAttribute("aria-expanded", "true");
|
||||
await expect(page.locator(`[id="${libraryContentId}"]`)).toBeVisible();
|
||||
await expect(
|
||||
palette.getByRole("button", { name: "介质物性", exact: true }),
|
||||
).toHaveAttribute("aria-expanded", "true");
|
||||
});
|
||||
|
||||
test("仅图标模式名称截断且卡片尺寸不随侧栏和视口改变", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page
|
||||
.getByRole("button", { name: "仅图标显示", exact: true })
|
||||
.click();
|
||||
|
||||
const palette = page.locator("aside.palette");
|
||||
const longNameItem = palette.getByRole("button", {
|
||||
name: "AMESim 气动测试元件",
|
||||
exact: true,
|
||||
});
|
||||
const name = longNameItem.locator(".palette-item-name");
|
||||
const meta = longNameItem.locator(".palette-item-meta");
|
||||
|
||||
await expect(name).toBeVisible();
|
||||
await expect(name).toHaveText("AMESim 气动测试元件");
|
||||
await expect(meta).toBeHidden();
|
||||
await expect(longNameItem).not.toHaveAttribute("title");
|
||||
const truncation = await name.evaluate((element) => {
|
||||
const style = window.getComputedStyle(element);
|
||||
return {
|
||||
clipped: element.scrollWidth > element.clientWidth,
|
||||
overflow: style.overflow,
|
||||
textOverflow: style.textOverflow,
|
||||
whiteSpace: style.whiteSpace,
|
||||
};
|
||||
});
|
||||
expect(truncation).toEqual({
|
||||
clipped: true,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
});
|
||||
|
||||
await expectFixedIconCardSize(longNameItem);
|
||||
const initialPaletteBox = await palette.boundingBox();
|
||||
expect(initialPaletteBox).not.toBeNull();
|
||||
|
||||
const paletteSeparator = page.getByRole("separator", {
|
||||
name: "调整组件库宽度",
|
||||
});
|
||||
await paletteSeparator.focus();
|
||||
for (let index = 0; index < 5; index += 1) {
|
||||
await page.keyboard.press("ArrowRight");
|
||||
}
|
||||
await expect(paletteSeparator).toHaveAttribute("aria-valuenow", "330");
|
||||
const expandedPaletteBox = await palette.boundingBox();
|
||||
expect(expandedPaletteBox).not.toBeNull();
|
||||
expect(expandedPaletteBox!.width).toBeGreaterThan(initialPaletteBox!.width + 90);
|
||||
await expectFixedIconCardSize(longNameItem);
|
||||
|
||||
await page.setViewportSize({ width: 1000, height: 760 });
|
||||
await expect
|
||||
.poll(async () => Number(await paletteSeparator.getAttribute("aria-valuenow")))
|
||||
.toBeLessThan(330);
|
||||
await expectFixedIconCardSize(longNameItem);
|
||||
});
|
||||
|
||||
test("仅图标模式悬停后显示完整名称和图标预览并在移开后关闭", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await page
|
||||
.getByRole("button", { name: "仅图标显示", exact: true })
|
||||
.click();
|
||||
|
||||
const item = page.locator("aside.palette").getByRole("button", {
|
||||
name: "AMESim 气动测试元件",
|
||||
exact: true,
|
||||
});
|
||||
const preview = page.locator("body > .palette-icon-preview[role='tooltip']");
|
||||
|
||||
await item.hover();
|
||||
await page.waitForTimeout(600);
|
||||
await expect(preview).toBeVisible();
|
||||
await expect(preview.locator(".palette-icon-preview-label")).toHaveText(
|
||||
"AMESim 气动测试元件",
|
||||
);
|
||||
await expect(
|
||||
preview.locator(
|
||||
'.palette-icon-preview-symbol [data-component-symbol="generic"] svg',
|
||||
),
|
||||
).toBeVisible();
|
||||
await expect(item).toHaveAttribute("aria-describedby", "palette-icon-preview");
|
||||
|
||||
await page.locator(".palette-panel-title").hover();
|
||||
await expect(preview).toHaveCount(0);
|
||||
await expect(item).not.toHaveAttribute("aria-describedby", "palette-icon-preview");
|
||||
});
|
||||
|
||||
test("仅图标模式仍可拖入氦气介质并在画布显示专用图标", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
await page
|
||||
.getByRole("button", { name: "仅图标显示", exact: true })
|
||||
.click();
|
||||
|
||||
const palette = page.locator("aside.palette");
|
||||
const heliumItem = palette.getByRole("button", {
|
||||
name: "氦气介质定义",
|
||||
exact: true,
|
||||
});
|
||||
const canvas = page.locator(".flow-canvas .react-flow__pane");
|
||||
await heliumItem.dragTo(canvas, {
|
||||
targetPosition: { x: 420, y: 220 },
|
||||
});
|
||||
|
||||
const heliumNode = page.locator(
|
||||
'.flow-canvas .react-flow__node[data-id="amesim_helium_medium_1"]',
|
||||
);
|
||||
await expect(heliumNode).toBeVisible();
|
||||
await expect(
|
||||
heliumNode.locator(".sim-node.symbol-presentation-bare"),
|
||||
).toBeVisible();
|
||||
|
||||
const heliumCanvasSymbol = heliumNode.locator(
|
||||
'.node-symbol [data-component-symbol="amesim_helium_medium"]',
|
||||
);
|
||||
await expect(heliumCanvasSymbol).toHaveAttribute(
|
||||
"data-symbol-presentation",
|
||||
"bare",
|
||||
);
|
||||
await expect(heliumCanvasSymbol.locator("svg")).toBeVisible();
|
||||
await expect(
|
||||
heliumCanvasSymbol.locator('[data-symbol-label="He"]'),
|
||||
).toHaveText("He");
|
||||
});
|
||||
@@ -0,0 +1,203 @@
|
||||
import { expect, test, type Locator, type Page } from "@playwright/test";
|
||||
|
||||
import { prepareApp } from "./fixtures";
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await prepareApp(page);
|
||||
});
|
||||
|
||||
function parameterRow(table: Locator, name: string) {
|
||||
return table.getByRole("rowheader", { name, exact: true }).locator("..");
|
||||
}
|
||||
|
||||
async function addPneumaticTestComponent(page: Page) {
|
||||
const canvas = page.locator(".flow-canvas .react-flow__pane");
|
||||
await page
|
||||
.getByRole("button", { name: "AMESim 气动测试元件", exact: true })
|
||||
.dragTo(canvas, { targetPosition: { x: 620, y: 330 } });
|
||||
const node = page.locator(
|
||||
'.flow-canvas .react-flow__node[data-id="amesim_pneumatic_test_1"]',
|
||||
);
|
||||
await expect(node).toBeVisible();
|
||||
await node.click();
|
||||
}
|
||||
|
||||
test("组件参数和仿真设置使用共享的三列表格", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await addPneumaticTestComponent(page);
|
||||
|
||||
const componentTable = page.getByRole("table", { name: "组件参数" });
|
||||
const simulationTable = page.getByRole("table", { name: "仿真设置" });
|
||||
await expect(componentTable).toBeVisible();
|
||||
await expect(simulationTable).toBeVisible();
|
||||
|
||||
await expect(componentTable.getByRole("columnheader")).toHaveText([
|
||||
"参数",
|
||||
"值",
|
||||
"单位",
|
||||
]);
|
||||
await expect(componentTable.getByRole("rowheader")).toHaveText([
|
||||
"组件名称",
|
||||
"节点 ID",
|
||||
"介质物性索引",
|
||||
"参考压力",
|
||||
]);
|
||||
await expect(simulationTable.getByRole("rowheader")).toHaveText([
|
||||
"起始时间",
|
||||
"结束时间",
|
||||
"采样步长",
|
||||
"最大积分步长",
|
||||
"求解器",
|
||||
]);
|
||||
|
||||
await expect(parameterRow(simulationTable, "起始时间").locator("td").last()).toHaveText(
|
||||
"s",
|
||||
);
|
||||
await expect(parameterRow(simulationTable, "求解器").locator("td").last()).toHaveText(
|
||||
"—",
|
||||
);
|
||||
|
||||
const componentNameWidth = await componentTable
|
||||
.locator("thead .parameter-table-name-cell")
|
||||
.evaluate((element) => element.getBoundingClientRect().width);
|
||||
const simulationNameWidth = await simulationTable
|
||||
.locator("thead .parameter-table-name-cell")
|
||||
.evaluate((element) => element.getBoundingClientRect().width);
|
||||
expect(Math.abs(componentNameWidth - simulationNameWidth)).toBeLessThanOrEqual(1);
|
||||
});
|
||||
|
||||
test("参数说明延迟显示,单位切换不改变 XML 基础值", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await addPneumaticTestComponent(page);
|
||||
|
||||
const componentTable = page.getByRole("table", { name: "组件参数" });
|
||||
const pressureRow = parameterRow(componentTable, "参考压力");
|
||||
const preview = page.locator("body > #parameter-help-preview");
|
||||
|
||||
const pressureRowBox = await pressureRow.boundingBox();
|
||||
expect(pressureRowBox).not.toBeNull();
|
||||
const pointer = {
|
||||
x: pressureRowBox!.x + pressureRowBox!.width / 2,
|
||||
y: pressureRowBox!.y + pressureRowBox!.height / 2,
|
||||
};
|
||||
await page.mouse.move(pointer.x, pointer.y);
|
||||
await page.waitForTimeout(600);
|
||||
await expect(preview).toBeVisible();
|
||||
await expect(preview.locator(".parameter-help-preview-title")).toHaveText("参考压力");
|
||||
await expect(preview.locator(".parameter-help-preview-description")).toContainText(
|
||||
"单位换算和 XML 基础单位输出",
|
||||
);
|
||||
await expect(preview.locator(".parameter-help-preview-meta")).toContainText(
|
||||
"默认值:100000",
|
||||
);
|
||||
await expect(preview.locator(".parameter-help-preview-meta")).toContainText(
|
||||
"当前单位:Pa",
|
||||
);
|
||||
const previewBox = await preview.boundingBox();
|
||||
expect(previewBox).not.toBeNull();
|
||||
const horizontalGap =
|
||||
pointer.x < previewBox!.x
|
||||
? previewBox!.x - pointer.x
|
||||
: pointer.x > previewBox!.x + previewBox!.width
|
||||
? pointer.x - (previewBox!.x + previewBox!.width)
|
||||
: 0;
|
||||
const verticalGap =
|
||||
pointer.y < previewBox!.y
|
||||
? previewBox!.y - pointer.y
|
||||
: pointer.y > previewBox!.y + previewBox!.height
|
||||
? pointer.y - (previewBox!.y + previewBox!.height)
|
||||
: 0;
|
||||
expect(Math.hypot(horizontalGap, verticalGap)).toBeLessThanOrEqual(24);
|
||||
await page.mouse.move(pointer.x + 24, pointer.y);
|
||||
await page.waitForTimeout(100);
|
||||
const stationaryPreviewBox = await preview.boundingBox();
|
||||
expect(stationaryPreviewBox).not.toBeNull();
|
||||
expect(Math.abs(stationaryPreviewBox!.x - previewBox!.x)).toBeLessThanOrEqual(1);
|
||||
expect(Math.abs(stationaryPreviewBox!.y - previewBox!.y)).toBeLessThanOrEqual(1);
|
||||
|
||||
await page.locator(".panel-title").filter({ hasText: "组件参数" }).hover();
|
||||
await expect(preview).toHaveCount(0);
|
||||
|
||||
await page.getByLabel("参考压力单位").selectOption("kPa");
|
||||
await expect(page.getByLabel("参考压力", { exact: true })).toHaveValue("100");
|
||||
|
||||
await pressureRow.hover();
|
||||
await page.waitForTimeout(600);
|
||||
await expect(preview.locator(".parameter-help-preview-meta")).toContainText(
|
||||
"默认值:100",
|
||||
);
|
||||
await expect(preview.locator(".parameter-help-preview-meta")).toContainText(
|
||||
"当前单位:kPa",
|
||||
);
|
||||
|
||||
await page.getByRole("button", { name: "生成系统 XML", exact: true }).click();
|
||||
const consolePanel = page.getByRole("complementary", {
|
||||
name: "仿真控制台",
|
||||
exact: true,
|
||||
});
|
||||
await expect(consolePanel.getByLabel("生成的系统 XML")).toContainText(
|
||||
'<Parameter name="reference_pressure" value="100000"',
|
||||
);
|
||||
});
|
||||
|
||||
test("参数表列宽可调整并在刷新后保留", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
const simulationTable = page.getByRole("table", { name: "仿真设置" });
|
||||
const nameHeader = simulationTable.locator("thead .parameter-table-name-cell");
|
||||
const separator = simulationTable.getByRole("separator", {
|
||||
name: "调整参数名列宽",
|
||||
exact: true,
|
||||
});
|
||||
const initialWidth = await nameHeader.evaluate(
|
||||
(element) => element.getBoundingClientRect().width,
|
||||
);
|
||||
const initialValue = Number(await separator.getAttribute("aria-valuenow"));
|
||||
const separatorBox = await separator.boundingBox();
|
||||
expect(separatorBox).not.toBeNull();
|
||||
await page.mouse.move(
|
||||
separatorBox!.x + separatorBox!.width / 2,
|
||||
separatorBox!.y + separatorBox!.height / 2,
|
||||
);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(
|
||||
separatorBox!.x + separatorBox!.width / 2 + 16,
|
||||
separatorBox!.y + separatorBox!.height / 2,
|
||||
{ steps: 3 },
|
||||
);
|
||||
await page.mouse.up();
|
||||
|
||||
await expect(separator).toHaveAttribute(
|
||||
"aria-valuenow",
|
||||
String(initialValue + 16),
|
||||
);
|
||||
const resizedWidth = await nameHeader.evaluate(
|
||||
(element) => element.getBoundingClientRect().width,
|
||||
);
|
||||
expect(resizedWidth).toBeGreaterThan(initialWidth + 14);
|
||||
|
||||
await page.reload();
|
||||
const reloadedTable = page.getByRole("table", { name: "仿真设置" });
|
||||
await expect(
|
||||
reloadedTable.getByRole("separator", {
|
||||
name: "调整参数名列宽",
|
||||
exact: true,
|
||||
}),
|
||||
).toHaveAttribute("aria-valuenow", String(initialValue + 16));
|
||||
|
||||
const wrapOverflow = await reloadedTable.evaluate((table) => {
|
||||
const wrap = table.closest<HTMLElement>(".parameter-table-wrap");
|
||||
return wrap ? wrap.scrollWidth - wrap.clientWidth : Number.POSITIVE_INFINITY;
|
||||
});
|
||||
expect(wrapOverflow).toBeLessThanOrEqual(1);
|
||||
|
||||
const reloadedSeparator = reloadedTable.getByRole("separator", {
|
||||
name: "调整参数名列宽",
|
||||
exact: true,
|
||||
});
|
||||
await reloadedSeparator.dblclick();
|
||||
await expect(reloadedSeparator).toHaveAttribute("aria-valuenow", "112");
|
||||
await reloadedSeparator.focus();
|
||||
await page.keyboard.press("ArrowRight");
|
||||
await expect(reloadedSeparator).toHaveAttribute("aria-valuenow", "120");
|
||||
});
|
||||
Reference in new issue
Block a user