fix: resolve merge conflicts with main

Amp-Thread-ID: https://ampcode.com/threads/T-019c253f-1719-76a5-83de-624054bb486c
This commit is contained in:
bymyself
2026-02-03 12:55:43 -08:00
179 changed files with 5162 additions and 3137 deletions

16
.gitattributes vendored
View File

@@ -1,17 +1,5 @@
# Default
* text=auto
# Force TS to LF to make the unixy scripts not break on Windows
*.cjs text eol=lf
*.js text eol=lf
*.json text eol=lf
*.mjs text eol=lf
*.mts text eol=lf
*.snap text eol=lf
*.ts text eol=lf
*.vue text eol=lf
*.yaml text eol=lf
*.yml text eol=lf
# Force all text files to use LF line endings
* text=auto eol=lf
# Generated files
packages/registry-types/src/comfyRegistryTypes.ts linguist-generated=true

View File

@@ -37,7 +37,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 60
container:
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.10
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.12
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
@@ -85,7 +85,7 @@ jobs:
needs: setup
runs-on: ubuntu-latest
container:
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.10
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.12
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -77,7 +77,7 @@ jobs:
needs: setup
runs-on: ubuntu-latest
container:
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.10
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.12
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -110,6 +110,17 @@
"rules": {
"no-console": "allow"
}
},
{
"files": ["browser_tests/**/*.ts"],
"rules": {
"typescript/no-explicit-any": "error",
"no-async-promise-executor": "error",
"no-control-regex": "error",
"no-useless-rename": "error",
"no-unused-private-class-members": "error",
"unicorn/no-empty-file": "error"
}
}
]
}

View File

@@ -44,7 +44,7 @@ The project uses **Nx** for build orchestration and task management
- `pnpm build`: Type-check then production build to `dist/`
- `pnpm preview`: Preview the production build locally
- `pnpm test:unit`: Run Vitest unit tests
- `pnpm test:browser`: Run Playwright E2E tests (`browser_tests/`)
- `pnpm test:browser:local`: Run Playwright E2E tests (`browser_tests/`)
- `pnpm lint` / `pnpm lint:fix`: Lint (ESLint)
- `pnpm format` / `pnpm format:check`: oxfmt
- `pnpm typecheck`: Vue TSC type checking

View File

@@ -6,3 +6,9 @@ See `@docs/guidance/playwright.md` for Playwright best practices (auto-loaded fo
- `assets/` - Test data (JSON workflows, fixtures)
- Tests use premade JSON workflows to load desired graph state
## After Making Changes
- Run `pnpm typecheck:browser` after modifying TypeScript files in this directory
- Run `pnpm exec eslint browser_tests/path/to/file.ts` to lint specific files
- Run `pnpm exec oxlint browser_tests/path/to/file.ts` to check with oxlint

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@
*/
import type { Locator, Page } from '@playwright/test'
import { TestIds } from './selectors'
import { VueNodeFixture } from './utils/vueNodeFixtures'
export class VueNodeHelpers {
@@ -148,9 +149,9 @@ export class VueNodeHelpers {
* Get a specific widget by node title and widget name
*/
getWidgetByName(nodeTitle: string, widgetName: string): Locator {
return this.getNodeByTitle(nodeTitle).locator(
`_vue=[widget.name="${widgetName}"]`
)
return this.getNodeByTitle(nodeTitle).getByLabel(widgetName, {
exact: true
})
}
/**
@@ -159,8 +160,8 @@ export class VueNodeHelpers {
getInputNumberControls(widget: Locator) {
return {
input: widget.locator('input'),
decrementButton: widget.getByTestId('decrement'),
incrementButton: widget.getByTestId('increment')
decrementButton: widget.getByTestId(TestIds.widgets.decrement),
incrementButton: widget.getByTestId(TestIds.widgets.increment)
}
}
@@ -170,7 +171,7 @@ export class VueNodeHelpers {
*/
async enterSubgraph(nodeId?: string): Promise<void> {
const locator = nodeId ? this.getNodeLocator(nodeId) : this.page
const editButton = locator.getByTestId('subgraph-enter-button')
const editButton = locator.getByTestId(TestIds.widgets.subgraphEnterButton)
await editButton.click()
}
}

View File

@@ -0,0 +1,31 @@
import type { Locator, Page } from '@playwright/test'
export class BaseDialog {
readonly root: Locator
readonly closeButton: Locator
constructor(
public readonly page: Page,
testId?: string
) {
this.root = testId ? page.getByTestId(testId) : page.locator('.p-dialog')
this.closeButton = this.root.getByRole('button', { name: 'Close' })
}
async isVisible(): Promise<boolean> {
return this.root.isVisible()
}
async waitForVisible(): Promise<void> {
await this.root.waitFor({ state: 'visible' })
}
async waitForHidden(): Promise<void> {
await this.root.waitFor({ state: 'hidden' })
}
async close(): Promise<void> {
await this.closeButton.click({ force: true })
await this.waitForHidden()
}
}

View File

@@ -0,0 +1,35 @@
import type { Locator, Page } from '@playwright/test'
class ShortcutsTab {
readonly essentialsTab: Locator
readonly viewControlsTab: Locator
readonly manageButton: Locator
readonly keyBadges: Locator
readonly subcategoryTitles: Locator
constructor(readonly page: Page) {
this.essentialsTab = page.getByRole('tab', { name: /Essential/i })
this.viewControlsTab = page.getByRole('tab', { name: /View Controls/i })
this.manageButton = page.getByRole('button', { name: /Manage Shortcuts/i })
this.keyBadges = page.locator('.key-badge')
this.subcategoryTitles = page.locator('.subcategory-title')
}
}
export class BottomPanel {
readonly root: Locator
readonly keyboardShortcutsButton: Locator
readonly toggleButton: Locator
readonly shortcuts: ShortcutsTab
constructor(readonly page: Page) {
this.root = page.locator('.bottom-panel')
this.keyboardShortcutsButton = page.getByRole('button', {
name: /Keyboard Shortcuts/i
})
this.toggleButton = page.getByRole('button', {
name: /Toggle Bottom Panel/i
})
this.shortcuts = new ShortcutsTab(page)
}
}

View File

@@ -30,7 +30,7 @@ export class ComfyNodeSearchFilterSelectionPanel {
async addFilter(filterValue: string, filterType: string) {
await this.selectFilterType(filterType)
await this.selectFilterValue(filterValue)
await this.page.locator('button:has-text("Add")').click()
await this.page.getByRole('button', { name: 'Add', exact: true }).click()
}
}

View File

@@ -0,0 +1,54 @@
import type { Locator, Page } from '@playwright/test'
export class ContextMenu {
constructor(public readonly page: Page) {}
get primeVueMenu() {
return this.page.locator('.p-contextmenu, .p-menu')
}
get litegraphMenu() {
return this.page.locator('.litemenu')
}
get menuItems() {
return this.page.locator('.p-menuitem, .litemenu-entry')
}
async clickMenuItem(name: string): Promise<void> {
await this.page.getByRole('menuitem', { name }).click()
}
async clickLitegraphMenuItem(name: string): Promise<void> {
await this.page.locator(`.litemenu-entry:has-text("${name}")`).click()
}
async isVisible(): Promise<boolean> {
const primeVueVisible = await this.primeVueMenu
.isVisible()
.catch(() => false)
const litegraphVisible = await this.litegraphMenu
.isVisible()
.catch(() => false)
return primeVueVisible || litegraphVisible
}
async waitForHidden(): Promise<void> {
const waitIfExists = async (locator: Locator, menuName: string) => {
const count = await locator.count()
if (count > 0) {
await locator.waitFor({ state: 'hidden' }).catch((error: Error) => {
console.warn(
`[waitForHidden] ${menuName} waitFor failed:`,
error.message
)
})
}
}
await Promise.all([
waitIfExists(this.primeVueMenu, 'primeVueMenu'),
waitIfExists(this.litegraphMenu, 'litegraphMenu')
])
}
}

View File

@@ -1,20 +1,20 @@
import type { Page } from '@playwright/test'
import type { ComfyPage } from '../ComfyPage'
import { TestIds } from '../selectors'
import { BaseDialog } from './BaseDialog'
export class SettingDialog {
export class SettingDialog extends BaseDialog {
constructor(
public readonly page: Page,
page: Page,
public readonly comfyPage: ComfyPage
) {}
get root() {
return this.page.locator('div.settings-container')
) {
super(page, TestIds.dialogs.settings)
}
async open() {
await this.comfyPage.executeCommand('Comfy.ShowSettingsDialog')
await this.page.waitForSelector('div.settings-container')
await this.comfyPage.command.executeCommand('Comfy.ShowSettingsDialog')
await this.waitForVisible()
}
/**
@@ -41,8 +41,9 @@ export class SettingDialog {
}
async goToAboutPanel() {
const aboutButton = this.page.locator('li[aria-label="About"]')
await aboutButton.click()
await this.page.waitForSelector('div.about-container')
await this.page.getByTestId(TestIds.dialogs.settingsTabAbout).click()
await this.page
.getByTestId(TestIds.dialogs.about)
.waitFor({ state: 'visible' })
}
}

View File

@@ -1,5 +1,8 @@
import type { Locator, Page } from '@playwright/test'
import type { WorkspaceStore } from '../../types/globals'
import { TestIds } from '../selectors'
class SidebarTab {
constructor(
public readonly page: Page,
@@ -31,16 +34,16 @@ class SidebarTab {
}
export class NodeLibrarySidebarTab extends SidebarTab {
constructor(public readonly page: Page) {
constructor(public override readonly page: Page) {
super(page, 'node-library')
}
get nodeLibrarySearchBoxInput() {
return this.page.locator('.node-lib-search-box input[type="text"]')
return this.page.getByPlaceholder('Search Nodes...')
}
get nodeLibraryTree() {
return this.page.locator('.node-lib-tree-explorer')
return this.page.getByTestId(TestIds.sidebar.nodeLibrary)
}
get nodePreview() {
@@ -55,12 +58,12 @@ export class NodeLibrarySidebarTab extends SidebarTab {
return this.tabContainer.locator('.new-folder-button')
}
async open() {
override async open() {
await super.open()
await this.nodeLibraryTree.waitFor({ state: 'visible' })
}
async close() {
override async close() {
if (!this.tabButton.isVisible()) {
return
}
@@ -69,30 +72,40 @@ export class NodeLibrarySidebarTab extends SidebarTab {
await this.nodeLibraryTree.waitFor({ state: 'hidden' })
}
folderSelector(folderName: string) {
return `.p-tree-node-content:has(> .tree-explorer-node-label:has(.tree-folder .node-label:has-text("${folderName}")))`
}
getFolder(folderName: string) {
return this.page.locator(this.folderSelector(folderName))
}
nodeSelector(nodeName: string) {
return `.p-tree-node-content:has(> .tree-explorer-node-label:has(.tree-leaf .node-label:has-text("${nodeName}")))`
return this.page.locator(
`[data-testid="node-tree-folder"][data-folder-name="${folderName}"]`
)
}
getNode(nodeName: string) {
return this.page.locator(this.nodeSelector(nodeName))
return this.page.locator(
`[data-testid="node-tree-leaf"][data-node-name="${nodeName}"]`
)
}
nodeSelector(nodeName: string): string {
return `[data-testid="node-tree-leaf"][data-node-name="${nodeName}"]`
}
folderSelector(folderName: string): string {
return `[data-testid="node-tree-folder"][data-folder-name="${folderName}"]`
}
getNodeInFolder(nodeName: string, folderName: string) {
return this.getFolder(folderName)
.locator('xpath=ancestor::li')
.locator(`[data-testid="node-tree-leaf"][data-node-name="${nodeName}"]`)
}
}
export class WorkflowsSidebarTab extends SidebarTab {
constructor(public readonly page: Page) {
constructor(public override readonly page: Page) {
super(page, 'workflows')
}
get root() {
return this.page.locator('.workflows-sidebar-tab')
return this.page.getByTestId(TestIds.sidebar.workflows)
}
async getOpenedWorkflowNames() {
@@ -140,7 +153,9 @@ export class WorkflowsSidebarTab extends SidebarTab {
// Wait for workflow service to finish renaming
await this.page.waitForFunction(
() => !window['app']?.extensionManager?.workflow?.isBusy,
() =>
!(window.app?.extensionManager as WorkspaceStore | undefined)?.workflow
?.isBusy,
undefined,
{ timeout: 3000 }
)

View File

@@ -1,5 +1,6 @@
import type { Locator, Page } from '@playwright/test'
import { expect } from '@playwright/test'
import type { WorkspaceStore } from '../../types/globals'
export class Topbar {
private readonly menuLocator: Locator
@@ -57,7 +58,7 @@ export class Topbar {
async closeWorkflowTab(tabName: string) {
const tab = this.getWorkflowTab(tabName)
await tab.locator('.close-button').click({ force: true })
await tab.getByRole('button', { name: 'Close' }).click({ force: true })
}
getSaveDialog(): Locator {
@@ -86,7 +87,7 @@ export class Topbar {
// Wait for workflow service to finish saving
await this.page.waitForFunction(
() => !window['app'].extensionManager.workflow.isBusy,
() => !(window.app!.extensionManager as WorkspaceStore).workflow.isBusy,
undefined,
{ timeout: 3000 }
)
@@ -122,7 +123,7 @@ export class Topbar {
*/
async closeTopbarMenu() {
await this.page.locator('body').click({ position: { x: 300, y: 10 } })
await expect(this.menuLocator).not.toBeVisible()
await this.menuLocator.waitFor({ state: 'hidden' })
}
/**

View File

@@ -0,0 +1,51 @@
import type { Position } from './types'
/**
* Hardcoded positions for the default graph loaded in tests.
* These coordinates are specific to the default workflow viewport.
*/
export const DefaultGraphPositions = {
// Node click positions
textEncodeNode1: { x: 618, y: 191 },
textEncodeNode2: { x: 622, y: 400 },
textEncodeNodeToggler: { x: 430, y: 171 },
emptySpaceClick: { x: 35, y: 31 },
// Slot positions
clipTextEncodeNode1InputSlot: { x: 427, y: 198 },
clipTextEncodeNode2InputSlot: { x: 422, y: 402 },
clipTextEncodeNode2InputLinkPath: { x: 395, y: 422 },
loadCheckpointNodeClipOutputSlot: { x: 332, y: 509 },
emptySpace: { x: 427, y: 98 },
// Widget positions
emptyLatentWidgetClick: { x: 724, y: 645 },
// Node positions and sizes for resize operations
ksampler: {
pos: { x: 863, y: 156 },
size: { width: 315, height: 292 }
},
loadCheckpoint: {
pos: { x: 26, y: 444 },
size: { width: 315, height: 127 }
},
emptyLatent: {
pos: { x: 473, y: 579 },
size: { width: 315, height: 136 }
}
} as const satisfies {
textEncodeNode1: Position
textEncodeNode2: Position
textEncodeNodeToggler: Position
emptySpaceClick: Position
clipTextEncodeNode1InputSlot: Position
clipTextEncodeNode2InputSlot: Position
clipTextEncodeNode2InputLinkPath: Position
loadCheckpointNodeClipOutputSlot: Position
emptySpace: Position
emptyLatentWidgetClick: Position
ksampler: { pos: Position; size: { width: number; height: number } }
loadCheckpoint: { pos: Position; size: { width: number; height: number } }
emptyLatent: { pos: Position; size: { width: number; height: number } }
}

View File

@@ -0,0 +1,9 @@
export interface Position {
x: number
y: number
}
export interface Size {
width: number
height: number
}

View File

@@ -0,0 +1,184 @@
import type { Locator, Page } from '@playwright/test'
import { DefaultGraphPositions } from '../constants/defaultGraphPositions'
import type { Position } from '../types'
export class CanvasHelper {
constructor(
private page: Page,
private canvas: Locator,
private resetViewButton: Locator
) {}
private async nextFrame(): Promise<void> {
await this.page.evaluate(() => {
return new Promise<number>(requestAnimationFrame)
})
}
async resetView(): Promise<void> {
if (await this.resetViewButton.isVisible()) {
await this.resetViewButton.click()
}
await this.page.mouse.move(10, 10)
await this.nextFrame()
}
async zoom(deltaY: number, steps: number = 1): Promise<void> {
await this.page.mouse.move(10, 10)
for (let i = 0; i < steps; i++) {
await this.page.mouse.wheel(0, deltaY)
}
await this.nextFrame()
}
async pan(offset: Position, safeSpot?: Position): Promise<void> {
safeSpot = safeSpot || { x: 10, y: 10 }
await this.page.mouse.move(safeSpot.x, safeSpot.y)
await this.page.mouse.down()
await this.page.mouse.move(offset.x + safeSpot.x, offset.y + safeSpot.y)
await this.page.mouse.up()
await this.nextFrame()
}
async panWithTouch(offset: Position, safeSpot?: Position): Promise<void> {
safeSpot = safeSpot || { x: 10, y: 10 }
const client = await this.page.context().newCDPSession(this.page)
await client.send('Input.dispatchTouchEvent', {
type: 'touchStart',
touchPoints: [safeSpot]
})
await client.send('Input.dispatchTouchEvent', {
type: 'touchMove',
touchPoints: [{ x: offset.x + safeSpot.x, y: offset.y + safeSpot.y }]
})
await client.send('Input.dispatchTouchEvent', {
type: 'touchEnd',
touchPoints: []
})
await this.nextFrame()
}
async rightClick(x: number = 10, y: number = 10): Promise<void> {
await this.page.mouse.click(x, y, { button: 'right' })
await this.nextFrame()
}
async doubleClick(): Promise<void> {
await this.page.mouse.dblclick(10, 10, { delay: 5 })
await this.nextFrame()
}
async click(position: Position): Promise<void> {
await this.canvas.click({ position })
await this.nextFrame()
}
async clickEmptySpace(): Promise<void> {
await this.canvas.click({ position: DefaultGraphPositions.emptySpaceClick })
await this.nextFrame()
}
async dragAndDrop(source: Position, target: Position): Promise<void> {
await this.page.mouse.move(source.x, source.y)
await this.page.mouse.down()
await this.page.mouse.move(target.x, target.y, { steps: 100 })
await this.page.mouse.up()
await this.nextFrame()
}
async moveMouseToEmptyArea(): Promise<void> {
await this.page.mouse.move(10, 10)
}
async getScale(): Promise<number> {
return this.page.evaluate(() => {
return window.app!.canvas.ds.scale
})
}
async setScale(scale: number): Promise<void> {
await this.page.evaluate((s) => {
window.app!.canvas.ds.scale = s
}, scale)
await this.nextFrame()
}
async convertOffsetToCanvas(
pos: [number, number]
): Promise<[number, number]> {
return this.page.evaluate((pos) => {
return window.app!.canvas.ds.convertOffsetToCanvas(pos)
}, pos)
}
async getNodeCenterByTitle(title: string): Promise<Position | null> {
return this.page.evaluate((title) => {
const app = window.app!
const node = app.graph.nodes.find(
(n: { title: string }) => n.title === title
)
if (!node) return null
const centerX = node.pos[0] + node.size[0] / 2
const centerY = node.pos[1] + node.size[1] / 2
const [clientX, clientY] = app.canvasPosToClientPos([centerX, centerY])
return { x: clientX, y: clientY }
}, title)
}
async getGroupPosition(title: string): Promise<Position> {
const pos = await this.page.evaluate((title) => {
const groups = window.app!.graph.groups
const group = groups.find((g: { title: string }) => g.title === title)
if (!group) return null
return { x: group.pos[0], y: group.pos[1] }
}, title)
if (!pos) throw new Error(`Group "${title}" not found`)
return pos
}
async dragGroup(options: {
name: string
deltaX: number
deltaY: number
}): Promise<void> {
const { name, deltaX, deltaY } = options
const screenPos = await this.page.evaluate((title) => {
const app = window.app!
const groups = app.graph.groups
const group = groups.find((g: { title: string }) => g.title === title)
if (!group) return null
const clientPos = app.canvasPosToClientPos([
group.pos[0] + 50,
group.pos[1] + 15
])
return { x: clientPos[0], y: clientPos[1] }
}, name)
if (!screenPos) throw new Error(`Group "${name}" not found`)
await this.dragAndDrop(screenPos, {
x: screenPos.x + deltaX,
y: screenPos.y + deltaY
})
}
async disconnectEdge(): Promise<void> {
await this.dragAndDrop(
DefaultGraphPositions.clipTextEncodeNode1InputSlot,
DefaultGraphPositions.emptySpace
)
}
async connectEdge(options: { reverse?: boolean } = {}): Promise<void> {
const { reverse = false } = options
const start = reverse
? DefaultGraphPositions.clipTextEncodeNode1InputSlot
: DefaultGraphPositions.loadCheckpointNodeClipOutputSlot
const end = reverse
? DefaultGraphPositions.loadCheckpointNodeClipOutputSlot
: DefaultGraphPositions.clipTextEncodeNode1InputSlot
await this.dragAndDrop(start, end)
}
}

View File

@@ -0,0 +1,15 @@
import type { Locator } from '@playwright/test'
import type { KeyboardHelper } from './KeyboardHelper'
export class ClipboardHelper {
constructor(private readonly keyboard: KeyboardHelper) {}
async copy(locator?: Locator | null): Promise<void> {
await this.keyboard.ctrlSend('KeyC', locator ?? null)
}
async paste(locator?: Locator | null): Promise<void> {
await this.keyboard.ctrlSend('KeyV', locator ?? null)
}
}

View File

@@ -0,0 +1,76 @@
import type { Page } from '@playwright/test'
import type { KeyCombo } from '../../../src/platform/keybindings/types'
export class CommandHelper {
constructor(private readonly page: Page) {}
async executeCommand(commandId: string): Promise<void> {
await this.page.evaluate((id: string) => {
return window.app!.extensionManager.command.execute(id)
}, commandId)
}
async registerCommand(
commandId: string,
command: (() => void) | (() => Promise<void>)
): Promise<void> {
// SECURITY: eval() is intentionally used here to deserialize/execute functions
// passed from controlled test code across the Node/Playwright browser boundary.
// Execution happens in isolated Playwright browser contexts with test-only data.
// This pattern is unsafe for production and must not be copied elsewhere.
await this.page.evaluate(
({ commandId, commandStr }) => {
const app = window.app!
const randomSuffix = Math.random().toString(36).substring(2, 8)
const extensionName = `TestExtension_${randomSuffix}`
app.registerExtension({
name: extensionName,
commands: [
{
id: commandId,
function: eval(commandStr)
}
]
})
},
{ commandId, commandStr: command.toString() }
)
}
async registerKeybinding(
keyCombo: KeyCombo,
command: () => void
): Promise<void> {
// SECURITY: eval() is intentionally used here to deserialize/execute functions
// passed from controlled test code across the Node/Playwright browser boundary.
// Execution happens in isolated Playwright browser contexts with test-only data.
// This pattern is unsafe for production and must not be copied elsewhere.
await this.page.evaluate(
({ keyCombo, commandStr }) => {
const app = window.app!
const randomSuffix = Math.random().toString(36).substring(2, 8)
const extensionName = `TestExtension_${randomSuffix}`
const commandId = `TestCommand_${randomSuffix}`
app.registerExtension({
name: extensionName,
keybindings: [
{
combo: keyCombo,
commandId: commandId
}
],
commands: [
{
id: commandId,
function: eval(commandStr)
}
]
})
},
{ keyCombo, commandStr: command.toString() }
)
}
}

View File

@@ -0,0 +1,167 @@
import type { Locator, Page, TestInfo } from '@playwright/test'
import type { Position } from '../types'
export interface DebugScreenshotOptions {
fullPage?: boolean
element?: 'canvas' | 'page'
markers?: Array<{ position: Position; id?: string }>
}
export class DebugHelper {
constructor(
private page: Page,
private canvas: Locator
) {}
async addMarker(
position: Position,
id: string = 'debug-marker'
): Promise<void> {
await this.page.evaluate(
([pos, markerId]) => {
const existing = document.getElementById(markerId)
if (existing) existing.remove()
const marker = document.createElement('div')
marker.id = markerId
marker.style.position = 'fixed'
marker.style.left = `${pos.x - 10}px`
marker.style.top = `${pos.y - 10}px`
marker.style.width = '20px'
marker.style.height = '20px'
marker.style.border = '2px solid red'
marker.style.borderRadius = '50%'
marker.style.backgroundColor = 'rgba(255, 0, 0, 0.3)'
marker.style.pointerEvents = 'none'
marker.style.zIndex = '10000'
document.body.appendChild(marker)
},
[position, id] as const
)
}
async removeMarkers(): Promise<void> {
await this.page.evaluate(() => {
document
.querySelectorAll('[id^="debug-marker"]')
.forEach((el) => el.remove())
})
}
async attachScreenshot(
testInfo: TestInfo,
name: string,
options?: DebugScreenshotOptions
): Promise<void> {
if (options?.markers) {
for (const marker of options.markers) {
await this.addMarker(marker.position, marker.id)
}
}
let screenshot: Buffer
const targetElement = options?.element || 'page'
if (targetElement === 'canvas') {
screenshot = await this.canvas.screenshot()
} else if (options?.fullPage) {
screenshot = await this.page.screenshot({ fullPage: true })
} else {
screenshot = await this.page.screenshot()
}
await testInfo.attach(name, {
body: screenshot,
contentType: 'image/png'
})
if (options?.markers) {
await this.removeMarkers()
}
}
async saveCanvasScreenshot(filename: string): Promise<void> {
await this.page.evaluate(async (filename) => {
const canvas = document.getElementById(
'graph-canvas'
) as HTMLCanvasElement
if (!canvas) {
throw new Error('Canvas not found')
}
return new Promise<void>((resolve) => {
canvas.toBlob(async (blob) => {
if (!blob) {
throw new Error('Failed to create blob from canvas')
}
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
resolve()
}, 'image/png')
})
}, filename)
}
async getCanvasDataURL(): Promise<string> {
return await this.page.evaluate(() => {
const canvas = document.getElementById(
'graph-canvas'
) as HTMLCanvasElement
if (!canvas) {
throw new Error('Canvas not found')
}
return canvas.toDataURL('image/png')
})
}
async showCanvasOverlay(): Promise<void> {
await this.page.evaluate(() => {
const canvas = document.getElementById(
'graph-canvas'
) as HTMLCanvasElement
if (!canvas) {
throw new Error('Canvas not found')
}
const existingOverlay = document.getElementById('debug-canvas-overlay')
if (existingOverlay) {
existingOverlay.remove()
}
const overlay = document.createElement('div')
overlay.id = 'debug-canvas-overlay'
overlay.style.position = 'fixed'
overlay.style.top = '0'
overlay.style.left = '0'
overlay.style.zIndex = '9999'
overlay.style.backgroundColor = 'white'
overlay.style.padding = '10px'
overlay.style.border = '2px solid red'
const img = document.createElement('img')
img.src = canvas.toDataURL('image/png')
img.style.maxWidth = '800px'
img.style.maxHeight = '600px'
overlay.appendChild(img)
document.body.appendChild(overlay)
})
}
async hideCanvasOverlay(): Promise<void> {
await this.page.evaluate(() => {
const overlay = document.getElementById('debug-canvas-overlay')
if (overlay) {
overlay.remove()
}
})
}
}

View File

@@ -0,0 +1,161 @@
import { readFileSync } from 'fs'
import type { Page } from '@playwright/test'
import type { Position } from '../types'
export class DragDropHelper {
constructor(
private readonly page: Page,
private readonly assetPath: (fileName: string) => string
) {}
private async nextFrame(): Promise<void> {
await this.page.evaluate(() => {
return new Promise<void>((resolve) => {
requestAnimationFrame(() => resolve())
})
})
}
async dragAndDropExternalResource(
options: {
fileName?: string
url?: string
dropPosition?: Position
waitForUpload?: boolean
} = {}
): Promise<void> {
const {
dropPosition = { x: 100, y: 100 },
fileName,
url,
waitForUpload = false
} = options
if (!fileName && !url)
throw new Error('Must provide either fileName or url')
const evaluateParams: {
dropPosition: Position
fileName?: string
fileType?: string
buffer?: Uint8Array | number[]
url?: string
} = { dropPosition }
if (fileName) {
const filePath = this.assetPath(fileName)
const buffer = readFileSync(filePath)
const getFileType = (fileName: string) => {
if (fileName.endsWith('.png')) return 'image/png'
if (fileName.endsWith('.svg')) return 'image/svg+xml'
if (fileName.endsWith('.webp')) return 'image/webp'
if (fileName.endsWith('.webm')) return 'video/webm'
if (fileName.endsWith('.json')) return 'application/json'
if (fileName.endsWith('.glb')) return 'model/gltf-binary'
if (fileName.endsWith('.avif')) return 'image/avif'
return 'application/octet-stream'
}
evaluateParams.fileName = fileName
evaluateParams.fileType = getFileType(fileName)
evaluateParams.buffer = [...new Uint8Array(buffer)]
}
if (url) evaluateParams.url = url
const uploadResponsePromise = waitForUpload
? this.page.waitForResponse(
(resp) => resp.url().includes('/upload/') && resp.status() === 200,
{ timeout: 10000 }
)
: null
await this.page.evaluate(async (params) => {
const dataTransfer = new DataTransfer()
if (params.buffer && params.fileName && params.fileType) {
const file = new File(
[new Uint8Array(params.buffer)],
params.fileName,
{
type: params.fileType
}
)
dataTransfer.items.add(file)
}
if (params.url) {
dataTransfer.setData('text/uri-list', params.url)
dataTransfer.setData('text/x-moz-url', params.url)
}
const targetElement = document.elementFromPoint(
params.dropPosition.x,
params.dropPosition.y
)
if (!targetElement) {
throw new Error(
`No element found at drop position: (${params.dropPosition.x}, ${params.dropPosition.y}). ` +
`document.elementFromPoint returned null. Ensure the target is visible and not obscured.`
)
}
const eventOptions = {
bubbles: true,
cancelable: true,
dataTransfer,
clientX: params.dropPosition.x,
clientY: params.dropPosition.y
}
const dragOverEvent = new DragEvent('dragover', eventOptions)
const dropEvent = new DragEvent('drop', eventOptions)
Object.defineProperty(dropEvent, 'preventDefault', {
value: () => {},
writable: false
})
Object.defineProperty(dropEvent, 'stopPropagation', {
value: () => {},
writable: false
})
targetElement.dispatchEvent(dragOverEvent)
targetElement.dispatchEvent(dropEvent)
return {
success: true,
targetInfo: {
tagName: targetElement.tagName,
id: targetElement.id,
classList: Array.from(targetElement.classList)
}
}
}, evaluateParams)
if (uploadResponsePromise) {
await uploadResponsePromise
}
await this.nextFrame()
}
async dragAndDropFile(
fileName: string,
options: { dropPosition?: Position; waitForUpload?: boolean } = {}
): Promise<void> {
return this.dragAndDropExternalResource({ fileName, ...options })
}
async dragAndDropURL(
url: string,
options: { dropPosition?: Position } = {}
): Promise<void> {
return this.dragAndDropExternalResource({ url, ...options })
}
}

View File

@@ -0,0 +1,45 @@
import type { Locator, Page } from '@playwright/test'
export class KeyboardHelper {
constructor(
private readonly page: Page,
private readonly canvas: Locator
) {}
private async nextFrame(): Promise<void> {
await this.page.evaluate(() => new Promise<number>(requestAnimationFrame))
}
async ctrlSend(
keyToPress: string,
locator: Locator | null = this.canvas
): Promise<void> {
const target = locator ?? this.page.keyboard
await target.press(`Control+${keyToPress}`)
await this.nextFrame()
}
async selectAll(locator?: Locator | null): Promise<void> {
await this.ctrlSend('KeyA', locator)
}
async bypass(locator?: Locator | null): Promise<void> {
await this.ctrlSend('KeyB', locator)
}
async undo(locator?: Locator | null): Promise<void> {
await this.ctrlSend('KeyZ', locator)
}
async redo(locator?: Locator | null): Promise<void> {
await this.ctrlSend('KeyY', locator)
}
async moveUp(locator?: Locator | null): Promise<void> {
await this.ctrlSend('ArrowUp', locator)
}
async moveDown(locator?: Locator | null): Promise<void> {
await this.ctrlSend('ArrowDown', locator)
}
}

View File

@@ -0,0 +1,182 @@
import type { Locator } from '@playwright/test'
import type {
LGraph,
LGraphNode
} from '../../../src/lib/litegraph/src/litegraph'
import type { NodeId } from '../../../src/platform/workflow/validation/schemas/workflowSchema'
import type { ComfyPage } from '../ComfyPage'
import { DefaultGraphPositions } from '../constants/defaultGraphPositions'
import type { Position, Size } from '../types'
import { NodeReference } from '../utils/litegraphUtils'
export class NodeOperationsHelper {
constructor(private comfyPage: ComfyPage) {}
private get page() {
return this.comfyPage.page
}
async getGraphNodesCount(): Promise<number> {
return await this.page.evaluate(() => {
return window.app?.graph?.nodes?.length || 0
})
}
async getSelectedGraphNodesCount(): Promise<number> {
return await this.page.evaluate(() => {
return (
window.app?.graph?.nodes?.filter(
(node: LGraphNode) => node.is_selected === true
).length || 0
)
})
}
async getNodes(): Promise<LGraphNode[]> {
return await this.page.evaluate(() => {
return window.app!.graph.nodes
})
}
async waitForGraphNodes(count: number): Promise<void> {
await this.page.waitForFunction((count) => {
return window.app?.canvas.graph?.nodes?.length === count
}, count)
}
async getFirstNodeRef(): Promise<NodeReference | null> {
const id = await this.page.evaluate(() => {
return window.app!.graph.nodes[0]?.id
})
if (!id) return null
return this.getNodeRefById(id)
}
async getNodeRefById(id: NodeId): Promise<NodeReference> {
return new NodeReference(id, this.comfyPage)
}
async getNodeRefsByType(
type: string,
includeSubgraph: boolean = false
): Promise<NodeReference[]> {
return Promise.all(
(
await this.page.evaluate(
({ type, includeSubgraph }) => {
const graph = (
includeSubgraph ? window.app!.canvas.graph : window.app!.graph
) as LGraph
const nodes = graph.nodes
return nodes
.filter((n: LGraphNode) => n.type === type)
.map((n: LGraphNode) => n.id)
},
{ type, includeSubgraph }
)
).map((id: NodeId) => this.getNodeRefById(id))
)
}
async getNodeRefsByTitle(title: string): Promise<NodeReference[]> {
return Promise.all(
(
await this.page.evaluate((title) => {
return window
.app!.graph.nodes.filter((n: LGraphNode) => n.title === title)
.map((n: LGraphNode) => n.id)
}, title)
).map((id: NodeId) => this.getNodeRefById(id))
)
}
async selectNodes(nodeTitles: string[]): Promise<void> {
await this.page.keyboard.down('Control')
try {
for (const nodeTitle of nodeTitles) {
const nodes = await this.getNodeRefsByTitle(nodeTitle)
for (const node of nodes) {
await node.click('title')
}
}
} finally {
await this.page.keyboard.up('Control')
await this.comfyPage.nextFrame()
}
}
async resizeNode(
nodePos: Position,
nodeSize: Size,
ratioX: number,
ratioY: number,
revertAfter: boolean = false
): Promise<void> {
const bottomRight = {
x: nodePos.x + nodeSize.width,
y: nodePos.y + nodeSize.height
}
const target = {
x: nodePos.x + nodeSize.width * ratioX,
y: nodePos.y + nodeSize.height * ratioY
}
// -1 to be inside the node. -2 because nodes currently get an arbitrary +1 to width.
await this.comfyPage.canvasOps.dragAndDrop(
{ x: bottomRight.x - 2, y: bottomRight.y - 1 },
target
)
await this.comfyPage.nextFrame()
if (revertAfter) {
await this.comfyPage.canvasOps.dragAndDrop(
{ x: target.x - 2, y: target.y - 1 },
bottomRight
)
await this.comfyPage.nextFrame()
}
}
async convertAllNodesToGroupNode(groupNodeName: string): Promise<void> {
await this.comfyPage.canvas.press('Control+a')
const node = await this.getFirstNodeRef()
if (!node) {
throw new Error('No nodes found to convert')
}
await node.clickContextMenuOption('Convert to Group Node')
await this.fillPromptDialog(groupNodeName)
await this.comfyPage.nextFrame()
}
get promptDialogInput(): Locator {
return this.page.locator('.p-dialog-content input[type="text"]')
}
async fillPromptDialog(value: string): Promise<void> {
await this.promptDialogInput.fill(value)
await this.page.keyboard.press('Enter')
await this.promptDialogInput.waitFor({ state: 'hidden' })
await this.comfyPage.nextFrame()
}
async dragTextEncodeNode2(): Promise<void> {
await this.comfyPage.canvasOps.dragAndDrop(
DefaultGraphPositions.textEncodeNode2,
{
x: DefaultGraphPositions.textEncodeNode2.x,
y: 300
}
)
await this.comfyPage.nextFrame()
}
async adjustEmptyLatentWidth(): Promise<void> {
await this.page.locator('#graph-canvas').click({
position: DefaultGraphPositions.emptyLatentWidgetClick
})
const dialogInput = this.page.locator('.graphdialog input[type="text"]')
await dialogInput.click()
await dialogInput.fill('128')
await dialogInput.press('Enter')
await this.comfyPage.nextFrame()
}
}

View File

@@ -0,0 +1,20 @@
import type { Page } from '@playwright/test'
export class SettingsHelper {
constructor(private readonly page: Page) {}
async setSetting(settingId: string, settingValue: unknown): Promise<void> {
await this.page.evaluate(
async ({ id, value }) => {
await window.app!.extensionManager.setting.set(id, value)
},
{ id: settingId, value: settingValue }
)
}
async getSetting<T = unknown>(settingId: string): Promise<T> {
return (await this.page.evaluate(async (id) => {
return await window.app!.extensionManager.setting.get(id)
}, settingId)) as T
}
}

View File

@@ -0,0 +1,325 @@
import type { Page } from '@playwright/test'
import type {
CanvasPointerEvent,
Subgraph
} from '@/lib/litegraph/src/litegraph'
import type { ComfyPage } from '../ComfyPage'
import type { NodeReference } from '../utils/litegraphUtils'
import { SubgraphSlotReference } from '../utils/litegraphUtils'
export class SubgraphHelper {
constructor(private readonly comfyPage: ComfyPage) {}
private get page(): Page {
return this.comfyPage.page
}
/**
* Core helper method for interacting with subgraph I/O slots.
* Handles both input/output slots and both right-click/double-click actions.
*
* @param slotType - 'input' or 'output'
* @param action - 'rightClick' or 'doubleClick'
* @param slotName - Optional specific slot name to target
*/
private async interactWithSubgraphSlot(
slotType: 'input' | 'output',
action: 'rightClick' | 'doubleClick',
slotName?: string
): Promise<void> {
const foundSlot = await this.page.evaluate(
async (params) => {
const { slotType, action, targetSlotName } = params
const app = window.app!
const currentGraph = app.canvas!.graph!
// Check if we're in a subgraph
if (currentGraph.constructor.name !== 'Subgraph') {
throw new Error(
'Not in a subgraph - this method only works inside subgraphs'
)
}
const subgraph = currentGraph as Subgraph
// Get the appropriate node and slots
const node =
slotType === 'input' ? subgraph.inputNode : subgraph.outputNode
const slots = slotType === 'input' ? subgraph.inputs : subgraph.outputs
if (!node) {
throw new Error(`No ${slotType} node found in subgraph`)
}
if (!slots || slots.length === 0) {
throw new Error(`No ${slotType} slots found in subgraph`)
}
// Filter slots based on target name and action type
const slotsToTry = targetSlotName
? slots.filter((slot) => slot.name === targetSlotName)
: action === 'rightClick'
? slots
: [slots[0]] // Right-click tries all, double-click uses first
if (slotsToTry.length === 0) {
throw new Error(
targetSlotName
? `${slotType} slot '${targetSlotName}' not found`
: `No ${slotType} slots available to try`
)
}
// Handle the interaction based on action type
if (action === 'rightClick') {
// Right-click: try each slot until one works
for (const slot of slotsToTry) {
if (!slot.pos) continue
const event = {
canvasX: slot.pos[0],
canvasY: slot.pos[1],
button: 2, // Right mouse button
preventDefault: () => {},
stopPropagation: () => {}
}
if (node.onPointerDown) {
node.onPointerDown(
event as unknown as CanvasPointerEvent,
app.canvas.pointer,
app.canvas.linkConnector
)
return {
success: true,
slotName: slot.name,
x: slot.pos[0],
y: slot.pos[1]
}
}
}
} else if (action === 'doubleClick') {
// Double-click: use first slot with bounding rect center
const slot = slotsToTry[0]
if (!slot.boundingRect) {
throw new Error(`${slotType} slot bounding rect not found`)
}
const rect = slot.boundingRect
const testX = rect[0] + rect[2] / 2 // x + width/2
const testY = rect[1] + rect[3] / 2 // y + height/2
const event = {
canvasX: testX,
canvasY: testY,
button: 0, // Left mouse button
preventDefault: () => {},
stopPropagation: () => {}
}
if (node.onPointerDown) {
node.onPointerDown(
event as unknown as CanvasPointerEvent,
app.canvas.pointer,
app.canvas.linkConnector
)
// Trigger double-click
if (app.canvas.pointer.onDoubleClick) {
app.canvas.pointer.onDoubleClick(
event as unknown as CanvasPointerEvent
)
}
}
return { success: true, slotName: slot.name, x: testX, y: testY }
}
return { success: false }
},
{ slotType, action, targetSlotName: slotName }
)
if (!foundSlot.success) {
const actionText =
action === 'rightClick' ? 'open context menu for' : 'double-click'
throw new Error(
slotName
? `Could not ${actionText} ${slotType} slot '${slotName}'`
: `Could not find any ${slotType} slot to ${actionText}`
)
}
// Wait for the appropriate UI element to appear
if (action === 'rightClick') {
await this.page.waitForSelector('.litemenu-entry', {
state: 'visible',
timeout: 5000
})
} else {
await this.comfyPage.nextFrame()
}
}
/**
* Right-clicks on a subgraph input slot to open the context menu.
* Must be called when inside a subgraph.
*
* This method uses the actual slot positions from the subgraph.inputs array,
* which contain the correct coordinates for each input slot. These positions
* are different from the visual node positions and are specifically where
* the slots are rendered on the input node.
*
* @param inputName Optional name of the specific input slot to target (e.g., 'text').
* If not provided, tries all available input slots until one works.
* @returns Promise that resolves when the context menu appears
*/
async rightClickInputSlot(inputName?: string): Promise<void> {
return this.interactWithSubgraphSlot('input', 'rightClick', inputName)
}
/**
* Right-clicks on a subgraph output slot to open the context menu.
* Must be called when inside a subgraph.
*
* Similar to rightClickInputSlot but for output slots.
*
* @param outputName Optional name of the specific output slot to target.
* If not provided, tries all available output slots until one works.
* @returns Promise that resolves when the context menu appears
*/
async rightClickOutputSlot(outputName?: string): Promise<void> {
return this.interactWithSubgraphSlot('output', 'rightClick', outputName)
}
/**
* Double-clicks on a subgraph input slot to rename it.
* Must be called when inside a subgraph.
*
* @param inputName Optional name of the specific input slot to target (e.g., 'text').
* If not provided, tries the first available input slot.
* @returns Promise that resolves when the rename dialog appears
*/
async doubleClickInputSlot(inputName?: string): Promise<void> {
return this.interactWithSubgraphSlot('input', 'doubleClick', inputName)
}
/**
* Double-clicks on a subgraph output slot to rename it.
* Must be called when inside a subgraph.
*
* @param outputName Optional name of the specific output slot to target.
* If not provided, tries the first available output slot.
* @returns Promise that resolves when the rename dialog appears
*/
async doubleClickOutputSlot(outputName?: string): Promise<void> {
return this.interactWithSubgraphSlot('output', 'doubleClick', outputName)
}
/**
* Get a reference to a subgraph input slot
*/
getInputSlot(slotName?: string): SubgraphSlotReference {
return new SubgraphSlotReference('input', slotName || '', this.comfyPage)
}
/**
* Get a reference to a subgraph output slot
*/
getOutputSlot(slotName?: string): SubgraphSlotReference {
return new SubgraphSlotReference('output', slotName || '', this.comfyPage)
}
/**
* Connect a regular node output to a subgraph input.
* This creates a new input slot on the subgraph if targetInputName is not provided.
*/
async connectToInput(
sourceNode: NodeReference,
sourceSlotIndex: number,
targetInputName?: string
): Promise<void> {
const sourceSlot = await sourceNode.getOutput(sourceSlotIndex)
const targetSlot = this.getInputSlot(targetInputName)
const targetPosition = targetInputName
? await targetSlot.getPosition() // Connect to existing slot
: await targetSlot.getOpenSlotPosition() // Create new slot
await this.comfyPage.canvasOps.dragAndDrop(
await sourceSlot.getPosition(),
targetPosition
)
await this.comfyPage.nextFrame()
}
/**
* Connect a subgraph input to a regular node input.
* This creates a new input slot on the subgraph if sourceInputName is not provided.
*/
async connectFromInput(
targetNode: NodeReference,
targetSlotIndex: number,
sourceInputName?: string
): Promise<void> {
const sourceSlot = this.getInputSlot(sourceInputName)
const targetSlot = await targetNode.getInput(targetSlotIndex)
const sourcePosition = sourceInputName
? await sourceSlot.getPosition() // Connect from existing slot
: await sourceSlot.getOpenSlotPosition() // Create new slot
const targetPosition = await targetSlot.getPosition()
await this.comfyPage.canvasOps.dragAndDrop(sourcePosition, targetPosition)
await this.comfyPage.nextFrame()
}
/**
* Connect a regular node output to a subgraph output.
* This creates a new output slot on the subgraph if targetOutputName is not provided.
*/
async connectToOutput(
sourceNode: NodeReference,
sourceSlotIndex: number,
targetOutputName?: string
): Promise<void> {
const sourceSlot = await sourceNode.getOutput(sourceSlotIndex)
const targetSlot = this.getOutputSlot(targetOutputName)
const targetPosition = targetOutputName
? await targetSlot.getPosition() // Connect to existing slot
: await targetSlot.getOpenSlotPosition() // Create new slot
await this.comfyPage.canvasOps.dragAndDrop(
await sourceSlot.getPosition(),
targetPosition
)
await this.comfyPage.nextFrame()
}
/**
* Connect a subgraph output to a regular node input.
* This creates a new output slot on the subgraph if sourceOutputName is not provided.
*/
async connectFromOutput(
targetNode: NodeReference,
targetSlotIndex: number,
sourceOutputName?: string
): Promise<void> {
const sourceSlot = this.getOutputSlot(sourceOutputName)
const targetSlot = await targetNode.getInput(targetSlotIndex)
const sourcePosition = sourceOutputName
? await sourceSlot.getPosition() // Connect from existing slot
: await sourceSlot.getOpenSlotPosition() // Create new slot
await this.comfyPage.canvasOps.dragAndDrop(
sourcePosition,
await targetSlot.getPosition()
)
await this.comfyPage.nextFrame()
}
}

View File

@@ -0,0 +1,39 @@
import { expect } from '@playwright/test'
import type { Locator, Page } from '@playwright/test'
export class ToastHelper {
constructor(private readonly page: Page) {}
get visibleToasts(): Locator {
return this.page.locator('.p-toast-message:visible')
}
async getToastErrorCount(): Promise<number> {
return await this.page
.locator('.p-toast-message.p-toast-message-error')
.count()
}
async getVisibleToastCount(): Promise<number> {
return await this.visibleToasts.count()
}
async closeToasts(requireCount = 0): Promise<void> {
if (requireCount) {
await this.visibleToasts
.nth(requireCount - 1)
.waitFor({ state: 'visible' })
}
// Clear all toasts
const toastCloseButtons = await this.page
.locator('.p-toast-close-button')
.all()
for (const button of toastCloseButtons) {
await button.click()
}
// Assert all toasts are closed
await expect(this.visibleToasts).toHaveCount(0, { timeout: 1000 })
}
}

View File

@@ -0,0 +1,126 @@
import { readFileSync } from 'fs'
import type {
ComfyApiWorkflow,
ComfyWorkflowJSON
} from '../../../src/platform/workflow/validation/schemas/workflowSchema'
import type { WorkspaceStore } from '../../types/globals'
import type { ComfyPage } from '../ComfyPage'
type FolderStructure = {
[key: string]: FolderStructure | string
}
export class WorkflowHelper {
constructor(private readonly comfyPage: ComfyPage) {}
convertLeafToContent(structure: FolderStructure): FolderStructure {
const result: FolderStructure = {}
for (const [key, value] of Object.entries(structure)) {
if (typeof value === 'string') {
const filePath = this.comfyPage.assetPath(value)
result[key] = readFileSync(filePath, 'utf-8')
} else {
result[key] = this.convertLeafToContent(value)
}
}
return result
}
async setupWorkflowsDirectory(structure: FolderStructure) {
const resp = await this.comfyPage.request.post(
`${this.comfyPage.url}/api/devtools/setup_folder_structure`,
{
data: {
tree_structure: this.convertLeafToContent(structure),
base_path: `user/${this.comfyPage.id}/workflows`
}
}
)
if (resp.status() !== 200) {
throw new Error(
`Failed to setup workflows directory: ${await resp.text()}`
)
}
await this.comfyPage.page.evaluate(async () => {
await (
window.app!.extensionManager as WorkspaceStore
).workflow.syncWorkflows()
})
// Wait for Vue to re-render the workflow list
await this.comfyPage.nextFrame()
}
async loadWorkflow(workflowName: string) {
await this.comfyPage.workflowUploadInput.setInputFiles(
this.comfyPage.assetPath(`${workflowName}.json`)
)
await this.comfyPage.nextFrame()
}
async deleteWorkflow(
workflowName: string,
whenMissing: 'ignoreMissing' | 'throwIfMissing' = 'ignoreMissing'
) {
// Open workflows tab
const { workflowsTab } = this.comfyPage.menu
await workflowsTab.open()
// Action to take if workflow missing
if (whenMissing === 'ignoreMissing') {
const workflows = await workflowsTab.getTopLevelSavedWorkflowNames()
if (!workflows.includes(workflowName)) return
}
// Delete workflow
await workflowsTab.getPersistedItem(workflowName).click({ button: 'right' })
await this.comfyPage.contextMenu.clickMenuItem('Delete')
await this.comfyPage.nextFrame()
await this.comfyPage.confirmDialog.delete.click()
// Clear toast & close tab
await this.comfyPage.toast.closeToasts(1)
await workflowsTab.close()
}
async getUndoQueueSize(): Promise<number | undefined> {
return this.comfyPage.page.evaluate(() => {
const workflow = (window.app!.extensionManager as WorkspaceStore).workflow
.activeWorkflow
return workflow?.changeTracker.undoQueue.length
})
}
async getRedoQueueSize(): Promise<number | undefined> {
return this.comfyPage.page.evaluate(() => {
const workflow = (window.app!.extensionManager as WorkspaceStore).workflow
.activeWorkflow
return workflow?.changeTracker.redoQueue.length
})
}
async isCurrentWorkflowModified(): Promise<boolean | undefined> {
return this.comfyPage.page.evaluate(() => {
return (window.app!.extensionManager as WorkspaceStore).workflow
.activeWorkflow?.isModified
})
}
async getExportedWorkflow(options: { api: true }): Promise<ComfyApiWorkflow>
async getExportedWorkflow(options?: {
api?: false
}): Promise<ComfyWorkflowJSON>
async getExportedWorkflow(options?: {
api?: boolean
}): Promise<ComfyWorkflowJSON | ComfyApiWorkflow> {
const api = options?.api ?? false
return this.comfyPage.page.evaluate(async (api) => {
return (await window.app!.graphToPrompt())[api ? 'output' : 'workflow']
}, api)
}
}

View File

@@ -0,0 +1,77 @@
/**
* Centralized test selectors for browser tests.
* Use data-testid attributes for stable selectors.
*/
export const TestIds = {
sidebar: {
toolbar: 'side-toolbar',
nodeLibrary: 'node-library-tree',
nodeLibrarySearch: 'node-library-search',
workflows: 'workflows-sidebar',
modeToggle: 'mode-toggle'
},
tree: {
folder: 'tree-folder',
leaf: 'tree-leaf',
node: 'tree-node'
},
canvas: {
main: 'graph-canvas',
contextMenu: 'canvas-context-menu',
toggleMinimapButton: 'toggle-minimap-button',
toggleLinkVisibilityButton: 'toggle-link-visibility-button'
},
dialogs: {
settings: 'settings-dialog',
settingsContainer: 'settings-container',
settingsTabAbout: 'settings-tab-about',
confirm: 'confirm-dialog',
about: 'about-panel',
whatsNewSection: 'whats-new-section'
},
topbar: {
queueButton: 'queue-button',
saveButton: 'save-workflow-button'
},
nodeLibrary: {
bookmarksSection: 'node-library-bookmarks-section'
},
propertiesPanel: {
root: 'properties-panel'
},
node: {
titleInput: 'node-title-input'
},
widgets: {
decrement: 'decrement',
increment: 'increment',
subgraphEnterButton: 'subgraph-enter-button'
},
templates: {
content: 'template-workflows-content',
workflowCard: (id: string) => `template-workflow-${id}`
},
user: {
currentUserIndicator: 'current-user-indicator'
}
} as const
/**
* Helper type for accessing nested TestIds (excludes function values)
*/
export type TestIdValue =
| (typeof TestIds.sidebar)[keyof typeof TestIds.sidebar]
| (typeof TestIds.tree)[keyof typeof TestIds.tree]
| (typeof TestIds.canvas)[keyof typeof TestIds.canvas]
| (typeof TestIds.dialogs)[keyof typeof TestIds.dialogs]
| (typeof TestIds.topbar)[keyof typeof TestIds.topbar]
| (typeof TestIds.nodeLibrary)[keyof typeof TestIds.nodeLibrary]
| (typeof TestIds.propertiesPanel)[keyof typeof TestIds.propertiesPanel]
| (typeof TestIds.node)[keyof typeof TestIds.node]
| (typeof TestIds.widgets)[keyof typeof TestIds.widgets]
| Exclude<
(typeof TestIds.templates)[keyof typeof TestIds.templates],
(id: string) => string
>
| (typeof TestIds.user)[keyof typeof TestIds.user]

View File

@@ -1,3 +1,4 @@
import { expect } from '@playwright/test'
import type { Page } from '@playwright/test'
import type { NodeId } from '../../../src/platform/workflow/validation/schemas/workflowSchema'
@@ -22,10 +23,10 @@ export class SubgraphSlotReference {
async getPosition(): Promise<Position> {
const pos: [number, number] = await this.comfyPage.page.evaluate(
([type, slotName]) => {
const currentGraph = window['app'].canvas.graph
const currentGraph = window.app!.canvas.graph!
// Check if we're in a subgraph
if (currentGraph.constructor.name !== 'Subgraph') {
// Check if we're in a subgraph (subgraphs have inputNode property)
if (!('inputNode' in currentGraph)) {
throw new Error(
'Not in a subgraph - this method only works inside subgraphs'
)
@@ -51,7 +52,7 @@ export class SubgraphSlotReference {
}
// Convert from offset to canvas coordinates
const canvasPos = window['app'].canvas.ds.convertOffsetToCanvas([
const canvasPos = window.app!.canvas.ds.convertOffsetToCanvas([
slot.pos[0],
slot.pos[1]
])
@@ -69,9 +70,10 @@ export class SubgraphSlotReference {
async getOpenSlotPosition(): Promise<Position> {
const pos: [number, number] = await this.comfyPage.page.evaluate(
([type]) => {
const currentGraph = window['app'].canvas.graph
const currentGraph = window.app!.canvas.graph!
if (currentGraph.constructor.name !== 'Subgraph') {
// Check if we're in a subgraph (subgraphs have inputNode property)
if (!('inputNode' in currentGraph)) {
throw new Error(
'Not in a subgraph - this method only works inside subgraphs'
)
@@ -85,7 +87,7 @@ export class SubgraphSlotReference {
}
// Convert from offset to canvas coordinates
const canvasPos = window['app'].canvas.ds.convertOffsetToCanvas([
const canvasPos = window.app!.canvas.ds.convertOffsetToCanvas([
node.emptySlot.pos[0],
node.emptySlot.pos[1]
])
@@ -111,12 +113,12 @@ class NodeSlotReference {
const pos: [number, number] = await this.node.comfyPage.page.evaluate(
([type, id, index]) => {
// Use canvas.graph to get the current graph (works in both main graph and subgraphs)
const node = window['app'].canvas.graph.getNodeById(id)
const node = window.app!.canvas.graph!.getNodeById(id)
if (!node) throw new Error(`Node ${id} not found.`)
const rawPos = node.getConnectionPos(type === 'input', index)
const convertedPos =
window['app'].canvas.ds.convertOffsetToCanvas(rawPos)
window.app!.canvas.ds!.convertOffsetToCanvas(rawPos)
// Debug logging - convert Float64Arrays to regular arrays for visibility
console.warn(
@@ -126,7 +128,7 @@ class NodeSlotReference {
nodeSize: [node.size[0], node.size[1]],
rawConnectionPos: [rawPos[0], rawPos[1]],
convertedPos: [convertedPos[0], convertedPos[1]],
currentGraphType: window['app'].canvas.graph.constructor.name
currentGraphType: window.app!.canvas.graph!.constructor.name
}
)
@@ -142,7 +144,7 @@ class NodeSlotReference {
async getLinkCount() {
return await this.node.comfyPage.page.evaluate(
([type, id, index]) => {
const node = window['app'].canvas.graph.getNodeById(id)
const node = window.app!.canvas.graph!.getNodeById(id)
if (!node) throw new Error(`Node ${id} not found.`)
if (type === 'input') {
return node.inputs[index].link == null ? 0 : 1
@@ -155,7 +157,7 @@ class NodeSlotReference {
async removeLinks() {
await this.node.comfyPage.page.evaluate(
([type, id, index]) => {
const node = window['app'].canvas.graph.getNodeById(id)
const node = window.app!.canvas.graph!.getNodeById(id)
if (!node) throw new Error(`Node ${id} not found.`)
if (type === 'input') {
node.disconnectInput(index)
@@ -180,15 +182,15 @@ class NodeWidgetReference {
async getPosition(): Promise<Position> {
const pos: [number, number] = await this.node.comfyPage.page.evaluate(
([id, index]) => {
const node = window['app'].canvas.graph.getNodeById(id)
const node = window.app!.canvas.graph!.getNodeById(id)
if (!node) throw new Error(`Node ${id} not found.`)
const widget = node.widgets[index]
const widget = node.widgets![index]
if (!widget) throw new Error(`Widget ${index} not found.`)
const [x, y, w, h] = node.getBounding()
return window['app'].canvasPosToClientPos([
const [x, y, w, _h] = node.getBounding()
return window.app!.canvasPosToClientPos([
x + w / 2,
y + window['LiteGraph']['NODE_TITLE_HEIGHT'] + widget.last_y + 1
y + window.LiteGraph!['NODE_TITLE_HEIGHT'] + widget.last_y! + 1
])
},
[this.node.id, this.index] as const
@@ -205,9 +207,9 @@ class NodeWidgetReference {
async getSocketPosition(): Promise<Position> {
const pos: [number, number] = await this.node.comfyPage.page.evaluate(
([id, index]) => {
const node = window['app'].graph.getNodeById(id)
const node = window.app!.graph!.getNodeById(id)
if (!node) throw new Error(`Node ${id} not found.`)
const widget = node.widgets[index]
const widget = node.widgets![index]
if (!widget) throw new Error(`Widget ${index} not found.`)
const slot = node.inputs.find(
@@ -216,9 +218,9 @@ class NodeWidgetReference {
if (!slot) throw new Error(`Socket ${widget.name} not found.`)
const [x, y] = node.getBounding()
return window['app'].canvasPosToClientPos([
x + slot.pos[0],
y + slot.pos[1] + window['LiteGraph']['NODE_TITLE_HEIGHT']
return window.app!.canvasPosToClientPos([
x + slot.pos![0],
y + slot.pos![1] + window.LiteGraph!['NODE_TITLE_HEIGHT']
])
},
[this.node.id, this.index] as const
@@ -239,7 +241,7 @@ class NodeWidgetReference {
const pos = await this.getPosition()
const canvas = this.node.comfyPage.canvas
const canvasPos = (await canvas.boundingBox())!
await this.node.comfyPage.dragAndDrop(
await this.node.comfyPage.canvasOps.dragAndDrop(
{
x: canvasPos.x + pos.x,
y: canvasPos.y + pos.y
@@ -254,9 +256,9 @@ class NodeWidgetReference {
async getValue() {
return await this.node.comfyPage.page.evaluate(
([id, index]) => {
const node = window['app'].graph.getNodeById(id)
const node = window.app!.graph!.getNodeById(id)
if (!node) throw new Error(`Node ${id} not found.`)
const widget = node.widgets[index]
const widget = node.widgets![index]
if (!widget) throw new Error(`Widget ${index} not found.`)
return widget.value
},
@@ -271,7 +273,7 @@ export class NodeReference {
) {}
async exists(): Promise<boolean> {
return await this.comfyPage.page.evaluate((id) => {
const node = window['app'].canvas.graph.getNodeById(id)
const node = window.app!.canvas.graph!.getNodeById(id)
return !!node
}, this.id)
}
@@ -279,7 +281,7 @@ export class NodeReference {
return this.getProperty('type')
}
async getPosition(): Promise<Position> {
const pos = await this.comfyPage.convertOffsetToCanvas(
const pos = await this.comfyPage.canvasOps.convertOffsetToCanvas(
await this.getProperty<[number, number]>('pos')
)
return {
@@ -288,12 +290,11 @@ export class NodeReference {
}
}
async getBounding(): Promise<Position & Size> {
const [x, y, width, height]: [number, number, number, number] =
await this.comfyPage.page.evaluate((id) => {
const node = window['app'].canvas.graph.getNodeById(id)
if (!node) throw new Error('Node not found')
return node.getBounding()
}, this.id)
const [x, y, width, height] = await this.comfyPage.page.evaluate((id) => {
const node = window.app!.canvas.graph!.getNodeById(id)
if (!node) throw new Error('Node not found')
return [...node.getBounding()] as [number, number, number, number]
}, this.id)
return {
x,
y,
@@ -311,6 +312,11 @@ export class NodeReference {
async getFlags(): Promise<{ collapsed?: boolean; pinned?: boolean }> {
return await this.getProperty('flags')
}
async getTitlePosition(): Promise<Position> {
const nodePos = await this.getPosition()
const nodeSize = await this.getSize()
return { x: nodePos.x + nodeSize.width / 2, y: nodePos.y - 15 }
}
async isPinned() {
return !!(await this.getFlags()).pinned
}
@@ -323,9 +329,9 @@ export class NodeReference {
async getProperty<T>(prop: string): Promise<T> {
return await this.comfyPage.page.evaluate(
([id, prop]) => {
const node = window['app'].canvas.graph.getNodeById(id)
const node = window.app!.canvas.graph!.getNodeById(id)
if (!node) throw new Error('Node not found')
return node[prop]
return (node as unknown as Record<string, T>)[prop]
},
[this.id, prop] as const
)
@@ -343,16 +349,16 @@ export class NodeReference {
position: 'title' | 'collapse',
options?: Parameters<Page['click']>[1] & { moveMouseToEmptyArea?: boolean }
) {
const nodePos = await this.getPosition()
const nodeSize = await this.getSize()
let clickPos: Position
switch (position) {
case 'title':
clickPos = { x: nodePos.x + nodeSize.width / 2, y: nodePos.y - 15 }
clickPos = await this.getTitlePosition()
break
case 'collapse':
case 'collapse': {
const nodePos = await this.getPosition()
clickPos = { x: nodePos.x + 5, y: nodePos.y - 10 }
break
}
default:
throw new Error(`Invalid click position ${position}`)
}
@@ -369,12 +375,12 @@ export class NodeReference {
})
await this.comfyPage.nextFrame()
if (moveMouseToEmptyArea) {
await this.comfyPage.moveMouseToEmptyArea()
await this.comfyPage.canvasOps.moveMouseToEmptyArea()
}
}
async copy() {
await this.click('title')
await this.comfyPage.ctrlC()
await this.comfyPage.clipboard.copy()
await this.comfyPage.nextFrame()
}
async connectWidget(
@@ -384,7 +390,7 @@ export class NodeReference {
) {
const originSlot = await this.getOutput(originSlotIndex)
const targetWidget = await targetNode.getWidget(targetWidgetIndex)
await this.comfyPage.dragAndDrop(
await this.comfyPage.canvasOps.dragAndDrop(
await originSlot.getPosition(),
await targetWidget.getSocketPosition()
)
@@ -397,7 +403,7 @@ export class NodeReference {
) {
const originSlot = await this.getOutput(originSlotIndex)
const targetSlot = await targetNode.getInput(targetSlotIndex)
await this.comfyPage.dragAndDrop(
await this.comfyPage.canvasOps.dragAndDrop(
await originSlot.getPosition(),
await targetSlot.getPosition()
)
@@ -415,9 +421,9 @@ export class NodeReference {
}
async convertToGroupNode(groupNodeName: string = 'GroupNode') {
await this.clickContextMenuOption('Convert to Group Node')
await this.comfyPage.fillPromptDialog(groupNodeName)
await this.comfyPage.nodeOps.fillPromptDialog(groupNodeName)
await this.comfyPage.nextFrame()
const nodes = await this.comfyPage.getNodeRefsByType(
const nodes = await this.comfyPage.nodeOps.getNodeRefsByType(
`workflow>${groupNodeName}`
)
if (nodes.length !== 1) {
@@ -428,7 +434,8 @@ export class NodeReference {
async convertToSubgraph() {
await this.clickContextMenuOption('Convert to Subgraph')
await this.comfyPage.nextFrame()
const nodes = await this.comfyPage.getNodeRefsByTitle('New Subgraph')
const nodes =
await this.comfyPage.nodeOps.getNodeRefsByTitle('New Subgraph')
if (nodes.length !== 1) {
throw new Error(
`Did not find single subgraph node (found=${nodes.length})`
@@ -446,7 +453,7 @@ export class NodeReference {
}
async navigateIntoSubgraph() {
const titleHeight = await this.comfyPage.page.evaluate(() => {
return window['LiteGraph']['NODE_TITLE_HEIGHT']
return window.LiteGraph!['NODE_TITLE_HEIGHT']
})
const nodePos = await this.getPosition()
const nodeSize = await this.getSize()
@@ -458,13 +465,14 @@ export class NodeReference {
{ x: nodePos.x + 20, y: nodePos.y + titleHeight + 5 }
]
let isInSubgraph = false
let attempts = 0
const maxAttempts = 3
while (!isInSubgraph && attempts < maxAttempts) {
attempts++
const checkIsInSubgraph = async () => {
return this.comfyPage.page.evaluate(() => {
const graph = window.app!.canvas.graph
return graph?.constructor?.name === 'Subgraph'
})
}
await expect(async () => {
for (const position of clickPositions) {
// Clear any selection first
await this.comfyPage.canvas.click({
@@ -477,24 +485,9 @@ export class NodeReference {
await this.comfyPage.canvas.dblclick({ position, force: true })
await this.comfyPage.nextFrame()
// Check if we successfully entered the subgraph
isInSubgraph = await this.comfyPage.page.evaluate(() => {
const graph = window['app'].canvas.graph
return graph?.constructor?.name === 'Subgraph'
})
if (isInSubgraph) break
if (await checkIsInSubgraph()) return
}
if (!isInSubgraph && attempts < maxAttempts) {
await this.comfyPage.page.waitForTimeout(500)
}
}
if (!isInSubgraph) {
throw new Error(
'Failed to navigate into subgraph after ' + attempts + ' attempts'
)
}
throw new Error('Not in subgraph yet')
}).toPass({ timeout: 5000, intervals: [100, 200, 500] })
}
}

View File

@@ -1,4 +1,3 @@
import { expect } from '@playwright/test'
import type { Locator } from '@playwright/test'
/** DOM-centric helper for a single Vue-rendered node on the canvas. */
@@ -40,7 +39,7 @@ export class VueNodeFixture {
async setTitle(value: string): Promise<void> {
await this.header.dblclick()
const input = this.titleInput
await expect(input).toBeVisible()
await input.waitFor({ state: 'visible' })
await input.fill(value)
await input.press('Enter')
}
@@ -48,7 +47,7 @@ export class VueNodeFixture {
async cancelTitleEdit(): Promise<void> {
await this.header.dblclick()
const input = this.titleInput
await expect(input).toBeVisible()
await input.waitFor({ state: 'visible' })
await input.press('Escape')
}

View File

@@ -1,7 +1,7 @@
import { test as base } from '@playwright/test'
export const webSocketFixture = base.extend<{
ws: { trigger(data: any, url?: string): Promise<void> }
ws: { trigger(data: unknown, url?: string): Promise<void> }
}>({
ws: [
async ({ page }, use) => {
@@ -10,7 +10,7 @@ export const webSocketFixture = base.extend<{
await page.evaluate(function () {
// Create a wrapper for WebSocket that stores them globally
// so we can look it up to trigger messages
const store: Record<string, WebSocket> = ((window as any).__ws__ = {})
const store: Record<string, WebSocket> = (window.__ws__ = {})
window.WebSocket = class extends window.WebSocket {
constructor(
...rest: ConstructorParameters<typeof window.WebSocket>
@@ -34,7 +34,7 @@ export const webSocketFixture = base.extend<{
u.pathname = '/'
url = u.toString() + 'ws'
}
const ws: WebSocket = (window as any).__ws__[url]
const ws: WebSocket = window.__ws__![url]
ws.dispatchEvent(
new MessageEvent('message', {
data

View File

@@ -5,7 +5,7 @@ import { backupPath } from './utils/backupUtils'
dotenv.config()
export default function globalSetup(config: FullConfig) {
export default function globalSetup(_config: FullConfig) {
if (!process.env.CI) {
if (process.env.TEST_COMFYUI_DIR) {
backupPath([process.env.TEST_COMFYUI_DIR, 'user'])

View File

@@ -5,7 +5,7 @@ import { restorePath } from './utils/backupUtils'
dotenv.config()
export default function globalTeardown(config: FullConfig) {
export default function globalTeardown(_config: FullConfig) {
if (!process.env.CI && process.env.TEST_COMFYUI_DIR) {
restorePath([process.env.TEST_COMFYUI_DIR, 'user'])
restorePath([process.env.TEST_COMFYUI_DIR, 'models'])

View File

@@ -1,6 +1,8 @@
import type { Locator, Page } from '@playwright/test'
import type { AutoQueueMode } from '../../src/stores/queueStore'
import { TestIds } from '../fixtures/selectors'
import type { WorkspaceStore } from '../types/globals'
export class ComfyActionbar {
public readonly root: Locator
@@ -26,7 +28,7 @@ class ComfyQueueButton {
public readonly primaryButton: Locator
public readonly dropdownButton: Locator
constructor(public readonly actionbar: ComfyActionbar) {
this.root = actionbar.root.getByTestId('queue-button')
this.root = actionbar.root.getByTestId(TestIds.topbar.queueButton)
this.primaryButton = this.root.locator('.p-splitbutton-button')
this.dropdownButton = this.root.locator('.p-splitbutton-dropdown')
}
@@ -42,13 +44,14 @@ class ComfyQueueButtonOptions {
public async setMode(mode: AutoQueueMode) {
await this.page.evaluate((mode) => {
window['app'].extensionManager.queueSettings.mode = mode
;(window.app!.extensionManager as WorkspaceStore).queueSettings.mode =
mode
}, mode)
}
public async getMode() {
return await this.page.evaluate(() => {
return window['app'].extensionManager.queueSettings.mode
return (window.app!.extensionManager as WorkspaceStore).queueSettings.mode
})
}
}

View File

@@ -23,7 +23,7 @@ export async function fitToViewInstant(
{ selectionOnly: boolean }
>(
({ selectionOnly }) => {
const app = window['app']
const app = window.app
if (!app?.canvas) return null
const canvas = app.canvas
@@ -90,7 +90,7 @@ export async function fitToViewInstant(
await comfyPage.page.evaluate(
({ bounds, zoom }) => {
const app = window['app']
const app = window.app
if (!app?.canvas) return
const canvas = app.canvas

View File

@@ -0,0 +1,16 @@
import type { LGraph, Subgraph } from '../../src/lib/litegraph/src/litegraph'
import { isSubgraph } from '../../src/utils/typeGuardUtil'
/**
* Assertion helper for tests where being in a subgraph is a precondition.
* Throws a clear error if the graph is not a Subgraph.
*/
export function assertSubgraph(
graph: LGraph | Subgraph | null | undefined
): asserts graph is Subgraph {
if (!isSubgraph(graph)) {
throw new Error(
'Expected to be in a subgraph context, but graph is not a Subgraph'
)
}
}

View File

@@ -6,18 +6,19 @@ import type {
TemplateInfo,
WorkflowTemplates
} from '../../src/platform/workflow/templates/types/template'
import { TestIds } from '../fixtures/selectors'
export class ComfyTemplates {
readonly content: Locator
readonly allTemplateCards: Locator
constructor(readonly page: Page) {
this.content = page.getByTestId('template-workflows-content')
this.content = page.getByTestId(TestIds.templates.content)
this.allTemplateCards = page.locator('[data-testid^="template-workflow-"]')
}
async waitForMinimumCardCount(count: number) {
return await expect(async () => {
async expectMinimumCardCount(count: number) {
await expect(async () => {
const cardCount = await this.allTemplateCards.count()
expect(cardCount).toBeGreaterThanOrEqual(count)
}).toPass({
@@ -26,14 +27,16 @@ export class ComfyTemplates {
}
async loadTemplate(id: string) {
const templateCard = this.content.getByTestId(`template-workflow-${id}`)
const templateCard = this.content.getByTestId(
TestIds.templates.workflowCard(id)
)
await templateCard.scrollIntoViewIfNeeded()
await templateCard.getByRole('img').click()
}
async getAllTemplates(): Promise<TemplateInfo[]> {
const templates: WorkflowTemplates[] = await this.page.evaluate(() =>
window['app'].api.getCoreWorkflowTemplates()
window.app!.api.getCoreWorkflowTemplates()
)
return templates.flatMap((t) => t.templates)
}

View File

@@ -1,15 +1,16 @@
import type { Response } from '@playwright/test'
import { expect, mergeTests } from '@playwright/test'
import type { StatusWsMessage } from '../../src/schemas/apiSchema.ts'
import { comfyPageFixture } from '../fixtures/ComfyPage.ts'
import { webSocketFixture } from '../fixtures/ws.ts'
import type { StatusWsMessage } from '../../src/schemas/apiSchema'
import { comfyPageFixture } from '../fixtures/ComfyPage'
import { webSocketFixture } from '../fixtures/ws'
import type { WorkspaceStore } from '../types/globals'
const test = mergeTests(comfyPageFixture, webSocketFixture)
test.describe('Actionbar', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
})
/**
@@ -49,13 +50,14 @@ test.describe('Actionbar', { tag: '@ui' }, () => {
// Find and set the width on the latent node
const triggerChange = async (value: number) => {
return await comfyPage.page.evaluate((value) => {
const node = window['app'].graph._nodes.find(
const node = window.app!.graph!._nodes.find(
(n) => n.type === 'EmptyLatentImage'
)
node.widgets[0].value = value
window[
'app'
].extensionManager.workflow.activeWorkflow.changeTracker.checkState()
node!.widgets![0].value = value
;(
window.app!.extensionManager as WorkspaceStore
).workflow.activeWorkflow?.changeTracker.checkState()
}, value)
}

View File

@@ -3,18 +3,18 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Background Image Upload', () => {
test.beforeEach(async ({ comfyPage }) => {
// Reset the background image setting before each test
await comfyPage.setSetting('Comfy.Canvas.BackgroundImage', '')
await comfyPage.settings.setSetting('Comfy.Canvas.BackgroundImage', '')
})
test.afterEach(async ({ comfyPage }) => {
// Clean up background image setting after each test
await comfyPage.setSetting('Comfy.Canvas.BackgroundImage', '')
await comfyPage.settings.setSetting('Comfy.Canvas.BackgroundImage', '')
})
test('should show background image upload component in settings', async ({
@@ -34,16 +34,18 @@ test.describe('Background Image Upload', () => {
await expect(backgroundImageSetting).toBeVisible()
// Verify the component has the expected elements using semantic selectors
const urlInput = backgroundImageSetting.locator('input[type="text"]')
const urlInput = backgroundImageSetting.getByRole('textbox')
await expect(urlInput).toBeVisible()
await expect(urlInput).toHaveAttribute('placeholder')
const uploadButton = backgroundImageSetting.locator(
'button:has(.pi-upload)'
)
const uploadButton = backgroundImageSetting.getByRole('button', {
name: /upload/i
})
await expect(uploadButton).toBeVisible()
const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)')
const clearButton = backgroundImageSetting.getByRole('button', {
name: /clear/i
})
await expect(clearButton).toBeVisible()
await expect(clearButton).toBeDisabled() // Should be disabled when no image
})
@@ -63,9 +65,9 @@ test.describe('Background Image Upload', () => {
'#Comfy\\.Canvas\\.BackgroundImage'
)
// Click the upload button to trigger file input
const uploadButton = backgroundImageSetting.locator(
'button:has(.pi-upload)'
)
const uploadButton = backgroundImageSetting.getByRole('button', {
name: /upload/i
})
// Set up file upload handler
const fileChooserPromise = comfyPage.page.waitForEvent('filechooser')
@@ -76,15 +78,17 @@ test.describe('Background Image Upload', () => {
await fileChooser.setFiles(comfyPage.assetPath('image32x32.webp'))
// Verify the URL input now has an API URL
const urlInput = backgroundImageSetting.locator('input[type="text"]')
const urlInput = backgroundImageSetting.getByRole('textbox')
await expect(urlInput).toHaveValue(/^\/api\/view\?.*subfolder=backgrounds/)
// Verify clear button is now enabled
const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)')
const clearButton = backgroundImageSetting.getByRole('button', {
name: /clear/i
})
await expect(clearButton).toBeEnabled()
// Verify the setting value was actually set
const settingValue = await comfyPage.getSetting(
const settingValue = await comfyPage.settings.getSetting(
'Comfy.Canvas.BackgroundImage'
)
expect(settingValue).toMatch(/^\/api\/view\?.*subfolder=backgrounds/)
@@ -107,18 +111,20 @@ test.describe('Background Image Upload', () => {
'#Comfy\\.Canvas\\.BackgroundImage'
)
// Enter URL in the input field
const urlInput = backgroundImageSetting.locator('input[type="text"]')
const urlInput = backgroundImageSetting.getByRole('textbox')
await urlInput.fill(testImageUrl)
// Trigger blur event to ensure the value is set
await urlInput.blur()
// Verify clear button is now enabled
const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)')
const clearButton = backgroundImageSetting.getByRole('button', {
name: /clear/i
})
await expect(clearButton).toBeEnabled()
// Verify the setting value was updated
const settingValue = await comfyPage.getSetting(
const settingValue = await comfyPage.settings.getSetting(
'Comfy.Canvas.BackgroundImage'
)
expect(settingValue).toBe(testImageUrl)
@@ -130,7 +136,10 @@ test.describe('Background Image Upload', () => {
const testImageUrl = 'https://example.com/test-image.png'
// First set a background image
await comfyPage.setSetting('Comfy.Canvas.BackgroundImage', testImageUrl)
await comfyPage.settings.setSetting(
'Comfy.Canvas.BackgroundImage',
testImageUrl
)
// Open settings dialog
await comfyPage.page.keyboard.press('Control+,')
@@ -144,11 +153,13 @@ test.describe('Background Image Upload', () => {
'#Comfy\\.Canvas\\.BackgroundImage'
)
// Verify the input has the test URL
const urlInput = backgroundImageSetting.locator('input[type="text"]')
const urlInput = backgroundImageSetting.getByRole('textbox')
await expect(urlInput).toHaveValue(testImageUrl)
// Verify clear button is enabled
const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)')
const clearButton = backgroundImageSetting.getByRole('button', {
name: /clear/i
})
await expect(clearButton).toBeEnabled()
// Click the clear button
@@ -161,7 +172,7 @@ test.describe('Background Image Upload', () => {
await expect(clearButton).toBeDisabled()
// Verify the setting value was cleared
const settingValue = await comfyPage.getSetting(
const settingValue = await comfyPage.settings.getSetting(
'Comfy.Canvas.BackgroundImage'
)
expect(settingValue).toBe('')
@@ -182,9 +193,9 @@ test.describe('Background Image Upload', () => {
'#Comfy\\.Canvas\\.BackgroundImage'
)
// Hover over upload button and verify tooltip appears
const uploadButton = backgroundImageSetting.locator(
'button:has(.pi-upload)'
)
const uploadButton = backgroundImageSetting.getByRole('button', {
name: /upload/i
})
await uploadButton.hover()
const uploadTooltip = comfyPage.page.locator('.p-tooltip:visible')
@@ -194,12 +205,14 @@ test.describe('Background Image Upload', () => {
await comfyPage.page.locator('body').hover()
// Set a background to enable clear button
const urlInput = backgroundImageSetting.locator('input[type="text"]')
const urlInput = backgroundImageSetting.getByRole('textbox')
await urlInput.fill('https://example.com/test.png')
await urlInput.blur()
// Hover over clear button and verify tooltip appears
const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)')
const clearButton = backgroundImageSetting.getByRole('button', {
name: /clear/i
})
await clearButton.hover()
const clearTooltip = comfyPage.page.locator('.p-tooltip:visible')
@@ -220,8 +233,10 @@ test.describe('Background Image Upload', () => {
const backgroundImageSetting = comfyPage.page.locator(
'#Comfy\\.Canvas\\.BackgroundImage'
)
const urlInput = backgroundImageSetting.locator('input[type="text"]')
const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)')
const urlInput = backgroundImageSetting.getByRole('textbox')
const clearButton = backgroundImageSetting.getByRole('button', {
name: /clear/i
})
// Initially clear button should be disabled
await expect(clearButton).toBeDisabled()

View File

@@ -4,53 +4,33 @@ import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
})
test('should toggle shortcuts panel visibility', async ({ comfyPage }) => {
// Initially shortcuts panel should be hidden
await expect(comfyPage.page.locator('.bottom-panel')).not.toBeVisible()
const { bottomPanel } = comfyPage
// Click shortcuts toggle button in sidebar
await comfyPage.page
.locator('button[aria-label*="Keyboard Shortcuts"]')
.click()
// Shortcuts panel should now be visible
await expect(comfyPage.page.locator('.bottom-panel')).toBeVisible()
// Click toggle button again to hide
await comfyPage.page
.locator('button[aria-label*="Keyboard Shortcuts"]')
.click()
// Panel should be hidden again
await expect(comfyPage.page.locator('.bottom-panel')).not.toBeVisible()
await expect(bottomPanel.root).not.toBeVisible()
await bottomPanel.keyboardShortcutsButton.click()
await expect(bottomPanel.root).toBeVisible()
await bottomPanel.keyboardShortcutsButton.click()
await expect(bottomPanel.root).not.toBeVisible()
})
test('should display essentials shortcuts tab', async ({ comfyPage }) => {
// Open shortcuts panel
await comfyPage.page
.locator('button[aria-label*="Keyboard Shortcuts"]')
.click()
const { bottomPanel } = comfyPage
// Essentials tab should be visible and active by default
await expect(
comfyPage.page.getByRole('tab', { name: /Essential/i })
).toBeVisible()
await expect(
comfyPage.page.getByRole('tab', { name: /Essential/i })
).toHaveAttribute('aria-selected', 'true')
await bottomPanel.keyboardShortcutsButton.click()
// Should display shortcut categories
await expect(
comfyPage.page.locator('.subcategory-title').first()
).toBeVisible()
await expect(bottomPanel.shortcuts.essentialsTab).toBeVisible()
await expect(bottomPanel.shortcuts.essentialsTab).toHaveAttribute(
'aria-selected',
'true'
)
// Should display some keyboard shortcuts
await expect(comfyPage.page.locator('.key-badge').first()).toBeVisible()
await expect(bottomPanel.shortcuts.subcategoryTitles.first()).toBeVisible()
await expect(bottomPanel.shortcuts.keyBadges.first()).toBeVisible()
// Should have workflow, node, and queue sections
await expect(
comfyPage.page.getByRole('heading', { name: 'Workflow' })
).toBeVisible()
@@ -63,23 +43,18 @@ test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => {
})
test('should display view controls shortcuts tab', async ({ comfyPage }) => {
// Open shortcuts panel
await comfyPage.page
.locator('button[aria-label*="Keyboard Shortcuts"]')
.click()
const { bottomPanel } = comfyPage
// Click view controls tab
await comfyPage.page.getByRole('tab', { name: /View Controls/i }).click()
await bottomPanel.keyboardShortcutsButton.click()
await bottomPanel.shortcuts.viewControlsTab.click()
// View controls tab should be active
await expect(
comfyPage.page.getByRole('tab', { name: /View Controls/i })
).toHaveAttribute('aria-selected', 'true')
await expect(bottomPanel.shortcuts.viewControlsTab).toHaveAttribute(
'aria-selected',
'true'
)
// Should display view controls shortcuts
await expect(comfyPage.page.locator('.key-badge').first()).toBeVisible()
await expect(bottomPanel.shortcuts.keyBadges.first()).toBeVisible()
// Should have view and panel controls sections
await expect(
comfyPage.page.getByRole('heading', { name: 'View' })
).toBeVisible()
@@ -89,54 +64,48 @@ test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => {
})
test('should switch between shortcuts tabs', async ({ comfyPage }) => {
// Open shortcuts panel
await comfyPage.page
.locator('button[aria-label*="Keyboard Shortcuts"]')
.click()
const { bottomPanel } = comfyPage
// Essentials should be active initially
await expect(
comfyPage.page.getByRole('tab', { name: /Essential/i })
).toHaveAttribute('aria-selected', 'true')
await bottomPanel.keyboardShortcutsButton.click()
// Click view controls tab
await comfyPage.page.getByRole('tab', { name: /View Controls/i }).click()
await expect(bottomPanel.shortcuts.essentialsTab).toHaveAttribute(
'aria-selected',
'true'
)
// View controls should now be active
await expect(
comfyPage.page.getByRole('tab', { name: /View Controls/i })
).toHaveAttribute('aria-selected', 'true')
await expect(
comfyPage.page.getByRole('tab', { name: /Essential/i })
).not.toHaveAttribute('aria-selected', 'true')
await bottomPanel.shortcuts.viewControlsTab.click()
// Switch back to essentials
await comfyPage.page.getByRole('tab', { name: /Essential/i }).click()
await expect(bottomPanel.shortcuts.viewControlsTab).toHaveAttribute(
'aria-selected',
'true'
)
await expect(bottomPanel.shortcuts.essentialsTab).not.toHaveAttribute(
'aria-selected',
'true'
)
// Essentials should be active again
await expect(
comfyPage.page.getByRole('tab', { name: /Essential/i })
).toHaveAttribute('aria-selected', 'true')
await expect(
comfyPage.page.getByRole('tab', { name: /View Controls/i })
).not.toHaveAttribute('aria-selected', 'true')
await bottomPanel.shortcuts.essentialsTab.click()
await expect(bottomPanel.shortcuts.essentialsTab).toHaveAttribute(
'aria-selected',
'true'
)
await expect(bottomPanel.shortcuts.viewControlsTab).not.toHaveAttribute(
'aria-selected',
'true'
)
})
test('should display formatted keyboard shortcuts', async ({ comfyPage }) => {
// Open shortcuts panel
await comfyPage.page
.locator('button[aria-label*="Keyboard Shortcuts"]')
.click()
const { bottomPanel } = comfyPage
// Wait for shortcuts to load
await comfyPage.page.waitForSelector('.key-badge')
await bottomPanel.keyboardShortcutsButton.click()
// Check for common formatted keys
const keyBadges = comfyPage.page.locator('.key-badge')
const keyBadges = bottomPanel.shortcuts.keyBadges
await keyBadges.first().waitFor({ state: 'visible' })
const count = await keyBadges.count()
expect(count).toBeGreaterThanOrEqual(1)
// Should show formatted modifier keys
const badgeText = await keyBadges.allTextContents()
const hasModifiers = badgeText.some((text) =>
['Ctrl', 'Cmd', 'Shift', 'Alt'].includes(text)
@@ -147,20 +116,18 @@ test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => {
test('should maintain panel state when switching between panels', async ({
comfyPage
}) => {
const { bottomPanel } = comfyPage
// Open shortcuts panel first
await comfyPage.page
.locator('button[aria-label*="Keyboard Shortcuts"]')
.click()
await expect(comfyPage.page.locator('.bottom-panel')).toBeVisible()
await bottomPanel.keyboardShortcutsButton.click()
await expect(bottomPanel.root).toBeVisible()
await expect(
comfyPage.page.locator('[id*="tab_shortcuts-essentials"]')
).toBeVisible()
// Try to open terminal panel - may show terminal OR close shortcuts
// depending on whether terminal tabs have loaded (async loading)
await comfyPage.page
.locator('button[aria-label*="Toggle Bottom Panel"]')
.click()
await bottomPanel.toggleButton.click()
// Check if terminal tabs loaded (Logs tab visible) or fell back to shortcuts toggle
const logsTab = comfyPage.page.getByRole('tab', { name: /Logs/i })
@@ -168,12 +135,10 @@ test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => {
if (hasTerminalTabs) {
// Terminal panel is visible - verify we can switch back to shortcuts
await expect(comfyPage.page.locator('.bottom-panel')).toBeVisible()
await expect(bottomPanel.root).toBeVisible()
// Switch back to shortcuts
await comfyPage.page
.locator('button[aria-label*="Keyboard Shortcuts"]')
.click()
await bottomPanel.keyboardShortcutsButton.click()
// Should show shortcuts content again
await expect(
@@ -181,10 +146,8 @@ test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => {
).toBeVisible()
} else {
// Terminal tabs not loaded - button toggled shortcuts off, reopen for verification
await comfyPage.page
.locator('button[aria-label*="Keyboard Shortcuts"]')
.click()
await expect(comfyPage.page.locator('.bottom-panel')).toBeVisible()
await bottomPanel.keyboardShortcutsButton.click()
await expect(bottomPanel.root).toBeVisible()
await expect(
comfyPage.page.locator('[id*="tab_shortcuts-essentials"]')
).toBeVisible()
@@ -192,62 +155,47 @@ test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => {
})
test('should handle keyboard navigation', async ({ comfyPage }) => {
// Open shortcuts panel
await comfyPage.page
.locator('button[aria-label*="Keyboard Shortcuts"]')
.click()
const { bottomPanel } = comfyPage
// Focus the first tab
await comfyPage.page.getByRole('tab', { name: /Essential/i }).focus()
await bottomPanel.keyboardShortcutsButton.click()
await bottomPanel.shortcuts.essentialsTab.focus()
// Use arrow keys to navigate between tabs
await comfyPage.page.keyboard.press('ArrowRight')
// View controls tab should now have focus
await expect(
comfyPage.page.getByRole('tab', { name: /View Controls/i })
).toBeFocused()
await expect(bottomPanel.shortcuts.viewControlsTab).toBeFocused()
// Press Enter to activate the tab
await comfyPage.page.keyboard.press('Enter')
// Tab should be selected
await expect(
comfyPage.page.getByRole('tab', { name: /View Controls/i })
).toHaveAttribute('aria-selected', 'true')
await expect(bottomPanel.shortcuts.viewControlsTab).toHaveAttribute(
'aria-selected',
'true'
)
})
test('should close panel by clicking shortcuts button again', async ({
comfyPage
}) => {
// Open shortcuts panel
await comfyPage.page
.locator('button[aria-label*="Keyboard Shortcuts"]')
.click()
await expect(comfyPage.page.locator('.bottom-panel')).toBeVisible()
const { bottomPanel } = comfyPage
// Click shortcuts button again to close
await comfyPage.page
.locator('button[aria-label*="Keyboard Shortcuts"]')
.click()
await bottomPanel.keyboardShortcutsButton.click()
await expect(bottomPanel.root).toBeVisible()
// Panel should be hidden
await expect(comfyPage.page.locator('.bottom-panel')).not.toBeVisible()
await bottomPanel.keyboardShortcutsButton.click()
await expect(bottomPanel.root).not.toBeVisible()
})
test('should display shortcuts in organized columns', async ({
comfyPage
}) => {
// Open shortcuts panel
await comfyPage.page
.locator('button[aria-label*="Keyboard Shortcuts"]')
.click()
const { bottomPanel } = comfyPage
// Should have 3-column grid layout
await expect(comfyPage.page.locator('.md\\:grid-cols-3')).toBeVisible()
await bottomPanel.keyboardShortcutsButton.click()
// Should have multiple subcategory sections
const subcategoryTitles = comfyPage.page.locator('.subcategory-title')
await expect(
comfyPage.page.locator('[data-testid="shortcuts-columns"]')
).toBeVisible()
const subcategoryTitles = bottomPanel.shortcuts.subcategoryTitles
const titleCount = await subcategoryTitles.count()
expect(titleCount).toBeGreaterThanOrEqual(2)
})
@@ -255,43 +203,30 @@ test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => {
test('should open shortcuts panel with Ctrl+Shift+K', async ({
comfyPage
}) => {
// Initially shortcuts panel should be hidden
await expect(comfyPage.page.locator('.bottom-panel')).not.toBeVisible()
const { bottomPanel } = comfyPage
await expect(bottomPanel.root).not.toBeVisible()
// Press Ctrl+Shift+K to open shortcuts panel
await comfyPage.page.keyboard.press('Control+Shift+KeyK')
// Shortcuts panel should now be visible
await expect(comfyPage.page.locator('.bottom-panel')).toBeVisible()
// Should show essentials tab by default
await expect(
comfyPage.page.getByRole('tab', { name: /Essential/i })
).toHaveAttribute('aria-selected', 'true')
await expect(bottomPanel.root).toBeVisible()
await expect(bottomPanel.shortcuts.essentialsTab).toHaveAttribute(
'aria-selected',
'true'
)
})
test('should open settings dialog when clicking manage shortcuts button', async ({
comfyPage
}) => {
// Open shortcuts panel
await comfyPage.page
.locator('button[aria-label*="Keyboard Shortcuts"]')
.click()
const { bottomPanel } = comfyPage
// Manage shortcuts button should be visible
await expect(
comfyPage.page.getByRole('button', { name: /Manage Shortcuts/i })
).toBeVisible()
await bottomPanel.keyboardShortcutsButton.click()
// Click manage shortcuts button
await comfyPage.page
.getByRole('button', { name: /Manage Shortcuts/i })
.click()
await expect(bottomPanel.shortcuts.manageButton).toBeVisible()
await bottomPanel.shortcuts.manageButton.click()
// Settings dialog should open with keybinding tab
await expect(comfyPage.page.getByRole('dialog')).toBeVisible()
// Should show keybinding settings (check for keybinding-related content)
await expect(
comfyPage.page.getByRole('option', { name: 'Keybinding' })
).toBeVisible()

View File

@@ -1,16 +1,18 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import type { WorkspaceStore } from '../types/globals'
test.describe('Browser tab title', { tag: '@smoke' }, () => {
test.describe('Beta Menu', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
})
test('Can display workflow name', async ({ comfyPage }) => {
const workflowName = await comfyPage.page.evaluate(async () => {
return window['app'].extensionManager.workflow.activeWorkflow.filename
return (window.app!.extensionManager as WorkspaceStore).workflow
.activeWorkflow?.filename
})
expect(await comfyPage.page.title()).toBe(`*${workflowName} - ComfyUI`)
})
@@ -21,7 +23,8 @@ test.describe('Browser tab title', { tag: '@smoke' }, () => {
comfyPage
}) => {
const workflowName = await comfyPage.page.evaluate(async () => {
return window['app'].extensionManager.workflow.activeWorkflow.filename
return (window.app!.extensionManager as WorkspaceStore).workflow
.activeWorkflow?.filename
})
expect(await comfyPage.page.title()).toBe(`${workflowName} - ComfyUI`)
@@ -30,19 +33,21 @@ test.describe('Browser tab title', { tag: '@smoke' }, () => {
const textBox = comfyPage.widgetTextBox
await textBox.fill('Hello World')
await comfyPage.clickEmptySpace()
await comfyPage.canvasOps.clickEmptySpace()
expect(await comfyPage.page.title()).toBe(`*test - ComfyUI`)
// Delete the saved workflow for cleanup.
await comfyPage.page.evaluate(async () => {
return window['app'].extensionManager.workflow.activeWorkflow.delete()
return (
window.app!.extensionManager as WorkspaceStore
).workflow.activeWorkflow?.delete()
})
})
})
test.describe('Legacy Menu', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test('Can display default title', async ({ comfyPage }) => {

View File

@@ -6,69 +6,69 @@ import {
async function beforeChange(comfyPage: ComfyPage) {
await comfyPage.page.evaluate(() => {
window['app'].canvas.emitBeforeChange()
window.app!.canvas!.emitBeforeChange()
})
}
async function afterChange(comfyPage: ComfyPage) {
await comfyPage.page.evaluate(() => {
window['app'].canvas.emitAfterChange()
window.app!.canvas!.emitAfterChange()
})
}
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Change Tracker', { tag: '@workflow' }, () => {
test.describe('Undo/Redo', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setupWorkflowsDirectory({})
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.workflow.setupWorkflowsDirectory({})
})
test('Can undo multiple operations', async ({ comfyPage }) => {
expect(await comfyPage.getUndoQueueSize()).toBe(0)
expect(await comfyPage.getRedoQueueSize()).toBe(0)
expect(await comfyPage.workflow.getUndoQueueSize()).toBe(0)
expect(await comfyPage.workflow.getRedoQueueSize()).toBe(0)
// Save, confirm no errors & workflow modified flag removed
await comfyPage.menu.topbar.saveWorkflow('undo-redo-test')
expect(await comfyPage.getToastErrorCount()).toBe(0)
expect(await comfyPage.isCurrentWorkflowModified()).toBe(false)
expect(await comfyPage.getUndoQueueSize()).toBe(0)
expect(await comfyPage.getRedoQueueSize()).toBe(0)
expect(await comfyPage.toast.getToastErrorCount()).toBe(0)
expect(await comfyPage.workflow.isCurrentWorkflowModified()).toBe(false)
expect(await comfyPage.workflow.getUndoQueueSize()).toBe(0)
expect(await comfyPage.workflow.getRedoQueueSize()).toBe(0)
const node = (await comfyPage.getFirstNodeRef())!
const node = (await comfyPage.nodeOps.getFirstNodeRef())!
await node.click('title')
await node.click('collapse')
await expect(node).toBeCollapsed()
expect(await comfyPage.isCurrentWorkflowModified()).toBe(true)
expect(await comfyPage.getUndoQueueSize()).toBe(1)
expect(await comfyPage.getRedoQueueSize()).toBe(0)
expect(await comfyPage.workflow.isCurrentWorkflowModified()).toBe(true)
expect(await comfyPage.workflow.getUndoQueueSize()).toBe(1)
expect(await comfyPage.workflow.getRedoQueueSize()).toBe(0)
await comfyPage.ctrlB()
await comfyPage.keyboard.bypass()
await expect(node).toBeBypassed()
expect(await comfyPage.isCurrentWorkflowModified()).toBe(true)
expect(await comfyPage.getUndoQueueSize()).toBe(2)
expect(await comfyPage.getRedoQueueSize()).toBe(0)
expect(await comfyPage.workflow.isCurrentWorkflowModified()).toBe(true)
expect(await comfyPage.workflow.getUndoQueueSize()).toBe(2)
expect(await comfyPage.workflow.getRedoQueueSize()).toBe(0)
await comfyPage.ctrlZ()
await comfyPage.keyboard.undo()
await expect(node).not.toBeBypassed()
expect(await comfyPage.isCurrentWorkflowModified()).toBe(true)
expect(await comfyPage.getUndoQueueSize()).toBe(1)
expect(await comfyPage.getRedoQueueSize()).toBe(1)
expect(await comfyPage.workflow.isCurrentWorkflowModified()).toBe(true)
expect(await comfyPage.workflow.getUndoQueueSize()).toBe(1)
expect(await comfyPage.workflow.getRedoQueueSize()).toBe(1)
await comfyPage.ctrlZ()
await comfyPage.keyboard.undo()
await expect(node).not.toBeCollapsed()
expect(await comfyPage.isCurrentWorkflowModified()).toBe(false)
expect(await comfyPage.getUndoQueueSize()).toBe(0)
expect(await comfyPage.getRedoQueueSize()).toBe(2)
expect(await comfyPage.workflow.isCurrentWorkflowModified()).toBe(false)
expect(await comfyPage.workflow.getUndoQueueSize()).toBe(0)
expect(await comfyPage.workflow.getRedoQueueSize()).toBe(2)
})
})
test('Can group multiple change actions into a single transaction', async ({
comfyPage
}) => {
const node = (await comfyPage.getFirstNodeRef())!
const node = (await comfyPage.nodeOps.getFirstNodeRef())!
expect(node).toBeTruthy()
await expect(node).not.toBeCollapsed()
await expect(node).not.toBeBypassed()
@@ -77,27 +77,27 @@ test.describe('Change Tracker', { tag: '@workflow' }, () => {
// Bypass + collapse node
await node.click('title')
await node.click('collapse')
await comfyPage.ctrlB()
await comfyPage.keyboard.bypass()
await expect(node).toBeCollapsed()
await expect(node).toBeBypassed()
// Undo, undo, ensure both changes undone
await comfyPage.ctrlZ()
await comfyPage.keyboard.undo()
await expect(node).not.toBeBypassed()
await expect(node).toBeCollapsed()
await comfyPage.ctrlZ()
await comfyPage.keyboard.undo()
await expect(node).not.toBeBypassed()
await expect(node).not.toBeCollapsed()
// Prevent clicks registering a double-click
await comfyPage.clickEmptySpace()
await comfyPage.canvasOps.clickEmptySpace()
await node.click('title')
// Run again, but within a change transaction
await beforeChange(comfyPage)
await node.click('collapse')
await comfyPage.ctrlB()
await comfyPage.keyboard.bypass()
await expect(node).toBeCollapsed()
await expect(node).toBeBypassed()
@@ -105,7 +105,7 @@ test.describe('Change Tracker', { tag: '@workflow' }, () => {
await afterChange(comfyPage)
// Ensure undo reverts both changes
await comfyPage.ctrlZ()
await comfyPage.keyboard.undo()
await expect(node).not.toBeBypassed()
await expect(node).not.toBeCollapsed()
})
@@ -113,10 +113,10 @@ test.describe('Change Tracker', { tag: '@workflow' }, () => {
test('Can nest multiple change transactions without adding undo steps', async ({
comfyPage
}) => {
const node = (await comfyPage.getFirstNodeRef())!
const node = (await comfyPage.nodeOps.getFirstNodeRef())!
const bypassAndPin = async () => {
await beforeChange(comfyPage)
await comfyPage.ctrlB()
await comfyPage.keyboard.bypass()
await expect(node).toBeBypassed()
await comfyPage.page.keyboard.press('KeyP')
await comfyPage.nextFrame()
@@ -142,30 +142,30 @@ test.describe('Change Tracker', { tag: '@workflow' }, () => {
await multipleChanges()
await comfyPage.ctrlZ()
await comfyPage.keyboard.undo()
await expect(node).not.toBeBypassed()
await expect(node).not.toBePinned()
await expect(node).not.toBeCollapsed()
await comfyPage.ctrlY()
await comfyPage.keyboard.redo()
await expect(node).toBeBypassed()
await expect(node).toBePinned()
await expect(node).toBeCollapsed()
})
test('Can detect changes in workflow.extra', async ({ comfyPage }) => {
expect(await comfyPage.getUndoQueueSize()).toBe(0)
expect(await comfyPage.workflow.getUndoQueueSize()).toBe(0)
await comfyPage.page.evaluate(() => {
window['app'].graph.extra.foo = 'bar'
window.app!.graph!.extra.foo = 'bar'
})
// Click empty space to trigger a change detection.
await comfyPage.clickEmptySpace()
expect(await comfyPage.getUndoQueueSize()).toBe(1)
await comfyPage.canvasOps.clickEmptySpace()
expect(await comfyPage.workflow.getUndoQueueSize()).toBe(1)
})
test('Ignores changes in workflow.ds', async ({ comfyPage }) => {
expect(await comfyPage.getUndoQueueSize()).toBe(0)
await comfyPage.pan({ x: 10, y: 10 })
expect(await comfyPage.getUndoQueueSize()).toBe(0)
expect(await comfyPage.workflow.getUndoQueueSize()).toBe(0)
await comfyPage.canvasOps.pan({ x: 10, y: 10 })
expect(await comfyPage.workflow.getUndoQueueSize()).toBe(0)
})
})

View File

@@ -1,13 +1,13 @@
import { expect } from '@playwright/test'
import type { Palette } from '../../src/schemas/colorPaletteSchema'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import type { WorkspaceStore } from '../types/globals'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
const customColorPalettes: Record<string, Palette> = {
const customColorPalettes = {
obsidian: {
version: 102,
id: 'obsidian',
@@ -153,40 +153,48 @@ const customColorPalettes: Record<string, Palette> = {
test.describe('Color Palette', { tag: ['@screenshot', '@settings'] }, () => {
test('Can show custom color palette', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.CustomColorPalettes', customColorPalettes)
await comfyPage.settings.setSetting(
'Comfy.CustomColorPalettes',
customColorPalettes
)
// Reload to apply the new setting. Setting Comfy.CustomColorPalettes directly
// doesn't update the store immediately.
await comfyPage.setup()
await comfyPage.loadWorkflow('nodes/every_node_color')
await comfyPage.setSetting('Comfy.ColorPalette', 'obsidian_dark')
await comfyPage.workflow.loadWorkflow('nodes/every_node_color')
await comfyPage.settings.setSetting('Comfy.ColorPalette', 'obsidian_dark')
await expect(comfyPage.canvas).toHaveScreenshot(
'custom-color-palette-obsidian-dark-all-colors.png'
)
await comfyPage.setSetting('Comfy.ColorPalette', 'light_red')
await comfyPage.settings.setSetting('Comfy.ColorPalette', 'light_red')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'custom-color-palette-light-red.png'
)
await comfyPage.setSetting('Comfy.ColorPalette', 'dark')
await comfyPage.settings.setSetting('Comfy.ColorPalette', 'dark')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('default-color-palette.png')
})
test('Can add custom color palette', async ({ comfyPage }) => {
await comfyPage.page.evaluate((p) => {
window['app'].extensionManager.colorPalette.addCustomColorPalette(p)
await comfyPage.page.evaluate(async (p) => {
await (
window.app!.extensionManager as WorkspaceStore
).colorPalette.addCustomColorPalette(p)
}, customColorPalettes.obsidian_dark)
expect(await comfyPage.getToastErrorCount()).toBe(0)
expect(await comfyPage.toast.getToastErrorCount()).toBe(0)
await comfyPage.setSetting('Comfy.ColorPalette', 'obsidian_dark')
await comfyPage.settings.setSetting('Comfy.ColorPalette', 'obsidian_dark')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'custom-color-palette-obsidian-dark.png'
)
// Legacy `custom_` prefix is still supported
await comfyPage.setSetting('Comfy.ColorPalette', 'custom_obsidian_dark')
await comfyPage.settings.setSetting(
'Comfy.ColorPalette',
'custom_obsidian_dark'
)
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'custom-color-palette-obsidian-dark.png'
@@ -199,20 +207,20 @@ test.describe(
{ tag: ['@screenshot', '@settings'] },
() => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.loadWorkflow('nodes/every_node_color')
await comfyPage.workflow.loadWorkflow('nodes/every_node_color')
})
test('should adjust opacity via node opacity setting', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Node.Opacity', 0.5)
await comfyPage.settings.setSetting('Comfy.Node.Opacity', 0.5)
// Drag mouse to force canvas to redraw
await comfyPage.page.mouse.move(0, 0)
await expect(comfyPage.canvas).toHaveScreenshot('node-opacity-0.5.png')
await comfyPage.setSetting('Comfy.Node.Opacity', 1.0)
await comfyPage.settings.setSetting('Comfy.Node.Opacity', 1.0)
await comfyPage.page.mouse.move(8, 8)
await expect(comfyPage.canvas).toHaveScreenshot('node-opacity-1.png')
@@ -221,8 +229,8 @@ test.describe(
test('should persist color adjustments when changing themes', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Node.Opacity', 0.2)
await comfyPage.setSetting('Comfy.ColorPalette', 'arc')
await comfyPage.settings.setSetting('Comfy.Node.Opacity', 0.2)
await comfyPage.settings.setSetting('Comfy.ColorPalette', 'arc')
await comfyPage.nextFrame()
await comfyPage.page.mouse.move(0, 0)
await expect(comfyPage.canvas).toHaveScreenshot(
@@ -233,8 +241,8 @@ test.describe(
test('should not serialize color adjustments in workflow', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Node.Opacity', 0.5)
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
await comfyPage.settings.setSetting('Comfy.Node.Opacity', 0.5)
await comfyPage.settings.setSetting('Comfy.ColorPalette', 'light')
await comfyPage.nextFrame()
const parsed = await (
await comfyPage.page.waitForFunction(
@@ -262,7 +270,7 @@ test.describe(
test('should lighten node colors when switching to light theme', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
await comfyPage.settings.setSetting('Comfy.ColorPalette', 'light')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'node-lightened-colors.png'
@@ -271,9 +279,9 @@ test.describe(
test.describe('Context menu color adjustments', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
await comfyPage.setSetting('Comfy.Node.Opacity', 0.3)
const node = await comfyPage.getFirstNodeRef()
await comfyPage.settings.setSetting('Comfy.ColorPalette', 'light')
await comfyPage.settings.setSetting('Comfy.Node.Opacity', 0.3)
const node = await comfyPage.nodeOps.getFirstNodeRef()
await node?.clickContextMenuOption('Colors')
})

View File

@@ -3,52 +3,52 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Keybindings', { tag: '@keyboard' }, () => {
test('Should execute command', async ({ comfyPage }) => {
await comfyPage.registerCommand('TestCommand', () => {
window['foo'] = true
await comfyPage.command.registerCommand('TestCommand', () => {
window.foo = true
})
await comfyPage.executeCommand('TestCommand')
expect(await comfyPage.page.evaluate(() => window['foo'])).toBe(true)
await comfyPage.command.executeCommand('TestCommand')
expect(await comfyPage.page.evaluate(() => window.foo)).toBe(true)
})
test('Should execute async command', async ({ comfyPage }) => {
await comfyPage.registerCommand('TestCommand', async () => {
await comfyPage.command.registerCommand('TestCommand', async () => {
await new Promise<void>((resolve) =>
setTimeout(() => {
window['foo'] = true
window.foo = true
resolve()
}, 5)
)
})
await comfyPage.executeCommand('TestCommand')
expect(await comfyPage.page.evaluate(() => window['foo'])).toBe(true)
await comfyPage.command.executeCommand('TestCommand')
expect(await comfyPage.page.evaluate(() => window.foo)).toBe(true)
})
test('Should handle command errors', async ({ comfyPage }) => {
await comfyPage.registerCommand('TestCommand', () => {
await comfyPage.command.registerCommand('TestCommand', () => {
throw new Error('Test error')
})
await comfyPage.executeCommand('TestCommand')
expect(await comfyPage.getToastErrorCount()).toBe(1)
await comfyPage.command.executeCommand('TestCommand')
expect(await comfyPage.toast.getToastErrorCount()).toBe(1)
})
test('Should handle async command errors', async ({ comfyPage }) => {
await comfyPage.registerCommand('TestCommand', async () => {
await new Promise<void>((resolve, reject) =>
await comfyPage.command.registerCommand('TestCommand', async () => {
await new Promise<void>((_resolve, reject) =>
setTimeout(() => {
reject(new Error('Test error'))
}, 5)
)
})
await comfyPage.executeCommand('TestCommand')
expect(await comfyPage.getToastErrorCount()).toBe(1)
await comfyPage.command.executeCommand('TestCommand')
expect(await comfyPage.toast.getToastErrorCount()).toBe(1)
})
})

View File

@@ -1,24 +1,31 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { DefaultGraphPositions } from '../fixtures/constants/defaultGraphPositions'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Copy Paste', { tag: ['@screenshot', '@workflow'] }, () => {
test('Can copy and paste node', async ({ comfyPage }) => {
await comfyPage.clickEmptyLatentNode()
await comfyPage.canvas.click({
position: DefaultGraphPositions.emptyLatentWidgetClick
})
await comfyPage.page.mouse.move(10, 10)
await comfyPage.ctrlC()
await comfyPage.ctrlV()
await comfyPage.nextFrame()
await comfyPage.clipboard.copy()
await comfyPage.clipboard.paste()
await expect(comfyPage.canvas).toHaveScreenshot('copied-node.png')
})
test('Can copy and paste node with link', async ({ comfyPage }) => {
await comfyPage.clickTextEncodeNode1()
await comfyPage.canvas.click({
position: DefaultGraphPositions.textEncodeNode1
})
await comfyPage.nextFrame()
await comfyPage.page.mouse.move(10, 10)
await comfyPage.ctrlC()
await comfyPage.clipboard.copy()
await comfyPage.page.keyboard.press('Control+Shift+V')
await expect(comfyPage.canvas).toHaveScreenshot('copied-node-with-link.png')
})
@@ -28,9 +35,9 @@ test.describe('Copy Paste', { tag: ['@screenshot', '@workflow'] }, () => {
await textBox.click()
const originalString = await textBox.inputValue()
await textBox.selectText()
await comfyPage.ctrlC(null)
await comfyPage.ctrlV(null)
await comfyPage.ctrlV(null)
await comfyPage.clipboard.copy(null)
await comfyPage.clipboard.paste(null)
await comfyPage.clipboard.paste(null)
const resultString = await textBox.inputValue()
expect(resultString).toBe(originalString + originalString)
})
@@ -44,7 +51,7 @@ test.describe('Copy Paste', { tag: ['@screenshot', '@workflow'] }, () => {
y: 281
}
})
await comfyPage.ctrlC(null)
await comfyPage.clipboard.copy(null)
// Empty latent node's width
await comfyPage.canvas.click({
position: {
@@ -52,7 +59,7 @@ test.describe('Copy Paste', { tag: ['@screenshot', '@workflow'] }, () => {
y: 643
}
})
await comfyPage.ctrlV(null)
await comfyPage.clipboard.paste(null)
await comfyPage.page.keyboard.press('Enter')
await expect(comfyPage.canvas).toHaveScreenshot('copied-widget-value.png')
})
@@ -63,15 +70,19 @@ test.describe('Copy Paste', { tag: ['@screenshot', '@workflow'] }, () => {
test('Paste in text area with node previously copied', async ({
comfyPage
}) => {
await comfyPage.clickEmptyLatentNode()
await comfyPage.ctrlC(null)
await comfyPage.canvas.click({
position: DefaultGraphPositions.emptyLatentWidgetClick
})
await comfyPage.page.mouse.move(10, 10)
await comfyPage.nextFrame()
await comfyPage.clipboard.copy(null)
const textBox = comfyPage.widgetTextBox
await textBox.click()
await textBox.inputValue()
await textBox.selectText()
await comfyPage.ctrlC(null)
await comfyPage.ctrlV(null)
await comfyPage.ctrlV(null)
await comfyPage.clipboard.copy(null)
await comfyPage.clipboard.paste(null)
await comfyPage.clipboard.paste(null)
await expect(comfyPage.canvas).toHaveScreenshot(
'paste-in-text-area-with-node-previously-copied.png'
)
@@ -82,10 +93,10 @@ test.describe('Copy Paste', { tag: ['@screenshot', '@workflow'] }, () => {
await textBox.click()
await textBox.inputValue()
await textBox.selectText()
await comfyPage.ctrlC(null)
await comfyPage.clipboard.copy(null)
// Unfocus textbox.
await comfyPage.page.mouse.click(10, 10)
await comfyPage.ctrlV(null)
await comfyPage.clipboard.paste(null)
await expect(comfyPage.canvas).toHaveScreenshot('no-node-copied.png')
})
@@ -103,19 +114,19 @@ test.describe('Copy Paste', { tag: ['@screenshot', '@workflow'] }, () => {
test('Can undo paste multiple nodes as single action', async ({
comfyPage
}) => {
const initialCount = await comfyPage.getGraphNodesCount()
const initialCount = await comfyPage.nodeOps.getGraphNodesCount()
expect(initialCount).toBeGreaterThan(1)
await comfyPage.canvas.click()
await comfyPage.ctrlA()
await comfyPage.keyboard.selectAll()
await comfyPage.page.mouse.move(10, 10)
await comfyPage.ctrlC()
await comfyPage.ctrlV()
await comfyPage.clipboard.copy()
await comfyPage.clipboard.paste()
const pasteCount = await comfyPage.getGraphNodesCount()
const pasteCount = await comfyPage.nodeOps.getGraphNodesCount()
expect(pasteCount).toBe(initialCount * 2)
await comfyPage.ctrlZ()
const undoCount = await comfyPage.getGraphNodesCount()
await comfyPage.keyboard.undo()
const undoCount = await comfyPage.nodeOps.getGraphNodesCount()
expect(undoCount).toBe(initialCount)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 100 KiB

View File

@@ -24,7 +24,7 @@ async function verifyCustomIconSvg(iconElement: Locator) {
test.describe('Custom Icons', { tag: '@settings' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
})
test('sidebar tab icons use custom SVGs', async ({ comfyPage }) => {

View File

@@ -1,18 +1,19 @@
import type { Locator } from '@playwright/test'
import { expect } from '@playwright/test'
import type { Keybinding } from '../../src/platform/keybindings'
import type { Keybinding } from '../../src/platform/keybindings/types'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { DefaultGraphPositions } from '../fixtures/constants/defaultGraphPositions'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Load workflow warning', { tag: '@ui' }, () => {
test('Should display a warning when loading a workflow with missing nodes', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('missing/missing_nodes')
await comfyPage.workflow.loadWorkflow('missing/missing_nodes')
// Wait for the element with the .comfy-missing-nodes selector to be visible
const missingNodesWarning = comfyPage.page.locator('.comfy-missing-nodes')
@@ -22,7 +23,7 @@ test.describe('Load workflow warning', { tag: '@ui' }, () => {
test('Should display a warning when loading a workflow with missing nodes in subgraphs', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('missing/missing_nodes_in_subgraph')
await comfyPage.workflow.loadWorkflow('missing/missing_nodes_in_subgraph')
// Wait for the element with the .comfy-missing-nodes selector to be visible
const missingNodesWarning = comfyPage.page.locator('.comfy-missing-nodes')
@@ -36,22 +37,26 @@ test.describe('Load workflow warning', { tag: '@ui' }, () => {
})
test('Does not report warning on undo/redo', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default')
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default')
await comfyPage.loadWorkflow('missing/missing_nodes')
await comfyPage.closeDialog()
await comfyPage.workflow.loadWorkflow('missing/missing_nodes')
await comfyPage.page
.locator('.p-dialog')
.getByRole('button', { name: 'Close' })
.click({ force: true })
await comfyPage.page.locator('.p-dialog').waitFor({ state: 'hidden' })
// Wait for any async operations to complete after dialog closes
await comfyPage.nextFrame()
// Make a change to the graph
await comfyPage.doubleClickCanvas()
await comfyPage.canvasOps.doubleClick()
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler')
// Undo and redo the change
await comfyPage.ctrlZ()
await comfyPage.keyboard.undo()
await expect(comfyPage.page.locator('.comfy-missing-nodes')).not.toBeVisible()
await comfyPage.ctrlY()
await comfyPage.keyboard.redo()
await expect(comfyPage.page.locator('.comfy-missing-nodes')).not.toBeVisible()
})
@@ -59,7 +64,7 @@ test.describe('Execution error', () => {
test('Should display an error message when an execution error occurs', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('nodes/execution_error')
await comfyPage.workflow.loadWorkflow('nodes/execution_error')
await comfyPage.queueButton.click()
await comfyPage.nextFrame()
@@ -71,7 +76,10 @@ test.describe('Execution error', () => {
test.describe('Missing models warning', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Workflow.ShowMissingModelsWarning', true)
await comfyPage.settings.setSetting(
'Comfy.Workflow.ShowMissingModelsWarning',
true
)
await comfyPage.page.evaluate((url: string) => {
return fetch(`${url}/api/devtools/cleanup_fake_model`)
}, comfyPage.url)
@@ -80,7 +88,7 @@ test.describe('Missing models warning', () => {
test('Should display a warning when missing models are found', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('missing/missing_models')
await comfyPage.workflow.loadWorkflow('missing/missing_models')
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
await expect(missingModelsWarning).toBeVisible()
@@ -97,7 +105,9 @@ test.describe('Missing models warning', () => {
comfyPage
}) => {
// Load workflow that has a node with models metadata at the node level
await comfyPage.loadWorkflow('missing/missing_models_from_node_properties')
await comfyPage.workflow.loadWorkflow(
'missing/missing_models_from_node_properties'
)
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
await expect(missingModelsWarning).toBeVisible()
@@ -146,7 +156,7 @@ test.describe('Missing models warning', () => {
{ times: 1 }
)
await comfyPage.loadWorkflow('missing/missing_models')
await comfyPage.workflow.loadWorkflow('missing/missing_models')
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
await expect(missingModelsWarning).not.toBeVisible()
@@ -157,7 +167,9 @@ test.describe('Missing models warning', () => {
}) => {
// This tests the scenario where outdated model metadata exists in the workflow
// but the actual selected models (widget values) have changed
await comfyPage.loadWorkflow('missing/model_metadata_widget_mismatch')
await comfyPage.workflow.loadWorkflow(
'missing/model_metadata_widget_mismatch'
)
// The missing models warning should NOT appear
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
@@ -171,7 +183,7 @@ test.describe('Missing models warning', () => {
}) => {
// The fake_model.safetensors is served by
// https://github.com/Comfy-Org/ComfyUI_devtools/blob/main/__init__.py
await comfyPage.loadWorkflow('missing/missing_models')
await comfyPage.workflow.loadWorkflow('missing/missing_models')
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
await expect(missingModelsWarning).toBeVisible()
@@ -190,11 +202,11 @@ test.describe('Missing models warning', () => {
let closeButton: Locator
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting(
await comfyPage.settings.setSetting(
'Comfy.Workflow.ShowMissingModelsWarning',
true
)
await comfyPage.loadWorkflow('missing/missing_models')
await comfyPage.workflow.loadWorkflow('missing/missing_models')
checkbox = comfyPage.page.getByLabel("Don't show this again")
closeButton = comfyPage.page.getByLabel('Close')
@@ -210,7 +222,7 @@ test.describe('Missing models warning', () => {
await closeButton.click()
await changeSettingPromise
const settingValue = await comfyPage.getSetting(
const settingValue = await comfyPage.settings.getSetting(
'Comfy.Workflow.ShowMissingModelsWarning'
)
expect(settingValue).toBe(false)
@@ -221,7 +233,7 @@ test.describe('Missing models warning', () => {
}) => {
await closeButton.click()
const settingValue = await comfyPage.getSetting(
const settingValue = await comfyPage.settings.getSetting(
'Comfy.Workflow.ShowMissingModelsWarning'
)
expect(settingValue).toBe(true)
@@ -252,9 +264,11 @@ test.describe('Settings', () => {
test('Can change canvas zoom speed setting', async ({ comfyPage }) => {
const maxSpeed = 2.5
await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', maxSpeed)
await comfyPage.settings.setSetting('Comfy.Graph.ZoomSpeed', maxSpeed)
await test.step('Setting should persist', async () => {
expect(await comfyPage.getSetting('Comfy.Graph.ZoomSpeed')).toBe(maxSpeed)
expect(await comfyPage.settings.getSetting('Comfy.Graph.ZoomSpeed')).toBe(
maxSpeed
)
})
})
@@ -311,7 +325,7 @@ test.describe('Support', () => {
test('Should open external zendesk link with OSS tag', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
const pagePromise = comfyPage.page.context().waitForEvent('page')
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Support'])
const newPage = await pagePromise
@@ -331,13 +345,13 @@ test.describe('Error dialog', () => {
comfyPage
}) => {
await comfyPage.page.evaluate(() => {
const graph = window['graph']
graph.configure = () => {
const graph = window.graph!
;(graph as { configure: () => void }).configure = () => {
throw new Error('Error on configure!')
}
})
await comfyPage.loadWorkflow('default')
await comfyPage.workflow.loadWorkflow('default')
const errorDialog = comfyPage.page.locator('.comfy-error-report')
await expect(errorDialog).toBeVisible()
@@ -347,7 +361,7 @@ test.describe('Error dialog', () => {
comfyPage
}) => {
await comfyPage.page.evaluate(async () => {
const app = window['app']
const app = window.app!
app.api.queuePrompt = () => {
throw new Error('Error on queuePrompt!')
}
@@ -362,9 +376,13 @@ test.describe('Signin dialog', () => {
test('Paste content to signin dialog should not paste node on canvas', async ({
comfyPage
}) => {
const nodeNum = (await comfyPage.getNodes()).length
await comfyPage.clickEmptyLatentNode()
await comfyPage.ctrlC()
const nodeNum = (await comfyPage.nodeOps.getNodes()).length
await comfyPage.canvas.click({
position: DefaultGraphPositions.emptyLatentWidgetClick
})
await comfyPage.page.mouse.move(10, 10)
await comfyPage.nextFrame()
await comfyPage.clipboard.copy()
const textBox = comfyPage.widgetTextBox
await textBox.click()
@@ -373,7 +391,7 @@ test.describe('Signin dialog', () => {
await textBox.press('Control+c')
await comfyPage.page.evaluate(() => {
void window['app'].extensionManager.dialog.showSignInDialog()
void window.app!.extensionManager.dialog.showSignInDialog()
})
const input = comfyPage.page.locator('#comfy-org-sign-in-password')
@@ -381,6 +399,6 @@ test.describe('Signin dialog', () => {
await input.press('Control+v')
await expect(input).toHaveValue('test_password')
expect(await comfyPage.getNodes()).toHaveLength(nodeNum)
expect(await comfyPage.nodeOps.getNodes()).toHaveLength(nodeNum)
})
})

View File

@@ -3,12 +3,12 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('DOM Widget', { tag: '@widget' }, () => {
test('Collapsed multiline textarea is not visible', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/collapsed_multiline')
await comfyPage.workflow.loadWorkflow('widgets/collapsed_multiline')
const textareaWidget = comfyPage.page.locator('.comfy-multiline-input')
await expect(textareaWidget).not.toBeVisible()
})
@@ -21,7 +21,7 @@ test.describe('DOM Widget', { tag: '@widget' }, () => {
await expect(firstMultiline).toBeVisible()
await expect(lastMultiline).toBeVisible()
const nodes = await comfyPage.getNodeRefsByType('CLIPTextEncode')
const nodes = await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
for (const node of nodes) {
await node.click('collapse')
}
@@ -33,8 +33,8 @@ test.describe('DOM Widget', { tag: '@widget' }, () => {
'Position update when entering focus mode',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.executeCommand('Workspace.ToggleFocusMode')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.command.executeCommand('Workspace.ToggleFocusMode')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('focus-mode-on.png')
}
@@ -68,9 +68,9 @@ test.describe('DOM Widget', { tag: '@widget' }, () => {
.first()
await expect(textareaWidget).toBeVisible()
await comfyPage.setSetting('Comfy.Sidebar.Size', 'small')
await comfyPage.setSetting('Comfy.Sidebar.Location', 'left')
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.Sidebar.Size', 'small')
await comfyPage.settings.setSetting('Comfy.Sidebar.Location', 'left')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.nextFrame()
let oldPos: [number, number]
@@ -85,15 +85,15 @@ test.describe('DOM Widget', { tag: '@widget' }, () => {
// --- test ---
await comfyPage.setSetting('Comfy.Sidebar.Size', 'normal')
await comfyPage.settings.setSetting('Comfy.Sidebar.Size', 'normal')
await comfyPage.nextFrame()
await checkBboxChange()
await comfyPage.setSetting('Comfy.Sidebar.Location', 'right')
await comfyPage.settings.setSetting('Comfy.Sidebar.Location', 'right')
await comfyPage.nextFrame()
await checkBboxChange()
await comfyPage.setSetting('Comfy.UseNewMenu', 'Bottom')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Bottom')
await comfyPage.nextFrame()
await checkBboxChange()
})

View File

@@ -3,7 +3,7 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Execution', { tag: ['@smoke', '@workflow'] }, () => {
@@ -11,12 +11,15 @@ test.describe('Execution', { tag: ['@smoke', '@workflow'] }, () => {
'Report error on unconnected slot',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.disconnectEdge()
await comfyPage.clickEmptySpace()
await comfyPage.canvasOps.disconnectEdge()
await comfyPage.canvasOps.clickEmptySpace()
await comfyPage.executeCommand('Comfy.QueuePrompt')
await comfyPage.command.executeCommand('Comfy.QueuePrompt')
await expect(comfyPage.page.locator('.comfy-error-report')).toBeVisible()
await comfyPage.page.locator('.p-dialog-close-button').click()
await comfyPage.page
.locator('.p-dialog')
.getByRole('button', { name: 'Close' })
.click()
await comfyPage.page.locator('.comfy-error-report').waitFor({
state: 'hidden'
})
@@ -32,17 +35,17 @@ test.describe(
{ tag: ['@smoke', '@workflow'] },
() => {
test('Execute to selected output nodes', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('execution/partial_execution')
const input = await comfyPage.getNodeRefById(3)
const output1 = await comfyPage.getNodeRefById(1)
const output2 = await comfyPage.getNodeRefById(4)
await comfyPage.workflow.loadWorkflow('execution/partial_execution')
const input = await comfyPage.nodeOps.getNodeRefById(3)
const output1 = await comfyPage.nodeOps.getNodeRefById(1)
const output2 = await comfyPage.nodeOps.getNodeRefById(4)
expect(await (await input.getWidget(0)).getValue()).toBe('foo')
expect(await (await output1.getWidget(0)).getValue()).toBe('')
expect(await (await output2.getWidget(0)).getValue()).toBe('')
await output1.click('title')
await comfyPage.executeCommand('Comfy.QueueSelectedOutputNodes')
await comfyPage.command.executeCommand('Comfy.QueueSelectedOutputNodes')
await expect(async () => {
expect(await (await input.getWidget(0)).getValue()).toBe('foo')
expect(await (await output1.getWidget(0)).getValue()).toBe('foo')

View File

@@ -1,23 +1,32 @@
import { expect } from '@playwright/test'
import type { Settings } from '../../src/schemas/apiSchema'
import type { SettingParams } from '../../src/platform/settings/types'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
/**
* Type helper for test settings with arbitrary IDs.
* Extensions can register settings with any ID, but SettingParams.id
* is typed as keyof Settings for autocomplete. This helper allows
* arbitrary IDs in tests while keeping type safety for other fields.
*/
type TestSettingId = keyof Settings
test.describe('Topbar commands', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
})
test('Should allow registering topbar commands', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
window['app'].registerExtension({
window.app!.registerExtension({
name: 'TestExtension1',
commands: [
{
id: 'foo',
label: 'foo-command',
function: () => {
window['foo'] = true
window.foo = true
}
}
],
@@ -31,15 +40,15 @@ test.describe('Topbar commands', () => {
})
await comfyPage.menu.topbar.triggerTopbarCommand(['ext', 'foo-command'])
expect(await comfyPage.page.evaluate(() => window['foo'])).toBe(true)
expect(await comfyPage.page.evaluate(() => window.foo)).toBe(true)
})
test('Should not allow register command defined in other extension', async ({
comfyPage
}) => {
await comfyPage.registerCommand('foo', () => alert(1))
await comfyPage.command.registerCommand('foo', () => alert(1))
await comfyPage.page.evaluate(() => {
window['app'].registerExtension({
window.app!.registerExtension({
name: 'TestExtension1',
menuCommands: [
{
@@ -56,14 +65,14 @@ test.describe('Topbar commands', () => {
test('Should allow registering keybindings', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
const app = window['app']
const app = window.app!
app.registerExtension({
name: 'TestExtension1',
commands: [
{
id: 'TestCommand',
function: () => {
window['TestCommand'] = true
window.TestCommand = true
}
}
],
@@ -77,68 +86,77 @@ test.describe('Topbar commands', () => {
})
await comfyPage.page.keyboard.press('k')
expect(await comfyPage.page.evaluate(() => window['TestCommand'])).toBe(
true
)
expect(await comfyPage.page.evaluate(() => window.TestCommand)).toBe(true)
})
test.describe('Settings', () => {
test('Should allow adding settings', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
window['app'].registerExtension({
window.app!.registerExtension({
name: 'TestExtension1',
settings: [
{
id: 'TestSetting',
// Extensions can register arbitrary setting IDs
id: 'TestSetting' as TestSettingId,
name: 'Test Setting',
type: 'text',
defaultValue: 'Hello, world!',
onChange: () => {
window['changeCount'] = (window['changeCount'] ?? 0) + 1
window.changeCount = (window.changeCount ?? 0) + 1
}
}
]
})
})
// onChange is called when the setting is first added
expect(await comfyPage.page.evaluate(() => window['changeCount'])).toBe(1)
expect(await comfyPage.getSetting('TestSetting')).toBe('Hello, world!')
expect(await comfyPage.page.evaluate(() => window.changeCount)).toBe(1)
expect(await comfyPage.settings.getSetting('TestSetting')).toBe(
'Hello, world!'
)
await comfyPage.setSetting('TestSetting', 'Hello, universe!')
expect(await comfyPage.getSetting('TestSetting')).toBe('Hello, universe!')
expect(await comfyPage.page.evaluate(() => window['changeCount'])).toBe(2)
await comfyPage.settings.setSetting('TestSetting', 'Hello, universe!')
expect(await comfyPage.settings.getSetting('TestSetting')).toBe(
'Hello, universe!'
)
expect(await comfyPage.page.evaluate(() => window.changeCount)).toBe(2)
})
test('Should allow setting boolean settings', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
window['app'].registerExtension({
window.app!.registerExtension({
name: 'TestExtension1',
settings: [
{
id: 'Comfy.TestSetting',
// Extensions can register arbitrary setting IDs
id: 'Comfy.TestSetting' as TestSettingId,
name: 'Test Setting',
type: 'boolean',
defaultValue: false,
onChange: () => {
window['changeCount'] = (window['changeCount'] ?? 0) + 1
window.changeCount = (window.changeCount ?? 0) + 1
}
}
]
})
})
expect(await comfyPage.getSetting('Comfy.TestSetting')).toBe(false)
expect(await comfyPage.page.evaluate(() => window['changeCount'])).toBe(1)
expect(await comfyPage.settings.getSetting('Comfy.TestSetting')).toBe(
false
)
expect(await comfyPage.page.evaluate(() => window.changeCount)).toBe(1)
await comfyPage.settingDialog.open()
await comfyPage.settingDialog.toggleBooleanSetting('Comfy.TestSetting')
expect(await comfyPage.getSetting('Comfy.TestSetting')).toBe(true)
expect(await comfyPage.page.evaluate(() => window['changeCount'])).toBe(2)
expect(await comfyPage.settings.getSetting('Comfy.TestSetting')).toBe(
true
)
expect(await comfyPage.page.evaluate(() => window.changeCount)).toBe(2)
})
test.describe('Passing through attrs to setting components', () => {
const testCases: Array<{
config: Partial<SettingParams>
config: Pick<SettingParams, 'type' | 'defaultValue'> &
Partial<Omit<SettingParams, 'id' | 'type' | 'defaultValue'>>
selector: string
}> = [
{
@@ -191,13 +209,13 @@ test.describe('Topbar commands', () => {
comfyPage
}) => {
await comfyPage.page.evaluate((config) => {
window['app'].registerExtension({
window.app!.registerExtension({
name: 'TestExtension1',
settings: [
{
id: 'Comfy.TestSetting',
// Extensions can register arbitrary setting IDs
id: 'Comfy.TestSetting' as TestSettingId,
name: 'Test',
// The `disabled` attr is common to all settings components
attrs: { disabled: true },
...config
}
@@ -224,7 +242,7 @@ test.describe('Topbar commands', () => {
test.describe('About panel', () => {
test('Should allow adding badges', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
window['app'].registerExtension({
window.app!.registerExtension({
name: 'TestExtension1',
aboutPageBadges: [
{
@@ -247,61 +265,71 @@ test.describe('Topbar commands', () => {
test.describe('Dialog', () => {
test('Should allow showing a prompt dialog', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
void window['app'].extensionManager.dialog
.prompt({
void window
.app!.extensionManager.dialog.prompt({
title: 'Test Prompt',
message: 'Test Prompt Message'
})
.then((value: string) => {
window['value'] = value
.then((value: string | null) => {
;(window as unknown as Record<string, unknown>)['value'] = value
})
})
await comfyPage.fillPromptDialog('Hello, world!')
expect(await comfyPage.page.evaluate(() => window['value'])).toBe(
'Hello, world!'
)
await comfyPage.nodeOps.fillPromptDialog('Hello, world!')
expect(
await comfyPage.page.evaluate(
() => (window as unknown as Record<string, unknown>)['value']
)
).toBe('Hello, world!')
})
test('Should allow showing a confirmation dialog', async ({
comfyPage
}) => {
await comfyPage.page.evaluate(() => {
void window['app'].extensionManager.dialog
.confirm({
void window
.app!.extensionManager.dialog.confirm({
title: 'Test Confirm',
message: 'Test Confirm Message'
})
.then((value: boolean) => {
window['value'] = value
.then((value: boolean | null) => {
;(window as unknown as Record<string, unknown>)['value'] = value
})
})
await comfyPage.confirmDialog.click('confirm')
expect(await comfyPage.page.evaluate(() => window['value'])).toBe(true)
expect(
await comfyPage.page.evaluate(
() => (window as unknown as Record<string, unknown>)['value']
)
).toBe(true)
})
test('Should allow dismissing a dialog', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
window['value'] = 'foo'
void window['app'].extensionManager.dialog
.confirm({
;(window as unknown as Record<string, unknown>)['value'] = 'foo'
void window
.app!.extensionManager.dialog.confirm({
title: 'Test Confirm',
message: 'Test Confirm Message'
})
.then((value: boolean) => {
window['value'] = value
.then((value: boolean | null) => {
;(window as unknown as Record<string, unknown>)['value'] = value
})
})
await comfyPage.confirmDialog.click('reject')
expect(await comfyPage.page.evaluate(() => window['value'])).toBeNull()
expect(
await comfyPage.page.evaluate(
() => (window as unknown as Record<string, unknown>)['value']
)
).toBeNull()
})
})
test.describe('Selection Toolbox', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
await comfyPage.settings.setSetting('Comfy.Canvas.SelectionToolbox', true)
})
test('Should allow adding commands to selection toolbox', async ({
@@ -309,7 +337,7 @@ test.describe('Topbar commands', () => {
}) => {
// Register an extension with a selection toolbox command
await comfyPage.page.evaluate(() => {
window['app'].registerExtension({
window.app!.registerExtension({
name: 'TestExtension1',
commands: [
{
@@ -317,7 +345,9 @@ test.describe('Topbar commands', () => {
label: 'Test Command',
icon: 'pi pi-star',
function: () => {
window['selectionCommandExecuted'] = true
;(window as unknown as Record<string, unknown>)[
'selectionCommandExecuted'
] = true
}
}
],
@@ -325,7 +355,7 @@ test.describe('Topbar commands', () => {
})
})
await comfyPage.selectNodes(['CLIP Text Encode (Prompt)'])
await comfyPage.nodeOps.selectNodes(['CLIP Text Encode (Prompt)'])
// Click the command button in the selection toolbox
const toolboxButton = comfyPage.page.locator(
@@ -333,9 +363,13 @@ test.describe('Topbar commands', () => {
)
await toolboxButton.click()
// Verify the command was executed
expect(
await comfyPage.page.evaluate(() => window['selectionCommandExecuted'])
await comfyPage.page.evaluate(
() =>
(window as unknown as Record<string, unknown>)[
'selectionCommandExecuted'
]
)
).toBe(true)
})
})

View File

@@ -3,7 +3,7 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
@@ -25,7 +25,7 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
const originalSend = WebSocket.prototype.send
WebSocket.prototype.send = function (data) {
try {
const parsed = JSON.parse(data)
const parsed = JSON.parse(data as string)
if (parsed.type === 'feature_flags') {
window.__capturedMessages!.clientFeatureFlags = parsed
}
@@ -38,11 +38,11 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
// Monitor for server feature flags
const checkInterval = setInterval(() => {
if (
window['app']?.api?.serverFeatureFlags &&
Object.keys(window['app'].api.serverFeatureFlags).length > 0
window.app?.api?.serverFeatureFlags &&
Object.keys(window.app.api.serverFeatureFlags).length > 0
) {
window.__capturedMessages!.serverFeatureFlags =
window['app'].api.serverFeatureFlags
window.app.api.serverFeatureFlags
clearInterval(checkInterval)
}
}, 100)
@@ -96,7 +96,7 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
}) => {
// Get the actual server feature flags from the backend
const serverFlags = await comfyPage.page.evaluate(() => {
return window['app']!.api.serverFeatureFlags
return window.app!.api.serverFeatureFlags
})
// Verify we received real feature flags from the backend
@@ -115,26 +115,22 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
}) => {
// Test serverSupportsFeature with real backend flags
const supportsPreviewMetadata = await comfyPage.page.evaluate(() => {
return window['app']!.api.serverSupportsFeature(
'supports_preview_metadata'
)
return window.app!.api.serverSupportsFeature('supports_preview_metadata')
})
// The method should return a boolean based on the backend's value
expect(typeof supportsPreviewMetadata).toBe('boolean')
// Test non-existent feature - should always return false
const supportsNonExistent = await comfyPage.page.evaluate(() => {
return window['app']!.api.serverSupportsFeature(
'non_existent_feature_xyz'
)
return window.app!.api.serverSupportsFeature('non_existent_feature_xyz')
})
expect(supportsNonExistent).toBe(false)
// Test that the method only returns true for boolean true values
const testResults = await comfyPage.page.evaluate(() => {
// Temporarily modify serverFeatureFlags to test behavior
const original = window['app']!.api.serverFeatureFlags
window['app']!.api.serverFeatureFlags = {
const original = window.app!.api.serverFeatureFlags
window.app!.api.serverFeatureFlags = {
bool_true: true,
bool_false: false,
string_value: 'yes',
@@ -143,15 +139,15 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
}
const results = {
bool_true: window['app']!.api.serverSupportsFeature('bool_true'),
bool_false: window['app']!.api.serverSupportsFeature('bool_false'),
string_value: window['app']!.api.serverSupportsFeature('string_value'),
number_value: window['app']!.api.serverSupportsFeature('number_value'),
null_value: window['app']!.api.serverSupportsFeature('null_value')
bool_true: window.app!.api.serverSupportsFeature('bool_true'),
bool_false: window.app!.api.serverSupportsFeature('bool_false'),
string_value: window.app!.api.serverSupportsFeature('string_value'),
number_value: window.app!.api.serverSupportsFeature('number_value'),
null_value: window.app!.api.serverSupportsFeature('null_value')
}
// Restore original
window['app']!.api.serverFeatureFlags = original
window.app!.api.serverFeatureFlags = original
return results
})
@@ -168,20 +164,20 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
}) => {
// Test getServerFeature method
const previewMetadataValue = await comfyPage.page.evaluate(() => {
return window['app']!.api.getServerFeature('supports_preview_metadata')
return window.app!.api.getServerFeature('supports_preview_metadata')
})
expect(typeof previewMetadataValue).toBe('boolean')
// Test getting max_upload_size
const maxUploadSize = await comfyPage.page.evaluate(() => {
return window['app']!.api.getServerFeature('max_upload_size')
return window.app!.api.getServerFeature('max_upload_size')
})
expect(typeof maxUploadSize).toBe('number')
expect(maxUploadSize).toBeGreaterThan(0)
// Test getServerFeature with default value for non-existent feature
const defaultValue = await comfyPage.page.evaluate(() => {
return window['app']!.api.getServerFeature(
return window.app!.api.getServerFeature(
'non_existent_feature_xyz',
'default'
)
@@ -194,7 +190,7 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
}) => {
// Test getServerFeatures returns all flags
const allFeatures = await comfyPage.page.evaluate(() => {
return window['app']!.api.getServerFeatures()
return window.app!.api.getServerFeatures()
})
expect(allFeatures).toBeTruthy()
@@ -207,14 +203,14 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
test('Client feature flags are immutable', async ({ comfyPage }) => {
// Test that getClientFeatureFlags returns a copy
const immutabilityTest = await comfyPage.page.evaluate(() => {
const flags1 = window['app']!.api.getClientFeatureFlags()
const flags2 = window['app']!.api.getClientFeatureFlags()
const flags1 = window.app!.api.getClientFeatureFlags()
const flags2 = window.app!.api.getClientFeatureFlags()
// Modify the first object
flags1.test_modification = true
// Get flags again to check if original was modified
const flags3 = window['app']!.api.getClientFeatureFlags()
const flags3 = window.app!.api.getClientFeatureFlags()
return {
areEqual: flags1 === flags2,
@@ -240,14 +236,14 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
}) => {
const immutabilityTest = await comfyPage.page.evaluate(() => {
// Get a copy of server features
const features1 = window['app']!.api.getServerFeatures()
const features1 = window.app!.api.getServerFeatures()
// Try to modify it
features1.supports_preview_metadata = false
features1.new_feature = 'added'
// Get another copy
const features2 = window['app']!.api.getServerFeatures()
const features2 = window.app!.api.getServerFeatures()
return {
modifiedValue: features1.supports_preview_metadata,
@@ -286,35 +282,26 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
// Monitor when feature flags arrive by checking periodically
const checkFeatureFlags = setInterval(() => {
if (
window['app']?.api?.serverFeatureFlags?.supports_preview_metadata !==
window.app?.api?.serverFeatureFlags?.supports_preview_metadata !==
undefined
) {
window.__appReadiness = {
...window.__appReadiness,
featureFlagsReceived: true
}
window.__appReadiness!.featureFlagsReceived = true
clearInterval(checkFeatureFlags)
}
}, 10)
// Monitor API initialization
const checkApi = setInterval(() => {
if (window['app']?.api) {
window.__appReadiness = {
...window.__appReadiness,
apiInitialized: true
}
if (window.app?.api) {
window.__appReadiness!.apiInitialized = true
clearInterval(checkApi)
}
}, 10)
// Monitor app initialization
const checkApp = setInterval(() => {
if (window['app']?.graph) {
window.__appReadiness = {
...window.__appReadiness,
appInitialized: true
}
if (window.app?.graph) {
window.__appReadiness!.appInitialized = true
clearInterval(checkApp)
}
}, 10)
@@ -333,7 +320,7 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
// Wait for feature flags to be received
await newPage.waitForFunction(
() =>
window['app']?.api?.serverFeatureFlags?.supports_preview_metadata !==
window.app?.api?.serverFeatureFlags?.supports_preview_metadata !==
undefined,
{
timeout: 10000
@@ -344,7 +331,7 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
const readiness = await newPage.evaluate(() => {
return {
...window.__appReadiness,
currentFlags: window['app']!.api.serverFeatureFlags
currentFlags: window.app!.api.serverFeatureFlags
}
})

View File

@@ -3,24 +3,24 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Graph', { tag: ['@smoke', '@canvas'] }, () => {
// Should be able to fix link input slot index after swap the input order
// Ref: https://github.com/Comfy-Org/ComfyUI_frontend/issues/3348
test('Fix link input slots', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/input_order_swap')
await comfyPage.workflow.loadWorkflow('inputs/input_order_swap')
expect(
await comfyPage.page.evaluate(() => {
return window['app'].graph.links.get(1)?.target_slot
return window.app!.graph!.links.get(1)?.target_slot
})
).toBe(1)
})
test('Validate workflow links', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Validation.Workflows', true)
await comfyPage.loadWorkflow('links/bad_link')
await expect(comfyPage.getVisibleToastCount()).resolves.toBe(2)
await comfyPage.settings.setSetting('Comfy.Validation.Workflows', true)
await comfyPage.workflow.loadWorkflow('links/bad_link')
await expect.poll(() => comfyPage.toast.getVisibleToastCount()).toBe(2)
})
})

View File

@@ -1,34 +1,37 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { TestIds } from '../fixtures/selectors'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Graph Canvas Menu', { tag: ['@screenshot', '@canvas'] }, () => {
test.beforeEach(async ({ comfyPage }) => {
// Set link render mode to spline to make sure it's not affected by other tests'
// side effects.
await comfyPage.setSetting('Comfy.LinkRenderMode', 2)
await comfyPage.settings.setSetting('Comfy.LinkRenderMode', 2)
// Enable canvas menu for all tests
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true)
await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', true)
})
test(
'Can toggle link visibility',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
const button = comfyPage.page.getByTestId('toggle-link-visibility-button')
const button = comfyPage.page.getByTestId(
TestIds.canvas.toggleLinkVisibilityButton
)
await button.click()
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'canvas-with-hidden-links.png'
)
const hiddenLinkRenderMode = await comfyPage.page.evaluate(() => {
return window['LiteGraph'].HIDDEN_LINK
return window.LiteGraph!.HIDDEN_LINK
})
expect(await comfyPage.getSetting('Comfy.LinkRenderMode')).toBe(
expect(await comfyPage.settings.getSetting('Comfy.LinkRenderMode')).toBe(
hiddenLinkRenderMode
)
@@ -37,16 +40,18 @@ test.describe('Graph Canvas Menu', { tag: ['@screenshot', '@canvas'] }, () => {
await expect(comfyPage.canvas).toHaveScreenshot(
'canvas-with-visible-links.png'
)
expect(await comfyPage.getSetting('Comfy.LinkRenderMode')).not.toBe(
hiddenLinkRenderMode
)
expect(
await comfyPage.settings.getSetting('Comfy.LinkRenderMode')
).not.toBe(hiddenLinkRenderMode)
}
)
test('Toggle minimap button is clickable and has correct test id', async ({
comfyPage
}) => {
const minimapButton = comfyPage.page.getByTestId('toggle-minimap-button')
const minimapButton = comfyPage.page.getByTestId(
TestIds.canvas.toggleMinimapButton
)
await expect(minimapButton).toBeVisible()
await expect(minimapButton).toBeEnabled()

View File

@@ -1,11 +1,15 @@
import { expect } from '@playwright/test'
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
import type { ComfyPage } from '../fixtures/ComfyPage'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import type { NodeLibrarySidebarTab } from '../fixtures/components/SidebarTab'
import { DefaultGraphPositions } from '../fixtures/constants/defaultGraphPositions'
import type { NodeReference } from '../fixtures/utils/litegraphUtils'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Group Node', { tag: '@node' }, () => {
@@ -13,30 +17,34 @@ test.describe('Group Node', { tag: '@node' }, () => {
const groupNodeName = 'DefautWorkflowGroupNode'
const groupNodeCategory = 'group nodes>workflow'
const groupNodeBookmarkName = `workflow>${groupNodeName}`
let libraryTab
let libraryTab: NodeLibrarySidebarTab
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
libraryTab = comfyPage.menu.nodeLibraryTab
await comfyPage.convertAllNodesToGroupNode(groupNodeName)
await comfyPage.nodeOps.convertAllNodesToGroupNode(groupNodeName)
await libraryTab.open()
})
test('Is added to node library sidebar', async ({ comfyPage }) => {
expect(await libraryTab.getFolder('group nodes').count()).toBe(1)
test('Is added to node library sidebar', async ({
comfyPage: _comfyPage
}) => {
expect(await libraryTab.getFolder(groupNodeCategory).count()).toBe(1)
})
test('Can be added to canvas using node library sidebar', async ({
comfyPage
}) => {
const initialNodeCount = await comfyPage.getGraphNodesCount()
const initialNodeCount = await comfyPage.nodeOps.getGraphNodesCount()
// Add group node from node library sidebar
await libraryTab.getFolder(groupNodeCategory).click()
await libraryTab.getNode(groupNodeName).click()
// Verify the node is added to the canvas
expect(await comfyPage.getGraphNodesCount()).toBe(initialNodeCount + 1)
expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe(
initialNodeCount + 1
)
})
test('Can be bookmarked and unbookmarked', async ({ comfyPage }) => {
@@ -48,7 +56,7 @@ test.describe('Group Node', { tag: '@node' }, () => {
// Verify the node is added to the bookmarks tab
expect(
await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
).toEqual([groupNodeBookmarkName])
// Verify the bookmark node with the same name is added to the tree
expect(await libraryTab.getNode(groupNodeName).count()).not.toBe(0)
@@ -62,7 +70,7 @@ test.describe('Group Node', { tag: '@node' }, () => {
// Verify the node is removed from the bookmarks tab
expect(
await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
).toHaveLength(0)
})
@@ -94,8 +102,8 @@ test.describe('Group Node', { tag: '@node' }, () => {
{ tag: '@screenshot' },
async ({ comfyPage }) => {
const groupNodeName = 'DefautWorkflowGroupNode'
await comfyPage.convertAllNodesToGroupNode(groupNodeName)
await comfyPage.doubleClickCanvas()
await comfyPage.nodeOps.convertAllNodesToGroupNode(groupNodeName)
await comfyPage.canvasOps.doubleClick()
await comfyPage.nextFrame()
await comfyPage.searchBox.fillAndSelectFirstNode(groupNodeName)
await expect(comfyPage.canvas).toHaveScreenshot(
@@ -105,8 +113,8 @@ test.describe('Group Node', { tag: '@node' }, () => {
)
test('Displays tooltip on title hover', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.EnableTooltips', true)
await comfyPage.convertAllNodesToGroupNode('Group Node')
await comfyPage.settings.setSetting('Comfy.EnableTooltips', true)
await comfyPage.nodeOps.convertAllNodesToGroupNode('Group Node')
await comfyPage.page.mouse.move(47, 173)
await expect(comfyPage.page.locator('.node-tooltip')).toBeVisible()
})
@@ -114,9 +122,9 @@ test.describe('Group Node', { tag: '@node' }, () => {
test('Manage group opens with the correct group selected', async ({
comfyPage
}) => {
const makeGroup = async (name, type1, type2) => {
const node1 = (await comfyPage.getNodeRefsByType(type1))[0]
const node2 = (await comfyPage.getNodeRefsByType(type2))[0]
const makeGroup = async (name: string, type1: string, type2: string) => {
const node1 = (await comfyPage.nodeOps.getNodeRefsByType(type1))[0]
const node2 = (await comfyPage.nodeOps.getNodeRefsByType(type2))[0]
await node1.click('title')
await node2.click('title', {
modifiers: ['Shift']
@@ -144,7 +152,7 @@ test.describe('Group Node', { tag: '@node' }, () => {
test('Preserves hidden input configuration when containing duplicate node types', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow(
await comfyPage.workflow.loadWorkflow(
'groupnodes/group_node_identical_nodes_hidden_inputs'
)
await comfyPage.nextFrame()
@@ -155,16 +163,14 @@ test.describe('Group Node', { tag: '@node' }, () => {
const totalInputCount = await comfyPage.page.evaluate((nodeName) => {
const {
extra: { groupNodes }
} = window['app'].graph
const { nodes } = groupNodes[nodeName]
return nodes.reduce((acc: number, node) => {
return acc + node.inputs.length
}, 0)
} = window.app!.graph!
const { nodes } = groupNodes![nodeName]
return nodes.reduce((acc, node) => acc + (node.inputs?.length ?? 0), 0)
}, groupNodeName)
const visibleInputCount = await comfyPage.page.evaluate((id) => {
const node = window['app'].graph.getNodeById(id)
return node.inputs.length
const node = window.app!.graph!.getNodeById(id)
return node!.inputs.length
}, groupNodeId)
// Verify there are 4 total inputs (2 VAE decode nodes with 2 inputs each)
@@ -178,7 +184,7 @@ test.describe('Group Node', { tag: '@node' }, () => {
comfyPage
}) => {
const expectSingleNode = async (type: string) => {
const nodes = await comfyPage.getNodeRefsByType(type)
const nodes = await comfyPage.nodeOps.getNodeRefsByType(type)
expect(nodes).toHaveLength(1)
return nodes[0]
}
@@ -213,8 +219,8 @@ test.describe('Group Node', { tag: '@node' }, () => {
test('Loads from a workflow using the legacy path separator ("/")', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('groupnodes/legacy_group_node')
expect(await comfyPage.getGraphNodesCount()).toBe(1)
await comfyPage.workflow.loadWorkflow('groupnodes/legacy_group_node')
expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe(1)
await expect(
comfyPage.page.locator('.comfy-missing-nodes')
).not.toBeVisible()
@@ -230,7 +236,7 @@ test.describe('Group Node', { tag: '@node' }, () => {
const isRegisteredLitegraph = async (comfyPage: ComfyPage) => {
return await comfyPage.page.evaluate((nodeType: string) => {
return !!window['LiteGraph'].registered_node_types[nodeType]
return !!window.LiteGraph!.registered_node_types[nodeType]
}, GROUP_NODE_TYPE)
}
@@ -246,17 +252,17 @@ test.describe('Group Node', { tag: '@node' }, () => {
comfyPage: ComfyPage,
expectedCount: number
) => {
expect(await comfyPage.getNodeRefsByType(GROUP_NODE_TYPE)).toHaveLength(
expectedCount
)
expect(
await comfyPage.nodeOps.getNodeRefsByType(GROUP_NODE_TYPE)
).toHaveLength(expectedCount)
expect(await isRegisteredLitegraph(comfyPage)).toBe(true)
expect(await isRegisteredNodeDefStore(comfyPage)).toBe(true)
}
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.loadWorkflow(WORKFLOW_NAME)
groupNode = await comfyPage.getFirstNodeRef()
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.workflow.loadWorkflow(WORKFLOW_NAME)
groupNode = await comfyPage.nodeOps.getFirstNodeRef()
if (!groupNode)
throw new Error(`Group node not found in workflow ${WORKFLOW_NAME}`)
await groupNode.copy()
@@ -265,7 +271,7 @@ test.describe('Group Node', { tag: '@node' }, () => {
test('Copies and pastes group node within the same workflow', async ({
comfyPage
}) => {
await comfyPage.ctrlV()
await comfyPage.clipboard.paste()
await verifyNodeLoaded(comfyPage, 2)
})
@@ -273,12 +279,12 @@ test.describe('Group Node', { tag: '@node' }, () => {
comfyPage
}) => {
// Set setting
await comfyPage.setSetting('Comfy.ConfirmClear', false)
await comfyPage.settings.setSetting('Comfy.ConfirmClear', false)
// Clear workflow
await comfyPage.executeCommand('Comfy.ClearWorkflow')
await comfyPage.command.executeCommand('Comfy.ClearWorkflow')
await comfyPage.ctrlV()
await comfyPage.clipboard.paste()
await verifyNodeLoaded(comfyPage, 1)
})
@@ -286,15 +292,15 @@ test.describe('Group Node', { tag: '@node' }, () => {
comfyPage
}) => {
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
await comfyPage.ctrlV()
await comfyPage.clipboard.paste()
await verifyNodeLoaded(comfyPage, 1)
})
test('Copies and pastes group node across different workflows', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('default')
await comfyPage.ctrlV()
await comfyPage.workflow.loadWorkflow('default')
await comfyPage.clipboard.paste()
await verifyNodeLoaded(comfyPage, 1)
})
@@ -302,14 +308,15 @@ test.describe('Group Node', { tag: '@node' }, () => {
comfyPage
}) => {
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
await comfyPage.ctrlV()
await comfyPage.clipboard.paste()
const currentGraphState = await comfyPage.page.evaluate(() =>
window['app'].graph.serialize()
window.app!.graph!.serialize()
)
await test.step('Load workflow containing a group node pasted from a different workflow', async () => {
await comfyPage.page.evaluate(
(workflow) => window['app'].loadGraphData(workflow),
(workflow) =>
window.app!.loadGraphData(workflow as ComfyWorkflowJSON),
currentGraphState
)
await comfyPage.nextFrame()
@@ -320,15 +327,18 @@ test.describe('Group Node', { tag: '@node' }, () => {
test.describe('Keybindings', () => {
test('Convert to group node, no selection', async ({ comfyPage }) => {
expect(await comfyPage.getVisibleToastCount()).toBe(0)
await expect.poll(() => comfyPage.toast.getVisibleToastCount()).toBe(0)
await comfyPage.page.keyboard.press('Alt+g')
expect(await comfyPage.getVisibleToastCount()).toBe(1)
await expect.poll(() => comfyPage.toast.getVisibleToastCount()).toBe(1)
})
test('Convert to group node, selected 1 node', async ({ comfyPage }) => {
expect(await comfyPage.getVisibleToastCount()).toBe(0)
await comfyPage.clickTextEncodeNode1()
await expect.poll(() => comfyPage.toast.getVisibleToastCount()).toBe(0)
await comfyPage.canvas.click({
position: DefaultGraphPositions.textEncodeNode1
})
await comfyPage.nextFrame()
await comfyPage.page.keyboard.press('Alt+g')
expect(await comfyPage.getVisibleToastCount()).toBe(1)
await expect.poll(() => comfyPage.toast.getVisibleToastCount()).toBe(1)
})
})
})

View File

@@ -7,15 +7,17 @@ import {
testComfySnapToGridGridSize
} from '../fixtures/ComfyPage'
import type { ComfyPage } from '../fixtures/ComfyPage'
import { DefaultGraphPositions } from '../fixtures/constants/defaultGraphPositions'
import { TestIds } from '../fixtures/selectors'
import type { NodeReference } from '../fixtures/utils/litegraphUtils'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Item Interaction', { tag: ['@screenshot', '@node'] }, () => {
test('Can select/delete all items', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('groups/mixed_graph_items')
await comfyPage.workflow.loadWorkflow('groups/mixed_graph_items')
await comfyPage.canvas.press('Control+a')
await expect(comfyPage.canvas).toHaveScreenshot('selected-all.png')
await comfyPage.canvas.press('Delete')
@@ -23,7 +25,7 @@ test.describe('Item Interaction', { tag: ['@screenshot', '@node'] }, () => {
})
test('Can pin/unpin items with keyboard shortcut', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('groups/mixed_graph_items')
await comfyPage.workflow.loadWorkflow('groups/mixed_graph_items')
await comfyPage.canvas.press('Control+a')
await comfyPage.canvas.press('KeyP')
await comfyPage.nextFrame()
@@ -51,11 +53,13 @@ test.describe('Node Interaction', () => {
test(`Can add multiple nodes to selection using ${modifier}+Click`, async ({
comfyPage
}) => {
const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode')
const clipNodes =
await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
for (const node of clipNodes) {
await node.click('title', { modifiers: [modifier] })
}
const selectedNodeCount = await comfyPage.getSelectedGraphNodesCount()
const selectedNodeCount =
await comfyPage.nodeOps.getSelectedGraphNodesCount()
expect(selectedNodeCount).toBe(clipNodes.length)
})
})
@@ -65,9 +69,15 @@ test.describe('Node Interaction', () => {
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await expect(comfyPage.canvas).toHaveScreenshot('default.png')
await comfyPage.clickTextEncodeNode1()
await comfyPage.canvas.click({
position: DefaultGraphPositions.textEncodeNode1
})
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('selected-node1.png')
await comfyPage.clickTextEncodeNode2()
await comfyPage.canvas.click({
position: DefaultGraphPositions.textEncodeNode2
})
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('selected-node2.png')
}
)
@@ -80,7 +90,7 @@ test.describe('Node Interaction', () => {
const clipNode2Pos = await clipNodes[1].getPosition()
const offset = 64
await comfyPage.page.keyboard.down('Meta')
await comfyPage.dragAndDrop(
await comfyPage.canvasOps.dragAndDrop(
{
x: Math.min(clipNode1Pos.x, clipNode2Pos.x) - offset,
y: Math.min(clipNode1Pos.y, clipNode2Pos.y) - offset
@@ -94,9 +104,10 @@ test.describe('Node Interaction', () => {
}
test('Can drag-select nodes with Meta (mac)', async ({ comfyPage }) => {
const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode')
const clipNodes =
await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
await dragSelectNodes(comfyPage, clipNodes)
expect(await comfyPage.getSelectedGraphNodesCount()).toBe(
expect(await comfyPage.nodeOps.getSelectedGraphNodesCount()).toBe(
clipNodes.length
)
})
@@ -104,7 +115,8 @@ test.describe('Node Interaction', () => {
test('Can move selected nodes using the Comfy.Canvas.MoveSelectedNodes.{Up|Down|Left|Right} commands', async ({
comfyPage
}) => {
const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode')
const clipNodes =
await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
const getPositions = () =>
Promise.all(clipNodes.map((node) => node.getPosition()))
const testDirection = async ({
@@ -116,7 +128,7 @@ test.describe('Node Interaction', () => {
}) => {
const originalPositions = await getPositions()
await dragSelectNodes(comfyPage, clipNodes)
await comfyPage.executeCommand(
await comfyPage.command.executeCommand(
`Comfy.Canvas.MoveSelectedNodes.${direction}`
)
await comfyPage.canvas.press(`Control+Arrow${direction}`)
@@ -155,14 +167,20 @@ test.describe('Node Interaction', () => {
})
test('Can drag node', { tag: '@screenshot' }, async ({ comfyPage }) => {
await comfyPage.dragNode2()
await comfyPage.nodeOps.dragTextEncodeNode2()
await expect(comfyPage.canvas).toHaveScreenshot('dragged-node1.png')
})
test.describe('Edge Interaction', { tag: '@screenshot' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.LinkRelease.Action', 'no action')
await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'no action')
await comfyPage.settings.setSetting(
'Comfy.LinkRelease.Action',
'no action'
)
await comfyPage.settings.setSetting(
'Comfy.LinkRelease.ActionShift',
'no action'
)
})
// Test both directions of edge connection.
@@ -170,11 +188,11 @@ test.describe('Node Interaction', () => {
test(`Can disconnect/connect edge ${reverse ? 'reverse' : 'normal'}`, async ({
comfyPage
}) => {
await comfyPage.disconnectEdge()
await comfyPage.canvasOps.disconnectEdge()
await expect(comfyPage.canvas).toHaveScreenshot('disconnected-edge.png')
await comfyPage.connectEdge({ reverse })
await comfyPage.canvasOps.connectEdge({ reverse })
// Move mouse to empty area to avoid slot highlight.
await comfyPage.moveMouseToEmptyArea()
await comfyPage.canvasOps.moveMouseToEmptyArea()
// Litegraph renders edge with a slight offset.
await expect(comfyPage.canvas).toHaveScreenshot('default.png', {
maxDiffPixels: 50
@@ -183,14 +201,14 @@ test.describe('Node Interaction', () => {
})
test('Can move link', async ({ comfyPage }) => {
await comfyPage.dragAndDrop(
comfyPage.clipTextEncodeNode1InputSlot,
comfyPage.emptySpace
await comfyPage.canvasOps.dragAndDrop(
DefaultGraphPositions.clipTextEncodeNode1InputSlot,
DefaultGraphPositions.emptySpace
)
await expect(comfyPage.canvas).toHaveScreenshot('disconnected-edge.png')
await comfyPage.dragAndDrop(
comfyPage.clipTextEncodeNode2InputSlot,
comfyPage.clipTextEncodeNode1InputSlot
await comfyPage.canvasOps.dragAndDrop(
DefaultGraphPositions.clipTextEncodeNode2InputSlot,
DefaultGraphPositions.clipTextEncodeNode1InputSlot
)
await expect(comfyPage.canvas).toHaveScreenshot('moved-link.png')
})
@@ -199,15 +217,15 @@ test.describe('Node Interaction', () => {
test.skip('Can copy link by shift-drag existing link', async ({
comfyPage
}) => {
await comfyPage.dragAndDrop(
comfyPage.clipTextEncodeNode1InputSlot,
comfyPage.emptySpace
await comfyPage.canvasOps.dragAndDrop(
DefaultGraphPositions.clipTextEncodeNode1InputSlot,
DefaultGraphPositions.emptySpace
)
await expect(comfyPage.canvas).toHaveScreenshot('disconnected-edge.png')
await comfyPage.page.keyboard.down('Shift')
await comfyPage.dragAndDrop(
comfyPage.clipTextEncodeNode2InputLinkPath,
comfyPage.clipTextEncodeNode1InputSlot
await comfyPage.canvasOps.dragAndDrop(
DefaultGraphPositions.clipTextEncodeNode2InputLinkPath,
DefaultGraphPositions.clipTextEncodeNode1InputSlot
)
await comfyPage.page.keyboard.up('Shift')
await expect(comfyPage.canvas).toHaveScreenshot('copied-link.png')
@@ -217,11 +235,11 @@ test.describe('Node Interaction', () => {
comfyPage,
comfyMouse
}) => {
await comfyPage.setSetting('Comfy.Node.AutoSnapLinkToSlot', true)
await comfyPage.setSetting('Comfy.Node.SnapHighlightsNode', true)
await comfyPage.settings.setSetting('Comfy.Node.AutoSnapLinkToSlot', true)
await comfyPage.settings.setSetting('Comfy.Node.SnapHighlightsNode', true)
await comfyMouse.move(comfyPage.clipTextEncodeNode1InputSlot)
await comfyMouse.drag(comfyPage.clipTextEncodeNode2InputSlot)
await comfyMouse.move(DefaultGraphPositions.clipTextEncodeNode1InputSlot)
await comfyMouse.drag(DefaultGraphPositions.clipTextEncodeNode2InputSlot)
await expect(comfyPage.canvas).toHaveScreenshot('snapped-highlighted.png')
})
})
@@ -230,7 +248,7 @@ test.describe('Node Interaction', () => {
'Can adjust widget value',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.adjustWidgetValue()
await comfyPage.nodeOps.adjustEmptyLatentWidth()
await expect(comfyPage.canvas).toHaveScreenshot(
'adjusted-widget-value.png'
)
@@ -238,7 +256,7 @@ test.describe('Node Interaction', () => {
)
test('Link snap to slot', { tag: '@screenshot' }, async ({ comfyPage }) => {
await comfyPage.loadWorkflow('links/snap_to_slot')
await comfyPage.workflow.loadWorkflow('links/snap_to_slot')
await expect(comfyPage.canvas).toHaveScreenshot('snap_to_slot.png')
const outputSlotPos = {
@@ -249,7 +267,7 @@ test.describe('Node Interaction', () => {
x: 748,
y: 77
}
await comfyPage.dragAndDrop(outputSlotPos, samplerNodeCenterPos)
await comfyPage.canvasOps.dragAndDrop(outputSlotPos, samplerNodeCenterPos)
await expect(comfyPage.canvas).toHaveScreenshot('snap_to_slot_linked.png')
})
@@ -258,7 +276,7 @@ test.describe('Node Interaction', () => {
'Can batch move links by drag with shift',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.loadWorkflow('links/batch_move_links')
await comfyPage.workflow.loadWorkflow('links/batch_move_links')
await expect(comfyPage.canvas).toHaveScreenshot('batch_move_links.png')
const outputSlot1Pos = {
@@ -271,7 +289,7 @@ test.describe('Node Interaction', () => {
}
await comfyPage.page.keyboard.down('Shift')
await comfyPage.dragAndDrop(outputSlot1Pos, outputSlot2Pos)
await comfyPage.canvasOps.dragAndDrop(outputSlot1Pos, outputSlot2Pos)
await comfyPage.page.keyboard.up('Shift')
await expect(comfyPage.canvas).toHaveScreenshot(
@@ -304,12 +322,18 @@ test.describe('Node Interaction', () => {
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await expect(comfyPage.canvas).toHaveScreenshot('default.png')
await comfyPage.clickTextEncodeNodeToggler()
await comfyPage.canvas.click({
position: DefaultGraphPositions.textEncodeNodeToggler
})
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'text-encode-toggled-off.png'
)
await comfyPage.delay(1000)
await comfyPage.clickTextEncodeNodeToggler()
await comfyPage.canvas.click({
position: DefaultGraphPositions.textEncodeNodeToggler
})
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'text-encode-toggled-back-open.png'
)
@@ -345,7 +369,7 @@ test.describe('Node Interaction', () => {
x: 167,
y: 143
}
await comfyPage.loadWorkflow('nodes/single_save_image_node')
await comfyPage.workflow.loadWorkflow('nodes/single_save_image_node')
await comfyPage.canvas.click({
position: textWidgetPos
})
@@ -365,7 +389,7 @@ test.describe('Node Interaction', () => {
'Can double click node title to edit',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.loadWorkflow('nodes/single_ksampler')
await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
await comfyPage.canvas.dblclick({
position: {
x: 50,
@@ -382,7 +406,7 @@ test.describe('Node Interaction', () => {
test('Double click node body does not trigger edit', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('nodes/single_ksampler')
await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
await comfyPage.canvas.dblclick({
position: {
x: 50,
@@ -397,8 +421,11 @@ test.describe('Node Interaction', () => {
'Can group selected nodes',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.GroupSelectedNodes.Padding', 10)
await comfyPage.select2Nodes()
await comfyPage.settings.setSetting(
'Comfy.GroupSelectedNodes.Padding',
10
)
await comfyPage.nodeOps.selectNodes(['CLIP Text Encode (Prompt)'])
await comfyPage.page.keyboard.down('Control')
await comfyPage.page.keyboard.press('KeyG')
await comfyPage.page.keyboard.up('Control')
@@ -416,10 +443,10 @@ test.describe('Node Interaction', () => {
'Can fit group to contents',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.loadWorkflow('groups/oversized_group')
await comfyPage.ctrlA()
await comfyPage.workflow.loadWorkflow('groups/oversized_group')
await comfyPage.keyboard.selectAll()
await comfyPage.nextFrame()
await comfyPage.executeCommand('Comfy.Graph.FitGroupToContents')
await comfyPage.command.executeCommand('Comfy.Graph.FitGroupToContents')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'group-fit-to-contents.png'
@@ -428,11 +455,15 @@ test.describe('Node Interaction', () => {
)
test('Can pin/unpin nodes', { tag: '@screenshot' }, async ({ comfyPage }) => {
await comfyPage.select2Nodes()
await comfyPage.executeCommand('Comfy.Canvas.ToggleSelectedNodes.Pin')
await comfyPage.nodeOps.selectNodes(['CLIP Text Encode (Prompt)'])
await comfyPage.command.executeCommand(
'Comfy.Canvas.ToggleSelectedNodes.Pin'
)
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('nodes-pinned.png')
await comfyPage.executeCommand('Comfy.Canvas.ToggleSelectedNodes.Pin')
await comfyPage.command.executeCommand(
'Comfy.Canvas.ToggleSelectedNodes.Pin'
)
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('nodes-unpinned.png')
})
@@ -441,7 +472,7 @@ test.describe('Node Interaction', () => {
'Can bypass/unbypass nodes with keyboard shortcut',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.select2Nodes()
await comfyPage.nodeOps.selectNodes(['CLIP Text Encode (Prompt)'])
await comfyPage.canvas.press('Control+b')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('nodes-bypassed.png')
@@ -454,7 +485,7 @@ test.describe('Node Interaction', () => {
test.describe('Group Interaction', { tag: '@screenshot' }, () => {
test('Can double click group title to edit', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('groups/single_group')
await comfyPage.workflow.loadWorkflow('groups/single_group')
await comfyPage.canvas.dblclick({
position: {
x: 50,
@@ -470,16 +501,16 @@ test.describe('Group Interaction', { tag: '@screenshot' }, () => {
test.describe('Canvas Interaction', { tag: '@screenshot' }, () => {
test('Can zoom in/out', async ({ comfyPage }) => {
await comfyPage.zoom(-100)
await comfyPage.canvasOps.zoom(-100)
await expect(comfyPage.canvas).toHaveScreenshot('zoomed-in.png')
await comfyPage.zoom(200)
await comfyPage.canvasOps.zoom(200)
await expect(comfyPage.canvas).toHaveScreenshot('zoomed-out.png')
})
test('Can zoom very far out', async ({ comfyPage }) => {
await comfyPage.zoom(100, 12)
await comfyPage.canvasOps.zoom(100, 12)
await expect(comfyPage.canvas).toHaveScreenshot('zoomed-very-far-out.png')
await comfyPage.zoom(-100, 12)
await comfyPage.canvasOps.zoom(-100, 12)
await expect(comfyPage.canvas).toHaveScreenshot('zoomed-back-in.png')
})
@@ -488,11 +519,11 @@ test.describe('Canvas Interaction', { tag: '@screenshot' }, () => {
}) => {
await comfyPage.page.keyboard.down('Control')
await comfyPage.page.keyboard.down('Shift')
await comfyPage.dragAndDrop({ x: 10, y: 100 }, { x: 10, y: 40 })
await comfyPage.canvasOps.dragAndDrop({ x: 10, y: 100 }, { x: 10, y: 40 })
await expect(comfyPage.canvas).toHaveScreenshot('zoomed-in-ctrl-shift.png')
await comfyPage.dragAndDrop({ x: 10, y: 40 }, { x: 10, y: 160 })
await comfyPage.canvasOps.dragAndDrop({ x: 10, y: 40 }, { x: 10, y: 160 })
await expect(comfyPage.canvas).toHaveScreenshot('zoomed-out-ctrl-shift.png')
await comfyPage.dragAndDrop({ x: 10, y: 280 }, { x: 10, y: 220 })
await comfyPage.canvasOps.dragAndDrop({ x: 10, y: 280 }, { x: 10, y: 220 })
await expect(comfyPage.canvas).toHaveScreenshot(
'zoomed-default-ctrl-shift.png'
)
@@ -503,35 +534,35 @@ test.describe('Canvas Interaction', { tag: '@screenshot' }, () => {
test('Can zoom in/out after decreasing canvas zoom speed setting', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', 1.05)
await comfyPage.zoom(-100, 4)
await comfyPage.settings.setSetting('Comfy.Graph.ZoomSpeed', 1.05)
await comfyPage.canvasOps.zoom(-100, 4)
await expect(comfyPage.canvas).toHaveScreenshot(
'zoomed-in-low-zoom-speed.png'
)
await comfyPage.zoom(100, 8)
await comfyPage.canvasOps.zoom(100, 8)
await expect(comfyPage.canvas).toHaveScreenshot(
'zoomed-out-low-zoom-speed.png'
)
await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', 1.1)
await comfyPage.settings.setSetting('Comfy.Graph.ZoomSpeed', 1.1)
})
test('Can zoom in/out after increasing canvas zoom speed', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', 1.5)
await comfyPage.zoom(-100, 4)
await comfyPage.settings.setSetting('Comfy.Graph.ZoomSpeed', 1.5)
await comfyPage.canvasOps.zoom(-100, 4)
await expect(comfyPage.canvas).toHaveScreenshot(
'zoomed-in-high-zoom-speed.png'
)
await comfyPage.zoom(100, 8)
await comfyPage.canvasOps.zoom(100, 8)
await expect(comfyPage.canvas).toHaveScreenshot(
'zoomed-out-high-zoom-speed.png'
)
await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', 1.1)
await comfyPage.settings.setSetting('Comfy.Graph.ZoomSpeed', 1.1)
})
test('Can pan', async ({ comfyPage }) => {
await comfyPage.pan({ x: 200, y: 200 })
await comfyPage.canvasOps.pan({ x: 200, y: 200 })
await expect(comfyPage.canvas).toHaveScreenshot('panned.png')
})
@@ -602,9 +633,9 @@ test.describe('Canvas Interaction', { tag: '@screenshot' }, () => {
})
test('Can pan when dragging a link', async ({ comfyPage, comfyMouse }) => {
const posSlot1 = comfyPage.clipTextEncodeNode1InputSlot
const posSlot1 = DefaultGraphPositions.clipTextEncodeNode1InputSlot
await comfyMouse.move(posSlot1)
const posEmpty = comfyPage.emptySpace
const posEmpty = DefaultGraphPositions.emptySpace
await comfyMouse.drag(posEmpty)
await expect(comfyPage.canvas).toHaveScreenshot('dragging-link1.png')
@@ -623,23 +654,23 @@ test.describe('Canvas Interaction', { tag: '@screenshot' }, () => {
test('Can pan very far and back', async ({ comfyPage }) => {
// intentionally slice the edge of where the clip text encode dom widgets are
await comfyPage.pan({ x: -800, y: -300 }, { x: 1000, y: 10 })
await comfyPage.canvasOps.pan({ x: -800, y: -300 }, { x: 1000, y: 10 })
await expect(comfyPage.canvas).toHaveScreenshot('panned-step-one.png')
await comfyPage.pan({ x: -200, y: 0 }, { x: 1000, y: 10 })
await comfyPage.canvasOps.pan({ x: -200, y: 0 }, { x: 1000, y: 10 })
await expect(comfyPage.canvas).toHaveScreenshot('panned-step-two.png')
await comfyPage.pan({ x: -2200, y: -2200 }, { x: 1000, y: 10 })
await comfyPage.canvasOps.pan({ x: -2200, y: -2200 }, { x: 1000, y: 10 })
await expect(comfyPage.canvas).toHaveScreenshot('panned-far-away.png')
await comfyPage.pan({ x: 2200, y: 2200 }, { x: 1000, y: 10 })
await comfyPage.canvasOps.pan({ x: 2200, y: 2200 }, { x: 1000, y: 10 })
await expect(comfyPage.canvas).toHaveScreenshot('panned-back-from-far.png')
await comfyPage.pan({ x: 200, y: 0 }, { x: 1000, y: 10 })
await comfyPage.canvasOps.pan({ x: 200, y: 0 }, { x: 1000, y: 10 })
await expect(comfyPage.canvas).toHaveScreenshot('panned-back-to-two.png')
await comfyPage.pan({ x: 800, y: 300 }, { x: 1000, y: 10 })
await comfyPage.canvasOps.pan({ x: 800, y: 300 }, { x: 1000, y: 10 })
await expect(comfyPage.canvas).toHaveScreenshot('panned-back-to-one.png')
})
test('@mobile Can pan with touch', async ({ comfyPage }) => {
await comfyPage.closeMenu()
await comfyPage.panWithTouch({ x: 200, y: 200 })
await comfyPage.canvasOps.panWithTouch({ x: 200, y: 200 })
await expect(comfyPage.canvas).toHaveScreenshot('panned-touch.png')
})
})
@@ -652,41 +683,41 @@ test.describe('Widget Interaction', () => {
await expect(textBox).toHaveValue('')
await textBox.fill('Hello World')
await expect(textBox).toHaveValue('Hello World')
await comfyPage.ctrlZ(null)
await comfyPage.keyboard.undo(null)
await expect(textBox).toHaveValue('')
})
test('Undo attention edit', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.EditAttention.Delta', 0.05)
await comfyPage.settings.setSetting('Comfy.EditAttention.Delta', 0.05)
const textBox = comfyPage.widgetTextBox
await textBox.click()
await textBox.fill('1girl')
await expect(textBox).toHaveValue('1girl')
await textBox.selectText()
await comfyPage.ctrlArrowUp(null)
await comfyPage.keyboard.moveUp(null)
await expect(textBox).toHaveValue('(1girl:1.05)')
await comfyPage.ctrlZ(null)
await comfyPage.keyboard.undo(null)
await expect(textBox).toHaveValue('1girl')
})
})
test.describe('Load workflow', { tag: '@screenshot' }, () => {
test('Can load workflow with string node id', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('nodes/string_node_id')
await comfyPage.workflow.loadWorkflow('nodes/string_node_id')
await expect(comfyPage.canvas).toHaveScreenshot('string_node_id.png')
})
test('Can load workflow with ("STRING",) input node', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('inputs/string_input')
await comfyPage.workflow.loadWorkflow('inputs/string_input')
await expect(comfyPage.canvas).toHaveScreenshot('string_input.png')
})
test('Restore workflow on reload (switch workflow)', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('nodes/single_ksampler')
await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler.png')
await comfyPage.setup({ clearStorage: false })
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler.png')
@@ -695,10 +726,10 @@ test.describe('Load workflow', { tag: '@screenshot' }, () => {
test('Restore workflow on reload (modify workflow)', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('nodes/single_ksampler')
const node = (await comfyPage.getFirstNodeRef())!
await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
const node = (await comfyPage.nodeOps.getFirstNodeRef())!
await node.click('collapse')
await comfyPage.clickEmptySpace()
await comfyPage.canvasOps.clickEmptySpace()
await expect(comfyPage.canvas).toHaveScreenshot(
'single_ksampler_modified.png'
)
@@ -716,7 +747,7 @@ test.describe('Load workflow', { tag: '@screenshot' }, () => {
`${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}${extension}`
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
workflowA = generateUniqueFilename()
await comfyPage.menu.topbar.saveWorkflow(workflowA)
@@ -734,7 +765,7 @@ test.describe('Load workflow', { tag: '@screenshot' }, () => {
test('Restores topbar workflow tabs after reload', async ({
comfyPage
}) => {
await comfyPage.setSetting(
await comfyPage.settings.setSetting(
'Comfy.Workflow.WorkflowTabsPosition',
'Topbar'
)
@@ -747,7 +778,7 @@ test.describe('Load workflow', { tag: '@screenshot' }, () => {
})
test('Restores sidebar workflows after reload', async ({ comfyPage }) => {
await comfyPage.setSetting(
await comfyPage.settings.setSetting(
'Comfy.Workflow.WorkflowTabsPosition',
'Sidebar'
)
@@ -770,34 +801,40 @@ test.describe('Load workflow', { tag: '@screenshot' }, () => {
})
test('Auto fit view after loading workflow', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.EnableWorkflowViewRestore', false)
await comfyPage.loadWorkflow('nodes/single_ksampler')
await comfyPage.settings.setSetting(
'Comfy.EnableWorkflowViewRestore',
false
)
await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler_fit.png')
})
})
test.describe('Load duplicate workflow', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
})
test('A workflow can be loaded multiple times in a row', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('nodes/single_ksampler')
await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
await comfyPage.menu.workflowsTab.open()
await comfyPage.executeCommand('Comfy.NewBlankWorkflow')
await comfyPage.loadWorkflow('nodes/single_ksampler')
expect(await comfyPage.getGraphNodesCount()).toBe(1)
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe(1)
})
})
test.describe('Viewport settings', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting('Comfy.Workflow.WorkflowTabsPosition', 'Topbar')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting(
'Comfy.Workflow.WorkflowTabsPosition',
'Topbar'
)
await comfyPage.setupWorkflowsDirectory({})
await comfyPage.workflow.setupWorkflowsDirectory({})
})
test('Keeps viewport settings when changing tabs', async ({
@@ -807,7 +844,7 @@ test.describe('Viewport settings', () => {
const changeTab = async (tab: Locator) => {
await tab.click()
await comfyPage.nextFrame()
await comfyMouse.move(comfyPage.emptySpace)
await comfyMouse.move(DefaultGraphPositions.emptySpace)
// If tooltip is visible, wait for it to hide
await expect(
@@ -816,11 +853,13 @@ test.describe('Viewport settings', () => {
}
// Screenshot the canvas element
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true)
await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', true)
const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button')
const toggleButton = comfyPage.page.getByTestId(
TestIds.canvas.toggleMinimapButton
)
await toggleButton.click()
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false)
await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', false)
await comfyPage.menu.topbar.saveWorkflow('Workflow A')
await comfyPage.nextFrame()
@@ -837,7 +876,7 @@ test.describe('Viewport settings', () => {
const tabB = comfyPage.menu.topbar.getWorkflowTab('Workflow B')
await changeTab(tabB)
await comfyMouse.move(comfyPage.emptySpace)
await comfyMouse.move(DefaultGraphPositions.emptySpace)
for (let i = 0; i < 4; i++) {
await comfyMouse.wheel(0, 60)
}
@@ -865,13 +904,19 @@ test.describe('Viewport settings', () => {
test.describe('Canvas Navigation', { tag: '@screenshot' }, () => {
test.describe('Legacy Mode', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Canvas.NavigationMode', 'legacy')
await comfyPage.settings.setSetting(
'Comfy.Canvas.NavigationMode',
'legacy'
)
})
test('Left-click drag in empty area should pan canvas', async ({
comfyPage
}) => {
await comfyPage.dragAndDrop({ x: 50, y: 50 }, { x: 150, y: 150 })
await comfyPage.canvasOps.dragAndDrop(
{ x: 50, y: 50 },
{ x: 150, y: 150 }
)
await expect(comfyPage.canvas).toHaveScreenshot(
'legacy-left-drag-pan.png'
)
@@ -904,8 +949,11 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => {
})
test('Left-click on node should not pan canvas', async ({ comfyPage }) => {
await comfyPage.clickTextEncodeNode1()
const selectedCount = await comfyPage.getSelectedGraphNodesCount()
await comfyPage.canvas.click({
position: DefaultGraphPositions.textEncodeNode1
})
await comfyPage.nextFrame()
const selectedCount = await comfyPage.nodeOps.getSelectedGraphNodesCount()
expect(selectedCount).toBe(1)
await expect(comfyPage.canvas).toHaveScreenshot(
'legacy-click-node-select.png'
@@ -915,18 +963,22 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => {
test.describe('Standard Mode', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Canvas.NavigationMode', 'standard')
await comfyPage.settings.setSetting(
'Comfy.Canvas.NavigationMode',
'standard'
)
})
test('Left-click drag in empty area should select nodes', async ({
comfyPage
}) => {
const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode')
const clipNodes =
await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
const clipNode1Pos = await clipNodes[0].getPosition()
const clipNode2Pos = await clipNodes[1].getPosition()
const offset = 64
await comfyPage.dragAndDrop(
await comfyPage.canvasOps.dragAndDrop(
{
x: Math.min(clipNode1Pos.x, clipNode2Pos.x) - offset,
y: Math.min(clipNode1Pos.y, clipNode2Pos.y) - offset
@@ -937,7 +989,7 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => {
}
)
const selectedCount = await comfyPage.getSelectedGraphNodesCount()
const selectedCount = await comfyPage.nodeOps.getSelectedGraphNodesCount()
expect(selectedCount).toBe(clipNodes.length)
await expect(comfyPage.canvas).toHaveScreenshot(
'standard-left-drag-select.png'
@@ -977,8 +1029,11 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => {
test('Left-click on node should select node (not start selection box)', async ({
comfyPage
}) => {
await comfyPage.clickTextEncodeNode1()
const selectedCount = await comfyPage.getSelectedGraphNodesCount()
await comfyPage.canvas.click({
position: DefaultGraphPositions.textEncodeNode1
})
await comfyPage.nextFrame()
const selectedCount = await comfyPage.nodeOps.getSelectedGraphNodesCount()
expect(selectedCount).toBe(1)
await expect(comfyPage.canvas).toHaveScreenshot(
'standard-click-node-select.png'
@@ -991,7 +1046,10 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => {
await comfyPage.nextFrame()
await comfyPage.page.keyboard.down('Space')
await comfyPage.dragAndDrop({ x: 50, y: 50 }, { x: 150, y: 150 })
await comfyPage.canvasOps.dragAndDrop(
{ x: 50, y: 50 },
{ x: 150, y: 150 }
)
await comfyPage.page.keyboard.up('Space')
await expect(comfyPage.canvas).toHaveScreenshot(
'standard-space-drag-pan.png'
@@ -1001,11 +1059,12 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => {
test('Space key overrides default left-click behavior', async ({
comfyPage
}) => {
const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode')
const clipNodes =
await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
const clipNode1Pos = await clipNodes[0].getPosition()
const offset = 64
await comfyPage.dragAndDrop(
await comfyPage.canvasOps.dragAndDrop(
{
x: clipNode1Pos.x - offset,
y: clipNode1Pos.y - offset
@@ -1017,16 +1076,16 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => {
)
const selectedCountAfterDrag =
await comfyPage.getSelectedGraphNodesCount()
await comfyPage.nodeOps.getSelectedGraphNodesCount()
expect(selectedCountAfterDrag).toBeGreaterThan(0)
await comfyPage.clickEmptySpace()
await comfyPage.canvasOps.clickEmptySpace()
const selectedCountAfterClear =
await comfyPage.getSelectedGraphNodesCount()
await comfyPage.nodeOps.getSelectedGraphNodesCount()
expect(selectedCountAfterClear).toBe(0)
await comfyPage.page.keyboard.down('Space')
await comfyPage.dragAndDrop(
await comfyPage.canvasOps.dragAndDrop(
{
x: clipNode1Pos.x - offset,
y: clipNode1Pos.y - offset
@@ -1039,7 +1098,7 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => {
await comfyPage.page.keyboard.up('Space')
const selectedCountAfterSpaceDrag =
await comfyPage.getSelectedGraphNodesCount()
await comfyPage.nodeOps.getSelectedGraphNodesCount()
expect(selectedCountAfterSpaceDrag).toBe(0)
})
})
@@ -1047,7 +1106,10 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => {
test('Shift + mouse wheel should pan canvas horizontally', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Canvas.MouseWheelScroll', 'panning')
await comfyPage.settings.setSetting(
'Comfy.Canvas.MouseWheelScroll',
'panning'
)
await comfyPage.page.click('canvas')
await comfyPage.nextFrame()
@@ -1085,11 +1147,17 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => {
test('Multiple modifier keys work correctly in legacy mode', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Canvas.NavigationMode', 'legacy')
await comfyPage.settings.setSetting(
'Comfy.Canvas.NavigationMode',
'legacy'
)
await comfyPage.page.keyboard.down('Alt')
await comfyPage.page.keyboard.down('Shift')
await comfyPage.dragAndDrop({ x: 50, y: 50 }, { x: 150, y: 150 })
await comfyPage.canvasOps.dragAndDrop(
{ x: 50, y: 50 },
{ x: 150, y: 150 }
)
await comfyPage.page.keyboard.up('Shift')
await comfyPage.page.keyboard.up('Alt')
@@ -1109,7 +1177,10 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => {
})
}
await comfyPage.setSetting('Comfy.Canvas.NavigationMode', 'legacy')
await comfyPage.settings.setSetting(
'Comfy.Canvas.NavigationMode',
'legacy'
)
await comfyPage.page.mouse.move(50, 50)
await comfyPage.page.mouse.down()
expect(await getCursorStyle()).toBe('grabbing')

View File

@@ -3,22 +3,22 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Keybindings', { tag: '@keyboard' }, () => {
test('Should not trigger non-modifier keybinding when typing in input fields', async ({
comfyPage
}) => {
await comfyPage.registerKeybinding({ key: 'k' }, () => {
window['TestCommand'] = true
await comfyPage.command.registerKeybinding({ key: 'k' }, () => {
window.TestCommand = true
})
const textBox = comfyPage.widgetTextBox
await textBox.click()
await textBox.fill('k')
await expect(textBox).toHaveValue('k')
expect(await comfyPage.page.evaluate(() => window['TestCommand'])).toBe(
expect(await comfyPage.page.evaluate(() => window.TestCommand)).toBe(
undefined
)
})
@@ -26,8 +26,8 @@ test.describe('Keybindings', { tag: '@keyboard' }, () => {
test('Should not trigger modifier keybinding when typing in input fields', async ({
comfyPage
}) => {
await comfyPage.registerKeybinding({ key: 'k', ctrl: true }, () => {
window['TestCommand'] = true
await comfyPage.command.registerKeybinding({ key: 'k', ctrl: true }, () => {
window.TestCommand = true
})
const textBox = comfyPage.widgetTextBox
@@ -35,23 +35,21 @@ test.describe('Keybindings', { tag: '@keyboard' }, () => {
await textBox.fill('q')
await textBox.press('Control+k')
await expect(textBox).toHaveValue('q')
expect(await comfyPage.page.evaluate(() => window['TestCommand'])).toBe(
true
)
expect(await comfyPage.page.evaluate(() => window.TestCommand)).toBe(true)
})
test('Should not trigger keybinding reserved by text input when typing in input fields', async ({
comfyPage
}) => {
await comfyPage.registerKeybinding({ key: 'Ctrl+v' }, () => {
window['TestCommand'] = true
await comfyPage.command.registerKeybinding({ key: 'Ctrl+v' }, () => {
window.TestCommand = true
})
const textBox = comfyPage.widgetTextBox
await textBox.click()
await textBox.press('Control+v')
await expect(textBox).toBeFocused()
expect(await comfyPage.page.evaluate(() => window['TestCommand'])).toBe(
expect(await comfyPage.page.evaluate(() => window.TestCommand)).toBe(
undefined
)
})

View File

@@ -3,7 +3,7 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
function listenForEvent(): Promise<Event> {
@@ -17,7 +17,7 @@ function listenForEvent(): Promise<Event> {
test.describe('Canvas Event', { tag: '@canvas' }, () => {
test('Emit litegraph:canvas empty-release', async ({ comfyPage }) => {
const eventPromise = comfyPage.page.evaluate(listenForEvent)
const disconnectPromise = comfyPage.disconnectEdge()
const disconnectPromise = comfyPage.canvasOps.disconnectEdge()
const event = await eventPromise
await disconnectPromise
@@ -29,7 +29,7 @@ test.describe('Canvas Event', { tag: '@canvas' }, () => {
test('Emit litegraph:canvas empty-double-click', async ({ comfyPage }) => {
const eventPromise = comfyPage.page.evaluate(listenForEvent)
const doubleClickPromise = comfyPage.doubleClickCanvas()
const doubleClickPromise = comfyPage.canvasOps.doubleClick()
const event = await eventPromise
await doubleClickPromise

View File

@@ -3,7 +3,7 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe(
@@ -33,7 +33,7 @@ test.describe(
test(`Load workflow in ${fileName} (drop from filesystem)`, async ({
comfyPage
}) => {
await comfyPage.dragAndDropFile(`workflowInMedia/${fileName}`)
await comfyPage.dragDrop.dragAndDropFile(`workflowInMedia/${fileName}`)
await expect(comfyPage.canvas).toHaveScreenshot(`${fileName}.png`)
})
})
@@ -45,7 +45,7 @@ test.describe(
test(`Load workflow from URL ${url} (drop from different browser tabs)`, async ({
comfyPage
}) => {
await comfyPage.dragAndDropURL(url)
await comfyPage.dragDrop.dragAndDropURL(url)
const readableName = url.split('/').pop()
await expect(comfyPage.canvas).toHaveScreenshot(
`dropped_workflow_url_${readableName}.png`

View File

@@ -3,7 +3,7 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => {
@@ -11,11 +11,11 @@ test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => {
comfyPage
}) => {
// Load a workflow with some nodes to render
await comfyPage.loadWorkflow('default')
await comfyPage.workflow.loadWorkflow('default')
// Get initial LOD state and settings
const initialState = await comfyPage.page.evaluate(() => {
const canvas = window['app'].canvas
const canvas = window.app!.canvas
return {
lowQuality: canvas.low_quality,
scale: canvas.ds.scale,
@@ -32,11 +32,11 @@ test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => {
// Can't access private _lowQualityZoomThreshold directly
// Zoom out just above threshold (should still be high quality)
await comfyPage.zoom(120, 5) // Zoom out 5 steps
await comfyPage.canvasOps.zoom(120, 5) // Zoom out 5 steps
await comfyPage.nextFrame()
const aboveThresholdState = await comfyPage.page.evaluate(() => {
const canvas = window['app'].canvas
const canvas = window.app!.canvas
return {
lowQuality: canvas.low_quality,
scale: canvas.ds.scale
@@ -49,12 +49,12 @@ test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => {
}
// Zoom out more to trigger LOD (below threshold)
await comfyPage.zoom(120, 5) // Zoom out 5 more steps
await comfyPage.canvasOps.zoom(120, 5) // Zoom out 5 more steps
await comfyPage.nextFrame()
// Check that LOD is now active
const zoomedOutState = await comfyPage.page.evaluate(() => {
const canvas = window['app'].canvas
const canvas = window.app!.canvas
return {
lowQuality: canvas.low_quality,
scale: canvas.ds.scale
@@ -65,12 +65,12 @@ test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => {
expect(zoomedOutState.lowQuality).toBe(true)
// Zoom back in to disable LOD (above threshold)
await comfyPage.zoom(-120, 15) // Zoom in 15 steps
await comfyPage.canvasOps.zoom(-120, 15) // Zoom in 15 steps
await comfyPage.nextFrame()
// Check that LOD is now inactive
const zoomedInState = await comfyPage.page.evaluate(() => {
const canvas = window['app'].canvas
const canvas = window.app!.canvas
return {
lowQuality: canvas.low_quality,
scale: canvas.ds.scale
@@ -84,14 +84,17 @@ test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => {
test('Should update threshold when font size setting changes', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('default')
await comfyPage.workflow.loadWorkflow('default')
// Change the font size setting to 14px (more aggressive LOD)
await comfyPage.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 14)
await comfyPage.settings.setSetting(
'LiteGraph.Canvas.MinFontSizeForLOD',
14
)
// Check that font size updated
const newState = await comfyPage.page.evaluate(() => {
const canvas = window['app'].canvas
const canvas = window.app!.canvas
return {
minFontSize: canvas.min_font_size_for_lod
}
@@ -102,16 +105,16 @@ test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => {
// At default zoom, LOD should still be inactive (scale is exactly 1.0, not less than)
const lodState = await comfyPage.page.evaluate(() => {
return window['app'].canvas.low_quality
return window.app!.canvas.low_quality
})
expect(lodState).toBe(false)
// Zoom out slightly to trigger LOD
await comfyPage.zoom(120, 1) // Zoom out 1 step
await comfyPage.canvasOps.zoom(120, 1) // Zoom out 1 step
await comfyPage.nextFrame()
const afterZoom = await comfyPage.page.evaluate(() => {
const canvas = window['app'].canvas
const canvas = window.app!.canvas
return {
lowQuality: canvas.low_quality,
scale: canvas.ds.scale
@@ -125,18 +128,18 @@ test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => {
test('Should disable LOD when font size is set to 0', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('default')
await comfyPage.workflow.loadWorkflow('default')
// Disable LOD by setting font size to 0
await comfyPage.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 0)
await comfyPage.settings.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 0)
// Zoom out significantly
await comfyPage.zoom(120, 20) // Zoom out 20 steps
await comfyPage.canvasOps.zoom(120, 20) // Zoom out 20 steps
await comfyPage.nextFrame()
// LOD should remain disabled even at very low zoom
const state = await comfyPage.page.evaluate(() => {
const canvas = window['app'].canvas
const canvas = window.app!.canvas
return {
lowQuality: canvas.low_quality,
scale: canvas.ds.scale,
@@ -154,15 +157,15 @@ test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => {
{ tag: '@screenshot' },
async ({ comfyPage }) => {
// Load a workflow with text-heavy nodes for clear visual difference
await comfyPage.loadWorkflow('default')
await comfyPage.workflow.loadWorkflow('default')
// Set zoom level clearly below the threshold to ensure LOD activates
const targetZoom = 0.4 // Well below default threshold of ~0.571
// Zoom to target level
await comfyPage.page.evaluate((zoom) => {
window['app'].canvas.ds.scale = zoom
window['app'].canvas.setDirty(true, true)
window.app!.canvas.ds.scale = zoom
window.app!.canvas.setDirty(true, true)
}, targetZoom)
await comfyPage.nextFrame()
@@ -172,7 +175,7 @@ test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => {
)
const lowQualityState = await comfyPage.page.evaluate(() => {
const canvas = window['app'].canvas
const canvas = window.app!.canvas
return {
lowQuality: canvas.low_quality,
scale: canvas.ds.scale
@@ -181,7 +184,10 @@ test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => {
expect(lowQualityState.lowQuality).toBe(true)
// Disable LOD to see high quality at same zoom
await comfyPage.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 0)
await comfyPage.settings.setSetting(
'LiteGraph.Canvas.MinFontSizeForLOD',
0
)
await comfyPage.nextFrame()
// Take snapshot with LOD disabled (full quality at same zoom)
@@ -190,7 +196,7 @@ test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => {
)
const highQualityState = await comfyPage.page.evaluate(() => {
const canvas = window['app'].canvas
const canvas = window.app!.canvas
return {
lowQuality: canvas.low_quality,
scale: canvas.ds.scale

View File

@@ -4,14 +4,14 @@ import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Menu', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
})
test('Can register sidebar tab', async ({ comfyPage }) => {
const initialChildrenCount = await comfyPage.menu.buttons.count()
await comfyPage.page.evaluate(async () => {
window['app'].extensionManager.registerSidebarTab({
window.app!.extensionManager.registerSidebarTab({
id: 'search',
icon: 'pi pi-search',
title: 'search',
@@ -30,11 +30,11 @@ test.describe('Menu', { tag: '@ui' }, () => {
test.describe('Workflows topbar tabs', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting(
await comfyPage.settings.setSetting(
'Comfy.Workflow.WorkflowTabsPosition',
'Topbar'
)
await comfyPage.setupWorkflowsDirectory({})
await comfyPage.workflow.setupWorkflowsDirectory({})
})
test('Can show opened workflows', async ({ comfyPage }) => {
@@ -101,8 +101,8 @@ test.describe('Menu', { tag: '@ui' }, () => {
const checkmark = bottomPanelMenuItem.locator('.pi-check')
// Check initial state of bottom panel (it's initially hidden)
const bottomPanel = comfyPage.page.locator('.bottom-panel')
await expect(bottomPanel).not.toBeVisible()
const { bottomPanel } = comfyPage
await expect(bottomPanel.root).not.toBeVisible()
// Checkmark should be invisible initially (panel is hidden)
await expect(checkmark).toHaveClass(/invisible/)
@@ -113,7 +113,8 @@ test.describe('Menu', { tag: '@ui' }, () => {
await expect(menu).toBeVisible()
await expect(viewSubmenu).toBeVisible()
await expect(bottomPanel).toBeVisible()
// Verify bottom panel is now visible
await expect(bottomPanel.root).toBeVisible()
// Checkmark should now be visible (panel is shown)
await expect(checkmark).not.toHaveClass(/invisible/)
@@ -126,7 +127,7 @@ test.describe('Menu', { tag: '@ui' }, () => {
await expect(viewSubmenu).toBeVisible()
// Verify bottom panel is hidden again
await expect(bottomPanel).not.toBeVisible()
await expect(bottomPanel.root).not.toBeVisible()
// Checkmark should be invisible again (panel is hidden)
await expect(checkmark).toHaveClass(/invisible/)
@@ -153,7 +154,7 @@ test.describe('Menu', { tag: '@ui' }, () => {
test('Can catch error when executing command', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
window['app'].registerExtension({
window.app!.registerExtension({
name: 'TestExtension1',
commands: [
{
@@ -173,7 +174,7 @@ test.describe('Menu', { tag: '@ui' }, () => {
})
})
await comfyPage.menu.topbar.triggerTopbarCommand(['ext', 'foo-command'])
expect(await comfyPage.getVisibleToastCount()).toBe(1)
await expect.poll(() => comfyPage.toast.getVisibleToastCount()).toBe(1)
})
test('Can navigate Theme menu and switch between Dark and Light themes', async ({
@@ -212,7 +213,9 @@ test.describe('Menu', { tag: '@ui' }, () => {
await comfyPage.attachScreenshot('theme-menu-light-active')
// Verify ColorPalette setting is set to "light"
expect(await comfyPage.getSetting('Comfy.ColorPalette')).toBe('light')
expect(await comfyPage.settings.getSetting('Comfy.ColorPalette')).toBe(
'light'
)
// Close menu to see theme change
await topbar.closeTopbarMenu()
@@ -236,7 +239,9 @@ test.describe('Menu', { tag: '@ui' }, () => {
await comfyPage.attachScreenshot('theme-menu-dark-active')
// Verify ColorPalette setting is set to "dark"
expect(await comfyPage.getSetting('Comfy.ColorPalette')).toBe('dark')
expect(await comfyPage.settings.getSetting('Comfy.ColorPalette')).toBe(
'dark'
)
// Close menu
await topbar.closeTopbarMenu()
@@ -249,16 +254,20 @@ test.describe('Menu', { tag: '@ui' }, () => {
test(`Can migrate deprecated menu positions (${position})`, async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.UseNewMenu', position)
expect(await comfyPage.getSetting('Comfy.UseNewMenu')).toBe('Top')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', position)
expect(await comfyPage.settings.getSetting('Comfy.UseNewMenu')).toBe(
'Top'
)
})
test(`Can migrate deprecated menu positions on initial load (${position})`, async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.UseNewMenu', position)
await comfyPage.settings.setSetting('Comfy.UseNewMenu', position)
await comfyPage.setup()
expect(await comfyPage.getSetting('Comfy.UseNewMenu')).toBe('Top')
expect(await comfyPage.settings.getSetting('Comfy.UseNewMenu')).toBe(
'Top'
)
})
})
})

View File

@@ -1,16 +1,15 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { TestIds } from '../fixtures/selectors'
test.describe('Minimap', { tag: '@canvas' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting('Comfy.Minimap.Visible', true)
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true)
await comfyPage.loadWorkflow('default')
await comfyPage.page.waitForFunction(
() => window['app'] && window['app'].canvas
)
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.Minimap.Visible', true)
await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', true)
await comfyPage.workflow.loadWorkflow('default')
await comfyPage.page.waitForFunction(() => window.app && window.app.canvas)
})
test('Validate minimap is visible by default', async ({ comfyPage }) => {
@@ -35,7 +34,9 @@ test.describe('Minimap', { tag: '@canvas' }, () => {
})
test('Validate minimap toggle button state', async ({ comfyPage }) => {
const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button')
const toggleButton = comfyPage.page.getByTestId(
TestIds.canvas.toggleMinimapButton
)
await expect(toggleButton).toBeVisible()
@@ -45,7 +46,9 @@ test.describe('Minimap', { tag: '@canvas' }, () => {
test('Validate minimap can be toggled off and on', async ({ comfyPage }) => {
const minimapContainer = comfyPage.page.locator('.litegraph-minimap')
const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button')
const toggleButton = comfyPage.page.getByTestId(
TestIds.canvas.toggleMinimapButton
)
await expect(minimapContainer).toBeVisible()

View File

@@ -1,22 +1,24 @@
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { TestIds } from '../fixtures/selectors'
test.describe(
'Mobile Baseline Snapshots',
{ tag: ['@mobile', '@screenshot'] },
() => {
test('@mobile empty canvas', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.ConfirmClear', false)
await comfyPage.executeCommand('Comfy.ClearWorkflow')
await comfyPage.settings.setSetting('Comfy.ConfirmClear', false)
await comfyPage.command.executeCommand('Comfy.ClearWorkflow')
await expect(async () => {
expect(await comfyPage.getGraphNodesCount()).toBe(0)
}).toPass({ timeout: 256 })
expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe(0)
}).toPass({ timeout: 5000 })
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('mobile-empty-canvas.png')
})
test('@mobile default workflow', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('default')
await comfyPage.workflow.loadWorkflow('default')
await expect(comfyPage.canvas).toHaveScreenshot(
'mobile-default-workflow.png'
)
@@ -30,7 +32,9 @@ test.describe(
'mobile-settings-dialog.png',
{
mask: [
comfyPage.settingDialog.root.getByTestId('current-user-indicator')
comfyPage.settingDialog.root.getByTestId(
TestIds.user.currentUserIndicator
)
]
}
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -5,14 +5,14 @@ import { NodeBadgeMode } from '../../src/types/nodeSource'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Node Badge', { tag: ['@screenshot', '@smoke', '@node'] }, () => {
test('Can add badge', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
const LGraphBadge = window['LGraphBadge']
const app = window['app'] as ComfyApp
const LGraphBadge = window.LGraphBadge!
const app = window.app as ComfyApp
const graph = app.graph
const nodes = graph.nodes
@@ -28,8 +28,8 @@ test.describe('Node Badge', { tag: ['@screenshot', '@smoke', '@node'] }, () => {
test('Can add multiple badges', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
const LGraphBadge = window['LGraphBadge']
const app = window['app'] as ComfyApp
const LGraphBadge = window.LGraphBadge!
const app = window.app as ComfyApp
const graph = app.graph
const nodes = graph.nodes
@@ -48,8 +48,8 @@ test.describe('Node Badge', { tag: ['@screenshot', '@smoke', '@node'] }, () => {
test('Can add badge left-side', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
const LGraphBadge = window['LGraphBadge']
const app = window['app'] as ComfyApp
const LGraphBadge = window.LGraphBadge!
const app = window.app as ComfyApp
const graph = app.graph
const nodes = graph.nodes
@@ -73,11 +73,17 @@ test.describe(
Object.values(NodeBadgeMode).forEach(async (mode) => {
test(`Shows node badges (${mode})`, async ({ comfyPage }) => {
// Execution error workflow has both custom node and core node.
await comfyPage.loadWorkflow('nodes/execution_error')
await comfyPage.setSetting('Comfy.NodeBadge.NodeSourceBadgeMode', mode)
await comfyPage.setSetting('Comfy.NodeBadge.NodeIdBadgeMode', mode)
await comfyPage.workflow.loadWorkflow('nodes/execution_error')
await comfyPage.settings.setSetting(
'Comfy.NodeBadge.NodeSourceBadgeMode',
mode
)
await comfyPage.settings.setSetting(
'Comfy.NodeBadge.NodeIdBadgeMode',
mode
)
await comfyPage.nextFrame()
await comfyPage.resetView()
await comfyPage.canvasOps.resetView()
await expect(comfyPage.canvas).toHaveScreenshot(
`node-badge-${mode}.png`
)
@@ -93,14 +99,14 @@ test.describe(
test('Can show node badge with unknown color palette', async ({
comfyPage
}) => {
await comfyPage.setSetting(
await comfyPage.settings.setSetting(
'Comfy.NodeBadge.NodeIdBadgeMode',
NodeBadgeMode.ShowAll
)
await comfyPage.setSetting('Comfy.ColorPalette', 'unknown')
await comfyPage.settings.setSetting('Comfy.ColorPalette', 'unknown')
await comfyPage.nextFrame()
// Click empty space to trigger canvas re-render.
await comfyPage.clickEmptySpace()
await comfyPage.canvasOps.clickEmptySpace()
await expect(comfyPage.canvas).toHaveScreenshot(
'node-badge-unknown-color-palette.png'
)
@@ -109,14 +115,14 @@ test.describe(
test('Can show node badge with light color palette', async ({
comfyPage
}) => {
await comfyPage.setSetting(
await comfyPage.settings.setSetting(
'Comfy.NodeBadge.NodeIdBadgeMode',
NodeBadgeMode.ShowAll
)
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
await comfyPage.settings.setSetting('Comfy.ColorPalette', 'light')
await comfyPage.nextFrame()
// Click empty space to trigger canvas re-render.
await comfyPage.clickEmptySpace()
await comfyPage.canvasOps.clickEmptySpace()
await expect(comfyPage.canvas).toHaveScreenshot(
'node-badge-light-color-palette.png'
)

View File

@@ -3,40 +3,40 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
// If an input is optional by node definition, it should be shown as
// a hollow circle no matter what shape it was defined in the workflow JSON.
test.describe('Optional input', { tag: ['@screenshot', '@node'] }, () => {
test('No shape specified', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/optional_input_no_shape')
await comfyPage.workflow.loadWorkflow('inputs/optional_input_no_shape')
await expect(comfyPage.canvas).toHaveScreenshot('optional_input.png')
})
test('Wrong shape specified', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/optional_input_wrong_shape')
await comfyPage.workflow.loadWorkflow('inputs/optional_input_wrong_shape')
await expect(comfyPage.canvas).toHaveScreenshot('optional_input.png')
})
test('Correct shape specified', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/optional_input_correct_shape')
await comfyPage.workflow.loadWorkflow('inputs/optional_input_correct_shape')
await expect(comfyPage.canvas).toHaveScreenshot('optional_input.png')
})
test('Force input', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/force_input')
await comfyPage.workflow.loadWorkflow('inputs/force_input')
await expect(comfyPage.canvas).toHaveScreenshot('force_input.png')
})
test('Default input', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/default_input')
await comfyPage.workflow.loadWorkflow('inputs/default_input')
await expect(comfyPage.canvas).toHaveScreenshot('default_input.png')
})
test('Only optional inputs', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/only_optional_inputs')
expect(await comfyPage.getGraphNodesCount()).toBe(1)
await comfyPage.workflow.loadWorkflow('inputs/only_optional_inputs')
expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe(1)
await expect(
comfyPage.page.locator('.comfy-missing-nodes')
).not.toBeVisible()
@@ -47,37 +47,45 @@ test.describe('Optional input', { tag: ['@screenshot', '@node'] }, () => {
)
})
test('Old workflow with converted input', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/old_workflow_converted_input')
const node = await comfyPage.getNodeRefById('1')
const inputs = await node.getProperty('inputs')
await comfyPage.workflow.loadWorkflow('inputs/old_workflow_converted_input')
const node = await comfyPage.nodeOps.getNodeRefById('1')
const inputs = (await node.getProperty('inputs')) as {
name: string
link?: number | null
}[]
const vaeInput = inputs.find((w) => w.name === 'vae')
const convertedInput = inputs.find((w) => w.name === 'strength')
expect(vaeInput).toBeDefined()
expect(convertedInput).toBeDefined()
expect(vaeInput.link).toBeNull()
expect(convertedInput.link).not.toBeNull()
expect(vaeInput!.link).toBeNull()
expect(convertedInput!.link).not.toBeNull()
})
test('Renamed converted input', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/renamed_converted_widget')
const node = await comfyPage.getNodeRefById('3')
const inputs = await node.getProperty('inputs')
await comfyPage.workflow.loadWorkflow('inputs/renamed_converted_widget')
const node = await comfyPage.nodeOps.getNodeRefById('3')
const inputs = (await node.getProperty('inputs')) as { name: string }[]
const renamedInput = inputs.find((w) => w.name === 'breadth')
expect(renamedInput).toBeUndefined()
})
test('slider', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/simple_slider')
await comfyPage.workflow.loadWorkflow('inputs/simple_slider')
await expect(comfyPage.canvas).toHaveScreenshot('simple_slider.png')
})
test('unknown converted widget', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Workflow.ShowMissingNodesWarning', false)
await comfyPage.loadWorkflow('missing/missing_nodes_converted_widget')
await comfyPage.settings.setSetting(
'Comfy.Workflow.ShowMissingNodesWarning',
false
)
await comfyPage.workflow.loadWorkflow(
'missing/missing_nodes_converted_widget'
)
await expect(comfyPage.canvas).toHaveScreenshot(
'missing_nodes_converted_widget.png'
)
})
test('dynamically added input', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/dynamically_added_input')
await comfyPage.workflow.loadWorkflow('inputs/dynamically_added_input')
await expect(comfyPage.canvas).toHaveScreenshot(
'dynamically_added_input.png'
)

View File

@@ -12,7 +12,7 @@ async function selectNodeWithPan(comfyPage: ComfyPage, nodeRef: NodeReference) {
const nodePos = await nodeRef.getPosition()
await comfyPage.page.evaluate((pos) => {
const app = window['app']!
const app = window.app!
const canvas = app.canvas
canvas.ds.offset[0] = -pos.x + canvas.canvas.width / 2
canvas.ds.offset[1] = -pos.y + canvas.canvas.height / 2 + 100
@@ -26,17 +26,18 @@ async function selectNodeWithPan(comfyPage: ComfyPage, nodeRef: NodeReference) {
test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setup()
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
})
test.describe('Selection Toolbox', () => {
test('Should open help menu for selected node', async ({ comfyPage }) => {
// Load a workflow with a node
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
await comfyPage.loadWorkflow('default')
await comfyPage.settings.setSetting('Comfy.Canvas.SelectionToolbox', true)
await comfyPage.workflow.loadWorkflow('default')
// Select a single node (KSampler) using node references
const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler')
const ksamplerNodes =
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
if (ksamplerNodes.length === 0) {
throw new Error('No KSampler nodes found in the workflow')
}
@@ -87,7 +88,9 @@ test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => {
await ksamplerNode.hover()
// Click the help button
const helpButton = ksamplerNode.locator('button:has(.pi-question)')
const helpButton = ksamplerNode.getByRole('button', {
name: /learn more/i
})
await expect(helpButton).toBeVisible()
await helpButton.click()
@@ -117,7 +120,9 @@ test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => {
.filter({ hasText: 'KSampler' })
.first()
await ksamplerNode.hover()
const helpButton = ksamplerNode.locator('button:has(.pi-question)')
const helpButton = ksamplerNode.getByRole('button', {
name: /learn more/i
})
await helpButton.click()
// Verify help page is shown
@@ -141,7 +146,7 @@ test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => {
test.describe('Help Content', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
await comfyPage.settings.setSetting('Comfy.Canvas.SelectionToolbox', true)
})
test('Should display loading state while fetching help', async ({
@@ -157,8 +162,9 @@ test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => {
})
// Load workflow and select a node
await comfyPage.loadWorkflow('default')
const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler')
await comfyPage.workflow.loadWorkflow('default')
const ksamplerNodes =
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
// Click help button
@@ -189,8 +195,9 @@ test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => {
})
// Load workflow and select a node
await comfyPage.loadWorkflow('default')
const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler')
await comfyPage.workflow.loadWorkflow('default')
const ksamplerNodes =
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
// Click help button
@@ -226,8 +233,9 @@ test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => {
})
})
await comfyPage.loadWorkflow('default')
const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler')
await comfyPage.workflow.loadWorkflow('default')
const ksamplerNodes =
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
const helpButton = comfyPage.page.locator(
@@ -276,8 +284,9 @@ test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => {
})
})
await comfyPage.loadWorkflow('default')
const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler')
await comfyPage.workflow.loadWorkflow('default')
const ksamplerNodes =
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
const helpButton = comfyPage.page.locator(
@@ -323,7 +332,7 @@ test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => {
comfyPage
}) => {
// First load workflow with custom node
await comfyPage.loadWorkflow('groupnodes/group_node_v1.3.3')
await comfyPage.workflow.loadWorkflow('groupnodes/group_node_v1.3.3')
// Mock custom node documentation with fallback
await comfyPage.page.route(
@@ -347,10 +356,10 @@ This is documentation for a custom node.
// Find and select a custom/group node
const nodeRefs = await comfyPage.page.evaluate(() => {
return window['app']!.graph!.nodes.map((n) => n.id)
return window.app!.graph!.nodes.map((n) => n.id)
})
if (nodeRefs.length > 0) {
const firstNode = await comfyPage.getNodeRefById(nodeRefs[0])
const firstNode = await comfyPage.nodeOps.getNodeRefById(nodeRefs[0])
await selectNodeWithPan(comfyPage, firstNode)
}
@@ -393,8 +402,9 @@ This is documentation for a custom node.
})
})
await comfyPage.loadWorkflow('default')
const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler')
await comfyPage.workflow.loadWorkflow('default')
const ksamplerNodes =
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
const helpButton = comfyPage.page.locator(
@@ -460,10 +470,11 @@ This is English documentation.
})
// Set locale to Japanese
await comfyPage.setSetting('Comfy.Locale', 'ja')
await comfyPage.settings.setSetting('Comfy.Locale', 'ja')
await comfyPage.loadWorkflow('default')
const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler')
await comfyPage.workflow.loadWorkflow('default')
const ksamplerNodes =
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
const helpButton = comfyPage.page.locator(
@@ -478,7 +489,7 @@ This is English documentation.
await expect(helpPage).toContainText('これは日本語のドキュメントです')
// Reset locale
await comfyPage.setSetting('Comfy.Locale', 'en')
await comfyPage.settings.setSetting('Comfy.Locale', 'en')
})
test('Should handle network errors gracefully', async ({ comfyPage }) => {
@@ -487,8 +498,9 @@ This is English documentation.
await route.abort('failed')
})
await comfyPage.loadWorkflow('default')
const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler')
await comfyPage.workflow.loadWorkflow('default')
const ksamplerNodes =
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
const helpButton = comfyPage.page.locator(
@@ -530,11 +542,12 @@ This is English documentation.
}
)
await comfyPage.loadWorkflow('default')
await comfyPage.workflow.loadWorkflow('default')
await fitToViewInstant(comfyPage)
// Select KSampler first
const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler')
const ksamplerNodes =
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
const helpButton = comfyPage.page.locator(
@@ -549,7 +562,7 @@ This is English documentation.
await expect(helpPage).toContainText('This is KSampler documentation')
// Now select Checkpoint Loader
const checkpointNodes = await comfyPage.getNodeRefsByType(
const checkpointNodes = await comfyPage.nodeOps.getNodeRefsByType(
'CheckpointLoaderSimple'
)
await selectNodeWithPan(comfyPage, checkpointNodes[0])

View File

@@ -2,32 +2,39 @@ import {
comfyExpect as expect,
comfyPageFixture as test
} from '../fixtures/ComfyPage'
import type { ComfyPage } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Node search box', { tag: '@node' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.LinkRelease.Action', 'search box')
await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'search box')
await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default')
await comfyPage.settings.setSetting(
'Comfy.LinkRelease.Action',
'search box'
)
await comfyPage.settings.setSetting(
'Comfy.LinkRelease.ActionShift',
'search box'
)
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default')
})
test(`Can trigger on empty canvas double click`, async ({ comfyPage }) => {
await comfyPage.doubleClickCanvas()
await comfyPage.canvasOps.doubleClick()
await expect(comfyPage.searchBox.input).toHaveCount(1)
})
test(`Can trigger on group body double click`, async ({ comfyPage }) => {
await comfyPage.loadWorkflow('groups/single_group_only')
await comfyPage.workflow.loadWorkflow('groups/single_group_only')
await comfyPage.page.mouse.dblclick(50, 50, { delay: 5 })
await comfyPage.nextFrame()
await expect(comfyPage.searchBox.input).toHaveCount(1)
})
test('Can trigger on link release', async ({ comfyPage }) => {
await comfyPage.disconnectEdge()
await comfyPage.canvasOps.disconnectEdge()
await expect(comfyPage.searchBox.input).toHaveCount(1)
})
@@ -37,24 +44,24 @@ test.describe('Node search box', { tag: '@node' }, () => {
// Start fresh to test new user behavior
await comfyPage.setup({ clearStorage: true })
// Simulate new user with 1.24.1+ installed version
await comfyPage.setSetting('Comfy.InstalledVersion', '1.24.1')
await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default')
await comfyPage.settings.setSetting('Comfy.InstalledVersion', '1.24.1')
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default')
// Don't set LinkRelease settings explicitly to test versioned defaults
await comfyPage.disconnectEdge()
await comfyPage.canvasOps.disconnectEdge()
await expect(comfyPage.searchBox.input).toHaveCount(1)
await expect(comfyPage.searchBox.input).toBeVisible()
})
test('Can add node', { tag: '@screenshot' }, async ({ comfyPage }) => {
await comfyPage.doubleClickCanvas()
await comfyPage.canvasOps.doubleClick()
await expect(comfyPage.searchBox.input).toHaveCount(1)
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler')
await expect(comfyPage.canvas).toHaveScreenshot('added-node.png')
})
test('Can auto link node', { tag: '@screenshot' }, async ({ comfyPage }) => {
await comfyPage.disconnectEdge()
await comfyPage.canvasOps.disconnectEdge()
// Select the second item as the first item is always reroute
await comfyPage.searchBox.fillAndSelectFirstNode('CLIPTextEncode', {
suggestionIndex: 0
@@ -66,18 +73,18 @@ test.describe('Node search box', { tag: '@node' }, () => {
'Can auto link batch moved node',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.loadWorkflow('links/batch_move_links')
await comfyPage.workflow.loadWorkflow('links/batch_move_links')
// Get the CLIP output slot (index 1) from the first CheckpointLoaderSimple node (id: 4)
const checkpointNode = await comfyPage.nodeOps.getNodeRefById(4)
const clipOutputSlot = await checkpointNode.getOutput(1)
const outputSlotPos = await clipOutputSlot.getPosition()
// Use a position in the empty canvas area (top-left corner)
const emptySpacePos = { x: 5, y: 5 }
const outputSlot1Pos = {
x: 304,
y: 127
}
const emptySpacePos = {
x: 5,
y: 5
}
await comfyPage.page.keyboard.down('Shift')
await comfyPage.dragAndDrop(outputSlot1Pos, emptySpacePos)
await comfyPage.canvasOps.dragAndDrop(outputSlotPos, emptySpacePos)
await comfyPage.page.keyboard.up('Shift')
// Select the second item as the first item is always reroute
@@ -94,7 +101,7 @@ test.describe('Node search box', { tag: '@node' }, () => {
'Link release connecting to node with no slots',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.disconnectEdge()
await comfyPage.canvasOps.disconnectEdge()
await expect(comfyPage.searchBox.input).toHaveCount(1)
await comfyPage.page.locator('.p-chip-remove-icon').click()
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler')
@@ -106,7 +113,7 @@ test.describe('Node search box', { tag: '@node' }, () => {
test('Has correct aria-labels on search results', async ({ comfyPage }) => {
const node = 'Load Checkpoint'
await comfyPage.doubleClickCanvas()
await comfyPage.canvasOps.doubleClick()
await comfyPage.searchBox.input.waitFor({ state: 'visible' })
await comfyPage.searchBox.input.fill(node)
await comfyPage.searchBox.dropdown.waitFor({ state: 'visible' })
@@ -117,7 +124,7 @@ test.describe('Node search box', { tag: '@node' }, () => {
test('@mobile Can trigger on empty canvas tap', async ({ comfyPage }) => {
await comfyPage.closeMenu()
await comfyPage.loadWorkflow('nodes/single_ksampler')
await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
const screenCenter = {
x: 200,
y: 400
@@ -132,7 +139,10 @@ test.describe('Node search box', { tag: '@node' }, () => {
})
test.describe('Filtering', () => {
const expectFilterChips = async (comfyPage, expectedTexts: string[]) => {
const expectFilterChips = async (
comfyPage: ComfyPage,
expectedTexts: string[]
) => {
const chips = comfyPage.searchBox.filterChips
// Check that the number of chips matches the expected count
@@ -149,7 +159,7 @@ test.describe('Node search box', { tag: '@node' }, () => {
}
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.doubleClickCanvas()
await comfyPage.canvasOps.doubleClick()
})
test('Can add filter', async ({ comfyPage }) => {
@@ -241,7 +251,7 @@ test.describe('Node search box', { tag: '@node' }, () => {
test.describe('Input focus behavior', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.doubleClickCanvas()
await comfyPage.canvasOps.doubleClick()
})
test('focuses input after adding a filter', async ({ comfyPage }) => {
@@ -259,16 +269,22 @@ test.describe('Node search box', { tag: '@node' }, () => {
test.describe('Release context menu', { tag: '@node' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.LinkRelease.Action', 'context menu')
await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'search box')
await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default')
await comfyPage.settings.setSetting(
'Comfy.LinkRelease.Action',
'context menu'
)
await comfyPage.settings.setSetting(
'Comfy.LinkRelease.ActionShift',
'search box'
)
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default')
})
test(
'Can trigger on link release',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.disconnectEdge()
await comfyPage.canvasOps.disconnectEdge()
const contextMenu = comfyPage.page.locator('.litecontextmenu')
// Wait for context menu with correct title (slot name | slot type)
// The title shows the output slot name and type from the disconnected link
@@ -287,9 +303,10 @@ test.describe('Release context menu', { tag: '@node' }, () => {
'Can search and add node from context menu',
{ tag: '@screenshot' },
async ({ comfyPage, comfyMouse }) => {
await comfyPage.disconnectEdge()
await comfyPage.canvasOps.disconnectEdge()
await comfyMouse.move({ x: 10, y: 10 })
await comfyPage.clickContextMenuItem('Search')
await comfyPage.contextMenu.clickMenuItem('Search')
await comfyPage.nextFrame()
await comfyPage.searchBox.fillAndSelectFirstNode('CLIP Prompt')
await expect(comfyPage.canvas).toHaveScreenshot(
'link-context-menu-search.png'
@@ -303,11 +320,11 @@ test.describe('Release context menu', { tag: '@node' }, () => {
// Start fresh to test existing user behavior
await comfyPage.setup({ clearStorage: true })
// Simulate existing user with pre-1.24.1 version
await comfyPage.setSetting('Comfy.InstalledVersion', '1.23.0')
await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default')
await comfyPage.settings.setSetting('Comfy.InstalledVersion', '1.23.0')
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default')
// Don't set LinkRelease settings explicitly to test versioned defaults
await comfyPage.disconnectEdge()
await comfyPage.canvasOps.disconnectEdge()
// Context menu should appear, search box should not
await expect(comfyPage.searchBox.input).toHaveCount(0)
const contextMenu = comfyPage.page.locator('.litecontextmenu')
@@ -319,12 +336,15 @@ test.describe('Release context menu', { tag: '@node' }, () => {
}) => {
// Start fresh and simulate new user who should get search box by default
await comfyPage.setup({ clearStorage: true })
await comfyPage.setSetting('Comfy.InstalledVersion', '1.24.1')
await comfyPage.settings.setSetting('Comfy.InstalledVersion', '1.24.1')
// But explicitly set to context menu (overriding versioned default)
await comfyPage.setSetting('Comfy.LinkRelease.Action', 'context menu')
await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default')
await comfyPage.settings.setSetting(
'Comfy.LinkRelease.Action',
'context menu'
)
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default')
await comfyPage.disconnectEdge()
await comfyPage.canvasOps.disconnectEdge()
// Context menu should appear due to explicit setting, not search box
await expect(comfyPage.searchBox.input).toHaveCount(0)
const contextMenu = comfyPage.page.locator('.litecontextmenu')

View File

@@ -3,12 +3,12 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Note Node', { tag: '@node' }, () => {
test('Can load node nodes', { tag: '@screenshot' }, async ({ comfyPage }) => {
await comfyPage.loadWorkflow('nodes/note_nodes')
await comfyPage.workflow.loadWorkflow('nodes/note_nodes')
await expect(comfyPage.canvas).toHaveScreenshot('note_nodes.png')
})
})

View File

@@ -4,21 +4,25 @@ import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import type { NodeReference } from '../fixtures/utils/litegraphUtils'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Primitive Node', { tag: ['@screenshot', '@node'] }, () => {
test('Can load with correct size', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('primitive/primitive_node')
await comfyPage.workflow.loadWorkflow('primitive/primitive_node')
await expect(comfyPage.canvas).toHaveScreenshot('primitive_node.png')
})
// When link is dropped on widget, it should automatically convert the widget
// to input.
test('Can connect to widget', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('primitive/primitive_node_unconnected')
const primitiveNode: NodeReference = await comfyPage.getNodeRefById(1)
const ksamplerNode: NodeReference = await comfyPage.getNodeRefById(2)
await comfyPage.workflow.loadWorkflow(
'primitive/primitive_node_unconnected'
)
const primitiveNode: NodeReference =
await comfyPage.nodeOps.getNodeRefById(1)
const ksamplerNode: NodeReference =
await comfyPage.nodeOps.getNodeRefById(2)
// Connect the output of the primitive node to the input of first widget of the ksampler node
await primitiveNode.connectWidget(0, ksamplerNode, 0)
await expect(comfyPage.canvas).toHaveScreenshot(
@@ -27,11 +31,13 @@ test.describe('Primitive Node', { tag: ['@screenshot', '@node'] }, () => {
})
test('Can connect to dom widget', async ({ comfyPage }) => {
await comfyPage.loadWorkflow(
await comfyPage.workflow.loadWorkflow(
'primitive/primitive_node_unconnected_dom_widget'
)
const primitiveNode: NodeReference = await comfyPage.getNodeRefById(1)
const clipEncoderNode: NodeReference = await comfyPage.getNodeRefById(2)
const primitiveNode: NodeReference =
await comfyPage.nodeOps.getNodeRefById(1)
const clipEncoderNode: NodeReference =
await comfyPage.nodeOps.getNodeRefById(2)
await primitiveNode.connectWidget(0, clipEncoderNode, 0)
await expect(comfyPage.canvas).toHaveScreenshot(
'primitive_node_connected_dom_widget.png'
@@ -39,9 +45,13 @@ test.describe('Primitive Node', { tag: ['@screenshot', '@node'] }, () => {
})
test('Can connect to static primitive node', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('primitive/static_primitive_unconnected')
const primitiveNode: NodeReference = await comfyPage.getNodeRefById(1)
const ksamplerNode: NodeReference = await comfyPage.getNodeRefById(2)
await comfyPage.workflow.loadWorkflow(
'primitive/static_primitive_unconnected'
)
const primitiveNode: NodeReference =
await comfyPage.nodeOps.getNodeRefById(1)
const ksamplerNode: NodeReference =
await comfyPage.nodeOps.getNodeRefById(2)
await primitiveNode.connectWidget(0, ksamplerNode, 0)
await expect(comfyPage.canvas).toHaveScreenshot(
'static_primitive_connected.png'
@@ -51,7 +61,7 @@ test.describe('Primitive Node', { tag: ['@screenshot', '@node'] }, () => {
test('Report missing nodes when connect to missing node', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow(
await comfyPage.workflow.loadWorkflow(
'primitive/primitive_node_connect_missing_node'
)
// Wait for the element with the .comfy-missing-nodes selector to be visible

View File

@@ -10,7 +10,10 @@ test.describe('Properties panel', () => {
await expect(propertiesPanel.panelTitle).toContainText('Workflow Overview')
await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)'])
await comfyPage.nodeOps.selectNodes([
'KSampler',
'CLIP Text Encode (Prompt)'
])
await expect(propertiesPanel.panelTitle).toContainText('3 items selected')
await expect(propertiesPanel.root.getByText('KSampler')).toHaveCount(1)

View File

@@ -1,6 +1,7 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
import { TestIds } from '../../fixtures/selectors'
test.describe('Properties panel position', () => {
test.beforeEach(async ({ comfyPage }) => {
@@ -12,10 +13,12 @@ test.describe('Properties panel position', () => {
test('positions on the right when sidebar is on the left', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Sidebar.Location', 'left')
await comfyPage.settings.setSetting('Comfy.Sidebar.Location', 'left')
await comfyPage.nextFrame()
const propertiesPanel = comfyPage.page.getByTestId('properties-panel')
const propertiesPanel = comfyPage.page.getByTestId(
TestIds.propertiesPanel.root
)
const sidebar = comfyPage.page.locator('.side-bar-panel').first()
await expect(propertiesPanel).toBeVisible()
@@ -36,10 +39,12 @@ test.describe('Properties panel position', () => {
test('positions on the left when sidebar is on the right', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Sidebar.Location', 'right')
await comfyPage.settings.setSetting('Comfy.Sidebar.Location', 'right')
await comfyPage.nextFrame()
const propertiesPanel = comfyPage.page.getByTestId('properties-panel')
const propertiesPanel = comfyPage.page.getByTestId(
TestIds.propertiesPanel.root
)
const sidebar = comfyPage.page.locator('.side-bar-panel').first()
await expect(propertiesPanel).toBeVisible()
@@ -60,10 +65,12 @@ test.describe('Properties panel position', () => {
test('close button icon updates based on sidebar location', async ({
comfyPage
}) => {
const propertiesPanel = comfyPage.page.getByTestId('properties-panel')
const propertiesPanel = comfyPage.page.getByTestId(
TestIds.propertiesPanel.root
)
// When sidebar is on the left, panel is on the right
await comfyPage.setSetting('Comfy.Sidebar.Location', 'left')
await comfyPage.settings.setSetting('Comfy.Sidebar.Location', 'left')
await comfyPage.nextFrame()
await expect(propertiesPanel).toBeVisible()
@@ -74,7 +81,7 @@ test.describe('Properties panel position', () => {
await expect(closeButtonLeft).toHaveClass(/lucide--panel-right/)
// When sidebar is on the right, panel is on the left
await comfyPage.setSetting('Comfy.Sidebar.Location', 'right')
await comfyPage.settings.setSetting('Comfy.Sidebar.Location', 'right')
await comfyPage.nextFrame()
const closeButtonRight = propertiesPanel

View File

@@ -3,7 +3,7 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Record Audio Node', { tag: '@screenshot' }, () => {
@@ -11,7 +11,7 @@ test.describe('Record Audio Node', { tag: '@screenshot' }, () => {
comfyPage
}) => {
// Open the search box by double clicking on the canvas
await comfyPage.doubleClickCanvas()
await comfyPage.canvasOps.doubleClick()
await expect(comfyPage.searchBox.input).toHaveCount(1)
// Search for and add the RecordAudio node
@@ -19,7 +19,8 @@ test.describe('Record Audio Node', { tag: '@screenshot' }, () => {
await comfyPage.nextFrame()
// Verify the RecordAudio node was added
const recordAudioNodes = await comfyPage.getNodeRefsByType('RecordAudio')
const recordAudioNodes =
await comfyPage.nodeOps.getNodeRefsByType('RecordAudio')
expect(recordAudioNodes.length).toBe(1)
// Take a screenshot of the canvas with the RecordAudio node

View File

@@ -1,10 +1,11 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { TestIds } from '../fixtures/selectors'
test.describe('Release Notifications', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
})
test('should show help center with release information', async ({
@@ -50,7 +51,9 @@ test.describe('Release Notifications', () => {
await expect(helpMenu).toBeVisible()
// Verify "What's New?" section shows the release
const whatsNewSection = comfyPage.page.getByTestId('whats-new-section')
const whatsNewSection = comfyPage.page.getByTestId(
TestIds.dialogs.whatsNewSection
)
await expect(whatsNewSection).toBeVisible()
// Should show the release version
@@ -79,7 +82,9 @@ test.describe('Release Notifications', () => {
await expect(helpMenu).toBeVisible()
// Verify "What's New?" section shows no releases
const whatsNewSection = comfyPage.page.getByTestId('whats-new-section')
const whatsNewSection = comfyPage.page.getByTestId(
TestIds.dialogs.whatsNewSection
)
await expect(whatsNewSection).toBeVisible()
// Should show "No recent releases" message
@@ -125,7 +130,9 @@ test.describe('Release Notifications', () => {
await expect(helpMenu).toBeVisible()
// Should show no releases due to error
const whatsNewSection = comfyPage.page.getByTestId('whats-new-section')
const whatsNewSection = comfyPage.page.getByTestId(
TestIds.dialogs.whatsNewSection
)
await expect(
whatsNewSection.locator('text=No recent releases')
).toBeVisible()
@@ -135,7 +142,10 @@ test.describe('Release Notifications', () => {
comfyPage
}) => {
// Disable version update notifications
await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', false)
await comfyPage.settings.setSetting(
'Comfy.Notification.ShowVersionUpdates',
false
)
// Mock release API with test data
await comfyPage.page.route('**/releases**', async (route) => {
@@ -175,7 +185,9 @@ test.describe('Release Notifications', () => {
await expect(helpMenu).toBeVisible()
// Verify "What's New?" section is hidden
const whatsNewSection = comfyPage.page.getByTestId('whats-new-section')
const whatsNewSection = comfyPage.page.getByTestId(
TestIds.dialogs.whatsNewSection
)
await expect(whatsNewSection).not.toBeVisible()
// Should not show any popups or toasts
@@ -189,7 +201,10 @@ test.describe('Release Notifications', () => {
comfyPage
}) => {
// Disable version update notifications
await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', false)
await comfyPage.settings.setSetting(
'Comfy.Notification.ShowVersionUpdates',
false
)
// Track API calls
let apiCallCount = 0
@@ -220,7 +235,10 @@ test.describe('Release Notifications', () => {
comfyPage
}) => {
// Enable version update notifications (default behavior)
await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', true)
await comfyPage.settings.setSetting(
'Comfy.Notification.ShowVersionUpdates',
true
)
// Mock release API with test data
await comfyPage.page.route('**/releases**', async (route) => {
@@ -260,7 +278,9 @@ test.describe('Release Notifications', () => {
await expect(helpMenu).toBeVisible()
// Verify "What's New?" section is visible
const whatsNewSection = comfyPage.page.getByTestId('whats-new-section')
const whatsNewSection = comfyPage.page.getByTestId(
TestIds.dialogs.whatsNewSection
)
await expect(whatsNewSection).toBeVisible()
// Should show the release
@@ -299,7 +319,10 @@ test.describe('Release Notifications', () => {
})
// Start with notifications enabled
await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', true)
await comfyPage.settings.setSetting(
'Comfy.Notification.ShowVersionUpdates',
true
)
await comfyPage.setup({ mockReleases: false })
// Open help center
@@ -308,14 +331,19 @@ test.describe('Release Notifications', () => {
await helpCenterButton.click()
// Verify "What's New?" section is visible
const whatsNewSection = comfyPage.page.getByTestId('whats-new-section')
const whatsNewSection = comfyPage.page.getByTestId(
TestIds.dialogs.whatsNewSection
)
await expect(whatsNewSection).toBeVisible()
// Close help center
await comfyPage.page.click('.help-center-backdrop')
// Disable notifications
await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', false)
await comfyPage.settings.setSetting(
'Comfy.Notification.ShowVersionUpdates',
false
)
// Reopen help center
await helpCenterButton.click()
@@ -328,7 +356,10 @@ test.describe('Release Notifications', () => {
comfyPage
}) => {
// Disable notifications
await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', false)
await comfyPage.settings.setSetting(
'Comfy.Notification.ShowVersionUpdates',
false
)
// Mock empty releases
await comfyPage.page.route('**/releases**', async (route) => {
@@ -359,7 +390,9 @@ test.describe('Release Notifications', () => {
await expect(helpMenu).toBeVisible()
// Section should be hidden regardless of empty releases
const whatsNewSection = comfyPage.page.getByTestId('whats-new-section')
const whatsNewSection = comfyPage.page.getByTestId(
TestIds.dialogs.whatsNewSection
)
await expect(whatsNewSection).not.toBeVisible()
})
})

View File

@@ -26,23 +26,23 @@ test.describe('Remote COMBO Widget', { tag: '@widget' }, () => {
nodeName: string
): Promise<string[] | undefined> => {
return await comfyPage.page.evaluate((name) => {
const node = window['app'].graph.nodes.find((node) => node.title === name)
return node.widgets[0].options.values
const node = window.app!.graph!.nodes.find((node) => node.title === name)
return node!.widgets![0].options.values as string[] | undefined
}, nodeName)
}
const getWidgetValue = async (comfyPage: ComfyPage, nodeName: string) => {
return await comfyPage.page.evaluate((name) => {
const node = window['app'].graph.nodes.find((node) => node.title === name)
return node.widgets[0].value
const node = window.app!.graph!.nodes.find((node) => node.title === name)
return node!.widgets![0].value
}, nodeName)
}
const clickRefreshButton = (comfyPage: ComfyPage, nodeName: string) => {
return comfyPage.page.evaluate((name) => {
const node = window['app'].graph.nodes.find((node) => node.title === name)
const buttonWidget = node.widgets.find((w) => w.name === 'refresh')
return buttonWidget?.callback()
const node = window.app!.graph!.nodes.find((node) => node.title === name)
const buttonWidget = node!.widgets!.find((w) => w.name === 'refresh')
return buttonWidget?.callback?.(buttonWidget.value)
}, nodeName)
}
@@ -52,12 +52,12 @@ test.describe('Remote COMBO Widget', { tag: '@widget' }, () => {
}
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
})
test.describe('Loading options', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.page.route(
'**/api/models/checkpoints**',
async (route, request) => {
@@ -89,10 +89,10 @@ test.describe('Remote COMBO Widget', { tag: '@widget' }, () => {
comfyPage
}) => {
const nodeName = 'Remote Widget Node'
await comfyPage.loadWorkflow('inputs/remote_widget')
await comfyPage.workflow.loadWorkflow('inputs/remote_widget')
const node = await comfyPage.page.evaluate((name) => {
return window['app'].graph.nodes.find((node) => node.title === name)
return window.app!.graph!.nodes.find((node) => node.title === name)
}, nodeName)
expect(node).toBeDefined()
@@ -176,7 +176,7 @@ test.describe('Remote COMBO Widget', { tag: '@widget' }, () => {
test('refresh button is visible in selection toolbar when node is selected', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
await comfyPage.settings.setSetting('Comfy.Canvas.SelectionToolbox', true)
const nodeName = 'Remote Widget Node'
await addRemoteWidgetNode(comfyPage, nodeName)
@@ -196,7 +196,7 @@ test.describe('Remote COMBO Widget', { tag: '@widget' }, () => {
// Fulfill each request with a unique timestamp
await comfyPage.page.route(
'**/api/models/checkpoints**',
async (route, request) => {
async (route, _request) => {
await route.fulfill({
body: JSON.stringify([Date.now()]),
status: 200
@@ -257,13 +257,11 @@ test.describe('Remote COMBO Widget', { tag: '@widget' }, () => {
await addRemoteWidgetNode(comfyPage, nodeName)
await waitForWidgetUpdate(comfyPage)
// Wait for timeout and backoff, then force re-render, repeat
const requestTimeout = 512
await comfyPage.page.waitForTimeout(requestTimeout)
await waitForWidgetUpdate(comfyPage)
await comfyPage.page.waitForTimeout(requestTimeout * 2)
await waitForWidgetUpdate(comfyPage)
await comfyPage.page.waitForTimeout(requestTimeout * 3)
// Wait for exponential backoff retries to accumulate timestamps
await expect(async () => {
await waitForWidgetUpdate(comfyPage)
expect(timestamps.length).toBeGreaterThanOrEqual(3)
}).toPass({ timeout: 10000, intervals: [500, 1000, 1500] })
// Verify exponential backoff between retries
const intervals = timestamps.slice(1).map((t, i) => t - timestamps[i])

View File

@@ -5,16 +5,16 @@ import { getMiddlePoint } from '../fixtures/utils/litegraphUtils'
test.describe('Reroute Node', { tag: ['@screenshot', '@node'] }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
})
test.afterEach(async ({ comfyPage }) => {
await comfyPage.setupWorkflowsDirectory({})
await comfyPage.workflow.setupWorkflowsDirectory({})
})
test('loads from inserted workflow', async ({ comfyPage }) => {
const workflowName = 'single_connected_reroute_node.json'
await comfyPage.setupWorkflowsDirectory({
await comfyPage.workflow.setupWorkflowsDirectory({
[workflowName]: 'links/single_connected_reroute_node.json'
})
await comfyPage.setup()
@@ -43,12 +43,12 @@ test.describe(
{ tag: ['@screenshot', '@node'] },
() => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.setSetting('LiteGraph.Reroute.SplineOffset', 80)
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.settings.setSetting('LiteGraph.Reroute.SplineOffset', 80)
})
test('loads from workflow', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('reroute/native_reroute')
await comfyPage.workflow.loadWorkflow('reroute/native_reroute')
await expect(comfyPage.canvas).toHaveScreenshot('native_reroute.png')
})
@@ -56,10 +56,10 @@ test.describe(
comfyPage
}) => {
const loadCheckpointNode = (
await comfyPage.getNodeRefsByTitle('Load Checkpoint')
await comfyPage.nodeOps.getNodeRefsByTitle('Load Checkpoint')
)[0]
const clipEncodeNode = (
await comfyPage.getNodeRefsByTitle('CLIP Text Encode (Prompt)')
await comfyPage.nodeOps.getNodeRefsByTitle('CLIP Text Encode (Prompt)')
)[0]
const slot1 = await loadCheckpointNode.getOutput(1)
@@ -82,10 +82,10 @@ test.describe(
comfyPage
}) => {
const loadCheckpointNode = (
await comfyPage.getNodeRefsByTitle('Load Checkpoint')
await comfyPage.nodeOps.getNodeRefsByTitle('Load Checkpoint')
)[0]
const clipEncodeNode = (
await comfyPage.getNodeRefsByTitle('CLIP Text Encode (Prompt)')
await comfyPage.nodeOps.getNodeRefsByTitle('CLIP Text Encode (Prompt)')
)[0]
const slot1 = await loadCheckpointNode.getOutput(1)
@@ -109,7 +109,7 @@ test.describe(
comfyPage
}) => {
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/4695
await comfyPage.loadWorkflow(
await comfyPage.workflow.loadWorkflow(
'reroute/single-native-reroute-default-workflow'
)

View File

@@ -2,9 +2,10 @@ import { expect } from '@playwright/test'
import { NodeBadgeMode } from '../../src/types/nodeSource'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { DefaultGraphPositions } from '../fixtures/constants/defaultGraphPositions'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe(
@@ -12,7 +13,7 @@ test.describe(
{ tag: ['@screenshot', '@ui'] },
() => {
test('Can add node', async ({ comfyPage }) => {
await comfyPage.rightClickCanvas()
await comfyPage.canvasOps.rightClick()
await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png')
await comfyPage.page.getByText('Add Node').click()
await comfyPage.nextFrame()
@@ -24,7 +25,7 @@ test.describe(
})
test('Can add group', async ({ comfyPage }) => {
await comfyPage.rightClickCanvas()
await comfyPage.canvasOps.rightClick()
await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png')
await comfyPage.page.getByText('Add Group', { exact: true }).click()
await comfyPage.nextFrame()
@@ -34,13 +35,16 @@ test.describe(
})
test('Can convert to group node', async ({ comfyPage }) => {
await comfyPage.select2Nodes()
await comfyPage.nodeOps.selectNodes(['CLIP Text Encode (Prompt)'])
await expect(comfyPage.canvas).toHaveScreenshot('selected-2-nodes.png')
await comfyPage.rightClickCanvas()
await comfyPage.clickContextMenuItem('Convert to Group Node (Deprecated)')
await comfyPage.promptDialogInput.fill('GroupNode2CLIP')
await comfyPage.canvasOps.rightClick()
await comfyPage.contextMenu.clickMenuItem(
'Convert to Group Node (Deprecated)'
)
await comfyPage.nextFrame()
await comfyPage.nodeOps.promptDialogInput.fill('GroupNode2CLIP')
await comfyPage.page.keyboard.press('Enter')
await comfyPage.promptDialogInput.waitFor({ state: 'hidden' })
await comfyPage.nodeOps.promptDialogInput.waitFor({ state: 'hidden' })
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'right-click-node-group-node.png'
@@ -51,7 +55,12 @@ test.describe(
test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => {
test('Can open properties panel', async ({ comfyPage }) => {
await comfyPage.rightClickEmptyLatentNode()
await comfyPage.canvas.click({
position: DefaultGraphPositions.emptyLatentWidgetClick,
button: 'right'
})
await comfyPage.page.mouse.move(10, 10)
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
await comfyPage.page.getByText('Properties Panel').click()
await comfyPage.nextFrame()
@@ -61,7 +70,12 @@ test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => {
})
test('Can collapse', async ({ comfyPage }) => {
await comfyPage.rightClickEmptyLatentNode()
await comfyPage.canvas.click({
position: DefaultGraphPositions.emptyLatentWidgetClick,
button: 'right'
})
await comfyPage.page.mouse.move(10, 10)
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
await comfyPage.page.getByText('Collapse').click()
await comfyPage.nextFrame()
@@ -71,16 +85,21 @@ test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => {
})
test('Can collapse (Node Badge)', async ({ comfyPage }) => {
await comfyPage.setSetting(
await comfyPage.settings.setSetting(
'Comfy.NodeBadge.NodeIdBadgeMode',
NodeBadgeMode.ShowAll
)
await comfyPage.setSetting(
await comfyPage.settings.setSetting(
'Comfy.NodeBadge.NodeSourceBadgeMode',
NodeBadgeMode.ShowAll
)
await comfyPage.rightClickEmptyLatentNode()
await comfyPage.canvas.click({
position: DefaultGraphPositions.emptyLatentWidgetClick,
button: 'right'
})
await comfyPage.page.mouse.move(10, 10)
await comfyPage.nextFrame()
await comfyPage.page.getByText('Collapse').click()
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
@@ -89,7 +108,12 @@ test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => {
})
test('Can bypass', async ({ comfyPage }) => {
await comfyPage.rightClickEmptyLatentNode()
await comfyPage.canvas.click({
position: DefaultGraphPositions.emptyLatentWidgetClick,
button: 'right'
})
await comfyPage.page.mouse.move(10, 10)
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
await comfyPage.page.getByText('Bypass').click()
await comfyPage.nextFrame()
@@ -99,46 +123,89 @@ test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => {
})
test('Can pin and unpin', async ({ comfyPage }) => {
await comfyPage.rightClickEmptyLatentNode()
await comfyPage.canvas.click({
position: DefaultGraphPositions.emptyLatentWidgetClick,
button: 'right'
})
await comfyPage.page.mouse.move(10, 10)
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
await comfyPage.page.click('.litemenu-entry:has-text("Pin")')
await comfyPage.nextFrame()
await comfyPage.dragAndDrop({ x: 621, y: 617 }, { x: 16, y: 16 })
// Get EmptyLatentImage node title position dynamically (for dragging)
const emptyLatentNode = await comfyPage.nodeOps.getNodeRefById(5)
const titlePos = await emptyLatentNode.getTitlePosition()
await comfyPage.canvasOps.dragAndDrop(titlePos, { x: 16, y: 16 })
await expect(comfyPage.canvas).toHaveScreenshot('node-pinned.png')
await comfyPage.rightClickEmptyLatentNode()
await comfyPage.canvas.click({
position: DefaultGraphPositions.emptyLatentWidgetClick,
button: 'right'
})
await comfyPage.page.mouse.move(10, 10)
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'right-click-pinned-node.png'
)
await comfyPage.page.click('.litemenu-entry:has-text("Unpin")')
await comfyPage.nextFrame()
await comfyPage.rightClickEmptyLatentNode()
await comfyPage.canvas.click({
position: DefaultGraphPositions.emptyLatentWidgetClick,
button: 'right'
})
await comfyPage.page.mouse.move(10, 10)
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'right-click-unpinned-node.png'
)
})
test('Can move after unpin', async ({ comfyPage }) => {
await comfyPage.rightClickEmptyLatentNode()
await comfyPage.canvas.click({
position: DefaultGraphPositions.emptyLatentWidgetClick,
button: 'right'
})
await comfyPage.page.mouse.move(10, 10)
await comfyPage.nextFrame()
await comfyPage.page.click('.litemenu-entry:has-text("Pin")')
await comfyPage.nextFrame()
await comfyPage.rightClickEmptyLatentNode()
await comfyPage.canvas.click({
position: DefaultGraphPositions.emptyLatentWidgetClick,
button: 'right'
})
await comfyPage.page.mouse.move(10, 10)
await comfyPage.nextFrame()
await comfyPage.page.click('.litemenu-entry:has-text("Unpin")')
await comfyPage.nextFrame()
await comfyPage.dragAndDrop({ x: 496, y: 618 }, { x: 200, y: 590 })
// Get EmptyLatentImage node title position dynamically (for dragging)
const emptyLatentNode = await comfyPage.nodeOps.getNodeRefById(5)
const titlePos = await emptyLatentNode.getTitlePosition()
await comfyPage.canvasOps.dragAndDrop(titlePos, { x: 200, y: 590 })
await expect(comfyPage.canvas).toHaveScreenshot(
'right-click-unpinned-node-moved.png'
)
})
test('Can pin/unpin selected nodes', async ({ comfyPage }) => {
await comfyPage.select2Nodes()
await comfyPage.nodeOps.selectNodes(['CLIP Text Encode (Prompt)'])
await comfyPage.page.keyboard.down('Control')
await comfyPage.rightClickEmptyLatentNode()
await comfyPage.canvas.click({
position: DefaultGraphPositions.emptyLatentWidgetClick,
button: 'right'
})
await comfyPage.page.mouse.move(10, 10)
await comfyPage.nextFrame()
await comfyPage.page.click('.litemenu-entry:has-text("Pin")')
await comfyPage.page.keyboard.up('Control')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('selected-nodes-pinned.png')
await comfyPage.rightClickEmptyLatentNode()
await comfyPage.canvas.click({
position: DefaultGraphPositions.emptyLatentWidgetClick,
button: 'right'
})
await comfyPage.page.mouse.move(10, 10)
await comfyPage.nextFrame()
await comfyPage.page.click('.litemenu-entry:has-text("Unpin")')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
@@ -147,8 +214,8 @@ test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => {
})
test('Can clone pinned nodes', async ({ comfyPage }) => {
const nodeCount = await comfyPage.getGraphNodesCount()
const node = (await comfyPage.getFirstNodeRef())!
const nodeCount = await comfyPage.nodeOps.getGraphNodesCount()
const node = (await comfyPage.nodeOps.getFirstNodeRef())!
await node.clickContextMenuOption('Pin')
await comfyPage.nextFrame()
await node.click('title', { button: 'right' })
@@ -161,6 +228,6 @@ test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => {
await cloneItem.click()
await expect(cloneItem).toHaveCount(0)
await comfyPage.nextFrame()
expect(await comfyPage.getGraphNodesCount()).toBe(nodeCount + 1)
expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe(nodeCount + 1)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 93 KiB

View File

@@ -5,14 +5,14 @@ import { comfyPageFixture } from '../fixtures/ComfyPage'
const test = comfyPageFixture
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
const BLUE_COLOR = 'rgb(51, 51, 85)'
const RED_COLOR = 'rgb(85, 51, 51)'
test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
await comfyPage.settings.setSetting('Comfy.Canvas.SelectionToolbox', true)
})
test('shows selection toolbox', async ({ comfyPage }) => {
@@ -20,7 +20,10 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
await expect(comfyPage.selectionToolbox).not.toBeVisible()
// Select multiple nodes
await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)'])
await comfyPage.nodeOps.selectNodes([
'KSampler',
'CLIP Text Encode (Prompt)'
])
// Selection toolbox should be visible with multiple nodes selected
await expect(comfyPage.selectionToolbox).toBeVisible()
@@ -33,11 +36,11 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
test('shows at correct position when node is pasted', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('nodes/single_ksampler')
await comfyPage.selectNodes(['KSampler'])
await comfyPage.ctrlC()
await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
await comfyPage.nodeOps.selectNodes(['KSampler'])
await comfyPage.clipboard.copy()
await comfyPage.page.mouse.move(100, 100)
await comfyPage.ctrlV()
await comfyPage.clipboard.paste()
const toolboxContainer = comfyPage.selectionToolbox
await expect(toolboxContainer).toBeVisible()
@@ -54,8 +57,8 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
test('hide when select and drag happen at the same time', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('nodes/single_ksampler')
const node = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
const node = (await comfyPage.nodeOps.getNodeRefsByTitle('KSampler'))[0]
const nodePos = await node.getPosition()
// Drag on the title of the node
@@ -68,7 +71,7 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
test('shows border only with multiple selections', async ({ comfyPage }) => {
// Select single node
await comfyPage.selectNodes(['KSampler'])
await comfyPage.nodeOps.selectNodes(['KSampler'])
// Selection toolbox should be visible but without border
await expect(comfyPage.selectionToolbox).toBeVisible()
@@ -78,7 +81,10 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
)
// Select multiple nodes
await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)'])
await comfyPage.nodeOps.selectNodes([
'KSampler',
'CLIP Text Encode (Prompt)'
])
// Selection border should show with multiple selections (canvas-based)
await expect(comfyPage.canvas).toHaveScreenshot(
@@ -86,7 +92,7 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
)
// Deselect to single node
await comfyPage.selectNodes(['CLIP Text Encode (Prompt)'])
await comfyPage.nodeOps.selectNodes(['CLIP Text Encode (Prompt)'])
// Border should be hidden again (canvas-based)
await expect(comfyPage.canvas).toHaveScreenshot(
@@ -98,7 +104,7 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
comfyPage
}) => {
// A group + a KSampler node
await comfyPage.loadWorkflow('groups/single_group')
await comfyPage.workflow.loadWorkflow('groups/single_group')
// Select group + node should show bypass button
await comfyPage.page.focus('canvas')
@@ -110,7 +116,7 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
).toBeVisible()
// Deselect node (Only group is selected) should hide bypass button
await comfyPage.selectNodes(['KSampler'])
await comfyPage.nodeOps.selectNodes(['KSampler'])
await expect(
comfyPage.page.locator(
'.selection-toolbox *[data-testid="bypass-button"]'
@@ -123,7 +129,7 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
comfyPage
}) => {
// Select a node
await comfyPage.selectNodes(['KSampler'])
await comfyPage.nodeOps.selectNodes(['KSampler'])
// Color picker button should be visible
const colorPickerButton = comfyPage.page.locator(
@@ -151,7 +157,9 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
// Node should have the selected color class/style
// Note: Exact verification method depends on how color is applied to nodes
const selectedNode = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
const selectedNode = (
await comfyPage.nodeOps.getNodeRefsByTitle('KSampler')
)[0]
expect(await selectedNode.getProperty('color')).not.toBeNull()
})
@@ -159,7 +167,10 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
comfyPage
}) => {
// Select multiple nodes
await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)'])
await comfyPage.nodeOps.selectNodes([
'KSampler',
'CLIP Text Encode (Prompt)'
])
const colorPickerButton = comfyPage.page.locator(
'.selection-toolbox .pi-circle-fill'
@@ -183,22 +194,25 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
comfyPage
}) => {
// Select first node and color it
await comfyPage.selectNodes(['KSampler'])
await comfyPage.nodeOps.selectNodes(['KSampler'])
await comfyPage.page.locator('.selection-toolbox .pi-circle-fill').click()
await comfyPage.page
.locator('.color-picker-container i[data-testid="blue"]')
.click()
await comfyPage.selectNodes(['KSampler'])
await comfyPage.nodeOps.selectNodes(['KSampler'])
// Select second node and color it differently
await comfyPage.selectNodes(['CLIP Text Encode (Prompt)'])
await comfyPage.nodeOps.selectNodes(['CLIP Text Encode (Prompt)'])
await comfyPage.page.locator('.selection-toolbox .pi-circle-fill').click()
await comfyPage.page
.locator('.color-picker-container i[data-testid="red"]')
.click()
// Select both nodes
await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)'])
await comfyPage.nodeOps.selectNodes([
'KSampler',
'CLIP Text Encode (Prompt)'
])
// Color picker should show null/mixed state
const colorPickerButton = comfyPage.page.locator(
@@ -211,17 +225,17 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
comfyPage
}) => {
// First color a node
await comfyPage.selectNodes(['KSampler'])
await comfyPage.nodeOps.selectNodes(['KSampler'])
await comfyPage.page.locator('.selection-toolbox .pi-circle-fill').click()
await comfyPage.page
.locator('.color-picker-container i[data-testid="blue"]')
.click()
// Clear selection
await comfyPage.selectNodes(['KSampler'])
await comfyPage.nodeOps.selectNodes(['KSampler'])
// Re-select the node
await comfyPage.selectNodes(['KSampler'])
await comfyPage.nodeOps.selectNodes(['KSampler'])
// Color picker button should show the correct color
const colorPickerButton = comfyPage.page.locator(
@@ -234,7 +248,7 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
comfyPage
}) => {
// Select a node and color it
await comfyPage.selectNodes(['KSampler'])
await comfyPage.nodeOps.selectNodes(['KSampler'])
await comfyPage.page.locator('.selection-toolbox .pi-circle-fill').click()
await comfyPage.page
.locator('.color-picker-container i[data-testid="blue"]')
@@ -245,7 +259,9 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
await comfyPage.nextFrame()
// Node should be uncolored again
const selectedNode = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
const selectedNode = (
await comfyPage.nodeOps.getNodeRefsByTitle('KSampler')
)[0]
expect(await selectedNode.getProperty('color')).toBeUndefined()
})
})

View File

@@ -4,7 +4,7 @@ import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import type { ComfyPage } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe(
@@ -12,15 +12,16 @@ test.describe(
{ tag: '@ui' },
() => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
await comfyPage.loadWorkflow('nodes/single_ksampler')
await comfyPage.settings.setSetting('Comfy.Canvas.SelectionToolbox', true)
await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
await comfyPage.nextFrame()
await comfyPage.selectNodes(['KSampler'])
await comfyPage.nodeOps.selectNodes(['KSampler'])
await comfyPage.nextFrame()
})
const openMoreOptions = async (comfyPage: ComfyPage) => {
const ksamplerNodes = await comfyPage.getNodeRefsByTitle('KSampler')
const ksamplerNodes =
await comfyPage.nodeOps.getNodeRefsByTitle('KSampler')
if (ksamplerNodes.length === 0) {
throw new Error('No KSampler nodes found')
}
@@ -28,9 +29,14 @@ test.describe(
// Drag the KSampler to the center of the screen
const nodePos = await ksamplerNodes[0].getPosition()
const viewportSize = comfyPage.page.viewportSize()
if (!viewportSize) {
throw new Error(
'Viewport size is null - page may not be properly initialized'
)
}
const centerX = viewportSize.width / 3
const centerY = viewportSize.height / 2
await comfyPage.dragAndDrop(
await comfyPage.canvasOps.dragAndDrop(
{ x: nodePos.x, y: nodePos.y },
{ x: centerX, y: centerY }
)
@@ -85,7 +91,9 @@ test.describe(
})
test('changes node shape via Shape submenu', async ({ comfyPage }) => {
const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
const nodeRef = (
await comfyPage.nodeOps.getNodeRefsByTitle('KSampler')
)[0]
const initialShape = await nodeRef.getProperty<number>('shape')
await openMoreOptions(comfyPage)
@@ -106,7 +114,9 @@ test.describe(
test('changes node color via Color submenu swatch', async ({
comfyPage
}) => {
const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
const nodeRef = (
await comfyPage.nodeOps.getNodeRefsByTitle('KSampler')
)[0]
const initialColor = await nodeRef.getProperty<string | undefined>(
'color'
)
@@ -126,7 +136,9 @@ test.describe(
})
test('renames a node using Rename action', async ({ comfyPage }) => {
const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
const nodeRef = (
await comfyPage.nodeOps.getNodeRefsByTitle('KSampler')
)[0]
await openMoreOptions(comfyPage)
await comfyPage.page
.getByText('Rename', { exact: true })

View File

@@ -4,9 +4,12 @@ import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
test.describe('Node library sidebar', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [])
await comfyPage.setSetting('Comfy.NodeLibrary.BookmarksCustomization', {})
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [])
await comfyPage.settings.setSetting(
'Comfy.NodeLibrary.BookmarksCustomization',
{}
)
// Open the sidebar
const tab = comfyPage.menu.nodeLibraryTab
await tab.open()
@@ -26,7 +29,7 @@ test.describe('Node library sidebar', () => {
)
expect(previewVisible).toBe(true)
const count = await comfyPage.getGraphNodesCount()
const count = await comfyPage.nodeOps.getGraphNodesCount()
// Drag the node onto the canvas
const canvasSelector = '#graph-canvas'
@@ -46,7 +49,7 @@ test.describe('Node library sidebar', () => {
})
// Verify the node is added to the canvas
expect(await comfyPage.getGraphNodesCount()).toBe(count + 1)
expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe(count + 1)
})
test('Bookmark node', async ({ comfyPage }) => {
@@ -58,7 +61,7 @@ test.describe('Node library sidebar', () => {
// Verify the bookmark is added to the bookmarks tab
expect(
await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
).toEqual(['KSamplerAdvanced'])
// Verify the bookmark node with the same name is added to the tree.
expect(await tab.getNode('KSampler (Advanced)').count()).toBe(2)
@@ -69,7 +72,9 @@ test.describe('Node library sidebar', () => {
})
test('Ignores unrecognized node', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo'])
await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [
'foo'
])
const tab = comfyPage.menu.nodeLibraryTab
expect(await tab.getFolder('sampling').count()).toBe(1)
@@ -77,7 +82,9 @@ test.describe('Node library sidebar', () => {
})
test('Displays empty bookmarks folder', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/'])
await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [
'foo/'
])
const tab = comfyPage.menu.nodeLibraryTab
expect(await tab.getFolder('foo').count()).toBe(1)
})
@@ -91,12 +98,14 @@ test.describe('Node library sidebar', () => {
await textInput.press('Enter')
expect(await tab.getFolder('New Folder').count()).toBe(1)
expect(
await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
).toEqual(['New Folder/'])
})
test('Can add nested bookmark folder', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/'])
await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [
'foo/'
])
const tab = comfyPage.menu.nodeLibraryTab
await tab.getFolder('foo').click({ button: 'right' })
@@ -108,24 +117,28 @@ test.describe('Node library sidebar', () => {
expect(await tab.getFolder('bar').count()).toBe(1)
expect(
await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
).toEqual(['foo/', 'foo/bar/'])
})
test('Can delete bookmark folder', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/'])
await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [
'foo/'
])
const tab = comfyPage.menu.nodeLibraryTab
await tab.getFolder('foo').click({ button: 'right' })
await comfyPage.page.getByLabel('Delete').click()
expect(
await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
).toEqual([])
})
test('Can rename bookmark folder', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/'])
await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [
'foo/'
])
const tab = comfyPage.menu.nodeLibraryTab
await tab.getFolder('foo').click({ button: 'right' })
@@ -136,14 +149,16 @@ test.describe('Node library sidebar', () => {
await comfyPage.page.keyboard.press('Enter')
expect(
await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
).toEqual(['bar/'])
})
test('Can add bookmark by dragging node to bookmark folder', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/'])
await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [
'foo/'
])
const tab = comfyPage.menu.nodeLibraryTab
await tab.getFolder('sampling').click()
await comfyPage.page.dragAndDrop(
@@ -151,7 +166,7 @@ test.describe('Node library sidebar', () => {
tab.folderSelector('foo')
)
expect(
await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
).toEqual(['foo/', 'foo/KSamplerAdvanced'])
})
@@ -162,51 +177,60 @@ test.describe('Node library sidebar', () => {
await tab.getFolder('sampling').click()
await tab.getNode('KSampler (Advanced)').locator('.bookmark-button').click()
expect(
await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
).toEqual(['KSamplerAdvanced'])
})
test('Can unbookmark node (Top level bookmark)', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [
await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [
'KSamplerAdvanced'
])
const tab = comfyPage.menu.nodeLibraryTab
await tab.getNode('KSampler (Advanced)').locator('.bookmark-button').click()
expect(
await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
).toEqual([])
})
test('Can unbookmark node (Library node bookmark)', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [
await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [
'KSamplerAdvanced'
])
const tab = comfyPage.menu.nodeLibraryTab
await tab.getFolder('sampling').click()
await comfyPage.page
.locator(tab.nodeSelector('KSampler (Advanced)'))
.nth(1)
await tab
.getNodeInFolder('KSampler (Advanced)', 'sampling')
.locator('.bookmark-button')
.click()
expect(
await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
).toEqual([])
})
test('Can customize icon', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/'])
await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [
'foo/'
])
const tab = comfyPage.menu.nodeLibraryTab
await tab.getFolder('foo').click({ button: 'right' })
await comfyPage.page.getByLabel('Customize').click()
await comfyPage.page
.locator('.icon-field .p-selectbutton > *:nth-child(2)')
.click()
await comfyPage.page
.locator('.color-field .p-selectbutton > *:nth-child(2)')
.click()
await comfyPage.page.getByRole('button', { name: 'Confirm' }).click()
const dialog = comfyPage.page.getByRole('dialog', {
name: 'Customize Folder'
})
// Select Folder icon (2nd button in Icon group)
const iconGroup = dialog.getByText('Icon').locator('..').getByRole('group')
await iconGroup.getByRole('button').nth(1).click()
// Select Blue color (2nd button in Color group)
const colorGroup = dialog
.getByText('Color')
.locator('..')
.getByRole('group')
await colorGroup.getByRole('button').nth(1).click()
await dialog.getByRole('button', { name: 'Confirm' }).click()
await comfyPage.nextFrame()
expect(
await comfyPage.getSetting('Comfy.NodeLibrary.BookmarksCustomization')
await comfyPage.settings.getSetting(
'Comfy.NodeLibrary.BookmarksCustomization'
)
).toEqual({
'foo/': {
icon: 'pi-folder',
@@ -216,17 +240,24 @@ test.describe('Node library sidebar', () => {
})
// If color is left as default, it should not be saved
test('Can customize icon (default field)', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/'])
await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [
'foo/'
])
const tab = comfyPage.menu.nodeLibraryTab
await tab.getFolder('foo').click({ button: 'right' })
await comfyPage.page.getByLabel('Customize').click()
await comfyPage.page
.locator('.icon-field .p-selectbutton > *:nth-child(2)')
.click()
await comfyPage.page.getByRole('button', { name: 'Confirm' }).click()
const dialog = comfyPage.page.getByRole('dialog', {
name: 'Customize Folder'
})
// Select Folder icon (2nd button in Icon group)
const iconGroup = dialog.getByText('Icon').locator('..').getByRole('group')
await iconGroup.getByRole('button').nth(1).click()
await dialog.getByRole('button', { name: 'Confirm' }).click()
await comfyPage.nextFrame()
expect(
await comfyPage.getSetting('Comfy.NodeLibrary.BookmarksCustomization')
await comfyPage.settings.getSetting(
'Comfy.NodeLibrary.BookmarksCustomization'
)
).toEqual({
'foo/': {
icon: 'pi-folder'
@@ -238,7 +269,9 @@ test.describe('Node library sidebar', () => {
comfyPage
}) => {
// Open customization dialog
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/'])
await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [
'foo/'
])
const tab = comfyPage.menu.nodeLibraryTab
await tab.getFolder('foo').click({ button: 'right' })
await comfyPage.page.getByLabel('Customize').click()
@@ -258,16 +291,19 @@ test.describe('Node library sidebar', () => {
await comfyPage.page.locator('.p-colorpicker-color-background').click()
// Finalize the customization
await comfyPage.page
.locator('.icon-field .p-selectbutton > *:nth-child(2)')
.click()
await comfyPage.page.getByRole('button', { name: 'Confirm' }).click()
const dialog = comfyPage.page.getByRole('dialog', {
name: 'Customize Folder'
})
// Select Folder icon (2nd button in Icon group)
const iconGroup = dialog.getByText('Icon').locator('..').getByRole('group')
await iconGroup.getByRole('button').nth(1).click()
await dialog.getByRole('button', { name: 'Confirm' }).click()
await comfyPage.nextFrame()
// Verify the color selection is saved
const setting = await comfyPage.getSetting(
'Comfy.NodeLibrary.BookmarksCustomization'
)
const setting = await comfyPage.settings.getSetting<
Record<string, { icon?: string; color?: string }>
>('Comfy.NodeLibrary.BookmarksCustomization')
await expect(setting).toHaveProperty(['foo/', 'color'])
await expect(setting['foo/'].color).not.toBeNull()
await expect(setting['foo/'].color).not.toBeUndefined()
@@ -275,13 +311,18 @@ test.describe('Node library sidebar', () => {
})
test('Can rename customized bookmark folder', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/'])
await comfyPage.setSetting('Comfy.NodeLibrary.BookmarksCustomization', {
'foo/': {
icon: 'pi-folder',
color: '#007bff'
await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [
'foo/'
])
await comfyPage.settings.setSetting(
'Comfy.NodeLibrary.BookmarksCustomization',
{
'foo/': {
icon: 'pi-folder',
color: '#007bff'
}
}
})
)
const tab = comfyPage.menu.nodeLibraryTab
await tab.getFolder('foo').click({ button: 'right' })
await comfyPage.page
@@ -292,10 +333,12 @@ test.describe('Node library sidebar', () => {
await comfyPage.nextFrame()
await expect(async () => {
expect(
await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
).toEqual(['bar/'])
expect(
await comfyPage.getSetting('Comfy.NodeLibrary.BookmarksCustomization')
await comfyPage.settings.getSetting(
'Comfy.NodeLibrary.BookmarksCustomization'
)
).toEqual({
'bar/': {
icon: 'pi-folder',
@@ -308,27 +351,34 @@ test.describe('Node library sidebar', () => {
})
test('Can delete customized bookmark folder', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/'])
await comfyPage.setSetting('Comfy.NodeLibrary.BookmarksCustomization', {
'foo/': {
icon: 'pi-folder',
color: '#007bff'
await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [
'foo/'
])
await comfyPage.settings.setSetting(
'Comfy.NodeLibrary.BookmarksCustomization',
{
'foo/': {
icon: 'pi-folder',
color: '#007bff'
}
}
})
)
const tab = comfyPage.menu.nodeLibraryTab
await tab.getFolder('foo').click({ button: 'right' })
await comfyPage.page.getByLabel('Delete').click()
await comfyPage.nextFrame()
expect(
await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
).toEqual([])
expect(
await comfyPage.getSetting('Comfy.NodeLibrary.BookmarksCustomization')
await comfyPage.settings.getSetting(
'Comfy.NodeLibrary.BookmarksCustomization'
)
).toEqual({})
})
test('Can filter nodes in both trees', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [
await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [
'foo/',
'foo/KSamplerAdvanced',
'KSampler'
@@ -336,8 +386,6 @@ test.describe('Node library sidebar', () => {
const tab = comfyPage.menu.nodeLibraryTab
await tab.nodeLibrarySearchBoxInput.fill('KSampler')
// Node search box is debounced and may take some time to update.
await comfyPage.page.waitForTimeout(1000)
expect(await tab.getNode('KSampler (Advanced)').count()).toBe(2)
await expect(tab.getNode('KSampler (Advanced)')).toHaveCount(2)
})
})

View File

@@ -4,8 +4,11 @@ import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
test.describe('Workflows sidebar', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting('Comfy.Workflow.WorkflowTabsPosition', 'Sidebar')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting(
'Comfy.Workflow.WorkflowTabsPosition',
'Sidebar'
)
// Open the sidebar
const tab = comfyPage.menu.workflowsTab
@@ -13,7 +16,7 @@ test.describe('Workflows sidebar', () => {
})
test.afterEach(async ({ comfyPage }) => {
await comfyPage.setupWorkflowsDirectory({})
await comfyPage.workflow.setupWorkflowsDirectory({})
})
test('Can create new blank workflow', async ({ comfyPage }) => {
@@ -22,7 +25,7 @@ test.describe('Workflows sidebar', () => {
'*Unsaved Workflow.json'
])
await comfyPage.executeCommand('Comfy.NewBlankWorkflow')
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
expect(await tab.getOpenedWorkflowNames()).toEqual([
'*Unsaved Workflow.json',
'*Unsaved Workflow (2).json'
@@ -30,7 +33,7 @@ test.describe('Workflows sidebar', () => {
})
test('Can show top level saved workflows', async ({ comfyPage }) => {
await comfyPage.setupWorkflowsDirectory({
await comfyPage.workflow.setupWorkflowsDirectory({
'workflow1.json': 'default.json',
'workflow2.json': 'default.json'
})
@@ -50,20 +53,20 @@ test.describe('Workflows sidebar', () => {
expect.arrayContaining(['workflow1.json'])
)
await comfyPage.executeCommand('Comfy.DuplicateWorkflow')
await comfyPage.command.executeCommand('Comfy.DuplicateWorkflow')
expect(await tab.getOpenedWorkflowNames()).toEqual([
'workflow1.json',
'*workflow1 (Copy).json'
])
await comfyPage.executeCommand('Comfy.DuplicateWorkflow')
await comfyPage.command.executeCommand('Comfy.DuplicateWorkflow')
expect(await tab.getOpenedWorkflowNames()).toEqual([
'workflow1.json',
'*workflow1 (Copy).json',
'*workflow1 (Copy) (2).json'
])
await comfyPage.executeCommand('Comfy.DuplicateWorkflow')
await comfyPage.command.executeCommand('Comfy.DuplicateWorkflow')
expect(await tab.getOpenedWorkflowNames()).toEqual([
'workflow1.json',
'*workflow1 (Copy).json',
@@ -73,28 +76,30 @@ test.describe('Workflows sidebar', () => {
})
test('Can open workflow after insert', async ({ comfyPage }) => {
await comfyPage.setupWorkflowsDirectory({
await comfyPage.workflow.setupWorkflowsDirectory({
'workflow1.json': 'nodes/single_ksampler.json'
})
const tab = comfyPage.menu.workflowsTab
await tab.open()
await comfyPage.executeCommand('Comfy.LoadDefaultWorkflow')
const originalNodeCount = (await comfyPage.getNodes()).length
await comfyPage.command.executeCommand('Comfy.LoadDefaultWorkflow')
const originalNodeCount = (await comfyPage.nodeOps.getNodes()).length
await tab.insertWorkflow(tab.getPersistedItem('workflow1.json'))
await comfyPage.nextFrame()
expect((await comfyPage.getNodes()).length).toEqual(originalNodeCount + 1)
await expect
.poll(() => comfyPage.nodeOps.getNodes().then((n) => n.length))
.toEqual(originalNodeCount + 1)
await tab.getPersistedItem('workflow1.json').click()
await comfyPage.nextFrame()
expect((await comfyPage.getNodes()).length).toEqual(1)
await expect
.poll(() => comfyPage.nodeOps.getNodes().then((n) => n.length))
.toEqual(1)
})
test('Can rename nested workflow from opened workflow item', async ({
comfyPage
}) => {
await comfyPage.setupWorkflowsDirectory({
await comfyPage.workflow.setupWorkflowsDirectory({
foo: {
'bar.json': 'default.json'
}
@@ -116,7 +121,7 @@ test.describe('Workflows sidebar', () => {
})
test('Can save workflow as', async ({ comfyPage }) => {
await comfyPage.executeCommand('Comfy.NewBlankWorkflow')
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
await comfyPage.menu.topbar.saveWorkflowAs('workflow3.json')
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
'*Unsaved Workflow.json',
@@ -134,17 +139,17 @@ test.describe('Workflows sidebar', () => {
test('Exported workflow does not contain localized slot names', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('default')
const exportedWorkflow = await comfyPage.getExportedWorkflow({
await comfyPage.workflow.loadWorkflow('default')
const exportedWorkflow = await comfyPage.workflow.getExportedWorkflow({
api: false
})
expect(exportedWorkflow).toBeDefined()
for (const node of exportedWorkflow.nodes) {
for (const slot of node.inputs) {
for (const slot of node.inputs ?? []) {
expect(slot.localized_name).toBeUndefined()
expect(slot.label).toBeUndefined()
}
for (const slot of node.outputs) {
for (const slot of node.outputs ?? []) {
expect(slot.localized_name).toBeUndefined()
expect(slot.label).toBeUndefined()
}
@@ -154,7 +159,7 @@ test.describe('Workflows sidebar', () => {
test('Can export same workflow with different locales', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('default')
await comfyPage.workflow.loadWorkflow('default')
// Setup download listener before triggering the export
const downloadPromise = comfyPage.page.waitForEvent('download')
@@ -165,14 +170,14 @@ test.describe('Workflows sidebar', () => {
expect(download.suggestedFilename()).toBe('exported_default.json')
// Get the exported workflow content
const downloadedContent = await comfyPage.getExportedWorkflow({
const downloadedContent = await comfyPage.workflow.getExportedWorkflow({
api: false
})
await comfyPage.setSetting('Comfy.Locale', 'zh')
await comfyPage.settings.setSetting('Comfy.Locale', 'zh')
await comfyPage.setup()
const downloadedContentZh = await comfyPage.getExportedWorkflow({
const downloadedContentZh = await comfyPage.workflow.getExportedWorkflow({
api: false
})
@@ -199,7 +204,7 @@ test.describe('Workflows sidebar', () => {
test('Can save temporary workflow with unmodified name', async ({
comfyPage
}) => {
expect(await comfyPage.isCurrentWorkflowModified()).toBe(false)
expect(await comfyPage.workflow.isCurrentWorkflowModified()).toBe(false)
await comfyPage.menu.topbar.saveWorkflow('Unsaved Workflow')
// Should not trigger the overwrite dialog
@@ -207,7 +212,7 @@ test.describe('Workflows sidebar', () => {
await comfyPage.page.locator('.comfy-modal-content:visible').count()
).toBe(0)
expect(await comfyPage.isCurrentWorkflowModified()).toBe(false)
expect(await comfyPage.workflow.isCurrentWorkflowModified()).toBe(false)
})
test('Can overwrite other workflows with save as', async ({ comfyPage }) => {
@@ -238,12 +243,16 @@ test.describe('Workflows sidebar', () => {
test('Does not report warning when switching between opened workflows', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('missing/missing_nodes')
await comfyPage.closeDialog()
await comfyPage.workflow.loadWorkflow('missing/missing_nodes')
await comfyPage.page
.locator('.p-dialog')
.getByRole('button', { name: 'Close' })
.click({ force: true })
await comfyPage.page.locator('.p-dialog').waitFor({ state: 'hidden' })
// Load blank workflow
await comfyPage.menu.workflowsTab.open()
await comfyPage.executeCommand('Comfy.NewBlankWorkflow')
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
// Switch back to the missing_nodes workflow
await comfyPage.menu.workflowsTab.switchToWorkflow('missing_nodes')
@@ -271,14 +280,14 @@ test.describe('Workflows sidebar', () => {
test('Can close saved workflow with command', async ({ comfyPage }) => {
const tab = comfyPage.menu.workflowsTab
await comfyPage.menu.topbar.saveWorkflow('workflow1.json')
await comfyPage.executeCommand('Workspace.CloseWorkflow')
await comfyPage.command.executeCommand('Workspace.CloseWorkflow')
expect(await tab.getOpenedWorkflowNames()).toEqual([
'*Unsaved Workflow.json'
])
})
test('Can delete workflows (confirm disabled)', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Workflow.ConfirmDelete', false)
await comfyPage.settings.setSetting('Comfy.Workflow.ConfirmDelete', false)
const { topbar, workflowsTab } = comfyPage.menu
@@ -288,7 +297,8 @@ test.describe('Workflows sidebar', () => {
await workflowsTab.getOpenedItem(filename).click({ button: 'right' })
await comfyPage.nextFrame()
await comfyPage.clickContextMenuItem('Delete')
await comfyPage.contextMenu.clickMenuItem('Delete')
await comfyPage.nextFrame()
await expect(workflowsTab.getOpenedItem(filename)).not.toBeVisible()
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([
@@ -304,7 +314,8 @@ test.describe('Workflows sidebar', () => {
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([filename])
await workflowsTab.getOpenedItem(filename).click({ button: 'right' })
await comfyPage.clickContextMenuItem('Delete')
await comfyPage.contextMenu.clickMenuItem('Delete')
await comfyPage.nextFrame()
await comfyPage.confirmDialog.click('delete')
@@ -315,7 +326,7 @@ test.describe('Workflows sidebar', () => {
})
test('Can duplicate workflow from context menu', async ({ comfyPage }) => {
await comfyPage.setupWorkflowsDirectory({
await comfyPage.workflow.setupWorkflowsDirectory({
'workflow1.json': 'default.json'
})
@@ -325,7 +336,8 @@ test.describe('Workflows sidebar', () => {
await workflowsTab
.getPersistedItem('workflow1.json')
.click({ button: 'right' })
await comfyPage.clickContextMenuItem('Duplicate')
await comfyPage.contextMenu.clickMenuItem('Duplicate')
await comfyPage.nextFrame()
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([
'*Unsaved Workflow.json',
@@ -334,7 +346,7 @@ test.describe('Workflows sidebar', () => {
})
test('Can drop workflow from workflows sidebar', async ({ comfyPage }) => {
await comfyPage.setupWorkflowsDirectory({
await comfyPage.workflow.setupWorkflowsDirectory({
'workflow1.json': 'default.json'
})
@@ -345,7 +357,7 @@ test.describe('Workflows sidebar', () => {
comfyPage.menu.workflowsTab.getPersistedItem('workflow1.json')
await expect(workflowItem).toBeVisible({ timeout: 3000 })
const nodeCount = await comfyPage.getGraphNodesCount()
const nodeCount = await comfyPage.nodeOps.getGraphNodesCount()
// Get the bounding box of the canvas element
const canvasBoundingBox = (await comfyPage.page
@@ -366,7 +378,7 @@ test.describe('Workflows sidebar', () => {
// Wait for nodes to be inserted after drag-drop with retryable assertion
await expect
.poll(() => comfyPage.getGraphNodesCount(), { timeout: 3000 })
.poll(() => comfyPage.nodeOps.getGraphNodesCount(), { timeout: 3000 })
.toBe(nodeCount * 2)
})
})

View File

@@ -3,7 +3,6 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
// Constants
const INITIAL_NAME = 'initial_slot_name'
const RENAMED_NAME = 'renamed_slot_name'
const SECOND_RENAMED_NAME = 'second_renamed_name'
@@ -14,26 +13,34 @@ const SELECTORS = {
test.describe('Subgraph Slot Rename Dialog', { tag: '@subgraph' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test('Shows current slot label (not stale) in rename dialog', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph')
const subgraphNode = await comfyPage.getNodeRefById('2')
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2')
await subgraphNode.navigateIntoSubgraph()
// Get initial slot label
const initialInputLabel = await comfyPage.page.evaluate(() => {
const graph = window['app'].canvas.graph
const graph = window.app!.canvas.graph
if (!graph || !('inputNode' in graph)) return null
return graph.inputs?.[0]?.label || graph.inputs?.[0]?.name || null
})
if (initialInputLabel === null) {
throw new Error(
'Expected subgraph to have an input slot label for rightClickInputSlot'
)
}
// First rename
await comfyPage.rightClickSubgraphInputSlot(initialInputLabel)
await comfyPage.clickLitegraphContextMenuItem('Rename Slot')
await comfyPage.subgraph.rightClickInputSlot(initialInputLabel)
await comfyPage.contextMenu.clickLitegraphMenuItem('Rename Slot')
await comfyPage.nextFrame()
await comfyPage.page.waitForSelector(SELECTORS.promptDialog, {
state: 'visible'
@@ -55,7 +62,9 @@ test.describe('Subgraph Slot Rename Dialog', { tag: '@subgraph' }, () => {
// Verify the rename worked
const afterFirstRename = await comfyPage.page.evaluate(() => {
const graph = window['app'].canvas.graph
const graph = window.app!.canvas.graph
if (!graph || !('inputNode' in graph))
return { label: null, name: null, displayName: null }
const slot = graph.inputs?.[0]
return {
label: slot?.label || null,
@@ -67,8 +76,9 @@ test.describe('Subgraph Slot Rename Dialog', { tag: '@subgraph' }, () => {
// Now rename again - this is where the bug would show
// We need to use the index-based approach since the method looks for slot.name
await comfyPage.rightClickSubgraphInputSlot()
await comfyPage.clickLitegraphContextMenuItem('Rename Slot')
await comfyPage.subgraph.rightClickInputSlot()
await comfyPage.contextMenu.clickLitegraphMenuItem('Rename Slot')
await comfyPage.nextFrame()
await comfyPage.page.waitForSelector(SELECTORS.promptDialog, {
state: 'visible'
@@ -97,7 +107,8 @@ test.describe('Subgraph Slot Rename Dialog', { tag: '@subgraph' }, () => {
// Verify the second rename worked
const afterSecondRename = await comfyPage.page.evaluate(() => {
const graph = window['app'].canvas.graph
const graph = window.app!.canvas.graph
if (!graph || !('inputNode' in graph)) return null
return graph.inputs?.[0]?.label || null
})
expect(afterSecondRename).toBe(SECOND_RENAMED_NAME)
@@ -106,20 +117,28 @@ test.describe('Subgraph Slot Rename Dialog', { tag: '@subgraph' }, () => {
test('Shows current output slot label in rename dialog', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph')
const subgraphNode = await comfyPage.getNodeRefById('2')
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2')
await subgraphNode.navigateIntoSubgraph()
// Get initial output slot label
const initialOutputLabel = await comfyPage.page.evaluate(() => {
const graph = window['app'].canvas.graph
const graph = window.app!.canvas.graph
if (!graph || !('inputNode' in graph)) return null
return graph.outputs?.[0]?.label || graph.outputs?.[0]?.name || null
})
if (initialOutputLabel === null) {
throw new Error(
'Expected subgraph to have an output slot label for rightClickOutputSlot'
)
}
// First rename
await comfyPage.rightClickSubgraphOutputSlot(initialOutputLabel)
await comfyPage.clickLitegraphContextMenuItem('Rename Slot')
await comfyPage.subgraph.rightClickOutputSlot(initialOutputLabel)
await comfyPage.contextMenu.clickLitegraphMenuItem('Rename Slot')
await comfyPage.nextFrame()
await comfyPage.page.waitForSelector(SELECTORS.promptDialog, {
state: 'visible'
@@ -141,8 +160,9 @@ test.describe('Subgraph Slot Rename Dialog', { tag: '@subgraph' }, () => {
// Now rename again to check for stale content
// We need to use the index-based approach since the method looks for slot.name
await comfyPage.rightClickSubgraphOutputSlot()
await comfyPage.clickLitegraphContextMenuItem('Rename Slot')
await comfyPage.subgraph.rightClickOutputSlot()
await comfyPage.contextMenu.clickLitegraphMenuItem('Rename Slot')
await comfyPage.nextFrame()
await comfyPage.page.waitForSelector(SELECTORS.promptDialog, {
state: 'visible'

View File

@@ -18,7 +18,7 @@ const SELECTORS = {
test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
// Helper to get subgraph slot count
@@ -26,8 +26,11 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
comfyPage: typeof test.prototype.comfyPage,
type: 'inputs' | 'outputs'
): Promise<number> {
return await comfyPage.page.evaluate((slotType) => {
return window['app'].canvas.graph[slotType]?.length || 0
return await comfyPage.page.evaluate((slotType: 'inputs' | 'outputs') => {
const graph = window.app!.canvas.graph
// isSubgraph check: subgraphs have isRootGraph === false
if (!graph || !('inputNode' in graph)) return 0
return graph[slotType]?.length || 0
}, type)
}
@@ -36,7 +39,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
comfyPage: typeof test.prototype.comfyPage
): Promise<number> {
return await comfyPage.page.evaluate(() => {
return window['app'].canvas.graph.nodes?.length || 0
return window.app!.canvas.graph!.nodes?.length || 0
})
}
@@ -45,22 +48,22 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
comfyPage: typeof test.prototype.comfyPage
): Promise<boolean> {
return await comfyPage.page.evaluate(() => {
const graph = window['app'].canvas.graph
const graph = window.app!.canvas.graph
return graph?.constructor?.name === 'Subgraph'
})
}
test.describe('I/O Slot Management', () => {
test('Can add input slots to subgraph', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph')
const subgraphNode = await comfyPage.getNodeRefById('2')
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2')
await subgraphNode.navigateIntoSubgraph()
const initialCount = await getSubgraphSlotCount(comfyPage, 'inputs')
const vaeEncodeNode = await comfyPage.getNodeRefById('2')
const vaeEncodeNode = await comfyPage.nodeOps.getNodeRefById('2')
await comfyPage.connectFromSubgraphInput(vaeEncodeNode, 0)
await comfyPage.subgraph.connectFromInput(vaeEncodeNode, 0)
await comfyPage.nextFrame()
const finalCount = await getSubgraphSlotCount(comfyPage, 'inputs')
@@ -68,15 +71,15 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
})
test('Can add output slots to subgraph', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph')
const subgraphNode = await comfyPage.getNodeRefById('2')
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2')
await subgraphNode.navigateIntoSubgraph()
const initialCount = await getSubgraphSlotCount(comfyPage, 'outputs')
const vaeEncodeNode = await comfyPage.getNodeRefById('2')
const vaeEncodeNode = await comfyPage.nodeOps.getNodeRefById('2')
await comfyPage.connectToSubgraphOutput(vaeEncodeNode, 0)
await comfyPage.subgraph.connectToOutput(vaeEncodeNode, 0)
await comfyPage.nextFrame()
const finalCount = await getSubgraphSlotCount(comfyPage, 'outputs')
@@ -84,16 +87,17 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
})
test('Can remove input slots from subgraph', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph')
const subgraphNode = await comfyPage.getNodeRefById('2')
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2')
await subgraphNode.navigateIntoSubgraph()
const initialCount = await getSubgraphSlotCount(comfyPage, 'inputs')
expect(initialCount).toBeGreaterThan(0)
await comfyPage.rightClickSubgraphInputSlot()
await comfyPage.clickLitegraphContextMenuItem('Remove Slot')
await comfyPage.subgraph.rightClickInputSlot()
await comfyPage.contextMenu.clickLitegraphMenuItem('Remove Slot')
await comfyPage.nextFrame()
// Force re-render
await comfyPage.canvas.click({ position: { x: 100, y: 100 } })
@@ -104,16 +108,17 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
})
test('Can remove output slots from subgraph', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph')
const subgraphNode = await comfyPage.getNodeRefById('2')
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2')
await subgraphNode.navigateIntoSubgraph()
const initialCount = await getSubgraphSlotCount(comfyPage, 'outputs')
expect(initialCount).toBeGreaterThan(0)
await comfyPage.rightClickSubgraphOutputSlot()
await comfyPage.clickLitegraphContextMenuItem('Remove Slot')
await comfyPage.subgraph.rightClickOutputSlot()
await comfyPage.contextMenu.clickLitegraphMenuItem('Remove Slot')
await comfyPage.nextFrame()
// Force re-render
await comfyPage.canvas.click({ position: { x: 100, y: 100 } })
@@ -124,18 +129,20 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
})
test('Can rename I/O slots', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph')
const subgraphNode = await comfyPage.getNodeRefById('2')
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2')
await subgraphNode.navigateIntoSubgraph()
const initialInputLabel = await comfyPage.page.evaluate(() => {
const graph = window['app'].canvas.graph
const graph = window.app!.canvas.graph
if (!graph || !('inputNode' in graph)) return null
return graph.inputs?.[0]?.label || null
})
await comfyPage.rightClickSubgraphInputSlot(initialInputLabel)
await comfyPage.clickLitegraphContextMenuItem('Rename Slot')
await comfyPage.subgraph.rightClickInputSlot(initialInputLabel!)
await comfyPage.contextMenu.clickLitegraphMenuItem('Rename Slot')
await comfyPage.nextFrame()
await comfyPage.page.waitForSelector(SELECTORS.promptDialog, {
state: 'visible'
@@ -148,7 +155,8 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
await comfyPage.nextFrame()
const newInputName = await comfyPage.page.evaluate(() => {
const graph = window['app'].canvas.graph
const graph = window.app!.canvas.graph
if (!graph || !('inputNode' in graph)) return null
return graph.inputs?.[0]?.label || null
})
@@ -157,17 +165,18 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
})
test('Can rename input slots via double-click', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph')
const subgraphNode = await comfyPage.getNodeRefById('2')
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2')
await subgraphNode.navigateIntoSubgraph()
const initialInputLabel = await comfyPage.page.evaluate(() => {
const graph = window['app'].canvas.graph
const graph = window.app!.canvas.graph
if (!graph || !('inputNode' in graph)) return null
return graph.inputs?.[0]?.label || null
})
await comfyPage.doubleClickSubgraphInputSlot(initialInputLabel)
await comfyPage.subgraph.doubleClickInputSlot(initialInputLabel!)
await comfyPage.page.waitForSelector(SELECTORS.promptDialog, {
state: 'visible'
@@ -180,7 +189,8 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
await comfyPage.nextFrame()
const newInputName = await comfyPage.page.evaluate(() => {
const graph = window['app'].canvas.graph
const graph = window.app!.canvas.graph
if (!graph || !('inputNode' in graph)) return null
return graph.inputs?.[0]?.label || null
})
@@ -189,17 +199,18 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
})
test('Can rename output slots via double-click', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph')
const subgraphNode = await comfyPage.getNodeRefById('2')
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2')
await subgraphNode.navigateIntoSubgraph()
const initialOutputLabel = await comfyPage.page.evaluate(() => {
const graph = window['app'].canvas.graph
const graph = window.app!.canvas.graph
if (!graph || !('inputNode' in graph)) return null
return graph.outputs?.[0]?.label || null
})
await comfyPage.doubleClickSubgraphOutputSlot(initialOutputLabel)
await comfyPage.subgraph.doubleClickOutputSlot(initialOutputLabel!)
await comfyPage.page.waitForSelector(SELECTORS.promptDialog, {
state: 'visible'
@@ -213,7 +224,8 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
await comfyPage.nextFrame()
const newOutputName = await comfyPage.page.evaluate(() => {
const graph = window['app'].canvas.graph
const graph = window.app!.canvas.graph
if (!graph || !('inputNode' in graph)) return null
return graph.outputs?.[0]?.label || null
})
@@ -224,19 +236,21 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
test('Right-click context menu still works alongside double-click', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph')
const subgraphNode = await comfyPage.getNodeRefById('2')
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2')
await subgraphNode.navigateIntoSubgraph()
const initialInputLabel = await comfyPage.page.evaluate(() => {
const graph = window['app'].canvas.graph
const graph = window.app!.canvas.graph
if (!graph || !('inputNode' in graph)) return null
return graph.inputs?.[0]?.label || null
})
// Test that right-click still works for renaming
await comfyPage.rightClickSubgraphInputSlot(initialInputLabel)
await comfyPage.clickLitegraphContextMenuItem('Rename Slot')
await comfyPage.subgraph.rightClickInputSlot(initialInputLabel!)
await comfyPage.contextMenu.clickLitegraphMenuItem('Rename Slot')
await comfyPage.nextFrame()
await comfyPage.page.waitForSelector(SELECTORS.promptDialog, {
state: 'visible'
@@ -250,7 +264,8 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
await comfyPage.nextFrame()
const newInputName = await comfyPage.page.evaluate(() => {
const graph = window['app'].canvas.graph
const graph = window.app!.canvas.graph
if (!graph || !('inputNode' in graph)) return null
return graph.inputs?.[0]?.label || null
})
@@ -261,20 +276,25 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
test('Can double-click on slot label text to rename', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph')
const subgraphNode = await comfyPage.getNodeRefById('2')
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2')
await subgraphNode.navigateIntoSubgraph()
const initialInputLabel = await comfyPage.page.evaluate(() => {
const graph = window['app'].canvas.graph
const graph = window.app!.canvas.graph
if (!graph || !('inputNode' in graph)) return null
return graph.inputs?.[0]?.label || null
})
// Use direct pointer event approach to double-click on label
await comfyPage.page.evaluate(() => {
const app = window['app']
const app = window.app!
const graph = app.canvas.graph
if (!graph || !('inputNode' in graph)) {
throw new Error('Expected to be in subgraph')
}
const input = graph.inputs?.[0]
if (!input?.labelPos) {
@@ -285,13 +305,15 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
const testX = input.labelPos[0]
const testY = input.labelPos[1]
// Create a minimal mock event with required properties
// Full PointerEvent creation is unnecessary for this test
const leftClickEvent = {
canvasX: testX,
canvasY: testY,
button: 0, // Left mouse button
button: 0,
preventDefault: () => {},
stopPropagation: () => {}
}
} as Parameters<typeof graph.inputNode.onPointerDown>[0]
const inputNode = graph.inputNode
if (inputNode?.onPointerDown) {
@@ -322,7 +344,8 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
await comfyPage.nextFrame()
const newInputName = await comfyPage.page.evaluate(() => {
const graph = window['app'].canvas.graph
const graph = window.app!.canvas.graph
if (!graph || !('inputNode' in graph)) return null
return graph.inputs?.[0]?.label || null
})
@@ -332,9 +355,11 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
test('Can create widget from link with compressed target_slot', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('subgraphs/subgraph-compressed-target-slot')
await comfyPage.workflow.loadWorkflow(
'subgraphs/subgraph-compressed-target-slot'
)
const step = await comfyPage.page.evaluate(() => {
return window['app'].graph.nodes[0].widgets[0].options.step
return window.app!.graph!.nodes[0].widgets![0].options.step
})
expect(step).toBe(10)
})
@@ -342,19 +367,17 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
test.describe('Subgraph Creation and Deletion', () => {
test('Can create subgraph from selected nodes', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('default')
await comfyPage.workflow.loadWorkflow('default')
const initialNodeCount = await getGraphNodeCount(comfyPage)
await comfyPage.ctrlA()
await comfyPage.keyboard.selectAll()
await comfyPage.nextFrame()
const node = await comfyPage.getNodeRefById('5')
const node = await comfyPage.nodeOps.getNodeRefById('5')
await node.convertToSubgraph()
await comfyPage.nextFrame()
const subgraphNodes =
await comfyPage.getNodeRefsByTitle(NEW_SUBGRAPH_TITLE)
await comfyPage.nodeOps.getNodeRefsByTitle(NEW_SUBGRAPH_TITLE)
expect(subgraphNodes.length).toBe(1)
const finalNodeCount = await getGraphNodeCount(comfyPage)
@@ -362,9 +385,9 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
})
test('Can delete subgraph node', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph')
const subgraphNode = await comfyPage.getNodeRefById('2')
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2')
expect(await subgraphNode.exists()).toBe(true)
const initialNodeCount = await getGraphNodeCount(comfyPage)
@@ -376,7 +399,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
const finalNodeCount = await getGraphNodeCount(comfyPage)
expect(finalNodeCount).toBe(initialNodeCount - 1)
const deletedNode = await comfyPage.getNodeRefById('2')
const deletedNode = await comfyPage.nodeOps.getNodeRefById('2')
expect(await deletedNode.exists()).toBe(false)
})
@@ -384,9 +407,9 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
test('Can copy subgraph node by dragging + alt', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph')
const subgraphNode = await comfyPage.getNodeRefById('2')
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2')
// Get position of subgraph node
const subgraphPos = await subgraphNode.getPosition()
@@ -404,7 +427,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
// Find all subgraph nodes
const subgraphNodes =
await comfyPage.getNodeRefsByTitle(NEW_SUBGRAPH_TITLE)
await comfyPage.nodeOps.getNodeRefsByTitle(NEW_SUBGRAPH_TITLE)
// Expect a second subgraph node to be created (2 total)
expect(subgraphNodes.length).toBe(2)
@@ -413,9 +436,9 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
test('Copying subgraph node by dragging + alt creates a new subgraph node with unique type', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph')
const subgraphNode = await comfyPage.getNodeRefById('2')
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2')
// Get position of subgraph node
const subgraphPos = await subgraphNode.getPosition()
@@ -433,7 +456,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
// Find all subgraph nodes and expect all unique IDs
const subgraphNodes =
await comfyPage.getNodeRefsByTitle(NEW_SUBGRAPH_TITLE)
await comfyPage.nodeOps.getNodeRefsByTitle(NEW_SUBGRAPH_TITLE)
// Expect the second subgraph node to have a unique type
const nodeType1 = await subgraphNodes[0].getType()
@@ -445,21 +468,21 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
test.describe('Operations Inside Subgraphs', () => {
test('Can copy and paste nodes in subgraph', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph')
const subgraphNode = await comfyPage.getNodeRefById('2')
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2')
await subgraphNode.navigateIntoSubgraph()
const initialNodeCount = await getGraphNodeCount(comfyPage)
const nodesInSubgraph = await comfyPage.page.evaluate(() => {
const nodes = window['app'].canvas.graph.nodes
const nodes = window.app!.canvas.graph!.nodes
return nodes?.[0]?.id || null
})
expect(nodesInSubgraph).not.toBeNull()
const nodeToClone = await comfyPage.getNodeRefById(
const nodeToClone = await comfyPage.nodeOps.getNodeRefById(
String(nodesInSubgraph)
)
await nodeToClone.click('title')
@@ -476,13 +499,13 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
})
test('Can undo and redo operations in subgraph', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph')
const subgraphNode = await comfyPage.getNodeRefById('2')
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2')
await subgraphNode.navigateIntoSubgraph()
// Add a node
await comfyPage.doubleClickCanvas()
await comfyPage.canvasOps.doubleClick()
await comfyPage.searchBox.fillAndSelectFirstNode('Note')
await comfyPage.nextFrame()
@@ -490,14 +513,14 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
const initialCount = await getGraphNodeCount(comfyPage)
// Undo
await comfyPage.ctrlZ()
await comfyPage.keyboard.undo()
await comfyPage.nextFrame()
const afterUndoCount = await getGraphNodeCount(comfyPage)
expect(afterUndoCount).toBe(initialCount - 1)
// Redo
await comfyPage.ctrlY()
await comfyPage.keyboard.redo()
await comfyPage.nextFrame()
const afterRedoCount = await getGraphNodeCount(comfyPage)
@@ -507,16 +530,16 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
test.describe('Subgraph Navigation and UI', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
})
test('Breadcrumb updates when subgraph node title is changed', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('subgraphs/nested-subgraph')
await comfyPage.workflow.loadWorkflow('subgraphs/nested-subgraph')
await comfyPage.nextFrame()
const subgraphNode = await comfyPage.getNodeRefById('10')
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('10')
const nodePos = await subgraphNode.getPosition()
const nodeSize = await subgraphNode.getSize()
@@ -565,7 +588,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
test('DOM widget visibility persists through subgraph navigation', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow(
await comfyPage.workflow.loadWorkflow(
'subgraphs/subgraph-with-promoted-text-widget'
)
await comfyPage.nextFrame()
@@ -575,7 +598,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
await expect(parentTextarea).toBeVisible()
await expect(parentTextarea).toHaveCount(1)
const subgraphNode = await comfyPage.getNodeRefById('11')
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('11')
expect(await subgraphNode.exists()).toBe(true)
await subgraphNode.navigateIntoSubgraph()
@@ -598,14 +621,14 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
test('DOM widget content is preserved through navigation', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow(
await comfyPage.workflow.loadWorkflow(
'subgraphs/subgraph-with-promoted-text-widget'
)
const textarea = comfyPage.page.locator(SELECTORS.domWidget)
await textarea.fill(TEST_WIDGET_CONTENT)
const subgraphNode = await comfyPage.getNodeRefById('11')
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('11')
await subgraphNode.navigateIntoSubgraph()
const subgraphTextarea = comfyPage.page.locator(SELECTORS.domWidget)
@@ -621,7 +644,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
test('DOM elements are cleaned up when subgraph node is removed', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow(
await comfyPage.workflow.loadWorkflow(
'subgraphs/subgraph-with-promoted-text-widget'
)
@@ -630,7 +653,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
.count()
expect(initialCount).toBe(1)
const subgraphNode = await comfyPage.getNodeRefById('11')
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('11')
await subgraphNode.click('title')
await comfyPage.page.keyboard.press('Delete')
@@ -646,23 +669,24 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
comfyPage
}) => {
// Enable new menu for breadcrumb navigation
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
const workflowName = 'subgraphs/subgraph-with-promoted-text-widget'
await comfyPage.loadWorkflow(workflowName)
await comfyPage.workflow.loadWorkflow(workflowName)
const textareaCount = await comfyPage.page
.locator(SELECTORS.domWidget)
.count()
expect(textareaCount).toBe(1)
const subgraphNode = await comfyPage.getNodeRefById('11')
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('11')
// Navigate into subgraph (method now handles retries internally)
await subgraphNode.navigateIntoSubgraph()
await comfyPage.rightClickSubgraphInputSlot('text')
await comfyPage.clickLitegraphContextMenuItem('Remove Slot')
await comfyPage.subgraph.rightClickInputSlot('text')
await comfyPage.contextMenu.clickLitegraphMenuItem('Remove Slot')
await comfyPage.nextFrame()
// Wait for breadcrumb to be visible
await comfyPage.page.waitForSelector(SELECTORS.breadcrumb, {
@@ -682,7 +706,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
// Check that the subgraph node has no widgets after removing the text slot
const widgetCount = await comfyPage.page.evaluate(() => {
return window['app'].canvas.graph.nodes[0].widgets?.length || 0
return window.app!.canvas.graph!.nodes[0].widgets?.length || 0
})
expect(widgetCount).toBe(0)
@@ -691,7 +715,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
test('Multiple promoted widgets are handled correctly', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow(
await comfyPage.workflow.loadWorkflow(
'subgraphs/subgraph-with-multiple-promoted-widgets'
)
@@ -700,7 +724,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
.count()
expect(parentCount).toBeGreaterThan(1)
const subgraphNode = await comfyPage.getNodeRefById('11')
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('11')
await subgraphNode.navigateIntoSubgraph()
const subgraphCount = await comfyPage.page
@@ -720,15 +744,15 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
test.describe('Navigation Hotkeys', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
})
test('Navigation hotkey can be customized', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.nextFrame()
// Change the Exit Subgraph keybinding from Escape to Alt+Q
await comfyPage.setSetting('Comfy.Keybinding.NewBindings', [
await comfyPage.settings.setSetting('Comfy.Keybinding.NewBindings', [
{
commandId: 'Comfy.Graph.ExitSubgraph',
combo: {
@@ -740,7 +764,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
}
])
await comfyPage.setSetting('Comfy.Keybinding.UnsetBindings', [
await comfyPage.settings.setSetting('Comfy.Keybinding.UnsetBindings', [
{
commandId: 'Comfy.Graph.ExitSubgraph',
combo: {
@@ -754,10 +778,12 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
// Reload the page
await comfyPage.page.reload()
await comfyPage.page.waitForTimeout(1024)
await comfyPage.setup()
await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.nextFrame()
// Navigate into subgraph
const subgraphNode = await comfyPage.getNodeRefById('2')
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2')
await subgraphNode.navigateIntoSubgraph()
await comfyPage.page.waitForSelector(SELECTORS.breadcrumb)
@@ -780,10 +806,10 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
test('Escape prioritizes closing dialogs over exiting subgraph', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.nextFrame()
const subgraphNode = await comfyPage.getNodeRefById('2')
const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2')
await subgraphNode.navigateIntoSubgraph()
await comfyPage.page.waitForSelector(SELECTORS.breadcrumb)

View File

@@ -15,8 +15,11 @@ async function checkTemplateFileExists(
test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting('Comfy.Workflow.ShowMissingModelsWarning', false)
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting(
'Comfy.Workflow.ShowMissingModelsWarning',
false
)
})
test('should have a JSON workflow file for each template', async ({
@@ -72,13 +75,13 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => {
test('Can load template workflows', async ({ comfyPage }) => {
// Clear the workflow
await comfyPage.menu.workflowsTab.open()
await comfyPage.executeCommand('Comfy.NewBlankWorkflow')
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
await expect(async () => {
expect(await comfyPage.getGraphNodesCount()).toBe(0)
expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe(0)
}).toPass({ timeout: 250 })
// Load a template
await comfyPage.executeCommand('Comfy.BrowseTemplates')
await comfyPage.command.executeCommand('Comfy.BrowseTemplates')
await expect(comfyPage.templates.content).toBeVisible()
await comfyPage.page
@@ -89,7 +92,7 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => {
// Ensure we now have some nodes
await expect(async () => {
expect(await comfyPage.getGraphNodesCount()).toBeGreaterThan(0)
expect(await comfyPage.nodeOps.getGraphNodesCount()).toBeGreaterThan(0)
}).toPass({ timeout: 250 })
})
@@ -97,7 +100,7 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => {
comfyPage
}) => {
// Set the tutorial as not completed to mark the user as a first-time user
await comfyPage.setSetting('Comfy.TutorialCompleted', false)
await comfyPage.settings.setSetting('Comfy.TutorialCompleted', false)
// Load the page
await comfyPage.setup({ clearStorage: true })
@@ -107,9 +110,9 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => {
})
test('Uses proper locale files for templates', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Locale', 'fr')
await comfyPage.settings.setSetting('Comfy.Locale', 'fr')
await comfyPage.executeCommand('Comfy.BrowseTemplates')
await comfyPage.command.executeCommand('Comfy.BrowseTemplates')
const dialog = comfyPage.page.getByRole('dialog').filter({
has: comfyPage.page.getByRole('heading', { name: 'Modèles', exact: true })
@@ -134,7 +137,7 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => {
comfyPage
}) => {
// Set locale to a language that doesn't have a template file
await comfyPage.setSetting('Comfy.Locale', 'de') // German - no index.de.json exists
await comfyPage.settings.setSetting('Comfy.Locale', 'de') // German - no index.de.json exists
// Wait for the German request (expected to 404)
const germanRequestPromise = comfyPage.page.waitForRequest(
@@ -161,7 +164,7 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => {
)
// Load the templates dialog
await comfyPage.executeCommand('Comfy.BrowseTemplates')
await comfyPage.command.executeCommand('Comfy.BrowseTemplates')
await expect(comfyPage.templates.content).toBeVisible()
// Verify German was requested first, then English as fallback
@@ -181,7 +184,7 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => {
comfyPage
}) => {
// Open templates dialog
await comfyPage.executeCommand('Comfy.BrowseTemplates')
await comfyPage.command.executeCommand('Comfy.BrowseTemplates')
await comfyPage.templates.content.waitFor({ state: 'visible' })
const templateGrid = comfyPage.page.locator(
@@ -189,20 +192,20 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => {
)
const nav = comfyPage.page.locator('header', { hasText: 'Templates' })
await comfyPage.templates.waitForMinimumCardCount(1)
await comfyPage.templates.expectMinimumCardCount(1)
await expect(templateGrid).toBeVisible()
await expect(nav).toBeVisible() // Nav should be visible at desktop size
const mobileSize = { width: 640, height: 800 }
await comfyPage.page.setViewportSize(mobileSize)
await comfyPage.templates.waitForMinimumCardCount(1)
await comfyPage.templates.expectMinimumCardCount(1)
await expect(templateGrid).toBeVisible()
// Nav header is clipped by overflow-hidden parent at mobile size
await expect(nav).not.toBeInViewport()
const tabletSize = { width: 1024, height: 800 }
await comfyPage.page.setViewportSize(tabletSize)
await comfyPage.templates.waitForMinimumCardCount(1)
await comfyPage.templates.expectMinimumCardCount(1)
await expect(templateGrid).toBeVisible()
await expect(nav).toBeVisible() // Nav should be visible at tablet size
})
@@ -272,7 +275,7 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => {
})
// Open templates dialog
await comfyPage.executeCommand('Comfy.BrowseTemplates')
await comfyPage.command.executeCommand('Comfy.BrowseTemplates')
await expect(comfyPage.templates.content).toBeVisible()
// Wait for cards to load

View File

@@ -1,27 +1,37 @@
import { expect } from '@playwright/test'
import type { Settings } from '../../src/schemas/apiSchema'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
/**
* Type helper for test settings with arbitrary IDs.
* Extensions can register settings with any ID, but SettingParams.id
* is typed as keyof Settings for autocomplete.
*/
type TestSettingId = keyof Settings
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Settings Search functionality', { tag: '@settings' }, () => {
test.beforeEach(async ({ comfyPage }) => {
// Register test settings to verify hidden/deprecated filtering
await comfyPage.page.evaluate(() => {
window['app'].registerExtension({
window.app!.registerExtension({
name: 'TestSettingsExtension',
settings: [
{
id: 'TestHiddenSetting',
// Extensions can register arbitrary setting IDs
id: 'TestHiddenSetting' as TestSettingId,
name: 'Test Hidden Setting',
type: 'hidden',
defaultValue: 'hidden_value',
category: ['Test', 'Hidden']
},
{
id: 'TestDeprecatedSetting',
// Extensions can register arbitrary setting IDs
id: 'TestDeprecatedSetting' as TestSettingId,
name: 'Test Deprecated Setting',
type: 'text',
defaultValue: 'deprecated_value',
@@ -29,7 +39,8 @@ test.describe('Settings Search functionality', { tag: '@settings' }, () => {
category: ['Test', 'Deprecated']
},
{
id: 'TestVisibleSetting',
// Extensions can register arbitrary setting IDs
id: 'TestVisibleSetting' as TestSettingId,
name: 'Test Visible Setting',
type: 'text',
defaultValue: 'visible_value',
@@ -109,19 +120,14 @@ test.describe('Settings Search functionality', { tag: '@settings' }, () => {
const settingsDialog = comfyPage.page.locator('.settings-container')
await expect(settingsDialog).toBeVisible()
// Get categories and click on different ones
const categories = comfyPage.page.locator(
'.settings-sidebar .p-listbox-option'
)
const categoryCount = await categories.count()
// Click on a specific category (Appearance) to verify category switching
const appearanceCategory = comfyPage.page.getByRole('option', {
name: 'Appearance'
})
await appearanceCategory.click()
if (categoryCount > 1) {
// Click on the second category
await categories.nth(1).click()
// Verify the category is selected
await expect(categories.nth(1)).toHaveClass(/p-listbox-option-selected/)
}
// Verify the category is selected
await expect(appearanceCategory).toHaveClass(/p-listbox-option-selected/)
})
test('settings content area is visible', async ({ comfyPage }) => {

View File

@@ -37,8 +37,8 @@ test.describe('Version Mismatch Warnings', { tag: '@slow' }, () => {
}
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting(
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting(
'Comfy.VersionCompatibility.DisableWarnings',
false
)
@@ -103,10 +103,9 @@ test.describe('Version Mismatch Warnings', { tag: '@slow' }, () => {
await comfyPage.setup()
// Locate the warning toast and dismiss it
const warningToast = comfyPage.page
.locator('div')
.filter({ hasText: 'Version Compatibility' })
.nth(3)
const warningToast = comfyPage.page.locator('.p-toast-message').filter({
hasText: 'Version Compatibility'
})
await warningToast.waitFor({ state: 'visible' })
const dismissButton = warningToast.getByRole('button', { name: 'Close' })
await dismissButton.click()

View File

@@ -6,7 +6,9 @@ test.describe('Viewport', { tag: ['@screenshot', '@smoke', '@canvas'] }, () => {
test('Fits view to nodes when saved viewport position is offscreen', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('viewport/default-viewport-saved-offscreen')
await comfyPage.workflow.loadWorkflow(
'viewport/default-viewport-saved-offscreen'
)
// Wait a few frames for rendering to stabilize
for (let i = 0; i < 5; i++) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 73 KiB

View File

@@ -7,8 +7,8 @@ const CREATE_GROUP_HOTKEY = 'Control+g'
test.describe('Vue Node Groups', { tag: '@screenshot' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.setSetting('Comfy.Minimap.ShowGroups', true)
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.settings.setSetting('Comfy.Minimap.ShowGroups', true)
await comfyPage.vueNodes.waitForNodes()
})
@@ -24,9 +24,9 @@ test.describe('Vue Node Groups', { tag: '@screenshot' }, () => {
test('should allow fitting group to contents', async ({ comfyPage }) => {
await comfyPage.setup()
await comfyPage.loadWorkflow('groups/oversized_group')
await comfyPage.ctrlA()
await comfyPage.executeCommand('Comfy.Graph.FitGroupToContents')
await comfyPage.workflow.loadWorkflow('groups/oversized_group')
await comfyPage.keyboard.selectAll()
await comfyPage.command.executeCommand('Comfy.Graph.FitGroupToContents')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-groups-fit-to-contents.png'
@@ -36,18 +36,20 @@ test.describe('Vue Node Groups', { tag: '@screenshot' }, () => {
test('should move nested groups together when dragging outer group', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('groups/nested-groups-1-inner-node')
await comfyPage.workflow.loadWorkflow('groups/nested-groups-1-inner-node')
// Get initial positions with null guards
const outerInitial = await comfyPage.getGroupPosition('Outer Group')
const innerInitial = await comfyPage.getGroupPosition('Inner Group')
const outerInitial =
await comfyPage.canvasOps.getGroupPosition('Outer Group')
const innerInitial =
await comfyPage.canvasOps.getGroupPosition('Inner Group')
const initialOffsetX = innerInitial.x - outerInitial.x
const initialOffsetY = innerInitial.y - outerInitial.y
// Drag the outer group
const dragDelta = { x: 100, y: 80 }
await comfyPage.dragGroup({
await comfyPage.canvasOps.dragGroup({
name: 'Outer Group',
deltaX: dragDelta.x,
deltaY: dragDelta.y
@@ -55,8 +57,10 @@ test.describe('Vue Node Groups', { tag: '@screenshot' }, () => {
// Use retrying assertion to wait for positions to update
await expect(async () => {
const outerFinal = await comfyPage.getGroupPosition('Outer Group')
const innerFinal = await comfyPage.getGroupPosition('Inner Group')
const outerFinal =
await comfyPage.canvasOps.getGroupPosition('Outer Group')
const innerFinal =
await comfyPage.canvasOps.getGroupPosition('Inner Group')
const finalOffsetX = innerFinal.x - outerFinal.x
const finalOffsetY = innerFinal.y - outerFinal.y

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

View File

@@ -5,7 +5,7 @@ import {
test.describe('Vue Nodes Canvas Pan', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.vueNodes.waitForNodes()
})
@@ -13,7 +13,10 @@ test.describe('Vue Nodes Canvas Pan', () => {
'@mobile Can pan with touch',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.panWithTouch({ x: 64, y: 64 }, { x: 256, y: 256 })
await comfyPage.canvasOps.panWithTouch(
{ x: 64, y: 64 },
{ x: 256, y: 256 }
)
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-nodes-paned-with-touch.png'
)

View File

@@ -5,8 +5,8 @@ import {
test.describe('Vue Nodes Zoom', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 8)
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.settings.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 8)
await comfyPage.vueNodes.waitForNodes()
})
@@ -26,7 +26,7 @@ test.describe('Vue Nodes Zoom', () => {
// the node. The node should not capture the drag while drag-zooming.
await comfyPage.page.keyboard.down('Control')
await comfyPage.page.keyboard.down('Shift')
await comfyPage.dragAndDrop(
await comfyPage.canvasOps.dragAndDrop(
{ x: 200, y: 300 },
{ x: nodeMidpointX, y: nodeMidpointY }
)

View File

@@ -25,7 +25,7 @@ async function getInputLinkDetails(
) {
return await page.evaluate(
([targetNodeId, targetSlot]) => {
const app = window['app']
const app = window.app
const graph = app?.canvas?.graph ?? app?.graph
if (!graph) return null
@@ -100,10 +100,10 @@ async function connectSlots(
test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
// await comfyPage.setup()
await comfyPage.loadWorkflow('vueNodes/simple-triple')
await comfyPage.workflow.loadWorkflow('vueNodes/simple-triple')
await comfyPage.vueNodes.waitForNodes()
await fitToViewInstant(comfyPage)
})
@@ -112,7 +112,9 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
comfyPage,
comfyMouse
}) => {
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
const samplerNode = (
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
)[0]
expect(samplerNode).toBeTruthy()
const slot = slotLocator(comfyPage.page, samplerNode.id, 0, false)
@@ -142,8 +144,10 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
test('should create a link when dropping on a compatible slot', async ({
comfyPage
}) => {
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0]
const samplerNode = (
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
)[0]
const vaeNode = (await comfyPage.nodeOps.getNodeRefsByType('VAEDecode'))[0]
expect(samplerNode && vaeNode).toBeTruthy()
const samplerOutput = await samplerNode.getOutput(0)
@@ -172,8 +176,12 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
test('should not create a link when slot types are incompatible', async ({
comfyPage
}) => {
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0]
const samplerNode = (
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
)[0]
const clipNode = (
await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
)[0]
expect(samplerNode && clipNode).toBeTruthy()
const samplerOutput = await samplerNode.getOutput(0)
@@ -200,7 +208,9 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
test('should not create a link when dropping onto a slot on the same node', async ({
comfyPage
}) => {
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
const samplerNode = (
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
)[0]
expect(samplerNode).toBeTruthy()
const samplerOutput = await samplerNode.getOutput(0)
@@ -221,8 +231,10 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
comfyPage,
comfyMouse
}) => {
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0]
const samplerNode = (
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
)[0]
const vaeNode = (await comfyPage.nodeOps.getNodeRefsByType('VAEDecode'))[0]
expect(samplerNode && vaeNode).toBeTruthy()
const samplerOutputCenter = await getSlotCenter(
comfyPage.page,
@@ -258,8 +270,10 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
comfyPage,
comfyMouse
}) => {
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0]
const samplerNode = (
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
)[0]
const vaeNode = (await comfyPage.nodeOps.getNodeRefsByType('VAEDecode'))[0]
expect(samplerNode && vaeNode).toBeTruthy()
const samplerOutput = await samplerNode.getOutput(0)
@@ -315,8 +329,10 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
comfyPage,
comfyMouse
}) => {
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0]
const samplerNode = (
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
)[0]
const vaeNode = (await comfyPage.nodeOps.getNodeRefsByType('VAEDecode'))[0]
expect(samplerNode && vaeNode).toBeTruthy()
const samplerOutput = await samplerNode.getOutput(0)
@@ -398,8 +414,10 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
comfyPage,
comfyMouse
}) => {
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0]
const samplerNode = (
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
)[0]
const vaeNode = (await comfyPage.nodeOps.getNodeRefsByType('VAEDecode'))[0]
const samplerOutput = await samplerNode.getOutput(0)
const vaeInput = await vaeNode.getInput(0)
@@ -419,7 +437,7 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
// This avoids relying on an exact path hit-test position.
await comfyPage.page.evaluate(
([targetNodeId, targetSlot, clientPoint]) => {
const app = window['app']
const app = window.app
const graph = app?.canvas?.graph ?? app?.graph
if (!graph) throw new Error('Graph not available')
const node = graph.getNodeById(targetNodeId)
@@ -433,7 +451,7 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
if (!link) throw new Error('Link not found')
// Convert the client/canvas pixel coordinates to graph space
const pos = app.canvas.ds.convertCanvasToOffset([
const pos = app!.canvas.ds.convertCanvasToOffset([
clientPoint.x,
clientPoint.y
])
@@ -483,8 +501,10 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
comfyPage,
comfyMouse
}) => {
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0]
const samplerNode = (
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
)[0]
const vaeNode = (await comfyPage.nodeOps.getNodeRefsByType('VAEDecode'))[0]
expect(samplerNode && vaeNode).toBeTruthy()
const samplerOutput = await samplerNode.getOutput(0)
@@ -505,7 +525,7 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
// This avoids relying on an exact path hit-test position.
await comfyPage.page.evaluate(
([targetNodeId, targetSlot, clientPoint]) => {
const app = window['app']
const app = window.app
const graph = app?.canvas?.graph ?? app?.graph
if (!graph) throw new Error('Graph not available')
const node = graph.getNodeById(targetNodeId)
@@ -519,7 +539,7 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
if (!link) throw new Error('Link not found')
// Convert the client/canvas pixel coordinates to graph space
const pos = app.canvas.ds.convertCanvasToOffset([
const pos = app!.canvas.ds.convertCanvasToOffset([
clientPoint.x,
clientPoint.y
])
@@ -572,8 +592,12 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
comfyPage,
comfyMouse
}) => {
const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0]
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
const clipNode = (
await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
)[0]
const samplerNode = (
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
)[0]
expect(clipNode && samplerNode).toBeTruthy()
// Step 1: Connect CLIP's only output (index 0) to KSampler's second input (index 1)
@@ -642,8 +666,12 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
comfyPage,
comfyMouse
}) => {
const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0]
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
const clipNode = (
await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
)[0]
const samplerNode = (
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
)[0]
expect(clipNode && samplerNode).toBeTruthy()
const clipOutput = await clipNode.getOutput(0)
@@ -697,8 +725,12 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
comfyPage,
comfyMouse
}) => {
const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0]
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
const clipNode = (
await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
)[0]
const samplerNode = (
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
)[0]
expect(clipNode && samplerNode).toBeTruthy()
// Start drag from CLIP output[0]
@@ -746,8 +778,12 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
comfyPage,
comfyMouse
}) => {
const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0]
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
const clipNode = (
await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
)[0]
const samplerNode = (
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
)[0]
expect(clipNode && samplerNode).toBeTruthy()
// Drag from CLIP output[0] to KSampler input[2] (third slot) which is the
@@ -791,8 +827,12 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
test('should batch disconnect all links with ctrl+alt+click on slot', async ({
comfyPage
}) => {
const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0]
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
const clipNode = (
await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
)[0]
const samplerNode = (
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
)[0]
expect(clipNode && samplerNode).toBeTruthy()
await connectSlots(
@@ -832,12 +872,14 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
comfyPage,
comfyMouse
}) => {
await comfyPage.setSetting(
await comfyPage.settings.setSetting(
'Comfy.LinkRelease.ActionShift',
'context menu'
)
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
const samplerNode = (
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
)[0]
expect(samplerNode).toBeTruthy()
const outputCenter = await getSlotCenter(
@@ -864,7 +906,7 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
// Pinned endpoint should not change with mouse movement while menu is open
const before = await comfyPage.page.evaluate(() => {
const snap = window['app']?.canvas?.linkConnector?.state?.snapLinksPos
const snap = window.app?.canvas?.linkConnector?.state?.snapLinksPos
return Array.isArray(snap) ? [snap[0], snap[1]] : null
})
expect(before).not.toBeNull()
@@ -872,7 +914,7 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
// Move mouse elsewhere and verify snap position is unchanged
await comfyMouse.move({ x: dropPos.x + 160, y: dropPos.y + 100 })
const after = await comfyPage.page.evaluate(() => {
const snap = window['app']?.canvas?.linkConnector?.state?.snapLinksPos
const snap = window.app?.canvas?.linkConnector?.state?.snapLinksPos
return Array.isArray(snap) ? [snap[0], snap[1]] : null
})
expect(after).toEqual(before)
@@ -882,13 +924,15 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
comfyPage,
comfyMouse
}) => {
await comfyPage.setSetting(
await comfyPage.settings.setSetting(
'Comfy.LinkRelease.ActionShift',
'context menu'
)
await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default')
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default')
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
const samplerNode = (
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
)[0]
expect(samplerNode).toBeTruthy()
const outputCenter = await getSlotCenter(
@@ -909,7 +953,8 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
}
// Open Search from the context menu
await comfyPage.clickContextMenuItem('Search')
await comfyPage.contextMenu.clickMenuItem('Search')
await comfyPage.nextFrame()
// Search box opens with prefilled type filter based on link type (LATENT)
await expect(comfyPage.searchBox.input).toBeVisible()
@@ -928,7 +973,7 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
expect(await samplerOutput.getLinkCount()).toBe(1)
// One of the VAEDecode nodes should have an incoming link on input[0]
const vaeNodes = await comfyPage.getNodeRefsByType('VAEDecode')
const vaeNodes = await comfyPage.nodeOps.getNodeRefsByType('VAEDecode')
let linked = false
for (const vae of vaeNodes) {
const details = await getInputLinkDetails(comfyPage.page, vae.id, 0)
@@ -945,9 +990,14 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
comfyPage,
comfyMouse
}) => {
await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'search box')
await comfyPage.settings.setSetting(
'Comfy.LinkRelease.ActionShift',
'search box'
)
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
const samplerNode = (
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
)[0]
expect(samplerNode).toBeTruthy()
const outputCenter = await getSlotCenter(
@@ -980,7 +1030,7 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
const samplerOutput = await samplerNode.getOutput(0)
expect(await samplerOutput.getLinkCount()).toBe(1)
const vaeNodes = await comfyPage.getNodeRefsByType('VAEDecode')
const vaeNodes = await comfyPage.nodeOps.getNodeRefsByType('VAEDecode')
let linked = false
for (const vae of vaeNodes) {
const details = await getInputLinkDetails(comfyPage.page, vae.id, 0)
@@ -999,24 +1049,28 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
comfyMouse
}) => {
// Setup workflow with a KSampler node
await comfyPage.executeCommand('Comfy.NewBlankWorkflow')
await comfyPage.waitForGraphNodes(0)
await comfyPage.executeCommand('Workspace.SearchBox.Toggle')
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
await comfyPage.nodeOps.waitForGraphNodes(0)
await comfyPage.command.executeCommand('Workspace.SearchBox.Toggle')
await comfyPage.nextFrame()
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler')
await comfyPage.waitForGraphNodes(1)
await comfyPage.nodeOps.waitForGraphNodes(1)
// Convert the KSampler node to a subgraph
let ksamplerNode = (await comfyPage.getNodeRefsByType('KSampler'))?.[0]
let ksamplerNode = (
await comfyPage.nodeOps.getNodeRefsByType('KSampler')
)?.[0]
await comfyPage.vueNodes.selectNode(String(ksamplerNode.id))
await comfyPage.executeCommand('Comfy.Graph.ConvertToSubgraph')
await comfyPage.command.executeCommand('Comfy.Graph.ConvertToSubgraph')
// Enter the subgraph
await comfyPage.vueNodes.enterSubgraph()
await fitToViewInstant(comfyPage)
// Get the KSampler node inside the subgraph
ksamplerNode = (await comfyPage.getNodeRefsByType('KSampler', true))?.[0]
ksamplerNode = (
await comfyPage.nodeOps.getNodeRefsByType('KSampler', true)
)?.[0]
const positiveInput = await ksamplerNode.getInput(1)
const negativeInput = await ksamplerNode.getInput(2)
@@ -1027,7 +1081,7 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
true
)
const sourceSlot = await comfyPage.getSubgraphInputSlot()
const sourceSlot = await comfyPage.subgraph.getInputSlot()
const calculatedSourcePos = await sourceSlot.getOpenSlotPosition()
await comfyMouse.move(calculatedSourcePos)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Some files were not shown because too many files have changed in this diff Show More