Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/ci-tests-e2e-cloud.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: "CI: Tests E2E Cloud"
description: "Cloud E2E testing with Playwright against stagingcloud.comfy.org"

on:
workflow_dispatch:
push:
branches: [cloud/*, main]
pull_request:
branches: [main]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
playwright-tests-cloud:
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v5

- name: Setup frontend
uses: ./.github/actions/setup-frontend
with:
include_build_step: false # Cloud tests don't need build

- name: Setup Playwright
uses: ./.github/actions/setup-playwright

- name: Run Playwright cloud tests
id: playwright
env:
CLOUD_TEST_EMAIL: ${{ secrets.CLOUD_TEST_EMAIL }}
CLOUD_TEST_PASSWORD: ${{ secrets.CLOUD_TEST_PASSWORD }}
run: |
PLAYWRIGHT_JSON_OUTPUT_NAME=playwright-report/report.json \
pnpm exec playwright test --config=playwright.cloud.config.ts \
--reporter=list \
--reporter=html \
--reporter=json

- name: Upload Playwright report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-cloud
path: ./playwright-report/
retention-days: 30
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ coverage/
/playwright/.cache/
browser_tests/**/*-win32.png
browser_tests/local/
browser_tests/.auth/

.env

Expand Down
2 changes: 1 addition & 1 deletion browser_tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ When writing new tests, follow these patterns:

```typescript
// Import the test fixture
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'

test.describe('Feature Name', () => {
// Set up test environment if needed
Expand Down
44 changes: 44 additions & 0 deletions browser_tests/fixtures/CloudComfyPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { APIRequestContext, Page } from '@playwright/test'

import { ComfyPage } from './ComfyPage'
import type { FolderStructure } from './ComfyPage'

/**
* Cloud-specific implementation of ComfyPage.
* Uses Firebase auth persistence and cloud API for settings.
*/
export class CloudComfyPage extends ComfyPage {
constructor(
page: Page,
request: APIRequestContext,
parallelIndex: number = 0
) {
super(page, request, parallelIndex)
}

async setupUser(username: string): Promise<string | null> {
// No-op for cloud - user already authenticated via Firebase in globalSetup
// Firebase auth is persisted via storageState in the fixture
return null
}

async setupSettings(settings: Record<string, any>): Promise<void> {
// Cloud uses batch settings API (not devtools)
// Firebase auth token is automatically included from restored localStorage
const resp = await this.request.post(`${this.url}/api/settings`, {
data: settings
})

if (!resp.ok()) {
throw new Error(`Failed to setup cloud settings: ${await resp.text()}`)
}
}

async setupWorkflowsDirectory(structure: FolderStructure): Promise<void> {
// Cloud workflow API not yet implemented
// For initial smoke tests, we can skip this functionality
console.warn(
'setupWorkflowsDirectory: not yet implemented for cloud mode - skipping'
)
}
}
136 changes: 28 additions & 108 deletions browser_tests/fixtures/ComfyPage.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import type { APIRequestContext, Locator, Page } from '@playwright/test'
import { test as base, expect } from '@playwright/test'
import { expect } from '@playwright/test'
import dotenv from 'dotenv'
import * as fs from 'fs'

import type { LGraphNode } from '../../src/lib/litegraph/src/litegraph'
import type { NodeId } from '../../src/platform/workflow/validation/schemas/workflowSchema'
import type { KeyCombo } from '../../src/schemas/keyBindingSchema'
import type { useWorkspaceStore } from '../../src/stores/workspaceStore'
import { NodeBadgeMode } from '../../src/types/nodeSource'
import { ComfyActionbar } from '../helpers/actionbar'
import { ComfyTemplates } from '../helpers/templates'
import { ComfyMouse } from './ComfyMouse'
import { VueNodeHelpers } from './VueNodeHelpers'
import { ComfyNodeSearchBox } from './components/ComfyNodeSearchBox'
import { SettingDialog } from './components/SettingDialog'
Expand Down Expand Up @@ -94,7 +92,7 @@ class ComfyMenu {
}
}

type FolderStructure = {
export type FolderStructure = {
[key: string]: FolderStructure | string
}

Expand Down Expand Up @@ -122,7 +120,11 @@ class ConfirmDialog {
}
}

export class ComfyPage {
/**
* Abstract base class for ComfyUI page objects.
* Subclasses must implement backend-specific methods for different environments (localhost, cloud, etc.)
*/
export abstract class ComfyPage {
private _history: TaskHistory | null = null

public readonly url: string
Expand Down Expand Up @@ -156,12 +158,13 @@ export class ComfyPage {

/** Test user ID for the current context */
get id() {
return this.userIds[comfyPageFixture.info().parallelIndex]
return this.userIds[this.parallelIndex]
}

constructor(
public readonly page: Page,
public readonly request: APIRequestContext
public readonly request: APIRequestContext,
public readonly parallelIndex: number = 0
) {
this.url = process.env.PLAYWRIGHT_TEST_URL || 'http://localhost:8188'
this.canvas = page.locator('#graph-canvas')
Expand Down Expand Up @@ -215,65 +218,24 @@ export class ComfyPage {
})
}

async setupWorkflowsDirectory(structure: FolderStructure) {
const resp = await this.request.post(
`${this.url}/api/devtools/setup_folder_structure`,
{
data: {
tree_structure: this.convertLeafToContent(structure),
base_path: `user/${this.id}/workflows`
}
}
)

if (resp.status() !== 200) {
throw new Error(
`Failed to setup workflows directory: ${await resp.text()}`
)
}

await this.page.evaluate(async () => {
await window['app'].extensionManager.workflow.syncWorkflows()
})
}

async setupUser(username: string) {
const res = await this.request.get(`${this.url}/api/users`)
if (res.status() !== 200)
throw new Error(`Failed to retrieve users: ${await res.text()}`)

const apiRes = await res.json()
const user = Object.entries(apiRes?.users ?? {}).find(
([, name]) => name === username
)
const id = user?.[0]

return id ? id : await this.createUser(username)
}

async createUser(username: string) {
const resp = await this.request.post(`${this.url}/api/users`, {
data: { username }
})

if (resp.status() !== 200)
throw new Error(`Failed to create user: ${await resp.text()}`)

return await resp.json()
}
/**
* Setup workflows directory structure. Implementation varies by environment.
* @param structure - Folder structure to create
*/
abstract setupWorkflowsDirectory(structure: FolderStructure): Promise<void>

async setupSettings(settings: Record<string, any>) {
const resp = await this.request.post(
`${this.url}/api/devtools/set_settings`,
{
data: settings
}
)
/**
* Setup user for testing. Implementation varies by environment.
* @param username - Username to setup
* @returns User ID or null if not applicable
*/
abstract setupUser(username: string): Promise<string | null>

if (resp.status() !== 200) {
throw new Error(`Failed to setup settings: ${await resp.text()}`)
}
}
/**
* Setup settings for testing. Implementation varies by environment.
* @param settings - Settings object to apply
*/
abstract setupSettings(settings: Record<string, any>): Promise<void>

setupHistory(): TaskHistory {
this._history ??= new TaskHistory(this)
Expand Down Expand Up @@ -1628,50 +1590,8 @@ export class ComfyPage {
}
}

export const testComfySnapToGridGridSize = 50

export const comfyPageFixture = base.extend<{
comfyPage: ComfyPage
comfyMouse: ComfyMouse
}>({
comfyPage: async ({ page, request }, use, testInfo) => {
const comfyPage = new ComfyPage(page, request)

const { parallelIndex } = testInfo
const username = `playwright-test-${parallelIndex}`
const userId = await comfyPage.setupUser(username)
comfyPage.userIds[parallelIndex] = userId

try {
await comfyPage.setupSettings({
'Comfy.UseNewMenu': 'Top',
// Hide canvas menu/info/selection toolbox by default.
'Comfy.Graph.CanvasInfo': false,
'Comfy.Graph.CanvasMenu': false,
'Comfy.Canvas.SelectionToolbox': false,
// Hide all badges by default.
'Comfy.NodeBadge.NodeIdBadgeMode': NodeBadgeMode.None,
'Comfy.NodeBadge.NodeSourceBadgeMode': NodeBadgeMode.None,
// Disable tooltips by default to avoid flakiness.
'Comfy.EnableTooltips': false,
'Comfy.userId': userId,
// Set tutorial completed to true to avoid loading the tutorial workflow.
'Comfy.TutorialCompleted': true,
'Comfy.SnapToGrid.GridSize': testComfySnapToGridGridSize,
'Comfy.VueNodes.AutoScaleLayout': false
})
} catch (e) {
console.error(e)
}

await comfyPage.setup()
await use(comfyPage)
},
comfyMouse: async ({ comfyPage }, use) => {
const comfyMouse = new ComfyMouse(comfyPage)
await use(comfyMouse)
}
})
// Re-export shared constants and fixture
export { testComfySnapToGridGridSize } from './constants'

const makeMatcher = function <T>(
getValue: (node: NodeReference) => Promise<T> | T,
Expand Down
48 changes: 48 additions & 0 deletions browser_tests/fixtures/ComfyPageCloud.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { test as base } from '@playwright/test'

import { CloudComfyPage } from './CloudComfyPage'
import { ComfyMouse } from './ComfyMouse'
import type { ComfyPage } from './ComfyPage'

/**
* Cloud-specific fixture for ComfyPage.
* Uses Firebase auth persisted from globalSetupCloud.ts.
*/
export const comfyPageCloudFixture = base.extend<{
comfyPage: ComfyPage
comfyMouse: ComfyMouse
}>({
// Use the storageState saved by globalSetupCloud
storageState: 'browser_tests/.auth/cloudUser.json',

comfyPage: async ({ page, request }, use) => {
const comfyPage = new CloudComfyPage(page, request)

// Note: No setupUser needed - Firebase auth persisted via storageState
// Setup cloud-specific settings (optional - can customize per test)
try {
await comfyPage.setupSettings({
'Comfy.UseNewMenu': 'Top',
// Hide canvas menu/info/selection toolbox by default.
'Comfy.Graph.CanvasInfo': false,
'Comfy.Graph.CanvasMenu': false,
'Comfy.Canvas.SelectionToolbox': false,
// Disable tooltips by default to avoid flakiness.
'Comfy.EnableTooltips': false,
// Set tutorial completed to true to avoid loading the tutorial workflow.
'Comfy.TutorialCompleted': true
})
} catch (e) {
console.error('Failed to setup cloud settings:', e)
}

// Don't mock releases for cloud - cloud handles its own releases
await comfyPage.setup({ mockReleases: false })
await use(comfyPage)
},

comfyMouse: async ({ comfyPage }, use) => {
const comfyMouse = new ComfyMouse(comfyPage)
await use(comfyMouse)
}
})
Loading