Procházet zdrojové kódy

feat: enhance fixed-layout e2e & add e2e cache (#919)

* chore: add e2e cache

* feat: enhance e2e cases

* chore: cache add hash key
chenjiawei.inizio před 3 měsíci
rodič
revize
eba517510f

+ 9 - 0
.github/workflows/e2e.yml

@@ -23,6 +23,15 @@ jobs:
       - name: Rush build
         run: node common/scripts/install-run-rush.js build
 
+      # 缓存 Playwright 浏览器
+      - name: Cache Playwright browsers
+        uses: actions/cache@v3
+        with:
+          path: ~/.cache/ms-playwright
+          key: ${{ runner.os }}-playwright-${{ hashFiles('e2e/fixed-layout/package.json') }}
+          restore-keys: |
+            ${{ runner.os }}-playwright-
+
       - name: Install Playwright Browsers
         run: pushd e2e/fixed-layout && npx playwright install --with-deps --only-shell chromium && popd
 

+ 33 - 0
e2e/fixed-layout/README.md

@@ -0,0 +1,33 @@
+# FixedLayout E2E Testing Project
+
+> This project contains end-to-end (E2E) tests for demo-fixed-layout to ensure core workflows are stable and reliable.
+
+---
+
+## 📦 Project Structure
+
+e2e/
+├─ tests/           # Test cases
+│ ├─ layout.spec.js
+│ ├─ node.spec.js
+│ └─ ...
+├─ test-results/    # Store Test Results
+├─ utils/           # Some utils
+
+
+---
+
+## 🚀 How to Run
+
+```bash
+
+# Install dependencies
+rush update
+
+# Run all tests
+cd e2e/fixed-layout & npm run e2e:test
+
+# Update ScreenShots
+cd e2e/fixed-layout & npm run e2e:update-screenshot
+
+```

+ 1 - 0
e2e/fixed-layout/playwright.config.ts

@@ -12,6 +12,7 @@ export default defineConfig({
   use: {
     baseURL: 'http://localhost:3000',
     headless: true,
+    actionTimeout: 10 * 1000, // timeout for waitFor/click...
   },
   webServer: {
     command: 'rush dev:demo-fixed-layout',

+ 72 - 0
e2e/fixed-layout/tests/drag.spec.ts

@@ -0,0 +1,72 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { test, expect } from '@playwright/test';
+
+import { getOffsetByLocator, cssEscape } from '../utils';
+import PageModel from './models';
+
+const OFFSET = 10;
+
+test.describe('test drag', () => {
+  let editorPage: PageModel;
+
+  test.beforeEach(async ({ page }) => {
+    editorPage = new PageModel(page);
+    await page.goto('http://localhost:3000');
+    await page.waitForTimeout(1000);
+  });
+
+  test('drag node', async ({ page }) => {
+    // 获取 node
+    const DRAG_NODE_ID = 'agent_0';
+    const DRAG_TO_PORT_ID = 'switch_0';
+    const agentLocator = await page.locator(`#${cssEscape(`$slotIcon$${DRAG_NODE_ID}`)}`);
+
+    const fromOffset = await getOffsetByLocator(agentLocator);
+    const from = {
+      x: fromOffset.left + OFFSET,
+      y: fromOffset.top + OFFSET,
+    };
+
+    const toLocator = await page.locator(`[data-from="${DRAG_TO_PORT_ID}"]`);
+    const toOffset = await getOffsetByLocator(toLocator);
+
+    const to = {
+      x: toOffset.left,
+      y: toOffset.top,
+    };
+
+    await editorPage.drag(from, to);
+    await page.waitForTimeout(100);
+
+    // 通过 data-to 判断是否移动成功
+    const toLocator2 = await page.locator(`[data-from="${DRAG_TO_PORT_ID}"]`);
+    const attribute = await toLocator2?.getAttribute('data-to');
+    expect(attribute).toEqual(DRAG_NODE_ID);
+  });
+
+  test('drag branch', async ({ page }) => {
+    const START_ID = 'case_0';
+    const END_ID = 'case_default_1';
+    const branchLocator = page.locator(`#${cssEscape(`$blockOrderIcon$${START_ID}`)}`);
+    const fromOffset = await getOffsetByLocator(branchLocator);
+    const from = {
+      x: fromOffset.left + OFFSET,
+      y: fromOffset.top + OFFSET,
+    };
+    const toBranchLocator = await page.locator(`#${cssEscape(`$blockOrderIcon$${END_ID}`)}`);
+    const toOffset = await getOffsetByLocator(toBranchLocator);
+    const to = {
+      x: toOffset.left - OFFSET / 2,
+      y: toOffset.top + OFFSET,
+    };
+    await editorPage.drag(from, to);
+    await page.waitForTimeout(100);
+
+    const fromOffset2 = await getOffsetByLocator(branchLocator);
+    expect(fromOffset2.centerX).toBeGreaterThan(fromOffset.centerX);
+  });
+});

+ 39 - 0
e2e/fixed-layout/tests/drawer.spec.ts

@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { test, expect } from '@playwright/test';
+
+import PageModel from './models';
+
+test.describe('test llm drawer', () => {
+  let editorPage: PageModel;
+
+  test.beforeEach(async ({ page }) => {
+    editorPage = new PageModel(page);
+    await page.goto('http://localhost:3000');
+    await page.waitForTimeout(1000);
+  });
+
+  test('sync data', async ({ page }) => {
+    // 确保 llm drawer 更改表单数据,数据同步
+    const LLM_NODE_ID = 'llm_0';
+    const DRAWER_CLASSNAME = 'float-panel-wrap';
+
+    const TEST_FILL_VALUE = '123';
+
+    const llmLocator = await page.locator(`#${LLM_NODE_ID}`);
+
+    await llmLocator.click();
+
+    const drawerLocator = await page.locator(`.${DRAWER_CLASSNAME}`);
+    expect(drawerLocator).toBeVisible();
+
+    const input = await drawerLocator.locator('input').first();
+    await input.fill(TEST_FILL_VALUE);
+
+    const inputValue = await llmLocator.locator('input').first().inputValue();
+    expect(inputValue).toEqual(TEST_FILL_VALUE);
+  });
+});

+ 1 - 1
e2e/fixed-layout/tests/layout.spec.ts

@@ -3,7 +3,7 @@
  * SPDX-License-Identifier: MIT
  */
 
-import { test, expect } from '@playwright/test';
+import { test } from '@playwright/test';
 
 test('page render test', async ({ page }) => {
   await page.goto('http://localhost:3000');

+ 9 - 0
e2e/fixed-layout/tests/models/index.ts

@@ -5,6 +5,8 @@
 
 import type { Page } from '@playwright/test';
 
+import type { DragPosition } from '../typings/index';
+
 type InsertEdgeOptions = {
   from: string;
   to: string;
@@ -33,6 +35,13 @@ class FixedLayoutModel {
     return await this.page.locator('[data-node-id="$blockIcon$switch_0"]').count();
   }
 
+  public async drag(from: DragPosition, to: DragPosition) {
+    await this.page.mouse.move(from.x, from.y);
+    await this.page.mouse.down();
+    await this.page.mouse.move(to.x, to.y);
+    await this.page.mouse.up();
+  }
+
   public async insert(searchText: string, { from, to }: InsertEdgeOptions) {
     const preConditionNodes = await this.page.locator('.gedit-flow-activity-node');
     const preCount = await preConditionNodes.count();

+ 37 - 0
e2e/fixed-layout/tests/testrun.spec.ts

@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { expect, test } from '@playwright/test';
+
+import PageModel from './models';
+
+test.describe('test testrun', () => {
+  let editorPage: PageModel;
+
+  test.beforeEach(async ({ page }) => {
+    editorPage = new PageModel(page);
+    await page.goto('http://localhost:3000');
+    await page.waitForTimeout(1000);
+  });
+
+  test('trigger testrun', async ({ page }) => {
+    const runBtn = await page.getByText('Run');
+    await runBtn.click();
+
+    // 等待第一条 flowing line
+    const hasAnimation = await page.$eval('[data-line-id="start_0"]', (el) => {
+      const style = window.getComputedStyle(el);
+      return style.animationName !== 'none';
+    });
+
+    expect(hasAnimation).toBe(true);
+
+    await page.waitForFunction(() => {
+      const start_line = document.querySelector('[data-line-id="start_0"]');
+      const style = window.getComputedStyle(start_line!);
+      return style.animationName === 'none';
+    });
+  });
+});

+ 9 - 0
e2e/fixed-layout/tests/typings/drag.ts

@@ -0,0 +1,9 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+export interface DragPosition {
+  x: number;
+  y: number;
+}

+ 6 - 0
e2e/fixed-layout/tests/typings/index.ts

@@ -0,0 +1,6 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+export { DragPosition } from './drag';

+ 25 - 0
e2e/fixed-layout/tests/validate.spec.ts

@@ -0,0 +1,25 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { test, expect } from '@playwright/test';
+
+import PageModel from './models';
+
+test.describe('test validate', () => {
+  let editorPage: PageModel;
+
+  test.beforeEach(async ({ page }) => {
+    editorPage = new PageModel(page);
+    await page.goto('http://localhost:3000');
+  });
+
+  test('save', async ({ page }) => {
+    const saveBtn = await page.getByText('Save');
+    saveBtn.click();
+
+    const badge = page.locator('span.semi-badge-danger');
+    await expect(badge).toHaveText('2');
+  });
+});

+ 40 - 0
e2e/fixed-layout/tests/variable.spec.ts

@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import { expect, test } from '@playwright/test';
+
+import PageModel from './models';
+
+test.describe('test variable', () => {
+  let editorPage: PageModel;
+
+  test.beforeEach(async ({ page }) => {
+    editorPage = new PageModel(page);
+    await page.goto('http://localhost:3000');
+    await page.waitForTimeout(1000);
+  });
+
+  test('test variable type', async ({ page }) => {
+    const llmNode = page.locator('#llm_0');
+    const trigger = llmNode.locator('.semi-icon-setting').first();
+    await trigger.click();
+    const selectionBefore = llmNode.locator('.semi-tree-option-level-2');
+    await expect(selectionBefore).not.toBeVisible();
+
+    const semiTreeWrapper = llmNode.locator('.semi-tree-wrapper');
+
+    const dropdown = semiTreeWrapper.locator('.semi-tree-option-expand-icon').first();
+    await dropdown.click({
+      force: true,
+    });
+
+    const selection = llmNode.locator('.semi-tree-option-level-2');
+    await expect(selection).toBeVisible({
+      timeout: 10000,
+    });
+    const selectionCount = await selection.count();
+    expect(selectionCount).toEqual(1);
+  });
+});

+ 20 - 5
e2e/fixed-layout/tsconfig.json

@@ -12,11 +12,26 @@
     "noImplicitAny": true,
     "allowJs": true,
     "resolveJsonModule": true,
-    "types": ["node"],
-    "typeRoots": ["node_modules/@types"],
+    "types": [
+      "node"
+    ],
+    "typeRoots": [
+      "node_modules/@types"
+    ],
     "jsx": "react",
-    "lib": ["es6", "dom", "es2020", "es2019.Array"]
+    "lib": [
+      "es6",
+      "dom",
+      "es2020",
+      "es2019.Array"
+    ]
   },
-  "include": ["./tests", "playwright.config.ts"],
-  "exclude": ["node_modules"]
+  "include": [
+    "./tests",
+    "playwright.config.ts",
+    "./utils"
+  ],
+  "exclude": [
+    "node_modules"
+  ]
 }

+ 34 - 0
e2e/fixed-layout/utils/index.ts

@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
+ * SPDX-License-Identifier: MIT
+ */
+
+import type { Locator } from '@playwright/test';
+
+/**
+ * @param {import('@playwright/test').Locator} locator
+ */
+export async function getOffsetByLocator(locator: Locator) {
+  return locator.evaluate((el) => {
+    const rect = el.getBoundingClientRect();
+    const left = rect.left;
+    const top = rect.top;
+    const width = rect.width;
+    const height = rect.height;
+
+    return {
+      left,
+      top,
+      width,
+      height,
+      centerX: left + width / 2,
+      centerY: top + height / 2,
+      right: left + width,
+      bottom: top + height,
+    };
+  });
+}
+
+export function cssEscape(str: string) {
+  return str.replace(/([ !"#$%&'()*+,.\/:;<=>?@[\]^`{|}~])/g, '\\$1');
+}