diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
new file mode 100644
index 00000000..e43e314f
--- /dev/null
+++ b/.github/workflows/e2e.yml
@@ -0,0 +1,32 @@
+name: E2E Tests
+
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+ branches: [ "main" ]
+ merge_group:
+ branches: [ "main" ]
+
+jobs:
+ e2e:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - uses: actions/setup-node@v3
+ with:
+ node-version: 18
+
+ - name: Rush Install
+ run: node common/scripts/install-run-rush.js install
+
+ - name: Rush build
+ run: node common/scripts/install-run-rush.js build
+
+ - name: Install Playwright Browsers
+ run: npx playwright install --with-deps
+
+ - name: Run E2E tests
+ run: node common/scripts/install-run-rush.js e2e:test --verbose
diff --git a/.github/workflows/sync-screenshot.yml b/.github/workflows/sync-screenshot.yml
new file mode 100644
index 00000000..330f52bb
--- /dev/null
+++ b/.github/workflows/sync-screenshot.yml
@@ -0,0 +1,49 @@
+name: Sync Screenshot
+on:
+ workflow_dispatch
+
+concurrency:
+ group: "manual-sync-screenshot"
+ cancel-in-progress: false
+
+jobs:
+ e2e:
+ # can not update screenshot run on main.
+ if: github.ref_name != 'main'
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v3
+
+ - uses: actions/setup-node@v3
+ with:
+ node-version: 18
+
+ - name: Set Git user.name and user.email from trigger actor
+ run: |
+ git config --global user.name "${{ github.actor }}"
+ git config --global user.email "${{ github.actor }}@users.noreply.github.com"
+
+ - name: Rush Install
+ run: node common/scripts/install-run-rush.js install
+
+ - name: Rush build
+ run: node common/scripts/install-run-rush.js build
+
+ - name: Install Playwright Browsers
+ run: npx playwright install --with-deps
+
+ - name: Run E2E tests
+ run: node common/scripts/install-run-rush.js e2e:update-screenshot --verbose
+
+ - name: Commit and push changes
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ git add .
+ if git diff-index --quiet HEAD; then
+ echo "No changes to commit"
+ else
+ git commit -m "chore: sync screenshot"
+ git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }} HEAD:${{ github.ref_name }}
+ fi
diff --git a/.gitignore b/.gitignore
index 240f4c66..bc0211d3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
+# e2e results
+test-results/
+
# Logs
*.log
npm-debug.log*
diff --git a/apps/demo-free-layout/src/components/add-node/index.tsx b/apps/demo-free-layout/src/components/add-node/index.tsx
index 872c1a69..66bc3911 100644
--- a/apps/demo-free-layout/src/components/add-node/index.tsx
+++ b/apps/demo-free-layout/src/components/add-node/index.tsx
@@ -7,6 +7,7 @@ export const AddNode = (props: { disabled: boolean }) => {
const addNode = useAddNode();
return (
}
color="highlight"
style={{ backgroundColor: 'rgba(171,181,255,0.3)', borderRadius: '8px' }}
diff --git a/apps/demo-free-layout/src/components/node-panel/node-list.tsx b/apps/demo-free-layout/src/components/node-panel/node-list.tsx
index b06fde08..3d357f59 100644
--- a/apps/demo-free-layout/src/components/node-panel/node-list.tsx
+++ b/apps/demo-free-layout/src/components/node-panel/node-list.tsx
@@ -37,6 +37,7 @@ interface NodeProps {
function Node(props: NodeProps) {
return (
diff --git a/apps/docs/rspress.config.ts b/apps/docs/rspress.config.ts
index 65fd2fee..05c183be 100644
--- a/apps/docs/rspress.config.ts
+++ b/apps/docs/rspress.config.ts
@@ -8,6 +8,9 @@ export default defineConfig({
base: '/',
title: 'FlowGram.AI',
globalStyles: path.join(__dirname, './global.less'),
+ route: {
+ exclude: ['./global.d.ts'],
+ },
builderConfig: {
source: {
decorators: {
@@ -29,23 +32,6 @@ export default defineConfig({
},
],
},
- optimization: {
- splitChunks: {
- chunks: 'all', // 拆分所有模块,包括异步和同步
- minSize: 30 * 1024, // 30KB 以下不拆分
- maxSize: 500 * 1024, // 500KB 以上强制拆分
- minChunks: 1, // 最少被引用 1 次就可以拆分
- automaticNameDelimiter: '-',
- cacheGroups: {
- vendors: {
- test: /[\\/]node_modules[\\/]/,
- name: 'vendors',
- chunks: 'all',
- priority: -10, // 优先级
- },
- },
- },
- },
// 禁用 ES 模块输出(启用 CommonJS)
experiments: {
outputModule: false,
@@ -58,7 +44,7 @@ export default defineConfig({
},
},
},
- ssg: false,
+ ssg: true,
// locales 为一个对象数组
locales: [
{
diff --git a/common/config/rush/command-line.json b/common/config/rush/command-line.json
index 332a0c67..898293cb 100644
--- a/common/config/rush/command-line.json
+++ b/common/config/rush/command-line.json
@@ -267,6 +267,24 @@
"enableParallelism": true,
"safeForSimultaneousRushProcesses": true
},
+ {
+ "name": "e2e:test",
+ "commandKind": "bulk",
+ "summary": "⭐️️ Run e2e cases in packages",
+ "ignoreMissingScript": true,
+ "enableParallelism": false,
+ "allowWarningsInSuccessfulBuild": true,
+ "safeForSimultaneousRushProcesses": false
+ },
+ {
+ "name": "e2e:update-screenshot",
+ "commandKind": "bulk",
+ "summary": "⭐️️ Update screenshots of e2e cases",
+ "ignoreMissingScript": true,
+ "enableParallelism": false,
+ "allowWarningsInSuccessfulBuild": true,
+ "safeForSimultaneousRushProcesses": false
+ },
{
"name": "dev:demo-fixed-layout",
"commandKind": "global",
diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml
index f45bd513..10c28cd3 100644
--- a/common/config/rush/pnpm-lock.yaml
+++ b/common/config/rush/pnpm-lock.yaml
@@ -869,6 +869,32 @@ importers:
specifier: ^5.0.4
version: 5.0.4
+ ../../e2e/fixed-layout:
+ dependencies:
+ '@playwright/test':
+ specifier: ^1.52.0
+ version: 1.52.0
+ devDependencies:
+ '@flowgram.ai/eslint-config':
+ specifier: workspace:*
+ version: link:../../config/eslint-config
+ '@types/node':
+ specifier: ^18
+ version: 18.19.68
+
+ ../../e2e/free-layout:
+ dependencies:
+ '@playwright/test':
+ specifier: ^1.52.0
+ version: 1.52.0
+ devDependencies:
+ '@flowgram.ai/eslint-config':
+ specifier: workspace:*
+ version: link:../../config/eslint-config
+ '@types/node':
+ specifier: ^18
+ version: 18.19.68
+
../../packages/canvas-engine/core:
dependencies:
'@flowgram.ai/command':
@@ -6820,6 +6846,14 @@ packages:
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
dev: false
+ /@playwright/test@1.52.0:
+ resolution: {integrity: sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==}
+ engines: {node: '>=18'}
+ hasBin: true
+ dependencies:
+ playwright: 1.52.0
+ dev: false
+
/@react-hook/intersection-observer@3.1.2(react@18.3.1):
resolution: {integrity: sha512-mWU3BMkmmzyYMSuhO9wu3eJVP21N8TcgYm9bZnTrMwuM818bEk+0NRM3hP+c/TqA9Ln5C7qE53p1H0QMtzYdvQ==}
peerDependencies:
@@ -11220,6 +11254,14 @@ packages:
/fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
+ /fsevents@2.3.2:
+ resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
+ engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
+ os: [darwin]
+ requiresBuild: true
+ dev: false
+ optional: true
+
/fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -14299,6 +14341,22 @@ packages:
pathe: 1.1.2
dev: true
+ /playwright-core@1.52.0:
+ resolution: {integrity: sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==}
+ engines: {node: '>=18'}
+ hasBin: true
+ dev: false
+
+ /playwright@1.52.0:
+ resolution: {integrity: sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==}
+ engines: {node: '>=18'}
+ hasBin: true
+ dependencies:
+ playwright-core: 1.52.0
+ optionalDependencies:
+ fsevents: 2.3.2
+ dev: false
+
/possible-typed-array-names@1.0.0:
resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==}
engines: {node: '>= 0.4'}
diff --git a/cspell.json b/cspell.json
index f1f87814..0b6c7f27 100644
--- a/cspell.json
+++ b/cspell.json
@@ -16,7 +16,8 @@
"rspress",
"Sandpack",
"zoomin",
- "zoomout"
+ "zoomout",
+ "gedit"
],
"ignoreWords": [],
"import": []
diff --git a/e2e/fixed-layout/.eslintrc.js b/e2e/fixed-layout/.eslintrc.js
new file mode 100644
index 00000000..3aef16ba
--- /dev/null
+++ b/e2e/fixed-layout/.eslintrc.js
@@ -0,0 +1,6 @@
+const { defineConfig } = require('@flowgram.ai/eslint-config');
+
+module.exports = defineConfig({
+ preset: 'web',
+ packageRoot: __dirname,
+});
diff --git a/e2e/fixed-layout/package.json b/e2e/fixed-layout/package.json
new file mode 100644
index 00000000..54028ad0
--- /dev/null
+++ b/e2e/fixed-layout/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "@flowgram.ai/e2e-fixed-layout",
+ "version": "0.1.0",
+ "description": "",
+ "keywords": [],
+ "license": "MIT",
+ "scripts": {
+ "build": "exit",
+ "e2e:test": "npx playwright test",
+ "e2e:update-screenshot": "npx playwright test --update-snapshots"
+ },
+ "dependencies": {
+ "@playwright/test": "^1.52.0"
+ },
+ "devDependencies": {
+ "@flowgram.ai/eslint-config": "workspace:*",
+ "@types/node": "^18"
+ }
+}
diff --git a/e2e/fixed-layout/playwright.config.ts b/e2e/fixed-layout/playwright.config.ts
new file mode 100644
index 00000000..893b3274
--- /dev/null
+++ b/e2e/fixed-layout/playwright.config.ts
@@ -0,0 +1,17 @@
+import { defineConfig } from '@playwright/test';
+
+export default defineConfig({
+ testDir: './tests',
+ timeout: 30 * 1000,
+ retries: 1,
+ use: {
+ baseURL: 'http://localhost:3000',
+ headless: true,
+ },
+ webServer: {
+ command: 'rush dev:demo-fixed-layout',
+ port: 3000,
+ timeout: 120 * 1000,
+ reuseExistingServer: !process.env.GITHUB_ACTIONS,
+ },
+});
diff --git a/e2e/fixed-layout/tests/layout.spec.ts b/e2e/fixed-layout/tests/layout.spec.ts
new file mode 100644
index 00000000..051dc455
--- /dev/null
+++ b/e2e/fixed-layout/tests/layout.spec.ts
@@ -0,0 +1,6 @@
+import { test, expect } from '@playwright/test';
+
+test('page render test', async ({ page }) => {
+ await page.goto('http://localhost:3000');
+ await expect(page).toHaveScreenshot('homepage.png');
+});
diff --git a/e2e/fixed-layout/tests/layout.spec.ts-snapshots/homepage-linux.png b/e2e/fixed-layout/tests/layout.spec.ts-snapshots/homepage-linux.png
new file mode 100644
index 00000000..48179f81
Binary files /dev/null and b/e2e/fixed-layout/tests/layout.spec.ts-snapshots/homepage-linux.png differ
diff --git a/e2e/fixed-layout/tsconfig.json b/e2e/fixed-layout/tsconfig.json
new file mode 100644
index 00000000..6595dee3
--- /dev/null
+++ b/e2e/fixed-layout/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "compilerOptions": {
+ "experimentalDecorators": true,
+ "target": "es2020",
+ "module": "esnext",
+ "strictPropertyInitialization": false,
+ "strict": true,
+ "esModuleInterop": true,
+ "moduleResolution": "node",
+ "skipLibCheck": true,
+ "noUnusedLocals": true,
+ "noImplicitAny": true,
+ "allowJs": true,
+ "resolveJsonModule": true,
+ "types": ["node"],
+ "typeRoots": ["node_modules/@types"],
+ "jsx": "react",
+ "lib": ["es6", "dom", "es2020", "es2019.Array"]
+ },
+ "include": ["./tests", "playwright.config.ts"],
+ "exclude": ["node_modules"]
+}
diff --git a/e2e/free-layout/.eslintrc.js b/e2e/free-layout/.eslintrc.js
new file mode 100644
index 00000000..be186c32
--- /dev/null
+++ b/e2e/free-layout/.eslintrc.js
@@ -0,0 +1,15 @@
+const { defineConfig } = require('@flowgram.ai/eslint-config');
+
+module.exports = defineConfig({
+ preset: 'web',
+ packageRoot: __dirname,
+ rules: {
+ 'no-restricted-syntax': [
+ 'warn',
+ {
+ selector: "CallExpression[callee.property.name='waitForTimeout']",
+ message: 'Consider using waitForFunction instead of waitForTimeout.',
+ },
+ ],
+ },
+});
diff --git a/e2e/free-layout/package.json b/e2e/free-layout/package.json
new file mode 100644
index 00000000..88802d2c
--- /dev/null
+++ b/e2e/free-layout/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "@flowgram.ai/e2e-free-layout",
+ "version": "0.1.0",
+ "description": "",
+ "keywords": [],
+ "license": "MIT",
+ "scripts": {
+ "build": "exit",
+ "e2e:debug": "npx playwright test --debug",
+ "e2e:test": "npx playwright test",
+ "e2e:update-screenshot": "npx playwright test --update-snapshots"
+ },
+ "dependencies": {
+ "@playwright/test": "^1.52.0"
+ },
+ "devDependencies": {
+ "@flowgram.ai/eslint-config": "workspace:*",
+ "@types/node": "^18"
+ }
+}
diff --git a/e2e/free-layout/playwright.config.ts b/e2e/free-layout/playwright.config.ts
new file mode 100644
index 00000000..e4c037ec
--- /dev/null
+++ b/e2e/free-layout/playwright.config.ts
@@ -0,0 +1,17 @@
+import { defineConfig } from '@playwright/test';
+
+export default defineConfig({
+ testDir: './tests',
+ timeout: 30 * 1000,
+ retries: 1,
+ use: {
+ baseURL: 'http://localhost:3000',
+ headless: true,
+ },
+ webServer: {
+ command: 'rush dev:demo-free-layout',
+ port: 3000,
+ timeout: 120 * 1000,
+ reuseExistingServer: !process.env.GITHUB_ACTIONS,
+ },
+});
diff --git a/e2e/free-layout/tests/layout.spec.ts b/e2e/free-layout/tests/layout.spec.ts
new file mode 100644
index 00000000..84e2dc58
--- /dev/null
+++ b/e2e/free-layout/tests/layout.spec.ts
@@ -0,0 +1,8 @@
+import { test, expect } from '@playwright/test';
+
+test.describe('page render screen shot', () => {
+ test('screenshot', async ({ page }) => {
+ await page.goto('http://localhost:3000');
+ await expect(page).toHaveScreenshot('homepage.png');
+ });
+});
diff --git a/e2e/free-layout/tests/layout.spec.ts-snapshots/homepage-linux.png b/e2e/free-layout/tests/layout.spec.ts-snapshots/homepage-linux.png
new file mode 100644
index 00000000..2e21ea5f
Binary files /dev/null and b/e2e/free-layout/tests/layout.spec.ts-snapshots/homepage-linux.png differ
diff --git a/e2e/free-layout/tests/models/index.ts b/e2e/free-layout/tests/models/index.ts
new file mode 100644
index 00000000..c8f76e84
--- /dev/null
+++ b/e2e/free-layout/tests/models/index.ts
@@ -0,0 +1,36 @@
+import type { Page } from '@playwright/test';
+
+class FreeLayoutModel {
+ public readonly page: Page;
+
+ constructor(page: Page) {
+ this.page = page;
+ }
+
+ // 获取节点数量
+ async getNodeCount() {
+ return await this.page.evaluate(
+ () => document.querySelectorAll('[data-testid="sdk.workflow.canvas.node"]').length
+ );
+ }
+
+ async addConditionNode() {
+ const preConditionNodes = await this.page.locator('.gedit-flow-activity-node');
+ const preCount = await preConditionNodes.count();
+ const button = this.page.locator('[data-testid="demo.free-layout.add-node"]');
+ // open add node panel
+ await button.click();
+ await this.page.waitForSelector('[data-testid="demo-free-node-list-condition"]');
+ // add condition
+ const conditionItem = this.page.locator('[data-testid="demo-free-node-list-condition"]');
+ await conditionItem.click();
+ // determine whether the node was successfully added
+ await this.page.waitForFunction(
+ (expectedCount) =>
+ document.querySelectorAll('.gedit-flow-activity-node').length === expectedCount,
+ preCount + 1
+ );
+ }
+}
+
+export default FreeLayoutModel;
diff --git a/e2e/free-layout/tests/node.spec.ts b/e2e/free-layout/tests/node.spec.ts
new file mode 100644
index 00000000..0ae44d9b
--- /dev/null
+++ b/e2e/free-layout/tests/node.spec.ts
@@ -0,0 +1,23 @@
+import { test, expect } from '@playwright/test';
+
+import PageModel from './models';
+
+test.describe('node operations', () => {
+ let editorPage: PageModel;
+
+ test.beforeEach(async ({ page }) => {
+ editorPage = new PageModel(page);
+ await page.goto('http://localhost:3000');
+ });
+
+ test('node preview', async () => {
+ const defaultNodeCount = await editorPage.getNodeCount();
+ expect(defaultNodeCount).toEqual(10);
+ });
+
+ test('add node', async () => {
+ await editorPage.addConditionNode();
+ const defaultNodeCount = await editorPage.getNodeCount();
+ expect(defaultNodeCount).toEqual(11);
+ });
+});
diff --git a/e2e/free-layout/tsconfig.json b/e2e/free-layout/tsconfig.json
new file mode 100644
index 00000000..6595dee3
--- /dev/null
+++ b/e2e/free-layout/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "compilerOptions": {
+ "experimentalDecorators": true,
+ "target": "es2020",
+ "module": "esnext",
+ "strictPropertyInitialization": false,
+ "strict": true,
+ "esModuleInterop": true,
+ "moduleResolution": "node",
+ "skipLibCheck": true,
+ "noUnusedLocals": true,
+ "noImplicitAny": true,
+ "allowJs": true,
+ "resolveJsonModule": true,
+ "types": ["node"],
+ "typeRoots": ["node_modules/@types"],
+ "jsx": "react",
+ "lib": ["es6", "dom", "es2020", "es2019.Array"]
+ },
+ "include": ["./tests", "playwright.config.ts"],
+ "exclude": ["node_modules"]
+}
diff --git a/rush.json b/rush.json
index fe83f6e1..d55c3431 100644
--- a/rush.json
+++ b/rush.json
@@ -428,6 +428,16 @@
// "tags": [ "frontend-team" ]
// },
//
+ {
+ "packageName": "@flowgram.ai/e2e-fixed-layout",
+ "projectFolder": "e2e/fixed-layout",
+ "tags": ["e2e"]
+ },
+ {
+ "packageName": "@flowgram.ai/e2e-free-layout",
+ "projectFolder": "e2e/free-layout",
+ "tags": ["e2e"]
+ },
// eslint 通用配置
{
"packageName": "@flowgram.ai/eslint-config",