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 # Force all text files to use LF line endings
* text=auto * text=auto eol=lf
# 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
# Generated files # Generated files
packages/registry-types/src/comfyRegistryTypes.ts linguist-generated=true packages/registry-types/src/comfyRegistryTypes.ts linguist-generated=true

View File

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

View File

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

View File

@@ -110,6 +110,17 @@
"rules": { "rules": {
"no-console": "allow" "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 build`: Type-check then production build to `dist/`
- `pnpm preview`: Preview the production build locally - `pnpm preview`: Preview the production build locally
- `pnpm test:unit`: Run Vitest unit tests - `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 lint` / `pnpm lint:fix`: Lint (ESLint)
- `pnpm format` / `pnpm format:check`: oxfmt - `pnpm format` / `pnpm format:check`: oxfmt
- `pnpm typecheck`: Vue TSC type checking - `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) - `assets/` - Test data (JSON workflows, fixtures)
- Tests use premade JSON workflows to load desired graph state - 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 type { Locator, Page } from '@playwright/test'
import { TestIds } from './selectors'
import { VueNodeFixture } from './utils/vueNodeFixtures' import { VueNodeFixture } from './utils/vueNodeFixtures'
export class VueNodeHelpers { export class VueNodeHelpers {
@@ -148,9 +149,9 @@ export class VueNodeHelpers {
* Get a specific widget by node title and widget name * Get a specific widget by node title and widget name
*/ */
getWidgetByName(nodeTitle: string, widgetName: string): Locator { getWidgetByName(nodeTitle: string, widgetName: string): Locator {
return this.getNodeByTitle(nodeTitle).locator( return this.getNodeByTitle(nodeTitle).getByLabel(widgetName, {
`_vue=[widget.name="${widgetName}"]` exact: true
) })
} }
/** /**
@@ -159,8 +160,8 @@ export class VueNodeHelpers {
getInputNumberControls(widget: Locator) { getInputNumberControls(widget: Locator) {
return { return {
input: widget.locator('input'), input: widget.locator('input'),
decrementButton: widget.getByTestId('decrement'), decrementButton: widget.getByTestId(TestIds.widgets.decrement),
incrementButton: widget.getByTestId('increment') incrementButton: widget.getByTestId(TestIds.widgets.increment)
} }
} }
@@ -170,7 +171,7 @@ export class VueNodeHelpers {
*/ */
async enterSubgraph(nodeId?: string): Promise<void> { async enterSubgraph(nodeId?: string): Promise<void> {
const locator = nodeId ? this.getNodeLocator(nodeId) : this.page 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() 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) { async addFilter(filterValue: string, filterType: string) {
await this.selectFilterType(filterType) await this.selectFilterType(filterType)
await this.selectFilterValue(filterValue) 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 { Page } from '@playwright/test'
import type { ComfyPage } from '../ComfyPage' import type { ComfyPage } from '../ComfyPage'
import { TestIds } from '../selectors'
import { BaseDialog } from './BaseDialog'
export class SettingDialog { export class SettingDialog extends BaseDialog {
constructor( constructor(
public readonly page: Page, page: Page,
public readonly comfyPage: ComfyPage public readonly comfyPage: ComfyPage
) {} ) {
super(page, TestIds.dialogs.settings)
get root() {
return this.page.locator('div.settings-container')
} }
async open() { async open() {
await this.comfyPage.executeCommand('Comfy.ShowSettingsDialog') await this.comfyPage.command.executeCommand('Comfy.ShowSettingsDialog')
await this.page.waitForSelector('div.settings-container') await this.waitForVisible()
} }
/** /**
@@ -41,8 +41,9 @@ export class SettingDialog {
} }
async goToAboutPanel() { async goToAboutPanel() {
const aboutButton = this.page.locator('li[aria-label="About"]') await this.page.getByTestId(TestIds.dialogs.settingsTabAbout).click()
await aboutButton.click() await this.page
await this.page.waitForSelector('div.about-container') .getByTestId(TestIds.dialogs.about)
.waitFor({ state: 'visible' })
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,8 @@
import type { Locator, Page } from '@playwright/test' import type { Locator, Page } from '@playwright/test'
import type { AutoQueueMode } from '../../src/stores/queueStore' import type { AutoQueueMode } from '../../src/stores/queueStore'
import { TestIds } from '../fixtures/selectors'
import type { WorkspaceStore } from '../types/globals'
export class ComfyActionbar { export class ComfyActionbar {
public readonly root: Locator public readonly root: Locator
@@ -26,7 +28,7 @@ class ComfyQueueButton {
public readonly primaryButton: Locator public readonly primaryButton: Locator
public readonly dropdownButton: Locator public readonly dropdownButton: Locator
constructor(public readonly actionbar: ComfyActionbar) { 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.primaryButton = this.root.locator('.p-splitbutton-button')
this.dropdownButton = this.root.locator('.p-splitbutton-dropdown') this.dropdownButton = this.root.locator('.p-splitbutton-dropdown')
} }
@@ -42,13 +44,14 @@ class ComfyQueueButtonOptions {
public async setMode(mode: AutoQueueMode) { public async setMode(mode: AutoQueueMode) {
await this.page.evaluate((mode) => { await this.page.evaluate((mode) => {
window['app'].extensionManager.queueSettings.mode = mode ;(window.app!.extensionManager as WorkspaceStore).queueSettings.mode =
mode
}, mode) }, mode)
} }
public async getMode() { public async getMode() {
return await this.page.evaluate(() => { 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: boolean }
>( >(
({ selectionOnly }) => { ({ selectionOnly }) => {
const app = window['app'] const app = window.app
if (!app?.canvas) return null if (!app?.canvas) return null
const canvas = app.canvas const canvas = app.canvas
@@ -90,7 +90,7 @@ export async function fitToViewInstant(
await comfyPage.page.evaluate( await comfyPage.page.evaluate(
({ bounds, zoom }) => { ({ bounds, zoom }) => {
const app = window['app'] const app = window.app
if (!app?.canvas) return if (!app?.canvas) return
const canvas = app.canvas 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, TemplateInfo,
WorkflowTemplates WorkflowTemplates
} from '../../src/platform/workflow/templates/types/template' } from '../../src/platform/workflow/templates/types/template'
import { TestIds } from '../fixtures/selectors'
export class ComfyTemplates { export class ComfyTemplates {
readonly content: Locator readonly content: Locator
readonly allTemplateCards: Locator readonly allTemplateCards: Locator
constructor(readonly page: Page) { 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-"]') this.allTemplateCards = page.locator('[data-testid^="template-workflow-"]')
} }
async waitForMinimumCardCount(count: number) { async expectMinimumCardCount(count: number) {
return await expect(async () => { await expect(async () => {
const cardCount = await this.allTemplateCards.count() const cardCount = await this.allTemplateCards.count()
expect(cardCount).toBeGreaterThanOrEqual(count) expect(cardCount).toBeGreaterThanOrEqual(count)
}).toPass({ }).toPass({
@@ -26,14 +27,16 @@ export class ComfyTemplates {
} }
async loadTemplate(id: string) { 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.scrollIntoViewIfNeeded()
await templateCard.getByRole('img').click() await templateCard.getByRole('img').click()
} }
async getAllTemplates(): Promise<TemplateInfo[]> { async getAllTemplates(): Promise<TemplateInfo[]> {
const templates: WorkflowTemplates[] = await this.page.evaluate(() => const templates: WorkflowTemplates[] = await this.page.evaluate(() =>
window['app'].api.getCoreWorkflowTemplates() window.app!.api.getCoreWorkflowTemplates()
) )
return templates.flatMap((t) => t.templates) return templates.flatMap((t) => t.templates)
} }

View File

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

View File

@@ -3,18 +3,18 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage' import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
}) })
test.describe('Background Image Upload', () => { test.describe('Background Image Upload', () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
// Reset the background image setting before each test // 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 }) => { test.afterEach(async ({ comfyPage }) => {
// Clean up background image setting after each test // 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 ({ test('should show background image upload component in settings', async ({
@@ -34,16 +34,18 @@ test.describe('Background Image Upload', () => {
await expect(backgroundImageSetting).toBeVisible() await expect(backgroundImageSetting).toBeVisible()
// Verify the component has the expected elements using semantic selectors // 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).toBeVisible()
await expect(urlInput).toHaveAttribute('placeholder') await expect(urlInput).toHaveAttribute('placeholder')
const uploadButton = backgroundImageSetting.locator( const uploadButton = backgroundImageSetting.getByRole('button', {
'button:has(.pi-upload)' name: /upload/i
) })
await expect(uploadButton).toBeVisible() 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).toBeVisible()
await expect(clearButton).toBeDisabled() // Should be disabled when no image await expect(clearButton).toBeDisabled() // Should be disabled when no image
}) })
@@ -63,9 +65,9 @@ test.describe('Background Image Upload', () => {
'#Comfy\\.Canvas\\.BackgroundImage' '#Comfy\\.Canvas\\.BackgroundImage'
) )
// Click the upload button to trigger file input // Click the upload button to trigger file input
const uploadButton = backgroundImageSetting.locator( const uploadButton = backgroundImageSetting.getByRole('button', {
'button:has(.pi-upload)' name: /upload/i
) })
// Set up file upload handler // Set up file upload handler
const fileChooserPromise = comfyPage.page.waitForEvent('filechooser') const fileChooserPromise = comfyPage.page.waitForEvent('filechooser')
@@ -76,15 +78,17 @@ test.describe('Background Image Upload', () => {
await fileChooser.setFiles(comfyPage.assetPath('image32x32.webp')) await fileChooser.setFiles(comfyPage.assetPath('image32x32.webp'))
// Verify the URL input now has an API URL // 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/) await expect(urlInput).toHaveValue(/^\/api\/view\?.*subfolder=backgrounds/)
// Verify clear button is now enabled // 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() await expect(clearButton).toBeEnabled()
// Verify the setting value was actually set // Verify the setting value was actually set
const settingValue = await comfyPage.getSetting( const settingValue = await comfyPage.settings.getSetting(
'Comfy.Canvas.BackgroundImage' 'Comfy.Canvas.BackgroundImage'
) )
expect(settingValue).toMatch(/^\/api\/view\?.*subfolder=backgrounds/) expect(settingValue).toMatch(/^\/api\/view\?.*subfolder=backgrounds/)
@@ -107,18 +111,20 @@ test.describe('Background Image Upload', () => {
'#Comfy\\.Canvas\\.BackgroundImage' '#Comfy\\.Canvas\\.BackgroundImage'
) )
// Enter URL in the input field // Enter URL in the input field
const urlInput = backgroundImageSetting.locator('input[type="text"]') const urlInput = backgroundImageSetting.getByRole('textbox')
await urlInput.fill(testImageUrl) await urlInput.fill(testImageUrl)
// Trigger blur event to ensure the value is set // Trigger blur event to ensure the value is set
await urlInput.blur() await urlInput.blur()
// Verify clear button is now enabled // 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() await expect(clearButton).toBeEnabled()
// Verify the setting value was updated // Verify the setting value was updated
const settingValue = await comfyPage.getSetting( const settingValue = await comfyPage.settings.getSetting(
'Comfy.Canvas.BackgroundImage' 'Comfy.Canvas.BackgroundImage'
) )
expect(settingValue).toBe(testImageUrl) expect(settingValue).toBe(testImageUrl)
@@ -130,7 +136,10 @@ test.describe('Background Image Upload', () => {
const testImageUrl = 'https://example.com/test-image.png' const testImageUrl = 'https://example.com/test-image.png'
// First set a background image // First set a background image
await comfyPage.setSetting('Comfy.Canvas.BackgroundImage', testImageUrl) await comfyPage.settings.setSetting(
'Comfy.Canvas.BackgroundImage',
testImageUrl
)
// Open settings dialog // Open settings dialog
await comfyPage.page.keyboard.press('Control+,') await comfyPage.page.keyboard.press('Control+,')
@@ -144,11 +153,13 @@ test.describe('Background Image Upload', () => {
'#Comfy\\.Canvas\\.BackgroundImage' '#Comfy\\.Canvas\\.BackgroundImage'
) )
// Verify the input has the test URL // Verify the input has the test URL
const urlInput = backgroundImageSetting.locator('input[type="text"]') const urlInput = backgroundImageSetting.getByRole('textbox')
await expect(urlInput).toHaveValue(testImageUrl) await expect(urlInput).toHaveValue(testImageUrl)
// Verify clear button is enabled // 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() await expect(clearButton).toBeEnabled()
// Click the clear button // Click the clear button
@@ -161,7 +172,7 @@ test.describe('Background Image Upload', () => {
await expect(clearButton).toBeDisabled() await expect(clearButton).toBeDisabled()
// Verify the setting value was cleared // Verify the setting value was cleared
const settingValue = await comfyPage.getSetting( const settingValue = await comfyPage.settings.getSetting(
'Comfy.Canvas.BackgroundImage' 'Comfy.Canvas.BackgroundImage'
) )
expect(settingValue).toBe('') expect(settingValue).toBe('')
@@ -182,9 +193,9 @@ test.describe('Background Image Upload', () => {
'#Comfy\\.Canvas\\.BackgroundImage' '#Comfy\\.Canvas\\.BackgroundImage'
) )
// Hover over upload button and verify tooltip appears // Hover over upload button and verify tooltip appears
const uploadButton = backgroundImageSetting.locator( const uploadButton = backgroundImageSetting.getByRole('button', {
'button:has(.pi-upload)' name: /upload/i
) })
await uploadButton.hover() await uploadButton.hover()
const uploadTooltip = comfyPage.page.locator('.p-tooltip:visible') const uploadTooltip = comfyPage.page.locator('.p-tooltip:visible')
@@ -194,12 +205,14 @@ test.describe('Background Image Upload', () => {
await comfyPage.page.locator('body').hover() await comfyPage.page.locator('body').hover()
// Set a background to enable clear button // 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.fill('https://example.com/test.png')
await urlInput.blur() await urlInput.blur()
// Hover over clear button and verify tooltip appears // 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() await clearButton.hover()
const clearTooltip = comfyPage.page.locator('.p-tooltip:visible') const clearTooltip = comfyPage.page.locator('.p-tooltip:visible')
@@ -220,8 +233,10 @@ test.describe('Background Image Upload', () => {
const backgroundImageSetting = comfyPage.page.locator( const backgroundImageSetting = comfyPage.page.locator(
'#Comfy\\.Canvas\\.BackgroundImage' '#Comfy\\.Canvas\\.BackgroundImage'
) )
const urlInput = backgroundImageSetting.locator('input[type="text"]') const urlInput = backgroundImageSetting.getByRole('textbox')
const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)') const clearButton = backgroundImageSetting.getByRole('button', {
name: /clear/i
})
// Initially clear button should be disabled // Initially clear button should be disabled
await expect(clearButton).toBeDisabled() 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.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => { 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 }) => { test('should toggle shortcuts panel visibility', async ({ comfyPage }) => {
// Initially shortcuts panel should be hidden const { bottomPanel } = comfyPage
await expect(comfyPage.page.locator('.bottom-panel')).not.toBeVisible()
// Click shortcuts toggle button in sidebar await expect(bottomPanel.root).not.toBeVisible()
await comfyPage.page await bottomPanel.keyboardShortcutsButton.click()
.locator('button[aria-label*="Keyboard Shortcuts"]') await expect(bottomPanel.root).toBeVisible()
.click() await bottomPanel.keyboardShortcutsButton.click()
await expect(bottomPanel.root).not.toBeVisible()
// 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()
}) })
test('should display essentials shortcuts tab', async ({ comfyPage }) => { test('should display essentials shortcuts tab', async ({ comfyPage }) => {
// Open shortcuts panel const { bottomPanel } = comfyPage
await comfyPage.page
.locator('button[aria-label*="Keyboard Shortcuts"]')
.click()
// Essentials tab should be visible and active by default await bottomPanel.keyboardShortcutsButton.click()
await expect(
comfyPage.page.getByRole('tab', { name: /Essential/i })
).toBeVisible()
await expect(
comfyPage.page.getByRole('tab', { name: /Essential/i })
).toHaveAttribute('aria-selected', 'true')
// Should display shortcut categories await expect(bottomPanel.shortcuts.essentialsTab).toBeVisible()
await expect( await expect(bottomPanel.shortcuts.essentialsTab).toHaveAttribute(
comfyPage.page.locator('.subcategory-title').first() 'aria-selected',
).toBeVisible() 'true'
)
// Should display some keyboard shortcuts await expect(bottomPanel.shortcuts.subcategoryTitles.first()).toBeVisible()
await expect(comfyPage.page.locator('.key-badge').first()).toBeVisible() await expect(bottomPanel.shortcuts.keyBadges.first()).toBeVisible()
// Should have workflow, node, and queue sections
await expect( await expect(
comfyPage.page.getByRole('heading', { name: 'Workflow' }) comfyPage.page.getByRole('heading', { name: 'Workflow' })
).toBeVisible() ).toBeVisible()
@@ -63,23 +43,18 @@ test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => {
}) })
test('should display view controls shortcuts tab', async ({ comfyPage }) => { test('should display view controls shortcuts tab', async ({ comfyPage }) => {
// Open shortcuts panel const { bottomPanel } = comfyPage
await comfyPage.page
.locator('button[aria-label*="Keyboard Shortcuts"]')
.click()
// Click view controls tab await bottomPanel.keyboardShortcutsButton.click()
await comfyPage.page.getByRole('tab', { name: /View Controls/i }).click() await bottomPanel.shortcuts.viewControlsTab.click()
// View controls tab should be active await expect(bottomPanel.shortcuts.viewControlsTab).toHaveAttribute(
await expect( 'aria-selected',
comfyPage.page.getByRole('tab', { name: /View Controls/i }) 'true'
).toHaveAttribute('aria-selected', 'true') )
// Should display view controls shortcuts await expect(bottomPanel.shortcuts.keyBadges.first()).toBeVisible()
await expect(comfyPage.page.locator('.key-badge').first()).toBeVisible()
// Should have view and panel controls sections
await expect( await expect(
comfyPage.page.getByRole('heading', { name: 'View' }) comfyPage.page.getByRole('heading', { name: 'View' })
).toBeVisible() ).toBeVisible()
@@ -89,54 +64,48 @@ test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => {
}) })
test('should switch between shortcuts tabs', async ({ comfyPage }) => { test('should switch between shortcuts tabs', async ({ comfyPage }) => {
// Open shortcuts panel const { bottomPanel } = comfyPage
await comfyPage.page
.locator('button[aria-label*="Keyboard Shortcuts"]')
.click()
// Essentials should be active initially await bottomPanel.keyboardShortcutsButton.click()
await expect(
comfyPage.page.getByRole('tab', { name: /Essential/i })
).toHaveAttribute('aria-selected', 'true')
// Click view controls tab await expect(bottomPanel.shortcuts.essentialsTab).toHaveAttribute(
await comfyPage.page.getByRole('tab', { name: /View Controls/i }).click() 'aria-selected',
'true'
)
// View controls should now be active await bottomPanel.shortcuts.viewControlsTab.click()
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')
// Switch back to essentials await expect(bottomPanel.shortcuts.viewControlsTab).toHaveAttribute(
await comfyPage.page.getByRole('tab', { name: /Essential/i }).click() 'aria-selected',
'true'
)
await expect(bottomPanel.shortcuts.essentialsTab).not.toHaveAttribute(
'aria-selected',
'true'
)
// Essentials should be active again await bottomPanel.shortcuts.essentialsTab.click()
await expect(
comfyPage.page.getByRole('tab', { name: /Essential/i }) await expect(bottomPanel.shortcuts.essentialsTab).toHaveAttribute(
).toHaveAttribute('aria-selected', 'true') 'aria-selected',
await expect( 'true'
comfyPage.page.getByRole('tab', { name: /View Controls/i }) )
).not.toHaveAttribute('aria-selected', 'true') await expect(bottomPanel.shortcuts.viewControlsTab).not.toHaveAttribute(
'aria-selected',
'true'
)
}) })
test('should display formatted keyboard shortcuts', async ({ comfyPage }) => { test('should display formatted keyboard shortcuts', async ({ comfyPage }) => {
// Open shortcuts panel const { bottomPanel } = comfyPage
await comfyPage.page
.locator('button[aria-label*="Keyboard Shortcuts"]')
.click()
// Wait for shortcuts to load await bottomPanel.keyboardShortcutsButton.click()
await comfyPage.page.waitForSelector('.key-badge')
// Check for common formatted keys const keyBadges = bottomPanel.shortcuts.keyBadges
const keyBadges = comfyPage.page.locator('.key-badge') await keyBadges.first().waitFor({ state: 'visible' })
const count = await keyBadges.count() const count = await keyBadges.count()
expect(count).toBeGreaterThanOrEqual(1) expect(count).toBeGreaterThanOrEqual(1)
// Should show formatted modifier keys
const badgeText = await keyBadges.allTextContents() const badgeText = await keyBadges.allTextContents()
const hasModifiers = badgeText.some((text) => const hasModifiers = badgeText.some((text) =>
['Ctrl', 'Cmd', 'Shift', 'Alt'].includes(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 ({ test('should maintain panel state when switching between panels', async ({
comfyPage comfyPage
}) => { }) => {
const { bottomPanel } = comfyPage
// Open shortcuts panel first // Open shortcuts panel first
await comfyPage.page await bottomPanel.keyboardShortcutsButton.click()
.locator('button[aria-label*="Keyboard Shortcuts"]') await expect(bottomPanel.root).toBeVisible()
.click()
await expect(comfyPage.page.locator('.bottom-panel')).toBeVisible()
await expect( await expect(
comfyPage.page.locator('[id*="tab_shortcuts-essentials"]') comfyPage.page.locator('[id*="tab_shortcuts-essentials"]')
).toBeVisible() ).toBeVisible()
// Try to open terminal panel - may show terminal OR close shortcuts // Try to open terminal panel - may show terminal OR close shortcuts
// depending on whether terminal tabs have loaded (async loading) // depending on whether terminal tabs have loaded (async loading)
await comfyPage.page await bottomPanel.toggleButton.click()
.locator('button[aria-label*="Toggle Bottom Panel"]')
.click()
// Check if terminal tabs loaded (Logs tab visible) or fell back to shortcuts toggle // Check if terminal tabs loaded (Logs tab visible) or fell back to shortcuts toggle
const logsTab = comfyPage.page.getByRole('tab', { name: /Logs/i }) const logsTab = comfyPage.page.getByRole('tab', { name: /Logs/i })
@@ -168,12 +135,10 @@ test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => {
if (hasTerminalTabs) { if (hasTerminalTabs) {
// Terminal panel is visible - verify we can switch back to shortcuts // 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 // Switch back to shortcuts
await comfyPage.page await bottomPanel.keyboardShortcutsButton.click()
.locator('button[aria-label*="Keyboard Shortcuts"]')
.click()
// Should show shortcuts content again // Should show shortcuts content again
await expect( await expect(
@@ -181,10 +146,8 @@ test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => {
).toBeVisible() ).toBeVisible()
} else { } else {
// Terminal tabs not loaded - button toggled shortcuts off, reopen for verification // Terminal tabs not loaded - button toggled shortcuts off, reopen for verification
await comfyPage.page await bottomPanel.keyboardShortcutsButton.click()
.locator('button[aria-label*="Keyboard Shortcuts"]') await expect(bottomPanel.root).toBeVisible()
.click()
await expect(comfyPage.page.locator('.bottom-panel')).toBeVisible()
await expect( await expect(
comfyPage.page.locator('[id*="tab_shortcuts-essentials"]') comfyPage.page.locator('[id*="tab_shortcuts-essentials"]')
).toBeVisible() ).toBeVisible()
@@ -192,62 +155,47 @@ test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => {
}) })
test('should handle keyboard navigation', async ({ comfyPage }) => { test('should handle keyboard navigation', async ({ comfyPage }) => {
// Open shortcuts panel const { bottomPanel } = comfyPage
await comfyPage.page
.locator('button[aria-label*="Keyboard Shortcuts"]')
.click()
// Focus the first tab await bottomPanel.keyboardShortcutsButton.click()
await comfyPage.page.getByRole('tab', { name: /Essential/i }).focus() await bottomPanel.shortcuts.essentialsTab.focus()
// Use arrow keys to navigate between tabs
await comfyPage.page.keyboard.press('ArrowRight') await comfyPage.page.keyboard.press('ArrowRight')
// View controls tab should now have focus await expect(bottomPanel.shortcuts.viewControlsTab).toBeFocused()
await expect(
comfyPage.page.getByRole('tab', { name: /View Controls/i })
).toBeFocused()
// Press Enter to activate the tab
await comfyPage.page.keyboard.press('Enter') await comfyPage.page.keyboard.press('Enter')
// Tab should be selected await expect(bottomPanel.shortcuts.viewControlsTab).toHaveAttribute(
await expect( 'aria-selected',
comfyPage.page.getByRole('tab', { name: /View Controls/i }) 'true'
).toHaveAttribute('aria-selected', 'true') )
}) })
test('should close panel by clicking shortcuts button again', async ({ test('should close panel by clicking shortcuts button again', async ({
comfyPage comfyPage
}) => { }) => {
// Open shortcuts panel const { bottomPanel } = comfyPage
await comfyPage.page
.locator('button[aria-label*="Keyboard Shortcuts"]')
.click()
await expect(comfyPage.page.locator('.bottom-panel')).toBeVisible()
// Click shortcuts button again to close await bottomPanel.keyboardShortcutsButton.click()
await comfyPage.page await expect(bottomPanel.root).toBeVisible()
.locator('button[aria-label*="Keyboard Shortcuts"]')
.click()
// Panel should be hidden await bottomPanel.keyboardShortcutsButton.click()
await expect(comfyPage.page.locator('.bottom-panel')).not.toBeVisible() await expect(bottomPanel.root).not.toBeVisible()
}) })
test('should display shortcuts in organized columns', async ({ test('should display shortcuts in organized columns', async ({
comfyPage comfyPage
}) => { }) => {
// Open shortcuts panel const { bottomPanel } = comfyPage
await comfyPage.page
.locator('button[aria-label*="Keyboard Shortcuts"]')
.click()
// Should have 3-column grid layout await bottomPanel.keyboardShortcutsButton.click()
await expect(comfyPage.page.locator('.md\\:grid-cols-3')).toBeVisible()
// Should have multiple subcategory sections await expect(
const subcategoryTitles = comfyPage.page.locator('.subcategory-title') comfyPage.page.locator('[data-testid="shortcuts-columns"]')
).toBeVisible()
const subcategoryTitles = bottomPanel.shortcuts.subcategoryTitles
const titleCount = await subcategoryTitles.count() const titleCount = await subcategoryTitles.count()
expect(titleCount).toBeGreaterThanOrEqual(2) 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 ({ test('should open shortcuts panel with Ctrl+Shift+K', async ({
comfyPage comfyPage
}) => { }) => {
// Initially shortcuts panel should be hidden const { bottomPanel } = comfyPage
await expect(comfyPage.page.locator('.bottom-panel')).not.toBeVisible()
await expect(bottomPanel.root).not.toBeVisible()
// Press Ctrl+Shift+K to open shortcuts panel
await comfyPage.page.keyboard.press('Control+Shift+KeyK') await comfyPage.page.keyboard.press('Control+Shift+KeyK')
// Shortcuts panel should now be visible await expect(bottomPanel.root).toBeVisible()
await expect(comfyPage.page.locator('.bottom-panel')).toBeVisible() await expect(bottomPanel.shortcuts.essentialsTab).toHaveAttribute(
'aria-selected',
// Should show essentials tab by default 'true'
await expect( )
comfyPage.page.getByRole('tab', { name: /Essential/i })
).toHaveAttribute('aria-selected', 'true')
}) })
test('should open settings dialog when clicking manage shortcuts button', async ({ test('should open settings dialog when clicking manage shortcuts button', async ({
comfyPage comfyPage
}) => { }) => {
// Open shortcuts panel const { bottomPanel } = comfyPage
await comfyPage.page
.locator('button[aria-label*="Keyboard Shortcuts"]')
.click()
// Manage shortcuts button should be visible await bottomPanel.keyboardShortcutsButton.click()
await expect(
comfyPage.page.getByRole('button', { name: /Manage Shortcuts/i })
).toBeVisible()
// Click manage shortcuts button await expect(bottomPanel.shortcuts.manageButton).toBeVisible()
await comfyPage.page await bottomPanel.shortcuts.manageButton.click()
.getByRole('button', { name: /Manage Shortcuts/i })
.click()
// Settings dialog should open with keybinding tab
await expect(comfyPage.page.getByRole('dialog')).toBeVisible() await expect(comfyPage.page.getByRole('dialog')).toBeVisible()
// Should show keybinding settings (check for keybinding-related content)
await expect( await expect(
comfyPage.page.getByRole('option', { name: 'Keybinding' }) comfyPage.page.getByRole('option', { name: 'Keybinding' })
).toBeVisible() ).toBeVisible()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,18 +1,19 @@
import type { Locator } from '@playwright/test' import type { Locator } from '@playwright/test'
import { expect } 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 { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { DefaultGraphPositions } from '../fixtures/constants/defaultGraphPositions'
test.beforeEach(async ({ comfyPage }) => { 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.describe('Load workflow warning', { tag: '@ui' }, () => {
test('Should display a warning when loading a workflow with missing nodes', async ({ test('Should display a warning when loading a workflow with missing nodes', async ({
comfyPage 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 // Wait for the element with the .comfy-missing-nodes selector to be visible
const missingNodesWarning = comfyPage.page.locator('.comfy-missing-nodes') 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 ({ test('Should display a warning when loading a workflow with missing nodes in subgraphs', async ({
comfyPage 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 // Wait for the element with the .comfy-missing-nodes selector to be visible
const missingNodesWarning = comfyPage.page.locator('.comfy-missing-nodes') 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 }) => { 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.workflow.loadWorkflow('missing/missing_nodes')
await comfyPage.closeDialog() 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 // Wait for any async operations to complete after dialog closes
await comfyPage.nextFrame() await comfyPage.nextFrame()
// Make a change to the graph // Make a change to the graph
await comfyPage.doubleClickCanvas() await comfyPage.canvasOps.doubleClick()
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler') await comfyPage.searchBox.fillAndSelectFirstNode('KSampler')
// Undo and redo the change // Undo and redo the change
await comfyPage.ctrlZ() await comfyPage.keyboard.undo()
await expect(comfyPage.page.locator('.comfy-missing-nodes')).not.toBeVisible() 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() 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 ({ test('Should display an error message when an execution error occurs', async ({
comfyPage comfyPage
}) => { }) => {
await comfyPage.loadWorkflow('nodes/execution_error') await comfyPage.workflow.loadWorkflow('nodes/execution_error')
await comfyPage.queueButton.click() await comfyPage.queueButton.click()
await comfyPage.nextFrame() await comfyPage.nextFrame()
@@ -71,7 +76,10 @@ test.describe('Execution error', () => {
test.describe('Missing models warning', () => { test.describe('Missing models warning', () => {
test.beforeEach(async ({ comfyPage }) => { 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) => { await comfyPage.page.evaluate((url: string) => {
return fetch(`${url}/api/devtools/cleanup_fake_model`) return fetch(`${url}/api/devtools/cleanup_fake_model`)
}, comfyPage.url) }, comfyPage.url)
@@ -80,7 +88,7 @@ test.describe('Missing models warning', () => {
test('Should display a warning when missing models are found', async ({ test('Should display a warning when missing models are found', async ({
comfyPage comfyPage
}) => { }) => {
await comfyPage.loadWorkflow('missing/missing_models') await comfyPage.workflow.loadWorkflow('missing/missing_models')
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models') const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
await expect(missingModelsWarning).toBeVisible() await expect(missingModelsWarning).toBeVisible()
@@ -97,7 +105,9 @@ test.describe('Missing models warning', () => {
comfyPage comfyPage
}) => { }) => {
// Load workflow that has a node with models metadata at the node level // 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') const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
await expect(missingModelsWarning).toBeVisible() await expect(missingModelsWarning).toBeVisible()
@@ -146,7 +156,7 @@ test.describe('Missing models warning', () => {
{ times: 1 } { times: 1 }
) )
await comfyPage.loadWorkflow('missing/missing_models') await comfyPage.workflow.loadWorkflow('missing/missing_models')
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models') const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
await expect(missingModelsWarning).not.toBeVisible() 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 // This tests the scenario where outdated model metadata exists in the workflow
// but the actual selected models (widget values) have changed // 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 // The missing models warning should NOT appear
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models') const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
@@ -171,7 +183,7 @@ test.describe('Missing models warning', () => {
}) => { }) => {
// The fake_model.safetensors is served by // The fake_model.safetensors is served by
// https://github.com/Comfy-Org/ComfyUI_devtools/blob/main/__init__.py // 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') const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
await expect(missingModelsWarning).toBeVisible() await expect(missingModelsWarning).toBeVisible()
@@ -190,11 +202,11 @@ test.describe('Missing models warning', () => {
let closeButton: Locator let closeButton: Locator
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting( await comfyPage.settings.setSetting(
'Comfy.Workflow.ShowMissingModelsWarning', 'Comfy.Workflow.ShowMissingModelsWarning',
true true
) )
await comfyPage.loadWorkflow('missing/missing_models') await comfyPage.workflow.loadWorkflow('missing/missing_models')
checkbox = comfyPage.page.getByLabel("Don't show this again") checkbox = comfyPage.page.getByLabel("Don't show this again")
closeButton = comfyPage.page.getByLabel('Close') closeButton = comfyPage.page.getByLabel('Close')
@@ -210,7 +222,7 @@ test.describe('Missing models warning', () => {
await closeButton.click() await closeButton.click()
await changeSettingPromise await changeSettingPromise
const settingValue = await comfyPage.getSetting( const settingValue = await comfyPage.settings.getSetting(
'Comfy.Workflow.ShowMissingModelsWarning' 'Comfy.Workflow.ShowMissingModelsWarning'
) )
expect(settingValue).toBe(false) expect(settingValue).toBe(false)
@@ -221,7 +233,7 @@ test.describe('Missing models warning', () => {
}) => { }) => {
await closeButton.click() await closeButton.click()
const settingValue = await comfyPage.getSetting( const settingValue = await comfyPage.settings.getSetting(
'Comfy.Workflow.ShowMissingModelsWarning' 'Comfy.Workflow.ShowMissingModelsWarning'
) )
expect(settingValue).toBe(true) expect(settingValue).toBe(true)
@@ -252,9 +264,11 @@ test.describe('Settings', () => {
test('Can change canvas zoom speed setting', async ({ comfyPage }) => { test('Can change canvas zoom speed setting', async ({ comfyPage }) => {
const maxSpeed = 2.5 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 () => { 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 ({ test('Should open external zendesk link with OSS tag', async ({
comfyPage comfyPage
}) => { }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
const pagePromise = comfyPage.page.context().waitForEvent('page') const pagePromise = comfyPage.page.context().waitForEvent('page')
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Support']) await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Support'])
const newPage = await pagePromise const newPage = await pagePromise
@@ -331,13 +345,13 @@ test.describe('Error dialog', () => {
comfyPage comfyPage
}) => { }) => {
await comfyPage.page.evaluate(() => { await comfyPage.page.evaluate(() => {
const graph = window['graph'] const graph = window.graph!
graph.configure = () => { ;(graph as { configure: () => void }).configure = () => {
throw new Error('Error on configure!') throw new Error('Error on configure!')
} }
}) })
await comfyPage.loadWorkflow('default') await comfyPage.workflow.loadWorkflow('default')
const errorDialog = comfyPage.page.locator('.comfy-error-report') const errorDialog = comfyPage.page.locator('.comfy-error-report')
await expect(errorDialog).toBeVisible() await expect(errorDialog).toBeVisible()
@@ -347,7 +361,7 @@ test.describe('Error dialog', () => {
comfyPage comfyPage
}) => { }) => {
await comfyPage.page.evaluate(async () => { await comfyPage.page.evaluate(async () => {
const app = window['app'] const app = window.app!
app.api.queuePrompt = () => { app.api.queuePrompt = () => {
throw new Error('Error on 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 ({ test('Paste content to signin dialog should not paste node on canvas', async ({
comfyPage comfyPage
}) => { }) => {
const nodeNum = (await comfyPage.getNodes()).length const nodeNum = (await comfyPage.nodeOps.getNodes()).length
await comfyPage.clickEmptyLatentNode() await comfyPage.canvas.click({
await comfyPage.ctrlC() position: DefaultGraphPositions.emptyLatentWidgetClick
})
await comfyPage.page.mouse.move(10, 10)
await comfyPage.nextFrame()
await comfyPage.clipboard.copy()
const textBox = comfyPage.widgetTextBox const textBox = comfyPage.widgetTextBox
await textBox.click() await textBox.click()
@@ -373,7 +391,7 @@ test.describe('Signin dialog', () => {
await textBox.press('Control+c') await textBox.press('Control+c')
await comfyPage.page.evaluate(() => { 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') const input = comfyPage.page.locator('#comfy-org-sign-in-password')
@@ -381,6 +399,6 @@ test.describe('Signin dialog', () => {
await input.press('Control+v') await input.press('Control+v')
await expect(input).toHaveValue('test_password') 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' import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ 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.describe('DOM Widget', { tag: '@widget' }, () => {
test('Collapsed multiline textarea is not visible', async ({ comfyPage }) => { 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') const textareaWidget = comfyPage.page.locator('.comfy-multiline-input')
await expect(textareaWidget).not.toBeVisible() await expect(textareaWidget).not.toBeVisible()
}) })
@@ -21,7 +21,7 @@ test.describe('DOM Widget', { tag: '@widget' }, () => {
await expect(firstMultiline).toBeVisible() await expect(firstMultiline).toBeVisible()
await expect(lastMultiline).toBeVisible() await expect(lastMultiline).toBeVisible()
const nodes = await comfyPage.getNodeRefsByType('CLIPTextEncode') const nodes = await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
for (const node of nodes) { for (const node of nodes) {
await node.click('collapse') await node.click('collapse')
} }
@@ -33,8 +33,8 @@ test.describe('DOM Widget', { tag: '@widget' }, () => {
'Position update when entering focus mode', 'Position update when entering focus mode',
{ tag: '@screenshot' }, { tag: '@screenshot' },
async ({ comfyPage }) => { async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.executeCommand('Workspace.ToggleFocusMode') await comfyPage.command.executeCommand('Workspace.ToggleFocusMode')
await comfyPage.nextFrame() await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('focus-mode-on.png') await expect(comfyPage.canvas).toHaveScreenshot('focus-mode-on.png')
} }
@@ -68,9 +68,9 @@ test.describe('DOM Widget', { tag: '@widget' }, () => {
.first() .first()
await expect(textareaWidget).toBeVisible() await expect(textareaWidget).toBeVisible()
await comfyPage.setSetting('Comfy.Sidebar.Size', 'small') await comfyPage.settings.setSetting('Comfy.Sidebar.Size', 'small')
await comfyPage.setSetting('Comfy.Sidebar.Location', 'left') await comfyPage.settings.setSetting('Comfy.Sidebar.Location', 'left')
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.nextFrame() await comfyPage.nextFrame()
let oldPos: [number, number] let oldPos: [number, number]
@@ -85,15 +85,15 @@ test.describe('DOM Widget', { tag: '@widget' }, () => {
// --- test --- // --- test ---
await comfyPage.setSetting('Comfy.Sidebar.Size', 'normal') await comfyPage.settings.setSetting('Comfy.Sidebar.Size', 'normal')
await comfyPage.nextFrame() await comfyPage.nextFrame()
await checkBboxChange() await checkBboxChange()
await comfyPage.setSetting('Comfy.Sidebar.Location', 'right') await comfyPage.settings.setSetting('Comfy.Sidebar.Location', 'right')
await comfyPage.nextFrame() await comfyPage.nextFrame()
await checkBboxChange() await checkBboxChange()
await comfyPage.setSetting('Comfy.UseNewMenu', 'Bottom') await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Bottom')
await comfyPage.nextFrame() await comfyPage.nextFrame()
await checkBboxChange() await checkBboxChange()
}) })

View File

@@ -3,7 +3,7 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage' import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
}) })
test.describe('Execution', { tag: ['@smoke', '@workflow'] }, () => { test.describe('Execution', { tag: ['@smoke', '@workflow'] }, () => {
@@ -11,12 +11,15 @@ test.describe('Execution', { tag: ['@smoke', '@workflow'] }, () => {
'Report error on unconnected slot', 'Report error on unconnected slot',
{ tag: '@screenshot' }, { tag: '@screenshot' },
async ({ comfyPage }) => { async ({ comfyPage }) => {
await comfyPage.disconnectEdge() await comfyPage.canvasOps.disconnectEdge()
await comfyPage.clickEmptySpace() 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 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({ await comfyPage.page.locator('.comfy-error-report').waitFor({
state: 'hidden' state: 'hidden'
}) })
@@ -32,17 +35,17 @@ test.describe(
{ tag: ['@smoke', '@workflow'] }, { tag: ['@smoke', '@workflow'] },
() => { () => {
test('Execute to selected output nodes', async ({ comfyPage }) => { test('Execute to selected output nodes', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('execution/partial_execution') await comfyPage.workflow.loadWorkflow('execution/partial_execution')
const input = await comfyPage.getNodeRefById(3) const input = await comfyPage.nodeOps.getNodeRefById(3)
const output1 = await comfyPage.getNodeRefById(1) const output1 = await comfyPage.nodeOps.getNodeRefById(1)
const output2 = await comfyPage.getNodeRefById(4) const output2 = await comfyPage.nodeOps.getNodeRefById(4)
expect(await (await input.getWidget(0)).getValue()).toBe('foo') expect(await (await input.getWidget(0)).getValue()).toBe('foo')
expect(await (await output1.getWidget(0)).getValue()).toBe('') expect(await (await output1.getWidget(0)).getValue()).toBe('')
expect(await (await output2.getWidget(0)).getValue()).toBe('') expect(await (await output2.getWidget(0)).getValue()).toBe('')
await output1.click('title') await output1.click('title')
await comfyPage.executeCommand('Comfy.QueueSelectedOutputNodes') await comfyPage.command.executeCommand('Comfy.QueueSelectedOutputNodes')
await expect(async () => { await expect(async () => {
expect(await (await input.getWidget(0)).getValue()).toBe('foo') expect(await (await input.getWidget(0)).getValue()).toBe('foo')
expect(await (await output1.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 { expect } from '@playwright/test'
import type { Settings } from '../../src/schemas/apiSchema'
import type { SettingParams } from '../../src/platform/settings/types' import type { SettingParams } from '../../src/platform/settings/types'
import { comfyPageFixture as test } from '../fixtures/ComfyPage' 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.describe('Topbar commands', () => {
test.beforeEach(async ({ comfyPage }) => { 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 }) => { test('Should allow registering topbar commands', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => { await comfyPage.page.evaluate(() => {
window['app'].registerExtension({ window.app!.registerExtension({
name: 'TestExtension1', name: 'TestExtension1',
commands: [ commands: [
{ {
id: 'foo', id: 'foo',
label: 'foo-command', label: 'foo-command',
function: () => { function: () => {
window['foo'] = true window.foo = true
} }
} }
], ],
@@ -31,15 +40,15 @@ test.describe('Topbar commands', () => {
}) })
await comfyPage.menu.topbar.triggerTopbarCommand(['ext', 'foo-command']) 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 ({ test('Should not allow register command defined in other extension', async ({
comfyPage comfyPage
}) => { }) => {
await comfyPage.registerCommand('foo', () => alert(1)) await comfyPage.command.registerCommand('foo', () => alert(1))
await comfyPage.page.evaluate(() => { await comfyPage.page.evaluate(() => {
window['app'].registerExtension({ window.app!.registerExtension({
name: 'TestExtension1', name: 'TestExtension1',
menuCommands: [ menuCommands: [
{ {
@@ -56,14 +65,14 @@ test.describe('Topbar commands', () => {
test('Should allow registering keybindings', async ({ comfyPage }) => { test('Should allow registering keybindings', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => { await comfyPage.page.evaluate(() => {
const app = window['app'] const app = window.app!
app.registerExtension({ app.registerExtension({
name: 'TestExtension1', name: 'TestExtension1',
commands: [ commands: [
{ {
id: 'TestCommand', id: 'TestCommand',
function: () => { function: () => {
window['TestCommand'] = true window.TestCommand = true
} }
} }
], ],
@@ -77,68 +86,77 @@ test.describe('Topbar commands', () => {
}) })
await comfyPage.page.keyboard.press('k') await comfyPage.page.keyboard.press('k')
expect(await comfyPage.page.evaluate(() => window['TestCommand'])).toBe( expect(await comfyPage.page.evaluate(() => window.TestCommand)).toBe(true)
true
)
}) })
test.describe('Settings', () => { test.describe('Settings', () => {
test('Should allow adding settings', async ({ comfyPage }) => { test('Should allow adding settings', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => { await comfyPage.page.evaluate(() => {
window['app'].registerExtension({ window.app!.registerExtension({
name: 'TestExtension1', name: 'TestExtension1',
settings: [ settings: [
{ {
id: 'TestSetting', // Extensions can register arbitrary setting IDs
id: 'TestSetting' as TestSettingId,
name: 'Test Setting', name: 'Test Setting',
type: 'text', type: 'text',
defaultValue: 'Hello, world!', defaultValue: 'Hello, world!',
onChange: () => { onChange: () => {
window['changeCount'] = (window['changeCount'] ?? 0) + 1 window.changeCount = (window.changeCount ?? 0) + 1
} }
} }
] ]
}) })
}) })
// onChange is called when the setting is first added // onChange is called when the setting is first added
expect(await comfyPage.page.evaluate(() => window['changeCount'])).toBe(1) expect(await comfyPage.page.evaluate(() => window.changeCount)).toBe(1)
expect(await comfyPage.getSetting('TestSetting')).toBe('Hello, world!') expect(await comfyPage.settings.getSetting('TestSetting')).toBe(
'Hello, world!'
)
await comfyPage.setSetting('TestSetting', 'Hello, universe!') await comfyPage.settings.setSetting('TestSetting', 'Hello, universe!')
expect(await comfyPage.getSetting('TestSetting')).toBe('Hello, universe!') expect(await comfyPage.settings.getSetting('TestSetting')).toBe(
expect(await comfyPage.page.evaluate(() => window['changeCount'])).toBe(2) 'Hello, universe!'
)
expect(await comfyPage.page.evaluate(() => window.changeCount)).toBe(2)
}) })
test('Should allow setting boolean settings', async ({ comfyPage }) => { test('Should allow setting boolean settings', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => { await comfyPage.page.evaluate(() => {
window['app'].registerExtension({ window.app!.registerExtension({
name: 'TestExtension1', name: 'TestExtension1',
settings: [ settings: [
{ {
id: 'Comfy.TestSetting', // Extensions can register arbitrary setting IDs
id: 'Comfy.TestSetting' as TestSettingId,
name: 'Test Setting', name: 'Test Setting',
type: 'boolean', type: 'boolean',
defaultValue: false, defaultValue: false,
onChange: () => { onChange: () => {
window['changeCount'] = (window['changeCount'] ?? 0) + 1 window.changeCount = (window.changeCount ?? 0) + 1
} }
} }
] ]
}) })
}) })
expect(await comfyPage.getSetting('Comfy.TestSetting')).toBe(false) expect(await comfyPage.settings.getSetting('Comfy.TestSetting')).toBe(
expect(await comfyPage.page.evaluate(() => window['changeCount'])).toBe(1) false
)
expect(await comfyPage.page.evaluate(() => window.changeCount)).toBe(1)
await comfyPage.settingDialog.open() await comfyPage.settingDialog.open()
await comfyPage.settingDialog.toggleBooleanSetting('Comfy.TestSetting') await comfyPage.settingDialog.toggleBooleanSetting('Comfy.TestSetting')
expect(await comfyPage.getSetting('Comfy.TestSetting')).toBe(true) expect(await comfyPage.settings.getSetting('Comfy.TestSetting')).toBe(
expect(await comfyPage.page.evaluate(() => window['changeCount'])).toBe(2) true
)
expect(await comfyPage.page.evaluate(() => window.changeCount)).toBe(2)
}) })
test.describe('Passing through attrs to setting components', () => { test.describe('Passing through attrs to setting components', () => {
const testCases: Array<{ const testCases: Array<{
config: Partial<SettingParams> config: Pick<SettingParams, 'type' | 'defaultValue'> &
Partial<Omit<SettingParams, 'id' | 'type' | 'defaultValue'>>
selector: string selector: string
}> = [ }> = [
{ {
@@ -191,13 +209,13 @@ test.describe('Topbar commands', () => {
comfyPage comfyPage
}) => { }) => {
await comfyPage.page.evaluate((config) => { await comfyPage.page.evaluate((config) => {
window['app'].registerExtension({ window.app!.registerExtension({
name: 'TestExtension1', name: 'TestExtension1',
settings: [ settings: [
{ {
id: 'Comfy.TestSetting', // Extensions can register arbitrary setting IDs
id: 'Comfy.TestSetting' as TestSettingId,
name: 'Test', name: 'Test',
// The `disabled` attr is common to all settings components
attrs: { disabled: true }, attrs: { disabled: true },
...config ...config
} }
@@ -224,7 +242,7 @@ test.describe('Topbar commands', () => {
test.describe('About panel', () => { test.describe('About panel', () => {
test('Should allow adding badges', async ({ comfyPage }) => { test('Should allow adding badges', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => { await comfyPage.page.evaluate(() => {
window['app'].registerExtension({ window.app!.registerExtension({
name: 'TestExtension1', name: 'TestExtension1',
aboutPageBadges: [ aboutPageBadges: [
{ {
@@ -247,61 +265,71 @@ test.describe('Topbar commands', () => {
test.describe('Dialog', () => { test.describe('Dialog', () => {
test('Should allow showing a prompt dialog', async ({ comfyPage }) => { test('Should allow showing a prompt dialog', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => { await comfyPage.page.evaluate(() => {
void window['app'].extensionManager.dialog void window
.prompt({ .app!.extensionManager.dialog.prompt({
title: 'Test Prompt', title: 'Test Prompt',
message: 'Test Prompt Message' message: 'Test Prompt Message'
}) })
.then((value: string) => { .then((value: string | null) => {
window['value'] = value ;(window as unknown as Record<string, unknown>)['value'] = value
}) })
}) })
await comfyPage.fillPromptDialog('Hello, world!') await comfyPage.nodeOps.fillPromptDialog('Hello, world!')
expect(await comfyPage.page.evaluate(() => window['value'])).toBe( expect(
'Hello, world!' await comfyPage.page.evaluate(
) () => (window as unknown as Record<string, unknown>)['value']
)
).toBe('Hello, world!')
}) })
test('Should allow showing a confirmation dialog', async ({ test('Should allow showing a confirmation dialog', async ({
comfyPage comfyPage
}) => { }) => {
await comfyPage.page.evaluate(() => { await comfyPage.page.evaluate(() => {
void window['app'].extensionManager.dialog void window
.confirm({ .app!.extensionManager.dialog.confirm({
title: 'Test Confirm', title: 'Test Confirm',
message: 'Test Confirm Message' message: 'Test Confirm Message'
}) })
.then((value: boolean) => { .then((value: boolean | null) => {
window['value'] = value ;(window as unknown as Record<string, unknown>)['value'] = value
}) })
}) })
await comfyPage.confirmDialog.click('confirm') 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 }) => { test('Should allow dismissing a dialog', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => { await comfyPage.page.evaluate(() => {
window['value'] = 'foo' ;(window as unknown as Record<string, unknown>)['value'] = 'foo'
void window['app'].extensionManager.dialog void window
.confirm({ .app!.extensionManager.dialog.confirm({
title: 'Test Confirm', title: 'Test Confirm',
message: 'Test Confirm Message' message: 'Test Confirm Message'
}) })
.then((value: boolean) => { .then((value: boolean | null) => {
window['value'] = value ;(window as unknown as Record<string, unknown>)['value'] = value
}) })
}) })
await comfyPage.confirmDialog.click('reject') 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.describe('Selection Toolbox', () => {
test.beforeEach(async ({ comfyPage }) => { 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 ({ 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 // Register an extension with a selection toolbox command
await comfyPage.page.evaluate(() => { await comfyPage.page.evaluate(() => {
window['app'].registerExtension({ window.app!.registerExtension({
name: 'TestExtension1', name: 'TestExtension1',
commands: [ commands: [
{ {
@@ -317,7 +345,9 @@ test.describe('Topbar commands', () => {
label: 'Test Command', label: 'Test Command',
icon: 'pi pi-star', icon: 'pi pi-star',
function: () => { 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 // Click the command button in the selection toolbox
const toolboxButton = comfyPage.page.locator( const toolboxButton = comfyPage.page.locator(
@@ -333,9 +363,13 @@ test.describe('Topbar commands', () => {
) )
await toolboxButton.click() await toolboxButton.click()
// Verify the command was executed
expect( expect(
await comfyPage.page.evaluate(() => window['selectionCommandExecuted']) await comfyPage.page.evaluate(
() =>
(window as unknown as Record<string, unknown>)[
'selectionCommandExecuted'
]
)
).toBe(true) ).toBe(true)
}) })
}) })

View File

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

View File

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

View File

@@ -1,11 +1,15 @@
import { expect } from '@playwright/test' import { expect } from '@playwright/test'
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
import type { ComfyPage } from '../fixtures/ComfyPage' import type { ComfyPage } from '../fixtures/ComfyPage'
import { comfyPageFixture as test } 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' import type { NodeReference } from '../fixtures/utils/litegraphUtils'
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
}) })
test.describe('Group Node', { tag: '@node' }, () => { test.describe('Group Node', { tag: '@node' }, () => {
@@ -13,30 +17,34 @@ test.describe('Group Node', { tag: '@node' }, () => {
const groupNodeName = 'DefautWorkflowGroupNode' const groupNodeName = 'DefautWorkflowGroupNode'
const groupNodeCategory = 'group nodes>workflow' const groupNodeCategory = 'group nodes>workflow'
const groupNodeBookmarkName = `workflow>${groupNodeName}` const groupNodeBookmarkName = `workflow>${groupNodeName}`
let libraryTab let libraryTab: NodeLibrarySidebarTab
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
libraryTab = comfyPage.menu.nodeLibraryTab libraryTab = comfyPage.menu.nodeLibraryTab
await comfyPage.convertAllNodesToGroupNode(groupNodeName) await comfyPage.nodeOps.convertAllNodesToGroupNode(groupNodeName)
await libraryTab.open() await libraryTab.open()
}) })
test('Is added to node library sidebar', async ({ comfyPage }) => { test('Is added to node library sidebar', async ({
expect(await libraryTab.getFolder('group nodes').count()).toBe(1) comfyPage: _comfyPage
}) => {
expect(await libraryTab.getFolder(groupNodeCategory).count()).toBe(1)
}) })
test('Can be added to canvas using node library sidebar', async ({ test('Can be added to canvas using node library sidebar', async ({
comfyPage comfyPage
}) => { }) => {
const initialNodeCount = await comfyPage.getGraphNodesCount() const initialNodeCount = await comfyPage.nodeOps.getGraphNodesCount()
// Add group node from node library sidebar // Add group node from node library sidebar
await libraryTab.getFolder(groupNodeCategory).click() await libraryTab.getFolder(groupNodeCategory).click()
await libraryTab.getNode(groupNodeName).click() await libraryTab.getNode(groupNodeName).click()
// Verify the node is added to the canvas // 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 }) => { 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 // Verify the node is added to the bookmarks tab
expect( expect(
await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
).toEqual([groupNodeBookmarkName]) ).toEqual([groupNodeBookmarkName])
// Verify the bookmark node with the same name is added to the tree // Verify the bookmark node with the same name is added to the tree
expect(await libraryTab.getNode(groupNodeName).count()).not.toBe(0) 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 // Verify the node is removed from the bookmarks tab
expect( expect(
await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
).toHaveLength(0) ).toHaveLength(0)
}) })
@@ -94,8 +102,8 @@ test.describe('Group Node', { tag: '@node' }, () => {
{ tag: '@screenshot' }, { tag: '@screenshot' },
async ({ comfyPage }) => { async ({ comfyPage }) => {
const groupNodeName = 'DefautWorkflowGroupNode' const groupNodeName = 'DefautWorkflowGroupNode'
await comfyPage.convertAllNodesToGroupNode(groupNodeName) await comfyPage.nodeOps.convertAllNodesToGroupNode(groupNodeName)
await comfyPage.doubleClickCanvas() await comfyPage.canvasOps.doubleClick()
await comfyPage.nextFrame() await comfyPage.nextFrame()
await comfyPage.searchBox.fillAndSelectFirstNode(groupNodeName) await comfyPage.searchBox.fillAndSelectFirstNode(groupNodeName)
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
@@ -105,8 +113,8 @@ test.describe('Group Node', { tag: '@node' }, () => {
) )
test('Displays tooltip on title hover', async ({ comfyPage }) => { test('Displays tooltip on title hover', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.EnableTooltips', true) await comfyPage.settings.setSetting('Comfy.EnableTooltips', true)
await comfyPage.convertAllNodesToGroupNode('Group Node') await comfyPage.nodeOps.convertAllNodesToGroupNode('Group Node')
await comfyPage.page.mouse.move(47, 173) await comfyPage.page.mouse.move(47, 173)
await expect(comfyPage.page.locator('.node-tooltip')).toBeVisible() 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 ({ test('Manage group opens with the correct group selected', async ({
comfyPage comfyPage
}) => { }) => {
const makeGroup = async (name, type1, type2) => { const makeGroup = async (name: string, type1: string, type2: string) => {
const node1 = (await comfyPage.getNodeRefsByType(type1))[0] const node1 = (await comfyPage.nodeOps.getNodeRefsByType(type1))[0]
const node2 = (await comfyPage.getNodeRefsByType(type2))[0] const node2 = (await comfyPage.nodeOps.getNodeRefsByType(type2))[0]
await node1.click('title') await node1.click('title')
await node2.click('title', { await node2.click('title', {
modifiers: ['Shift'] modifiers: ['Shift']
@@ -144,7 +152,7 @@ test.describe('Group Node', { tag: '@node' }, () => {
test('Preserves hidden input configuration when containing duplicate node types', async ({ test('Preserves hidden input configuration when containing duplicate node types', async ({
comfyPage comfyPage
}) => { }) => {
await comfyPage.loadWorkflow( await comfyPage.workflow.loadWorkflow(
'groupnodes/group_node_identical_nodes_hidden_inputs' 'groupnodes/group_node_identical_nodes_hidden_inputs'
) )
await comfyPage.nextFrame() await comfyPage.nextFrame()
@@ -155,16 +163,14 @@ test.describe('Group Node', { tag: '@node' }, () => {
const totalInputCount = await comfyPage.page.evaluate((nodeName) => { const totalInputCount = await comfyPage.page.evaluate((nodeName) => {
const { const {
extra: { groupNodes } extra: { groupNodes }
} = window['app'].graph } = window.app!.graph!
const { nodes } = groupNodes[nodeName] const { nodes } = groupNodes![nodeName]
return nodes.reduce((acc: number, node) => { return nodes.reduce((acc, node) => acc + (node.inputs?.length ?? 0), 0)
return acc + node.inputs.length
}, 0)
}, groupNodeName) }, groupNodeName)
const visibleInputCount = await comfyPage.page.evaluate((id) => { const visibleInputCount = await comfyPage.page.evaluate((id) => {
const node = window['app'].graph.getNodeById(id) const node = window.app!.graph!.getNodeById(id)
return node.inputs.length return node!.inputs.length
}, groupNodeId) }, groupNodeId)
// Verify there are 4 total inputs (2 VAE decode nodes with 2 inputs each) // 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 comfyPage
}) => { }) => {
const expectSingleNode = async (type: string) => { const expectSingleNode = async (type: string) => {
const nodes = await comfyPage.getNodeRefsByType(type) const nodes = await comfyPage.nodeOps.getNodeRefsByType(type)
expect(nodes).toHaveLength(1) expect(nodes).toHaveLength(1)
return nodes[0] return nodes[0]
} }
@@ -213,8 +219,8 @@ test.describe('Group Node', { tag: '@node' }, () => {
test('Loads from a workflow using the legacy path separator ("/")', async ({ test('Loads from a workflow using the legacy path separator ("/")', async ({
comfyPage comfyPage
}) => { }) => {
await comfyPage.loadWorkflow('groupnodes/legacy_group_node') await comfyPage.workflow.loadWorkflow('groupnodes/legacy_group_node')
expect(await comfyPage.getGraphNodesCount()).toBe(1) expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe(1)
await expect( await expect(
comfyPage.page.locator('.comfy-missing-nodes') comfyPage.page.locator('.comfy-missing-nodes')
).not.toBeVisible() ).not.toBeVisible()
@@ -230,7 +236,7 @@ test.describe('Group Node', { tag: '@node' }, () => {
const isRegisteredLitegraph = async (comfyPage: ComfyPage) => { const isRegisteredLitegraph = async (comfyPage: ComfyPage) => {
return await comfyPage.page.evaluate((nodeType: string) => { return await comfyPage.page.evaluate((nodeType: string) => {
return !!window['LiteGraph'].registered_node_types[nodeType] return !!window.LiteGraph!.registered_node_types[nodeType]
}, GROUP_NODE_TYPE) }, GROUP_NODE_TYPE)
} }
@@ -246,17 +252,17 @@ test.describe('Group Node', { tag: '@node' }, () => {
comfyPage: ComfyPage, comfyPage: ComfyPage,
expectedCount: number expectedCount: number
) => { ) => {
expect(await comfyPage.getNodeRefsByType(GROUP_NODE_TYPE)).toHaveLength( expect(
expectedCount await comfyPage.nodeOps.getNodeRefsByType(GROUP_NODE_TYPE)
) ).toHaveLength(expectedCount)
expect(await isRegisteredLitegraph(comfyPage)).toBe(true) expect(await isRegisteredLitegraph(comfyPage)).toBe(true)
expect(await isRegisteredNodeDefStore(comfyPage)).toBe(true) expect(await isRegisteredNodeDefStore(comfyPage)).toBe(true)
} }
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.loadWorkflow(WORKFLOW_NAME) await comfyPage.workflow.loadWorkflow(WORKFLOW_NAME)
groupNode = await comfyPage.getFirstNodeRef() groupNode = await comfyPage.nodeOps.getFirstNodeRef()
if (!groupNode) if (!groupNode)
throw new Error(`Group node not found in workflow ${WORKFLOW_NAME}`) throw new Error(`Group node not found in workflow ${WORKFLOW_NAME}`)
await groupNode.copy() await groupNode.copy()
@@ -265,7 +271,7 @@ test.describe('Group Node', { tag: '@node' }, () => {
test('Copies and pastes group node within the same workflow', async ({ test('Copies and pastes group node within the same workflow', async ({
comfyPage comfyPage
}) => { }) => {
await comfyPage.ctrlV() await comfyPage.clipboard.paste()
await verifyNodeLoaded(comfyPage, 2) await verifyNodeLoaded(comfyPage, 2)
}) })
@@ -273,12 +279,12 @@ test.describe('Group Node', { tag: '@node' }, () => {
comfyPage comfyPage
}) => { }) => {
// Set setting // Set setting
await comfyPage.setSetting('Comfy.ConfirmClear', false) await comfyPage.settings.setSetting('Comfy.ConfirmClear', false)
// Clear workflow // Clear workflow
await comfyPage.executeCommand('Comfy.ClearWorkflow') await comfyPage.command.executeCommand('Comfy.ClearWorkflow')
await comfyPage.ctrlV() await comfyPage.clipboard.paste()
await verifyNodeLoaded(comfyPage, 1) await verifyNodeLoaded(comfyPage, 1)
}) })
@@ -286,15 +292,15 @@ test.describe('Group Node', { tag: '@node' }, () => {
comfyPage comfyPage
}) => { }) => {
await comfyPage.menu.topbar.triggerTopbarCommand(['New']) await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
await comfyPage.ctrlV() await comfyPage.clipboard.paste()
await verifyNodeLoaded(comfyPage, 1) await verifyNodeLoaded(comfyPage, 1)
}) })
test('Copies and pastes group node across different workflows', async ({ test('Copies and pastes group node across different workflows', async ({
comfyPage comfyPage
}) => { }) => {
await comfyPage.loadWorkflow('default') await comfyPage.workflow.loadWorkflow('default')
await comfyPage.ctrlV() await comfyPage.clipboard.paste()
await verifyNodeLoaded(comfyPage, 1) await verifyNodeLoaded(comfyPage, 1)
}) })
@@ -302,14 +308,15 @@ test.describe('Group Node', { tag: '@node' }, () => {
comfyPage comfyPage
}) => { }) => {
await comfyPage.menu.topbar.triggerTopbarCommand(['New']) await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
await comfyPage.ctrlV() await comfyPage.clipboard.paste()
const currentGraphState = await comfyPage.page.evaluate(() => 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 test.step('Load workflow containing a group node pasted from a different workflow', async () => {
await comfyPage.page.evaluate( await comfyPage.page.evaluate(
(workflow) => window['app'].loadGraphData(workflow), (workflow) =>
window.app!.loadGraphData(workflow as ComfyWorkflowJSON),
currentGraphState currentGraphState
) )
await comfyPage.nextFrame() await comfyPage.nextFrame()
@@ -320,15 +327,18 @@ test.describe('Group Node', { tag: '@node' }, () => {
test.describe('Keybindings', () => { test.describe('Keybindings', () => {
test('Convert to group node, no selection', async ({ comfyPage }) => { 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') 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 }) => { test('Convert to group node, selected 1 node', async ({ comfyPage }) => {
expect(await comfyPage.getVisibleToastCount()).toBe(0) await expect.poll(() => comfyPage.toast.getVisibleToastCount()).toBe(0)
await comfyPage.clickTextEncodeNode1() await comfyPage.canvas.click({
position: DefaultGraphPositions.textEncodeNode1
})
await comfyPage.nextFrame()
await comfyPage.page.keyboard.press('Alt+g') 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 testComfySnapToGridGridSize
} from '../fixtures/ComfyPage' } from '../fixtures/ComfyPage'
import type { ComfyPage } 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' import type { NodeReference } from '../fixtures/utils/litegraphUtils'
test.beforeEach(async ({ comfyPage }) => { 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.describe('Item Interaction', { tag: ['@screenshot', '@node'] }, () => {
test('Can select/delete all items', async ({ comfyPage }) => { 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 comfyPage.canvas.press('Control+a')
await expect(comfyPage.canvas).toHaveScreenshot('selected-all.png') await expect(comfyPage.canvas).toHaveScreenshot('selected-all.png')
await comfyPage.canvas.press('Delete') 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 }) => { 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('Control+a')
await comfyPage.canvas.press('KeyP') await comfyPage.canvas.press('KeyP')
await comfyPage.nextFrame() await comfyPage.nextFrame()
@@ -51,11 +53,13 @@ test.describe('Node Interaction', () => {
test(`Can add multiple nodes to selection using ${modifier}+Click`, async ({ test(`Can add multiple nodes to selection using ${modifier}+Click`, async ({
comfyPage comfyPage
}) => { }) => {
const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode') const clipNodes =
await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
for (const node of clipNodes) { for (const node of clipNodes) {
await node.click('title', { modifiers: [modifier] }) await node.click('title', { modifiers: [modifier] })
} }
const selectedNodeCount = await comfyPage.getSelectedGraphNodesCount() const selectedNodeCount =
await comfyPage.nodeOps.getSelectedGraphNodesCount()
expect(selectedNodeCount).toBe(clipNodes.length) expect(selectedNodeCount).toBe(clipNodes.length)
}) })
}) })
@@ -65,9 +69,15 @@ test.describe('Node Interaction', () => {
{ tag: '@screenshot' }, { tag: '@screenshot' },
async ({ comfyPage }) => { async ({ comfyPage }) => {
await expect(comfyPage.canvas).toHaveScreenshot('default.png') 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 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') await expect(comfyPage.canvas).toHaveScreenshot('selected-node2.png')
} }
) )
@@ -80,7 +90,7 @@ test.describe('Node Interaction', () => {
const clipNode2Pos = await clipNodes[1].getPosition() const clipNode2Pos = await clipNodes[1].getPosition()
const offset = 64 const offset = 64
await comfyPage.page.keyboard.down('Meta') await comfyPage.page.keyboard.down('Meta')
await comfyPage.dragAndDrop( await comfyPage.canvasOps.dragAndDrop(
{ {
x: Math.min(clipNode1Pos.x, clipNode2Pos.x) - offset, x: Math.min(clipNode1Pos.x, clipNode2Pos.x) - offset,
y: Math.min(clipNode1Pos.y, clipNode2Pos.y) - 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 }) => { 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) await dragSelectNodes(comfyPage, clipNodes)
expect(await comfyPage.getSelectedGraphNodesCount()).toBe( expect(await comfyPage.nodeOps.getSelectedGraphNodesCount()).toBe(
clipNodes.length 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 ({ test('Can move selected nodes using the Comfy.Canvas.MoveSelectedNodes.{Up|Down|Left|Right} commands', async ({
comfyPage comfyPage
}) => { }) => {
const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode') const clipNodes =
await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
const getPositions = () => const getPositions = () =>
Promise.all(clipNodes.map((node) => node.getPosition())) Promise.all(clipNodes.map((node) => node.getPosition()))
const testDirection = async ({ const testDirection = async ({
@@ -116,7 +128,7 @@ test.describe('Node Interaction', () => {
}) => { }) => {
const originalPositions = await getPositions() const originalPositions = await getPositions()
await dragSelectNodes(comfyPage, clipNodes) await dragSelectNodes(comfyPage, clipNodes)
await comfyPage.executeCommand( await comfyPage.command.executeCommand(
`Comfy.Canvas.MoveSelectedNodes.${direction}` `Comfy.Canvas.MoveSelectedNodes.${direction}`
) )
await comfyPage.canvas.press(`Control+Arrow${direction}`) await comfyPage.canvas.press(`Control+Arrow${direction}`)
@@ -155,14 +167,20 @@ test.describe('Node Interaction', () => {
}) })
test('Can drag node', { tag: '@screenshot' }, async ({ comfyPage }) => { test('Can drag node', { tag: '@screenshot' }, async ({ comfyPage }) => {
await comfyPage.dragNode2() await comfyPage.nodeOps.dragTextEncodeNode2()
await expect(comfyPage.canvas).toHaveScreenshot('dragged-node1.png') await expect(comfyPage.canvas).toHaveScreenshot('dragged-node1.png')
}) })
test.describe('Edge Interaction', { tag: '@screenshot' }, () => { test.describe('Edge Interaction', { tag: '@screenshot' }, () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.LinkRelease.Action', 'no action') await comfyPage.settings.setSetting(
await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'no action') 'Comfy.LinkRelease.Action',
'no action'
)
await comfyPage.settings.setSetting(
'Comfy.LinkRelease.ActionShift',
'no action'
)
}) })
// Test both directions of edge connection. // Test both directions of edge connection.
@@ -170,11 +188,11 @@ test.describe('Node Interaction', () => {
test(`Can disconnect/connect edge ${reverse ? 'reverse' : 'normal'}`, async ({ test(`Can disconnect/connect edge ${reverse ? 'reverse' : 'normal'}`, async ({
comfyPage comfyPage
}) => { }) => {
await comfyPage.disconnectEdge() await comfyPage.canvasOps.disconnectEdge()
await expect(comfyPage.canvas).toHaveScreenshot('disconnected-edge.png') 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. // Move mouse to empty area to avoid slot highlight.
await comfyPage.moveMouseToEmptyArea() await comfyPage.canvasOps.moveMouseToEmptyArea()
// Litegraph renders edge with a slight offset. // Litegraph renders edge with a slight offset.
await expect(comfyPage.canvas).toHaveScreenshot('default.png', { await expect(comfyPage.canvas).toHaveScreenshot('default.png', {
maxDiffPixels: 50 maxDiffPixels: 50
@@ -183,14 +201,14 @@ test.describe('Node Interaction', () => {
}) })
test('Can move link', async ({ comfyPage }) => { test('Can move link', async ({ comfyPage }) => {
await comfyPage.dragAndDrop( await comfyPage.canvasOps.dragAndDrop(
comfyPage.clipTextEncodeNode1InputSlot, DefaultGraphPositions.clipTextEncodeNode1InputSlot,
comfyPage.emptySpace DefaultGraphPositions.emptySpace
) )
await expect(comfyPage.canvas).toHaveScreenshot('disconnected-edge.png') await expect(comfyPage.canvas).toHaveScreenshot('disconnected-edge.png')
await comfyPage.dragAndDrop( await comfyPage.canvasOps.dragAndDrop(
comfyPage.clipTextEncodeNode2InputSlot, DefaultGraphPositions.clipTextEncodeNode2InputSlot,
comfyPage.clipTextEncodeNode1InputSlot DefaultGraphPositions.clipTextEncodeNode1InputSlot
) )
await expect(comfyPage.canvas).toHaveScreenshot('moved-link.png') 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 ({ test.skip('Can copy link by shift-drag existing link', async ({
comfyPage comfyPage
}) => { }) => {
await comfyPage.dragAndDrop( await comfyPage.canvasOps.dragAndDrop(
comfyPage.clipTextEncodeNode1InputSlot, DefaultGraphPositions.clipTextEncodeNode1InputSlot,
comfyPage.emptySpace DefaultGraphPositions.emptySpace
) )
await expect(comfyPage.canvas).toHaveScreenshot('disconnected-edge.png') await expect(comfyPage.canvas).toHaveScreenshot('disconnected-edge.png')
await comfyPage.page.keyboard.down('Shift') await comfyPage.page.keyboard.down('Shift')
await comfyPage.dragAndDrop( await comfyPage.canvasOps.dragAndDrop(
comfyPage.clipTextEncodeNode2InputLinkPath, DefaultGraphPositions.clipTextEncodeNode2InputLinkPath,
comfyPage.clipTextEncodeNode1InputSlot DefaultGraphPositions.clipTextEncodeNode1InputSlot
) )
await comfyPage.page.keyboard.up('Shift') await comfyPage.page.keyboard.up('Shift')
await expect(comfyPage.canvas).toHaveScreenshot('copied-link.png') await expect(comfyPage.canvas).toHaveScreenshot('copied-link.png')
@@ -217,11 +235,11 @@ test.describe('Node Interaction', () => {
comfyPage, comfyPage,
comfyMouse comfyMouse
}) => { }) => {
await comfyPage.setSetting('Comfy.Node.AutoSnapLinkToSlot', true) await comfyPage.settings.setSetting('Comfy.Node.AutoSnapLinkToSlot', true)
await comfyPage.setSetting('Comfy.Node.SnapHighlightsNode', true) await comfyPage.settings.setSetting('Comfy.Node.SnapHighlightsNode', true)
await comfyMouse.move(comfyPage.clipTextEncodeNode1InputSlot) await comfyMouse.move(DefaultGraphPositions.clipTextEncodeNode1InputSlot)
await comfyMouse.drag(comfyPage.clipTextEncodeNode2InputSlot) await comfyMouse.drag(DefaultGraphPositions.clipTextEncodeNode2InputSlot)
await expect(comfyPage.canvas).toHaveScreenshot('snapped-highlighted.png') await expect(comfyPage.canvas).toHaveScreenshot('snapped-highlighted.png')
}) })
}) })
@@ -230,7 +248,7 @@ test.describe('Node Interaction', () => {
'Can adjust widget value', 'Can adjust widget value',
{ tag: '@screenshot' }, { tag: '@screenshot' },
async ({ comfyPage }) => { async ({ comfyPage }) => {
await comfyPage.adjustWidgetValue() await comfyPage.nodeOps.adjustEmptyLatentWidth()
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
'adjusted-widget-value.png' 'adjusted-widget-value.png'
) )
@@ -238,7 +256,7 @@ test.describe('Node Interaction', () => {
) )
test('Link snap to slot', { tag: '@screenshot' }, async ({ comfyPage }) => { 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') await expect(comfyPage.canvas).toHaveScreenshot('snap_to_slot.png')
const outputSlotPos = { const outputSlotPos = {
@@ -249,7 +267,7 @@ test.describe('Node Interaction', () => {
x: 748, x: 748,
y: 77 y: 77
} }
await comfyPage.dragAndDrop(outputSlotPos, samplerNodeCenterPos) await comfyPage.canvasOps.dragAndDrop(outputSlotPos, samplerNodeCenterPos)
await expect(comfyPage.canvas).toHaveScreenshot('snap_to_slot_linked.png') 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', 'Can batch move links by drag with shift',
{ tag: '@screenshot' }, { tag: '@screenshot' },
async ({ comfyPage }) => { 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') await expect(comfyPage.canvas).toHaveScreenshot('batch_move_links.png')
const outputSlot1Pos = { const outputSlot1Pos = {
@@ -271,7 +289,7 @@ test.describe('Node Interaction', () => {
} }
await comfyPage.page.keyboard.down('Shift') await comfyPage.page.keyboard.down('Shift')
await comfyPage.dragAndDrop(outputSlot1Pos, outputSlot2Pos) await comfyPage.canvasOps.dragAndDrop(outputSlot1Pos, outputSlot2Pos)
await comfyPage.page.keyboard.up('Shift') await comfyPage.page.keyboard.up('Shift')
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
@@ -304,12 +322,18 @@ test.describe('Node Interaction', () => {
{ tag: '@screenshot' }, { tag: '@screenshot' },
async ({ comfyPage }) => { async ({ comfyPage }) => {
await expect(comfyPage.canvas).toHaveScreenshot('default.png') 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( await expect(comfyPage.canvas).toHaveScreenshot(
'text-encode-toggled-off.png' 'text-encode-toggled-off.png'
) )
await comfyPage.delay(1000) await comfyPage.delay(1000)
await comfyPage.clickTextEncodeNodeToggler() await comfyPage.canvas.click({
position: DefaultGraphPositions.textEncodeNodeToggler
})
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
'text-encode-toggled-back-open.png' 'text-encode-toggled-back-open.png'
) )
@@ -345,7 +369,7 @@ test.describe('Node Interaction', () => {
x: 167, x: 167,
y: 143 y: 143
} }
await comfyPage.loadWorkflow('nodes/single_save_image_node') await comfyPage.workflow.loadWorkflow('nodes/single_save_image_node')
await comfyPage.canvas.click({ await comfyPage.canvas.click({
position: textWidgetPos position: textWidgetPos
}) })
@@ -365,7 +389,7 @@ test.describe('Node Interaction', () => {
'Can double click node title to edit', 'Can double click node title to edit',
{ tag: '@screenshot' }, { tag: '@screenshot' },
async ({ comfyPage }) => { async ({ comfyPage }) => {
await comfyPage.loadWorkflow('nodes/single_ksampler') await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
await comfyPage.canvas.dblclick({ await comfyPage.canvas.dblclick({
position: { position: {
x: 50, x: 50,
@@ -382,7 +406,7 @@ test.describe('Node Interaction', () => {
test('Double click node body does not trigger edit', async ({ test('Double click node body does not trigger edit', async ({
comfyPage comfyPage
}) => { }) => {
await comfyPage.loadWorkflow('nodes/single_ksampler') await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
await comfyPage.canvas.dblclick({ await comfyPage.canvas.dblclick({
position: { position: {
x: 50, x: 50,
@@ -397,8 +421,11 @@ test.describe('Node Interaction', () => {
'Can group selected nodes', 'Can group selected nodes',
{ tag: '@screenshot' }, { tag: '@screenshot' },
async ({ comfyPage }) => { async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.GroupSelectedNodes.Padding', 10) await comfyPage.settings.setSetting(
await comfyPage.select2Nodes() 'Comfy.GroupSelectedNodes.Padding',
10
)
await comfyPage.nodeOps.selectNodes(['CLIP Text Encode (Prompt)'])
await comfyPage.page.keyboard.down('Control') await comfyPage.page.keyboard.down('Control')
await comfyPage.page.keyboard.press('KeyG') await comfyPage.page.keyboard.press('KeyG')
await comfyPage.page.keyboard.up('Control') await comfyPage.page.keyboard.up('Control')
@@ -416,10 +443,10 @@ test.describe('Node Interaction', () => {
'Can fit group to contents', 'Can fit group to contents',
{ tag: '@screenshot' }, { tag: '@screenshot' },
async ({ comfyPage }) => { async ({ comfyPage }) => {
await comfyPage.loadWorkflow('groups/oversized_group') await comfyPage.workflow.loadWorkflow('groups/oversized_group')
await comfyPage.ctrlA() await comfyPage.keyboard.selectAll()
await comfyPage.nextFrame() await comfyPage.nextFrame()
await comfyPage.executeCommand('Comfy.Graph.FitGroupToContents') await comfyPage.command.executeCommand('Comfy.Graph.FitGroupToContents')
await comfyPage.nextFrame() await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
'group-fit-to-contents.png' 'group-fit-to-contents.png'
@@ -428,11 +455,15 @@ test.describe('Node Interaction', () => {
) )
test('Can pin/unpin nodes', { tag: '@screenshot' }, async ({ comfyPage }) => { test('Can pin/unpin nodes', { tag: '@screenshot' }, async ({ comfyPage }) => {
await comfyPage.select2Nodes() await comfyPage.nodeOps.selectNodes(['CLIP Text Encode (Prompt)'])
await comfyPage.executeCommand('Comfy.Canvas.ToggleSelectedNodes.Pin') await comfyPage.command.executeCommand(
'Comfy.Canvas.ToggleSelectedNodes.Pin'
)
await comfyPage.nextFrame() await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('nodes-pinned.png') 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 comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('nodes-unpinned.png') await expect(comfyPage.canvas).toHaveScreenshot('nodes-unpinned.png')
}) })
@@ -441,7 +472,7 @@ test.describe('Node Interaction', () => {
'Can bypass/unbypass nodes with keyboard shortcut', 'Can bypass/unbypass nodes with keyboard shortcut',
{ tag: '@screenshot' }, { tag: '@screenshot' },
async ({ comfyPage }) => { async ({ comfyPage }) => {
await comfyPage.select2Nodes() await comfyPage.nodeOps.selectNodes(['CLIP Text Encode (Prompt)'])
await comfyPage.canvas.press('Control+b') await comfyPage.canvas.press('Control+b')
await comfyPage.nextFrame() await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('nodes-bypassed.png') await expect(comfyPage.canvas).toHaveScreenshot('nodes-bypassed.png')
@@ -454,7 +485,7 @@ test.describe('Node Interaction', () => {
test.describe('Group Interaction', { tag: '@screenshot' }, () => { test.describe('Group Interaction', { tag: '@screenshot' }, () => {
test('Can double click group title to edit', async ({ comfyPage }) => { 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({ await comfyPage.canvas.dblclick({
position: { position: {
x: 50, x: 50,
@@ -470,16 +501,16 @@ test.describe('Group Interaction', { tag: '@screenshot' }, () => {
test.describe('Canvas Interaction', { tag: '@screenshot' }, () => { test.describe('Canvas Interaction', { tag: '@screenshot' }, () => {
test('Can zoom in/out', async ({ comfyPage }) => { 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 expect(comfyPage.canvas).toHaveScreenshot('zoomed-in.png')
await comfyPage.zoom(200) await comfyPage.canvasOps.zoom(200)
await expect(comfyPage.canvas).toHaveScreenshot('zoomed-out.png') await expect(comfyPage.canvas).toHaveScreenshot('zoomed-out.png')
}) })
test('Can zoom very far out', async ({ comfyPage }) => { 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 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') 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('Control')
await comfyPage.page.keyboard.down('Shift') 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 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 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( await expect(comfyPage.canvas).toHaveScreenshot(
'zoomed-default-ctrl-shift.png' '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 ({ test('Can zoom in/out after decreasing canvas zoom speed setting', async ({
comfyPage comfyPage
}) => { }) => {
await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', 1.05) await comfyPage.settings.setSetting('Comfy.Graph.ZoomSpeed', 1.05)
await comfyPage.zoom(-100, 4) await comfyPage.canvasOps.zoom(-100, 4)
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
'zoomed-in-low-zoom-speed.png' 'zoomed-in-low-zoom-speed.png'
) )
await comfyPage.zoom(100, 8) await comfyPage.canvasOps.zoom(100, 8)
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
'zoomed-out-low-zoom-speed.png' '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 ({ test('Can zoom in/out after increasing canvas zoom speed', async ({
comfyPage comfyPage
}) => { }) => {
await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', 1.5) await comfyPage.settings.setSetting('Comfy.Graph.ZoomSpeed', 1.5)
await comfyPage.zoom(-100, 4) await comfyPage.canvasOps.zoom(-100, 4)
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
'zoomed-in-high-zoom-speed.png' 'zoomed-in-high-zoom-speed.png'
) )
await comfyPage.zoom(100, 8) await comfyPage.canvasOps.zoom(100, 8)
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
'zoomed-out-high-zoom-speed.png' '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 }) => { 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') 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 }) => { test('Can pan when dragging a link', async ({ comfyPage, comfyMouse }) => {
const posSlot1 = comfyPage.clipTextEncodeNode1InputSlot const posSlot1 = DefaultGraphPositions.clipTextEncodeNode1InputSlot
await comfyMouse.move(posSlot1) await comfyMouse.move(posSlot1)
const posEmpty = comfyPage.emptySpace const posEmpty = DefaultGraphPositions.emptySpace
await comfyMouse.drag(posEmpty) await comfyMouse.drag(posEmpty)
await expect(comfyPage.canvas).toHaveScreenshot('dragging-link1.png') 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 }) => { test('Can pan very far and back', async ({ comfyPage }) => {
// intentionally slice the edge of where the clip text encode dom widgets are // 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 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 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 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 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 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') await expect(comfyPage.canvas).toHaveScreenshot('panned-back-to-one.png')
}) })
test('@mobile Can pan with touch', async ({ comfyPage }) => { test('@mobile Can pan with touch', async ({ comfyPage }) => {
await comfyPage.closeMenu() 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') await expect(comfyPage.canvas).toHaveScreenshot('panned-touch.png')
}) })
}) })
@@ -652,41 +683,41 @@ test.describe('Widget Interaction', () => {
await expect(textBox).toHaveValue('') await expect(textBox).toHaveValue('')
await textBox.fill('Hello World') await textBox.fill('Hello World')
await expect(textBox).toHaveValue('Hello World') await expect(textBox).toHaveValue('Hello World')
await comfyPage.ctrlZ(null) await comfyPage.keyboard.undo(null)
await expect(textBox).toHaveValue('') await expect(textBox).toHaveValue('')
}) })
test('Undo attention edit', async ({ comfyPage }) => { 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 const textBox = comfyPage.widgetTextBox
await textBox.click() await textBox.click()
await textBox.fill('1girl') await textBox.fill('1girl')
await expect(textBox).toHaveValue('1girl') await expect(textBox).toHaveValue('1girl')
await textBox.selectText() await textBox.selectText()
await comfyPage.ctrlArrowUp(null) await comfyPage.keyboard.moveUp(null)
await expect(textBox).toHaveValue('(1girl:1.05)') await expect(textBox).toHaveValue('(1girl:1.05)')
await comfyPage.ctrlZ(null) await comfyPage.keyboard.undo(null)
await expect(textBox).toHaveValue('1girl') await expect(textBox).toHaveValue('1girl')
}) })
}) })
test.describe('Load workflow', { tag: '@screenshot' }, () => { test.describe('Load workflow', { tag: '@screenshot' }, () => {
test('Can load workflow with string node id', async ({ comfyPage }) => { 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') await expect(comfyPage.canvas).toHaveScreenshot('string_node_id.png')
}) })
test('Can load workflow with ("STRING",) input node', async ({ test('Can load workflow with ("STRING",) input node', async ({
comfyPage comfyPage
}) => { }) => {
await comfyPage.loadWorkflow('inputs/string_input') await comfyPage.workflow.loadWorkflow('inputs/string_input')
await expect(comfyPage.canvas).toHaveScreenshot('string_input.png') await expect(comfyPage.canvas).toHaveScreenshot('string_input.png')
}) })
test('Restore workflow on reload (switch workflow)', async ({ test('Restore workflow on reload (switch workflow)', async ({
comfyPage comfyPage
}) => { }) => {
await comfyPage.loadWorkflow('nodes/single_ksampler') await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler.png') await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler.png')
await comfyPage.setup({ clearStorage: false }) await comfyPage.setup({ clearStorage: false })
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler.png') 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 ({ test('Restore workflow on reload (modify workflow)', async ({
comfyPage comfyPage
}) => { }) => {
await comfyPage.loadWorkflow('nodes/single_ksampler') await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
const node = (await comfyPage.getFirstNodeRef())! const node = (await comfyPage.nodeOps.getFirstNodeRef())!
await node.click('collapse') await node.click('collapse')
await comfyPage.clickEmptySpace() await comfyPage.canvasOps.clickEmptySpace()
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
'single_ksampler_modified.png' '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}` `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}${extension}`
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
workflowA = generateUniqueFilename() workflowA = generateUniqueFilename()
await comfyPage.menu.topbar.saveWorkflow(workflowA) await comfyPage.menu.topbar.saveWorkflow(workflowA)
@@ -734,7 +765,7 @@ test.describe('Load workflow', { tag: '@screenshot' }, () => {
test('Restores topbar workflow tabs after reload', async ({ test('Restores topbar workflow tabs after reload', async ({
comfyPage comfyPage
}) => { }) => {
await comfyPage.setSetting( await comfyPage.settings.setSetting(
'Comfy.Workflow.WorkflowTabsPosition', 'Comfy.Workflow.WorkflowTabsPosition',
'Topbar' 'Topbar'
) )
@@ -747,7 +778,7 @@ test.describe('Load workflow', { tag: '@screenshot' }, () => {
}) })
test('Restores sidebar workflows after reload', async ({ comfyPage }) => { test('Restores sidebar workflows after reload', async ({ comfyPage }) => {
await comfyPage.setSetting( await comfyPage.settings.setSetting(
'Comfy.Workflow.WorkflowTabsPosition', 'Comfy.Workflow.WorkflowTabsPosition',
'Sidebar' 'Sidebar'
) )
@@ -770,34 +801,40 @@ test.describe('Load workflow', { tag: '@screenshot' }, () => {
}) })
test('Auto fit view after loading workflow', async ({ comfyPage }) => { test('Auto fit view after loading workflow', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.EnableWorkflowViewRestore', false) await comfyPage.settings.setSetting(
await comfyPage.loadWorkflow('nodes/single_ksampler') 'Comfy.EnableWorkflowViewRestore',
false
)
await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler_fit.png') await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler_fit.png')
}) })
}) })
test.describe('Load duplicate workflow', () => { test.describe('Load duplicate workflow', () => {
test.beforeEach(async ({ comfyPage }) => { 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 ({ test('A workflow can be loaded multiple times in a row', async ({
comfyPage comfyPage
}) => { }) => {
await comfyPage.loadWorkflow('nodes/single_ksampler') await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
await comfyPage.menu.workflowsTab.open() await comfyPage.menu.workflowsTab.open()
await comfyPage.executeCommand('Comfy.NewBlankWorkflow') await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
await comfyPage.loadWorkflow('nodes/single_ksampler') await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
expect(await comfyPage.getGraphNodesCount()).toBe(1) expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe(1)
}) })
}) })
test.describe('Viewport settings', () => { test.describe('Viewport settings', () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting('Comfy.Workflow.WorkflowTabsPosition', 'Topbar') await comfyPage.settings.setSetting(
'Comfy.Workflow.WorkflowTabsPosition',
'Topbar'
)
await comfyPage.setupWorkflowsDirectory({}) await comfyPage.workflow.setupWorkflowsDirectory({})
}) })
test('Keeps viewport settings when changing tabs', async ({ test('Keeps viewport settings when changing tabs', async ({
@@ -807,7 +844,7 @@ test.describe('Viewport settings', () => {
const changeTab = async (tab: Locator) => { const changeTab = async (tab: Locator) => {
await tab.click() await tab.click()
await comfyPage.nextFrame() await comfyPage.nextFrame()
await comfyMouse.move(comfyPage.emptySpace) await comfyMouse.move(DefaultGraphPositions.emptySpace)
// If tooltip is visible, wait for it to hide // If tooltip is visible, wait for it to hide
await expect( await expect(
@@ -816,11 +853,13 @@ test.describe('Viewport settings', () => {
} }
// Screenshot the canvas element // 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 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.menu.topbar.saveWorkflow('Workflow A')
await comfyPage.nextFrame() await comfyPage.nextFrame()
@@ -837,7 +876,7 @@ test.describe('Viewport settings', () => {
const tabB = comfyPage.menu.topbar.getWorkflowTab('Workflow B') const tabB = comfyPage.menu.topbar.getWorkflowTab('Workflow B')
await changeTab(tabB) await changeTab(tabB)
await comfyMouse.move(comfyPage.emptySpace) await comfyMouse.move(DefaultGraphPositions.emptySpace)
for (let i = 0; i < 4; i++) { for (let i = 0; i < 4; i++) {
await comfyMouse.wheel(0, 60) await comfyMouse.wheel(0, 60)
} }
@@ -865,13 +904,19 @@ test.describe('Viewport settings', () => {
test.describe('Canvas Navigation', { tag: '@screenshot' }, () => { test.describe('Canvas Navigation', { tag: '@screenshot' }, () => {
test.describe('Legacy Mode', () => { test.describe('Legacy Mode', () => {
test.beforeEach(async ({ comfyPage }) => { 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 ({ test('Left-click drag in empty area should pan canvas', async ({
comfyPage 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( await expect(comfyPage.canvas).toHaveScreenshot(
'legacy-left-drag-pan.png' '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 }) => { test('Left-click on node should not pan canvas', async ({ comfyPage }) => {
await comfyPage.clickTextEncodeNode1() await comfyPage.canvas.click({
const selectedCount = await comfyPage.getSelectedGraphNodesCount() position: DefaultGraphPositions.textEncodeNode1
})
await comfyPage.nextFrame()
const selectedCount = await comfyPage.nodeOps.getSelectedGraphNodesCount()
expect(selectedCount).toBe(1) expect(selectedCount).toBe(1)
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
'legacy-click-node-select.png' 'legacy-click-node-select.png'
@@ -915,18 +963,22 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => {
test.describe('Standard Mode', () => { test.describe('Standard Mode', () => {
test.beforeEach(async ({ comfyPage }) => { 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 ({ test('Left-click drag in empty area should select nodes', async ({
comfyPage comfyPage
}) => { }) => {
const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode') const clipNodes =
await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
const clipNode1Pos = await clipNodes[0].getPosition() const clipNode1Pos = await clipNodes[0].getPosition()
const clipNode2Pos = await clipNodes[1].getPosition() const clipNode2Pos = await clipNodes[1].getPosition()
const offset = 64 const offset = 64
await comfyPage.dragAndDrop( await comfyPage.canvasOps.dragAndDrop(
{ {
x: Math.min(clipNode1Pos.x, clipNode2Pos.x) - offset, x: Math.min(clipNode1Pos.x, clipNode2Pos.x) - offset,
y: Math.min(clipNode1Pos.y, clipNode2Pos.y) - 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) expect(selectedCount).toBe(clipNodes.length)
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
'standard-left-drag-select.png' '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 ({ test('Left-click on node should select node (not start selection box)', async ({
comfyPage comfyPage
}) => { }) => {
await comfyPage.clickTextEncodeNode1() await comfyPage.canvas.click({
const selectedCount = await comfyPage.getSelectedGraphNodesCount() position: DefaultGraphPositions.textEncodeNode1
})
await comfyPage.nextFrame()
const selectedCount = await comfyPage.nodeOps.getSelectedGraphNodesCount()
expect(selectedCount).toBe(1) expect(selectedCount).toBe(1)
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
'standard-click-node-select.png' 'standard-click-node-select.png'
@@ -991,7 +1046,10 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => {
await comfyPage.nextFrame() await comfyPage.nextFrame()
await comfyPage.page.keyboard.down('Space') 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 comfyPage.page.keyboard.up('Space')
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
'standard-space-drag-pan.png' 'standard-space-drag-pan.png'
@@ -1001,11 +1059,12 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => {
test('Space key overrides default left-click behavior', async ({ test('Space key overrides default left-click behavior', async ({
comfyPage comfyPage
}) => { }) => {
const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode') const clipNodes =
await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
const clipNode1Pos = await clipNodes[0].getPosition() const clipNode1Pos = await clipNodes[0].getPosition()
const offset = 64 const offset = 64
await comfyPage.dragAndDrop( await comfyPage.canvasOps.dragAndDrop(
{ {
x: clipNode1Pos.x - offset, x: clipNode1Pos.x - offset,
y: clipNode1Pos.y - offset y: clipNode1Pos.y - offset
@@ -1017,16 +1076,16 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => {
) )
const selectedCountAfterDrag = const selectedCountAfterDrag =
await comfyPage.getSelectedGraphNodesCount() await comfyPage.nodeOps.getSelectedGraphNodesCount()
expect(selectedCountAfterDrag).toBeGreaterThan(0) expect(selectedCountAfterDrag).toBeGreaterThan(0)
await comfyPage.clickEmptySpace() await comfyPage.canvasOps.clickEmptySpace()
const selectedCountAfterClear = const selectedCountAfterClear =
await comfyPage.getSelectedGraphNodesCount() await comfyPage.nodeOps.getSelectedGraphNodesCount()
expect(selectedCountAfterClear).toBe(0) expect(selectedCountAfterClear).toBe(0)
await comfyPage.page.keyboard.down('Space') await comfyPage.page.keyboard.down('Space')
await comfyPage.dragAndDrop( await comfyPage.canvasOps.dragAndDrop(
{ {
x: clipNode1Pos.x - offset, x: clipNode1Pos.x - offset,
y: clipNode1Pos.y - offset y: clipNode1Pos.y - offset
@@ -1039,7 +1098,7 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => {
await comfyPage.page.keyboard.up('Space') await comfyPage.page.keyboard.up('Space')
const selectedCountAfterSpaceDrag = const selectedCountAfterSpaceDrag =
await comfyPage.getSelectedGraphNodesCount() await comfyPage.nodeOps.getSelectedGraphNodesCount()
expect(selectedCountAfterSpaceDrag).toBe(0) expect(selectedCountAfterSpaceDrag).toBe(0)
}) })
}) })
@@ -1047,7 +1106,10 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => {
test('Shift + mouse wheel should pan canvas horizontally', async ({ test('Shift + mouse wheel should pan canvas horizontally', async ({
comfyPage comfyPage
}) => { }) => {
await comfyPage.setSetting('Comfy.Canvas.MouseWheelScroll', 'panning') await comfyPage.settings.setSetting(
'Comfy.Canvas.MouseWheelScroll',
'panning'
)
await comfyPage.page.click('canvas') await comfyPage.page.click('canvas')
await comfyPage.nextFrame() await comfyPage.nextFrame()
@@ -1085,11 +1147,17 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => {
test('Multiple modifier keys work correctly in legacy mode', async ({ test('Multiple modifier keys work correctly in legacy mode', async ({
comfyPage 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('Alt')
await comfyPage.page.keyboard.down('Shift') 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('Shift')
await comfyPage.page.keyboard.up('Alt') 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.move(50, 50)
await comfyPage.page.mouse.down() await comfyPage.page.mouse.down()
expect(await getCursorStyle()).toBe('grabbing') expect(await getCursorStyle()).toBe('grabbing')

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,14 +4,14 @@ import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Menu', { tag: '@ui' }, () => { test.describe('Menu', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
}) })
test('Can register sidebar tab', async ({ comfyPage }) => { test('Can register sidebar tab', async ({ comfyPage }) => {
const initialChildrenCount = await comfyPage.menu.buttons.count() const initialChildrenCount = await comfyPage.menu.buttons.count()
await comfyPage.page.evaluate(async () => { await comfyPage.page.evaluate(async () => {
window['app'].extensionManager.registerSidebarTab({ window.app!.extensionManager.registerSidebarTab({
id: 'search', id: 'search',
icon: 'pi pi-search', icon: 'pi pi-search',
title: 'search', title: 'search',
@@ -30,11 +30,11 @@ test.describe('Menu', { tag: '@ui' }, () => {
test.describe('Workflows topbar tabs', () => { test.describe('Workflows topbar tabs', () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting( await comfyPage.settings.setSetting(
'Comfy.Workflow.WorkflowTabsPosition', 'Comfy.Workflow.WorkflowTabsPosition',
'Topbar' 'Topbar'
) )
await comfyPage.setupWorkflowsDirectory({}) await comfyPage.workflow.setupWorkflowsDirectory({})
}) })
test('Can show opened workflows', async ({ comfyPage }) => { test('Can show opened workflows', async ({ comfyPage }) => {
@@ -101,8 +101,8 @@ test.describe('Menu', { tag: '@ui' }, () => {
const checkmark = bottomPanelMenuItem.locator('.pi-check') const checkmark = bottomPanelMenuItem.locator('.pi-check')
// Check initial state of bottom panel (it's initially hidden) // Check initial state of bottom panel (it's initially hidden)
const bottomPanel = comfyPage.page.locator('.bottom-panel') const { bottomPanel } = comfyPage
await expect(bottomPanel).not.toBeVisible() await expect(bottomPanel.root).not.toBeVisible()
// Checkmark should be invisible initially (panel is hidden) // Checkmark should be invisible initially (panel is hidden)
await expect(checkmark).toHaveClass(/invisible/) await expect(checkmark).toHaveClass(/invisible/)
@@ -113,7 +113,8 @@ test.describe('Menu', { tag: '@ui' }, () => {
await expect(menu).toBeVisible() await expect(menu).toBeVisible()
await expect(viewSubmenu).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) // Checkmark should now be visible (panel is shown)
await expect(checkmark).not.toHaveClass(/invisible/) await expect(checkmark).not.toHaveClass(/invisible/)
@@ -126,7 +127,7 @@ test.describe('Menu', { tag: '@ui' }, () => {
await expect(viewSubmenu).toBeVisible() await expect(viewSubmenu).toBeVisible()
// Verify bottom panel is hidden again // 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) // Checkmark should be invisible again (panel is hidden)
await expect(checkmark).toHaveClass(/invisible/) await expect(checkmark).toHaveClass(/invisible/)
@@ -153,7 +154,7 @@ test.describe('Menu', { tag: '@ui' }, () => {
test('Can catch error when executing command', async ({ comfyPage }) => { test('Can catch error when executing command', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => { await comfyPage.page.evaluate(() => {
window['app'].registerExtension({ window.app!.registerExtension({
name: 'TestExtension1', name: 'TestExtension1',
commands: [ commands: [
{ {
@@ -173,7 +174,7 @@ test.describe('Menu', { tag: '@ui' }, () => {
}) })
}) })
await comfyPage.menu.topbar.triggerTopbarCommand(['ext', 'foo-command']) 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 ({ 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') await comfyPage.attachScreenshot('theme-menu-light-active')
// Verify ColorPalette setting is set to "light" // 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 // Close menu to see theme change
await topbar.closeTopbarMenu() await topbar.closeTopbarMenu()
@@ -236,7 +239,9 @@ test.describe('Menu', { tag: '@ui' }, () => {
await comfyPage.attachScreenshot('theme-menu-dark-active') await comfyPage.attachScreenshot('theme-menu-dark-active')
// Verify ColorPalette setting is set to "dark" // 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 // Close menu
await topbar.closeTopbarMenu() await topbar.closeTopbarMenu()
@@ -249,16 +254,20 @@ test.describe('Menu', { tag: '@ui' }, () => {
test(`Can migrate deprecated menu positions (${position})`, async ({ test(`Can migrate deprecated menu positions (${position})`, async ({
comfyPage comfyPage
}) => { }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', position) await comfyPage.settings.setSetting('Comfy.UseNewMenu', position)
expect(await comfyPage.getSetting('Comfy.UseNewMenu')).toBe('Top') expect(await comfyPage.settings.getSetting('Comfy.UseNewMenu')).toBe(
'Top'
)
}) })
test(`Can migrate deprecated menu positions on initial load (${position})`, async ({ test(`Can migrate deprecated menu positions on initial load (${position})`, async ({
comfyPage comfyPage
}) => { }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', position) await comfyPage.settings.setSetting('Comfy.UseNewMenu', position)
await comfyPage.setup() 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 { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage' import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { TestIds } from '../fixtures/selectors'
test.describe('Minimap', { tag: '@canvas' }, () => { test.describe('Minimap', { tag: '@canvas' }, () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting('Comfy.Minimap.Visible', true) await comfyPage.settings.setSetting('Comfy.Minimap.Visible', true)
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true) await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', true)
await comfyPage.loadWorkflow('default') await comfyPage.workflow.loadWorkflow('default')
await comfyPage.page.waitForFunction( await comfyPage.page.waitForFunction(() => window.app && window.app.canvas)
() => window['app'] && window['app'].canvas
)
}) })
test('Validate minimap is visible by default', async ({ comfyPage }) => { 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 }) => { 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() await expect(toggleButton).toBeVisible()
@@ -45,7 +46,9 @@ test.describe('Minimap', { tag: '@canvas' }, () => {
test('Validate minimap can be toggled off and on', async ({ comfyPage }) => { test('Validate minimap can be toggled off and on', async ({ comfyPage }) => {
const minimapContainer = comfyPage.page.locator('.litegraph-minimap') 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() await expect(minimapContainer).toBeVisible()

View File

@@ -1,22 +1,24 @@
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { expect } from '@playwright/test' import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { TestIds } from '../fixtures/selectors'
test.describe( test.describe(
'Mobile Baseline Snapshots', 'Mobile Baseline Snapshots',
{ tag: ['@mobile', '@screenshot'] }, { tag: ['@mobile', '@screenshot'] },
() => { () => {
test('@mobile empty canvas', async ({ comfyPage }) => { test('@mobile empty canvas', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.ConfirmClear', false) await comfyPage.settings.setSetting('Comfy.ConfirmClear', false)
await comfyPage.executeCommand('Comfy.ClearWorkflow') await comfyPage.command.executeCommand('Comfy.ClearWorkflow')
await expect(async () => { await expect(async () => {
expect(await comfyPage.getGraphNodesCount()).toBe(0) expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe(0)
}).toPass({ timeout: 256 }) }).toPass({ timeout: 5000 })
await comfyPage.nextFrame() await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('mobile-empty-canvas.png') await expect(comfyPage.canvas).toHaveScreenshot('mobile-empty-canvas.png')
}) })
test('@mobile default workflow', async ({ comfyPage }) => { test('@mobile default workflow', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('default') await comfyPage.workflow.loadWorkflow('default')
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
'mobile-default-workflow.png' 'mobile-default-workflow.png'
) )
@@ -30,7 +32,9 @@ test.describe(
'mobile-settings-dialog.png', 'mobile-settings-dialog.png',
{ {
mask: [ 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' import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ 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.describe('Node Badge', { tag: ['@screenshot', '@smoke', '@node'] }, () => {
test('Can add badge', async ({ comfyPage }) => { test('Can add badge', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => { await comfyPage.page.evaluate(() => {
const LGraphBadge = window['LGraphBadge'] const LGraphBadge = window.LGraphBadge!
const app = window['app'] as ComfyApp const app = window.app as ComfyApp
const graph = app.graph const graph = app.graph
const nodes = graph.nodes const nodes = graph.nodes
@@ -28,8 +28,8 @@ test.describe('Node Badge', { tag: ['@screenshot', '@smoke', '@node'] }, () => {
test('Can add multiple badges', async ({ comfyPage }) => { test('Can add multiple badges', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => { await comfyPage.page.evaluate(() => {
const LGraphBadge = window['LGraphBadge'] const LGraphBadge = window.LGraphBadge!
const app = window['app'] as ComfyApp const app = window.app as ComfyApp
const graph = app.graph const graph = app.graph
const nodes = graph.nodes const nodes = graph.nodes
@@ -48,8 +48,8 @@ test.describe('Node Badge', { tag: ['@screenshot', '@smoke', '@node'] }, () => {
test('Can add badge left-side', async ({ comfyPage }) => { test('Can add badge left-side', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => { await comfyPage.page.evaluate(() => {
const LGraphBadge = window['LGraphBadge'] const LGraphBadge = window.LGraphBadge!
const app = window['app'] as ComfyApp const app = window.app as ComfyApp
const graph = app.graph const graph = app.graph
const nodes = graph.nodes const nodes = graph.nodes
@@ -73,11 +73,17 @@ test.describe(
Object.values(NodeBadgeMode).forEach(async (mode) => { Object.values(NodeBadgeMode).forEach(async (mode) => {
test(`Shows node badges (${mode})`, async ({ comfyPage }) => { test(`Shows node badges (${mode})`, async ({ comfyPage }) => {
// Execution error workflow has both custom node and core node. // Execution error workflow has both custom node and core node.
await comfyPage.loadWorkflow('nodes/execution_error') await comfyPage.workflow.loadWorkflow('nodes/execution_error')
await comfyPage.setSetting('Comfy.NodeBadge.NodeSourceBadgeMode', mode) await comfyPage.settings.setSetting(
await comfyPage.setSetting('Comfy.NodeBadge.NodeIdBadgeMode', mode) 'Comfy.NodeBadge.NodeSourceBadgeMode',
mode
)
await comfyPage.settings.setSetting(
'Comfy.NodeBadge.NodeIdBadgeMode',
mode
)
await comfyPage.nextFrame() await comfyPage.nextFrame()
await comfyPage.resetView() await comfyPage.canvasOps.resetView()
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
`node-badge-${mode}.png` `node-badge-${mode}.png`
) )
@@ -93,14 +99,14 @@ test.describe(
test('Can show node badge with unknown color palette', async ({ test('Can show node badge with unknown color palette', async ({
comfyPage comfyPage
}) => { }) => {
await comfyPage.setSetting( await comfyPage.settings.setSetting(
'Comfy.NodeBadge.NodeIdBadgeMode', 'Comfy.NodeBadge.NodeIdBadgeMode',
NodeBadgeMode.ShowAll NodeBadgeMode.ShowAll
) )
await comfyPage.setSetting('Comfy.ColorPalette', 'unknown') await comfyPage.settings.setSetting('Comfy.ColorPalette', 'unknown')
await comfyPage.nextFrame() await comfyPage.nextFrame()
// Click empty space to trigger canvas re-render. // Click empty space to trigger canvas re-render.
await comfyPage.clickEmptySpace() await comfyPage.canvasOps.clickEmptySpace()
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
'node-badge-unknown-color-palette.png' 'node-badge-unknown-color-palette.png'
) )
@@ -109,14 +115,14 @@ test.describe(
test('Can show node badge with light color palette', async ({ test('Can show node badge with light color palette', async ({
comfyPage comfyPage
}) => { }) => {
await comfyPage.setSetting( await comfyPage.settings.setSetting(
'Comfy.NodeBadge.NodeIdBadgeMode', 'Comfy.NodeBadge.NodeIdBadgeMode',
NodeBadgeMode.ShowAll NodeBadgeMode.ShowAll
) )
await comfyPage.setSetting('Comfy.ColorPalette', 'light') await comfyPage.settings.setSetting('Comfy.ColorPalette', 'light')
await comfyPage.nextFrame() await comfyPage.nextFrame()
// Click empty space to trigger canvas re-render. // Click empty space to trigger canvas re-render.
await comfyPage.clickEmptySpace() await comfyPage.canvasOps.clickEmptySpace()
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
'node-badge-light-color-palette.png' 'node-badge-light-color-palette.png'
) )

View File

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

View File

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

View File

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

View File

@@ -3,12 +3,12 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage' import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ 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.describe('Note Node', { tag: '@node' }, () => {
test('Can load node nodes', { tag: '@screenshot' }, async ({ comfyPage }) => { 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') 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' import type { NodeReference } from '../fixtures/utils/litegraphUtils'
test.beforeEach(async ({ comfyPage }) => { 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.describe('Primitive Node', { tag: ['@screenshot', '@node'] }, () => {
test('Can load with correct size', async ({ comfyPage }) => { 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') await expect(comfyPage.canvas).toHaveScreenshot('primitive_node.png')
}) })
// When link is dropped on widget, it should automatically convert the widget // When link is dropped on widget, it should automatically convert the widget
// to input. // to input.
test('Can connect to widget', async ({ comfyPage }) => { test('Can connect to widget', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('primitive/primitive_node_unconnected') await comfyPage.workflow.loadWorkflow(
const primitiveNode: NodeReference = await comfyPage.getNodeRefById(1) 'primitive/primitive_node_unconnected'
const ksamplerNode: NodeReference = await comfyPage.getNodeRefById(2) )
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 // Connect the output of the primitive node to the input of first widget of the ksampler node
await primitiveNode.connectWidget(0, ksamplerNode, 0) await primitiveNode.connectWidget(0, ksamplerNode, 0)
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
@@ -27,11 +31,13 @@ test.describe('Primitive Node', { tag: ['@screenshot', '@node'] }, () => {
}) })
test('Can connect to dom widget', async ({ comfyPage }) => { test('Can connect to dom widget', async ({ comfyPage }) => {
await comfyPage.loadWorkflow( await comfyPage.workflow.loadWorkflow(
'primitive/primitive_node_unconnected_dom_widget' 'primitive/primitive_node_unconnected_dom_widget'
) )
const primitiveNode: NodeReference = await comfyPage.getNodeRefById(1) const primitiveNode: NodeReference =
const clipEncoderNode: NodeReference = await comfyPage.getNodeRefById(2) await comfyPage.nodeOps.getNodeRefById(1)
const clipEncoderNode: NodeReference =
await comfyPage.nodeOps.getNodeRefById(2)
await primitiveNode.connectWidget(0, clipEncoderNode, 0) await primitiveNode.connectWidget(0, clipEncoderNode, 0)
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
'primitive_node_connected_dom_widget.png' '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 }) => { test('Can connect to static primitive node', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('primitive/static_primitive_unconnected') await comfyPage.workflow.loadWorkflow(
const primitiveNode: NodeReference = await comfyPage.getNodeRefById(1) 'primitive/static_primitive_unconnected'
const ksamplerNode: NodeReference = await comfyPage.getNodeRefById(2) )
const primitiveNode: NodeReference =
await comfyPage.nodeOps.getNodeRefById(1)
const ksamplerNode: NodeReference =
await comfyPage.nodeOps.getNodeRefById(2)
await primitiveNode.connectWidget(0, ksamplerNode, 0) await primitiveNode.connectWidget(0, ksamplerNode, 0)
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
'static_primitive_connected.png' '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 ({ test('Report missing nodes when connect to missing node', async ({
comfyPage comfyPage
}) => { }) => {
await comfyPage.loadWorkflow( await comfyPage.workflow.loadWorkflow(
'primitive/primitive_node_connect_missing_node' 'primitive/primitive_node_connect_missing_node'
) )
// Wait for the element with the .comfy-missing-nodes selector to be visible // 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 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.panelTitle).toContainText('3 items selected')
await expect(propertiesPanel.root.getByText('KSampler')).toHaveCount(1) await expect(propertiesPanel.root.getByText('KSampler')).toHaveCount(1)

View File

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

View File

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

View File

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

View File

@@ -26,23 +26,23 @@ test.describe('Remote COMBO Widget', { tag: '@widget' }, () => {
nodeName: string nodeName: string
): Promise<string[] | undefined> => { ): Promise<string[] | undefined> => {
return await comfyPage.page.evaluate((name) => { return await comfyPage.page.evaluate((name) => {
const node = window['app'].graph.nodes.find((node) => node.title === name) const node = window.app!.graph!.nodes.find((node) => node.title === name)
return node.widgets[0].options.values return node!.widgets![0].options.values as string[] | undefined
}, nodeName) }, nodeName)
} }
const getWidgetValue = async (comfyPage: ComfyPage, nodeName: string) => { const getWidgetValue = async (comfyPage: ComfyPage, nodeName: string) => {
return await comfyPage.page.evaluate((name) => { return await comfyPage.page.evaluate((name) => {
const node = window['app'].graph.nodes.find((node) => node.title === name) const node = window.app!.graph!.nodes.find((node) => node.title === name)
return node.widgets[0].value return node!.widgets![0].value
}, nodeName) }, nodeName)
} }
const clickRefreshButton = (comfyPage: ComfyPage, nodeName: string) => { const clickRefreshButton = (comfyPage: ComfyPage, nodeName: string) => {
return comfyPage.page.evaluate((name) => { return comfyPage.page.evaluate((name) => {
const node = window['app'].graph.nodes.find((node) => node.title === name) const node = window.app!.graph!.nodes.find((node) => node.title === name)
const buttonWidget = node.widgets.find((w) => w.name === 'refresh') const buttonWidget = node!.widgets!.find((w) => w.name === 'refresh')
return buttonWidget?.callback() return buttonWidget?.callback?.(buttonWidget.value)
}, nodeName) }, nodeName)
} }
@@ -52,12 +52,12 @@ test.describe('Remote COMBO Widget', { tag: '@widget' }, () => {
} }
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
}) })
test.describe('Loading options', () => { test.describe('Loading options', () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.page.route( await comfyPage.page.route(
'**/api/models/checkpoints**', '**/api/models/checkpoints**',
async (route, request) => { async (route, request) => {
@@ -89,10 +89,10 @@ test.describe('Remote COMBO Widget', { tag: '@widget' }, () => {
comfyPage comfyPage
}) => { }) => {
const nodeName = 'Remote Widget Node' 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) => { 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) }, nodeName)
expect(node).toBeDefined() 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 ({ test('refresh button is visible in selection toolbar when node is selected', async ({
comfyPage comfyPage
}) => { }) => {
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true) await comfyPage.settings.setSetting('Comfy.Canvas.SelectionToolbox', true)
const nodeName = 'Remote Widget Node' const nodeName = 'Remote Widget Node'
await addRemoteWidgetNode(comfyPage, nodeName) await addRemoteWidgetNode(comfyPage, nodeName)
@@ -196,7 +196,7 @@ test.describe('Remote COMBO Widget', { tag: '@widget' }, () => {
// Fulfill each request with a unique timestamp // Fulfill each request with a unique timestamp
await comfyPage.page.route( await comfyPage.page.route(
'**/api/models/checkpoints**', '**/api/models/checkpoints**',
async (route, request) => { async (route, _request) => {
await route.fulfill({ await route.fulfill({
body: JSON.stringify([Date.now()]), body: JSON.stringify([Date.now()]),
status: 200 status: 200
@@ -257,13 +257,11 @@ test.describe('Remote COMBO Widget', { tag: '@widget' }, () => {
await addRemoteWidgetNode(comfyPage, nodeName) await addRemoteWidgetNode(comfyPage, nodeName)
await waitForWidgetUpdate(comfyPage) await waitForWidgetUpdate(comfyPage)
// Wait for timeout and backoff, then force re-render, repeat // Wait for exponential backoff retries to accumulate timestamps
const requestTimeout = 512 await expect(async () => {
await comfyPage.page.waitForTimeout(requestTimeout) await waitForWidgetUpdate(comfyPage)
await waitForWidgetUpdate(comfyPage) expect(timestamps.length).toBeGreaterThanOrEqual(3)
await comfyPage.page.waitForTimeout(requestTimeout * 2) }).toPass({ timeout: 10000, intervals: [500, 1000, 1500] })
await waitForWidgetUpdate(comfyPage)
await comfyPage.page.waitForTimeout(requestTimeout * 3)
// Verify exponential backoff between retries // Verify exponential backoff between retries
const intervals = timestamps.slice(1).map((t, i) => t - timestamps[i]) 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.describe('Reroute Node', { tag: ['@screenshot', '@node'] }, () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
}) })
test.afterEach(async ({ comfyPage }) => { test.afterEach(async ({ comfyPage }) => {
await comfyPage.setupWorkflowsDirectory({}) await comfyPage.workflow.setupWorkflowsDirectory({})
}) })
test('loads from inserted workflow', async ({ comfyPage }) => { test('loads from inserted workflow', async ({ comfyPage }) => {
const workflowName = 'single_connected_reroute_node.json' const workflowName = 'single_connected_reroute_node.json'
await comfyPage.setupWorkflowsDirectory({ await comfyPage.workflow.setupWorkflowsDirectory({
[workflowName]: 'links/single_connected_reroute_node.json' [workflowName]: 'links/single_connected_reroute_node.json'
}) })
await comfyPage.setup() await comfyPage.setup()
@@ -43,12 +43,12 @@ test.describe(
{ tag: ['@screenshot', '@node'] }, { tag: ['@screenshot', '@node'] },
() => { () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.setSetting('LiteGraph.Reroute.SplineOffset', 80) await comfyPage.settings.setSetting('LiteGraph.Reroute.SplineOffset', 80)
}) })
test('loads from workflow', async ({ comfyPage }) => { 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') await expect(comfyPage.canvas).toHaveScreenshot('native_reroute.png')
}) })
@@ -56,10 +56,10 @@ test.describe(
comfyPage comfyPage
}) => { }) => {
const loadCheckpointNode = ( const loadCheckpointNode = (
await comfyPage.getNodeRefsByTitle('Load Checkpoint') await comfyPage.nodeOps.getNodeRefsByTitle('Load Checkpoint')
)[0] )[0]
const clipEncodeNode = ( const clipEncodeNode = (
await comfyPage.getNodeRefsByTitle('CLIP Text Encode (Prompt)') await comfyPage.nodeOps.getNodeRefsByTitle('CLIP Text Encode (Prompt)')
)[0] )[0]
const slot1 = await loadCheckpointNode.getOutput(1) const slot1 = await loadCheckpointNode.getOutput(1)
@@ -82,10 +82,10 @@ test.describe(
comfyPage comfyPage
}) => { }) => {
const loadCheckpointNode = ( const loadCheckpointNode = (
await comfyPage.getNodeRefsByTitle('Load Checkpoint') await comfyPage.nodeOps.getNodeRefsByTitle('Load Checkpoint')
)[0] )[0]
const clipEncodeNode = ( const clipEncodeNode = (
await comfyPage.getNodeRefsByTitle('CLIP Text Encode (Prompt)') await comfyPage.nodeOps.getNodeRefsByTitle('CLIP Text Encode (Prompt)')
)[0] )[0]
const slot1 = await loadCheckpointNode.getOutput(1) const slot1 = await loadCheckpointNode.getOutput(1)
@@ -109,7 +109,7 @@ test.describe(
comfyPage comfyPage
}) => { }) => {
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/4695 // https://github.com/Comfy-Org/ComfyUI_frontend/issues/4695
await comfyPage.loadWorkflow( await comfyPage.workflow.loadWorkflow(
'reroute/single-native-reroute-default-workflow' 'reroute/single-native-reroute-default-workflow'
) )

View File

@@ -2,9 +2,10 @@ import { expect } from '@playwright/test'
import { NodeBadgeMode } from '../../src/types/nodeSource' import { NodeBadgeMode } from '../../src/types/nodeSource'
import { comfyPageFixture as test } from '../fixtures/ComfyPage' import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { DefaultGraphPositions } from '../fixtures/constants/defaultGraphPositions'
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
}) })
test.describe( test.describe(
@@ -12,7 +13,7 @@ test.describe(
{ tag: ['@screenshot', '@ui'] }, { tag: ['@screenshot', '@ui'] },
() => { () => {
test('Can add node', async ({ comfyPage }) => { test('Can add node', async ({ comfyPage }) => {
await comfyPage.rightClickCanvas() await comfyPage.canvasOps.rightClick()
await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png') await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png')
await comfyPage.page.getByText('Add Node').click() await comfyPage.page.getByText('Add Node').click()
await comfyPage.nextFrame() await comfyPage.nextFrame()
@@ -24,7 +25,7 @@ test.describe(
}) })
test('Can add group', async ({ comfyPage }) => { test('Can add group', async ({ comfyPage }) => {
await comfyPage.rightClickCanvas() await comfyPage.canvasOps.rightClick()
await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png') await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png')
await comfyPage.page.getByText('Add Group', { exact: true }).click() await comfyPage.page.getByText('Add Group', { exact: true }).click()
await comfyPage.nextFrame() await comfyPage.nextFrame()
@@ -34,13 +35,16 @@ test.describe(
}) })
test('Can convert to group node', async ({ comfyPage }) => { 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 expect(comfyPage.canvas).toHaveScreenshot('selected-2-nodes.png')
await comfyPage.rightClickCanvas() await comfyPage.canvasOps.rightClick()
await comfyPage.clickContextMenuItem('Convert to Group Node (Deprecated)') await comfyPage.contextMenu.clickMenuItem(
await comfyPage.promptDialogInput.fill('GroupNode2CLIP') 'Convert to Group Node (Deprecated)'
)
await comfyPage.nextFrame()
await comfyPage.nodeOps.promptDialogInput.fill('GroupNode2CLIP')
await comfyPage.page.keyboard.press('Enter') await comfyPage.page.keyboard.press('Enter')
await comfyPage.promptDialogInput.waitFor({ state: 'hidden' }) await comfyPage.nodeOps.promptDialogInput.waitFor({ state: 'hidden' })
await comfyPage.nextFrame() await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
'right-click-node-group-node.png' 'right-click-node-group-node.png'
@@ -51,7 +55,12 @@ test.describe(
test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => { test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => {
test('Can open properties panel', async ({ comfyPage }) => { 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 expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
await comfyPage.page.getByText('Properties Panel').click() await comfyPage.page.getByText('Properties Panel').click()
await comfyPage.nextFrame() await comfyPage.nextFrame()
@@ -61,7 +70,12 @@ test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => {
}) })
test('Can collapse', async ({ comfyPage }) => { 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 expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
await comfyPage.page.getByText('Collapse').click() await comfyPage.page.getByText('Collapse').click()
await comfyPage.nextFrame() await comfyPage.nextFrame()
@@ -71,16 +85,21 @@ test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => {
}) })
test('Can collapse (Node Badge)', async ({ comfyPage }) => { test('Can collapse (Node Badge)', async ({ comfyPage }) => {
await comfyPage.setSetting( await comfyPage.settings.setSetting(
'Comfy.NodeBadge.NodeIdBadgeMode', 'Comfy.NodeBadge.NodeIdBadgeMode',
NodeBadgeMode.ShowAll NodeBadgeMode.ShowAll
) )
await comfyPage.setSetting( await comfyPage.settings.setSetting(
'Comfy.NodeBadge.NodeSourceBadgeMode', 'Comfy.NodeBadge.NodeSourceBadgeMode',
NodeBadgeMode.ShowAll 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.page.getByText('Collapse').click()
await comfyPage.nextFrame() await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
@@ -89,7 +108,12 @@ test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => {
}) })
test('Can bypass', async ({ comfyPage }) => { 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 expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
await comfyPage.page.getByText('Bypass').click() await comfyPage.page.getByText('Bypass').click()
await comfyPage.nextFrame() await comfyPage.nextFrame()
@@ -99,46 +123,89 @@ test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => {
}) })
test('Can pin and unpin', async ({ comfyPage }) => { 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 expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')
await comfyPage.page.click('.litemenu-entry:has-text("Pin")') await comfyPage.page.click('.litemenu-entry:has-text("Pin")')
await comfyPage.nextFrame() 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 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( await expect(comfyPage.canvas).toHaveScreenshot(
'right-click-pinned-node.png' 'right-click-pinned-node.png'
) )
await comfyPage.page.click('.litemenu-entry:has-text("Unpin")') await comfyPage.page.click('.litemenu-entry:has-text("Unpin")')
await comfyPage.nextFrame() 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( await expect(comfyPage.canvas).toHaveScreenshot(
'right-click-unpinned-node.png' 'right-click-unpinned-node.png'
) )
}) })
test('Can move after unpin', async ({ comfyPage }) => { 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.page.click('.litemenu-entry:has-text("Pin")')
await comfyPage.nextFrame() 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.page.click('.litemenu-entry:has-text("Unpin")')
await comfyPage.nextFrame() 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( await expect(comfyPage.canvas).toHaveScreenshot(
'right-click-unpinned-node-moved.png' 'right-click-unpinned-node-moved.png'
) )
}) })
test('Can pin/unpin selected nodes', async ({ comfyPage }) => { 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.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.click('.litemenu-entry:has-text("Pin")')
await comfyPage.page.keyboard.up('Control') await comfyPage.page.keyboard.up('Control')
await comfyPage.nextFrame() await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('selected-nodes-pinned.png') 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.page.click('.litemenu-entry:has-text("Unpin")')
await comfyPage.nextFrame() await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot( 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 }) => { test('Can clone pinned nodes', async ({ comfyPage }) => {
const nodeCount = await comfyPage.getGraphNodesCount() const nodeCount = await comfyPage.nodeOps.getGraphNodesCount()
const node = (await comfyPage.getFirstNodeRef())! const node = (await comfyPage.nodeOps.getFirstNodeRef())!
await node.clickContextMenuOption('Pin') await node.clickContextMenuOption('Pin')
await comfyPage.nextFrame() await comfyPage.nextFrame()
await node.click('title', { button: 'right' }) await node.click('title', { button: 'right' })
@@ -161,6 +228,6 @@ test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => {
await cloneItem.click() await cloneItem.click()
await expect(cloneItem).toHaveCount(0) await expect(cloneItem).toHaveCount(0)
await comfyPage.nextFrame() 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 const test = comfyPageFixture
test.beforeEach(async ({ comfyPage }) => { 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 BLUE_COLOR = 'rgb(51, 51, 85)'
const RED_COLOR = 'rgb(85, 51, 51)' const RED_COLOR = 'rgb(85, 51, 51)'
test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => { test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
test.beforeEach(async ({ comfyPage }) => { 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 }) => { test('shows selection toolbox', async ({ comfyPage }) => {
@@ -20,7 +20,10 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
await expect(comfyPage.selectionToolbox).not.toBeVisible() await expect(comfyPage.selectionToolbox).not.toBeVisible()
// Select multiple nodes // 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 // Selection toolbox should be visible with multiple nodes selected
await expect(comfyPage.selectionToolbox).toBeVisible() 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 ({ test('shows at correct position when node is pasted', async ({
comfyPage comfyPage
}) => { }) => {
await comfyPage.loadWorkflow('nodes/single_ksampler') await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
await comfyPage.selectNodes(['KSampler']) await comfyPage.nodeOps.selectNodes(['KSampler'])
await comfyPage.ctrlC() await comfyPage.clipboard.copy()
await comfyPage.page.mouse.move(100, 100) await comfyPage.page.mouse.move(100, 100)
await comfyPage.ctrlV() await comfyPage.clipboard.paste()
const toolboxContainer = comfyPage.selectionToolbox const toolboxContainer = comfyPage.selectionToolbox
await expect(toolboxContainer).toBeVisible() 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 ({ test('hide when select and drag happen at the same time', async ({
comfyPage comfyPage
}) => { }) => {
await comfyPage.loadWorkflow('nodes/single_ksampler') await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
const node = (await comfyPage.getNodeRefsByTitle('KSampler'))[0] const node = (await comfyPage.nodeOps.getNodeRefsByTitle('KSampler'))[0]
const nodePos = await node.getPosition() const nodePos = await node.getPosition()
// Drag on the title of the node // 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 }) => { test('shows border only with multiple selections', async ({ comfyPage }) => {
// Select single node // Select single node
await comfyPage.selectNodes(['KSampler']) await comfyPage.nodeOps.selectNodes(['KSampler'])
// Selection toolbox should be visible but without border // Selection toolbox should be visible but without border
await expect(comfyPage.selectionToolbox).toBeVisible() await expect(comfyPage.selectionToolbox).toBeVisible()
@@ -78,7 +81,10 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
) )
// Select multiple nodes // 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) // Selection border should show with multiple selections (canvas-based)
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
@@ -86,7 +92,7 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
) )
// Deselect to single node // 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) // Border should be hidden again (canvas-based)
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
@@ -98,7 +104,7 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
comfyPage comfyPage
}) => { }) => {
// A group + a KSampler node // 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 // Select group + node should show bypass button
await comfyPage.page.focus('canvas') await comfyPage.page.focus('canvas')
@@ -110,7 +116,7 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
).toBeVisible() ).toBeVisible()
// Deselect node (Only group is selected) should hide bypass button // Deselect node (Only group is selected) should hide bypass button
await comfyPage.selectNodes(['KSampler']) await comfyPage.nodeOps.selectNodes(['KSampler'])
await expect( await expect(
comfyPage.page.locator( comfyPage.page.locator(
'.selection-toolbox *[data-testid="bypass-button"]' '.selection-toolbox *[data-testid="bypass-button"]'
@@ -123,7 +129,7 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
comfyPage comfyPage
}) => { }) => {
// Select a node // Select a node
await comfyPage.selectNodes(['KSampler']) await comfyPage.nodeOps.selectNodes(['KSampler'])
// Color picker button should be visible // Color picker button should be visible
const colorPickerButton = comfyPage.page.locator( const colorPickerButton = comfyPage.page.locator(
@@ -151,7 +157,9 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
// Node should have the selected color class/style // Node should have the selected color class/style
// Note: Exact verification method depends on how color is applied to nodes // 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() expect(await selectedNode.getProperty('color')).not.toBeNull()
}) })
@@ -159,7 +167,10 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
comfyPage comfyPage
}) => { }) => {
// Select multiple nodes // 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( const colorPickerButton = comfyPage.page.locator(
'.selection-toolbox .pi-circle-fill' '.selection-toolbox .pi-circle-fill'
@@ -183,22 +194,25 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
comfyPage comfyPage
}) => { }) => {
// Select first node and color it // 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('.selection-toolbox .pi-circle-fill').click()
await comfyPage.page await comfyPage.page
.locator('.color-picker-container i[data-testid="blue"]') .locator('.color-picker-container i[data-testid="blue"]')
.click() .click()
await comfyPage.selectNodes(['KSampler']) await comfyPage.nodeOps.selectNodes(['KSampler'])
// Select second node and color it differently // 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('.selection-toolbox .pi-circle-fill').click()
await comfyPage.page await comfyPage.page
.locator('.color-picker-container i[data-testid="red"]') .locator('.color-picker-container i[data-testid="red"]')
.click() .click()
// Select both nodes // 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 // Color picker should show null/mixed state
const colorPickerButton = comfyPage.page.locator( const colorPickerButton = comfyPage.page.locator(
@@ -211,17 +225,17 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
comfyPage comfyPage
}) => { }) => {
// First color a node // 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('.selection-toolbox .pi-circle-fill').click()
await comfyPage.page await comfyPage.page
.locator('.color-picker-container i[data-testid="blue"]') .locator('.color-picker-container i[data-testid="blue"]')
.click() .click()
// Clear selection // Clear selection
await comfyPage.selectNodes(['KSampler']) await comfyPage.nodeOps.selectNodes(['KSampler'])
// Re-select the node // Re-select the node
await comfyPage.selectNodes(['KSampler']) await comfyPage.nodeOps.selectNodes(['KSampler'])
// Color picker button should show the correct color // Color picker button should show the correct color
const colorPickerButton = comfyPage.page.locator( const colorPickerButton = comfyPage.page.locator(
@@ -234,7 +248,7 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
comfyPage comfyPage
}) => { }) => {
// Select a node and color it // 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('.selection-toolbox .pi-circle-fill').click()
await comfyPage.page await comfyPage.page
.locator('.color-picker-container i[data-testid="blue"]') .locator('.color-picker-container i[data-testid="blue"]')
@@ -245,7 +259,9 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
await comfyPage.nextFrame() await comfyPage.nextFrame()
// Node should be uncolored again // 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() 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' import type { ComfyPage } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
}) })
test.describe( test.describe(
@@ -12,15 +12,16 @@ test.describe(
{ tag: '@ui' }, { tag: '@ui' },
() => { () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true) await comfyPage.settings.setSetting('Comfy.Canvas.SelectionToolbox', true)
await comfyPage.loadWorkflow('nodes/single_ksampler') await comfyPage.workflow.loadWorkflow('nodes/single_ksampler')
await comfyPage.nextFrame() await comfyPage.nextFrame()
await comfyPage.selectNodes(['KSampler']) await comfyPage.nodeOps.selectNodes(['KSampler'])
await comfyPage.nextFrame() await comfyPage.nextFrame()
}) })
const openMoreOptions = async (comfyPage: ComfyPage) => { const openMoreOptions = async (comfyPage: ComfyPage) => {
const ksamplerNodes = await comfyPage.getNodeRefsByTitle('KSampler') const ksamplerNodes =
await comfyPage.nodeOps.getNodeRefsByTitle('KSampler')
if (ksamplerNodes.length === 0) { if (ksamplerNodes.length === 0) {
throw new Error('No KSampler nodes found') throw new Error('No KSampler nodes found')
} }
@@ -28,9 +29,14 @@ test.describe(
// Drag the KSampler to the center of the screen // Drag the KSampler to the center of the screen
const nodePos = await ksamplerNodes[0].getPosition() const nodePos = await ksamplerNodes[0].getPosition()
const viewportSize = comfyPage.page.viewportSize() 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 centerX = viewportSize.width / 3
const centerY = viewportSize.height / 2 const centerY = viewportSize.height / 2
await comfyPage.dragAndDrop( await comfyPage.canvasOps.dragAndDrop(
{ x: nodePos.x, y: nodePos.y }, { x: nodePos.x, y: nodePos.y },
{ x: centerX, y: centerY } { x: centerX, y: centerY }
) )
@@ -85,7 +91,9 @@ test.describe(
}) })
test('changes node shape via Shape submenu', async ({ comfyPage }) => { 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') const initialShape = await nodeRef.getProperty<number>('shape')
await openMoreOptions(comfyPage) await openMoreOptions(comfyPage)
@@ -106,7 +114,9 @@ test.describe(
test('changes node color via Color submenu swatch', async ({ test('changes node color via Color submenu swatch', async ({
comfyPage comfyPage
}) => { }) => {
const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0] const nodeRef = (
await comfyPage.nodeOps.getNodeRefsByTitle('KSampler')
)[0]
const initialColor = await nodeRef.getProperty<string | undefined>( const initialColor = await nodeRef.getProperty<string | undefined>(
'color' 'color'
) )
@@ -126,7 +136,9 @@ test.describe(
}) })
test('renames a node using Rename action', async ({ comfyPage }) => { 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 openMoreOptions(comfyPage)
await comfyPage.page await comfyPage.page
.getByText('Rename', { exact: true }) .getByText('Rename', { exact: true })

View File

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

View File

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

View File

@@ -3,7 +3,6 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage' import { comfyPageFixture as test } from '../fixtures/ComfyPage'
// Constants // Constants
const INITIAL_NAME = 'initial_slot_name'
const RENAMED_NAME = 'renamed_slot_name' const RENAMED_NAME = 'renamed_slot_name'
const SECOND_RENAMED_NAME = 'second_renamed_name' const SECOND_RENAMED_NAME = 'second_renamed_name'
@@ -14,26 +13,34 @@ const SELECTORS = {
test.describe('Subgraph Slot Rename Dialog', { tag: '@subgraph' }, () => { test.describe('Subgraph Slot Rename Dialog', { tag: '@subgraph' }, () => {
test.beforeEach(async ({ comfyPage }) => { 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 ({ test('Shows current slot label (not stale) in rename dialog', async ({
comfyPage 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() await subgraphNode.navigateIntoSubgraph()
// Get initial slot label // Get initial slot label
const initialInputLabel = await comfyPage.page.evaluate(() => { 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 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 // First rename
await comfyPage.rightClickSubgraphInputSlot(initialInputLabel) await comfyPage.subgraph.rightClickInputSlot(initialInputLabel)
await comfyPage.clickLitegraphContextMenuItem('Rename Slot') await comfyPage.contextMenu.clickLitegraphMenuItem('Rename Slot')
await comfyPage.nextFrame()
await comfyPage.page.waitForSelector(SELECTORS.promptDialog, { await comfyPage.page.waitForSelector(SELECTORS.promptDialog, {
state: 'visible' state: 'visible'
@@ -55,7 +62,9 @@ test.describe('Subgraph Slot Rename Dialog', { tag: '@subgraph' }, () => {
// Verify the rename worked // Verify the rename worked
const afterFirstRename = await comfyPage.page.evaluate(() => { 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] const slot = graph.inputs?.[0]
return { return {
label: slot?.label || null, 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 // 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 // We need to use the index-based approach since the method looks for slot.name
await comfyPage.rightClickSubgraphInputSlot() await comfyPage.subgraph.rightClickInputSlot()
await comfyPage.clickLitegraphContextMenuItem('Rename Slot') await comfyPage.contextMenu.clickLitegraphMenuItem('Rename Slot')
await comfyPage.nextFrame()
await comfyPage.page.waitForSelector(SELECTORS.promptDialog, { await comfyPage.page.waitForSelector(SELECTORS.promptDialog, {
state: 'visible' state: 'visible'
@@ -97,7 +107,8 @@ test.describe('Subgraph Slot Rename Dialog', { tag: '@subgraph' }, () => {
// Verify the second rename worked // Verify the second rename worked
const afterSecondRename = await comfyPage.page.evaluate(() => { 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 return graph.inputs?.[0]?.label || null
}) })
expect(afterSecondRename).toBe(SECOND_RENAMED_NAME) 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 ({ test('Shows current output slot label in rename dialog', async ({
comfyPage 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() await subgraphNode.navigateIntoSubgraph()
// Get initial output slot label // Get initial output slot label
const initialOutputLabel = await comfyPage.page.evaluate(() => { 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 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 // First rename
await comfyPage.rightClickSubgraphOutputSlot(initialOutputLabel) await comfyPage.subgraph.rightClickOutputSlot(initialOutputLabel)
await comfyPage.clickLitegraphContextMenuItem('Rename Slot') await comfyPage.contextMenu.clickLitegraphMenuItem('Rename Slot')
await comfyPage.nextFrame()
await comfyPage.page.waitForSelector(SELECTORS.promptDialog, { await comfyPage.page.waitForSelector(SELECTORS.promptDialog, {
state: 'visible' state: 'visible'
@@ -141,8 +160,9 @@ test.describe('Subgraph Slot Rename Dialog', { tag: '@subgraph' }, () => {
// Now rename again to check for stale content // Now rename again to check for stale content
// We need to use the index-based approach since the method looks for slot.name // We need to use the index-based approach since the method looks for slot.name
await comfyPage.rightClickSubgraphOutputSlot() await comfyPage.subgraph.rightClickOutputSlot()
await comfyPage.clickLitegraphContextMenuItem('Rename Slot') await comfyPage.contextMenu.clickLitegraphMenuItem('Rename Slot')
await comfyPage.nextFrame()
await comfyPage.page.waitForSelector(SELECTORS.promptDialog, { await comfyPage.page.waitForSelector(SELECTORS.promptDialog, {
state: 'visible' state: 'visible'

View File

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

View File

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

View File

@@ -1,27 +1,37 @@
import { expect } from '@playwright/test' import { expect } from '@playwright/test'
import type { Settings } from '../../src/schemas/apiSchema'
import { comfyPageFixture as test } from '../fixtures/ComfyPage' 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 }) => { 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.describe('Settings Search functionality', { tag: '@settings' }, () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
// Register test settings to verify hidden/deprecated filtering // Register test settings to verify hidden/deprecated filtering
await comfyPage.page.evaluate(() => { await comfyPage.page.evaluate(() => {
window['app'].registerExtension({ window.app!.registerExtension({
name: 'TestSettingsExtension', name: 'TestSettingsExtension',
settings: [ settings: [
{ {
id: 'TestHiddenSetting', // Extensions can register arbitrary setting IDs
id: 'TestHiddenSetting' as TestSettingId,
name: 'Test Hidden Setting', name: 'Test Hidden Setting',
type: 'hidden', type: 'hidden',
defaultValue: 'hidden_value', defaultValue: 'hidden_value',
category: ['Test', 'Hidden'] category: ['Test', 'Hidden']
}, },
{ {
id: 'TestDeprecatedSetting', // Extensions can register arbitrary setting IDs
id: 'TestDeprecatedSetting' as TestSettingId,
name: 'Test Deprecated Setting', name: 'Test Deprecated Setting',
type: 'text', type: 'text',
defaultValue: 'deprecated_value', defaultValue: 'deprecated_value',
@@ -29,7 +39,8 @@ test.describe('Settings Search functionality', { tag: '@settings' }, () => {
category: ['Test', 'Deprecated'] category: ['Test', 'Deprecated']
}, },
{ {
id: 'TestVisibleSetting', // Extensions can register arbitrary setting IDs
id: 'TestVisibleSetting' as TestSettingId,
name: 'Test Visible Setting', name: 'Test Visible Setting',
type: 'text', type: 'text',
defaultValue: 'visible_value', defaultValue: 'visible_value',
@@ -109,19 +120,14 @@ test.describe('Settings Search functionality', { tag: '@settings' }, () => {
const settingsDialog = comfyPage.page.locator('.settings-container') const settingsDialog = comfyPage.page.locator('.settings-container')
await expect(settingsDialog).toBeVisible() await expect(settingsDialog).toBeVisible()
// Get categories and click on different ones // Click on a specific category (Appearance) to verify category switching
const categories = comfyPage.page.locator( const appearanceCategory = comfyPage.page.getByRole('option', {
'.settings-sidebar .p-listbox-option' name: 'Appearance'
) })
const categoryCount = await categories.count() await appearanceCategory.click()
if (categoryCount > 1) { // Verify the category is selected
// Click on the second category await expect(appearanceCategory).toHaveClass(/p-listbox-option-selected/)
await categories.nth(1).click()
// Verify the category is selected
await expect(categories.nth(1)).toHaveClass(/p-listbox-option-selected/)
}
}) })
test('settings content area is visible', async ({ comfyPage }) => { 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 }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting( await comfyPage.settings.setSetting(
'Comfy.VersionCompatibility.DisableWarnings', 'Comfy.VersionCompatibility.DisableWarnings',
false false
) )
@@ -103,10 +103,9 @@ test.describe('Version Mismatch Warnings', { tag: '@slow' }, () => {
await comfyPage.setup() await comfyPage.setup()
// Locate the warning toast and dismiss it // Locate the warning toast and dismiss it
const warningToast = comfyPage.page const warningToast = comfyPage.page.locator('.p-toast-message').filter({
.locator('div') hasText: 'Version Compatibility'
.filter({ hasText: 'Version Compatibility' }) })
.nth(3)
await warningToast.waitFor({ state: 'visible' }) await warningToast.waitFor({ state: 'visible' })
const dismissButton = warningToast.getByRole('button', { name: 'Close' }) const dismissButton = warningToast.getByRole('button', { name: 'Close' })
await dismissButton.click() 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 ({ test('Fits view to nodes when saved viewport position is offscreen', async ({
comfyPage 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 // Wait a few frames for rendering to stabilize
for (let i = 0; i < 5; i++) { 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.describe('Vue Node Groups', { tag: '@screenshot' }, () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.setSetting('Comfy.Minimap.ShowGroups', true) await comfyPage.settings.setSetting('Comfy.Minimap.ShowGroups', true)
await comfyPage.vueNodes.waitForNodes() await comfyPage.vueNodes.waitForNodes()
}) })
@@ -24,9 +24,9 @@ test.describe('Vue Node Groups', { tag: '@screenshot' }, () => {
test('should allow fitting group to contents', async ({ comfyPage }) => { test('should allow fitting group to contents', async ({ comfyPage }) => {
await comfyPage.setup() await comfyPage.setup()
await comfyPage.loadWorkflow('groups/oversized_group') await comfyPage.workflow.loadWorkflow('groups/oversized_group')
await comfyPage.ctrlA() await comfyPage.keyboard.selectAll()
await comfyPage.executeCommand('Comfy.Graph.FitGroupToContents') await comfyPage.command.executeCommand('Comfy.Graph.FitGroupToContents')
await comfyPage.nextFrame() await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot( await expect(comfyPage.canvas).toHaveScreenshot(
'vue-groups-fit-to-contents.png' '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 ({ test('should move nested groups together when dragging outer group', async ({
comfyPage 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 // Get initial positions with null guards
const outerInitial = await comfyPage.getGroupPosition('Outer Group') const outerInitial =
const innerInitial = await comfyPage.getGroupPosition('Inner Group') await comfyPage.canvasOps.getGroupPosition('Outer Group')
const innerInitial =
await comfyPage.canvasOps.getGroupPosition('Inner Group')
const initialOffsetX = innerInitial.x - outerInitial.x const initialOffsetX = innerInitial.x - outerInitial.x
const initialOffsetY = innerInitial.y - outerInitial.y const initialOffsetY = innerInitial.y - outerInitial.y
// Drag the outer group // Drag the outer group
const dragDelta = { x: 100, y: 80 } const dragDelta = { x: 100, y: 80 }
await comfyPage.dragGroup({ await comfyPage.canvasOps.dragGroup({
name: 'Outer Group', name: 'Outer Group',
deltaX: dragDelta.x, deltaX: dragDelta.x,
deltaY: dragDelta.y deltaY: dragDelta.y
@@ -55,8 +57,10 @@ test.describe('Vue Node Groups', { tag: '@screenshot' }, () => {
// Use retrying assertion to wait for positions to update // Use retrying assertion to wait for positions to update
await expect(async () => { await expect(async () => {
const outerFinal = await comfyPage.getGroupPosition('Outer Group') const outerFinal =
const innerFinal = await comfyPage.getGroupPosition('Inner Group') await comfyPage.canvasOps.getGroupPosition('Outer Group')
const innerFinal =
await comfyPage.canvasOps.getGroupPosition('Inner Group')
const finalOffsetX = innerFinal.x - outerFinal.x const finalOffsetX = innerFinal.x - outerFinal.x
const finalOffsetY = innerFinal.y - outerFinal.y 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.describe('Vue Nodes Canvas Pan', () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.vueNodes.waitForNodes() await comfyPage.vueNodes.waitForNodes()
}) })
@@ -13,7 +13,10 @@ test.describe('Vue Nodes Canvas Pan', () => {
'@mobile Can pan with touch', '@mobile Can pan with touch',
{ tag: '@screenshot' }, { tag: '@screenshot' },
async ({ comfyPage }) => { 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( await expect(comfyPage.canvas).toHaveScreenshot(
'vue-nodes-paned-with-touch.png' 'vue-nodes-paned-with-touch.png'
) )

View File

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

View File

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