diff --git a/.github/workflows/test-ui.yaml b/.github/workflows/test-ui.yaml index d7e919695..a07ce0455 100644 --- a/.github/workflows/test-ui.yaml +++ b/.github/workflows/test-ui.yaml @@ -1,8 +1,10 @@ name: Tests CI on: - - push - - pull_request + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] jobs: test: @@ -19,9 +21,19 @@ jobs: with: repository: "huchenlei/ComfyUI_frontend" path: "ComfyUI_frontend" + ref: ${{ github.head_ref }} + - name: Get commit message + id: commit-message + run: echo "::set-output name=message::$(git log -1 --pretty=%B)" + working-directory: ComfyUI_frontend + - name: Skip CI + if: contains(steps.commit-message.outputs.message, '[skip ci]') + run: echo "Skipping CI as commit contains '[skip ci]'" + continue-on-error: true + working-directory: ComfyUI_frontend - uses: actions/setup-node@v3 with: - node-version: 18 + node-version: lts/* - uses: actions/setup-python@v4 with: python-version: '3.10' @@ -32,14 +44,49 @@ jobs: pip install -r requirements.txt pip install wait-for-it working-directory: ComfyUI + - name: Build & Install ComfyUI_frontend + run: | + npm ci + npm run build + rm -rf ../ComfyUI/web/* + mv dist/* ../ComfyUI/web/ + working-directory: ComfyUI_frontend - name: Start ComfyUI server run: | python main.py --cpu & + wait-for-it --service 127.0.0.1:8188 -t 600 working-directory: ComfyUI - name: Run UI tests run: | - wait-for-it --service 127.0.0.1:8188 -t 600 - npm ci npm run test:generate npm test -- --verbose working-directory: ComfyUI_frontend + - name: Install Playwright Browsers + run: npx playwright install --with-deps + working-directory: ComfyUI_frontend + - name: Run Playwright tests + id: playwright-tests + run: npx playwright test + continue-on-error: true + working-directory: ComfyUI_frontend + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: ComfyUI_frontend/playwright-report/ + retention-days: 30 + - name: Debugging info + run: | + echo "Branch: ${{ github.head_ref }}" + git status + working-directory: ComfyUI_frontend + - name: Commit updated expectations + if: steps.playwright-tests.outcome == 'failure' && contains(github.event.pull_request.labels.*.name, 'New Browser Test Expectations') + # Pushes back to the source branch of the PR + run: | + git config --global user.name 'github-actions' + git config --global user.email 'github-actions@github.com' + git add browser_tests + git commit -m "Update test expectations [skip ci]" + git push origin HEAD:${{ github.head_ref }} + working-directory: ComfyUI_frontend diff --git a/.gitignore b/.gitignore index 0516fe13b..a2625fc56 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,11 @@ dist-ssr # Ignore test data. tests-ui/data/* + +# Browser tests +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ + +.env \ No newline at end of file diff --git a/browser_tests/ComfyPage.ts b/browser_tests/ComfyPage.ts new file mode 100644 index 000000000..b14f298ca --- /dev/null +++ b/browser_tests/ComfyPage.ts @@ -0,0 +1,88 @@ +import type { Page, Locator } from '@playwright/test'; +import dotenv from "dotenv"; +dotenv.config(); + +interface Position { + x: number; + y: number; +} + +export class ComfyPage { + public readonly url: string; + // All canvas position operations are based on default view of canvas. + public readonly canvas: Locator; + public readonly widgetTextBox: Locator; + + // Buttons + public readonly resetViewButton: Locator; + + constructor( + public readonly page: Page, + ) { + this.url = process.env.PLAYWRIGHT_TEST_URL || 'http://localhost:8188'; + this.canvas = page.locator('#graph-canvas'); + this.widgetTextBox = page.getByPlaceholder('text').nth(1); + this.resetViewButton = page.getByRole('button', { name: 'Reset View' }); + } + + async goto() { + await this.page.goto(this.url); + } + + async nextFrame() { + await this.page.evaluate(() => { + return new Promise(requestAnimationFrame); + }); + } + + async resetView() { + await this.resetViewButton.click(); + await this.nextFrame(); + } + + async clickTextEncodeNode1() { + await this.canvas.click({ + position: { + x: 618, + y: 191 + } + }); + await this.nextFrame(); + } + + async clickTextEncodeNode2() { + await this.canvas.click({ + position: { + x: 622, + y: 400 + } + }); + await this.nextFrame(); + } + + async clickEmptySpace() { + await this.canvas.click({ + position: { + x: 35, + y: 31 + } + }); + await this.nextFrame(); + } + + async dragAndDrop(source: Position, target: Position) { + await this.page.mouse.move(source.x, source.y); + await this.page.mouse.down(); + await this.page.mouse.move(target.x, target.y); + await this.page.mouse.up(); + await this.nextFrame(); + } + + async dragNode2() { + await this.dragAndDrop( + { x: 622, y: 400 }, + { x: 622, y: 300 }, + ); + await this.nextFrame(); + } +} \ No newline at end of file diff --git a/browser_tests/interaction.spec.ts b/browser_tests/interaction.spec.ts new file mode 100644 index 000000000..f01b870de --- /dev/null +++ b/browser_tests/interaction.spec.ts @@ -0,0 +1,55 @@ +import { test as base, expect } from '@playwright/test'; +import { ComfyPage } from './ComfyPage'; + +const test = base.extend<{ comfyPage: ComfyPage }>({ + comfyPage: async ({ page }, use) => { + const comfyPage = new ComfyPage(page); + await comfyPage.goto(); + // Unify font for consistent screenshots. + await page.addStyleTag({ + url: "https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" + }); + await page.addStyleTag({ + url: "https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" + }); + await page.addStyleTag({ + content: ` + * { + font-family: 'Roboto Mono', 'Noto Color Emoji'; + }` + }); + + await page.waitForFunction(() => document.fonts.ready); + await page.waitForFunction(() => window['app'] != undefined); + await page.evaluate(() => { window['app']['canvas'].show_info = false; }); + await comfyPage.nextFrame(); + // Reset view to force re-rendering of canvas. So that info fields like fps + // become hidden. + await comfyPage.resetView(); + await use(comfyPage); + }, +}); + +test.describe('Node Interaction', () => { + test('Can enter prompt', async ({ comfyPage }) => { + const textBox = comfyPage.widgetTextBox; + await textBox.click(); + await textBox.fill('Hello World'); + await expect(textBox).toHaveValue('Hello World'); + await textBox.fill('Hello World 2'); + await expect(textBox).toHaveValue('Hello World 2'); + }); + + test('Can highlight selected', async ({ comfyPage }) => { + await expect(comfyPage.canvas).toHaveScreenshot('deselected-node.png'); + await comfyPage.clickTextEncodeNode1(); + await expect(comfyPage.canvas).toHaveScreenshot('selected-node1.png'); + await comfyPage.clickTextEncodeNode2(); + await expect(comfyPage.canvas).toHaveScreenshot('selected-node2.png'); + }); + + test('Can drag node', async ({ comfyPage }) => { + await comfyPage.dragNode2(); + await expect(comfyPage.canvas).toHaveScreenshot('dragged-node1.png'); + }); +}); diff --git a/browser_tests/interaction.spec.ts-snapshots/deselected-node-chromium-linux.png b/browser_tests/interaction.spec.ts-snapshots/deselected-node-chromium-linux.png new file mode 100644 index 000000000..1b8629a5e Binary files /dev/null and b/browser_tests/interaction.spec.ts-snapshots/deselected-node-chromium-linux.png differ diff --git a/browser_tests/interaction.spec.ts-snapshots/dragged-node1-chromium-linux.png b/browser_tests/interaction.spec.ts-snapshots/dragged-node1-chromium-linux.png new file mode 100644 index 000000000..84022e700 Binary files /dev/null and b/browser_tests/interaction.spec.ts-snapshots/dragged-node1-chromium-linux.png differ diff --git a/browser_tests/interaction.spec.ts-snapshots/selected-node1-chromium-linux.png b/browser_tests/interaction.spec.ts-snapshots/selected-node1-chromium-linux.png new file mode 100644 index 000000000..32de7e50b Binary files /dev/null and b/browser_tests/interaction.spec.ts-snapshots/selected-node1-chromium-linux.png differ diff --git a/browser_tests/interaction.spec.ts-snapshots/selected-node2-chromium-linux.png b/browser_tests/interaction.spec.ts-snapshots/selected-node2-chromium-linux.png new file mode 100644 index 000000000..0060e9330 Binary files /dev/null and b/browser_tests/interaction.spec.ts-snapshots/selected-node2-chromium-linux.png differ diff --git a/index.html b/index.html index 230ec00b4..3c4347576 100644 --- a/index.html +++ b/index.html @@ -4,6 +4,14 @@ ComfyUI + + diff --git a/jest.config.ts b/jest.config.ts index 30cbcf97f..760ade635 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,6 +1,7 @@ import type { JestConfigWithTsJest } from "ts-jest"; const jestConfig: JestConfigWithTsJest = { + testMatch: ["**/tests-ui/**/*.test.ts"], testEnvironment: "jsdom", transform: { '^.+\\.m?[tj]sx?$': ["ts-jest", { diff --git a/package-lock.json b/package-lock.json index 3d044b7ad..dce01c227 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,13 +8,16 @@ "name": "comfyui-frontend", "version": "1.0.0", "dependencies": { + "dotenv": "^16.4.5", "zod": "^3.23.8", "zod-validation-error": "^3.3.0" }, "devDependencies": { "@babel/core": "^7.24.7", "@babel/preset-env": "^7.22.20", + "@playwright/test": "^1.44.1", "@types/jest": "^29.5.12", + "@types/node": "^20.14.8", "babel-plugin-transform-import-meta": "^2.2.1", "babel-plugin-transform-rename-import": "^2.3.0", "identity-obj-proxy": "^3.0.0", @@ -2894,6 +2897,21 @@ "node": ">= 8" } }, + "node_modules/@playwright/test": { + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz", + "integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==", + "dev": true, + "dependencies": { + "playwright": "1.44.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.18.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", @@ -3261,9 +3279,9 @@ } }, "node_modules/@types/node": { - "version": "20.14.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz", - "integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==", + "version": "20.14.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.8.tgz", + "integrity": "sha512-DO+2/jZinXfROG7j7WKFn/3C6nFwxy2lLpgLjEXJz+0XKphZlTLJ14mo8Vfg8X5BWN6XjyESXq+LcYdT7tR3bA==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -4185,6 +4203,17 @@ "node": ">=12" } }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.803", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.803.tgz", @@ -7080,6 +7109,50 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz", + "integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==", + "dev": true, + "dependencies": { + "playwright-core": "1.44.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.44.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz", + "integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.4.38", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", diff --git a/package.json b/package.json index 622d3d4ee..3590830d5 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,15 @@ "typecheck": "tsc --noEmit", "test": "npm run build && jest", "test:generate": "npx tsx tests-ui/setup", + "test:browser": "npx playwright test", "preview": "vite preview" }, "devDependencies": { "@babel/core": "^7.24.7", "@babel/preset-env": "^7.22.20", + "@playwright/test": "^1.44.1", "@types/jest": "^29.5.12", + "@types/node": "^20.14.8", "babel-plugin-transform-import-meta": "^2.2.1", "babel-plugin-transform-rename-import": "^2.3.0", "identity-obj-proxy": "^3.0.0", @@ -28,6 +31,7 @@ "vite-plugin-static-copy": "^1.0.5" }, "dependencies": { + "dotenv": "^16.4.5", "zod": "^3.23.8", "zod-validation-error": "^3.3.0" } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 000000000..f7ccd98cb --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,78 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './browser_tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://127.0.0.1:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + // { + // name: 'firefox', + // use: { ...devices['Desktop Firefox'] }, + // }, + + // { + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, + // }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +});