refactor: extract CanvasHelper from ComfyPage.ts

- Create CanvasHelper class for canvas viewport operations

- Extract pan, zoom, dragAndDrop, rightClick, doubleClick methods

- Extract moveMouseToEmptyArea, convertOffsetToCanvas methods

- Add deprecation proxies for backward compatibility

- Add canvasOps property to ComfyPage

Amp-Thread-ID: https://ampcode.com/threads/T-019c1300-e933-769c-b05f-ea00c2d32dd1
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Alexander Brown
2026-01-30 23:45:43 -08:00
parent e4b520c602
commit 5ddce4025c
2 changed files with 151 additions and 50 deletions

View File

@@ -20,10 +20,11 @@ import {
} from './components/SidebarTab'
import { Topbar } from './components/Topbar'
import { DefaultGraphPositions } from './constants/defaultGraphPositions'
import { CanvasHelper } from './helpers/CanvasHelper'
import { DebugHelper } from './helpers/DebugHelper'
import { SubgraphHelper } from './helpers/SubgraphHelper'
import type { Position, Size } from './types'
import type { SubgraphSlotReference } from './utils/litegraphUtils';
import type { SubgraphSlotReference } from './utils/litegraphUtils'
import { NodeReference } from './utils/litegraphUtils'
dotenv.config()
@@ -176,6 +177,7 @@ export class ComfyPage {
public readonly vueNodes: VueNodeHelpers
public readonly debug: DebugHelper
public readonly subgraph: SubgraphHelper
public readonly canvasOps: CanvasHelper
/** Worker index to test user ID */
public readonly userIds: string[] = []
@@ -210,6 +212,7 @@ export class ComfyPage {
this.vueNodes = new VueNodeHelpers(page)
this.debug = new DebugHelper(page, this.canvas)
this.subgraph = new SubgraphHelper(this)
this.canvasOps = new CanvasHelper(page, this.canvas, this.resetViewButton)
}
convertLeafToContent(structure: FolderStructure): FolderStructure {
@@ -502,13 +505,9 @@ export class ComfyPage {
})
}
/** @deprecated Use this.canvasOps.resetView() instead */
async resetView() {
if (await this.resetViewButton.isVisible()) {
await this.resetViewButton.click()
}
// Avoid "Reset View" button highlight.
await this.page.mouse.move(10, 10)
await this.nextFrame()
return this.canvasOps.resetView()
}
async getToastErrorCount() {
@@ -565,18 +564,12 @@ export class ComfyPage {
}
async clickEmptySpace() {
await this.canvas.click({
position: DefaultGraphPositions.emptySpaceClick
})
await this.nextFrame()
return this.canvasOps.clickEmptySpace(DefaultGraphPositions.emptySpaceClick)
}
/** @deprecated Use this.canvasOps.dragAndDrop() instead */
async dragAndDrop(source: Position, target: Position) {
await this.page.mouse.move(source.x, source.y)
await this.page.mouse.down()
await this.page.mouse.move(target.x, target.y, { steps: 100 })
await this.page.mouse.up()
await this.nextFrame()
return this.canvasOps.dragAndDrop(source, target)
}
async dragAndDropExternalResource(
@@ -793,44 +786,24 @@ export class ComfyPage {
await this.nextFrame()
}
/** @deprecated Use this.canvasOps.zoom() instead */
async zoom(deltaY: number, steps: number = 1) {
await this.page.mouse.move(10, 10)
for (let i = 0; i < steps; i++) {
await this.page.mouse.wheel(0, deltaY)
}
await this.nextFrame()
return this.canvasOps.zoom(deltaY, steps)
}
/** @deprecated Use this.canvasOps.pan() instead */
async pan(offset: Position, safeSpot?: Position) {
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()
return this.canvasOps.pan(offset, safeSpot)
}
/** @deprecated Use this.canvasOps.panWithTouch() instead */
async panWithTouch(offset: Position, safeSpot?: Position) {
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()
return this.canvasOps.panWithTouch(offset, safeSpot)
}
/** @deprecated Use this.canvasOps.rightClick() instead */
async rightClickCanvas(x: number = 10, y: number = 10) {
await this.page.mouse.click(x, y, { button: 'right' })
await this.nextFrame()
return this.canvasOps.rightClick(x, y)
}
async clickContextMenuItem(name: string): Promise<void> {
@@ -959,9 +932,9 @@ export class ComfyPage {
return this.debug.attachScreenshot(testInfo, name, options)
}
/** @deprecated Use this.canvasOps.doubleClick() instead */
async doubleClickCanvas() {
await this.page.mouse.dblclick(10, 10, { delay: 5 })
await this.nextFrame()
return this.canvasOps.doubleClick()
}
/** @deprecated Use this.debug.saveCanvasScreenshot() instead */
@@ -1166,10 +1139,9 @@ export class ComfyPage {
await this.nextFrame()
}
/** @deprecated Use this.canvasOps.convertOffsetToCanvas() instead */
async convertOffsetToCanvas(pos: [number, number]) {
return this.page.evaluate((pos) => {
return window['app'].canvas.ds.convertOffsetToCanvas(pos)
}, pos)
return this.canvasOps.convertOffsetToCanvas(pos)
}
/** Get number of DOM widgets on the canvas. */
@@ -1230,8 +1202,9 @@ export class ComfyPage {
if (!id) return null
return this.getNodeRefById(id)
}
/** @deprecated Use this.canvasOps.moveMouseToEmptyArea() instead */
async moveMouseToEmptyArea() {
await this.page.mouse.move(10, 10)
return this.canvasOps.moveMouseToEmptyArea()
}
async getUndoQueueSize() {
return this.page.evaluate(() => {

View File

@@ -0,0 +1,128 @@
import type { Locator, Page } from '@playwright/test'
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(position: Position): Promise<void> {
await this.canvas.click({ position })
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)
}
}