diff --git a/.gitattributes b/.gitattributes index 17591e2d4..c153cc320 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,17 +1,5 @@ -# Default -* text=auto - -# Force TS to LF to make the unixy scripts not break on Windows -*.cjs text eol=lf -*.js text eol=lf -*.json text eol=lf -*.mjs text eol=lf -*.mts text eol=lf -*.snap text eol=lf -*.ts text eol=lf -*.vue text eol=lf -*.yaml text eol=lf -*.yml text eol=lf +# Force all text files to use LF line endings +* text=auto eol=lf # Generated files packages/registry-types/src/comfyRegistryTypes.ts linguist-generated=true diff --git a/.github/workflows/ci-tests-e2e.yaml b/.github/workflows/ci-tests-e2e.yaml index b3c4e1998..34d24f7f0 100644 --- a/.github/workflows/ci-tests-e2e.yaml +++ b/.github/workflows/ci-tests-e2e.yaml @@ -37,7 +37,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 container: - image: ghcr.io/comfy-org/comfyui-ci-container:0.0.10 + image: ghcr.io/comfy-org/comfyui-ci-container:0.0.12 credentials: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} @@ -85,7 +85,7 @@ jobs: needs: setup runs-on: ubuntu-latest container: - image: ghcr.io/comfy-org/comfyui-ci-container:0.0.10 + image: ghcr.io/comfy-org/comfyui-ci-container:0.0.12 credentials: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pr-update-playwright-expectations.yaml b/.github/workflows/pr-update-playwright-expectations.yaml index c0865f85b..b9cf9bb75 100644 --- a/.github/workflows/pr-update-playwright-expectations.yaml +++ b/.github/workflows/pr-update-playwright-expectations.yaml @@ -77,7 +77,7 @@ jobs: needs: setup runs-on: ubuntu-latest container: - image: ghcr.io/comfy-org/comfyui-ci-container:0.0.10 + image: ghcr.io/comfy-org/comfyui-ci-container:0.0.12 credentials: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} diff --git a/.oxlintrc.json b/.oxlintrc.json index a0769d8f4..fb5d00dab 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -110,6 +110,17 @@ "rules": { "no-console": "allow" } + }, + { + "files": ["browser_tests/**/*.ts"], + "rules": { + "typescript/no-explicit-any": "error", + "no-async-promise-executor": "error", + "no-control-regex": "error", + "no-useless-rename": "error", + "no-unused-private-class-members": "error", + "unicorn/no-empty-file": "error" + } } ] } diff --git a/AGENTS.md b/AGENTS.md index 506a22f4f..4603eeabc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,7 +44,7 @@ The project uses **Nx** for build orchestration and task management - `pnpm build`: Type-check then production build to `dist/` - `pnpm preview`: Preview the production build locally - `pnpm test:unit`: Run Vitest unit tests -- `pnpm test:browser`: Run Playwright E2E tests (`browser_tests/`) +- `pnpm test:browser:local`: Run Playwright E2E tests (`browser_tests/`) - `pnpm lint` / `pnpm lint:fix`: Lint (ESLint) - `pnpm format` / `pnpm format:check`: oxfmt - `pnpm typecheck`: Vue TSC type checking diff --git a/browser_tests/AGENTS.md b/browser_tests/AGENTS.md index 177529ee1..b5cbe18c4 100644 --- a/browser_tests/AGENTS.md +++ b/browser_tests/AGENTS.md @@ -6,3 +6,9 @@ See `@docs/guidance/playwright.md` for Playwright best practices (auto-loaded fo - `assets/` - Test data (JSON workflows, fixtures) - Tests use premade JSON workflows to load desired graph state + +## After Making Changes + +- Run `pnpm typecheck:browser` after modifying TypeScript files in this directory +- Run `pnpm exec eslint browser_tests/path/to/file.ts` to lint specific files +- Run `pnpm exec oxlint browser_tests/path/to/file.ts` to check with oxlint diff --git a/browser_tests/fixtures/ComfyPage.ts b/browser_tests/fixtures/ComfyPage.ts index 2d5df88b0..2ed1759c7 100644 --- a/browser_tests/fixtures/ComfyPage.ts +++ b/browser_tests/fixtures/ComfyPage.ts @@ -1,38 +1,50 @@ -import type { APIRequestContext, Locator, Page } from '@playwright/test' +import type { + APIRequestContext, + ExpectMatcherState, + Locator, + Page +} from '@playwright/test' import { test as base, expect } from '@playwright/test' import dotenv from 'dotenv' -import * as fs from 'fs' -import type { LGraphNode, LGraph } from '../../src/lib/litegraph/src/litegraph' -import type { NodeId } from '../../src/platform/workflow/validation/schemas/workflowSchema' -import type { KeyCombo } from '../../src/platform/keybindings' -import type { useWorkspaceStore } from '../../src/stores/workspaceStore' +import { TestIds } from './selectors' import { NodeBadgeMode } from '../../src/types/nodeSource' import { ComfyActionbar } from '../helpers/actionbar' import { ComfyTemplates } from '../helpers/templates' import { ComfyMouse } from './ComfyMouse' import { VueNodeHelpers } from './VueNodeHelpers' import { ComfyNodeSearchBox } from './components/ComfyNodeSearchBox' +import { ContextMenu } from './components/ContextMenu' import { SettingDialog } from './components/SettingDialog' +import { BottomPanel } from './components/BottomPanel' import { NodeLibrarySidebarTab, WorkflowsSidebarTab } from './components/SidebarTab' import { Topbar } from './components/Topbar' -import type { Position, Size } from './types' -import { NodeReference, SubgraphSlotReference } from './utils/litegraphUtils' +import { CanvasHelper } from './helpers/CanvasHelper' +import { ClipboardHelper } from './helpers/ClipboardHelper' +import { CommandHelper } from './helpers/CommandHelper' +import { DebugHelper } from './helpers/DebugHelper' +import { DragDropHelper } from './helpers/DragDropHelper' +import { KeyboardHelper } from './helpers/KeyboardHelper' +import { NodeOperationsHelper } from './helpers/NodeOperationsHelper' +import { SettingsHelper } from './helpers/SettingsHelper' +import { SubgraphHelper } from './helpers/SubgraphHelper' +import { ToastHelper } from './helpers/ToastHelper' +import { WorkflowHelper } from './helpers/WorkflowHelper' +import type { NodeReference } from './utils/litegraphUtils' +import type { WorkspaceStore } from '../types/globals' dotenv.config() -type WorkspaceStore = ReturnType - class ComfyPropertiesPanel { readonly root: Locator readonly panelTitle: Locator readonly searchBox: Locator constructor(readonly page: Page) { - this.root = page.getByTestId('properties-panel') + this.root = page.getByTestId(TestIds.propertiesPanel.root) this.panelTitle = this.root.locator('h3') this.searchBox = this.root.getByPlaceholder('Search...') } @@ -45,16 +57,12 @@ class ComfyMenu { public readonly sideToolbar: Locator public readonly propertiesPanel: ComfyPropertiesPanel - public readonly themeToggleButton: Locator - public readonly saveButton: Locator + public readonly modeToggleButton: Locator constructor(public readonly page: Page) { - this.sideToolbar = page.locator('.side-tool-bar-container') - this.themeToggleButton = page.locator('.comfy-vue-theme-toggle') + this.sideToolbar = page.getByTestId(TestIds.sidebar.toolbar) + this.modeToggleButton = page.getByTestId(TestIds.sidebar.modeToggle) this.propertiesPanel = new ComfyPropertiesPanel(page) - this.saveButton = page - .locator('button[title="Save the current workflow"]') - .nth(0) } get buttons() { @@ -77,33 +85,28 @@ class ComfyMenu { } async toggleTheme() { - await this.themeToggleButton.click() - await this.page.evaluate(() => { - return new Promise((resolve) => { - window['app'].ui.settings.addEventListener( - 'Comfy.ColorPalette.change', - resolve, - { once: true } + const currentTheme = await this.getThemeId() + await this.modeToggleButton.click() + await this.page.waitForFunction( + (prevTheme) => { + const settings = window.app?.ui?.settings + return ( + settings && + settings.getSettingValue('Comfy.ColorPalette') !== prevTheme ) - - setTimeout(resolve, 5000) - }) - }) + }, + currentTheme, + { timeout: 5000 } + ) } async getThemeId() { return await this.page.evaluate(async () => { - return await window['app'].ui.settings.getSettingValue( - 'Comfy.ColorPalette' - ) + return await window.app!.ui.settings.getSettingValue('Comfy.ColorPalette') }) } } -type FolderStructure = { - [key: string]: FolderStructure | string -} - type KeysOfType = { [K in keyof T]: T[K] extends Match ? K : never }[keyof T] @@ -125,7 +128,7 @@ class ConfirmDialog { async click(locator: KeysOfType) { const loc = this[locator] - await expect(loc).toBeVisible() + await loc.waitFor({ state: 'visible' }) await loc.click() // Wait for the dialog mask to disappear after confirming @@ -137,7 +140,9 @@ class ConfirmDialog { // Wait for workflow service to finish if it's busy await this.page.waitForFunction( - () => window['app']?.extensionManager?.workflow?.isBusy === false, + () => + (window.app?.extensionManager as WorkspaceStore | undefined)?.workflow + ?.isBusy === false, undefined, { timeout: 3000 } ) @@ -159,9 +164,6 @@ export class ComfyPage { // Inputs public readonly workflowUploadInput: Locator - // Toasts - public readonly visibleToasts: Locator - // Components public readonly searchBox: ComfyNodeSearchBox public readonly menu: ComfyMenu @@ -170,6 +172,19 @@ export class ComfyPage { public readonly settingDialog: SettingDialog public readonly confirmDialog: ConfirmDialog public readonly vueNodes: VueNodeHelpers + public readonly debug: DebugHelper + public readonly subgraph: SubgraphHelper + public readonly canvasOps: CanvasHelper + public readonly nodeOps: NodeOperationsHelper + public readonly settings: SettingsHelper + public readonly keyboard: KeyboardHelper + public readonly clipboard: ClipboardHelper + public readonly workflow: WorkflowHelper + public readonly contextMenu: ContextMenu + public readonly toast: ToastHelper + public readonly dragDrop: DragDropHelper + public readonly command: CommandHelper + public readonly bottomPanel: BottomPanel /** Worker index to test user ID */ public readonly userIds: string[] = [] @@ -190,10 +205,9 @@ export class ComfyPage { this.resetViewButton = page.getByRole('button', { name: 'Reset View' }) this.queueButton = page.getByRole('button', { name: 'Queue Prompt' }) this.runButton = page - .getByTestId('queue-button') + .getByTestId(TestIds.topbar.queueButton) .getByRole('button', { name: 'Run' }) this.workflowUploadInput = page.locator('#comfy-file-input') - this.visibleToasts = page.locator('.p-toast-message:visible') this.searchBox = new ComfyNodeSearchBox(page) this.menu = new ComfyMenu(page) @@ -202,62 +216,23 @@ export class ComfyPage { this.settingDialog = new SettingDialog(page, this) this.confirmDialog = new ConfirmDialog(page) 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) + this.nodeOps = new NodeOperationsHelper(this) + this.settings = new SettingsHelper(page) + this.keyboard = new KeyboardHelper(page, this.canvas) + this.clipboard = new ClipboardHelper(this.keyboard) + this.workflow = new WorkflowHelper(this) + this.contextMenu = new ContextMenu(page) + this.toast = new ToastHelper(page) + this.dragDrop = new DragDropHelper(page, this.assetPath.bind(this)) + this.command = new CommandHelper(page) + this.bottomPanel = new BottomPanel(page) } - convertLeafToContent(structure: FolderStructure): FolderStructure { - const result: FolderStructure = {} - - for (const [key, value] of Object.entries(structure)) { - if (typeof value === 'string') { - const filePath = this.assetPath(value) - result[key] = fs.readFileSync(filePath, 'utf-8') - } else { - result[key] = this.convertLeafToContent(value) - } - } - - return result - } - - async getGraphNodesCount(): Promise { - return await this.page.evaluate(() => { - return window['app']?.graph?.nodes?.length || 0 - }) - } - - async getSelectedGraphNodesCount(): Promise { - return await this.page.evaluate(() => { - return ( - window['app']?.graph?.nodes?.filter( - (node: any) => node.is_selected === true - ).length || 0 - ) - }) - } - - async setupWorkflowsDirectory(structure: FolderStructure) { - const resp = await this.request.post( - `${this.url}/api/devtools/setup_folder_structure`, - { - data: { - tree_structure: this.convertLeafToContent(structure), - base_path: `user/${this.id}/workflows` - } - } - ) - - if (resp.status() !== 200) { - throw new Error( - `Failed to setup workflows directory: ${await resp.text()}` - ) - } - - await this.page.evaluate(async () => { - await window['app'].extensionManager.workflow.syncWorkflows() - }) - - // Wait for Vue to re-render the workflow list - await this.nextFrame() + get visibleToasts() { + return this.toast.visibleToasts } async setupUser(username: string) { @@ -285,7 +260,7 @@ export class ComfyPage { return await resp.json() } - async setupSettings(settings: Record) { + async setupSettings(settings: Record) { const resp = await this.request.post( `${this.url}/api/devtools/set_settings`, { @@ -338,9 +313,9 @@ export class ComfyPage { await this.page.waitForFunction(() => document.fonts.ready) await this.page.waitForFunction( () => - // window['app'] => GraphCanvas ready - // window['app'].extensionManager => GraphView ready - window['app'] && window['app'].extensionManager + // window.app => GraphCanvas ready + // window.app.extensionManager => GraphView ready + window.app && window.app.extensionManager ) await this.page.waitForSelector('.p-blockui-mask', { state: 'hidden' }) await this.nextFrame() @@ -350,79 +325,6 @@ export class ComfyPage { return `./browser_tests/assets/${fileName}` } - async executeCommand(commandId: string) { - await this.page.evaluate((id: string) => { - return window['app'].extensionManager.command.execute(id) - }, commandId) - } - - async registerCommand( - commandId: string, - command: (() => void) | (() => Promise) - ) { - 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) { - 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() } - ) - } - - async setSetting(settingId: string, settingValue: any) { - return await this.page.evaluate( - async ({ id, value }) => { - await window['app'].extensionManager.setting.set(id, value) - }, - { id: settingId, value: settingValue } - ) - } - - async getSetting(settingId: string) { - return await this.page.evaluate(async (id) => { - return await window['app'].extensionManager.setting.get(id) - }, settingId) - } - async goto() { await this.page.goto(this.url) } @@ -437,37 +339,6 @@ export class ComfyPage { return new Promise((resolve) => setTimeout(resolve, ms)) } - async loadWorkflow(workflowName: string) { - await this.workflowUploadInput.setInputFiles( - this.assetPath(`${workflowName}.json`) - ) - await this.nextFrame() - } - - async deleteWorkflow( - workflowName: string, - whenMissing: 'ignoreMissing' | 'throwIfMissing' = 'ignoreMissing' - ) { - // Open workflows tab - const { workflowsTab } = this.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.clickContextMenuItem('Delete') - await this.confirmDialog.delete.click() - - // Clear toast & close tab - await this.closeToasts(1) - await workflowsTab.close() - } - /** * Attach a screenshot to the test report. * By default, screenshots are only taken in non-CI environments. @@ -494,1080 +365,22 @@ export class ComfyPage { }) } - 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() - } - - async getToastErrorCount() { - return await this.page - .locator('.p-toast-message.p-toast-message-error') - .count() - } - - async getVisibleToastCount() { - return await this.visibleToasts.count() - } - - async closeToasts(requireCount = 0) { - if (requireCount) await expect(this.visibleToasts).toHaveCount(requireCount) - - // Clear all toasts - const toastCloseButtons = await this.page - .locator('.p-toast-close-button') - .all() - for (const button of toastCloseButtons) { - await button.click() - } - await expect(this.visibleToasts).toHaveCount(0) - } - - async clickTextEncodeNode1() { - await this.canvas.click({ - position: { - x: 618, - y: 191 - } - }) - await this.nextFrame() - } - - async clickTextEncodeNodeToggler() { - await this.canvas.click({ - position: { - x: 430, - y: 171 - } - }) - await this.nextFrame() - } - - async clickTextEncodeNode2() { - await this.canvas.click({ - position: { - x: 622, - y: 400 - } - }) - await this.nextFrame() - } - - async clickEmptySpace() { - await this.canvas.click({ - position: { - x: 35, - y: 31 - } - }) - await this.nextFrame() - } - - async dragAndDrop(source: Position, target: Position) { - await this.page.mouse.move(source.x, source.y) - await this.page.mouse.down() - await this.page.mouse.move(target.x, target.y, { steps: 100 }) - await this.page.mouse.up() - await this.nextFrame() - } - - async dragAndDropExternalResource( - options: { - fileName?: string - url?: string - dropPosition?: Position - waitForUpload?: boolean - } = {} - ) { - 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 } - - // Dropping a file from the filesystem - if (fileName) { - const filePath = this.assetPath(fileName) - const buffer = fs.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)] - } - - // Dropping a URL (e.g., dropping image across browser tabs in Firefox) - if (url) evaluateParams.url = url - - // Set up response waiter for file uploads before triggering the drop - const uploadResponsePromise = waitForUpload - ? this.page.waitForResponse( - (resp) => resp.url().includes('/upload/') && resp.status() === 200, - { timeout: 10000 } - ) - : null - - // Execute the drag and drop in the browser - await this.page.evaluate(async (params) => { - const dataTransfer = new DataTransfer() - - // Add file if provided - if (params.buffer && params.fileName && params.fileType) { - const file = new File( - [new Uint8Array(params.buffer)], - params.fileName, - { - type: params.fileType - } - ) - dataTransfer.items.add(file) - } - - // Add URL data if provided - 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) { - console.error('No element found at drop position:', params.dropPosition) - return { success: false, error: 'No element at position' } - } - - 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) - - // Wait for file upload to complete - if (uploadResponsePromise) { - await uploadResponsePromise - } - - await this.nextFrame() - } - - async dragAndDropFile( - fileName: string, - options: { dropPosition?: Position; waitForUpload?: boolean } = {} - ) { - return this.dragAndDropExternalResource({ fileName, ...options }) - } - - async dragAndDropURL(url: string, options: { dropPosition?: Position } = {}) { - return this.dragAndDropExternalResource({ url, ...options }) - } - - async dragNode2() { - await this.dragAndDrop({ x: 622, y: 400 }, { x: 622, y: 300 }) - await this.nextFrame() - } - - // Default graph positions - get clipTextEncodeNode1InputSlot(): Position { - return { x: 427, y: 198 } - } - - get clipTextEncodeNode2InputSlot(): Position { - return { x: 422, y: 402 } - } - - // A point on input edge. - get clipTextEncodeNode2InputLinkPath(): Position { - return { - x: 395, - y: 422 - } - } - - get loadCheckpointNodeClipOutputSlot(): Position { - return { x: 332, y: 509 } - } - - get emptySpace(): Position { - return { x: 427, y: 98 } - } - - get promptDialogInput() { - return this.page.locator('.p-dialog-content input[type="text"]') - } - - async fillPromptDialog(value: string) { - await this.promptDialogInput.fill(value) - await this.page.keyboard.press('Enter') - await this.promptDialogInput.waitFor({ state: 'hidden' }) - await this.nextFrame() - } - - async disconnectEdge() { - await this.dragAndDrop(this.clipTextEncodeNode1InputSlot, this.emptySpace) - } - - async connectEdge( - options: { - reverse?: boolean - } = {} - ) { - const { reverse = false } = options - const start = reverse - ? this.clipTextEncodeNode1InputSlot - : this.loadCheckpointNodeClipOutputSlot - const end = reverse - ? this.loadCheckpointNodeClipOutputSlot - : this.clipTextEncodeNode1InputSlot - - await this.dragAndDrop(start, end) - } - - async adjustWidgetValue() { - // Adjust Empty Latent Image's width input. - const page = this.page - await page.locator('#graph-canvas').click({ - position: { - x: 724, - y: 645 - } - }) - const dialogInput = page.locator('.graphdialog input[type="text"]') - await dialogInput.click() - await dialogInput.fill('128') - await dialogInput.press('Enter') - await this.nextFrame() - } - - 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() - } - - 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() - } - - 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() - } - - async rightClickCanvas(x: number = 10, y: number = 10) { - await this.page.mouse.click(x, y, { button: 'right' }) - await this.nextFrame() - } - - async clickContextMenuItem(name: string): Promise { - await this.page.getByRole('menuitem', { name }).click() - await this.nextFrame() - } - - /** - * Clicks on a litegraph context menu item (uses .litemenu-entry selector). - * Use this for canvas/node context menus, not PrimeVue menus. - */ - async clickLitegraphContextMenuItem(name: string): Promise { - await this.page.locator(`.litemenu-entry:has-text("${name}")`).click() - await this.nextFrame() - } - - /** - * 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 - */ - private async interactWithSubgraphSlot( - slotType: 'input' | 'output', - action: 'rightClick' | 'doubleClick', - slotName?: string - ): Promise { - 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' - ) - } - - // Get the appropriate node and slots - const node = - slotType === 'input' - ? currentGraph.inputNode - : currentGraph.outputNode - const slots = - slotType === 'input' ? currentGraph.inputs : currentGraph.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, - app.canvas.pointer, - app.canvas.linkConnector - ) - } - - // Wait briefly for menu to appear - await new Promise((resolve) => setTimeout(resolve, 100)) - - // Check if context menu appeared - const menuExists = document.querySelector('.litemenu-entry') - if (menuExists) { - 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, - app.canvas.pointer, - app.canvas.linkConnector - ) - - // Trigger double-click - if (app.canvas.pointer.onDoubleClick) { - app.canvas.pointer.onDoubleClick(event) - } - } - - // Wait briefly for dialog to appear - await new Promise((resolve) => setTimeout(resolve, 200)) - - 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.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 rightClickSubgraphInputSlot(inputName?: string): Promise { - 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 rightClickSubgraphInputSlot 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 rightClickSubgraphOutputSlot(outputName?: string): Promise { - 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 doubleClickSubgraphInputSlot(inputName?: string): Promise { - 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 doubleClickSubgraphOutputSlot(outputName?: string): Promise { - return this.interactWithSubgraphSlot('output', 'doubleClick', outputName) - } - - /** - * Get a reference to a subgraph input slot - */ - async getSubgraphInputSlot( - slotName?: string - ): Promise { - return new SubgraphSlotReference('input', slotName || '', this) - } - - /** - * Get a reference to a subgraph output slot - */ - async getSubgraphOutputSlot( - slotName?: string - ): Promise { - return new SubgraphSlotReference('output', slotName || '', this) - } - - /** - * Connect a regular node output to a subgraph input. - * This creates a new input slot on the subgraph if targetInputName is not provided. - */ - async connectToSubgraphInput( - sourceNode: NodeReference, - sourceSlotIndex: number, - targetInputName?: string - ): Promise { - const sourceSlot = await sourceNode.getOutput(sourceSlotIndex) - const targetSlot = await this.getSubgraphInputSlot(targetInputName) - - const targetPosition = targetInputName - ? await targetSlot.getPosition() // Connect to existing slot - : await targetSlot.getOpenSlotPosition() // Create new slot - - await this.dragAndDrop(await sourceSlot.getPosition(), targetPosition) - await this.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 connectFromSubgraphInput( - targetNode: NodeReference, - targetSlotIndex: number, - sourceInputName?: string - ): Promise { - const sourceSlot = await this.getSubgraphInputSlot(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.dragAndDrop(sourcePosition, targetPosition) - await this.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 connectToSubgraphOutput( - sourceNode: NodeReference, - sourceSlotIndex: number, - targetOutputName?: string - ): Promise { - const sourceSlot = await sourceNode.getOutput(sourceSlotIndex) - const targetSlot = await this.getSubgraphOutputSlot(targetOutputName) - - const targetPosition = targetOutputName - ? await targetSlot.getPosition() // Connect to existing slot - : await targetSlot.getOpenSlotPosition() // Create new slot - - await this.dragAndDrop(await sourceSlot.getPosition(), targetPosition) - await this.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 connectFromSubgraphOutput( - targetNode: NodeReference, - targetSlotIndex: number, - sourceOutputName?: string - ): Promise { - const sourceSlot = await this.getSubgraphOutputSlot(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.dragAndDrop(sourcePosition, await targetSlot.getPosition()) - await this.nextFrame() - } - - /** - * Add a visual marker at a position for debugging - */ - async debugAddMarker( - position: Position, - id: string = 'debug-marker' - ): Promise { - await this.page.evaluate( - ([pos, markerId]) => { - // Remove existing marker if present - const existing = document.getElementById(markerId) - if (existing) existing.remove() - - // Create marker - 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 - ) - } - - /** - * Remove debug markers - */ - async debugRemoveMarkers(): Promise { - await this.page.evaluate(() => { - document - .querySelectorAll('[id^="debug-marker"]') - .forEach((el) => el.remove()) - }) - } - - /** - * Take a screenshot and attach it to the test report for debugging - * This is a convenience method that combines screenshot capture and test attachment - * - * @param testInfo The Playwright TestInfo object (from test parameters) - * @param name Name for the attachment - * @param options Optional screenshot options (defaults to page screenshot) - */ - async debugAttachScreenshot( - testInfo: any, - name: string, - options?: { - fullPage?: boolean - element?: 'canvas' | 'page' - markers?: Array<{ position: Position; id?: string }> - } - ): Promise { - // Add markers if requested - if (options?.markers) { - for (const marker of options.markers) { - await this.debugAddMarker(marker.position, marker.id) - } - } - - // Take screenshot - default to page if not specified - 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() - } - - // Attach to test report - await testInfo.attach(name, { - body: screenshot, - contentType: 'image/png' - }) - - // Clean up markers if we added any - if (options?.markers) { - await this.debugRemoveMarkers() - } - } - - async doubleClickCanvas() { - await this.page.mouse.dblclick(10, 10, { delay: 5 }) - await this.nextFrame() - } - - /** - * Capture the canvas as a PNG and save it for debugging - */ - async debugSaveCanvasScreenshot(filename: string): Promise { - await this.page.evaluate(async (filename) => { - const canvas = document.getElementById( - 'graph-canvas' - ) as HTMLCanvasElement - if (!canvas) { - throw new Error('Canvas not found') - } - - // Convert canvas to blob - return new Promise((resolve) => { - canvas.toBlob(async (blob) => { - if (!blob) { - throw new Error('Failed to create blob from canvas') - } - - // Create a download link and trigger it - 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) - } - - /** - * Capture canvas as base64 data URL for inspection - */ - async debugGetCanvasDataURL(): Promise { - 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') - }) - } - - /** - * Create an overlay div with the canvas image for easier Playwright screenshot - */ - async debugShowCanvasOverlay(): Promise { - await this.page.evaluate(() => { - const canvas = document.getElementById( - 'graph-canvas' - ) as HTMLCanvasElement - if (!canvas) { - throw new Error('Canvas not found') - } - - // Remove existing overlay if present - const existingOverlay = document.getElementById('debug-canvas-overlay') - if (existingOverlay) { - existingOverlay.remove() - } - - // Create overlay div - 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' - - // Create image from canvas - 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) - }) - } - - /** - * Remove the debug canvas overlay - */ - async debugHideCanvasOverlay(): Promise { - await this.page.evaluate(() => { - const overlay = document.getElementById('debug-canvas-overlay') - if (overlay) { - overlay.remove() - } - }) - } - - async clickEmptyLatentNode() { - await this.canvas.click({ - position: { - x: 724, - y: 625 - } - }) - await this.page.mouse.move(10, 10) - await this.nextFrame() - } - - async rightClickEmptyLatentNode() { - await this.canvas.click({ - position: { - x: 724, - y: 645 - }, - button: 'right' - }) - await this.page.mouse.move(10, 10) - await this.nextFrame() - } - - async selectNodes(nodeTitles: string[]) { - await this.page.keyboard.down('Control') - for (const nodeTitle of nodeTitles) { - const nodes = await this.getNodeRefsByTitle(nodeTitle) - for (const node of nodes) { - await node.click('title') - } - } - await this.page.keyboard.up('Control') - await this.nextFrame() - } - - async select2Nodes() { - // Select 2 CLIP nodes. - await this.page.keyboard.down('Control') - await this.clickTextEncodeNode1() - await this.clickTextEncodeNode2() - await this.page.keyboard.up('Control') - await this.nextFrame() - } - - async ctrlSend(keyToPress: string, locator: Locator | null = this.canvas) { - const target = locator ?? this.page.keyboard - await target.press(`Control+${keyToPress}`) - await this.nextFrame() - } - - async ctrlA(locator?: Locator | null) { - await this.ctrlSend('KeyA', locator) - } - - async ctrlB(locator?: Locator | null) { - await this.ctrlSend('KeyB', locator) - } - - async ctrlC(locator?: Locator | null) { - await this.ctrlSend('KeyC', locator) - } - - async ctrlV(locator?: Locator | null) { - await this.ctrlSend('KeyV', locator) - } - - async ctrlZ(locator?: Locator | null) { - await this.ctrlSend('KeyZ', locator) - } - - async ctrlY(locator?: Locator | null) { - await this.ctrlSend('KeyY', locator) - } - - async ctrlArrowUp(locator?: Locator | null) { - await this.ctrlSend('ArrowUp', locator) - } - - async ctrlArrowDown(locator?: Locator | null) { - await this.ctrlSend('ArrowDown', locator) - } - async closeMenu() { await this.page.click('button.comfy-close-menu-btn') await this.nextFrame() } - async closeDialog() { - await this.page.locator('.p-dialog-close-button').click({ force: true }) - await expect(this.page.locator('.p-dialog')).toBeHidden() - } - - async resizeNode( - nodePos: Position, - nodeSize: Size, - ratioX: number, - ratioY: number, - revertAfter: boolean = false - ) { - 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.dragAndDrop( - { x: bottomRight.x - 2, y: bottomRight.y - 1 }, - target - ) - await this.nextFrame() - if (revertAfter) { - await this.dragAndDrop({ x: target.x - 2, y: target.y - 1 }, bottomRight) - await this.nextFrame() - } - } - - async resizeKsamplerNode( - percentX: number, - percentY: number, - revertAfter: boolean = false - ) { - const ksamplerPos = { - x: 863, - y: 156 - } - const ksamplerSize = { - width: 315, - height: 292 - } - return this.resizeNode( - ksamplerPos, - ksamplerSize, - percentX, - percentY, - revertAfter - ) - } - - async resizeLoadCheckpointNode( - percentX: number, - percentY: number, - revertAfter: boolean = false - ) { - const loadCheckpointPos = { - x: 26, - y: 444 - } - const loadCheckpointSize = { - width: 315, - height: 127 - } - return this.resizeNode( - loadCheckpointPos, - loadCheckpointSize, - percentX, - percentY, - revertAfter - ) - } - - async resizeEmptyLatentNode( - percentX: number, - percentY: number, - revertAfter: boolean = false - ) { - const emptyLatentPos = { - x: 473, - y: 579 - } - const emptyLatentSize = { - width: 315, - height: 136 - } - return this.resizeNode( - emptyLatentPos, - emptyLatentSize, - percentX, - percentY, - revertAfter - ) - } - async clickDialogButton(prompt: string, buttonText: string = 'Yes') { const modal = this.page.locator( `.comfy-modal-content:has-text("${prompt}")` ) - await expect(modal).toBeVisible() + await modal.waitFor({ state: 'visible' }) await modal .locator('.comfyui-button', { hasText: buttonText }) .click() - await expect(modal).toBeHidden() - } - - async convertAllNodesToGroupNode(groupNodeName: string) { - await this.canvas.press('Control+a') - const node = await this.getFirstNodeRef() - await node!.clickContextMenuOption('Convert to Group Node') - await this.fillPromptDialog(groupNodeName) - await this.nextFrame() - } - - async convertOffsetToCanvas(pos: [number, number]) { - return this.page.evaluate((pos) => { - return window['app'].canvas.ds.convertOffsetToCanvas(pos) - }, pos) + await modal.waitFor({ state: 'hidden' }) } /** Get number of DOM widgets on the canvas. */ @@ -1575,142 +388,12 @@ export class ComfyPage { return await this.page.locator('.dom-widget').count() } - async getNodeRefById(id: NodeId) { - return new NodeReference(id, this) - } - async getNodes(): Promise { - return await this.page.evaluate(() => { - return window['app'].graph.nodes - }) - } - async waitForGraphNodes(count: number) { - await this.page.waitForFunction((count) => { - return window['app']?.canvas.graph?.nodes?.length === count - }, count) - } - async getNodeRefsByType( - type: string, - includeSubgraph: boolean = false - ): Promise { - 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 { - 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 getFirstNodeRef(): Promise { - const id = await this.page.evaluate(() => { - return window['app'].graph.nodes[0]?.id - }) - if (!id) return null - return this.getNodeRefById(id) - } - async moveMouseToEmptyArea() { - await this.page.mouse.move(10, 10) - } - async getUndoQueueSize() { - return this.page.evaluate(() => { - const workflow = (window['app'].extensionManager as WorkspaceStore) - .workflow.activeWorkflow - return workflow?.changeTracker.undoQueue.length - }) - } - async getRedoQueueSize() { - return this.page.evaluate(() => { - const workflow = (window['app'].extensionManager as WorkspaceStore) - .workflow.activeWorkflow - return workflow?.changeTracker.redoQueue.length - }) - } - async isCurrentWorkflowModified() { - return this.page.evaluate(() => { - return (window['app'].extensionManager as WorkspaceStore).workflow - .activeWorkflow?.isModified - }) - } - async getExportedWorkflow({ api = false }: { api?: boolean } = {}) { - return this.page.evaluate(async (api) => { - return (await window['app'].graphToPrompt())[api ? 'output' : 'workflow'] - }, api) - } async setFocusMode(focusMode: boolean) { await this.page.evaluate((focusMode) => { - window['app'].extensionManager.focusMode = focusMode + ;(window.app!.extensionManager as WorkspaceStore).focusMode = focusMode }, focusMode) await this.nextFrame() } - - /** - * Get the position of a group by title. - * @param title The title of the group to find - * @returns The group's canvas position - * @throws Error if group not found - */ - async getGroupPosition(title: string): Promise { - 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 - } - - /** - * Drag a group by its title. - * @param options.name The title of the group to drag - * @param options.deltaX Horizontal drag distance in screen pixels - * @param options.deltaY Vertical drag distance in screen pixels - */ - async dragGroup(options: { - name: string - deltaX: number - deltaY: number - }): Promise { - 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 - // Position in the title area of the group - 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 - }) - } } export const testComfySnapToGridGridSize = 50 @@ -1766,6 +449,7 @@ const makeMatcher = function ( type: string ) { return async function ( + this: ExpectMatcherState, node: NodeReference, options?: { timeout?: number; intervals?: number[] } ) { diff --git a/browser_tests/fixtures/VueNodeHelpers.ts b/browser_tests/fixtures/VueNodeHelpers.ts index 3c11cfda2..64d76481c 100644 --- a/browser_tests/fixtures/VueNodeHelpers.ts +++ b/browser_tests/fixtures/VueNodeHelpers.ts @@ -3,6 +3,7 @@ */ import type { Locator, Page } from '@playwright/test' +import { TestIds } from './selectors' import { VueNodeFixture } from './utils/vueNodeFixtures' export class VueNodeHelpers { @@ -148,9 +149,9 @@ export class VueNodeHelpers { * Get a specific widget by node title and widget name */ getWidgetByName(nodeTitle: string, widgetName: string): Locator { - return this.getNodeByTitle(nodeTitle).locator( - `_vue=[widget.name="${widgetName}"]` - ) + return this.getNodeByTitle(nodeTitle).getByLabel(widgetName, { + exact: true + }) } /** @@ -159,8 +160,8 @@ export class VueNodeHelpers { getInputNumberControls(widget: Locator) { return { input: widget.locator('input'), - decrementButton: widget.getByTestId('decrement'), - incrementButton: widget.getByTestId('increment') + decrementButton: widget.getByTestId(TestIds.widgets.decrement), + incrementButton: widget.getByTestId(TestIds.widgets.increment) } } @@ -170,7 +171,7 @@ export class VueNodeHelpers { */ async enterSubgraph(nodeId?: string): Promise { 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() } } diff --git a/browser_tests/fixtures/components/BaseDialog.ts b/browser_tests/fixtures/components/BaseDialog.ts new file mode 100644 index 000000000..62b4ff64c --- /dev/null +++ b/browser_tests/fixtures/components/BaseDialog.ts @@ -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 { + return this.root.isVisible() + } + + async waitForVisible(): Promise { + await this.root.waitFor({ state: 'visible' }) + } + + async waitForHidden(): Promise { + await this.root.waitFor({ state: 'hidden' }) + } + + async close(): Promise { + await this.closeButton.click({ force: true }) + await this.waitForHidden() + } +} diff --git a/browser_tests/fixtures/components/BottomPanel.ts b/browser_tests/fixtures/components/BottomPanel.ts new file mode 100644 index 000000000..85ef2694a --- /dev/null +++ b/browser_tests/fixtures/components/BottomPanel.ts @@ -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) + } +} diff --git a/browser_tests/fixtures/components/ComfyNodeSearchBox.ts b/browser_tests/fixtures/components/ComfyNodeSearchBox.ts index 59a33e254..f2a96fa8f 100644 --- a/browser_tests/fixtures/components/ComfyNodeSearchBox.ts +++ b/browser_tests/fixtures/components/ComfyNodeSearchBox.ts @@ -30,7 +30,7 @@ export class ComfyNodeSearchFilterSelectionPanel { async addFilter(filterValue: string, filterType: string) { await this.selectFilterType(filterType) await this.selectFilterValue(filterValue) - await this.page.locator('button:has-text("Add")').click() + await this.page.getByRole('button', { name: 'Add', exact: true }).click() } } diff --git a/browser_tests/fixtures/components/ContextMenu.ts b/browser_tests/fixtures/components/ContextMenu.ts new file mode 100644 index 000000000..c502ccdd5 --- /dev/null +++ b/browser_tests/fixtures/components/ContextMenu.ts @@ -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 { + await this.page.getByRole('menuitem', { name }).click() + } + + async clickLitegraphMenuItem(name: string): Promise { + await this.page.locator(`.litemenu-entry:has-text("${name}")`).click() + } + + async isVisible(): Promise { + const primeVueVisible = await this.primeVueMenu + .isVisible() + .catch(() => false) + const litegraphVisible = await this.litegraphMenu + .isVisible() + .catch(() => false) + return primeVueVisible || litegraphVisible + } + + async waitForHidden(): Promise { + 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') + ]) + } +} diff --git a/browser_tests/fixtures/components/SettingDialog.ts b/browser_tests/fixtures/components/SettingDialog.ts index e9040a3a9..4587d85f3 100644 --- a/browser_tests/fixtures/components/SettingDialog.ts +++ b/browser_tests/fixtures/components/SettingDialog.ts @@ -1,20 +1,20 @@ import type { Page } from '@playwright/test' import type { ComfyPage } from '../ComfyPage' +import { TestIds } from '../selectors' +import { BaseDialog } from './BaseDialog' -export class SettingDialog { +export class SettingDialog extends BaseDialog { constructor( - public readonly page: Page, + page: Page, public readonly comfyPage: ComfyPage - ) {} - - get root() { - return this.page.locator('div.settings-container') + ) { + super(page, TestIds.dialogs.settings) } async open() { - await this.comfyPage.executeCommand('Comfy.ShowSettingsDialog') - await this.page.waitForSelector('div.settings-container') + await this.comfyPage.command.executeCommand('Comfy.ShowSettingsDialog') + await this.waitForVisible() } /** @@ -41,8 +41,9 @@ export class SettingDialog { } async goToAboutPanel() { - const aboutButton = this.page.locator('li[aria-label="About"]') - await aboutButton.click() - await this.page.waitForSelector('div.about-container') + await this.page.getByTestId(TestIds.dialogs.settingsTabAbout).click() + await this.page + .getByTestId(TestIds.dialogs.about) + .waitFor({ state: 'visible' }) } } diff --git a/browser_tests/fixtures/components/SidebarTab.ts b/browser_tests/fixtures/components/SidebarTab.ts index 3254e27c8..02da0b043 100644 --- a/browser_tests/fixtures/components/SidebarTab.ts +++ b/browser_tests/fixtures/components/SidebarTab.ts @@ -1,5 +1,8 @@ import type { Locator, Page } from '@playwright/test' +import type { WorkspaceStore } from '../../types/globals' +import { TestIds } from '../selectors' + class SidebarTab { constructor( public readonly page: Page, @@ -31,16 +34,16 @@ class SidebarTab { } export class NodeLibrarySidebarTab extends SidebarTab { - constructor(public readonly page: Page) { + constructor(public override readonly page: Page) { super(page, 'node-library') } get nodeLibrarySearchBoxInput() { - return this.page.locator('.node-lib-search-box input[type="text"]') + return this.page.getByPlaceholder('Search Nodes...') } get nodeLibraryTree() { - return this.page.locator('.node-lib-tree-explorer') + return this.page.getByTestId(TestIds.sidebar.nodeLibrary) } get nodePreview() { @@ -55,12 +58,12 @@ export class NodeLibrarySidebarTab extends SidebarTab { return this.tabContainer.locator('.new-folder-button') } - async open() { + override async open() { await super.open() await this.nodeLibraryTree.waitFor({ state: 'visible' }) } - async close() { + override async close() { if (!this.tabButton.isVisible()) { return } @@ -69,30 +72,40 @@ export class NodeLibrarySidebarTab extends SidebarTab { await this.nodeLibraryTree.waitFor({ state: 'hidden' }) } - folderSelector(folderName: string) { - return `.p-tree-node-content:has(> .tree-explorer-node-label:has(.tree-folder .node-label:has-text("${folderName}")))` - } - getFolder(folderName: string) { - return this.page.locator(this.folderSelector(folderName)) - } - - nodeSelector(nodeName: string) { - return `.p-tree-node-content:has(> .tree-explorer-node-label:has(.tree-leaf .node-label:has-text("${nodeName}")))` + return this.page.locator( + `[data-testid="node-tree-folder"][data-folder-name="${folderName}"]` + ) } getNode(nodeName: string) { - return this.page.locator(this.nodeSelector(nodeName)) + return this.page.locator( + `[data-testid="node-tree-leaf"][data-node-name="${nodeName}"]` + ) + } + + nodeSelector(nodeName: string): string { + return `[data-testid="node-tree-leaf"][data-node-name="${nodeName}"]` + } + + folderSelector(folderName: string): string { + return `[data-testid="node-tree-folder"][data-folder-name="${folderName}"]` + } + + getNodeInFolder(nodeName: string, folderName: string) { + return this.getFolder(folderName) + .locator('xpath=ancestor::li') + .locator(`[data-testid="node-tree-leaf"][data-node-name="${nodeName}"]`) } } export class WorkflowsSidebarTab extends SidebarTab { - constructor(public readonly page: Page) { + constructor(public override readonly page: Page) { super(page, 'workflows') } get root() { - return this.page.locator('.workflows-sidebar-tab') + return this.page.getByTestId(TestIds.sidebar.workflows) } async getOpenedWorkflowNames() { @@ -140,7 +153,9 @@ export class WorkflowsSidebarTab extends SidebarTab { // Wait for workflow service to finish renaming await this.page.waitForFunction( - () => !window['app']?.extensionManager?.workflow?.isBusy, + () => + !(window.app?.extensionManager as WorkspaceStore | undefined)?.workflow + ?.isBusy, undefined, { timeout: 3000 } ) diff --git a/browser_tests/fixtures/components/Topbar.ts b/browser_tests/fixtures/components/Topbar.ts index c5e7c8155..e91311c7b 100644 --- a/browser_tests/fixtures/components/Topbar.ts +++ b/browser_tests/fixtures/components/Topbar.ts @@ -1,5 +1,6 @@ import type { Locator, Page } from '@playwright/test' -import { expect } from '@playwright/test' + +import type { WorkspaceStore } from '../../types/globals' export class Topbar { private readonly menuLocator: Locator @@ -57,7 +58,7 @@ export class Topbar { async closeWorkflowTab(tabName: string) { const tab = this.getWorkflowTab(tabName) - await tab.locator('.close-button').click({ force: true }) + await tab.getByRole('button', { name: 'Close' }).click({ force: true }) } getSaveDialog(): Locator { @@ -86,7 +87,7 @@ export class Topbar { // Wait for workflow service to finish saving await this.page.waitForFunction( - () => !window['app'].extensionManager.workflow.isBusy, + () => !(window.app!.extensionManager as WorkspaceStore).workflow.isBusy, undefined, { timeout: 3000 } ) @@ -122,7 +123,7 @@ export class Topbar { */ async closeTopbarMenu() { await this.page.locator('body').click({ position: { x: 300, y: 10 } }) - await expect(this.menuLocator).not.toBeVisible() + await this.menuLocator.waitFor({ state: 'hidden' }) } /** diff --git a/browser_tests/fixtures/constants/defaultGraphPositions.ts b/browser_tests/fixtures/constants/defaultGraphPositions.ts new file mode 100644 index 000000000..576aecad6 --- /dev/null +++ b/browser_tests/fixtures/constants/defaultGraphPositions.ts @@ -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 } } +} diff --git a/browser_tests/fixtures/constants/types.ts b/browser_tests/fixtures/constants/types.ts new file mode 100644 index 000000000..57aece7a2 --- /dev/null +++ b/browser_tests/fixtures/constants/types.ts @@ -0,0 +1,9 @@ +export interface Position { + x: number + y: number +} + +export interface Size { + width: number + height: number +} diff --git a/browser_tests/fixtures/helpers/CanvasHelper.ts b/browser_tests/fixtures/helpers/CanvasHelper.ts new file mode 100644 index 000000000..dfd730c8e --- /dev/null +++ b/browser_tests/fixtures/helpers/CanvasHelper.ts @@ -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 { + await this.page.evaluate(() => { + return new Promise(requestAnimationFrame) + }) + } + + async resetView(): Promise { + 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 { + 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 { + 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 { + 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 { + await this.page.mouse.click(x, y, { button: 'right' }) + await this.nextFrame() + } + + async doubleClick(): Promise { + await this.page.mouse.dblclick(10, 10, { delay: 5 }) + await this.nextFrame() + } + + async click(position: Position): Promise { + await this.canvas.click({ position }) + await this.nextFrame() + } + + async clickEmptySpace(): Promise { + await this.canvas.click({ position: DefaultGraphPositions.emptySpaceClick }) + await this.nextFrame() + } + + async dragAndDrop(source: Position, target: Position): Promise { + 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 { + await this.page.mouse.move(10, 10) + } + + async getScale(): Promise { + return this.page.evaluate(() => { + return window.app!.canvas.ds.scale + }) + } + + async setScale(scale: number): Promise { + 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 { + 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 { + 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 { + 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 { + await this.dragAndDrop( + DefaultGraphPositions.clipTextEncodeNode1InputSlot, + DefaultGraphPositions.emptySpace + ) + } + + async connectEdge(options: { reverse?: boolean } = {}): Promise { + const { reverse = false } = options + const start = reverse + ? DefaultGraphPositions.clipTextEncodeNode1InputSlot + : DefaultGraphPositions.loadCheckpointNodeClipOutputSlot + const end = reverse + ? DefaultGraphPositions.loadCheckpointNodeClipOutputSlot + : DefaultGraphPositions.clipTextEncodeNode1InputSlot + + await this.dragAndDrop(start, end) + } +} diff --git a/browser_tests/fixtures/helpers/ClipboardHelper.ts b/browser_tests/fixtures/helpers/ClipboardHelper.ts new file mode 100644 index 000000000..074c3d19f --- /dev/null +++ b/browser_tests/fixtures/helpers/ClipboardHelper.ts @@ -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 { + await this.keyboard.ctrlSend('KeyC', locator ?? null) + } + + async paste(locator?: Locator | null): Promise { + await this.keyboard.ctrlSend('KeyV', locator ?? null) + } +} diff --git a/browser_tests/fixtures/helpers/CommandHelper.ts b/browser_tests/fixtures/helpers/CommandHelper.ts new file mode 100644 index 000000000..966f99b81 --- /dev/null +++ b/browser_tests/fixtures/helpers/CommandHelper.ts @@ -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 { + await this.page.evaluate((id: string) => { + return window.app!.extensionManager.command.execute(id) + }, commandId) + } + + async registerCommand( + commandId: string, + command: (() => void) | (() => Promise) + ): Promise { + // 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 { + // 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() } + ) + } +} diff --git a/browser_tests/fixtures/helpers/DebugHelper.ts b/browser_tests/fixtures/helpers/DebugHelper.ts new file mode 100644 index 000000000..819a876dd --- /dev/null +++ b/browser_tests/fixtures/helpers/DebugHelper.ts @@ -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 { + 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 { + await this.page.evaluate(() => { + document + .querySelectorAll('[id^="debug-marker"]') + .forEach((el) => el.remove()) + }) + } + + async attachScreenshot( + testInfo: TestInfo, + name: string, + options?: DebugScreenshotOptions + ): Promise { + 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 { + 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((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 { + 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 { + 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 { + await this.page.evaluate(() => { + const overlay = document.getElementById('debug-canvas-overlay') + if (overlay) { + overlay.remove() + } + }) + } +} diff --git a/browser_tests/fixtures/helpers/DragDropHelper.ts b/browser_tests/fixtures/helpers/DragDropHelper.ts new file mode 100644 index 000000000..f55c73915 --- /dev/null +++ b/browser_tests/fixtures/helpers/DragDropHelper.ts @@ -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 { + await this.page.evaluate(() => { + return new Promise((resolve) => { + requestAnimationFrame(() => resolve()) + }) + }) + } + + async dragAndDropExternalResource( + options: { + fileName?: string + url?: string + dropPosition?: Position + waitForUpload?: boolean + } = {} + ): Promise { + 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 { + return this.dragAndDropExternalResource({ fileName, ...options }) + } + + async dragAndDropURL( + url: string, + options: { dropPosition?: Position } = {} + ): Promise { + return this.dragAndDropExternalResource({ url, ...options }) + } +} diff --git a/browser_tests/fixtures/helpers/KeyboardHelper.ts b/browser_tests/fixtures/helpers/KeyboardHelper.ts new file mode 100644 index 000000000..b2c30346e --- /dev/null +++ b/browser_tests/fixtures/helpers/KeyboardHelper.ts @@ -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 { + await this.page.evaluate(() => new Promise(requestAnimationFrame)) + } + + async ctrlSend( + keyToPress: string, + locator: Locator | null = this.canvas + ): Promise { + const target = locator ?? this.page.keyboard + await target.press(`Control+${keyToPress}`) + await this.nextFrame() + } + + async selectAll(locator?: Locator | null): Promise { + await this.ctrlSend('KeyA', locator) + } + + async bypass(locator?: Locator | null): Promise { + await this.ctrlSend('KeyB', locator) + } + + async undo(locator?: Locator | null): Promise { + await this.ctrlSend('KeyZ', locator) + } + + async redo(locator?: Locator | null): Promise { + await this.ctrlSend('KeyY', locator) + } + + async moveUp(locator?: Locator | null): Promise { + await this.ctrlSend('ArrowUp', locator) + } + + async moveDown(locator?: Locator | null): Promise { + await this.ctrlSend('ArrowDown', locator) + } +} diff --git a/browser_tests/fixtures/helpers/NodeOperationsHelper.ts b/browser_tests/fixtures/helpers/NodeOperationsHelper.ts new file mode 100644 index 000000000..5f11b473b --- /dev/null +++ b/browser_tests/fixtures/helpers/NodeOperationsHelper.ts @@ -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 { + return await this.page.evaluate(() => { + return window.app?.graph?.nodes?.length || 0 + }) + } + + async getSelectedGraphNodesCount(): Promise { + return await this.page.evaluate(() => { + return ( + window.app?.graph?.nodes?.filter( + (node: LGraphNode) => node.is_selected === true + ).length || 0 + ) + }) + } + + async getNodes(): Promise { + return await this.page.evaluate(() => { + return window.app!.graph.nodes + }) + } + + async waitForGraphNodes(count: number): Promise { + await this.page.waitForFunction((count) => { + return window.app?.canvas.graph?.nodes?.length === count + }, count) + } + + async getFirstNodeRef(): Promise { + 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 { + return new NodeReference(id, this.comfyPage) + } + + async getNodeRefsByType( + type: string, + includeSubgraph: boolean = false + ): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + await this.promptDialogInput.fill(value) + await this.page.keyboard.press('Enter') + await this.promptDialogInput.waitFor({ state: 'hidden' }) + await this.comfyPage.nextFrame() + } + + async dragTextEncodeNode2(): Promise { + await this.comfyPage.canvasOps.dragAndDrop( + DefaultGraphPositions.textEncodeNode2, + { + x: DefaultGraphPositions.textEncodeNode2.x, + y: 300 + } + ) + await this.comfyPage.nextFrame() + } + + async adjustEmptyLatentWidth(): Promise { + 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() + } +} diff --git a/browser_tests/fixtures/helpers/SettingsHelper.ts b/browser_tests/fixtures/helpers/SettingsHelper.ts new file mode 100644 index 000000000..8ff512d95 --- /dev/null +++ b/browser_tests/fixtures/helpers/SettingsHelper.ts @@ -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 { + await this.page.evaluate( + async ({ id, value }) => { + await window.app!.extensionManager.setting.set(id, value) + }, + { id: settingId, value: settingValue } + ) + } + + async getSetting(settingId: string): Promise { + return (await this.page.evaluate(async (id) => { + return await window.app!.extensionManager.setting.get(id) + }, settingId)) as T + } +} diff --git a/browser_tests/fixtures/helpers/SubgraphHelper.ts b/browser_tests/fixtures/helpers/SubgraphHelper.ts new file mode 100644 index 000000000..eb74c2fb5 --- /dev/null +++ b/browser_tests/fixtures/helpers/SubgraphHelper.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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() + } +} diff --git a/browser_tests/fixtures/helpers/ToastHelper.ts b/browser_tests/fixtures/helpers/ToastHelper.ts new file mode 100644 index 000000000..544e20316 --- /dev/null +++ b/browser_tests/fixtures/helpers/ToastHelper.ts @@ -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 { + return await this.page + .locator('.p-toast-message.p-toast-message-error') + .count() + } + + async getVisibleToastCount(): Promise { + return await this.visibleToasts.count() + } + + async closeToasts(requireCount = 0): Promise { + 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 }) + } +} diff --git a/browser_tests/fixtures/helpers/WorkflowHelper.ts b/browser_tests/fixtures/helpers/WorkflowHelper.ts new file mode 100644 index 000000000..234e8bd63 --- /dev/null +++ b/browser_tests/fixtures/helpers/WorkflowHelper.ts @@ -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 { + return this.comfyPage.page.evaluate(() => { + const workflow = (window.app!.extensionManager as WorkspaceStore).workflow + .activeWorkflow + return workflow?.changeTracker.undoQueue.length + }) + } + + async getRedoQueueSize(): Promise { + return this.comfyPage.page.evaluate(() => { + const workflow = (window.app!.extensionManager as WorkspaceStore).workflow + .activeWorkflow + return workflow?.changeTracker.redoQueue.length + }) + } + + async isCurrentWorkflowModified(): Promise { + return this.comfyPage.page.evaluate(() => { + return (window.app!.extensionManager as WorkspaceStore).workflow + .activeWorkflow?.isModified + }) + } + + async getExportedWorkflow(options: { api: true }): Promise + async getExportedWorkflow(options?: { + api?: false + }): Promise + async getExportedWorkflow(options?: { + api?: boolean + }): Promise { + const api = options?.api ?? false + return this.comfyPage.page.evaluate(async (api) => { + return (await window.app!.graphToPrompt())[api ? 'output' : 'workflow'] + }, api) + } +} diff --git a/browser_tests/fixtures/selectors.ts b/browser_tests/fixtures/selectors.ts new file mode 100644 index 000000000..4ad8d1d82 --- /dev/null +++ b/browser_tests/fixtures/selectors.ts @@ -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] diff --git a/browser_tests/fixtures/utils/litegraphUtils.ts b/browser_tests/fixtures/utils/litegraphUtils.ts index ea5a0b78f..08e504150 100644 --- a/browser_tests/fixtures/utils/litegraphUtils.ts +++ b/browser_tests/fixtures/utils/litegraphUtils.ts @@ -1,3 +1,4 @@ +import { expect } from '@playwright/test' import type { Page } from '@playwright/test' import type { NodeId } from '../../../src/platform/workflow/validation/schemas/workflowSchema' @@ -22,10 +23,10 @@ export class SubgraphSlotReference { async getPosition(): Promise { const pos: [number, number] = await this.comfyPage.page.evaluate( ([type, slotName]) => { - const currentGraph = window['app'].canvas.graph + const currentGraph = window.app!.canvas.graph! - // Check if we're in a subgraph - if (currentGraph.constructor.name !== 'Subgraph') { + // Check if we're in a subgraph (subgraphs have inputNode property) + if (!('inputNode' in currentGraph)) { throw new Error( 'Not in a subgraph - this method only works inside subgraphs' ) @@ -51,7 +52,7 @@ export class SubgraphSlotReference { } // Convert from offset to canvas coordinates - const canvasPos = window['app'].canvas.ds.convertOffsetToCanvas([ + const canvasPos = window.app!.canvas.ds.convertOffsetToCanvas([ slot.pos[0], slot.pos[1] ]) @@ -69,9 +70,10 @@ export class SubgraphSlotReference { async getOpenSlotPosition(): Promise { const pos: [number, number] = await this.comfyPage.page.evaluate( ([type]) => { - const currentGraph = window['app'].canvas.graph + const currentGraph = window.app!.canvas.graph! - if (currentGraph.constructor.name !== 'Subgraph') { + // Check if we're in a subgraph (subgraphs have inputNode property) + if (!('inputNode' in currentGraph)) { throw new Error( 'Not in a subgraph - this method only works inside subgraphs' ) @@ -85,7 +87,7 @@ export class SubgraphSlotReference { } // Convert from offset to canvas coordinates - const canvasPos = window['app'].canvas.ds.convertOffsetToCanvas([ + const canvasPos = window.app!.canvas.ds.convertOffsetToCanvas([ node.emptySlot.pos[0], node.emptySlot.pos[1] ]) @@ -111,12 +113,12 @@ class NodeSlotReference { const pos: [number, number] = await this.node.comfyPage.page.evaluate( ([type, id, index]) => { // Use canvas.graph to get the current graph (works in both main graph and subgraphs) - const node = window['app'].canvas.graph.getNodeById(id) + const node = window.app!.canvas.graph!.getNodeById(id) if (!node) throw new Error(`Node ${id} not found.`) const rawPos = node.getConnectionPos(type === 'input', index) const convertedPos = - window['app'].canvas.ds.convertOffsetToCanvas(rawPos) + window.app!.canvas.ds!.convertOffsetToCanvas(rawPos) // Debug logging - convert Float64Arrays to regular arrays for visibility console.warn( @@ -126,7 +128,7 @@ class NodeSlotReference { nodeSize: [node.size[0], node.size[1]], rawConnectionPos: [rawPos[0], rawPos[1]], convertedPos: [convertedPos[0], convertedPos[1]], - currentGraphType: window['app'].canvas.graph.constructor.name + currentGraphType: window.app!.canvas.graph!.constructor.name } ) @@ -142,7 +144,7 @@ class NodeSlotReference { async getLinkCount() { return await this.node.comfyPage.page.evaluate( ([type, id, index]) => { - const node = window['app'].canvas.graph.getNodeById(id) + const node = window.app!.canvas.graph!.getNodeById(id) if (!node) throw new Error(`Node ${id} not found.`) if (type === 'input') { return node.inputs[index].link == null ? 0 : 1 @@ -155,7 +157,7 @@ class NodeSlotReference { async removeLinks() { await this.node.comfyPage.page.evaluate( ([type, id, index]) => { - const node = window['app'].canvas.graph.getNodeById(id) + const node = window.app!.canvas.graph!.getNodeById(id) if (!node) throw new Error(`Node ${id} not found.`) if (type === 'input') { node.disconnectInput(index) @@ -180,15 +182,15 @@ class NodeWidgetReference { async getPosition(): Promise { const pos: [number, number] = await this.node.comfyPage.page.evaluate( ([id, index]) => { - const node = window['app'].canvas.graph.getNodeById(id) + const node = window.app!.canvas.graph!.getNodeById(id) if (!node) throw new Error(`Node ${id} not found.`) - const widget = node.widgets[index] + const widget = node.widgets![index] if (!widget) throw new Error(`Widget ${index} not found.`) - const [x, y, w, h] = node.getBounding() - return window['app'].canvasPosToClientPos([ + const [x, y, w, _h] = node.getBounding() + return window.app!.canvasPosToClientPos([ x + w / 2, - y + window['LiteGraph']['NODE_TITLE_HEIGHT'] + widget.last_y + 1 + y + window.LiteGraph!['NODE_TITLE_HEIGHT'] + widget.last_y! + 1 ]) }, [this.node.id, this.index] as const @@ -205,9 +207,9 @@ class NodeWidgetReference { async getSocketPosition(): Promise { const pos: [number, number] = await this.node.comfyPage.page.evaluate( ([id, index]) => { - const node = window['app'].graph.getNodeById(id) + const node = window.app!.graph!.getNodeById(id) if (!node) throw new Error(`Node ${id} not found.`) - const widget = node.widgets[index] + const widget = node.widgets![index] if (!widget) throw new Error(`Widget ${index} not found.`) const slot = node.inputs.find( @@ -216,9 +218,9 @@ class NodeWidgetReference { if (!slot) throw new Error(`Socket ${widget.name} not found.`) const [x, y] = node.getBounding() - return window['app'].canvasPosToClientPos([ - x + slot.pos[0], - y + slot.pos[1] + window['LiteGraph']['NODE_TITLE_HEIGHT'] + return window.app!.canvasPosToClientPos([ + x + slot.pos![0], + y + slot.pos![1] + window.LiteGraph!['NODE_TITLE_HEIGHT'] ]) }, [this.node.id, this.index] as const @@ -239,7 +241,7 @@ class NodeWidgetReference { const pos = await this.getPosition() const canvas = this.node.comfyPage.canvas const canvasPos = (await canvas.boundingBox())! - await this.node.comfyPage.dragAndDrop( + await this.node.comfyPage.canvasOps.dragAndDrop( { x: canvasPos.x + pos.x, y: canvasPos.y + pos.y @@ -254,9 +256,9 @@ class NodeWidgetReference { async getValue() { return await this.node.comfyPage.page.evaluate( ([id, index]) => { - const node = window['app'].graph.getNodeById(id) + const node = window.app!.graph!.getNodeById(id) if (!node) throw new Error(`Node ${id} not found.`) - const widget = node.widgets[index] + const widget = node.widgets![index] if (!widget) throw new Error(`Widget ${index} not found.`) return widget.value }, @@ -271,7 +273,7 @@ export class NodeReference { ) {} async exists(): Promise { return await this.comfyPage.page.evaluate((id) => { - const node = window['app'].canvas.graph.getNodeById(id) + const node = window.app!.canvas.graph!.getNodeById(id) return !!node }, this.id) } @@ -279,7 +281,7 @@ export class NodeReference { return this.getProperty('type') } async getPosition(): Promise { - const pos = await this.comfyPage.convertOffsetToCanvas( + const pos = await this.comfyPage.canvasOps.convertOffsetToCanvas( await this.getProperty<[number, number]>('pos') ) return { @@ -288,12 +290,11 @@ export class NodeReference { } } async getBounding(): Promise { - const [x, y, width, height]: [number, number, number, number] = - await this.comfyPage.page.evaluate((id) => { - const node = window['app'].canvas.graph.getNodeById(id) - if (!node) throw new Error('Node not found') - return node.getBounding() - }, this.id) + const [x, y, width, height] = await this.comfyPage.page.evaluate((id) => { + const node = window.app!.canvas.graph!.getNodeById(id) + if (!node) throw new Error('Node not found') + return [...node.getBounding()] as [number, number, number, number] + }, this.id) return { x, y, @@ -311,6 +312,11 @@ export class NodeReference { async getFlags(): Promise<{ collapsed?: boolean; pinned?: boolean }> { return await this.getProperty('flags') } + async getTitlePosition(): Promise { + const nodePos = await this.getPosition() + const nodeSize = await this.getSize() + return { x: nodePos.x + nodeSize.width / 2, y: nodePos.y - 15 } + } async isPinned() { return !!(await this.getFlags()).pinned } @@ -323,9 +329,9 @@ export class NodeReference { async getProperty(prop: string): Promise { return await this.comfyPage.page.evaluate( ([id, prop]) => { - const node = window['app'].canvas.graph.getNodeById(id) + const node = window.app!.canvas.graph!.getNodeById(id) if (!node) throw new Error('Node not found') - return node[prop] + return (node as unknown as Record)[prop] }, [this.id, prop] as const ) @@ -343,16 +349,16 @@ export class NodeReference { position: 'title' | 'collapse', options?: Parameters[1] & { moveMouseToEmptyArea?: boolean } ) { - const nodePos = await this.getPosition() - const nodeSize = await this.getSize() let clickPos: Position switch (position) { case 'title': - clickPos = { x: nodePos.x + nodeSize.width / 2, y: nodePos.y - 15 } + clickPos = await this.getTitlePosition() break - case 'collapse': + case 'collapse': { + const nodePos = await this.getPosition() clickPos = { x: nodePos.x + 5, y: nodePos.y - 10 } break + } default: throw new Error(`Invalid click position ${position}`) } @@ -369,12 +375,12 @@ export class NodeReference { }) await this.comfyPage.nextFrame() if (moveMouseToEmptyArea) { - await this.comfyPage.moveMouseToEmptyArea() + await this.comfyPage.canvasOps.moveMouseToEmptyArea() } } async copy() { await this.click('title') - await this.comfyPage.ctrlC() + await this.comfyPage.clipboard.copy() await this.comfyPage.nextFrame() } async connectWidget( @@ -384,7 +390,7 @@ export class NodeReference { ) { const originSlot = await this.getOutput(originSlotIndex) const targetWidget = await targetNode.getWidget(targetWidgetIndex) - await this.comfyPage.dragAndDrop( + await this.comfyPage.canvasOps.dragAndDrop( await originSlot.getPosition(), await targetWidget.getSocketPosition() ) @@ -397,7 +403,7 @@ export class NodeReference { ) { const originSlot = await this.getOutput(originSlotIndex) const targetSlot = await targetNode.getInput(targetSlotIndex) - await this.comfyPage.dragAndDrop( + await this.comfyPage.canvasOps.dragAndDrop( await originSlot.getPosition(), await targetSlot.getPosition() ) @@ -415,9 +421,9 @@ export class NodeReference { } async convertToGroupNode(groupNodeName: string = 'GroupNode') { await this.clickContextMenuOption('Convert to Group Node') - await this.comfyPage.fillPromptDialog(groupNodeName) + await this.comfyPage.nodeOps.fillPromptDialog(groupNodeName) await this.comfyPage.nextFrame() - const nodes = await this.comfyPage.getNodeRefsByType( + const nodes = await this.comfyPage.nodeOps.getNodeRefsByType( `workflow>${groupNodeName}` ) if (nodes.length !== 1) { @@ -428,7 +434,8 @@ export class NodeReference { async convertToSubgraph() { await this.clickContextMenuOption('Convert to Subgraph') await this.comfyPage.nextFrame() - const nodes = await this.comfyPage.getNodeRefsByTitle('New Subgraph') + const nodes = + await this.comfyPage.nodeOps.getNodeRefsByTitle('New Subgraph') if (nodes.length !== 1) { throw new Error( `Did not find single subgraph node (found=${nodes.length})` @@ -446,7 +453,7 @@ export class NodeReference { } async navigateIntoSubgraph() { const titleHeight = await this.comfyPage.page.evaluate(() => { - return window['LiteGraph']['NODE_TITLE_HEIGHT'] + return window.LiteGraph!['NODE_TITLE_HEIGHT'] }) const nodePos = await this.getPosition() const nodeSize = await this.getSize() @@ -458,13 +465,14 @@ export class NodeReference { { x: nodePos.x + 20, y: nodePos.y + titleHeight + 5 } ] - let isInSubgraph = false - let attempts = 0 - const maxAttempts = 3 - - while (!isInSubgraph && attempts < maxAttempts) { - attempts++ + const checkIsInSubgraph = async () => { + return this.comfyPage.page.evaluate(() => { + const graph = window.app!.canvas.graph + return graph?.constructor?.name === 'Subgraph' + }) + } + await expect(async () => { for (const position of clickPositions) { // Clear any selection first await this.comfyPage.canvas.click({ @@ -477,24 +485,9 @@ export class NodeReference { await this.comfyPage.canvas.dblclick({ position, force: true }) await this.comfyPage.nextFrame() - // Check if we successfully entered the subgraph - isInSubgraph = await this.comfyPage.page.evaluate(() => { - const graph = window['app'].canvas.graph - return graph?.constructor?.name === 'Subgraph' - }) - - if (isInSubgraph) break + if (await checkIsInSubgraph()) return } - - if (!isInSubgraph && attempts < maxAttempts) { - await this.comfyPage.page.waitForTimeout(500) - } - } - - if (!isInSubgraph) { - throw new Error( - 'Failed to navigate into subgraph after ' + attempts + ' attempts' - ) - } + throw new Error('Not in subgraph yet') + }).toPass({ timeout: 5000, intervals: [100, 200, 500] }) } } diff --git a/browser_tests/fixtures/utils/vueNodeFixtures.ts b/browser_tests/fixtures/utils/vueNodeFixtures.ts index fca464405..e7368d775 100644 --- a/browser_tests/fixtures/utils/vueNodeFixtures.ts +++ b/browser_tests/fixtures/utils/vueNodeFixtures.ts @@ -1,4 +1,3 @@ -import { expect } from '@playwright/test' import type { Locator } from '@playwright/test' /** DOM-centric helper for a single Vue-rendered node on the canvas. */ @@ -40,7 +39,7 @@ export class VueNodeFixture { async setTitle(value: string): Promise { await this.header.dblclick() const input = this.titleInput - await expect(input).toBeVisible() + await input.waitFor({ state: 'visible' }) await input.fill(value) await input.press('Enter') } @@ -48,7 +47,7 @@ export class VueNodeFixture { async cancelTitleEdit(): Promise { await this.header.dblclick() const input = this.titleInput - await expect(input).toBeVisible() + await input.waitFor({ state: 'visible' }) await input.press('Escape') } diff --git a/browser_tests/fixtures/ws.ts b/browser_tests/fixtures/ws.ts index f1ab1a538..02e68bdb7 100644 --- a/browser_tests/fixtures/ws.ts +++ b/browser_tests/fixtures/ws.ts @@ -1,7 +1,7 @@ import { test as base } from '@playwright/test' export const webSocketFixture = base.extend<{ - ws: { trigger(data: any, url?: string): Promise } + ws: { trigger(data: unknown, url?: string): Promise } }>({ ws: [ async ({ page }, use) => { @@ -10,7 +10,7 @@ export const webSocketFixture = base.extend<{ await page.evaluate(function () { // Create a wrapper for WebSocket that stores them globally // so we can look it up to trigger messages - const store: Record = ((window as any).__ws__ = {}) + const store: Record = (window.__ws__ = {}) window.WebSocket = class extends window.WebSocket { constructor( ...rest: ConstructorParameters @@ -34,7 +34,7 @@ export const webSocketFixture = base.extend<{ u.pathname = '/' url = u.toString() + 'ws' } - const ws: WebSocket = (window as any).__ws__[url] + const ws: WebSocket = window.__ws__![url] ws.dispatchEvent( new MessageEvent('message', { data diff --git a/browser_tests/globalSetup.ts b/browser_tests/globalSetup.ts index 881ef11c4..b43b77c6a 100644 --- a/browser_tests/globalSetup.ts +++ b/browser_tests/globalSetup.ts @@ -5,7 +5,7 @@ import { backupPath } from './utils/backupUtils' dotenv.config() -export default function globalSetup(config: FullConfig) { +export default function globalSetup(_config: FullConfig) { if (!process.env.CI) { if (process.env.TEST_COMFYUI_DIR) { backupPath([process.env.TEST_COMFYUI_DIR, 'user']) diff --git a/browser_tests/globalTeardown.ts b/browser_tests/globalTeardown.ts index aeed77294..c69f563df 100644 --- a/browser_tests/globalTeardown.ts +++ b/browser_tests/globalTeardown.ts @@ -5,7 +5,7 @@ import { restorePath } from './utils/backupUtils' dotenv.config() -export default function globalTeardown(config: FullConfig) { +export default function globalTeardown(_config: FullConfig) { if (!process.env.CI && process.env.TEST_COMFYUI_DIR) { restorePath([process.env.TEST_COMFYUI_DIR, 'user']) restorePath([process.env.TEST_COMFYUI_DIR, 'models']) diff --git a/browser_tests/helpers/actionbar.ts b/browser_tests/helpers/actionbar.ts index 6c368c4d6..29f679e0c 100644 --- a/browser_tests/helpers/actionbar.ts +++ b/browser_tests/helpers/actionbar.ts @@ -1,6 +1,8 @@ import type { Locator, Page } from '@playwright/test' import type { AutoQueueMode } from '../../src/stores/queueStore' +import { TestIds } from '../fixtures/selectors' +import type { WorkspaceStore } from '../types/globals' export class ComfyActionbar { public readonly root: Locator @@ -26,7 +28,7 @@ class ComfyQueueButton { public readonly primaryButton: Locator public readonly dropdownButton: Locator constructor(public readonly actionbar: ComfyActionbar) { - this.root = actionbar.root.getByTestId('queue-button') + this.root = actionbar.root.getByTestId(TestIds.topbar.queueButton) this.primaryButton = this.root.locator('.p-splitbutton-button') this.dropdownButton = this.root.locator('.p-splitbutton-dropdown') } @@ -42,13 +44,14 @@ class ComfyQueueButtonOptions { public async setMode(mode: AutoQueueMode) { await this.page.evaluate((mode) => { - window['app'].extensionManager.queueSettings.mode = mode + ;(window.app!.extensionManager as WorkspaceStore).queueSettings.mode = + mode }, mode) } public async getMode() { return await this.page.evaluate(() => { - return window['app'].extensionManager.queueSettings.mode + return (window.app!.extensionManager as WorkspaceStore).queueSettings.mode }) } } diff --git a/browser_tests/helpers/fitToView.ts b/browser_tests/helpers/fitToView.ts index af6c10e9d..6aea86dba 100644 --- a/browser_tests/helpers/fitToView.ts +++ b/browser_tests/helpers/fitToView.ts @@ -23,7 +23,7 @@ export async function fitToViewInstant( { selectionOnly: boolean } >( ({ selectionOnly }) => { - const app = window['app'] + const app = window.app if (!app?.canvas) return null const canvas = app.canvas @@ -90,7 +90,7 @@ export async function fitToViewInstant( await comfyPage.page.evaluate( ({ bounds, zoom }) => { - const app = window['app'] + const app = window.app if (!app?.canvas) return const canvas = app.canvas diff --git a/browser_tests/helpers/subgraphTestUtils.ts b/browser_tests/helpers/subgraphTestUtils.ts new file mode 100644 index 000000000..0eae24651 --- /dev/null +++ b/browser_tests/helpers/subgraphTestUtils.ts @@ -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' + ) + } +} diff --git a/browser_tests/helpers/templates.ts b/browser_tests/helpers/templates.ts index ca74096ad..ddbf1f79d 100644 --- a/browser_tests/helpers/templates.ts +++ b/browser_tests/helpers/templates.ts @@ -6,18 +6,19 @@ import type { TemplateInfo, WorkflowTemplates } from '../../src/platform/workflow/templates/types/template' +import { TestIds } from '../fixtures/selectors' export class ComfyTemplates { readonly content: Locator readonly allTemplateCards: Locator constructor(readonly page: Page) { - this.content = page.getByTestId('template-workflows-content') + this.content = page.getByTestId(TestIds.templates.content) this.allTemplateCards = page.locator('[data-testid^="template-workflow-"]') } - async waitForMinimumCardCount(count: number) { - return await expect(async () => { + async expectMinimumCardCount(count: number) { + await expect(async () => { const cardCount = await this.allTemplateCards.count() expect(cardCount).toBeGreaterThanOrEqual(count) }).toPass({ @@ -26,14 +27,16 @@ export class ComfyTemplates { } async loadTemplate(id: string) { - const templateCard = this.content.getByTestId(`template-workflow-${id}`) + const templateCard = this.content.getByTestId( + TestIds.templates.workflowCard(id) + ) await templateCard.scrollIntoViewIfNeeded() await templateCard.getByRole('img').click() } async getAllTemplates(): Promise { const templates: WorkflowTemplates[] = await this.page.evaluate(() => - window['app'].api.getCoreWorkflowTemplates() + window.app!.api.getCoreWorkflowTemplates() ) return templates.flatMap((t) => t.templates) } diff --git a/browser_tests/tests/actionbar.spec.ts b/browser_tests/tests/actionbar.spec.ts index 1a18577d7..4d57b65f4 100644 --- a/browser_tests/tests/actionbar.spec.ts +++ b/browser_tests/tests/actionbar.spec.ts @@ -1,15 +1,16 @@ import type { Response } from '@playwright/test' import { expect, mergeTests } from '@playwright/test' -import type { StatusWsMessage } from '../../src/schemas/apiSchema.ts' -import { comfyPageFixture } from '../fixtures/ComfyPage.ts' -import { webSocketFixture } from '../fixtures/ws.ts' +import type { StatusWsMessage } from '../../src/schemas/apiSchema' +import { comfyPageFixture } from '../fixtures/ComfyPage' +import { webSocketFixture } from '../fixtures/ws' +import type { WorkspaceStore } from '../types/globals' const test = mergeTests(comfyPageFixture, webSocketFixture) test.describe('Actionbar', { tag: '@ui' }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') }) /** @@ -49,13 +50,14 @@ test.describe('Actionbar', { tag: '@ui' }, () => { // Find and set the width on the latent node const triggerChange = async (value: number) => { return await comfyPage.page.evaluate((value) => { - const node = window['app'].graph._nodes.find( + const node = window.app!.graph!._nodes.find( (n) => n.type === 'EmptyLatentImage' ) - node.widgets[0].value = value - window[ - 'app' - ].extensionManager.workflow.activeWorkflow.changeTracker.checkState() + node!.widgets![0].value = value + + ;( + window.app!.extensionManager as WorkspaceStore + ).workflow.activeWorkflow?.changeTracker.checkState() }, value) } diff --git a/browser_tests/tests/backgroundImageUpload.spec.ts b/browser_tests/tests/backgroundImageUpload.spec.ts index b620f5441..0f49787b2 100644 --- a/browser_tests/tests/backgroundImageUpload.spec.ts +++ b/browser_tests/tests/backgroundImageUpload.spec.ts @@ -3,18 +3,18 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Background Image Upload', () => { test.beforeEach(async ({ comfyPage }) => { // Reset the background image setting before each test - await comfyPage.setSetting('Comfy.Canvas.BackgroundImage', '') + await comfyPage.settings.setSetting('Comfy.Canvas.BackgroundImage', '') }) test.afterEach(async ({ comfyPage }) => { // Clean up background image setting after each test - await comfyPage.setSetting('Comfy.Canvas.BackgroundImage', '') + await comfyPage.settings.setSetting('Comfy.Canvas.BackgroundImage', '') }) test('should show background image upload component in settings', async ({ @@ -34,16 +34,18 @@ test.describe('Background Image Upload', () => { await expect(backgroundImageSetting).toBeVisible() // Verify the component has the expected elements using semantic selectors - const urlInput = backgroundImageSetting.locator('input[type="text"]') + const urlInput = backgroundImageSetting.getByRole('textbox') await expect(urlInput).toBeVisible() await expect(urlInput).toHaveAttribute('placeholder') - const uploadButton = backgroundImageSetting.locator( - 'button:has(.pi-upload)' - ) + const uploadButton = backgroundImageSetting.getByRole('button', { + name: /upload/i + }) await expect(uploadButton).toBeVisible() - const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)') + const clearButton = backgroundImageSetting.getByRole('button', { + name: /clear/i + }) await expect(clearButton).toBeVisible() await expect(clearButton).toBeDisabled() // Should be disabled when no image }) @@ -63,9 +65,9 @@ test.describe('Background Image Upload', () => { '#Comfy\\.Canvas\\.BackgroundImage' ) // Click the upload button to trigger file input - const uploadButton = backgroundImageSetting.locator( - 'button:has(.pi-upload)' - ) + const uploadButton = backgroundImageSetting.getByRole('button', { + name: /upload/i + }) // Set up file upload handler const fileChooserPromise = comfyPage.page.waitForEvent('filechooser') @@ -76,15 +78,17 @@ test.describe('Background Image Upload', () => { await fileChooser.setFiles(comfyPage.assetPath('image32x32.webp')) // Verify the URL input now has an API URL - const urlInput = backgroundImageSetting.locator('input[type="text"]') + const urlInput = backgroundImageSetting.getByRole('textbox') await expect(urlInput).toHaveValue(/^\/api\/view\?.*subfolder=backgrounds/) // Verify clear button is now enabled - const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)') + const clearButton = backgroundImageSetting.getByRole('button', { + name: /clear/i + }) await expect(clearButton).toBeEnabled() // Verify the setting value was actually set - const settingValue = await comfyPage.getSetting( + const settingValue = await comfyPage.settings.getSetting( 'Comfy.Canvas.BackgroundImage' ) expect(settingValue).toMatch(/^\/api\/view\?.*subfolder=backgrounds/) @@ -107,18 +111,20 @@ test.describe('Background Image Upload', () => { '#Comfy\\.Canvas\\.BackgroundImage' ) // Enter URL in the input field - const urlInput = backgroundImageSetting.locator('input[type="text"]') + const urlInput = backgroundImageSetting.getByRole('textbox') await urlInput.fill(testImageUrl) // Trigger blur event to ensure the value is set await urlInput.blur() // Verify clear button is now enabled - const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)') + const clearButton = backgroundImageSetting.getByRole('button', { + name: /clear/i + }) await expect(clearButton).toBeEnabled() // Verify the setting value was updated - const settingValue = await comfyPage.getSetting( + const settingValue = await comfyPage.settings.getSetting( 'Comfy.Canvas.BackgroundImage' ) expect(settingValue).toBe(testImageUrl) @@ -130,7 +136,10 @@ test.describe('Background Image Upload', () => { const testImageUrl = 'https://example.com/test-image.png' // First set a background image - await comfyPage.setSetting('Comfy.Canvas.BackgroundImage', testImageUrl) + await comfyPage.settings.setSetting( + 'Comfy.Canvas.BackgroundImage', + testImageUrl + ) // Open settings dialog await comfyPage.page.keyboard.press('Control+,') @@ -144,11 +153,13 @@ test.describe('Background Image Upload', () => { '#Comfy\\.Canvas\\.BackgroundImage' ) // Verify the input has the test URL - const urlInput = backgroundImageSetting.locator('input[type="text"]') + const urlInput = backgroundImageSetting.getByRole('textbox') await expect(urlInput).toHaveValue(testImageUrl) // Verify clear button is enabled - const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)') + const clearButton = backgroundImageSetting.getByRole('button', { + name: /clear/i + }) await expect(clearButton).toBeEnabled() // Click the clear button @@ -161,7 +172,7 @@ test.describe('Background Image Upload', () => { await expect(clearButton).toBeDisabled() // Verify the setting value was cleared - const settingValue = await comfyPage.getSetting( + const settingValue = await comfyPage.settings.getSetting( 'Comfy.Canvas.BackgroundImage' ) expect(settingValue).toBe('') @@ -182,9 +193,9 @@ test.describe('Background Image Upload', () => { '#Comfy\\.Canvas\\.BackgroundImage' ) // Hover over upload button and verify tooltip appears - const uploadButton = backgroundImageSetting.locator( - 'button:has(.pi-upload)' - ) + const uploadButton = backgroundImageSetting.getByRole('button', { + name: /upload/i + }) await uploadButton.hover() const uploadTooltip = comfyPage.page.locator('.p-tooltip:visible') @@ -194,12 +205,14 @@ test.describe('Background Image Upload', () => { await comfyPage.page.locator('body').hover() // Set a background to enable clear button - const urlInput = backgroundImageSetting.locator('input[type="text"]') + const urlInput = backgroundImageSetting.getByRole('textbox') await urlInput.fill('https://example.com/test.png') await urlInput.blur() // Hover over clear button and verify tooltip appears - const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)') + const clearButton = backgroundImageSetting.getByRole('button', { + name: /clear/i + }) await clearButton.hover() const clearTooltip = comfyPage.page.locator('.p-tooltip:visible') @@ -220,8 +233,10 @@ test.describe('Background Image Upload', () => { const backgroundImageSetting = comfyPage.page.locator( '#Comfy\\.Canvas\\.BackgroundImage' ) - const urlInput = backgroundImageSetting.locator('input[type="text"]') - const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)') + const urlInput = backgroundImageSetting.getByRole('textbox') + const clearButton = backgroundImageSetting.getByRole('button', { + name: /clear/i + }) // Initially clear button should be disabled await expect(clearButton).toBeDisabled() diff --git a/browser_tests/tests/bottomPanelShortcuts.spec.ts b/browser_tests/tests/bottomPanelShortcuts.spec.ts index 4b3d39783..fba61ebd4 100644 --- a/browser_tests/tests/bottomPanelShortcuts.spec.ts +++ b/browser_tests/tests/bottomPanelShortcuts.spec.ts @@ -4,53 +4,33 @@ import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') }) test('should toggle shortcuts panel visibility', async ({ comfyPage }) => { - // Initially shortcuts panel should be hidden - await expect(comfyPage.page.locator('.bottom-panel')).not.toBeVisible() + const { bottomPanel } = comfyPage - // Click shortcuts toggle button in sidebar - await comfyPage.page - .locator('button[aria-label*="Keyboard Shortcuts"]') - .click() - - // Shortcuts panel should now be visible - await expect(comfyPage.page.locator('.bottom-panel')).toBeVisible() - - // Click toggle button again to hide - await comfyPage.page - .locator('button[aria-label*="Keyboard Shortcuts"]') - .click() - - // Panel should be hidden again - await expect(comfyPage.page.locator('.bottom-panel')).not.toBeVisible() + await expect(bottomPanel.root).not.toBeVisible() + await bottomPanel.keyboardShortcutsButton.click() + await expect(bottomPanel.root).toBeVisible() + await bottomPanel.keyboardShortcutsButton.click() + await expect(bottomPanel.root).not.toBeVisible() }) test('should display essentials shortcuts tab', async ({ comfyPage }) => { - // Open shortcuts panel - await comfyPage.page - .locator('button[aria-label*="Keyboard Shortcuts"]') - .click() + const { bottomPanel } = comfyPage - // Essentials tab should be visible and active by default - await expect( - comfyPage.page.getByRole('tab', { name: /Essential/i }) - ).toBeVisible() - await expect( - comfyPage.page.getByRole('tab', { name: /Essential/i }) - ).toHaveAttribute('aria-selected', 'true') + await bottomPanel.keyboardShortcutsButton.click() - // Should display shortcut categories - await expect( - comfyPage.page.locator('.subcategory-title').first() - ).toBeVisible() + await expect(bottomPanel.shortcuts.essentialsTab).toBeVisible() + await expect(bottomPanel.shortcuts.essentialsTab).toHaveAttribute( + 'aria-selected', + 'true' + ) - // Should display some keyboard shortcuts - await expect(comfyPage.page.locator('.key-badge').first()).toBeVisible() + await expect(bottomPanel.shortcuts.subcategoryTitles.first()).toBeVisible() + await expect(bottomPanel.shortcuts.keyBadges.first()).toBeVisible() - // Should have workflow, node, and queue sections await expect( comfyPage.page.getByRole('heading', { name: 'Workflow' }) ).toBeVisible() @@ -63,23 +43,18 @@ test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => { }) test('should display view controls shortcuts tab', async ({ comfyPage }) => { - // Open shortcuts panel - await comfyPage.page - .locator('button[aria-label*="Keyboard Shortcuts"]') - .click() + const { bottomPanel } = comfyPage - // Click view controls tab - await comfyPage.page.getByRole('tab', { name: /View Controls/i }).click() + await bottomPanel.keyboardShortcutsButton.click() + await bottomPanel.shortcuts.viewControlsTab.click() - // View controls tab should be active - await expect( - comfyPage.page.getByRole('tab', { name: /View Controls/i }) - ).toHaveAttribute('aria-selected', 'true') + await expect(bottomPanel.shortcuts.viewControlsTab).toHaveAttribute( + 'aria-selected', + 'true' + ) - // Should display view controls shortcuts - await expect(comfyPage.page.locator('.key-badge').first()).toBeVisible() + await expect(bottomPanel.shortcuts.keyBadges.first()).toBeVisible() - // Should have view and panel controls sections await expect( comfyPage.page.getByRole('heading', { name: 'View' }) ).toBeVisible() @@ -89,54 +64,48 @@ test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => { }) test('should switch between shortcuts tabs', async ({ comfyPage }) => { - // Open shortcuts panel - await comfyPage.page - .locator('button[aria-label*="Keyboard Shortcuts"]') - .click() + const { bottomPanel } = comfyPage - // Essentials should be active initially - await expect( - comfyPage.page.getByRole('tab', { name: /Essential/i }) - ).toHaveAttribute('aria-selected', 'true') + await bottomPanel.keyboardShortcutsButton.click() - // Click view controls tab - await comfyPage.page.getByRole('tab', { name: /View Controls/i }).click() + await expect(bottomPanel.shortcuts.essentialsTab).toHaveAttribute( + 'aria-selected', + 'true' + ) - // View controls should now be active - await expect( - comfyPage.page.getByRole('tab', { name: /View Controls/i }) - ).toHaveAttribute('aria-selected', 'true') - await expect( - comfyPage.page.getByRole('tab', { name: /Essential/i }) - ).not.toHaveAttribute('aria-selected', 'true') + await bottomPanel.shortcuts.viewControlsTab.click() - // Switch back to essentials - await comfyPage.page.getByRole('tab', { name: /Essential/i }).click() + await expect(bottomPanel.shortcuts.viewControlsTab).toHaveAttribute( + 'aria-selected', + 'true' + ) + await expect(bottomPanel.shortcuts.essentialsTab).not.toHaveAttribute( + 'aria-selected', + 'true' + ) - // Essentials should be active again - await expect( - comfyPage.page.getByRole('tab', { name: /Essential/i }) - ).toHaveAttribute('aria-selected', 'true') - await expect( - comfyPage.page.getByRole('tab', { name: /View Controls/i }) - ).not.toHaveAttribute('aria-selected', 'true') + await bottomPanel.shortcuts.essentialsTab.click() + + await expect(bottomPanel.shortcuts.essentialsTab).toHaveAttribute( + 'aria-selected', + 'true' + ) + await expect(bottomPanel.shortcuts.viewControlsTab).not.toHaveAttribute( + 'aria-selected', + 'true' + ) }) test('should display formatted keyboard shortcuts', async ({ comfyPage }) => { - // Open shortcuts panel - await comfyPage.page - .locator('button[aria-label*="Keyboard Shortcuts"]') - .click() + const { bottomPanel } = comfyPage - // Wait for shortcuts to load - await comfyPage.page.waitForSelector('.key-badge') + await bottomPanel.keyboardShortcutsButton.click() - // Check for common formatted keys - const keyBadges = comfyPage.page.locator('.key-badge') + const keyBadges = bottomPanel.shortcuts.keyBadges + await keyBadges.first().waitFor({ state: 'visible' }) const count = await keyBadges.count() expect(count).toBeGreaterThanOrEqual(1) - // Should show formatted modifier keys const badgeText = await keyBadges.allTextContents() const hasModifiers = badgeText.some((text) => ['Ctrl', 'Cmd', 'Shift', 'Alt'].includes(text) @@ -147,20 +116,18 @@ test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => { test('should maintain panel state when switching between panels', async ({ comfyPage }) => { + const { bottomPanel } = comfyPage + // Open shortcuts panel first - await comfyPage.page - .locator('button[aria-label*="Keyboard Shortcuts"]') - .click() - await expect(comfyPage.page.locator('.bottom-panel')).toBeVisible() + await bottomPanel.keyboardShortcutsButton.click() + await expect(bottomPanel.root).toBeVisible() await expect( comfyPage.page.locator('[id*="tab_shortcuts-essentials"]') ).toBeVisible() // Try to open terminal panel - may show terminal OR close shortcuts // depending on whether terminal tabs have loaded (async loading) - await comfyPage.page - .locator('button[aria-label*="Toggle Bottom Panel"]') - .click() + await bottomPanel.toggleButton.click() // Check if terminal tabs loaded (Logs tab visible) or fell back to shortcuts toggle const logsTab = comfyPage.page.getByRole('tab', { name: /Logs/i }) @@ -168,12 +135,10 @@ test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => { if (hasTerminalTabs) { // Terminal panel is visible - verify we can switch back to shortcuts - await expect(comfyPage.page.locator('.bottom-panel')).toBeVisible() + await expect(bottomPanel.root).toBeVisible() // Switch back to shortcuts - await comfyPage.page - .locator('button[aria-label*="Keyboard Shortcuts"]') - .click() + await bottomPanel.keyboardShortcutsButton.click() // Should show shortcuts content again await expect( @@ -181,10 +146,8 @@ test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => { ).toBeVisible() } else { // Terminal tabs not loaded - button toggled shortcuts off, reopen for verification - await comfyPage.page - .locator('button[aria-label*="Keyboard Shortcuts"]') - .click() - await expect(comfyPage.page.locator('.bottom-panel')).toBeVisible() + await bottomPanel.keyboardShortcutsButton.click() + await expect(bottomPanel.root).toBeVisible() await expect( comfyPage.page.locator('[id*="tab_shortcuts-essentials"]') ).toBeVisible() @@ -192,62 +155,47 @@ test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => { }) test('should handle keyboard navigation', async ({ comfyPage }) => { - // Open shortcuts panel - await comfyPage.page - .locator('button[aria-label*="Keyboard Shortcuts"]') - .click() + const { bottomPanel } = comfyPage - // Focus the first tab - await comfyPage.page.getByRole('tab', { name: /Essential/i }).focus() + await bottomPanel.keyboardShortcutsButton.click() + await bottomPanel.shortcuts.essentialsTab.focus() - // Use arrow keys to navigate between tabs await comfyPage.page.keyboard.press('ArrowRight') - // View controls tab should now have focus - await expect( - comfyPage.page.getByRole('tab', { name: /View Controls/i }) - ).toBeFocused() + await expect(bottomPanel.shortcuts.viewControlsTab).toBeFocused() - // Press Enter to activate the tab await comfyPage.page.keyboard.press('Enter') - // Tab should be selected - await expect( - comfyPage.page.getByRole('tab', { name: /View Controls/i }) - ).toHaveAttribute('aria-selected', 'true') + await expect(bottomPanel.shortcuts.viewControlsTab).toHaveAttribute( + 'aria-selected', + 'true' + ) }) test('should close panel by clicking shortcuts button again', async ({ comfyPage }) => { - // Open shortcuts panel - await comfyPage.page - .locator('button[aria-label*="Keyboard Shortcuts"]') - .click() - await expect(comfyPage.page.locator('.bottom-panel')).toBeVisible() + const { bottomPanel } = comfyPage - // Click shortcuts button again to close - await comfyPage.page - .locator('button[aria-label*="Keyboard Shortcuts"]') - .click() + await bottomPanel.keyboardShortcutsButton.click() + await expect(bottomPanel.root).toBeVisible() - // Panel should be hidden - await expect(comfyPage.page.locator('.bottom-panel')).not.toBeVisible() + await bottomPanel.keyboardShortcutsButton.click() + await expect(bottomPanel.root).not.toBeVisible() }) test('should display shortcuts in organized columns', async ({ comfyPage }) => { - // Open shortcuts panel - await comfyPage.page - .locator('button[aria-label*="Keyboard Shortcuts"]') - .click() + const { bottomPanel } = comfyPage - // Should have 3-column grid layout - await expect(comfyPage.page.locator('.md\\:grid-cols-3')).toBeVisible() + await bottomPanel.keyboardShortcutsButton.click() - // Should have multiple subcategory sections - const subcategoryTitles = comfyPage.page.locator('.subcategory-title') + await expect( + comfyPage.page.locator('[data-testid="shortcuts-columns"]') + ).toBeVisible() + + const subcategoryTitles = bottomPanel.shortcuts.subcategoryTitles const titleCount = await subcategoryTitles.count() expect(titleCount).toBeGreaterThanOrEqual(2) }) @@ -255,43 +203,30 @@ test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => { test('should open shortcuts panel with Ctrl+Shift+K', async ({ comfyPage }) => { - // Initially shortcuts panel should be hidden - await expect(comfyPage.page.locator('.bottom-panel')).not.toBeVisible() + const { bottomPanel } = comfyPage + + await expect(bottomPanel.root).not.toBeVisible() - // Press Ctrl+Shift+K to open shortcuts panel await comfyPage.page.keyboard.press('Control+Shift+KeyK') - // Shortcuts panel should now be visible - await expect(comfyPage.page.locator('.bottom-panel')).toBeVisible() - - // Should show essentials tab by default - await expect( - comfyPage.page.getByRole('tab', { name: /Essential/i }) - ).toHaveAttribute('aria-selected', 'true') + await expect(bottomPanel.root).toBeVisible() + await expect(bottomPanel.shortcuts.essentialsTab).toHaveAttribute( + 'aria-selected', + 'true' + ) }) test('should open settings dialog when clicking manage shortcuts button', async ({ comfyPage }) => { - // Open shortcuts panel - await comfyPage.page - .locator('button[aria-label*="Keyboard Shortcuts"]') - .click() + const { bottomPanel } = comfyPage - // Manage shortcuts button should be visible - await expect( - comfyPage.page.getByRole('button', { name: /Manage Shortcuts/i }) - ).toBeVisible() + await bottomPanel.keyboardShortcutsButton.click() - // Click manage shortcuts button - await comfyPage.page - .getByRole('button', { name: /Manage Shortcuts/i }) - .click() + await expect(bottomPanel.shortcuts.manageButton).toBeVisible() + await bottomPanel.shortcuts.manageButton.click() - // Settings dialog should open with keybinding tab await expect(comfyPage.page.getByRole('dialog')).toBeVisible() - - // Should show keybinding settings (check for keybinding-related content) await expect( comfyPage.page.getByRole('option', { name: 'Keybinding' }) ).toBeVisible() diff --git a/browser_tests/tests/browserTabTitle.spec.ts b/browser_tests/tests/browserTabTitle.spec.ts index f3ad9515e..06bf55ec8 100644 --- a/browser_tests/tests/browserTabTitle.spec.ts +++ b/browser_tests/tests/browserTabTitle.spec.ts @@ -1,16 +1,18 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +import type { WorkspaceStore } from '../types/globals' test.describe('Browser tab title', { tag: '@smoke' }, () => { test.describe('Beta Menu', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') }) test('Can display workflow name', async ({ comfyPage }) => { const workflowName = await comfyPage.page.evaluate(async () => { - return window['app'].extensionManager.workflow.activeWorkflow.filename + return (window.app!.extensionManager as WorkspaceStore).workflow + .activeWorkflow?.filename }) expect(await comfyPage.page.title()).toBe(`*${workflowName} - ComfyUI`) }) @@ -21,7 +23,8 @@ test.describe('Browser tab title', { tag: '@smoke' }, () => { comfyPage }) => { const workflowName = await comfyPage.page.evaluate(async () => { - return window['app'].extensionManager.workflow.activeWorkflow.filename + return (window.app!.extensionManager as WorkspaceStore).workflow + .activeWorkflow?.filename }) expect(await comfyPage.page.title()).toBe(`${workflowName} - ComfyUI`) @@ -30,19 +33,21 @@ test.describe('Browser tab title', { tag: '@smoke' }, () => { const textBox = comfyPage.widgetTextBox await textBox.fill('Hello World') - await comfyPage.clickEmptySpace() + await comfyPage.canvasOps.clickEmptySpace() expect(await comfyPage.page.title()).toBe(`*test - ComfyUI`) // Delete the saved workflow for cleanup. await comfyPage.page.evaluate(async () => { - return window['app'].extensionManager.workflow.activeWorkflow.delete() + return ( + window.app!.extensionManager as WorkspaceStore + ).workflow.activeWorkflow?.delete() }) }) }) test.describe('Legacy Menu', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test('Can display default title', async ({ comfyPage }) => { diff --git a/browser_tests/tests/changeTracker.spec.ts b/browser_tests/tests/changeTracker.spec.ts index e115192c9..80d00c336 100644 --- a/browser_tests/tests/changeTracker.spec.ts +++ b/browser_tests/tests/changeTracker.spec.ts @@ -6,69 +6,69 @@ import { async function beforeChange(comfyPage: ComfyPage) { await comfyPage.page.evaluate(() => { - window['app'].canvas.emitBeforeChange() + window.app!.canvas!.emitBeforeChange() }) } async function afterChange(comfyPage: ComfyPage) { await comfyPage.page.evaluate(() => { - window['app'].canvas.emitAfterChange() + window.app!.canvas!.emitAfterChange() }) } test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Change Tracker', { tag: '@workflow' }, () => { test.describe('Undo/Redo', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') - await comfyPage.setupWorkflowsDirectory({}) + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.workflow.setupWorkflowsDirectory({}) }) test('Can undo multiple operations', async ({ comfyPage }) => { - expect(await comfyPage.getUndoQueueSize()).toBe(0) - expect(await comfyPage.getRedoQueueSize()).toBe(0) + expect(await comfyPage.workflow.getUndoQueueSize()).toBe(0) + expect(await comfyPage.workflow.getRedoQueueSize()).toBe(0) // Save, confirm no errors & workflow modified flag removed await comfyPage.menu.topbar.saveWorkflow('undo-redo-test') - expect(await comfyPage.getToastErrorCount()).toBe(0) - expect(await comfyPage.isCurrentWorkflowModified()).toBe(false) - expect(await comfyPage.getUndoQueueSize()).toBe(0) - expect(await comfyPage.getRedoQueueSize()).toBe(0) + expect(await comfyPage.toast.getToastErrorCount()).toBe(0) + expect(await comfyPage.workflow.isCurrentWorkflowModified()).toBe(false) + expect(await comfyPage.workflow.getUndoQueueSize()).toBe(0) + expect(await comfyPage.workflow.getRedoQueueSize()).toBe(0) - const node = (await comfyPage.getFirstNodeRef())! + const node = (await comfyPage.nodeOps.getFirstNodeRef())! await node.click('title') await node.click('collapse') await expect(node).toBeCollapsed() - expect(await comfyPage.isCurrentWorkflowModified()).toBe(true) - expect(await comfyPage.getUndoQueueSize()).toBe(1) - expect(await comfyPage.getRedoQueueSize()).toBe(0) + expect(await comfyPage.workflow.isCurrentWorkflowModified()).toBe(true) + expect(await comfyPage.workflow.getUndoQueueSize()).toBe(1) + expect(await comfyPage.workflow.getRedoQueueSize()).toBe(0) - await comfyPage.ctrlB() + await comfyPage.keyboard.bypass() await expect(node).toBeBypassed() - expect(await comfyPage.isCurrentWorkflowModified()).toBe(true) - expect(await comfyPage.getUndoQueueSize()).toBe(2) - expect(await comfyPage.getRedoQueueSize()).toBe(0) + expect(await comfyPage.workflow.isCurrentWorkflowModified()).toBe(true) + expect(await comfyPage.workflow.getUndoQueueSize()).toBe(2) + expect(await comfyPage.workflow.getRedoQueueSize()).toBe(0) - await comfyPage.ctrlZ() + await comfyPage.keyboard.undo() await expect(node).not.toBeBypassed() - expect(await comfyPage.isCurrentWorkflowModified()).toBe(true) - expect(await comfyPage.getUndoQueueSize()).toBe(1) - expect(await comfyPage.getRedoQueueSize()).toBe(1) + expect(await comfyPage.workflow.isCurrentWorkflowModified()).toBe(true) + expect(await comfyPage.workflow.getUndoQueueSize()).toBe(1) + expect(await comfyPage.workflow.getRedoQueueSize()).toBe(1) - await comfyPage.ctrlZ() + await comfyPage.keyboard.undo() await expect(node).not.toBeCollapsed() - expect(await comfyPage.isCurrentWorkflowModified()).toBe(false) - expect(await comfyPage.getUndoQueueSize()).toBe(0) - expect(await comfyPage.getRedoQueueSize()).toBe(2) + expect(await comfyPage.workflow.isCurrentWorkflowModified()).toBe(false) + expect(await comfyPage.workflow.getUndoQueueSize()).toBe(0) + expect(await comfyPage.workflow.getRedoQueueSize()).toBe(2) }) }) test('Can group multiple change actions into a single transaction', async ({ comfyPage }) => { - const node = (await comfyPage.getFirstNodeRef())! + const node = (await comfyPage.nodeOps.getFirstNodeRef())! expect(node).toBeTruthy() await expect(node).not.toBeCollapsed() await expect(node).not.toBeBypassed() @@ -77,27 +77,27 @@ test.describe('Change Tracker', { tag: '@workflow' }, () => { // Bypass + collapse node await node.click('title') await node.click('collapse') - await comfyPage.ctrlB() + await comfyPage.keyboard.bypass() await expect(node).toBeCollapsed() await expect(node).toBeBypassed() // Undo, undo, ensure both changes undone - await comfyPage.ctrlZ() + await comfyPage.keyboard.undo() await expect(node).not.toBeBypassed() await expect(node).toBeCollapsed() - await comfyPage.ctrlZ() + await comfyPage.keyboard.undo() await expect(node).not.toBeBypassed() await expect(node).not.toBeCollapsed() // Prevent clicks registering a double-click - await comfyPage.clickEmptySpace() + await comfyPage.canvasOps.clickEmptySpace() await node.click('title') // Run again, but within a change transaction await beforeChange(comfyPage) await node.click('collapse') - await comfyPage.ctrlB() + await comfyPage.keyboard.bypass() await expect(node).toBeCollapsed() await expect(node).toBeBypassed() @@ -105,7 +105,7 @@ test.describe('Change Tracker', { tag: '@workflow' }, () => { await afterChange(comfyPage) // Ensure undo reverts both changes - await comfyPage.ctrlZ() + await comfyPage.keyboard.undo() await expect(node).not.toBeBypassed() await expect(node).not.toBeCollapsed() }) @@ -113,10 +113,10 @@ test.describe('Change Tracker', { tag: '@workflow' }, () => { test('Can nest multiple change transactions without adding undo steps', async ({ comfyPage }) => { - const node = (await comfyPage.getFirstNodeRef())! + const node = (await comfyPage.nodeOps.getFirstNodeRef())! const bypassAndPin = async () => { await beforeChange(comfyPage) - await comfyPage.ctrlB() + await comfyPage.keyboard.bypass() await expect(node).toBeBypassed() await comfyPage.page.keyboard.press('KeyP') await comfyPage.nextFrame() @@ -142,30 +142,30 @@ test.describe('Change Tracker', { tag: '@workflow' }, () => { await multipleChanges() - await comfyPage.ctrlZ() + await comfyPage.keyboard.undo() await expect(node).not.toBeBypassed() await expect(node).not.toBePinned() await expect(node).not.toBeCollapsed() - await comfyPage.ctrlY() + await comfyPage.keyboard.redo() await expect(node).toBeBypassed() await expect(node).toBePinned() await expect(node).toBeCollapsed() }) test('Can detect changes in workflow.extra', async ({ comfyPage }) => { - expect(await comfyPage.getUndoQueueSize()).toBe(0) + expect(await comfyPage.workflow.getUndoQueueSize()).toBe(0) await comfyPage.page.evaluate(() => { - window['app'].graph.extra.foo = 'bar' + window.app!.graph!.extra.foo = 'bar' }) // Click empty space to trigger a change detection. - await comfyPage.clickEmptySpace() - expect(await comfyPage.getUndoQueueSize()).toBe(1) + await comfyPage.canvasOps.clickEmptySpace() + expect(await comfyPage.workflow.getUndoQueueSize()).toBe(1) }) test('Ignores changes in workflow.ds', async ({ comfyPage }) => { - expect(await comfyPage.getUndoQueueSize()).toBe(0) - await comfyPage.pan({ x: 10, y: 10 }) - expect(await comfyPage.getUndoQueueSize()).toBe(0) + expect(await comfyPage.workflow.getUndoQueueSize()).toBe(0) + await comfyPage.canvasOps.pan({ x: 10, y: 10 }) + expect(await comfyPage.workflow.getUndoQueueSize()).toBe(0) }) }) diff --git a/browser_tests/tests/colorPalette.spec.ts b/browser_tests/tests/colorPalette.spec.ts index 0118955fb..5ec6453c0 100644 --- a/browser_tests/tests/colorPalette.spec.ts +++ b/browser_tests/tests/colorPalette.spec.ts @@ -1,13 +1,13 @@ import { expect } from '@playwright/test' -import type { Palette } from '../../src/schemas/colorPaletteSchema' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +import type { WorkspaceStore } from '../types/globals' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) -const customColorPalettes: Record = { +const customColorPalettes = { obsidian: { version: 102, id: 'obsidian', @@ -153,40 +153,48 @@ const customColorPalettes: Record = { test.describe('Color Palette', { tag: ['@screenshot', '@settings'] }, () => { test('Can show custom color palette', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.CustomColorPalettes', customColorPalettes) + await comfyPage.settings.setSetting( + 'Comfy.CustomColorPalettes', + customColorPalettes + ) // Reload to apply the new setting. Setting Comfy.CustomColorPalettes directly // doesn't update the store immediately. await comfyPage.setup() - await comfyPage.loadWorkflow('nodes/every_node_color') - await comfyPage.setSetting('Comfy.ColorPalette', 'obsidian_dark') + await comfyPage.workflow.loadWorkflow('nodes/every_node_color') + await comfyPage.settings.setSetting('Comfy.ColorPalette', 'obsidian_dark') await expect(comfyPage.canvas).toHaveScreenshot( 'custom-color-palette-obsidian-dark-all-colors.png' ) - await comfyPage.setSetting('Comfy.ColorPalette', 'light_red') + await comfyPage.settings.setSetting('Comfy.ColorPalette', 'light_red') await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot( 'custom-color-palette-light-red.png' ) - await comfyPage.setSetting('Comfy.ColorPalette', 'dark') + await comfyPage.settings.setSetting('Comfy.ColorPalette', 'dark') await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot('default-color-palette.png') }) test('Can add custom color palette', async ({ comfyPage }) => { - await comfyPage.page.evaluate((p) => { - window['app'].extensionManager.colorPalette.addCustomColorPalette(p) + await comfyPage.page.evaluate(async (p) => { + await ( + window.app!.extensionManager as WorkspaceStore + ).colorPalette.addCustomColorPalette(p) }, customColorPalettes.obsidian_dark) - expect(await comfyPage.getToastErrorCount()).toBe(0) + expect(await comfyPage.toast.getToastErrorCount()).toBe(0) - await comfyPage.setSetting('Comfy.ColorPalette', 'obsidian_dark') + await comfyPage.settings.setSetting('Comfy.ColorPalette', 'obsidian_dark') await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot( 'custom-color-palette-obsidian-dark.png' ) // Legacy `custom_` prefix is still supported - await comfyPage.setSetting('Comfy.ColorPalette', 'custom_obsidian_dark') + await comfyPage.settings.setSetting( + 'Comfy.ColorPalette', + 'custom_obsidian_dark' + ) await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot( 'custom-color-palette-obsidian-dark.png' @@ -199,20 +207,20 @@ test.describe( { tag: ['@screenshot', '@settings'] }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.loadWorkflow('nodes/every_node_color') + await comfyPage.workflow.loadWorkflow('nodes/every_node_color') }) test('should adjust opacity via node opacity setting', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Node.Opacity', 0.5) + await comfyPage.settings.setSetting('Comfy.Node.Opacity', 0.5) // Drag mouse to force canvas to redraw await comfyPage.page.mouse.move(0, 0) await expect(comfyPage.canvas).toHaveScreenshot('node-opacity-0.5.png') - await comfyPage.setSetting('Comfy.Node.Opacity', 1.0) + await comfyPage.settings.setSetting('Comfy.Node.Opacity', 1.0) await comfyPage.page.mouse.move(8, 8) await expect(comfyPage.canvas).toHaveScreenshot('node-opacity-1.png') @@ -221,8 +229,8 @@ test.describe( test('should persist color adjustments when changing themes', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Node.Opacity', 0.2) - await comfyPage.setSetting('Comfy.ColorPalette', 'arc') + await comfyPage.settings.setSetting('Comfy.Node.Opacity', 0.2) + await comfyPage.settings.setSetting('Comfy.ColorPalette', 'arc') await comfyPage.nextFrame() await comfyPage.page.mouse.move(0, 0) await expect(comfyPage.canvas).toHaveScreenshot( @@ -233,8 +241,8 @@ test.describe( test('should not serialize color adjustments in workflow', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Node.Opacity', 0.5) - await comfyPage.setSetting('Comfy.ColorPalette', 'light') + await comfyPage.settings.setSetting('Comfy.Node.Opacity', 0.5) + await comfyPage.settings.setSetting('Comfy.ColorPalette', 'light') await comfyPage.nextFrame() const parsed = await ( await comfyPage.page.waitForFunction( @@ -262,7 +270,7 @@ test.describe( test('should lighten node colors when switching to light theme', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.ColorPalette', 'light') + await comfyPage.settings.setSetting('Comfy.ColorPalette', 'light') await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot( 'node-lightened-colors.png' @@ -271,9 +279,9 @@ test.describe( test.describe('Context menu color adjustments', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.ColorPalette', 'light') - await comfyPage.setSetting('Comfy.Node.Opacity', 0.3) - const node = await comfyPage.getFirstNodeRef() + await comfyPage.settings.setSetting('Comfy.ColorPalette', 'light') + await comfyPage.settings.setSetting('Comfy.Node.Opacity', 0.3) + const node = await comfyPage.nodeOps.getFirstNodeRef() await node?.clickContextMenuOption('Colors') }) diff --git a/browser_tests/tests/commands.spec.ts b/browser_tests/tests/commands.spec.ts index acf99d177..d4e4f7fbf 100644 --- a/browser_tests/tests/commands.spec.ts +++ b/browser_tests/tests/commands.spec.ts @@ -3,52 +3,52 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Keybindings', { tag: '@keyboard' }, () => { test('Should execute command', async ({ comfyPage }) => { - await comfyPage.registerCommand('TestCommand', () => { - window['foo'] = true + await comfyPage.command.registerCommand('TestCommand', () => { + window.foo = true }) - await comfyPage.executeCommand('TestCommand') - expect(await comfyPage.page.evaluate(() => window['foo'])).toBe(true) + await comfyPage.command.executeCommand('TestCommand') + expect(await comfyPage.page.evaluate(() => window.foo)).toBe(true) }) test('Should execute async command', async ({ comfyPage }) => { - await comfyPage.registerCommand('TestCommand', async () => { + await comfyPage.command.registerCommand('TestCommand', async () => { await new Promise((resolve) => setTimeout(() => { - window['foo'] = true + window.foo = true resolve() }, 5) ) }) - await comfyPage.executeCommand('TestCommand') - expect(await comfyPage.page.evaluate(() => window['foo'])).toBe(true) + await comfyPage.command.executeCommand('TestCommand') + expect(await comfyPage.page.evaluate(() => window.foo)).toBe(true) }) test('Should handle command errors', async ({ comfyPage }) => { - await comfyPage.registerCommand('TestCommand', () => { + await comfyPage.command.registerCommand('TestCommand', () => { throw new Error('Test error') }) - await comfyPage.executeCommand('TestCommand') - expect(await comfyPage.getToastErrorCount()).toBe(1) + await comfyPage.command.executeCommand('TestCommand') + expect(await comfyPage.toast.getToastErrorCount()).toBe(1) }) test('Should handle async command errors', async ({ comfyPage }) => { - await comfyPage.registerCommand('TestCommand', async () => { - await new Promise((resolve, reject) => + await comfyPage.command.registerCommand('TestCommand', async () => { + await new Promise((_resolve, reject) => setTimeout(() => { reject(new Error('Test error')) }, 5) ) }) - await comfyPage.executeCommand('TestCommand') - expect(await comfyPage.getToastErrorCount()).toBe(1) + await comfyPage.command.executeCommand('TestCommand') + expect(await comfyPage.toast.getToastErrorCount()).toBe(1) }) }) diff --git a/browser_tests/tests/copyPaste.spec.ts b/browser_tests/tests/copyPaste.spec.ts index a04cc191b..4481d00c0 100644 --- a/browser_tests/tests/copyPaste.spec.ts +++ b/browser_tests/tests/copyPaste.spec.ts @@ -1,24 +1,31 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +import { DefaultGraphPositions } from '../fixtures/constants/defaultGraphPositions' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Copy Paste', { tag: ['@screenshot', '@workflow'] }, () => { test('Can copy and paste node', async ({ comfyPage }) => { - await comfyPage.clickEmptyLatentNode() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.emptyLatentWidgetClick + }) await comfyPage.page.mouse.move(10, 10) - await comfyPage.ctrlC() - await comfyPage.ctrlV() + await comfyPage.nextFrame() + await comfyPage.clipboard.copy() + await comfyPage.clipboard.paste() await expect(comfyPage.canvas).toHaveScreenshot('copied-node.png') }) test('Can copy and paste node with link', async ({ comfyPage }) => { - await comfyPage.clickTextEncodeNode1() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.textEncodeNode1 + }) + await comfyPage.nextFrame() await comfyPage.page.mouse.move(10, 10) - await comfyPage.ctrlC() + await comfyPage.clipboard.copy() await comfyPage.page.keyboard.press('Control+Shift+V') await expect(comfyPage.canvas).toHaveScreenshot('copied-node-with-link.png') }) @@ -28,9 +35,9 @@ test.describe('Copy Paste', { tag: ['@screenshot', '@workflow'] }, () => { await textBox.click() const originalString = await textBox.inputValue() await textBox.selectText() - await comfyPage.ctrlC(null) - await comfyPage.ctrlV(null) - await comfyPage.ctrlV(null) + await comfyPage.clipboard.copy(null) + await comfyPage.clipboard.paste(null) + await comfyPage.clipboard.paste(null) const resultString = await textBox.inputValue() expect(resultString).toBe(originalString + originalString) }) @@ -44,7 +51,7 @@ test.describe('Copy Paste', { tag: ['@screenshot', '@workflow'] }, () => { y: 281 } }) - await comfyPage.ctrlC(null) + await comfyPage.clipboard.copy(null) // Empty latent node's width await comfyPage.canvas.click({ position: { @@ -52,7 +59,7 @@ test.describe('Copy Paste', { tag: ['@screenshot', '@workflow'] }, () => { y: 643 } }) - await comfyPage.ctrlV(null) + await comfyPage.clipboard.paste(null) await comfyPage.page.keyboard.press('Enter') await expect(comfyPage.canvas).toHaveScreenshot('copied-widget-value.png') }) @@ -63,15 +70,19 @@ test.describe('Copy Paste', { tag: ['@screenshot', '@workflow'] }, () => { test('Paste in text area with node previously copied', async ({ comfyPage }) => { - await comfyPage.clickEmptyLatentNode() - await comfyPage.ctrlC(null) + await comfyPage.canvas.click({ + position: DefaultGraphPositions.emptyLatentWidgetClick + }) + await comfyPage.page.mouse.move(10, 10) + await comfyPage.nextFrame() + await comfyPage.clipboard.copy(null) const textBox = comfyPage.widgetTextBox await textBox.click() await textBox.inputValue() await textBox.selectText() - await comfyPage.ctrlC(null) - await comfyPage.ctrlV(null) - await comfyPage.ctrlV(null) + await comfyPage.clipboard.copy(null) + await comfyPage.clipboard.paste(null) + await comfyPage.clipboard.paste(null) await expect(comfyPage.canvas).toHaveScreenshot( 'paste-in-text-area-with-node-previously-copied.png' ) @@ -82,10 +93,10 @@ test.describe('Copy Paste', { tag: ['@screenshot', '@workflow'] }, () => { await textBox.click() await textBox.inputValue() await textBox.selectText() - await comfyPage.ctrlC(null) + await comfyPage.clipboard.copy(null) // Unfocus textbox. await comfyPage.page.mouse.click(10, 10) - await comfyPage.ctrlV(null) + await comfyPage.clipboard.paste(null) await expect(comfyPage.canvas).toHaveScreenshot('no-node-copied.png') }) @@ -103,19 +114,19 @@ test.describe('Copy Paste', { tag: ['@screenshot', '@workflow'] }, () => { test('Can undo paste multiple nodes as single action', async ({ comfyPage }) => { - const initialCount = await comfyPage.getGraphNodesCount() + const initialCount = await comfyPage.nodeOps.getGraphNodesCount() expect(initialCount).toBeGreaterThan(1) await comfyPage.canvas.click() - await comfyPage.ctrlA() + await comfyPage.keyboard.selectAll() await comfyPage.page.mouse.move(10, 10) - await comfyPage.ctrlC() - await comfyPage.ctrlV() + await comfyPage.clipboard.copy() + await comfyPage.clipboard.paste() - const pasteCount = await comfyPage.getGraphNodesCount() + const pasteCount = await comfyPage.nodeOps.getGraphNodesCount() expect(pasteCount).toBe(initialCount * 2) - await comfyPage.ctrlZ() - const undoCount = await comfyPage.getGraphNodesCount() + await comfyPage.keyboard.undo() + const undoCount = await comfyPage.nodeOps.getGraphNodesCount() expect(undoCount).toBe(initialCount) }) }) diff --git a/browser_tests/tests/copyPaste.spec.ts-snapshots/copied-node-chromium-linux.png b/browser_tests/tests/copyPaste.spec.ts-snapshots/copied-node-chromium-linux.png index 782465751..f3d85d8da 100644 Binary files a/browser_tests/tests/copyPaste.spec.ts-snapshots/copied-node-chromium-linux.png and b/browser_tests/tests/copyPaste.spec.ts-snapshots/copied-node-chromium-linux.png differ diff --git a/browser_tests/tests/copyPaste.spec.ts-snapshots/paste-in-text-area-with-node-previously-copied-chromium-linux.png b/browser_tests/tests/copyPaste.spec.ts-snapshots/paste-in-text-area-with-node-previously-copied-chromium-linux.png index b1f181c75..e589ba6a7 100644 Binary files a/browser_tests/tests/copyPaste.spec.ts-snapshots/paste-in-text-area-with-node-previously-copied-chromium-linux.png and b/browser_tests/tests/copyPaste.spec.ts-snapshots/paste-in-text-area-with-node-previously-copied-chromium-linux.png differ diff --git a/browser_tests/tests/customIcons.spec.ts b/browser_tests/tests/customIcons.spec.ts index eb4eccfd5..ed9680406 100644 --- a/browser_tests/tests/customIcons.spec.ts +++ b/browser_tests/tests/customIcons.spec.ts @@ -24,7 +24,7 @@ async function verifyCustomIconSvg(iconElement: Locator) { test.describe('Custom Icons', { tag: '@settings' }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') }) test('sidebar tab icons use custom SVGs', async ({ comfyPage }) => { diff --git a/browser_tests/tests/dialog.spec.ts b/browser_tests/tests/dialog.spec.ts index cfa1c8cd2..fddbfbc70 100644 --- a/browser_tests/tests/dialog.spec.ts +++ b/browser_tests/tests/dialog.spec.ts @@ -1,18 +1,19 @@ import type { Locator } from '@playwright/test' import { expect } from '@playwright/test' -import type { Keybinding } from '../../src/platform/keybindings' +import type { Keybinding } from '../../src/platform/keybindings/types' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +import { DefaultGraphPositions } from '../fixtures/constants/defaultGraphPositions' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Load workflow warning', { tag: '@ui' }, () => { test('Should display a warning when loading a workflow with missing nodes', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('missing/missing_nodes') + await comfyPage.workflow.loadWorkflow('missing/missing_nodes') // Wait for the element with the .comfy-missing-nodes selector to be visible const missingNodesWarning = comfyPage.page.locator('.comfy-missing-nodes') @@ -22,7 +23,7 @@ test.describe('Load workflow warning', { tag: '@ui' }, () => { test('Should display a warning when loading a workflow with missing nodes in subgraphs', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('missing/missing_nodes_in_subgraph') + await comfyPage.workflow.loadWorkflow('missing/missing_nodes_in_subgraph') // Wait for the element with the .comfy-missing-nodes selector to be visible const missingNodesWarning = comfyPage.page.locator('.comfy-missing-nodes') @@ -36,22 +37,26 @@ test.describe('Load workflow warning', { tag: '@ui' }, () => { }) test('Does not report warning on undo/redo', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default') + await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default') - await comfyPage.loadWorkflow('missing/missing_nodes') - await comfyPage.closeDialog() + await comfyPage.workflow.loadWorkflow('missing/missing_nodes') + await comfyPage.page + .locator('.p-dialog') + .getByRole('button', { name: 'Close' }) + .click({ force: true }) + await comfyPage.page.locator('.p-dialog').waitFor({ state: 'hidden' }) // Wait for any async operations to complete after dialog closes await comfyPage.nextFrame() // Make a change to the graph - await comfyPage.doubleClickCanvas() + await comfyPage.canvasOps.doubleClick() await comfyPage.searchBox.fillAndSelectFirstNode('KSampler') // Undo and redo the change - await comfyPage.ctrlZ() + await comfyPage.keyboard.undo() await expect(comfyPage.page.locator('.comfy-missing-nodes')).not.toBeVisible() - await comfyPage.ctrlY() + await comfyPage.keyboard.redo() await expect(comfyPage.page.locator('.comfy-missing-nodes')).not.toBeVisible() }) @@ -59,7 +64,7 @@ test.describe('Execution error', () => { test('Should display an error message when an execution error occurs', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('nodes/execution_error') + await comfyPage.workflow.loadWorkflow('nodes/execution_error') await comfyPage.queueButton.click() await comfyPage.nextFrame() @@ -71,7 +76,10 @@ test.describe('Execution error', () => { test.describe('Missing models warning', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Workflow.ShowMissingModelsWarning', true) + await comfyPage.settings.setSetting( + 'Comfy.Workflow.ShowMissingModelsWarning', + true + ) await comfyPage.page.evaluate((url: string) => { return fetch(`${url}/api/devtools/cleanup_fake_model`) }, comfyPage.url) @@ -80,7 +88,7 @@ test.describe('Missing models warning', () => { test('Should display a warning when missing models are found', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('missing/missing_models') + await comfyPage.workflow.loadWorkflow('missing/missing_models') const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models') await expect(missingModelsWarning).toBeVisible() @@ -97,7 +105,9 @@ test.describe('Missing models warning', () => { comfyPage }) => { // Load workflow that has a node with models metadata at the node level - await comfyPage.loadWorkflow('missing/missing_models_from_node_properties') + await comfyPage.workflow.loadWorkflow( + 'missing/missing_models_from_node_properties' + ) const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models') await expect(missingModelsWarning).toBeVisible() @@ -146,7 +156,7 @@ test.describe('Missing models warning', () => { { times: 1 } ) - await comfyPage.loadWorkflow('missing/missing_models') + await comfyPage.workflow.loadWorkflow('missing/missing_models') const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models') await expect(missingModelsWarning).not.toBeVisible() @@ -157,7 +167,9 @@ test.describe('Missing models warning', () => { }) => { // This tests the scenario where outdated model metadata exists in the workflow // but the actual selected models (widget values) have changed - await comfyPage.loadWorkflow('missing/model_metadata_widget_mismatch') + await comfyPage.workflow.loadWorkflow( + 'missing/model_metadata_widget_mismatch' + ) // The missing models warning should NOT appear const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models') @@ -171,7 +183,7 @@ test.describe('Missing models warning', () => { }) => { // The fake_model.safetensors is served by // https://github.com/Comfy-Org/ComfyUI_devtools/blob/main/__init__.py - await comfyPage.loadWorkflow('missing/missing_models') + await comfyPage.workflow.loadWorkflow('missing/missing_models') const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models') await expect(missingModelsWarning).toBeVisible() @@ -190,11 +202,11 @@ test.describe('Missing models warning', () => { let closeButton: Locator test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting( + await comfyPage.settings.setSetting( 'Comfy.Workflow.ShowMissingModelsWarning', true ) - await comfyPage.loadWorkflow('missing/missing_models') + await comfyPage.workflow.loadWorkflow('missing/missing_models') checkbox = comfyPage.page.getByLabel("Don't show this again") closeButton = comfyPage.page.getByLabel('Close') @@ -210,7 +222,7 @@ test.describe('Missing models warning', () => { await closeButton.click() await changeSettingPromise - const settingValue = await comfyPage.getSetting( + const settingValue = await comfyPage.settings.getSetting( 'Comfy.Workflow.ShowMissingModelsWarning' ) expect(settingValue).toBe(false) @@ -221,7 +233,7 @@ test.describe('Missing models warning', () => { }) => { await closeButton.click() - const settingValue = await comfyPage.getSetting( + const settingValue = await comfyPage.settings.getSetting( 'Comfy.Workflow.ShowMissingModelsWarning' ) expect(settingValue).toBe(true) @@ -252,9 +264,11 @@ test.describe('Settings', () => { test('Can change canvas zoom speed setting', async ({ comfyPage }) => { const maxSpeed = 2.5 - await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', maxSpeed) + await comfyPage.settings.setSetting('Comfy.Graph.ZoomSpeed', maxSpeed) await test.step('Setting should persist', async () => { - expect(await comfyPage.getSetting('Comfy.Graph.ZoomSpeed')).toBe(maxSpeed) + expect(await comfyPage.settings.getSetting('Comfy.Graph.ZoomSpeed')).toBe( + maxSpeed + ) }) }) @@ -311,7 +325,7 @@ test.describe('Support', () => { test('Should open external zendesk link with OSS tag', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') const pagePromise = comfyPage.page.context().waitForEvent('page') await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Support']) const newPage = await pagePromise @@ -331,13 +345,13 @@ test.describe('Error dialog', () => { comfyPage }) => { await comfyPage.page.evaluate(() => { - const graph = window['graph'] - graph.configure = () => { + const graph = window.graph! + ;(graph as { configure: () => void }).configure = () => { throw new Error('Error on configure!') } }) - await comfyPage.loadWorkflow('default') + await comfyPage.workflow.loadWorkflow('default') const errorDialog = comfyPage.page.locator('.comfy-error-report') await expect(errorDialog).toBeVisible() @@ -347,7 +361,7 @@ test.describe('Error dialog', () => { comfyPage }) => { await comfyPage.page.evaluate(async () => { - const app = window['app'] + const app = window.app! app.api.queuePrompt = () => { throw new Error('Error on queuePrompt!') } @@ -362,9 +376,13 @@ test.describe('Signin dialog', () => { test('Paste content to signin dialog should not paste node on canvas', async ({ comfyPage }) => { - const nodeNum = (await comfyPage.getNodes()).length - await comfyPage.clickEmptyLatentNode() - await comfyPage.ctrlC() + const nodeNum = (await comfyPage.nodeOps.getNodes()).length + await comfyPage.canvas.click({ + position: DefaultGraphPositions.emptyLatentWidgetClick + }) + await comfyPage.page.mouse.move(10, 10) + await comfyPage.nextFrame() + await comfyPage.clipboard.copy() const textBox = comfyPage.widgetTextBox await textBox.click() @@ -373,7 +391,7 @@ test.describe('Signin dialog', () => { await textBox.press('Control+c') await comfyPage.page.evaluate(() => { - void window['app'].extensionManager.dialog.showSignInDialog() + void window.app!.extensionManager.dialog.showSignInDialog() }) const input = comfyPage.page.locator('#comfy-org-sign-in-password') @@ -381,6 +399,6 @@ test.describe('Signin dialog', () => { await input.press('Control+v') await expect(input).toHaveValue('test_password') - expect(await comfyPage.getNodes()).toHaveLength(nodeNum) + expect(await comfyPage.nodeOps.getNodes()).toHaveLength(nodeNum) }) }) diff --git a/browser_tests/tests/domWidget.spec.ts b/browser_tests/tests/domWidget.spec.ts index d0468210e..d8e2dab9b 100644 --- a/browser_tests/tests/domWidget.spec.ts +++ b/browser_tests/tests/domWidget.spec.ts @@ -3,12 +3,12 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('DOM Widget', { tag: '@widget' }, () => { test('Collapsed multiline textarea is not visible', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('widgets/collapsed_multiline') + await comfyPage.workflow.loadWorkflow('widgets/collapsed_multiline') const textareaWidget = comfyPage.page.locator('.comfy-multiline-input') await expect(textareaWidget).not.toBeVisible() }) @@ -21,7 +21,7 @@ test.describe('DOM Widget', { tag: '@widget' }, () => { await expect(firstMultiline).toBeVisible() await expect(lastMultiline).toBeVisible() - const nodes = await comfyPage.getNodeRefsByType('CLIPTextEncode') + const nodes = await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode') for (const node of nodes) { await node.click('collapse') } @@ -33,8 +33,8 @@ test.describe('DOM Widget', { tag: '@widget' }, () => { 'Position update when entering focus mode', { tag: '@screenshot' }, async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') - await comfyPage.executeCommand('Workspace.ToggleFocusMode') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.command.executeCommand('Workspace.ToggleFocusMode') await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot('focus-mode-on.png') } @@ -68,9 +68,9 @@ test.describe('DOM Widget', { tag: '@widget' }, () => { .first() await expect(textareaWidget).toBeVisible() - await comfyPage.setSetting('Comfy.Sidebar.Size', 'small') - await comfyPage.setSetting('Comfy.Sidebar.Location', 'left') - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.Sidebar.Size', 'small') + await comfyPage.settings.setSetting('Comfy.Sidebar.Location', 'left') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.nextFrame() let oldPos: [number, number] @@ -85,15 +85,15 @@ test.describe('DOM Widget', { tag: '@widget' }, () => { // --- test --- - await comfyPage.setSetting('Comfy.Sidebar.Size', 'normal') + await comfyPage.settings.setSetting('Comfy.Sidebar.Size', 'normal') await comfyPage.nextFrame() await checkBboxChange() - await comfyPage.setSetting('Comfy.Sidebar.Location', 'right') + await comfyPage.settings.setSetting('Comfy.Sidebar.Location', 'right') await comfyPage.nextFrame() await checkBboxChange() - await comfyPage.setSetting('Comfy.UseNewMenu', 'Bottom') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Bottom') await comfyPage.nextFrame() await checkBboxChange() }) diff --git a/browser_tests/tests/execution.spec.ts b/browser_tests/tests/execution.spec.ts index 2173c8623..1c8b68269 100644 --- a/browser_tests/tests/execution.spec.ts +++ b/browser_tests/tests/execution.spec.ts @@ -3,7 +3,7 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Execution', { tag: ['@smoke', '@workflow'] }, () => { @@ -11,12 +11,15 @@ test.describe('Execution', { tag: ['@smoke', '@workflow'] }, () => { 'Report error on unconnected slot', { tag: '@screenshot' }, async ({ comfyPage }) => { - await comfyPage.disconnectEdge() - await comfyPage.clickEmptySpace() + await comfyPage.canvasOps.disconnectEdge() + await comfyPage.canvasOps.clickEmptySpace() - await comfyPage.executeCommand('Comfy.QueuePrompt') + await comfyPage.command.executeCommand('Comfy.QueuePrompt') await expect(comfyPage.page.locator('.comfy-error-report')).toBeVisible() - await comfyPage.page.locator('.p-dialog-close-button').click() + await comfyPage.page + .locator('.p-dialog') + .getByRole('button', { name: 'Close' }) + .click() await comfyPage.page.locator('.comfy-error-report').waitFor({ state: 'hidden' }) @@ -32,17 +35,17 @@ test.describe( { tag: ['@smoke', '@workflow'] }, () => { test('Execute to selected output nodes', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('execution/partial_execution') - const input = await comfyPage.getNodeRefById(3) - const output1 = await comfyPage.getNodeRefById(1) - const output2 = await comfyPage.getNodeRefById(4) + await comfyPage.workflow.loadWorkflow('execution/partial_execution') + const input = await comfyPage.nodeOps.getNodeRefById(3) + const output1 = await comfyPage.nodeOps.getNodeRefById(1) + const output2 = await comfyPage.nodeOps.getNodeRefById(4) expect(await (await input.getWidget(0)).getValue()).toBe('foo') expect(await (await output1.getWidget(0)).getValue()).toBe('') expect(await (await output2.getWidget(0)).getValue()).toBe('') await output1.click('title') - await comfyPage.executeCommand('Comfy.QueueSelectedOutputNodes') + await comfyPage.command.executeCommand('Comfy.QueueSelectedOutputNodes') await expect(async () => { expect(await (await input.getWidget(0)).getValue()).toBe('foo') expect(await (await output1.getWidget(0)).getValue()).toBe('foo') diff --git a/browser_tests/tests/extensionAPI.spec.ts b/browser_tests/tests/extensionAPI.spec.ts index 38f4a6c1d..97d4424c9 100644 --- a/browser_tests/tests/extensionAPI.spec.ts +++ b/browser_tests/tests/extensionAPI.spec.ts @@ -1,23 +1,32 @@ import { expect } from '@playwright/test' +import type { Settings } from '../../src/schemas/apiSchema' import type { SettingParams } from '../../src/platform/settings/types' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +/** + * Type helper for test settings with arbitrary IDs. + * Extensions can register settings with any ID, but SettingParams.id + * is typed as keyof Settings for autocomplete. This helper allows + * arbitrary IDs in tests while keeping type safety for other fields. + */ +type TestSettingId = keyof Settings + test.describe('Topbar commands', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') }) test('Should allow registering topbar commands', async ({ comfyPage }) => { await comfyPage.page.evaluate(() => { - window['app'].registerExtension({ + window.app!.registerExtension({ name: 'TestExtension1', commands: [ { id: 'foo', label: 'foo-command', function: () => { - window['foo'] = true + window.foo = true } } ], @@ -31,15 +40,15 @@ test.describe('Topbar commands', () => { }) await comfyPage.menu.topbar.triggerTopbarCommand(['ext', 'foo-command']) - expect(await comfyPage.page.evaluate(() => window['foo'])).toBe(true) + expect(await comfyPage.page.evaluate(() => window.foo)).toBe(true) }) test('Should not allow register command defined in other extension', async ({ comfyPage }) => { - await comfyPage.registerCommand('foo', () => alert(1)) + await comfyPage.command.registerCommand('foo', () => alert(1)) await comfyPage.page.evaluate(() => { - window['app'].registerExtension({ + window.app!.registerExtension({ name: 'TestExtension1', menuCommands: [ { @@ -56,14 +65,14 @@ test.describe('Topbar commands', () => { test('Should allow registering keybindings', async ({ comfyPage }) => { await comfyPage.page.evaluate(() => { - const app = window['app'] + const app = window.app! app.registerExtension({ name: 'TestExtension1', commands: [ { id: 'TestCommand', function: () => { - window['TestCommand'] = true + window.TestCommand = true } } ], @@ -77,68 +86,77 @@ test.describe('Topbar commands', () => { }) await comfyPage.page.keyboard.press('k') - expect(await comfyPage.page.evaluate(() => window['TestCommand'])).toBe( - true - ) + expect(await comfyPage.page.evaluate(() => window.TestCommand)).toBe(true) }) test.describe('Settings', () => { test('Should allow adding settings', async ({ comfyPage }) => { await comfyPage.page.evaluate(() => { - window['app'].registerExtension({ + window.app!.registerExtension({ name: 'TestExtension1', settings: [ { - id: 'TestSetting', + // Extensions can register arbitrary setting IDs + id: 'TestSetting' as TestSettingId, name: 'Test Setting', type: 'text', defaultValue: 'Hello, world!', onChange: () => { - window['changeCount'] = (window['changeCount'] ?? 0) + 1 + window.changeCount = (window.changeCount ?? 0) + 1 } } ] }) }) // onChange is called when the setting is first added - expect(await comfyPage.page.evaluate(() => window['changeCount'])).toBe(1) - expect(await comfyPage.getSetting('TestSetting')).toBe('Hello, world!') + expect(await comfyPage.page.evaluate(() => window.changeCount)).toBe(1) + expect(await comfyPage.settings.getSetting('TestSetting')).toBe( + 'Hello, world!' + ) - await comfyPage.setSetting('TestSetting', 'Hello, universe!') - expect(await comfyPage.getSetting('TestSetting')).toBe('Hello, universe!') - expect(await comfyPage.page.evaluate(() => window['changeCount'])).toBe(2) + await comfyPage.settings.setSetting('TestSetting', 'Hello, universe!') + expect(await comfyPage.settings.getSetting('TestSetting')).toBe( + 'Hello, universe!' + ) + expect(await comfyPage.page.evaluate(() => window.changeCount)).toBe(2) }) test('Should allow setting boolean settings', async ({ comfyPage }) => { await comfyPage.page.evaluate(() => { - window['app'].registerExtension({ + window.app!.registerExtension({ name: 'TestExtension1', settings: [ { - id: 'Comfy.TestSetting', + // Extensions can register arbitrary setting IDs + id: 'Comfy.TestSetting' as TestSettingId, name: 'Test Setting', type: 'boolean', defaultValue: false, onChange: () => { - window['changeCount'] = (window['changeCount'] ?? 0) + 1 + window.changeCount = (window.changeCount ?? 0) + 1 } } ] }) }) - expect(await comfyPage.getSetting('Comfy.TestSetting')).toBe(false) - expect(await comfyPage.page.evaluate(() => window['changeCount'])).toBe(1) + expect(await comfyPage.settings.getSetting('Comfy.TestSetting')).toBe( + false + ) + expect(await comfyPage.page.evaluate(() => window.changeCount)).toBe(1) await comfyPage.settingDialog.open() await comfyPage.settingDialog.toggleBooleanSetting('Comfy.TestSetting') - expect(await comfyPage.getSetting('Comfy.TestSetting')).toBe(true) - expect(await comfyPage.page.evaluate(() => window['changeCount'])).toBe(2) + expect(await comfyPage.settings.getSetting('Comfy.TestSetting')).toBe( + true + ) + expect(await comfyPage.page.evaluate(() => window.changeCount)).toBe(2) }) test.describe('Passing through attrs to setting components', () => { const testCases: Array<{ - config: Partial + config: Pick & + Partial> selector: string }> = [ { @@ -191,13 +209,13 @@ test.describe('Topbar commands', () => { comfyPage }) => { await comfyPage.page.evaluate((config) => { - window['app'].registerExtension({ + window.app!.registerExtension({ name: 'TestExtension1', settings: [ { - id: 'Comfy.TestSetting', + // Extensions can register arbitrary setting IDs + id: 'Comfy.TestSetting' as TestSettingId, name: 'Test', - // The `disabled` attr is common to all settings components attrs: { disabled: true }, ...config } @@ -224,7 +242,7 @@ test.describe('Topbar commands', () => { test.describe('About panel', () => { test('Should allow adding badges', async ({ comfyPage }) => { await comfyPage.page.evaluate(() => { - window['app'].registerExtension({ + window.app!.registerExtension({ name: 'TestExtension1', aboutPageBadges: [ { @@ -247,61 +265,71 @@ test.describe('Topbar commands', () => { test.describe('Dialog', () => { test('Should allow showing a prompt dialog', async ({ comfyPage }) => { await comfyPage.page.evaluate(() => { - void window['app'].extensionManager.dialog - .prompt({ + void window + .app!.extensionManager.dialog.prompt({ title: 'Test Prompt', message: 'Test Prompt Message' }) - .then((value: string) => { - window['value'] = value + .then((value: string | null) => { + ;(window as unknown as Record)['value'] = value }) }) - await comfyPage.fillPromptDialog('Hello, world!') - expect(await comfyPage.page.evaluate(() => window['value'])).toBe( - 'Hello, world!' - ) + await comfyPage.nodeOps.fillPromptDialog('Hello, world!') + expect( + await comfyPage.page.evaluate( + () => (window as unknown as Record)['value'] + ) + ).toBe('Hello, world!') }) test('Should allow showing a confirmation dialog', async ({ comfyPage }) => { await comfyPage.page.evaluate(() => { - void window['app'].extensionManager.dialog - .confirm({ + void window + .app!.extensionManager.dialog.confirm({ title: 'Test Confirm', message: 'Test Confirm Message' }) - .then((value: boolean) => { - window['value'] = value + .then((value: boolean | null) => { + ;(window as unknown as Record)['value'] = value }) }) await comfyPage.confirmDialog.click('confirm') - expect(await comfyPage.page.evaluate(() => window['value'])).toBe(true) + expect( + await comfyPage.page.evaluate( + () => (window as unknown as Record)['value'] + ) + ).toBe(true) }) test('Should allow dismissing a dialog', async ({ comfyPage }) => { await comfyPage.page.evaluate(() => { - window['value'] = 'foo' - void window['app'].extensionManager.dialog - .confirm({ + ;(window as unknown as Record)['value'] = 'foo' + void window + .app!.extensionManager.dialog.confirm({ title: 'Test Confirm', message: 'Test Confirm Message' }) - .then((value: boolean) => { - window['value'] = value + .then((value: boolean | null) => { + ;(window as unknown as Record)['value'] = value }) }) await comfyPage.confirmDialog.click('reject') - expect(await comfyPage.page.evaluate(() => window['value'])).toBeNull() + expect( + await comfyPage.page.evaluate( + () => (window as unknown as Record)['value'] + ) + ).toBeNull() }) }) test.describe('Selection Toolbox', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true) + await comfyPage.settings.setSetting('Comfy.Canvas.SelectionToolbox', true) }) test('Should allow adding commands to selection toolbox', async ({ @@ -309,7 +337,7 @@ test.describe('Topbar commands', () => { }) => { // Register an extension with a selection toolbox command await comfyPage.page.evaluate(() => { - window['app'].registerExtension({ + window.app!.registerExtension({ name: 'TestExtension1', commands: [ { @@ -317,7 +345,9 @@ test.describe('Topbar commands', () => { label: 'Test Command', icon: 'pi pi-star', function: () => { - window['selectionCommandExecuted'] = true + ;(window as unknown as Record)[ + 'selectionCommandExecuted' + ] = true } } ], @@ -325,7 +355,7 @@ test.describe('Topbar commands', () => { }) }) - await comfyPage.selectNodes(['CLIP Text Encode (Prompt)']) + await comfyPage.nodeOps.selectNodes(['CLIP Text Encode (Prompt)']) // Click the command button in the selection toolbox const toolboxButton = comfyPage.page.locator( @@ -333,9 +363,13 @@ test.describe('Topbar commands', () => { ) await toolboxButton.click() - // Verify the command was executed expect( - await comfyPage.page.evaluate(() => window['selectionCommandExecuted']) + await comfyPage.page.evaluate( + () => + (window as unknown as Record)[ + 'selectionCommandExecuted' + ] + ) ).toBe(true) }) }) diff --git a/browser_tests/tests/featureFlags.spec.ts b/browser_tests/tests/featureFlags.spec.ts index 73f1cd2f5..aec0fc5bc 100644 --- a/browser_tests/tests/featureFlags.spec.ts +++ b/browser_tests/tests/featureFlags.spec.ts @@ -3,7 +3,7 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => { @@ -25,7 +25,7 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => { const originalSend = WebSocket.prototype.send WebSocket.prototype.send = function (data) { try { - const parsed = JSON.parse(data) + const parsed = JSON.parse(data as string) if (parsed.type === 'feature_flags') { window.__capturedMessages!.clientFeatureFlags = parsed } @@ -38,11 +38,11 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => { // Monitor for server feature flags const checkInterval = setInterval(() => { if ( - window['app']?.api?.serverFeatureFlags && - Object.keys(window['app'].api.serverFeatureFlags).length > 0 + window.app?.api?.serverFeatureFlags && + Object.keys(window.app.api.serverFeatureFlags).length > 0 ) { window.__capturedMessages!.serverFeatureFlags = - window['app'].api.serverFeatureFlags + window.app.api.serverFeatureFlags clearInterval(checkInterval) } }, 100) @@ -96,7 +96,7 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => { }) => { // Get the actual server feature flags from the backend const serverFlags = await comfyPage.page.evaluate(() => { - return window['app']!.api.serverFeatureFlags + return window.app!.api.serverFeatureFlags }) // Verify we received real feature flags from the backend @@ -115,26 +115,22 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => { }) => { // Test serverSupportsFeature with real backend flags const supportsPreviewMetadata = await comfyPage.page.evaluate(() => { - return window['app']!.api.serverSupportsFeature( - 'supports_preview_metadata' - ) + return window.app!.api.serverSupportsFeature('supports_preview_metadata') }) // The method should return a boolean based on the backend's value expect(typeof supportsPreviewMetadata).toBe('boolean') // Test non-existent feature - should always return false const supportsNonExistent = await comfyPage.page.evaluate(() => { - return window['app']!.api.serverSupportsFeature( - 'non_existent_feature_xyz' - ) + return window.app!.api.serverSupportsFeature('non_existent_feature_xyz') }) expect(supportsNonExistent).toBe(false) // Test that the method only returns true for boolean true values const testResults = await comfyPage.page.evaluate(() => { // Temporarily modify serverFeatureFlags to test behavior - const original = window['app']!.api.serverFeatureFlags - window['app']!.api.serverFeatureFlags = { + const original = window.app!.api.serverFeatureFlags + window.app!.api.serverFeatureFlags = { bool_true: true, bool_false: false, string_value: 'yes', @@ -143,15 +139,15 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => { } const results = { - bool_true: window['app']!.api.serverSupportsFeature('bool_true'), - bool_false: window['app']!.api.serverSupportsFeature('bool_false'), - string_value: window['app']!.api.serverSupportsFeature('string_value'), - number_value: window['app']!.api.serverSupportsFeature('number_value'), - null_value: window['app']!.api.serverSupportsFeature('null_value') + bool_true: window.app!.api.serverSupportsFeature('bool_true'), + bool_false: window.app!.api.serverSupportsFeature('bool_false'), + string_value: window.app!.api.serverSupportsFeature('string_value'), + number_value: window.app!.api.serverSupportsFeature('number_value'), + null_value: window.app!.api.serverSupportsFeature('null_value') } // Restore original - window['app']!.api.serverFeatureFlags = original + window.app!.api.serverFeatureFlags = original return results }) @@ -168,20 +164,20 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => { }) => { // Test getServerFeature method const previewMetadataValue = await comfyPage.page.evaluate(() => { - return window['app']!.api.getServerFeature('supports_preview_metadata') + return window.app!.api.getServerFeature('supports_preview_metadata') }) expect(typeof previewMetadataValue).toBe('boolean') // Test getting max_upload_size const maxUploadSize = await comfyPage.page.evaluate(() => { - return window['app']!.api.getServerFeature('max_upload_size') + return window.app!.api.getServerFeature('max_upload_size') }) expect(typeof maxUploadSize).toBe('number') expect(maxUploadSize).toBeGreaterThan(0) // Test getServerFeature with default value for non-existent feature const defaultValue = await comfyPage.page.evaluate(() => { - return window['app']!.api.getServerFeature( + return window.app!.api.getServerFeature( 'non_existent_feature_xyz', 'default' ) @@ -194,7 +190,7 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => { }) => { // Test getServerFeatures returns all flags const allFeatures = await comfyPage.page.evaluate(() => { - return window['app']!.api.getServerFeatures() + return window.app!.api.getServerFeatures() }) expect(allFeatures).toBeTruthy() @@ -207,14 +203,14 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => { test('Client feature flags are immutable', async ({ comfyPage }) => { // Test that getClientFeatureFlags returns a copy const immutabilityTest = await comfyPage.page.evaluate(() => { - const flags1 = window['app']!.api.getClientFeatureFlags() - const flags2 = window['app']!.api.getClientFeatureFlags() + const flags1 = window.app!.api.getClientFeatureFlags() + const flags2 = window.app!.api.getClientFeatureFlags() // Modify the first object flags1.test_modification = true // Get flags again to check if original was modified - const flags3 = window['app']!.api.getClientFeatureFlags() + const flags3 = window.app!.api.getClientFeatureFlags() return { areEqual: flags1 === flags2, @@ -240,14 +236,14 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => { }) => { const immutabilityTest = await comfyPage.page.evaluate(() => { // Get a copy of server features - const features1 = window['app']!.api.getServerFeatures() + const features1 = window.app!.api.getServerFeatures() // Try to modify it features1.supports_preview_metadata = false features1.new_feature = 'added' // Get another copy - const features2 = window['app']!.api.getServerFeatures() + const features2 = window.app!.api.getServerFeatures() return { modifiedValue: features1.supports_preview_metadata, @@ -286,35 +282,26 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => { // Monitor when feature flags arrive by checking periodically const checkFeatureFlags = setInterval(() => { if ( - window['app']?.api?.serverFeatureFlags?.supports_preview_metadata !== + window.app?.api?.serverFeatureFlags?.supports_preview_metadata !== undefined ) { - window.__appReadiness = { - ...window.__appReadiness, - featureFlagsReceived: true - } + window.__appReadiness!.featureFlagsReceived = true clearInterval(checkFeatureFlags) } }, 10) // Monitor API initialization const checkApi = setInterval(() => { - if (window['app']?.api) { - window.__appReadiness = { - ...window.__appReadiness, - apiInitialized: true - } + if (window.app?.api) { + window.__appReadiness!.apiInitialized = true clearInterval(checkApi) } }, 10) // Monitor app initialization const checkApp = setInterval(() => { - if (window['app']?.graph) { - window.__appReadiness = { - ...window.__appReadiness, - appInitialized: true - } + if (window.app?.graph) { + window.__appReadiness!.appInitialized = true clearInterval(checkApp) } }, 10) @@ -333,7 +320,7 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => { // Wait for feature flags to be received await newPage.waitForFunction( () => - window['app']?.api?.serverFeatureFlags?.supports_preview_metadata !== + window.app?.api?.serverFeatureFlags?.supports_preview_metadata !== undefined, { timeout: 10000 @@ -344,7 +331,7 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => { const readiness = await newPage.evaluate(() => { return { ...window.__appReadiness, - currentFlags: window['app']!.api.serverFeatureFlags + currentFlags: window.app!.api.serverFeatureFlags } }) diff --git a/browser_tests/tests/graph.spec.ts b/browser_tests/tests/graph.spec.ts index 340b5b697..9ddd1e48c 100644 --- a/browser_tests/tests/graph.spec.ts +++ b/browser_tests/tests/graph.spec.ts @@ -3,24 +3,24 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Graph', { tag: ['@smoke', '@canvas'] }, () => { // Should be able to fix link input slot index after swap the input order // Ref: https://github.com/Comfy-Org/ComfyUI_frontend/issues/3348 test('Fix link input slots', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('inputs/input_order_swap') + await comfyPage.workflow.loadWorkflow('inputs/input_order_swap') expect( await comfyPage.page.evaluate(() => { - return window['app'].graph.links.get(1)?.target_slot + return window.app!.graph!.links.get(1)?.target_slot }) ).toBe(1) }) test('Validate workflow links', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Validation.Workflows', true) - await comfyPage.loadWorkflow('links/bad_link') - await expect(comfyPage.getVisibleToastCount()).resolves.toBe(2) + await comfyPage.settings.setSetting('Comfy.Validation.Workflows', true) + await comfyPage.workflow.loadWorkflow('links/bad_link') + await expect.poll(() => comfyPage.toast.getVisibleToastCount()).toBe(2) }) }) diff --git a/browser_tests/tests/graphCanvasMenu.spec.ts b/browser_tests/tests/graphCanvasMenu.spec.ts index 4e6d1b2fb..168518898 100644 --- a/browser_tests/tests/graphCanvasMenu.spec.ts +++ b/browser_tests/tests/graphCanvasMenu.spec.ts @@ -1,34 +1,37 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +import { TestIds } from '../fixtures/selectors' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Graph Canvas Menu', { tag: ['@screenshot', '@canvas'] }, () => { test.beforeEach(async ({ comfyPage }) => { // Set link render mode to spline to make sure it's not affected by other tests' // side effects. - await comfyPage.setSetting('Comfy.LinkRenderMode', 2) + await comfyPage.settings.setSetting('Comfy.LinkRenderMode', 2) // Enable canvas menu for all tests - await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true) + await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', true) }) test( 'Can toggle link visibility', { tag: '@screenshot' }, async ({ comfyPage }) => { - const button = comfyPage.page.getByTestId('toggle-link-visibility-button') + const button = comfyPage.page.getByTestId( + TestIds.canvas.toggleLinkVisibilityButton + ) await button.click() await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot( 'canvas-with-hidden-links.png' ) const hiddenLinkRenderMode = await comfyPage.page.evaluate(() => { - return window['LiteGraph'].HIDDEN_LINK + return window.LiteGraph!.HIDDEN_LINK }) - expect(await comfyPage.getSetting('Comfy.LinkRenderMode')).toBe( + expect(await comfyPage.settings.getSetting('Comfy.LinkRenderMode')).toBe( hiddenLinkRenderMode ) @@ -37,16 +40,18 @@ test.describe('Graph Canvas Menu', { tag: ['@screenshot', '@canvas'] }, () => { await expect(comfyPage.canvas).toHaveScreenshot( 'canvas-with-visible-links.png' ) - expect(await comfyPage.getSetting('Comfy.LinkRenderMode')).not.toBe( - hiddenLinkRenderMode - ) + expect( + await comfyPage.settings.getSetting('Comfy.LinkRenderMode') + ).not.toBe(hiddenLinkRenderMode) } ) test('Toggle minimap button is clickable and has correct test id', async ({ comfyPage }) => { - const minimapButton = comfyPage.page.getByTestId('toggle-minimap-button') + const minimapButton = comfyPage.page.getByTestId( + TestIds.canvas.toggleMinimapButton + ) await expect(minimapButton).toBeVisible() await expect(minimapButton).toBeEnabled() diff --git a/browser_tests/tests/groupNode.spec.ts b/browser_tests/tests/groupNode.spec.ts index 9dbbe58f8..6af103c8f 100644 --- a/browser_tests/tests/groupNode.spec.ts +++ b/browser_tests/tests/groupNode.spec.ts @@ -1,11 +1,15 @@ import { expect } from '@playwright/test' +import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema' + import type { ComfyPage } from '../fixtures/ComfyPage' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +import type { NodeLibrarySidebarTab } from '../fixtures/components/SidebarTab' +import { DefaultGraphPositions } from '../fixtures/constants/defaultGraphPositions' import type { NodeReference } from '../fixtures/utils/litegraphUtils' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Group Node', { tag: '@node' }, () => { @@ -13,30 +17,34 @@ test.describe('Group Node', { tag: '@node' }, () => { const groupNodeName = 'DefautWorkflowGroupNode' const groupNodeCategory = 'group nodes>workflow' const groupNodeBookmarkName = `workflow>${groupNodeName}` - let libraryTab + let libraryTab: NodeLibrarySidebarTab test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') libraryTab = comfyPage.menu.nodeLibraryTab - await comfyPage.convertAllNodesToGroupNode(groupNodeName) + await comfyPage.nodeOps.convertAllNodesToGroupNode(groupNodeName) await libraryTab.open() }) - test('Is added to node library sidebar', async ({ comfyPage }) => { - expect(await libraryTab.getFolder('group nodes').count()).toBe(1) + test('Is added to node library sidebar', async ({ + comfyPage: _comfyPage + }) => { + expect(await libraryTab.getFolder(groupNodeCategory).count()).toBe(1) }) test('Can be added to canvas using node library sidebar', async ({ comfyPage }) => { - const initialNodeCount = await comfyPage.getGraphNodesCount() + const initialNodeCount = await comfyPage.nodeOps.getGraphNodesCount() // Add group node from node library sidebar await libraryTab.getFolder(groupNodeCategory).click() await libraryTab.getNode(groupNodeName).click() // Verify the node is added to the canvas - expect(await comfyPage.getGraphNodesCount()).toBe(initialNodeCount + 1) + expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe( + initialNodeCount + 1 + ) }) test('Can be bookmarked and unbookmarked', async ({ comfyPage }) => { @@ -48,7 +56,7 @@ test.describe('Group Node', { tag: '@node' }, () => { // Verify the node is added to the bookmarks tab expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2') ).toEqual([groupNodeBookmarkName]) // Verify the bookmark node with the same name is added to the tree expect(await libraryTab.getNode(groupNodeName).count()).not.toBe(0) @@ -62,7 +70,7 @@ test.describe('Group Node', { tag: '@node' }, () => { // Verify the node is removed from the bookmarks tab expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2') ).toHaveLength(0) }) @@ -94,8 +102,8 @@ test.describe('Group Node', { tag: '@node' }, () => { { tag: '@screenshot' }, async ({ comfyPage }) => { const groupNodeName = 'DefautWorkflowGroupNode' - await comfyPage.convertAllNodesToGroupNode(groupNodeName) - await comfyPage.doubleClickCanvas() + await comfyPage.nodeOps.convertAllNodesToGroupNode(groupNodeName) + await comfyPage.canvasOps.doubleClick() await comfyPage.nextFrame() await comfyPage.searchBox.fillAndSelectFirstNode(groupNodeName) await expect(comfyPage.canvas).toHaveScreenshot( @@ -105,8 +113,8 @@ test.describe('Group Node', { tag: '@node' }, () => { ) test('Displays tooltip on title hover', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.EnableTooltips', true) - await comfyPage.convertAllNodesToGroupNode('Group Node') + await comfyPage.settings.setSetting('Comfy.EnableTooltips', true) + await comfyPage.nodeOps.convertAllNodesToGroupNode('Group Node') await comfyPage.page.mouse.move(47, 173) await expect(comfyPage.page.locator('.node-tooltip')).toBeVisible() }) @@ -114,9 +122,9 @@ test.describe('Group Node', { tag: '@node' }, () => { test('Manage group opens with the correct group selected', async ({ comfyPage }) => { - const makeGroup = async (name, type1, type2) => { - const node1 = (await comfyPage.getNodeRefsByType(type1))[0] - const node2 = (await comfyPage.getNodeRefsByType(type2))[0] + const makeGroup = async (name: string, type1: string, type2: string) => { + const node1 = (await comfyPage.nodeOps.getNodeRefsByType(type1))[0] + const node2 = (await comfyPage.nodeOps.getNodeRefsByType(type2))[0] await node1.click('title') await node2.click('title', { modifiers: ['Shift'] @@ -144,7 +152,7 @@ test.describe('Group Node', { tag: '@node' }, () => { test('Preserves hidden input configuration when containing duplicate node types', async ({ comfyPage }) => { - await comfyPage.loadWorkflow( + await comfyPage.workflow.loadWorkflow( 'groupnodes/group_node_identical_nodes_hidden_inputs' ) await comfyPage.nextFrame() @@ -155,16 +163,14 @@ test.describe('Group Node', { tag: '@node' }, () => { const totalInputCount = await comfyPage.page.evaluate((nodeName) => { const { extra: { groupNodes } - } = window['app'].graph - const { nodes } = groupNodes[nodeName] - return nodes.reduce((acc: number, node) => { - return acc + node.inputs.length - }, 0) + } = window.app!.graph! + const { nodes } = groupNodes![nodeName] + return nodes.reduce((acc, node) => acc + (node.inputs?.length ?? 0), 0) }, groupNodeName) const visibleInputCount = await comfyPage.page.evaluate((id) => { - const node = window['app'].graph.getNodeById(id) - return node.inputs.length + const node = window.app!.graph!.getNodeById(id) + return node!.inputs.length }, groupNodeId) // Verify there are 4 total inputs (2 VAE decode nodes with 2 inputs each) @@ -178,7 +184,7 @@ test.describe('Group Node', { tag: '@node' }, () => { comfyPage }) => { const expectSingleNode = async (type: string) => { - const nodes = await comfyPage.getNodeRefsByType(type) + const nodes = await comfyPage.nodeOps.getNodeRefsByType(type) expect(nodes).toHaveLength(1) return nodes[0] } @@ -213,8 +219,8 @@ test.describe('Group Node', { tag: '@node' }, () => { test('Loads from a workflow using the legacy path separator ("/")', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('groupnodes/legacy_group_node') - expect(await comfyPage.getGraphNodesCount()).toBe(1) + await comfyPage.workflow.loadWorkflow('groupnodes/legacy_group_node') + expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe(1) await expect( comfyPage.page.locator('.comfy-missing-nodes') ).not.toBeVisible() @@ -230,7 +236,7 @@ test.describe('Group Node', { tag: '@node' }, () => { const isRegisteredLitegraph = async (comfyPage: ComfyPage) => { return await comfyPage.page.evaluate((nodeType: string) => { - return !!window['LiteGraph'].registered_node_types[nodeType] + return !!window.LiteGraph!.registered_node_types[nodeType] }, GROUP_NODE_TYPE) } @@ -246,17 +252,17 @@ test.describe('Group Node', { tag: '@node' }, () => { comfyPage: ComfyPage, expectedCount: number ) => { - expect(await comfyPage.getNodeRefsByType(GROUP_NODE_TYPE)).toHaveLength( - expectedCount - ) + expect( + await comfyPage.nodeOps.getNodeRefsByType(GROUP_NODE_TYPE) + ).toHaveLength(expectedCount) expect(await isRegisteredLitegraph(comfyPage)).toBe(true) expect(await isRegisteredNodeDefStore(comfyPage)).toBe(true) } test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') - await comfyPage.loadWorkflow(WORKFLOW_NAME) - groupNode = await comfyPage.getFirstNodeRef() + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.workflow.loadWorkflow(WORKFLOW_NAME) + groupNode = await comfyPage.nodeOps.getFirstNodeRef() if (!groupNode) throw new Error(`Group node not found in workflow ${WORKFLOW_NAME}`) await groupNode.copy() @@ -265,7 +271,7 @@ test.describe('Group Node', { tag: '@node' }, () => { test('Copies and pastes group node within the same workflow', async ({ comfyPage }) => { - await comfyPage.ctrlV() + await comfyPage.clipboard.paste() await verifyNodeLoaded(comfyPage, 2) }) @@ -273,12 +279,12 @@ test.describe('Group Node', { tag: '@node' }, () => { comfyPage }) => { // Set setting - await comfyPage.setSetting('Comfy.ConfirmClear', false) + await comfyPage.settings.setSetting('Comfy.ConfirmClear', false) // Clear workflow - await comfyPage.executeCommand('Comfy.ClearWorkflow') + await comfyPage.command.executeCommand('Comfy.ClearWorkflow') - await comfyPage.ctrlV() + await comfyPage.clipboard.paste() await verifyNodeLoaded(comfyPage, 1) }) @@ -286,15 +292,15 @@ test.describe('Group Node', { tag: '@node' }, () => { comfyPage }) => { await comfyPage.menu.topbar.triggerTopbarCommand(['New']) - await comfyPage.ctrlV() + await comfyPage.clipboard.paste() await verifyNodeLoaded(comfyPage, 1) }) test('Copies and pastes group node across different workflows', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('default') - await comfyPage.ctrlV() + await comfyPage.workflow.loadWorkflow('default') + await comfyPage.clipboard.paste() await verifyNodeLoaded(comfyPage, 1) }) @@ -302,14 +308,15 @@ test.describe('Group Node', { tag: '@node' }, () => { comfyPage }) => { await comfyPage.menu.topbar.triggerTopbarCommand(['New']) - await comfyPage.ctrlV() + await comfyPage.clipboard.paste() const currentGraphState = await comfyPage.page.evaluate(() => - window['app'].graph.serialize() + window.app!.graph!.serialize() ) await test.step('Load workflow containing a group node pasted from a different workflow', async () => { await comfyPage.page.evaluate( - (workflow) => window['app'].loadGraphData(workflow), + (workflow) => + window.app!.loadGraphData(workflow as ComfyWorkflowJSON), currentGraphState ) await comfyPage.nextFrame() @@ -320,15 +327,18 @@ test.describe('Group Node', { tag: '@node' }, () => { test.describe('Keybindings', () => { test('Convert to group node, no selection', async ({ comfyPage }) => { - expect(await comfyPage.getVisibleToastCount()).toBe(0) + await expect.poll(() => comfyPage.toast.getVisibleToastCount()).toBe(0) await comfyPage.page.keyboard.press('Alt+g') - expect(await comfyPage.getVisibleToastCount()).toBe(1) + await expect.poll(() => comfyPage.toast.getVisibleToastCount()).toBe(1) }) test('Convert to group node, selected 1 node', async ({ comfyPage }) => { - expect(await comfyPage.getVisibleToastCount()).toBe(0) - await comfyPage.clickTextEncodeNode1() + await expect.poll(() => comfyPage.toast.getVisibleToastCount()).toBe(0) + await comfyPage.canvas.click({ + position: DefaultGraphPositions.textEncodeNode1 + }) + await comfyPage.nextFrame() await comfyPage.page.keyboard.press('Alt+g') - expect(await comfyPage.getVisibleToastCount()).toBe(1) + await expect.poll(() => comfyPage.toast.getVisibleToastCount()).toBe(1) }) }) }) diff --git a/browser_tests/tests/interaction.spec.ts b/browser_tests/tests/interaction.spec.ts index a7bcc651d..87c3e8a0b 100644 --- a/browser_tests/tests/interaction.spec.ts +++ b/browser_tests/tests/interaction.spec.ts @@ -7,15 +7,17 @@ import { testComfySnapToGridGridSize } from '../fixtures/ComfyPage' import type { ComfyPage } from '../fixtures/ComfyPage' +import { DefaultGraphPositions } from '../fixtures/constants/defaultGraphPositions' +import { TestIds } from '../fixtures/selectors' import type { NodeReference } from '../fixtures/utils/litegraphUtils' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Item Interaction', { tag: ['@screenshot', '@node'] }, () => { test('Can select/delete all items', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('groups/mixed_graph_items') + await comfyPage.workflow.loadWorkflow('groups/mixed_graph_items') await comfyPage.canvas.press('Control+a') await expect(comfyPage.canvas).toHaveScreenshot('selected-all.png') await comfyPage.canvas.press('Delete') @@ -23,7 +25,7 @@ test.describe('Item Interaction', { tag: ['@screenshot', '@node'] }, () => { }) test('Can pin/unpin items with keyboard shortcut', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('groups/mixed_graph_items') + await comfyPage.workflow.loadWorkflow('groups/mixed_graph_items') await comfyPage.canvas.press('Control+a') await comfyPage.canvas.press('KeyP') await comfyPage.nextFrame() @@ -51,11 +53,13 @@ test.describe('Node Interaction', () => { test(`Can add multiple nodes to selection using ${modifier}+Click`, async ({ comfyPage }) => { - const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode') + const clipNodes = + await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode') for (const node of clipNodes) { await node.click('title', { modifiers: [modifier] }) } - const selectedNodeCount = await comfyPage.getSelectedGraphNodesCount() + const selectedNodeCount = + await comfyPage.nodeOps.getSelectedGraphNodesCount() expect(selectedNodeCount).toBe(clipNodes.length) }) }) @@ -65,9 +69,15 @@ test.describe('Node Interaction', () => { { tag: '@screenshot' }, async ({ comfyPage }) => { await expect(comfyPage.canvas).toHaveScreenshot('default.png') - await comfyPage.clickTextEncodeNode1() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.textEncodeNode1 + }) + await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot('selected-node1.png') - await comfyPage.clickTextEncodeNode2() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.textEncodeNode2 + }) + await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot('selected-node2.png') } ) @@ -80,7 +90,7 @@ test.describe('Node Interaction', () => { const clipNode2Pos = await clipNodes[1].getPosition() const offset = 64 await comfyPage.page.keyboard.down('Meta') - await comfyPage.dragAndDrop( + await comfyPage.canvasOps.dragAndDrop( { x: Math.min(clipNode1Pos.x, clipNode2Pos.x) - offset, y: Math.min(clipNode1Pos.y, clipNode2Pos.y) - offset @@ -94,9 +104,10 @@ test.describe('Node Interaction', () => { } test('Can drag-select nodes with Meta (mac)', async ({ comfyPage }) => { - const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode') + const clipNodes = + await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode') await dragSelectNodes(comfyPage, clipNodes) - expect(await comfyPage.getSelectedGraphNodesCount()).toBe( + expect(await comfyPage.nodeOps.getSelectedGraphNodesCount()).toBe( clipNodes.length ) }) @@ -104,7 +115,8 @@ test.describe('Node Interaction', () => { test('Can move selected nodes using the Comfy.Canvas.MoveSelectedNodes.{Up|Down|Left|Right} commands', async ({ comfyPage }) => { - const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode') + const clipNodes = + await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode') const getPositions = () => Promise.all(clipNodes.map((node) => node.getPosition())) const testDirection = async ({ @@ -116,7 +128,7 @@ test.describe('Node Interaction', () => { }) => { const originalPositions = await getPositions() await dragSelectNodes(comfyPage, clipNodes) - await comfyPage.executeCommand( + await comfyPage.command.executeCommand( `Comfy.Canvas.MoveSelectedNodes.${direction}` ) await comfyPage.canvas.press(`Control+Arrow${direction}`) @@ -155,14 +167,20 @@ test.describe('Node Interaction', () => { }) test('Can drag node', { tag: '@screenshot' }, async ({ comfyPage }) => { - await comfyPage.dragNode2() + await comfyPage.nodeOps.dragTextEncodeNode2() await expect(comfyPage.canvas).toHaveScreenshot('dragged-node1.png') }) test.describe('Edge Interaction', { tag: '@screenshot' }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.LinkRelease.Action', 'no action') - await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'no action') + await comfyPage.settings.setSetting( + 'Comfy.LinkRelease.Action', + 'no action' + ) + await comfyPage.settings.setSetting( + 'Comfy.LinkRelease.ActionShift', + 'no action' + ) }) // Test both directions of edge connection. @@ -170,11 +188,11 @@ test.describe('Node Interaction', () => { test(`Can disconnect/connect edge ${reverse ? 'reverse' : 'normal'}`, async ({ comfyPage }) => { - await comfyPage.disconnectEdge() + await comfyPage.canvasOps.disconnectEdge() await expect(comfyPage.canvas).toHaveScreenshot('disconnected-edge.png') - await comfyPage.connectEdge({ reverse }) + await comfyPage.canvasOps.connectEdge({ reverse }) // Move mouse to empty area to avoid slot highlight. - await comfyPage.moveMouseToEmptyArea() + await comfyPage.canvasOps.moveMouseToEmptyArea() // Litegraph renders edge with a slight offset. await expect(comfyPage.canvas).toHaveScreenshot('default.png', { maxDiffPixels: 50 @@ -183,14 +201,14 @@ test.describe('Node Interaction', () => { }) test('Can move link', async ({ comfyPage }) => { - await comfyPage.dragAndDrop( - comfyPage.clipTextEncodeNode1InputSlot, - comfyPage.emptySpace + await comfyPage.canvasOps.dragAndDrop( + DefaultGraphPositions.clipTextEncodeNode1InputSlot, + DefaultGraphPositions.emptySpace ) await expect(comfyPage.canvas).toHaveScreenshot('disconnected-edge.png') - await comfyPage.dragAndDrop( - comfyPage.clipTextEncodeNode2InputSlot, - comfyPage.clipTextEncodeNode1InputSlot + await comfyPage.canvasOps.dragAndDrop( + DefaultGraphPositions.clipTextEncodeNode2InputSlot, + DefaultGraphPositions.clipTextEncodeNode1InputSlot ) await expect(comfyPage.canvas).toHaveScreenshot('moved-link.png') }) @@ -199,15 +217,15 @@ test.describe('Node Interaction', () => { test.skip('Can copy link by shift-drag existing link', async ({ comfyPage }) => { - await comfyPage.dragAndDrop( - comfyPage.clipTextEncodeNode1InputSlot, - comfyPage.emptySpace + await comfyPage.canvasOps.dragAndDrop( + DefaultGraphPositions.clipTextEncodeNode1InputSlot, + DefaultGraphPositions.emptySpace ) await expect(comfyPage.canvas).toHaveScreenshot('disconnected-edge.png') await comfyPage.page.keyboard.down('Shift') - await comfyPage.dragAndDrop( - comfyPage.clipTextEncodeNode2InputLinkPath, - comfyPage.clipTextEncodeNode1InputSlot + await comfyPage.canvasOps.dragAndDrop( + DefaultGraphPositions.clipTextEncodeNode2InputLinkPath, + DefaultGraphPositions.clipTextEncodeNode1InputSlot ) await comfyPage.page.keyboard.up('Shift') await expect(comfyPage.canvas).toHaveScreenshot('copied-link.png') @@ -217,11 +235,11 @@ test.describe('Node Interaction', () => { comfyPage, comfyMouse }) => { - await comfyPage.setSetting('Comfy.Node.AutoSnapLinkToSlot', true) - await comfyPage.setSetting('Comfy.Node.SnapHighlightsNode', true) + await comfyPage.settings.setSetting('Comfy.Node.AutoSnapLinkToSlot', true) + await comfyPage.settings.setSetting('Comfy.Node.SnapHighlightsNode', true) - await comfyMouse.move(comfyPage.clipTextEncodeNode1InputSlot) - await comfyMouse.drag(comfyPage.clipTextEncodeNode2InputSlot) + await comfyMouse.move(DefaultGraphPositions.clipTextEncodeNode1InputSlot) + await comfyMouse.drag(DefaultGraphPositions.clipTextEncodeNode2InputSlot) await expect(comfyPage.canvas).toHaveScreenshot('snapped-highlighted.png') }) }) @@ -230,7 +248,7 @@ test.describe('Node Interaction', () => { 'Can adjust widget value', { tag: '@screenshot' }, async ({ comfyPage }) => { - await comfyPage.adjustWidgetValue() + await comfyPage.nodeOps.adjustEmptyLatentWidth() await expect(comfyPage.canvas).toHaveScreenshot( 'adjusted-widget-value.png' ) @@ -238,7 +256,7 @@ test.describe('Node Interaction', () => { ) test('Link snap to slot', { tag: '@screenshot' }, async ({ comfyPage }) => { - await comfyPage.loadWorkflow('links/snap_to_slot') + await comfyPage.workflow.loadWorkflow('links/snap_to_slot') await expect(comfyPage.canvas).toHaveScreenshot('snap_to_slot.png') const outputSlotPos = { @@ -249,7 +267,7 @@ test.describe('Node Interaction', () => { x: 748, y: 77 } - await comfyPage.dragAndDrop(outputSlotPos, samplerNodeCenterPos) + await comfyPage.canvasOps.dragAndDrop(outputSlotPos, samplerNodeCenterPos) await expect(comfyPage.canvas).toHaveScreenshot('snap_to_slot_linked.png') }) @@ -258,7 +276,7 @@ test.describe('Node Interaction', () => { 'Can batch move links by drag with shift', { tag: '@screenshot' }, async ({ comfyPage }) => { - await comfyPage.loadWorkflow('links/batch_move_links') + await comfyPage.workflow.loadWorkflow('links/batch_move_links') await expect(comfyPage.canvas).toHaveScreenshot('batch_move_links.png') const outputSlot1Pos = { @@ -271,7 +289,7 @@ test.describe('Node Interaction', () => { } await comfyPage.page.keyboard.down('Shift') - await comfyPage.dragAndDrop(outputSlot1Pos, outputSlot2Pos) + await comfyPage.canvasOps.dragAndDrop(outputSlot1Pos, outputSlot2Pos) await comfyPage.page.keyboard.up('Shift') await expect(comfyPage.canvas).toHaveScreenshot( @@ -304,12 +322,18 @@ test.describe('Node Interaction', () => { { tag: '@screenshot' }, async ({ comfyPage }) => { await expect(comfyPage.canvas).toHaveScreenshot('default.png') - await comfyPage.clickTextEncodeNodeToggler() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.textEncodeNodeToggler + }) + await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot( 'text-encode-toggled-off.png' ) await comfyPage.delay(1000) - await comfyPage.clickTextEncodeNodeToggler() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.textEncodeNodeToggler + }) + await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot( 'text-encode-toggled-back-open.png' ) @@ -345,7 +369,7 @@ test.describe('Node Interaction', () => { x: 167, y: 143 } - await comfyPage.loadWorkflow('nodes/single_save_image_node') + await comfyPage.workflow.loadWorkflow('nodes/single_save_image_node') await comfyPage.canvas.click({ position: textWidgetPos }) @@ -365,7 +389,7 @@ test.describe('Node Interaction', () => { 'Can double click node title to edit', { tag: '@screenshot' }, async ({ comfyPage }) => { - await comfyPage.loadWorkflow('nodes/single_ksampler') + await comfyPage.workflow.loadWorkflow('nodes/single_ksampler') await comfyPage.canvas.dblclick({ position: { x: 50, @@ -382,7 +406,7 @@ test.describe('Node Interaction', () => { test('Double click node body does not trigger edit', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('nodes/single_ksampler') + await comfyPage.workflow.loadWorkflow('nodes/single_ksampler') await comfyPage.canvas.dblclick({ position: { x: 50, @@ -397,8 +421,11 @@ test.describe('Node Interaction', () => { 'Can group selected nodes', { tag: '@screenshot' }, async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.GroupSelectedNodes.Padding', 10) - await comfyPage.select2Nodes() + await comfyPage.settings.setSetting( + 'Comfy.GroupSelectedNodes.Padding', + 10 + ) + await comfyPage.nodeOps.selectNodes(['CLIP Text Encode (Prompt)']) await comfyPage.page.keyboard.down('Control') await comfyPage.page.keyboard.press('KeyG') await comfyPage.page.keyboard.up('Control') @@ -416,10 +443,10 @@ test.describe('Node Interaction', () => { 'Can fit group to contents', { tag: '@screenshot' }, async ({ comfyPage }) => { - await comfyPage.loadWorkflow('groups/oversized_group') - await comfyPage.ctrlA() + await comfyPage.workflow.loadWorkflow('groups/oversized_group') + await comfyPage.keyboard.selectAll() await comfyPage.nextFrame() - await comfyPage.executeCommand('Comfy.Graph.FitGroupToContents') + await comfyPage.command.executeCommand('Comfy.Graph.FitGroupToContents') await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot( 'group-fit-to-contents.png' @@ -428,11 +455,15 @@ test.describe('Node Interaction', () => { ) test('Can pin/unpin nodes', { tag: '@screenshot' }, async ({ comfyPage }) => { - await comfyPage.select2Nodes() - await comfyPage.executeCommand('Comfy.Canvas.ToggleSelectedNodes.Pin') + await comfyPage.nodeOps.selectNodes(['CLIP Text Encode (Prompt)']) + await comfyPage.command.executeCommand( + 'Comfy.Canvas.ToggleSelectedNodes.Pin' + ) await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot('nodes-pinned.png') - await comfyPage.executeCommand('Comfy.Canvas.ToggleSelectedNodes.Pin') + await comfyPage.command.executeCommand( + 'Comfy.Canvas.ToggleSelectedNodes.Pin' + ) await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot('nodes-unpinned.png') }) @@ -441,7 +472,7 @@ test.describe('Node Interaction', () => { 'Can bypass/unbypass nodes with keyboard shortcut', { tag: '@screenshot' }, async ({ comfyPage }) => { - await comfyPage.select2Nodes() + await comfyPage.nodeOps.selectNodes(['CLIP Text Encode (Prompt)']) await comfyPage.canvas.press('Control+b') await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot('nodes-bypassed.png') @@ -454,7 +485,7 @@ test.describe('Node Interaction', () => { test.describe('Group Interaction', { tag: '@screenshot' }, () => { test('Can double click group title to edit', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('groups/single_group') + await comfyPage.workflow.loadWorkflow('groups/single_group') await comfyPage.canvas.dblclick({ position: { x: 50, @@ -470,16 +501,16 @@ test.describe('Group Interaction', { tag: '@screenshot' }, () => { test.describe('Canvas Interaction', { tag: '@screenshot' }, () => { test('Can zoom in/out', async ({ comfyPage }) => { - await comfyPage.zoom(-100) + await comfyPage.canvasOps.zoom(-100) await expect(comfyPage.canvas).toHaveScreenshot('zoomed-in.png') - await comfyPage.zoom(200) + await comfyPage.canvasOps.zoom(200) await expect(comfyPage.canvas).toHaveScreenshot('zoomed-out.png') }) test('Can zoom very far out', async ({ comfyPage }) => { - await comfyPage.zoom(100, 12) + await comfyPage.canvasOps.zoom(100, 12) await expect(comfyPage.canvas).toHaveScreenshot('zoomed-very-far-out.png') - await comfyPage.zoom(-100, 12) + await comfyPage.canvasOps.zoom(-100, 12) await expect(comfyPage.canvas).toHaveScreenshot('zoomed-back-in.png') }) @@ -488,11 +519,11 @@ test.describe('Canvas Interaction', { tag: '@screenshot' }, () => { }) => { await comfyPage.page.keyboard.down('Control') await comfyPage.page.keyboard.down('Shift') - await comfyPage.dragAndDrop({ x: 10, y: 100 }, { x: 10, y: 40 }) + await comfyPage.canvasOps.dragAndDrop({ x: 10, y: 100 }, { x: 10, y: 40 }) await expect(comfyPage.canvas).toHaveScreenshot('zoomed-in-ctrl-shift.png') - await comfyPage.dragAndDrop({ x: 10, y: 40 }, { x: 10, y: 160 }) + await comfyPage.canvasOps.dragAndDrop({ x: 10, y: 40 }, { x: 10, y: 160 }) await expect(comfyPage.canvas).toHaveScreenshot('zoomed-out-ctrl-shift.png') - await comfyPage.dragAndDrop({ x: 10, y: 280 }, { x: 10, y: 220 }) + await comfyPage.canvasOps.dragAndDrop({ x: 10, y: 280 }, { x: 10, y: 220 }) await expect(comfyPage.canvas).toHaveScreenshot( 'zoomed-default-ctrl-shift.png' ) @@ -503,35 +534,35 @@ test.describe('Canvas Interaction', { tag: '@screenshot' }, () => { test('Can zoom in/out after decreasing canvas zoom speed setting', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', 1.05) - await comfyPage.zoom(-100, 4) + await comfyPage.settings.setSetting('Comfy.Graph.ZoomSpeed', 1.05) + await comfyPage.canvasOps.zoom(-100, 4) await expect(comfyPage.canvas).toHaveScreenshot( 'zoomed-in-low-zoom-speed.png' ) - await comfyPage.zoom(100, 8) + await comfyPage.canvasOps.zoom(100, 8) await expect(comfyPage.canvas).toHaveScreenshot( 'zoomed-out-low-zoom-speed.png' ) - await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', 1.1) + await comfyPage.settings.setSetting('Comfy.Graph.ZoomSpeed', 1.1) }) test('Can zoom in/out after increasing canvas zoom speed', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', 1.5) - await comfyPage.zoom(-100, 4) + await comfyPage.settings.setSetting('Comfy.Graph.ZoomSpeed', 1.5) + await comfyPage.canvasOps.zoom(-100, 4) await expect(comfyPage.canvas).toHaveScreenshot( 'zoomed-in-high-zoom-speed.png' ) - await comfyPage.zoom(100, 8) + await comfyPage.canvasOps.zoom(100, 8) await expect(comfyPage.canvas).toHaveScreenshot( 'zoomed-out-high-zoom-speed.png' ) - await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', 1.1) + await comfyPage.settings.setSetting('Comfy.Graph.ZoomSpeed', 1.1) }) test('Can pan', async ({ comfyPage }) => { - await comfyPage.pan({ x: 200, y: 200 }) + await comfyPage.canvasOps.pan({ x: 200, y: 200 }) await expect(comfyPage.canvas).toHaveScreenshot('panned.png') }) @@ -602,9 +633,9 @@ test.describe('Canvas Interaction', { tag: '@screenshot' }, () => { }) test('Can pan when dragging a link', async ({ comfyPage, comfyMouse }) => { - const posSlot1 = comfyPage.clipTextEncodeNode1InputSlot + const posSlot1 = DefaultGraphPositions.clipTextEncodeNode1InputSlot await comfyMouse.move(posSlot1) - const posEmpty = comfyPage.emptySpace + const posEmpty = DefaultGraphPositions.emptySpace await comfyMouse.drag(posEmpty) await expect(comfyPage.canvas).toHaveScreenshot('dragging-link1.png') @@ -623,23 +654,23 @@ test.describe('Canvas Interaction', { tag: '@screenshot' }, () => { test('Can pan very far and back', async ({ comfyPage }) => { // intentionally slice the edge of where the clip text encode dom widgets are - await comfyPage.pan({ x: -800, y: -300 }, { x: 1000, y: 10 }) + await comfyPage.canvasOps.pan({ x: -800, y: -300 }, { x: 1000, y: 10 }) await expect(comfyPage.canvas).toHaveScreenshot('panned-step-one.png') - await comfyPage.pan({ x: -200, y: 0 }, { x: 1000, y: 10 }) + await comfyPage.canvasOps.pan({ x: -200, y: 0 }, { x: 1000, y: 10 }) await expect(comfyPage.canvas).toHaveScreenshot('panned-step-two.png') - await comfyPage.pan({ x: -2200, y: -2200 }, { x: 1000, y: 10 }) + await comfyPage.canvasOps.pan({ x: -2200, y: -2200 }, { x: 1000, y: 10 }) await expect(comfyPage.canvas).toHaveScreenshot('panned-far-away.png') - await comfyPage.pan({ x: 2200, y: 2200 }, { x: 1000, y: 10 }) + await comfyPage.canvasOps.pan({ x: 2200, y: 2200 }, { x: 1000, y: 10 }) await expect(comfyPage.canvas).toHaveScreenshot('panned-back-from-far.png') - await comfyPage.pan({ x: 200, y: 0 }, { x: 1000, y: 10 }) + await comfyPage.canvasOps.pan({ x: 200, y: 0 }, { x: 1000, y: 10 }) await expect(comfyPage.canvas).toHaveScreenshot('panned-back-to-two.png') - await comfyPage.pan({ x: 800, y: 300 }, { x: 1000, y: 10 }) + await comfyPage.canvasOps.pan({ x: 800, y: 300 }, { x: 1000, y: 10 }) await expect(comfyPage.canvas).toHaveScreenshot('panned-back-to-one.png') }) test('@mobile Can pan with touch', async ({ comfyPage }) => { await comfyPage.closeMenu() - await comfyPage.panWithTouch({ x: 200, y: 200 }) + await comfyPage.canvasOps.panWithTouch({ x: 200, y: 200 }) await expect(comfyPage.canvas).toHaveScreenshot('panned-touch.png') }) }) @@ -652,41 +683,41 @@ test.describe('Widget Interaction', () => { await expect(textBox).toHaveValue('') await textBox.fill('Hello World') await expect(textBox).toHaveValue('Hello World') - await comfyPage.ctrlZ(null) + await comfyPage.keyboard.undo(null) await expect(textBox).toHaveValue('') }) test('Undo attention edit', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.EditAttention.Delta', 0.05) + await comfyPage.settings.setSetting('Comfy.EditAttention.Delta', 0.05) const textBox = comfyPage.widgetTextBox await textBox.click() await textBox.fill('1girl') await expect(textBox).toHaveValue('1girl') await textBox.selectText() - await comfyPage.ctrlArrowUp(null) + await comfyPage.keyboard.moveUp(null) await expect(textBox).toHaveValue('(1girl:1.05)') - await comfyPage.ctrlZ(null) + await comfyPage.keyboard.undo(null) await expect(textBox).toHaveValue('1girl') }) }) test.describe('Load workflow', { tag: '@screenshot' }, () => { test('Can load workflow with string node id', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('nodes/string_node_id') + await comfyPage.workflow.loadWorkflow('nodes/string_node_id') await expect(comfyPage.canvas).toHaveScreenshot('string_node_id.png') }) test('Can load workflow with ("STRING",) input node', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('inputs/string_input') + await comfyPage.workflow.loadWorkflow('inputs/string_input') await expect(comfyPage.canvas).toHaveScreenshot('string_input.png') }) test('Restore workflow on reload (switch workflow)', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('nodes/single_ksampler') + await comfyPage.workflow.loadWorkflow('nodes/single_ksampler') await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler.png') await comfyPage.setup({ clearStorage: false }) await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler.png') @@ -695,10 +726,10 @@ test.describe('Load workflow', { tag: '@screenshot' }, () => { test('Restore workflow on reload (modify workflow)', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('nodes/single_ksampler') - const node = (await comfyPage.getFirstNodeRef())! + await comfyPage.workflow.loadWorkflow('nodes/single_ksampler') + const node = (await comfyPage.nodeOps.getFirstNodeRef())! await node.click('collapse') - await comfyPage.clickEmptySpace() + await comfyPage.canvasOps.clickEmptySpace() await expect(comfyPage.canvas).toHaveScreenshot( 'single_ksampler_modified.png' ) @@ -716,7 +747,7 @@ test.describe('Load workflow', { tag: '@screenshot' }, () => { `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}${extension}` test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') workflowA = generateUniqueFilename() await comfyPage.menu.topbar.saveWorkflow(workflowA) @@ -734,7 +765,7 @@ test.describe('Load workflow', { tag: '@screenshot' }, () => { test('Restores topbar workflow tabs after reload', async ({ comfyPage }) => { - await comfyPage.setSetting( + await comfyPage.settings.setSetting( 'Comfy.Workflow.WorkflowTabsPosition', 'Topbar' ) @@ -747,7 +778,7 @@ test.describe('Load workflow', { tag: '@screenshot' }, () => { }) test('Restores sidebar workflows after reload', async ({ comfyPage }) => { - await comfyPage.setSetting( + await comfyPage.settings.setSetting( 'Comfy.Workflow.WorkflowTabsPosition', 'Sidebar' ) @@ -770,34 +801,40 @@ test.describe('Load workflow', { tag: '@screenshot' }, () => { }) test('Auto fit view after loading workflow', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.EnableWorkflowViewRestore', false) - await comfyPage.loadWorkflow('nodes/single_ksampler') + await comfyPage.settings.setSetting( + 'Comfy.EnableWorkflowViewRestore', + false + ) + await comfyPage.workflow.loadWorkflow('nodes/single_ksampler') await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler_fit.png') }) }) test.describe('Load duplicate workflow', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') }) test('A workflow can be loaded multiple times in a row', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('nodes/single_ksampler') + await comfyPage.workflow.loadWorkflow('nodes/single_ksampler') await comfyPage.menu.workflowsTab.open() - await comfyPage.executeCommand('Comfy.NewBlankWorkflow') - await comfyPage.loadWorkflow('nodes/single_ksampler') - expect(await comfyPage.getGraphNodesCount()).toBe(1) + await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow') + await comfyPage.workflow.loadWorkflow('nodes/single_ksampler') + expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe(1) }) }) test.describe('Viewport settings', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') - await comfyPage.setSetting('Comfy.Workflow.WorkflowTabsPosition', 'Topbar') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting( + 'Comfy.Workflow.WorkflowTabsPosition', + 'Topbar' + ) - await comfyPage.setupWorkflowsDirectory({}) + await comfyPage.workflow.setupWorkflowsDirectory({}) }) test('Keeps viewport settings when changing tabs', async ({ @@ -807,7 +844,7 @@ test.describe('Viewport settings', () => { const changeTab = async (tab: Locator) => { await tab.click() await comfyPage.nextFrame() - await comfyMouse.move(comfyPage.emptySpace) + await comfyMouse.move(DefaultGraphPositions.emptySpace) // If tooltip is visible, wait for it to hide await expect( @@ -816,11 +853,13 @@ test.describe('Viewport settings', () => { } // Screenshot the canvas element - await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true) + await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', true) - const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button') + const toggleButton = comfyPage.page.getByTestId( + TestIds.canvas.toggleMinimapButton + ) await toggleButton.click() - await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false) + await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', false) await comfyPage.menu.topbar.saveWorkflow('Workflow A') await comfyPage.nextFrame() @@ -837,7 +876,7 @@ test.describe('Viewport settings', () => { const tabB = comfyPage.menu.topbar.getWorkflowTab('Workflow B') await changeTab(tabB) - await comfyMouse.move(comfyPage.emptySpace) + await comfyMouse.move(DefaultGraphPositions.emptySpace) for (let i = 0; i < 4; i++) { await comfyMouse.wheel(0, 60) } @@ -865,13 +904,19 @@ test.describe('Viewport settings', () => { test.describe('Canvas Navigation', { tag: '@screenshot' }, () => { test.describe('Legacy Mode', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Canvas.NavigationMode', 'legacy') + await comfyPage.settings.setSetting( + 'Comfy.Canvas.NavigationMode', + 'legacy' + ) }) test('Left-click drag in empty area should pan canvas', async ({ comfyPage }) => { - await comfyPage.dragAndDrop({ x: 50, y: 50 }, { x: 150, y: 150 }) + await comfyPage.canvasOps.dragAndDrop( + { x: 50, y: 50 }, + { x: 150, y: 150 } + ) await expect(comfyPage.canvas).toHaveScreenshot( 'legacy-left-drag-pan.png' ) @@ -904,8 +949,11 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => { }) test('Left-click on node should not pan canvas', async ({ comfyPage }) => { - await comfyPage.clickTextEncodeNode1() - const selectedCount = await comfyPage.getSelectedGraphNodesCount() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.textEncodeNode1 + }) + await comfyPage.nextFrame() + const selectedCount = await comfyPage.nodeOps.getSelectedGraphNodesCount() expect(selectedCount).toBe(1) await expect(comfyPage.canvas).toHaveScreenshot( 'legacy-click-node-select.png' @@ -915,18 +963,22 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => { test.describe('Standard Mode', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Canvas.NavigationMode', 'standard') + await comfyPage.settings.setSetting( + 'Comfy.Canvas.NavigationMode', + 'standard' + ) }) test('Left-click drag in empty area should select nodes', async ({ comfyPage }) => { - const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode') + const clipNodes = + await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode') const clipNode1Pos = await clipNodes[0].getPosition() const clipNode2Pos = await clipNodes[1].getPosition() const offset = 64 - await comfyPage.dragAndDrop( + await comfyPage.canvasOps.dragAndDrop( { x: Math.min(clipNode1Pos.x, clipNode2Pos.x) - offset, y: Math.min(clipNode1Pos.y, clipNode2Pos.y) - offset @@ -937,7 +989,7 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => { } ) - const selectedCount = await comfyPage.getSelectedGraphNodesCount() + const selectedCount = await comfyPage.nodeOps.getSelectedGraphNodesCount() expect(selectedCount).toBe(clipNodes.length) await expect(comfyPage.canvas).toHaveScreenshot( 'standard-left-drag-select.png' @@ -977,8 +1029,11 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => { test('Left-click on node should select node (not start selection box)', async ({ comfyPage }) => { - await comfyPage.clickTextEncodeNode1() - const selectedCount = await comfyPage.getSelectedGraphNodesCount() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.textEncodeNode1 + }) + await comfyPage.nextFrame() + const selectedCount = await comfyPage.nodeOps.getSelectedGraphNodesCount() expect(selectedCount).toBe(1) await expect(comfyPage.canvas).toHaveScreenshot( 'standard-click-node-select.png' @@ -991,7 +1046,10 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => { await comfyPage.nextFrame() await comfyPage.page.keyboard.down('Space') - await comfyPage.dragAndDrop({ x: 50, y: 50 }, { x: 150, y: 150 }) + await comfyPage.canvasOps.dragAndDrop( + { x: 50, y: 50 }, + { x: 150, y: 150 } + ) await comfyPage.page.keyboard.up('Space') await expect(comfyPage.canvas).toHaveScreenshot( 'standard-space-drag-pan.png' @@ -1001,11 +1059,12 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => { test('Space key overrides default left-click behavior', async ({ comfyPage }) => { - const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode') + const clipNodes = + await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode') const clipNode1Pos = await clipNodes[0].getPosition() const offset = 64 - await comfyPage.dragAndDrop( + await comfyPage.canvasOps.dragAndDrop( { x: clipNode1Pos.x - offset, y: clipNode1Pos.y - offset @@ -1017,16 +1076,16 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => { ) const selectedCountAfterDrag = - await comfyPage.getSelectedGraphNodesCount() + await comfyPage.nodeOps.getSelectedGraphNodesCount() expect(selectedCountAfterDrag).toBeGreaterThan(0) - await comfyPage.clickEmptySpace() + await comfyPage.canvasOps.clickEmptySpace() const selectedCountAfterClear = - await comfyPage.getSelectedGraphNodesCount() + await comfyPage.nodeOps.getSelectedGraphNodesCount() expect(selectedCountAfterClear).toBe(0) await comfyPage.page.keyboard.down('Space') - await comfyPage.dragAndDrop( + await comfyPage.canvasOps.dragAndDrop( { x: clipNode1Pos.x - offset, y: clipNode1Pos.y - offset @@ -1039,7 +1098,7 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => { await comfyPage.page.keyboard.up('Space') const selectedCountAfterSpaceDrag = - await comfyPage.getSelectedGraphNodesCount() + await comfyPage.nodeOps.getSelectedGraphNodesCount() expect(selectedCountAfterSpaceDrag).toBe(0) }) }) @@ -1047,7 +1106,10 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => { test('Shift + mouse wheel should pan canvas horizontally', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Canvas.MouseWheelScroll', 'panning') + await comfyPage.settings.setSetting( + 'Comfy.Canvas.MouseWheelScroll', + 'panning' + ) await comfyPage.page.click('canvas') await comfyPage.nextFrame() @@ -1085,11 +1147,17 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => { test('Multiple modifier keys work correctly in legacy mode', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Canvas.NavigationMode', 'legacy') + await comfyPage.settings.setSetting( + 'Comfy.Canvas.NavigationMode', + 'legacy' + ) await comfyPage.page.keyboard.down('Alt') await comfyPage.page.keyboard.down('Shift') - await comfyPage.dragAndDrop({ x: 50, y: 50 }, { x: 150, y: 150 }) + await comfyPage.canvasOps.dragAndDrop( + { x: 50, y: 50 }, + { x: 150, y: 150 } + ) await comfyPage.page.keyboard.up('Shift') await comfyPage.page.keyboard.up('Alt') @@ -1109,7 +1177,10 @@ test.describe('Canvas Navigation', { tag: '@screenshot' }, () => { }) } - await comfyPage.setSetting('Comfy.Canvas.NavigationMode', 'legacy') + await comfyPage.settings.setSetting( + 'Comfy.Canvas.NavigationMode', + 'legacy' + ) await comfyPage.page.mouse.move(50, 50) await comfyPage.page.mouse.down() expect(await getCursorStyle()).toBe('grabbing') diff --git a/browser_tests/tests/keybindings.spec.ts b/browser_tests/tests/keybindings.spec.ts index feb3b0f5d..4810a3fb1 100644 --- a/browser_tests/tests/keybindings.spec.ts +++ b/browser_tests/tests/keybindings.spec.ts @@ -3,22 +3,22 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Keybindings', { tag: '@keyboard' }, () => { test('Should not trigger non-modifier keybinding when typing in input fields', async ({ comfyPage }) => { - await comfyPage.registerKeybinding({ key: 'k' }, () => { - window['TestCommand'] = true + await comfyPage.command.registerKeybinding({ key: 'k' }, () => { + window.TestCommand = true }) const textBox = comfyPage.widgetTextBox await textBox.click() await textBox.fill('k') await expect(textBox).toHaveValue('k') - expect(await comfyPage.page.evaluate(() => window['TestCommand'])).toBe( + expect(await comfyPage.page.evaluate(() => window.TestCommand)).toBe( undefined ) }) @@ -26,8 +26,8 @@ test.describe('Keybindings', { tag: '@keyboard' }, () => { test('Should not trigger modifier keybinding when typing in input fields', async ({ comfyPage }) => { - await comfyPage.registerKeybinding({ key: 'k', ctrl: true }, () => { - window['TestCommand'] = true + await comfyPage.command.registerKeybinding({ key: 'k', ctrl: true }, () => { + window.TestCommand = true }) const textBox = comfyPage.widgetTextBox @@ -35,23 +35,21 @@ test.describe('Keybindings', { tag: '@keyboard' }, () => { await textBox.fill('q') await textBox.press('Control+k') await expect(textBox).toHaveValue('q') - expect(await comfyPage.page.evaluate(() => window['TestCommand'])).toBe( - true - ) + expect(await comfyPage.page.evaluate(() => window.TestCommand)).toBe(true) }) test('Should not trigger keybinding reserved by text input when typing in input fields', async ({ comfyPage }) => { - await comfyPage.registerKeybinding({ key: 'Ctrl+v' }, () => { - window['TestCommand'] = true + await comfyPage.command.registerKeybinding({ key: 'Ctrl+v' }, () => { + window.TestCommand = true }) const textBox = comfyPage.widgetTextBox await textBox.click() await textBox.press('Control+v') await expect(textBox).toBeFocused() - expect(await comfyPage.page.evaluate(() => window['TestCommand'])).toBe( + expect(await comfyPage.page.evaluate(() => window.TestCommand)).toBe( undefined ) }) diff --git a/browser_tests/tests/litegraphEvent.spec.ts b/browser_tests/tests/litegraphEvent.spec.ts index 12d6ec9c4..a412200b5 100644 --- a/browser_tests/tests/litegraphEvent.spec.ts +++ b/browser_tests/tests/litegraphEvent.spec.ts @@ -3,7 +3,7 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) function listenForEvent(): Promise { @@ -17,7 +17,7 @@ function listenForEvent(): Promise { test.describe('Canvas Event', { tag: '@canvas' }, () => { test('Emit litegraph:canvas empty-release', async ({ comfyPage }) => { const eventPromise = comfyPage.page.evaluate(listenForEvent) - const disconnectPromise = comfyPage.disconnectEdge() + const disconnectPromise = comfyPage.canvasOps.disconnectEdge() const event = await eventPromise await disconnectPromise @@ -29,7 +29,7 @@ test.describe('Canvas Event', { tag: '@canvas' }, () => { test('Emit litegraph:canvas empty-double-click', async ({ comfyPage }) => { const eventPromise = comfyPage.page.evaluate(listenForEvent) - const doubleClickPromise = comfyPage.doubleClickCanvas() + const doubleClickPromise = comfyPage.canvasOps.doubleClick() const event = await eventPromise await doubleClickPromise diff --git a/browser_tests/tests/loadWorkflowInMedia.spec.ts b/browser_tests/tests/loadWorkflowInMedia.spec.ts index 1b426c413..d8940f2cd 100644 --- a/browser_tests/tests/loadWorkflowInMedia.spec.ts +++ b/browser_tests/tests/loadWorkflowInMedia.spec.ts @@ -3,7 +3,7 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe( @@ -33,7 +33,7 @@ test.describe( test(`Load workflow in ${fileName} (drop from filesystem)`, async ({ comfyPage }) => { - await comfyPage.dragAndDropFile(`workflowInMedia/${fileName}`) + await comfyPage.dragDrop.dragAndDropFile(`workflowInMedia/${fileName}`) await expect(comfyPage.canvas).toHaveScreenshot(`${fileName}.png`) }) }) @@ -45,7 +45,7 @@ test.describe( test(`Load workflow from URL ${url} (drop from different browser tabs)`, async ({ comfyPage }) => { - await comfyPage.dragAndDropURL(url) + await comfyPage.dragDrop.dragAndDropURL(url) const readableName = url.split('/').pop() await expect(comfyPage.canvas).toHaveScreenshot( `dropped_workflow_url_${readableName}.png` diff --git a/browser_tests/tests/lodThreshold.spec.ts b/browser_tests/tests/lodThreshold.spec.ts index e1098cd28..e1bf11dae 100644 --- a/browser_tests/tests/lodThreshold.spec.ts +++ b/browser_tests/tests/lodThreshold.spec.ts @@ -3,7 +3,7 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => { @@ -11,11 +11,11 @@ test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => { comfyPage }) => { // Load a workflow with some nodes to render - await comfyPage.loadWorkflow('default') + await comfyPage.workflow.loadWorkflow('default') // Get initial LOD state and settings const initialState = await comfyPage.page.evaluate(() => { - const canvas = window['app'].canvas + const canvas = window.app!.canvas return { lowQuality: canvas.low_quality, scale: canvas.ds.scale, @@ -32,11 +32,11 @@ test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => { // Can't access private _lowQualityZoomThreshold directly // Zoom out just above threshold (should still be high quality) - await comfyPage.zoom(120, 5) // Zoom out 5 steps + await comfyPage.canvasOps.zoom(120, 5) // Zoom out 5 steps await comfyPage.nextFrame() const aboveThresholdState = await comfyPage.page.evaluate(() => { - const canvas = window['app'].canvas + const canvas = window.app!.canvas return { lowQuality: canvas.low_quality, scale: canvas.ds.scale @@ -49,12 +49,12 @@ test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => { } // Zoom out more to trigger LOD (below threshold) - await comfyPage.zoom(120, 5) // Zoom out 5 more steps + await comfyPage.canvasOps.zoom(120, 5) // Zoom out 5 more steps await comfyPage.nextFrame() // Check that LOD is now active const zoomedOutState = await comfyPage.page.evaluate(() => { - const canvas = window['app'].canvas + const canvas = window.app!.canvas return { lowQuality: canvas.low_quality, scale: canvas.ds.scale @@ -65,12 +65,12 @@ test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => { expect(zoomedOutState.lowQuality).toBe(true) // Zoom back in to disable LOD (above threshold) - await comfyPage.zoom(-120, 15) // Zoom in 15 steps + await comfyPage.canvasOps.zoom(-120, 15) // Zoom in 15 steps await comfyPage.nextFrame() // Check that LOD is now inactive const zoomedInState = await comfyPage.page.evaluate(() => { - const canvas = window['app'].canvas + const canvas = window.app!.canvas return { lowQuality: canvas.low_quality, scale: canvas.ds.scale @@ -84,14 +84,17 @@ test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => { test('Should update threshold when font size setting changes', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('default') + await comfyPage.workflow.loadWorkflow('default') // Change the font size setting to 14px (more aggressive LOD) - await comfyPage.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 14) + await comfyPage.settings.setSetting( + 'LiteGraph.Canvas.MinFontSizeForLOD', + 14 + ) // Check that font size updated const newState = await comfyPage.page.evaluate(() => { - const canvas = window['app'].canvas + const canvas = window.app!.canvas return { minFontSize: canvas.min_font_size_for_lod } @@ -102,16 +105,16 @@ test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => { // At default zoom, LOD should still be inactive (scale is exactly 1.0, not less than) const lodState = await comfyPage.page.evaluate(() => { - return window['app'].canvas.low_quality + return window.app!.canvas.low_quality }) expect(lodState).toBe(false) // Zoom out slightly to trigger LOD - await comfyPage.zoom(120, 1) // Zoom out 1 step + await comfyPage.canvasOps.zoom(120, 1) // Zoom out 1 step await comfyPage.nextFrame() const afterZoom = await comfyPage.page.evaluate(() => { - const canvas = window['app'].canvas + const canvas = window.app!.canvas return { lowQuality: canvas.low_quality, scale: canvas.ds.scale @@ -125,18 +128,18 @@ test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => { test('Should disable LOD when font size is set to 0', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('default') + await comfyPage.workflow.loadWorkflow('default') // Disable LOD by setting font size to 0 - await comfyPage.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 0) + await comfyPage.settings.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 0) // Zoom out significantly - await comfyPage.zoom(120, 20) // Zoom out 20 steps + await comfyPage.canvasOps.zoom(120, 20) // Zoom out 20 steps await comfyPage.nextFrame() // LOD should remain disabled even at very low zoom const state = await comfyPage.page.evaluate(() => { - const canvas = window['app'].canvas + const canvas = window.app!.canvas return { lowQuality: canvas.low_quality, scale: canvas.ds.scale, @@ -154,15 +157,15 @@ test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => { { tag: '@screenshot' }, async ({ comfyPage }) => { // Load a workflow with text-heavy nodes for clear visual difference - await comfyPage.loadWorkflow('default') + await comfyPage.workflow.loadWorkflow('default') // Set zoom level clearly below the threshold to ensure LOD activates const targetZoom = 0.4 // Well below default threshold of ~0.571 // Zoom to target level await comfyPage.page.evaluate((zoom) => { - window['app'].canvas.ds.scale = zoom - window['app'].canvas.setDirty(true, true) + window.app!.canvas.ds.scale = zoom + window.app!.canvas.setDirty(true, true) }, targetZoom) await comfyPage.nextFrame() @@ -172,7 +175,7 @@ test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => { ) const lowQualityState = await comfyPage.page.evaluate(() => { - const canvas = window['app'].canvas + const canvas = window.app!.canvas return { lowQuality: canvas.low_quality, scale: canvas.ds.scale @@ -181,7 +184,10 @@ test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => { expect(lowQualityState.lowQuality).toBe(true) // Disable LOD to see high quality at same zoom - await comfyPage.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 0) + await comfyPage.settings.setSetting( + 'LiteGraph.Canvas.MinFontSizeForLOD', + 0 + ) await comfyPage.nextFrame() // Take snapshot with LOD disabled (full quality at same zoom) @@ -190,7 +196,7 @@ test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => { ) const highQualityState = await comfyPage.page.evaluate(() => { - const canvas = window['app'].canvas + const canvas = window.app!.canvas return { lowQuality: canvas.low_quality, scale: canvas.ds.scale diff --git a/browser_tests/tests/menu.spec.ts b/browser_tests/tests/menu.spec.ts index 50d89f993..50005afc5 100644 --- a/browser_tests/tests/menu.spec.ts +++ b/browser_tests/tests/menu.spec.ts @@ -4,14 +4,14 @@ import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.describe('Menu', { tag: '@ui' }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') }) test('Can register sidebar tab', async ({ comfyPage }) => { const initialChildrenCount = await comfyPage.menu.buttons.count() await comfyPage.page.evaluate(async () => { - window['app'].extensionManager.registerSidebarTab({ + window.app!.extensionManager.registerSidebarTab({ id: 'search', icon: 'pi pi-search', title: 'search', @@ -30,11 +30,11 @@ test.describe('Menu', { tag: '@ui' }, () => { test.describe('Workflows topbar tabs', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting( + await comfyPage.settings.setSetting( 'Comfy.Workflow.WorkflowTabsPosition', 'Topbar' ) - await comfyPage.setupWorkflowsDirectory({}) + await comfyPage.workflow.setupWorkflowsDirectory({}) }) test('Can show opened workflows', async ({ comfyPage }) => { @@ -101,8 +101,8 @@ test.describe('Menu', { tag: '@ui' }, () => { const checkmark = bottomPanelMenuItem.locator('.pi-check') // Check initial state of bottom panel (it's initially hidden) - const bottomPanel = comfyPage.page.locator('.bottom-panel') - await expect(bottomPanel).not.toBeVisible() + const { bottomPanel } = comfyPage + await expect(bottomPanel.root).not.toBeVisible() // Checkmark should be invisible initially (panel is hidden) await expect(checkmark).toHaveClass(/invisible/) @@ -113,7 +113,8 @@ test.describe('Menu', { tag: '@ui' }, () => { await expect(menu).toBeVisible() await expect(viewSubmenu).toBeVisible() - await expect(bottomPanel).toBeVisible() + // Verify bottom panel is now visible + await expect(bottomPanel.root).toBeVisible() // Checkmark should now be visible (panel is shown) await expect(checkmark).not.toHaveClass(/invisible/) @@ -126,7 +127,7 @@ test.describe('Menu', { tag: '@ui' }, () => { await expect(viewSubmenu).toBeVisible() // Verify bottom panel is hidden again - await expect(bottomPanel).not.toBeVisible() + await expect(bottomPanel.root).not.toBeVisible() // Checkmark should be invisible again (panel is hidden) await expect(checkmark).toHaveClass(/invisible/) @@ -153,7 +154,7 @@ test.describe('Menu', { tag: '@ui' }, () => { test('Can catch error when executing command', async ({ comfyPage }) => { await comfyPage.page.evaluate(() => { - window['app'].registerExtension({ + window.app!.registerExtension({ name: 'TestExtension1', commands: [ { @@ -173,7 +174,7 @@ test.describe('Menu', { tag: '@ui' }, () => { }) }) await comfyPage.menu.topbar.triggerTopbarCommand(['ext', 'foo-command']) - expect(await comfyPage.getVisibleToastCount()).toBe(1) + await expect.poll(() => comfyPage.toast.getVisibleToastCount()).toBe(1) }) test('Can navigate Theme menu and switch between Dark and Light themes', async ({ @@ -212,7 +213,9 @@ test.describe('Menu', { tag: '@ui' }, () => { await comfyPage.attachScreenshot('theme-menu-light-active') // Verify ColorPalette setting is set to "light" - expect(await comfyPage.getSetting('Comfy.ColorPalette')).toBe('light') + expect(await comfyPage.settings.getSetting('Comfy.ColorPalette')).toBe( + 'light' + ) // Close menu to see theme change await topbar.closeTopbarMenu() @@ -236,7 +239,9 @@ test.describe('Menu', { tag: '@ui' }, () => { await comfyPage.attachScreenshot('theme-menu-dark-active') // Verify ColorPalette setting is set to "dark" - expect(await comfyPage.getSetting('Comfy.ColorPalette')).toBe('dark') + expect(await comfyPage.settings.getSetting('Comfy.ColorPalette')).toBe( + 'dark' + ) // Close menu await topbar.closeTopbarMenu() @@ -249,16 +254,20 @@ test.describe('Menu', { tag: '@ui' }, () => { test(`Can migrate deprecated menu positions (${position})`, async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', position) - expect(await comfyPage.getSetting('Comfy.UseNewMenu')).toBe('Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', position) + expect(await comfyPage.settings.getSetting('Comfy.UseNewMenu')).toBe( + 'Top' + ) }) test(`Can migrate deprecated menu positions on initial load (${position})`, async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', position) + await comfyPage.settings.setSetting('Comfy.UseNewMenu', position) await comfyPage.setup() - expect(await comfyPage.getSetting('Comfy.UseNewMenu')).toBe('Top') + expect(await comfyPage.settings.getSetting('Comfy.UseNewMenu')).toBe( + 'Top' + ) }) }) }) diff --git a/browser_tests/tests/minimap.spec.ts b/browser_tests/tests/minimap.spec.ts index b30adb933..175d5324d 100644 --- a/browser_tests/tests/minimap.spec.ts +++ b/browser_tests/tests/minimap.spec.ts @@ -1,16 +1,15 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +import { TestIds } from '../fixtures/selectors' test.describe('Minimap', { tag: '@canvas' }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') - await comfyPage.setSetting('Comfy.Minimap.Visible', true) - await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true) - await comfyPage.loadWorkflow('default') - await comfyPage.page.waitForFunction( - () => window['app'] && window['app'].canvas - ) + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.Minimap.Visible', true) + await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', true) + await comfyPage.workflow.loadWorkflow('default') + await comfyPage.page.waitForFunction(() => window.app && window.app.canvas) }) test('Validate minimap is visible by default', async ({ comfyPage }) => { @@ -35,7 +34,9 @@ test.describe('Minimap', { tag: '@canvas' }, () => { }) test('Validate minimap toggle button state', async ({ comfyPage }) => { - const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button') + const toggleButton = comfyPage.page.getByTestId( + TestIds.canvas.toggleMinimapButton + ) await expect(toggleButton).toBeVisible() @@ -45,7 +46,9 @@ test.describe('Minimap', { tag: '@canvas' }, () => { test('Validate minimap can be toggled off and on', async ({ comfyPage }) => { const minimapContainer = comfyPage.page.locator('.litegraph-minimap') - const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button') + const toggleButton = comfyPage.page.getByTestId( + TestIds.canvas.toggleMinimapButton + ) await expect(minimapContainer).toBeVisible() diff --git a/browser_tests/tests/mobileBaseline.spec.ts b/browser_tests/tests/mobileBaseline.spec.ts index 9e9ddec94..1b7b7e4d5 100644 --- a/browser_tests/tests/mobileBaseline.spec.ts +++ b/browser_tests/tests/mobileBaseline.spec.ts @@ -1,22 +1,24 @@ -import { comfyPageFixture as test } from '../fixtures/ComfyPage' import { expect } from '@playwright/test' +import { comfyPageFixture as test } from '../fixtures/ComfyPage' +import { TestIds } from '../fixtures/selectors' + test.describe( 'Mobile Baseline Snapshots', { tag: ['@mobile', '@screenshot'] }, () => { test('@mobile empty canvas', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.ConfirmClear', false) - await comfyPage.executeCommand('Comfy.ClearWorkflow') + await comfyPage.settings.setSetting('Comfy.ConfirmClear', false) + await comfyPage.command.executeCommand('Comfy.ClearWorkflow') await expect(async () => { - expect(await comfyPage.getGraphNodesCount()).toBe(0) - }).toPass({ timeout: 256 }) + expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe(0) + }).toPass({ timeout: 5000 }) await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot('mobile-empty-canvas.png') }) test('@mobile default workflow', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('default') + await comfyPage.workflow.loadWorkflow('default') await expect(comfyPage.canvas).toHaveScreenshot( 'mobile-default-workflow.png' ) @@ -30,7 +32,9 @@ test.describe( 'mobile-settings-dialog.png', { mask: [ - comfyPage.settingDialog.root.getByTestId('current-user-indicator') + comfyPage.settingDialog.root.getByTestId( + TestIds.user.currentUserIndicator + ) ] } ) diff --git a/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-default-workflow-mobile-chrome-linux.png b/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-default-workflow-mobile-chrome-linux.png index c7334eb16..c0276b2ac 100644 Binary files a/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-default-workflow-mobile-chrome-linux.png and b/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-default-workflow-mobile-chrome-linux.png differ diff --git a/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-settings-dialog-mobile-chrome-linux.png b/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-settings-dialog-mobile-chrome-linux.png index 6bb4ac6e3..17de9b03a 100644 Binary files a/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-settings-dialog-mobile-chrome-linux.png and b/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-settings-dialog-mobile-chrome-linux.png differ diff --git a/browser_tests/tests/nodeBadge.spec.ts b/browser_tests/tests/nodeBadge.spec.ts index f4cf493a9..873ac1005 100644 --- a/browser_tests/tests/nodeBadge.spec.ts +++ b/browser_tests/tests/nodeBadge.spec.ts @@ -5,14 +5,14 @@ import { NodeBadgeMode } from '../../src/types/nodeSource' import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Node Badge', { tag: ['@screenshot', '@smoke', '@node'] }, () => { test('Can add badge', async ({ comfyPage }) => { await comfyPage.page.evaluate(() => { - const LGraphBadge = window['LGraphBadge'] - const app = window['app'] as ComfyApp + const LGraphBadge = window.LGraphBadge! + const app = window.app as ComfyApp const graph = app.graph const nodes = graph.nodes @@ -28,8 +28,8 @@ test.describe('Node Badge', { tag: ['@screenshot', '@smoke', '@node'] }, () => { test('Can add multiple badges', async ({ comfyPage }) => { await comfyPage.page.evaluate(() => { - const LGraphBadge = window['LGraphBadge'] - const app = window['app'] as ComfyApp + const LGraphBadge = window.LGraphBadge! + const app = window.app as ComfyApp const graph = app.graph const nodes = graph.nodes @@ -48,8 +48,8 @@ test.describe('Node Badge', { tag: ['@screenshot', '@smoke', '@node'] }, () => { test('Can add badge left-side', async ({ comfyPage }) => { await comfyPage.page.evaluate(() => { - const LGraphBadge = window['LGraphBadge'] - const app = window['app'] as ComfyApp + const LGraphBadge = window.LGraphBadge! + const app = window.app as ComfyApp const graph = app.graph const nodes = graph.nodes @@ -73,11 +73,17 @@ test.describe( Object.values(NodeBadgeMode).forEach(async (mode) => { test(`Shows node badges (${mode})`, async ({ comfyPage }) => { // Execution error workflow has both custom node and core node. - await comfyPage.loadWorkflow('nodes/execution_error') - await comfyPage.setSetting('Comfy.NodeBadge.NodeSourceBadgeMode', mode) - await comfyPage.setSetting('Comfy.NodeBadge.NodeIdBadgeMode', mode) + await comfyPage.workflow.loadWorkflow('nodes/execution_error') + await comfyPage.settings.setSetting( + 'Comfy.NodeBadge.NodeSourceBadgeMode', + mode + ) + await comfyPage.settings.setSetting( + 'Comfy.NodeBadge.NodeIdBadgeMode', + mode + ) await comfyPage.nextFrame() - await comfyPage.resetView() + await comfyPage.canvasOps.resetView() await expect(comfyPage.canvas).toHaveScreenshot( `node-badge-${mode}.png` ) @@ -93,14 +99,14 @@ test.describe( test('Can show node badge with unknown color palette', async ({ comfyPage }) => { - await comfyPage.setSetting( + await comfyPage.settings.setSetting( 'Comfy.NodeBadge.NodeIdBadgeMode', NodeBadgeMode.ShowAll ) - await comfyPage.setSetting('Comfy.ColorPalette', 'unknown') + await comfyPage.settings.setSetting('Comfy.ColorPalette', 'unknown') await comfyPage.nextFrame() // Click empty space to trigger canvas re-render. - await comfyPage.clickEmptySpace() + await comfyPage.canvasOps.clickEmptySpace() await expect(comfyPage.canvas).toHaveScreenshot( 'node-badge-unknown-color-palette.png' ) @@ -109,14 +115,14 @@ test.describe( test('Can show node badge with light color palette', async ({ comfyPage }) => { - await comfyPage.setSetting( + await comfyPage.settings.setSetting( 'Comfy.NodeBadge.NodeIdBadgeMode', NodeBadgeMode.ShowAll ) - await comfyPage.setSetting('Comfy.ColorPalette', 'light') + await comfyPage.settings.setSetting('Comfy.ColorPalette', 'light') await comfyPage.nextFrame() // Click empty space to trigger canvas re-render. - await comfyPage.clickEmptySpace() + await comfyPage.canvasOps.clickEmptySpace() await expect(comfyPage.canvas).toHaveScreenshot( 'node-badge-light-color-palette.png' ) diff --git a/browser_tests/tests/nodeDisplay.spec.ts b/browser_tests/tests/nodeDisplay.spec.ts index 541ddaf0f..061419838 100644 --- a/browser_tests/tests/nodeDisplay.spec.ts +++ b/browser_tests/tests/nodeDisplay.spec.ts @@ -3,40 +3,40 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) // If an input is optional by node definition, it should be shown as // a hollow circle no matter what shape it was defined in the workflow JSON. test.describe('Optional input', { tag: ['@screenshot', '@node'] }, () => { test('No shape specified', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('inputs/optional_input_no_shape') + await comfyPage.workflow.loadWorkflow('inputs/optional_input_no_shape') await expect(comfyPage.canvas).toHaveScreenshot('optional_input.png') }) test('Wrong shape specified', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('inputs/optional_input_wrong_shape') + await comfyPage.workflow.loadWorkflow('inputs/optional_input_wrong_shape') await expect(comfyPage.canvas).toHaveScreenshot('optional_input.png') }) test('Correct shape specified', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('inputs/optional_input_correct_shape') + await comfyPage.workflow.loadWorkflow('inputs/optional_input_correct_shape') await expect(comfyPage.canvas).toHaveScreenshot('optional_input.png') }) test('Force input', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('inputs/force_input') + await comfyPage.workflow.loadWorkflow('inputs/force_input') await expect(comfyPage.canvas).toHaveScreenshot('force_input.png') }) test('Default input', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('inputs/default_input') + await comfyPage.workflow.loadWorkflow('inputs/default_input') await expect(comfyPage.canvas).toHaveScreenshot('default_input.png') }) test('Only optional inputs', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('inputs/only_optional_inputs') - expect(await comfyPage.getGraphNodesCount()).toBe(1) + await comfyPage.workflow.loadWorkflow('inputs/only_optional_inputs') + expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe(1) await expect( comfyPage.page.locator('.comfy-missing-nodes') ).not.toBeVisible() @@ -47,37 +47,45 @@ test.describe('Optional input', { tag: ['@screenshot', '@node'] }, () => { ) }) test('Old workflow with converted input', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('inputs/old_workflow_converted_input') - const node = await comfyPage.getNodeRefById('1') - const inputs = await node.getProperty('inputs') + await comfyPage.workflow.loadWorkflow('inputs/old_workflow_converted_input') + const node = await comfyPage.nodeOps.getNodeRefById('1') + const inputs = (await node.getProperty('inputs')) as { + name: string + link?: number | null + }[] const vaeInput = inputs.find((w) => w.name === 'vae') const convertedInput = inputs.find((w) => w.name === 'strength') expect(vaeInput).toBeDefined() expect(convertedInput).toBeDefined() - expect(vaeInput.link).toBeNull() - expect(convertedInput.link).not.toBeNull() + expect(vaeInput!.link).toBeNull() + expect(convertedInput!.link).not.toBeNull() }) test('Renamed converted input', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('inputs/renamed_converted_widget') - const node = await comfyPage.getNodeRefById('3') - const inputs = await node.getProperty('inputs') + await comfyPage.workflow.loadWorkflow('inputs/renamed_converted_widget') + const node = await comfyPage.nodeOps.getNodeRefById('3') + const inputs = (await node.getProperty('inputs')) as { name: string }[] const renamedInput = inputs.find((w) => w.name === 'breadth') expect(renamedInput).toBeUndefined() }) test('slider', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('inputs/simple_slider') + await comfyPage.workflow.loadWorkflow('inputs/simple_slider') await expect(comfyPage.canvas).toHaveScreenshot('simple_slider.png') }) test('unknown converted widget', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Workflow.ShowMissingNodesWarning', false) - await comfyPage.loadWorkflow('missing/missing_nodes_converted_widget') + await comfyPage.settings.setSetting( + 'Comfy.Workflow.ShowMissingNodesWarning', + false + ) + await comfyPage.workflow.loadWorkflow( + 'missing/missing_nodes_converted_widget' + ) await expect(comfyPage.canvas).toHaveScreenshot( 'missing_nodes_converted_widget.png' ) }) test('dynamically added input', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('inputs/dynamically_added_input') + await comfyPage.workflow.loadWorkflow('inputs/dynamically_added_input') await expect(comfyPage.canvas).toHaveScreenshot( 'dynamically_added_input.png' ) diff --git a/browser_tests/tests/nodeHelp.spec.ts b/browser_tests/tests/nodeHelp.spec.ts index 095cb37ec..5260ade0d 100644 --- a/browser_tests/tests/nodeHelp.spec.ts +++ b/browser_tests/tests/nodeHelp.spec.ts @@ -12,7 +12,7 @@ async function selectNodeWithPan(comfyPage: ComfyPage, nodeRef: NodeReference) { const nodePos = await nodeRef.getPosition() await comfyPage.page.evaluate((pos) => { - const app = window['app']! + const app = window.app! const canvas = app.canvas canvas.ds.offset[0] = -pos.x + canvas.canvas.width / 2 canvas.ds.offset[1] = -pos.y + canvas.canvas.height / 2 + 100 @@ -26,17 +26,18 @@ async function selectNodeWithPan(comfyPage: ComfyPage, nodeRef: NodeReference) { test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => { test.beforeEach(async ({ comfyPage }) => { await comfyPage.setup() - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') }) test.describe('Selection Toolbox', () => { test('Should open help menu for selected node', async ({ comfyPage }) => { // Load a workflow with a node - await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true) - await comfyPage.loadWorkflow('default') + await comfyPage.settings.setSetting('Comfy.Canvas.SelectionToolbox', true) + await comfyPage.workflow.loadWorkflow('default') // Select a single node (KSampler) using node references - const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler') + const ksamplerNodes = + await comfyPage.nodeOps.getNodeRefsByType('KSampler') if (ksamplerNodes.length === 0) { throw new Error('No KSampler nodes found in the workflow') } @@ -87,7 +88,9 @@ test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => { await ksamplerNode.hover() // Click the help button - const helpButton = ksamplerNode.locator('button:has(.pi-question)') + const helpButton = ksamplerNode.getByRole('button', { + name: /learn more/i + }) await expect(helpButton).toBeVisible() await helpButton.click() @@ -117,7 +120,9 @@ test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => { .filter({ hasText: 'KSampler' }) .first() await ksamplerNode.hover() - const helpButton = ksamplerNode.locator('button:has(.pi-question)') + const helpButton = ksamplerNode.getByRole('button', { + name: /learn more/i + }) await helpButton.click() // Verify help page is shown @@ -141,7 +146,7 @@ test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => { test.describe('Help Content', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true) + await comfyPage.settings.setSetting('Comfy.Canvas.SelectionToolbox', true) }) test('Should display loading state while fetching help', async ({ @@ -157,8 +162,9 @@ test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => { }) // Load workflow and select a node - await comfyPage.loadWorkflow('default') - const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler') + await comfyPage.workflow.loadWorkflow('default') + const ksamplerNodes = + await comfyPage.nodeOps.getNodeRefsByType('KSampler') await selectNodeWithPan(comfyPage, ksamplerNodes[0]) // Click help button @@ -189,8 +195,9 @@ test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => { }) // Load workflow and select a node - await comfyPage.loadWorkflow('default') - const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler') + await comfyPage.workflow.loadWorkflow('default') + const ksamplerNodes = + await comfyPage.nodeOps.getNodeRefsByType('KSampler') await selectNodeWithPan(comfyPage, ksamplerNodes[0]) // Click help button @@ -226,8 +233,9 @@ test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => { }) }) - await comfyPage.loadWorkflow('default') - const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler') + await comfyPage.workflow.loadWorkflow('default') + const ksamplerNodes = + await comfyPage.nodeOps.getNodeRefsByType('KSampler') await selectNodeWithPan(comfyPage, ksamplerNodes[0]) const helpButton = comfyPage.page.locator( @@ -276,8 +284,9 @@ test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => { }) }) - await comfyPage.loadWorkflow('default') - const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler') + await comfyPage.workflow.loadWorkflow('default') + const ksamplerNodes = + await comfyPage.nodeOps.getNodeRefsByType('KSampler') await selectNodeWithPan(comfyPage, ksamplerNodes[0]) const helpButton = comfyPage.page.locator( @@ -323,7 +332,7 @@ test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => { comfyPage }) => { // First load workflow with custom node - await comfyPage.loadWorkflow('groupnodes/group_node_v1.3.3') + await comfyPage.workflow.loadWorkflow('groupnodes/group_node_v1.3.3') // Mock custom node documentation with fallback await comfyPage.page.route( @@ -347,10 +356,10 @@ This is documentation for a custom node. // Find and select a custom/group node const nodeRefs = await comfyPage.page.evaluate(() => { - return window['app']!.graph!.nodes.map((n) => n.id) + return window.app!.graph!.nodes.map((n) => n.id) }) if (nodeRefs.length > 0) { - const firstNode = await comfyPage.getNodeRefById(nodeRefs[0]) + const firstNode = await comfyPage.nodeOps.getNodeRefById(nodeRefs[0]) await selectNodeWithPan(comfyPage, firstNode) } @@ -393,8 +402,9 @@ This is documentation for a custom node. }) }) - await comfyPage.loadWorkflow('default') - const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler') + await comfyPage.workflow.loadWorkflow('default') + const ksamplerNodes = + await comfyPage.nodeOps.getNodeRefsByType('KSampler') await selectNodeWithPan(comfyPage, ksamplerNodes[0]) const helpButton = comfyPage.page.locator( @@ -460,10 +470,11 @@ This is English documentation. }) // Set locale to Japanese - await comfyPage.setSetting('Comfy.Locale', 'ja') + await comfyPage.settings.setSetting('Comfy.Locale', 'ja') - await comfyPage.loadWorkflow('default') - const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler') + await comfyPage.workflow.loadWorkflow('default') + const ksamplerNodes = + await comfyPage.nodeOps.getNodeRefsByType('KSampler') await selectNodeWithPan(comfyPage, ksamplerNodes[0]) const helpButton = comfyPage.page.locator( @@ -478,7 +489,7 @@ This is English documentation. await expect(helpPage).toContainText('これは日本語のドキュメントです') // Reset locale - await comfyPage.setSetting('Comfy.Locale', 'en') + await comfyPage.settings.setSetting('Comfy.Locale', 'en') }) test('Should handle network errors gracefully', async ({ comfyPage }) => { @@ -487,8 +498,9 @@ This is English documentation. await route.abort('failed') }) - await comfyPage.loadWorkflow('default') - const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler') + await comfyPage.workflow.loadWorkflow('default') + const ksamplerNodes = + await comfyPage.nodeOps.getNodeRefsByType('KSampler') await selectNodeWithPan(comfyPage, ksamplerNodes[0]) const helpButton = comfyPage.page.locator( @@ -530,11 +542,12 @@ This is English documentation. } ) - await comfyPage.loadWorkflow('default') + await comfyPage.workflow.loadWorkflow('default') await fitToViewInstant(comfyPage) // Select KSampler first - const ksamplerNodes = await comfyPage.getNodeRefsByType('KSampler') + const ksamplerNodes = + await comfyPage.nodeOps.getNodeRefsByType('KSampler') await selectNodeWithPan(comfyPage, ksamplerNodes[0]) const helpButton = comfyPage.page.locator( @@ -549,7 +562,7 @@ This is English documentation. await expect(helpPage).toContainText('This is KSampler documentation') // Now select Checkpoint Loader - const checkpointNodes = await comfyPage.getNodeRefsByType( + const checkpointNodes = await comfyPage.nodeOps.getNodeRefsByType( 'CheckpointLoaderSimple' ) await selectNodeWithPan(comfyPage, checkpointNodes[0]) diff --git a/browser_tests/tests/nodeSearchBox.spec.ts b/browser_tests/tests/nodeSearchBox.spec.ts index f227b2932..89bfe4a5e 100644 --- a/browser_tests/tests/nodeSearchBox.spec.ts +++ b/browser_tests/tests/nodeSearchBox.spec.ts @@ -2,32 +2,39 @@ import { comfyExpect as expect, comfyPageFixture as test } from '../fixtures/ComfyPage' +import type { ComfyPage } from '../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Node search box', { tag: '@node' }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.LinkRelease.Action', 'search box') - await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'search box') - await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default') + await comfyPage.settings.setSetting( + 'Comfy.LinkRelease.Action', + 'search box' + ) + await comfyPage.settings.setSetting( + 'Comfy.LinkRelease.ActionShift', + 'search box' + ) + await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default') }) test(`Can trigger on empty canvas double click`, async ({ comfyPage }) => { - await comfyPage.doubleClickCanvas() + await comfyPage.canvasOps.doubleClick() await expect(comfyPage.searchBox.input).toHaveCount(1) }) test(`Can trigger on group body double click`, async ({ comfyPage }) => { - await comfyPage.loadWorkflow('groups/single_group_only') + await comfyPage.workflow.loadWorkflow('groups/single_group_only') await comfyPage.page.mouse.dblclick(50, 50, { delay: 5 }) await comfyPage.nextFrame() await expect(comfyPage.searchBox.input).toHaveCount(1) }) test('Can trigger on link release', async ({ comfyPage }) => { - await comfyPage.disconnectEdge() + await comfyPage.canvasOps.disconnectEdge() await expect(comfyPage.searchBox.input).toHaveCount(1) }) @@ -37,24 +44,24 @@ test.describe('Node search box', { tag: '@node' }, () => { // Start fresh to test new user behavior await comfyPage.setup({ clearStorage: true }) // Simulate new user with 1.24.1+ installed version - await comfyPage.setSetting('Comfy.InstalledVersion', '1.24.1') - await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default') + await comfyPage.settings.setSetting('Comfy.InstalledVersion', '1.24.1') + await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default') // Don't set LinkRelease settings explicitly to test versioned defaults - await comfyPage.disconnectEdge() + await comfyPage.canvasOps.disconnectEdge() await expect(comfyPage.searchBox.input).toHaveCount(1) await expect(comfyPage.searchBox.input).toBeVisible() }) test('Can add node', { tag: '@screenshot' }, async ({ comfyPage }) => { - await comfyPage.doubleClickCanvas() + await comfyPage.canvasOps.doubleClick() await expect(comfyPage.searchBox.input).toHaveCount(1) await comfyPage.searchBox.fillAndSelectFirstNode('KSampler') await expect(comfyPage.canvas).toHaveScreenshot('added-node.png') }) test('Can auto link node', { tag: '@screenshot' }, async ({ comfyPage }) => { - await comfyPage.disconnectEdge() + await comfyPage.canvasOps.disconnectEdge() // Select the second item as the first item is always reroute await comfyPage.searchBox.fillAndSelectFirstNode('CLIPTextEncode', { suggestionIndex: 0 @@ -66,18 +73,18 @@ test.describe('Node search box', { tag: '@node' }, () => { 'Can auto link batch moved node', { tag: '@screenshot' }, async ({ comfyPage }) => { - await comfyPage.loadWorkflow('links/batch_move_links') + await comfyPage.workflow.loadWorkflow('links/batch_move_links') + + // Get the CLIP output slot (index 1) from the first CheckpointLoaderSimple node (id: 4) + const checkpointNode = await comfyPage.nodeOps.getNodeRefById(4) + const clipOutputSlot = await checkpointNode.getOutput(1) + const outputSlotPos = await clipOutputSlot.getPosition() + + // Use a position in the empty canvas area (top-left corner) + const emptySpacePos = { x: 5, y: 5 } - const outputSlot1Pos = { - x: 304, - y: 127 - } - const emptySpacePos = { - x: 5, - y: 5 - } await comfyPage.page.keyboard.down('Shift') - await comfyPage.dragAndDrop(outputSlot1Pos, emptySpacePos) + await comfyPage.canvasOps.dragAndDrop(outputSlotPos, emptySpacePos) await comfyPage.page.keyboard.up('Shift') // Select the second item as the first item is always reroute @@ -94,7 +101,7 @@ test.describe('Node search box', { tag: '@node' }, () => { 'Link release connecting to node with no slots', { tag: '@screenshot' }, async ({ comfyPage }) => { - await comfyPage.disconnectEdge() + await comfyPage.canvasOps.disconnectEdge() await expect(comfyPage.searchBox.input).toHaveCount(1) await comfyPage.page.locator('.p-chip-remove-icon').click() await comfyPage.searchBox.fillAndSelectFirstNode('KSampler') @@ -106,7 +113,7 @@ test.describe('Node search box', { tag: '@node' }, () => { test('Has correct aria-labels on search results', async ({ comfyPage }) => { const node = 'Load Checkpoint' - await comfyPage.doubleClickCanvas() + await comfyPage.canvasOps.doubleClick() await comfyPage.searchBox.input.waitFor({ state: 'visible' }) await comfyPage.searchBox.input.fill(node) await comfyPage.searchBox.dropdown.waitFor({ state: 'visible' }) @@ -117,7 +124,7 @@ test.describe('Node search box', { tag: '@node' }, () => { test('@mobile Can trigger on empty canvas tap', async ({ comfyPage }) => { await comfyPage.closeMenu() - await comfyPage.loadWorkflow('nodes/single_ksampler') + await comfyPage.workflow.loadWorkflow('nodes/single_ksampler') const screenCenter = { x: 200, y: 400 @@ -132,7 +139,10 @@ test.describe('Node search box', { tag: '@node' }, () => { }) test.describe('Filtering', () => { - const expectFilterChips = async (comfyPage, expectedTexts: string[]) => { + const expectFilterChips = async ( + comfyPage: ComfyPage, + expectedTexts: string[] + ) => { const chips = comfyPage.searchBox.filterChips // Check that the number of chips matches the expected count @@ -149,7 +159,7 @@ test.describe('Node search box', { tag: '@node' }, () => { } test.beforeEach(async ({ comfyPage }) => { - await comfyPage.doubleClickCanvas() + await comfyPage.canvasOps.doubleClick() }) test('Can add filter', async ({ comfyPage }) => { @@ -241,7 +251,7 @@ test.describe('Node search box', { tag: '@node' }, () => { test.describe('Input focus behavior', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.doubleClickCanvas() + await comfyPage.canvasOps.doubleClick() }) test('focuses input after adding a filter', async ({ comfyPage }) => { @@ -259,16 +269,22 @@ test.describe('Node search box', { tag: '@node' }, () => { test.describe('Release context menu', { tag: '@node' }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.LinkRelease.Action', 'context menu') - await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'search box') - await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default') + await comfyPage.settings.setSetting( + 'Comfy.LinkRelease.Action', + 'context menu' + ) + await comfyPage.settings.setSetting( + 'Comfy.LinkRelease.ActionShift', + 'search box' + ) + await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default') }) test( 'Can trigger on link release', { tag: '@screenshot' }, async ({ comfyPage }) => { - await comfyPage.disconnectEdge() + await comfyPage.canvasOps.disconnectEdge() const contextMenu = comfyPage.page.locator('.litecontextmenu') // Wait for context menu with correct title (slot name | slot type) // The title shows the output slot name and type from the disconnected link @@ -287,9 +303,10 @@ test.describe('Release context menu', { tag: '@node' }, () => { 'Can search and add node from context menu', { tag: '@screenshot' }, async ({ comfyPage, comfyMouse }) => { - await comfyPage.disconnectEdge() + await comfyPage.canvasOps.disconnectEdge() await comfyMouse.move({ x: 10, y: 10 }) - await comfyPage.clickContextMenuItem('Search') + await comfyPage.contextMenu.clickMenuItem('Search') + await comfyPage.nextFrame() await comfyPage.searchBox.fillAndSelectFirstNode('CLIP Prompt') await expect(comfyPage.canvas).toHaveScreenshot( 'link-context-menu-search.png' @@ -303,11 +320,11 @@ test.describe('Release context menu', { tag: '@node' }, () => { // Start fresh to test existing user behavior await comfyPage.setup({ clearStorage: true }) // Simulate existing user with pre-1.24.1 version - await comfyPage.setSetting('Comfy.InstalledVersion', '1.23.0') - await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default') + await comfyPage.settings.setSetting('Comfy.InstalledVersion', '1.23.0') + await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default') // Don't set LinkRelease settings explicitly to test versioned defaults - await comfyPage.disconnectEdge() + await comfyPage.canvasOps.disconnectEdge() // Context menu should appear, search box should not await expect(comfyPage.searchBox.input).toHaveCount(0) const contextMenu = comfyPage.page.locator('.litecontextmenu') @@ -319,12 +336,15 @@ test.describe('Release context menu', { tag: '@node' }, () => { }) => { // Start fresh and simulate new user who should get search box by default await comfyPage.setup({ clearStorage: true }) - await comfyPage.setSetting('Comfy.InstalledVersion', '1.24.1') + await comfyPage.settings.setSetting('Comfy.InstalledVersion', '1.24.1') // But explicitly set to context menu (overriding versioned default) - await comfyPage.setSetting('Comfy.LinkRelease.Action', 'context menu') - await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default') + await comfyPage.settings.setSetting( + 'Comfy.LinkRelease.Action', + 'context menu' + ) + await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default') - await comfyPage.disconnectEdge() + await comfyPage.canvasOps.disconnectEdge() // Context menu should appear due to explicit setting, not search box await expect(comfyPage.searchBox.input).toHaveCount(0) const contextMenu = comfyPage.page.locator('.litecontextmenu') diff --git a/browser_tests/tests/noteNode.spec.ts b/browser_tests/tests/noteNode.spec.ts index 4bb551962..c50a533ea 100644 --- a/browser_tests/tests/noteNode.spec.ts +++ b/browser_tests/tests/noteNode.spec.ts @@ -3,12 +3,12 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Note Node', { tag: '@node' }, () => { test('Can load node nodes', { tag: '@screenshot' }, async ({ comfyPage }) => { - await comfyPage.loadWorkflow('nodes/note_nodes') + await comfyPage.workflow.loadWorkflow('nodes/note_nodes') await expect(comfyPage.canvas).toHaveScreenshot('note_nodes.png') }) }) diff --git a/browser_tests/tests/primitiveNode.spec.ts b/browser_tests/tests/primitiveNode.spec.ts index 2e2e11ae5..aa0e029e0 100644 --- a/browser_tests/tests/primitiveNode.spec.ts +++ b/browser_tests/tests/primitiveNode.spec.ts @@ -4,21 +4,25 @@ import { comfyPageFixture as test } from '../fixtures/ComfyPage' import type { NodeReference } from '../fixtures/utils/litegraphUtils' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Primitive Node', { tag: ['@screenshot', '@node'] }, () => { test('Can load with correct size', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('primitive/primitive_node') + await comfyPage.workflow.loadWorkflow('primitive/primitive_node') await expect(comfyPage.canvas).toHaveScreenshot('primitive_node.png') }) // When link is dropped on widget, it should automatically convert the widget // to input. test('Can connect to widget', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('primitive/primitive_node_unconnected') - const primitiveNode: NodeReference = await comfyPage.getNodeRefById(1) - const ksamplerNode: NodeReference = await comfyPage.getNodeRefById(2) + await comfyPage.workflow.loadWorkflow( + 'primitive/primitive_node_unconnected' + ) + const primitiveNode: NodeReference = + await comfyPage.nodeOps.getNodeRefById(1) + const ksamplerNode: NodeReference = + await comfyPage.nodeOps.getNodeRefById(2) // Connect the output of the primitive node to the input of first widget of the ksampler node await primitiveNode.connectWidget(0, ksamplerNode, 0) await expect(comfyPage.canvas).toHaveScreenshot( @@ -27,11 +31,13 @@ test.describe('Primitive Node', { tag: ['@screenshot', '@node'] }, () => { }) test('Can connect to dom widget', async ({ comfyPage }) => { - await comfyPage.loadWorkflow( + await comfyPage.workflow.loadWorkflow( 'primitive/primitive_node_unconnected_dom_widget' ) - const primitiveNode: NodeReference = await comfyPage.getNodeRefById(1) - const clipEncoderNode: NodeReference = await comfyPage.getNodeRefById(2) + const primitiveNode: NodeReference = + await comfyPage.nodeOps.getNodeRefById(1) + const clipEncoderNode: NodeReference = + await comfyPage.nodeOps.getNodeRefById(2) await primitiveNode.connectWidget(0, clipEncoderNode, 0) await expect(comfyPage.canvas).toHaveScreenshot( 'primitive_node_connected_dom_widget.png' @@ -39,9 +45,13 @@ test.describe('Primitive Node', { tag: ['@screenshot', '@node'] }, () => { }) test('Can connect to static primitive node', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('primitive/static_primitive_unconnected') - const primitiveNode: NodeReference = await comfyPage.getNodeRefById(1) - const ksamplerNode: NodeReference = await comfyPage.getNodeRefById(2) + await comfyPage.workflow.loadWorkflow( + 'primitive/static_primitive_unconnected' + ) + const primitiveNode: NodeReference = + await comfyPage.nodeOps.getNodeRefById(1) + const ksamplerNode: NodeReference = + await comfyPage.nodeOps.getNodeRefById(2) await primitiveNode.connectWidget(0, ksamplerNode, 0) await expect(comfyPage.canvas).toHaveScreenshot( 'static_primitive_connected.png' @@ -51,7 +61,7 @@ test.describe('Primitive Node', { tag: ['@screenshot', '@node'] }, () => { test('Report missing nodes when connect to missing node', async ({ comfyPage }) => { - await comfyPage.loadWorkflow( + await comfyPage.workflow.loadWorkflow( 'primitive/primitive_node_connect_missing_node' ) // Wait for the element with the .comfy-missing-nodes selector to be visible diff --git a/browser_tests/tests/propertiesPanel/propertiesPanel.spec.ts b/browser_tests/tests/propertiesPanel/propertiesPanel.spec.ts index ff452b54a..32ff8722d 100644 --- a/browser_tests/tests/propertiesPanel/propertiesPanel.spec.ts +++ b/browser_tests/tests/propertiesPanel/propertiesPanel.spec.ts @@ -10,7 +10,10 @@ test.describe('Properties panel', () => { await expect(propertiesPanel.panelTitle).toContainText('Workflow Overview') - await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)']) + await comfyPage.nodeOps.selectNodes([ + 'KSampler', + 'CLIP Text Encode (Prompt)' + ]) await expect(propertiesPanel.panelTitle).toContainText('3 items selected') await expect(propertiesPanel.root.getByText('KSampler')).toHaveCount(1) diff --git a/browser_tests/tests/propertiesPanel/propertiesPanelPosition.spec.ts b/browser_tests/tests/propertiesPanel/propertiesPanelPosition.spec.ts index ee2c85eda..eefbe6720 100644 --- a/browser_tests/tests/propertiesPanel/propertiesPanelPosition.spec.ts +++ b/browser_tests/tests/propertiesPanel/propertiesPanelPosition.spec.ts @@ -1,6 +1,7 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../../fixtures/ComfyPage' +import { TestIds } from '../../fixtures/selectors' test.describe('Properties panel position', () => { test.beforeEach(async ({ comfyPage }) => { @@ -12,10 +13,12 @@ test.describe('Properties panel position', () => { test('positions on the right when sidebar is on the left', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Sidebar.Location', 'left') + await comfyPage.settings.setSetting('Comfy.Sidebar.Location', 'left') await comfyPage.nextFrame() - const propertiesPanel = comfyPage.page.getByTestId('properties-panel') + const propertiesPanel = comfyPage.page.getByTestId( + TestIds.propertiesPanel.root + ) const sidebar = comfyPage.page.locator('.side-bar-panel').first() await expect(propertiesPanel).toBeVisible() @@ -36,10 +39,12 @@ test.describe('Properties panel position', () => { test('positions on the left when sidebar is on the right', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Sidebar.Location', 'right') + await comfyPage.settings.setSetting('Comfy.Sidebar.Location', 'right') await comfyPage.nextFrame() - const propertiesPanel = comfyPage.page.getByTestId('properties-panel') + const propertiesPanel = comfyPage.page.getByTestId( + TestIds.propertiesPanel.root + ) const sidebar = comfyPage.page.locator('.side-bar-panel').first() await expect(propertiesPanel).toBeVisible() @@ -60,10 +65,12 @@ test.describe('Properties panel position', () => { test('close button icon updates based on sidebar location', async ({ comfyPage }) => { - const propertiesPanel = comfyPage.page.getByTestId('properties-panel') + const propertiesPanel = comfyPage.page.getByTestId( + TestIds.propertiesPanel.root + ) // When sidebar is on the left, panel is on the right - await comfyPage.setSetting('Comfy.Sidebar.Location', 'left') + await comfyPage.settings.setSetting('Comfy.Sidebar.Location', 'left') await comfyPage.nextFrame() await expect(propertiesPanel).toBeVisible() @@ -74,7 +81,7 @@ test.describe('Properties panel position', () => { await expect(closeButtonLeft).toHaveClass(/lucide--panel-right/) // When sidebar is on the right, panel is on the left - await comfyPage.setSetting('Comfy.Sidebar.Location', 'right') + await comfyPage.settings.setSetting('Comfy.Sidebar.Location', 'right') await comfyPage.nextFrame() const closeButtonRight = propertiesPanel diff --git a/browser_tests/tests/recordAudio.spec.ts b/browser_tests/tests/recordAudio.spec.ts index d343990c1..07d9c4808 100644 --- a/browser_tests/tests/recordAudio.spec.ts +++ b/browser_tests/tests/recordAudio.spec.ts @@ -3,7 +3,7 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Record Audio Node', { tag: '@screenshot' }, () => { @@ -11,7 +11,7 @@ test.describe('Record Audio Node', { tag: '@screenshot' }, () => { comfyPage }) => { // Open the search box by double clicking on the canvas - await comfyPage.doubleClickCanvas() + await comfyPage.canvasOps.doubleClick() await expect(comfyPage.searchBox.input).toHaveCount(1) // Search for and add the RecordAudio node @@ -19,7 +19,8 @@ test.describe('Record Audio Node', { tag: '@screenshot' }, () => { await comfyPage.nextFrame() // Verify the RecordAudio node was added - const recordAudioNodes = await comfyPage.getNodeRefsByType('RecordAudio') + const recordAudioNodes = + await comfyPage.nodeOps.getNodeRefsByType('RecordAudio') expect(recordAudioNodes.length).toBe(1) // Take a screenshot of the canvas with the RecordAudio node diff --git a/browser_tests/tests/releaseNotifications.spec.ts b/browser_tests/tests/releaseNotifications.spec.ts index a45527de7..74ae32c38 100644 --- a/browser_tests/tests/releaseNotifications.spec.ts +++ b/browser_tests/tests/releaseNotifications.spec.ts @@ -1,10 +1,11 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +import { TestIds } from '../fixtures/selectors' test.describe('Release Notifications', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') }) test('should show help center with release information', async ({ @@ -50,7 +51,9 @@ test.describe('Release Notifications', () => { await expect(helpMenu).toBeVisible() // Verify "What's New?" section shows the release - const whatsNewSection = comfyPage.page.getByTestId('whats-new-section') + const whatsNewSection = comfyPage.page.getByTestId( + TestIds.dialogs.whatsNewSection + ) await expect(whatsNewSection).toBeVisible() // Should show the release version @@ -79,7 +82,9 @@ test.describe('Release Notifications', () => { await expect(helpMenu).toBeVisible() // Verify "What's New?" section shows no releases - const whatsNewSection = comfyPage.page.getByTestId('whats-new-section') + const whatsNewSection = comfyPage.page.getByTestId( + TestIds.dialogs.whatsNewSection + ) await expect(whatsNewSection).toBeVisible() // Should show "No recent releases" message @@ -125,7 +130,9 @@ test.describe('Release Notifications', () => { await expect(helpMenu).toBeVisible() // Should show no releases due to error - const whatsNewSection = comfyPage.page.getByTestId('whats-new-section') + const whatsNewSection = comfyPage.page.getByTestId( + TestIds.dialogs.whatsNewSection + ) await expect( whatsNewSection.locator('text=No recent releases') ).toBeVisible() @@ -135,7 +142,10 @@ test.describe('Release Notifications', () => { comfyPage }) => { // Disable version update notifications - await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', false) + await comfyPage.settings.setSetting( + 'Comfy.Notification.ShowVersionUpdates', + false + ) // Mock release API with test data await comfyPage.page.route('**/releases**', async (route) => { @@ -175,7 +185,9 @@ test.describe('Release Notifications', () => { await expect(helpMenu).toBeVisible() // Verify "What's New?" section is hidden - const whatsNewSection = comfyPage.page.getByTestId('whats-new-section') + const whatsNewSection = comfyPage.page.getByTestId( + TestIds.dialogs.whatsNewSection + ) await expect(whatsNewSection).not.toBeVisible() // Should not show any popups or toasts @@ -189,7 +201,10 @@ test.describe('Release Notifications', () => { comfyPage }) => { // Disable version update notifications - await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', false) + await comfyPage.settings.setSetting( + 'Comfy.Notification.ShowVersionUpdates', + false + ) // Track API calls let apiCallCount = 0 @@ -220,7 +235,10 @@ test.describe('Release Notifications', () => { comfyPage }) => { // Enable version update notifications (default behavior) - await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', true) + await comfyPage.settings.setSetting( + 'Comfy.Notification.ShowVersionUpdates', + true + ) // Mock release API with test data await comfyPage.page.route('**/releases**', async (route) => { @@ -260,7 +278,9 @@ test.describe('Release Notifications', () => { await expect(helpMenu).toBeVisible() // Verify "What's New?" section is visible - const whatsNewSection = comfyPage.page.getByTestId('whats-new-section') + const whatsNewSection = comfyPage.page.getByTestId( + TestIds.dialogs.whatsNewSection + ) await expect(whatsNewSection).toBeVisible() // Should show the release @@ -299,7 +319,10 @@ test.describe('Release Notifications', () => { }) // Start with notifications enabled - await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', true) + await comfyPage.settings.setSetting( + 'Comfy.Notification.ShowVersionUpdates', + true + ) await comfyPage.setup({ mockReleases: false }) // Open help center @@ -308,14 +331,19 @@ test.describe('Release Notifications', () => { await helpCenterButton.click() // Verify "What's New?" section is visible - const whatsNewSection = comfyPage.page.getByTestId('whats-new-section') + const whatsNewSection = comfyPage.page.getByTestId( + TestIds.dialogs.whatsNewSection + ) await expect(whatsNewSection).toBeVisible() // Close help center await comfyPage.page.click('.help-center-backdrop') // Disable notifications - await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', false) + await comfyPage.settings.setSetting( + 'Comfy.Notification.ShowVersionUpdates', + false + ) // Reopen help center await helpCenterButton.click() @@ -328,7 +356,10 @@ test.describe('Release Notifications', () => { comfyPage }) => { // Disable notifications - await comfyPage.setSetting('Comfy.Notification.ShowVersionUpdates', false) + await comfyPage.settings.setSetting( + 'Comfy.Notification.ShowVersionUpdates', + false + ) // Mock empty releases await comfyPage.page.route('**/releases**', async (route) => { @@ -359,7 +390,9 @@ test.describe('Release Notifications', () => { await expect(helpMenu).toBeVisible() // Section should be hidden regardless of empty releases - const whatsNewSection = comfyPage.page.getByTestId('whats-new-section') + const whatsNewSection = comfyPage.page.getByTestId( + TestIds.dialogs.whatsNewSection + ) await expect(whatsNewSection).not.toBeVisible() }) }) diff --git a/browser_tests/tests/remoteWidgets.spec.ts b/browser_tests/tests/remoteWidgets.spec.ts index cc7cad64f..b09421bc6 100644 --- a/browser_tests/tests/remoteWidgets.spec.ts +++ b/browser_tests/tests/remoteWidgets.spec.ts @@ -26,23 +26,23 @@ test.describe('Remote COMBO Widget', { tag: '@widget' }, () => { nodeName: string ): Promise => { return await comfyPage.page.evaluate((name) => { - const node = window['app'].graph.nodes.find((node) => node.title === name) - return node.widgets[0].options.values + const node = window.app!.graph!.nodes.find((node) => node.title === name) + return node!.widgets![0].options.values as string[] | undefined }, nodeName) } const getWidgetValue = async (comfyPage: ComfyPage, nodeName: string) => { return await comfyPage.page.evaluate((name) => { - const node = window['app'].graph.nodes.find((node) => node.title === name) - return node.widgets[0].value + const node = window.app!.graph!.nodes.find((node) => node.title === name) + return node!.widgets![0].value }, nodeName) } const clickRefreshButton = (comfyPage: ComfyPage, nodeName: string) => { return comfyPage.page.evaluate((name) => { - const node = window['app'].graph.nodes.find((node) => node.title === name) - const buttonWidget = node.widgets.find((w) => w.name === 'refresh') - return buttonWidget?.callback() + const node = window.app!.graph!.nodes.find((node) => node.title === name) + const buttonWidget = node!.widgets!.find((w) => w.name === 'refresh') + return buttonWidget?.callback?.(buttonWidget.value) }, nodeName) } @@ -52,12 +52,12 @@ test.describe('Remote COMBO Widget', { tag: '@widget' }, () => { } test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') }) test.describe('Loading options', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') await comfyPage.page.route( '**/api/models/checkpoints**', async (route, request) => { @@ -89,10 +89,10 @@ test.describe('Remote COMBO Widget', { tag: '@widget' }, () => { comfyPage }) => { const nodeName = 'Remote Widget Node' - await comfyPage.loadWorkflow('inputs/remote_widget') + await comfyPage.workflow.loadWorkflow('inputs/remote_widget') const node = await comfyPage.page.evaluate((name) => { - return window['app'].graph.nodes.find((node) => node.title === name) + return window.app!.graph!.nodes.find((node) => node.title === name) }, nodeName) expect(node).toBeDefined() @@ -176,7 +176,7 @@ test.describe('Remote COMBO Widget', { tag: '@widget' }, () => { test('refresh button is visible in selection toolbar when node is selected', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true) + await comfyPage.settings.setSetting('Comfy.Canvas.SelectionToolbox', true) const nodeName = 'Remote Widget Node' await addRemoteWidgetNode(comfyPage, nodeName) @@ -196,7 +196,7 @@ test.describe('Remote COMBO Widget', { tag: '@widget' }, () => { // Fulfill each request with a unique timestamp await comfyPage.page.route( '**/api/models/checkpoints**', - async (route, request) => { + async (route, _request) => { await route.fulfill({ body: JSON.stringify([Date.now()]), status: 200 @@ -257,13 +257,11 @@ test.describe('Remote COMBO Widget', { tag: '@widget' }, () => { await addRemoteWidgetNode(comfyPage, nodeName) await waitForWidgetUpdate(comfyPage) - // Wait for timeout and backoff, then force re-render, repeat - const requestTimeout = 512 - await comfyPage.page.waitForTimeout(requestTimeout) - await waitForWidgetUpdate(comfyPage) - await comfyPage.page.waitForTimeout(requestTimeout * 2) - await waitForWidgetUpdate(comfyPage) - await comfyPage.page.waitForTimeout(requestTimeout * 3) + // Wait for exponential backoff retries to accumulate timestamps + await expect(async () => { + await waitForWidgetUpdate(comfyPage) + expect(timestamps.length).toBeGreaterThanOrEqual(3) + }).toPass({ timeout: 10000, intervals: [500, 1000, 1500] }) // Verify exponential backoff between retries const intervals = timestamps.slice(1).map((t, i) => t - timestamps[i]) diff --git a/browser_tests/tests/rerouteNode.spec.ts b/browser_tests/tests/rerouteNode.spec.ts index 48f155419..93f92d1d5 100644 --- a/browser_tests/tests/rerouteNode.spec.ts +++ b/browser_tests/tests/rerouteNode.spec.ts @@ -5,16 +5,16 @@ import { getMiddlePoint } from '../fixtures/utils/litegraphUtils' test.describe('Reroute Node', { tag: ['@screenshot', '@node'] }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') }) test.afterEach(async ({ comfyPage }) => { - await comfyPage.setupWorkflowsDirectory({}) + await comfyPage.workflow.setupWorkflowsDirectory({}) }) test('loads from inserted workflow', async ({ comfyPage }) => { const workflowName = 'single_connected_reroute_node.json' - await comfyPage.setupWorkflowsDirectory({ + await comfyPage.workflow.setupWorkflowsDirectory({ [workflowName]: 'links/single_connected_reroute_node.json' }) await comfyPage.setup() @@ -43,12 +43,12 @@ test.describe( { tag: ['@screenshot', '@node'] }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') - await comfyPage.setSetting('LiteGraph.Reroute.SplineOffset', 80) + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('LiteGraph.Reroute.SplineOffset', 80) }) test('loads from workflow', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('reroute/native_reroute') + await comfyPage.workflow.loadWorkflow('reroute/native_reroute') await expect(comfyPage.canvas).toHaveScreenshot('native_reroute.png') }) @@ -56,10 +56,10 @@ test.describe( comfyPage }) => { const loadCheckpointNode = ( - await comfyPage.getNodeRefsByTitle('Load Checkpoint') + await comfyPage.nodeOps.getNodeRefsByTitle('Load Checkpoint') )[0] const clipEncodeNode = ( - await comfyPage.getNodeRefsByTitle('CLIP Text Encode (Prompt)') + await comfyPage.nodeOps.getNodeRefsByTitle('CLIP Text Encode (Prompt)') )[0] const slot1 = await loadCheckpointNode.getOutput(1) @@ -82,10 +82,10 @@ test.describe( comfyPage }) => { const loadCheckpointNode = ( - await comfyPage.getNodeRefsByTitle('Load Checkpoint') + await comfyPage.nodeOps.getNodeRefsByTitle('Load Checkpoint') )[0] const clipEncodeNode = ( - await comfyPage.getNodeRefsByTitle('CLIP Text Encode (Prompt)') + await comfyPage.nodeOps.getNodeRefsByTitle('CLIP Text Encode (Prompt)') )[0] const slot1 = await loadCheckpointNode.getOutput(1) @@ -109,7 +109,7 @@ test.describe( comfyPage }) => { // https://github.com/Comfy-Org/ComfyUI_frontend/issues/4695 - await comfyPage.loadWorkflow( + await comfyPage.workflow.loadWorkflow( 'reroute/single-native-reroute-default-workflow' ) diff --git a/browser_tests/tests/rightClickMenu.spec.ts b/browser_tests/tests/rightClickMenu.spec.ts index 3114a9456..6ddc9942e 100644 --- a/browser_tests/tests/rightClickMenu.spec.ts +++ b/browser_tests/tests/rightClickMenu.spec.ts @@ -2,9 +2,10 @@ import { expect } from '@playwright/test' import { NodeBadgeMode } from '../../src/types/nodeSource' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +import { DefaultGraphPositions } from '../fixtures/constants/defaultGraphPositions' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe( @@ -12,7 +13,7 @@ test.describe( { tag: ['@screenshot', '@ui'] }, () => { test('Can add node', async ({ comfyPage }) => { - await comfyPage.rightClickCanvas() + await comfyPage.canvasOps.rightClick() await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png') await comfyPage.page.getByText('Add Node').click() await comfyPage.nextFrame() @@ -24,7 +25,7 @@ test.describe( }) test('Can add group', async ({ comfyPage }) => { - await comfyPage.rightClickCanvas() + await comfyPage.canvasOps.rightClick() await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png') await comfyPage.page.getByText('Add Group', { exact: true }).click() await comfyPage.nextFrame() @@ -34,13 +35,16 @@ test.describe( }) test('Can convert to group node', async ({ comfyPage }) => { - await comfyPage.select2Nodes() + await comfyPage.nodeOps.selectNodes(['CLIP Text Encode (Prompt)']) await expect(comfyPage.canvas).toHaveScreenshot('selected-2-nodes.png') - await comfyPage.rightClickCanvas() - await comfyPage.clickContextMenuItem('Convert to Group Node (Deprecated)') - await comfyPage.promptDialogInput.fill('GroupNode2CLIP') + await comfyPage.canvasOps.rightClick() + await comfyPage.contextMenu.clickMenuItem( + 'Convert to Group Node (Deprecated)' + ) + await comfyPage.nextFrame() + await comfyPage.nodeOps.promptDialogInput.fill('GroupNode2CLIP') await comfyPage.page.keyboard.press('Enter') - await comfyPage.promptDialogInput.waitFor({ state: 'hidden' }) + await comfyPage.nodeOps.promptDialogInput.waitFor({ state: 'hidden' }) await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot( 'right-click-node-group-node.png' @@ -51,7 +55,12 @@ test.describe( test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => { test('Can open properties panel', async ({ comfyPage }) => { - await comfyPage.rightClickEmptyLatentNode() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.emptyLatentWidgetClick, + button: 'right' + }) + await comfyPage.page.mouse.move(10, 10) + await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png') await comfyPage.page.getByText('Properties Panel').click() await comfyPage.nextFrame() @@ -61,7 +70,12 @@ test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => { }) test('Can collapse', async ({ comfyPage }) => { - await comfyPage.rightClickEmptyLatentNode() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.emptyLatentWidgetClick, + button: 'right' + }) + await comfyPage.page.mouse.move(10, 10) + await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png') await comfyPage.page.getByText('Collapse').click() await comfyPage.nextFrame() @@ -71,16 +85,21 @@ test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => { }) test('Can collapse (Node Badge)', async ({ comfyPage }) => { - await comfyPage.setSetting( + await comfyPage.settings.setSetting( 'Comfy.NodeBadge.NodeIdBadgeMode', NodeBadgeMode.ShowAll ) - await comfyPage.setSetting( + await comfyPage.settings.setSetting( 'Comfy.NodeBadge.NodeSourceBadgeMode', NodeBadgeMode.ShowAll ) - await comfyPage.rightClickEmptyLatentNode() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.emptyLatentWidgetClick, + button: 'right' + }) + await comfyPage.page.mouse.move(10, 10) + await comfyPage.nextFrame() await comfyPage.page.getByText('Collapse').click() await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot( @@ -89,7 +108,12 @@ test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => { }) test('Can bypass', async ({ comfyPage }) => { - await comfyPage.rightClickEmptyLatentNode() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.emptyLatentWidgetClick, + button: 'right' + }) + await comfyPage.page.mouse.move(10, 10) + await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png') await comfyPage.page.getByText('Bypass').click() await comfyPage.nextFrame() @@ -99,46 +123,89 @@ test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => { }) test('Can pin and unpin', async ({ comfyPage }) => { - await comfyPage.rightClickEmptyLatentNode() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.emptyLatentWidgetClick, + button: 'right' + }) + await comfyPage.page.mouse.move(10, 10) + await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png') await comfyPage.page.click('.litemenu-entry:has-text("Pin")') await comfyPage.nextFrame() - await comfyPage.dragAndDrop({ x: 621, y: 617 }, { x: 16, y: 16 }) + + // Get EmptyLatentImage node title position dynamically (for dragging) + const emptyLatentNode = await comfyPage.nodeOps.getNodeRefById(5) + const titlePos = await emptyLatentNode.getTitlePosition() + await comfyPage.canvasOps.dragAndDrop(titlePos, { x: 16, y: 16 }) await expect(comfyPage.canvas).toHaveScreenshot('node-pinned.png') - await comfyPage.rightClickEmptyLatentNode() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.emptyLatentWidgetClick, + button: 'right' + }) + await comfyPage.page.mouse.move(10, 10) + await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot( 'right-click-pinned-node.png' ) await comfyPage.page.click('.litemenu-entry:has-text("Unpin")') await comfyPage.nextFrame() - await comfyPage.rightClickEmptyLatentNode() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.emptyLatentWidgetClick, + button: 'right' + }) + await comfyPage.page.mouse.move(10, 10) + await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot( 'right-click-unpinned-node.png' ) }) test('Can move after unpin', async ({ comfyPage }) => { - await comfyPage.rightClickEmptyLatentNode() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.emptyLatentWidgetClick, + button: 'right' + }) + await comfyPage.page.mouse.move(10, 10) + await comfyPage.nextFrame() await comfyPage.page.click('.litemenu-entry:has-text("Pin")') await comfyPage.nextFrame() - await comfyPage.rightClickEmptyLatentNode() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.emptyLatentWidgetClick, + button: 'right' + }) + await comfyPage.page.mouse.move(10, 10) + await comfyPage.nextFrame() await comfyPage.page.click('.litemenu-entry:has-text("Unpin")') await comfyPage.nextFrame() - await comfyPage.dragAndDrop({ x: 496, y: 618 }, { x: 200, y: 590 }) + + // Get EmptyLatentImage node title position dynamically (for dragging) + const emptyLatentNode = await comfyPage.nodeOps.getNodeRefById(5) + const titlePos = await emptyLatentNode.getTitlePosition() + await comfyPage.canvasOps.dragAndDrop(titlePos, { x: 200, y: 590 }) await expect(comfyPage.canvas).toHaveScreenshot( 'right-click-unpinned-node-moved.png' ) }) test('Can pin/unpin selected nodes', async ({ comfyPage }) => { - await comfyPage.select2Nodes() + await comfyPage.nodeOps.selectNodes(['CLIP Text Encode (Prompt)']) await comfyPage.page.keyboard.down('Control') - await comfyPage.rightClickEmptyLatentNode() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.emptyLatentWidgetClick, + button: 'right' + }) + await comfyPage.page.mouse.move(10, 10) + await comfyPage.nextFrame() await comfyPage.page.click('.litemenu-entry:has-text("Pin")') await comfyPage.page.keyboard.up('Control') await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot('selected-nodes-pinned.png') - await comfyPage.rightClickEmptyLatentNode() + await comfyPage.canvas.click({ + position: DefaultGraphPositions.emptyLatentWidgetClick, + button: 'right' + }) + await comfyPage.page.mouse.move(10, 10) + await comfyPage.nextFrame() await comfyPage.page.click('.litemenu-entry:has-text("Unpin")') await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot( @@ -147,8 +214,8 @@ test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => { }) test('Can clone pinned nodes', async ({ comfyPage }) => { - const nodeCount = await comfyPage.getGraphNodesCount() - const node = (await comfyPage.getFirstNodeRef())! + const nodeCount = await comfyPage.nodeOps.getGraphNodesCount() + const node = (await comfyPage.nodeOps.getFirstNodeRef())! await node.clickContextMenuOption('Pin') await comfyPage.nextFrame() await node.click('title', { button: 'right' }) @@ -161,6 +228,6 @@ test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => { await cloneItem.click() await expect(cloneItem).toHaveCount(0) await comfyPage.nextFrame() - expect(await comfyPage.getGraphNodesCount()).toBe(nodeCount + 1) + expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe(nodeCount + 1) }) }) diff --git a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-unpinned-node-moved-chromium-linux.png b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-unpinned-node-moved-chromium-linux.png index 1f3146378..0701d1d4c 100644 Binary files a/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-unpinned-node-moved-chromium-linux.png and b/browser_tests/tests/rightClickMenu.spec.ts-snapshots/right-click-unpinned-node-moved-chromium-linux.png differ diff --git a/browser_tests/tests/selectionToolbox.spec.ts b/browser_tests/tests/selectionToolbox.spec.ts index fd74a4aa7..fb0966d80 100644 --- a/browser_tests/tests/selectionToolbox.spec.ts +++ b/browser_tests/tests/selectionToolbox.spec.ts @@ -5,14 +5,14 @@ import { comfyPageFixture } from '../fixtures/ComfyPage' const test = comfyPageFixture test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) const BLUE_COLOR = 'rgb(51, 51, 85)' const RED_COLOR = 'rgb(85, 51, 51)' test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true) + await comfyPage.settings.setSetting('Comfy.Canvas.SelectionToolbox', true) }) test('shows selection toolbox', async ({ comfyPage }) => { @@ -20,7 +20,10 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => { await expect(comfyPage.selectionToolbox).not.toBeVisible() // Select multiple nodes - await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)']) + await comfyPage.nodeOps.selectNodes([ + 'KSampler', + 'CLIP Text Encode (Prompt)' + ]) // Selection toolbox should be visible with multiple nodes selected await expect(comfyPage.selectionToolbox).toBeVisible() @@ -33,11 +36,11 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => { test('shows at correct position when node is pasted', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('nodes/single_ksampler') - await comfyPage.selectNodes(['KSampler']) - await comfyPage.ctrlC() + await comfyPage.workflow.loadWorkflow('nodes/single_ksampler') + await comfyPage.nodeOps.selectNodes(['KSampler']) + await comfyPage.clipboard.copy() await comfyPage.page.mouse.move(100, 100) - await comfyPage.ctrlV() + await comfyPage.clipboard.paste() const toolboxContainer = comfyPage.selectionToolbox await expect(toolboxContainer).toBeVisible() @@ -54,8 +57,8 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => { test('hide when select and drag happen at the same time', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('nodes/single_ksampler') - const node = (await comfyPage.getNodeRefsByTitle('KSampler'))[0] + await comfyPage.workflow.loadWorkflow('nodes/single_ksampler') + const node = (await comfyPage.nodeOps.getNodeRefsByTitle('KSampler'))[0] const nodePos = await node.getPosition() // Drag on the title of the node @@ -68,7 +71,7 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => { test('shows border only with multiple selections', async ({ comfyPage }) => { // Select single node - await comfyPage.selectNodes(['KSampler']) + await comfyPage.nodeOps.selectNodes(['KSampler']) // Selection toolbox should be visible but without border await expect(comfyPage.selectionToolbox).toBeVisible() @@ -78,7 +81,10 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => { ) // Select multiple nodes - await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)']) + await comfyPage.nodeOps.selectNodes([ + 'KSampler', + 'CLIP Text Encode (Prompt)' + ]) // Selection border should show with multiple selections (canvas-based) await expect(comfyPage.canvas).toHaveScreenshot( @@ -86,7 +92,7 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => { ) // Deselect to single node - await comfyPage.selectNodes(['CLIP Text Encode (Prompt)']) + await comfyPage.nodeOps.selectNodes(['CLIP Text Encode (Prompt)']) // Border should be hidden again (canvas-based) await expect(comfyPage.canvas).toHaveScreenshot( @@ -98,7 +104,7 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => { comfyPage }) => { // A group + a KSampler node - await comfyPage.loadWorkflow('groups/single_group') + await comfyPage.workflow.loadWorkflow('groups/single_group') // Select group + node should show bypass button await comfyPage.page.focus('canvas') @@ -110,7 +116,7 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => { ).toBeVisible() // Deselect node (Only group is selected) should hide bypass button - await comfyPage.selectNodes(['KSampler']) + await comfyPage.nodeOps.selectNodes(['KSampler']) await expect( comfyPage.page.locator( '.selection-toolbox *[data-testid="bypass-button"]' @@ -123,7 +129,7 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => { comfyPage }) => { // Select a node - await comfyPage.selectNodes(['KSampler']) + await comfyPage.nodeOps.selectNodes(['KSampler']) // Color picker button should be visible const colorPickerButton = comfyPage.page.locator( @@ -151,7 +157,9 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => { // Node should have the selected color class/style // Note: Exact verification method depends on how color is applied to nodes - const selectedNode = (await comfyPage.getNodeRefsByTitle('KSampler'))[0] + const selectedNode = ( + await comfyPage.nodeOps.getNodeRefsByTitle('KSampler') + )[0] expect(await selectedNode.getProperty('color')).not.toBeNull() }) @@ -159,7 +167,10 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => { comfyPage }) => { // Select multiple nodes - await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)']) + await comfyPage.nodeOps.selectNodes([ + 'KSampler', + 'CLIP Text Encode (Prompt)' + ]) const colorPickerButton = comfyPage.page.locator( '.selection-toolbox .pi-circle-fill' @@ -183,22 +194,25 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => { comfyPage }) => { // Select first node and color it - await comfyPage.selectNodes(['KSampler']) + await comfyPage.nodeOps.selectNodes(['KSampler']) await comfyPage.page.locator('.selection-toolbox .pi-circle-fill').click() await comfyPage.page .locator('.color-picker-container i[data-testid="blue"]') .click() - await comfyPage.selectNodes(['KSampler']) + await comfyPage.nodeOps.selectNodes(['KSampler']) // Select second node and color it differently - await comfyPage.selectNodes(['CLIP Text Encode (Prompt)']) + await comfyPage.nodeOps.selectNodes(['CLIP Text Encode (Prompt)']) await comfyPage.page.locator('.selection-toolbox .pi-circle-fill').click() await comfyPage.page .locator('.color-picker-container i[data-testid="red"]') .click() // Select both nodes - await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)']) + await comfyPage.nodeOps.selectNodes([ + 'KSampler', + 'CLIP Text Encode (Prompt)' + ]) // Color picker should show null/mixed state const colorPickerButton = comfyPage.page.locator( @@ -211,17 +225,17 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => { comfyPage }) => { // First color a node - await comfyPage.selectNodes(['KSampler']) + await comfyPage.nodeOps.selectNodes(['KSampler']) await comfyPage.page.locator('.selection-toolbox .pi-circle-fill').click() await comfyPage.page .locator('.color-picker-container i[data-testid="blue"]') .click() // Clear selection - await comfyPage.selectNodes(['KSampler']) + await comfyPage.nodeOps.selectNodes(['KSampler']) // Re-select the node - await comfyPage.selectNodes(['KSampler']) + await comfyPage.nodeOps.selectNodes(['KSampler']) // Color picker button should show the correct color const colorPickerButton = comfyPage.page.locator( @@ -234,7 +248,7 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => { comfyPage }) => { // Select a node and color it - await comfyPage.selectNodes(['KSampler']) + await comfyPage.nodeOps.selectNodes(['KSampler']) await comfyPage.page.locator('.selection-toolbox .pi-circle-fill').click() await comfyPage.page .locator('.color-picker-container i[data-testid="blue"]') @@ -245,7 +259,9 @@ test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => { await comfyPage.nextFrame() // Node should be uncolored again - const selectedNode = (await comfyPage.getNodeRefsByTitle('KSampler'))[0] + const selectedNode = ( + await comfyPage.nodeOps.getNodeRefsByTitle('KSampler') + )[0] expect(await selectedNode.getProperty('color')).toBeUndefined() }) }) diff --git a/browser_tests/tests/selectionToolboxSubmenus.spec.ts b/browser_tests/tests/selectionToolboxSubmenus.spec.ts index c5d74c167..c97d5d5b3 100644 --- a/browser_tests/tests/selectionToolboxSubmenus.spec.ts +++ b/browser_tests/tests/selectionToolboxSubmenus.spec.ts @@ -4,7 +4,7 @@ import { comfyPageFixture as test } from '../fixtures/ComfyPage' import type { ComfyPage } from '../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe( @@ -12,15 +12,16 @@ test.describe( { tag: '@ui' }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true) - await comfyPage.loadWorkflow('nodes/single_ksampler') + await comfyPage.settings.setSetting('Comfy.Canvas.SelectionToolbox', true) + await comfyPage.workflow.loadWorkflow('nodes/single_ksampler') await comfyPage.nextFrame() - await comfyPage.selectNodes(['KSampler']) + await comfyPage.nodeOps.selectNodes(['KSampler']) await comfyPage.nextFrame() }) const openMoreOptions = async (comfyPage: ComfyPage) => { - const ksamplerNodes = await comfyPage.getNodeRefsByTitle('KSampler') + const ksamplerNodes = + await comfyPage.nodeOps.getNodeRefsByTitle('KSampler') if (ksamplerNodes.length === 0) { throw new Error('No KSampler nodes found') } @@ -28,9 +29,14 @@ test.describe( // Drag the KSampler to the center of the screen const nodePos = await ksamplerNodes[0].getPosition() const viewportSize = comfyPage.page.viewportSize() + if (!viewportSize) { + throw new Error( + 'Viewport size is null - page may not be properly initialized' + ) + } const centerX = viewportSize.width / 3 const centerY = viewportSize.height / 2 - await comfyPage.dragAndDrop( + await comfyPage.canvasOps.dragAndDrop( { x: nodePos.x, y: nodePos.y }, { x: centerX, y: centerY } ) @@ -85,7 +91,9 @@ test.describe( }) test('changes node shape via Shape submenu', async ({ comfyPage }) => { - const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0] + const nodeRef = ( + await comfyPage.nodeOps.getNodeRefsByTitle('KSampler') + )[0] const initialShape = await nodeRef.getProperty('shape') await openMoreOptions(comfyPage) @@ -106,7 +114,9 @@ test.describe( test('changes node color via Color submenu swatch', async ({ comfyPage }) => { - const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0] + const nodeRef = ( + await comfyPage.nodeOps.getNodeRefsByTitle('KSampler') + )[0] const initialColor = await nodeRef.getProperty( 'color' ) @@ -126,7 +136,9 @@ test.describe( }) test('renames a node using Rename action', async ({ comfyPage }) => { - const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0] + const nodeRef = ( + await comfyPage.nodeOps.getNodeRefsByTitle('KSampler') + )[0] await openMoreOptions(comfyPage) await comfyPage.page .getByText('Rename', { exact: true }) diff --git a/browser_tests/tests/sidebar/nodeLibrary.spec.ts b/browser_tests/tests/sidebar/nodeLibrary.spec.ts index 4f476376a..6f18bcdee 100644 --- a/browser_tests/tests/sidebar/nodeLibrary.spec.ts +++ b/browser_tests/tests/sidebar/nodeLibrary.spec.ts @@ -4,9 +4,12 @@ import { comfyPageFixture as test } from '../../fixtures/ComfyPage' test.describe('Node library sidebar', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', []) - await comfyPage.setSetting('Comfy.NodeLibrary.BookmarksCustomization', {}) + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', []) + await comfyPage.settings.setSetting( + 'Comfy.NodeLibrary.BookmarksCustomization', + {} + ) // Open the sidebar const tab = comfyPage.menu.nodeLibraryTab await tab.open() @@ -26,7 +29,7 @@ test.describe('Node library sidebar', () => { ) expect(previewVisible).toBe(true) - const count = await comfyPage.getGraphNodesCount() + const count = await comfyPage.nodeOps.getGraphNodesCount() // Drag the node onto the canvas const canvasSelector = '#graph-canvas' @@ -46,7 +49,7 @@ test.describe('Node library sidebar', () => { }) // Verify the node is added to the canvas - expect(await comfyPage.getGraphNodesCount()).toBe(count + 1) + expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe(count + 1) }) test('Bookmark node', async ({ comfyPage }) => { @@ -58,7 +61,7 @@ test.describe('Node library sidebar', () => { // Verify the bookmark is added to the bookmarks tab expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2') ).toEqual(['KSamplerAdvanced']) // Verify the bookmark node with the same name is added to the tree. expect(await tab.getNode('KSampler (Advanced)').count()).toBe(2) @@ -69,7 +72,9 @@ test.describe('Node library sidebar', () => { }) test('Ignores unrecognized node', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo']) + await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ + 'foo' + ]) const tab = comfyPage.menu.nodeLibraryTab expect(await tab.getFolder('sampling').count()).toBe(1) @@ -77,7 +82,9 @@ test.describe('Node library sidebar', () => { }) test('Displays empty bookmarks folder', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) + await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ + 'foo/' + ]) const tab = comfyPage.menu.nodeLibraryTab expect(await tab.getFolder('foo').count()).toBe(1) }) @@ -91,12 +98,14 @@ test.describe('Node library sidebar', () => { await textInput.press('Enter') expect(await tab.getFolder('New Folder').count()).toBe(1) expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2') ).toEqual(['New Folder/']) }) test('Can add nested bookmark folder', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) + await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ + 'foo/' + ]) const tab = comfyPage.menu.nodeLibraryTab await tab.getFolder('foo').click({ button: 'right' }) @@ -108,24 +117,28 @@ test.describe('Node library sidebar', () => { expect(await tab.getFolder('bar').count()).toBe(1) expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2') ).toEqual(['foo/', 'foo/bar/']) }) test('Can delete bookmark folder', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) + await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ + 'foo/' + ]) const tab = comfyPage.menu.nodeLibraryTab await tab.getFolder('foo').click({ button: 'right' }) await comfyPage.page.getByLabel('Delete').click() expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2') ).toEqual([]) }) test('Can rename bookmark folder', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) + await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ + 'foo/' + ]) const tab = comfyPage.menu.nodeLibraryTab await tab.getFolder('foo').click({ button: 'right' }) @@ -136,14 +149,16 @@ test.describe('Node library sidebar', () => { await comfyPage.page.keyboard.press('Enter') expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2') ).toEqual(['bar/']) }) test('Can add bookmark by dragging node to bookmark folder', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) + await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ + 'foo/' + ]) const tab = comfyPage.menu.nodeLibraryTab await tab.getFolder('sampling').click() await comfyPage.page.dragAndDrop( @@ -151,7 +166,7 @@ test.describe('Node library sidebar', () => { tab.folderSelector('foo') ) expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2') ).toEqual(['foo/', 'foo/KSamplerAdvanced']) }) @@ -162,51 +177,60 @@ test.describe('Node library sidebar', () => { await tab.getFolder('sampling').click() await tab.getNode('KSampler (Advanced)').locator('.bookmark-button').click() expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2') ).toEqual(['KSamplerAdvanced']) }) test('Can unbookmark node (Top level bookmark)', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ + await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ 'KSamplerAdvanced' ]) const tab = comfyPage.menu.nodeLibraryTab await tab.getNode('KSampler (Advanced)').locator('.bookmark-button').click() expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2') ).toEqual([]) }) test('Can unbookmark node (Library node bookmark)', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ + await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ 'KSamplerAdvanced' ]) const tab = comfyPage.menu.nodeLibraryTab await tab.getFolder('sampling').click() - await comfyPage.page - .locator(tab.nodeSelector('KSampler (Advanced)')) - .nth(1) + await tab + .getNodeInFolder('KSampler (Advanced)', 'sampling') .locator('.bookmark-button') .click() expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2') ).toEqual([]) }) test('Can customize icon', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) + await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ + 'foo/' + ]) const tab = comfyPage.menu.nodeLibraryTab await tab.getFolder('foo').click({ button: 'right' }) await comfyPage.page.getByLabel('Customize').click() - await comfyPage.page - .locator('.icon-field .p-selectbutton > *:nth-child(2)') - .click() - await comfyPage.page - .locator('.color-field .p-selectbutton > *:nth-child(2)') - .click() - await comfyPage.page.getByRole('button', { name: 'Confirm' }).click() + const dialog = comfyPage.page.getByRole('dialog', { + name: 'Customize Folder' + }) + // Select Folder icon (2nd button in Icon group) + const iconGroup = dialog.getByText('Icon').locator('..').getByRole('group') + await iconGroup.getByRole('button').nth(1).click() + // Select Blue color (2nd button in Color group) + const colorGroup = dialog + .getByText('Color') + .locator('..') + .getByRole('group') + await colorGroup.getByRole('button').nth(1).click() + await dialog.getByRole('button', { name: 'Confirm' }).click() await comfyPage.nextFrame() expect( - await comfyPage.getSetting('Comfy.NodeLibrary.BookmarksCustomization') + await comfyPage.settings.getSetting( + 'Comfy.NodeLibrary.BookmarksCustomization' + ) ).toEqual({ 'foo/': { icon: 'pi-folder', @@ -216,17 +240,24 @@ test.describe('Node library sidebar', () => { }) // If color is left as default, it should not be saved test('Can customize icon (default field)', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) + await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ + 'foo/' + ]) const tab = comfyPage.menu.nodeLibraryTab await tab.getFolder('foo').click({ button: 'right' }) await comfyPage.page.getByLabel('Customize').click() - await comfyPage.page - .locator('.icon-field .p-selectbutton > *:nth-child(2)') - .click() - await comfyPage.page.getByRole('button', { name: 'Confirm' }).click() + const dialog = comfyPage.page.getByRole('dialog', { + name: 'Customize Folder' + }) + // Select Folder icon (2nd button in Icon group) + const iconGroup = dialog.getByText('Icon').locator('..').getByRole('group') + await iconGroup.getByRole('button').nth(1).click() + await dialog.getByRole('button', { name: 'Confirm' }).click() await comfyPage.nextFrame() expect( - await comfyPage.getSetting('Comfy.NodeLibrary.BookmarksCustomization') + await comfyPage.settings.getSetting( + 'Comfy.NodeLibrary.BookmarksCustomization' + ) ).toEqual({ 'foo/': { icon: 'pi-folder' @@ -238,7 +269,9 @@ test.describe('Node library sidebar', () => { comfyPage }) => { // Open customization dialog - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) + await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ + 'foo/' + ]) const tab = comfyPage.menu.nodeLibraryTab await tab.getFolder('foo').click({ button: 'right' }) await comfyPage.page.getByLabel('Customize').click() @@ -258,16 +291,19 @@ test.describe('Node library sidebar', () => { await comfyPage.page.locator('.p-colorpicker-color-background').click() // Finalize the customization - await comfyPage.page - .locator('.icon-field .p-selectbutton > *:nth-child(2)') - .click() - await comfyPage.page.getByRole('button', { name: 'Confirm' }).click() + const dialog = comfyPage.page.getByRole('dialog', { + name: 'Customize Folder' + }) + // Select Folder icon (2nd button in Icon group) + const iconGroup = dialog.getByText('Icon').locator('..').getByRole('group') + await iconGroup.getByRole('button').nth(1).click() + await dialog.getByRole('button', { name: 'Confirm' }).click() await comfyPage.nextFrame() // Verify the color selection is saved - const setting = await comfyPage.getSetting( - 'Comfy.NodeLibrary.BookmarksCustomization' - ) + const setting = await comfyPage.settings.getSetting< + Record + >('Comfy.NodeLibrary.BookmarksCustomization') await expect(setting).toHaveProperty(['foo/', 'color']) await expect(setting['foo/'].color).not.toBeNull() await expect(setting['foo/'].color).not.toBeUndefined() @@ -275,13 +311,18 @@ test.describe('Node library sidebar', () => { }) test('Can rename customized bookmark folder', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) - await comfyPage.setSetting('Comfy.NodeLibrary.BookmarksCustomization', { - 'foo/': { - icon: 'pi-folder', - color: '#007bff' + await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ + 'foo/' + ]) + await comfyPage.settings.setSetting( + 'Comfy.NodeLibrary.BookmarksCustomization', + { + 'foo/': { + icon: 'pi-folder', + color: '#007bff' + } } - }) + ) const tab = comfyPage.menu.nodeLibraryTab await tab.getFolder('foo').click({ button: 'right' }) await comfyPage.page @@ -292,10 +333,12 @@ test.describe('Node library sidebar', () => { await comfyPage.nextFrame() await expect(async () => { expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2') ).toEqual(['bar/']) expect( - await comfyPage.getSetting('Comfy.NodeLibrary.BookmarksCustomization') + await comfyPage.settings.getSetting( + 'Comfy.NodeLibrary.BookmarksCustomization' + ) ).toEqual({ 'bar/': { icon: 'pi-folder', @@ -308,27 +351,34 @@ test.describe('Node library sidebar', () => { }) test('Can delete customized bookmark folder', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', ['foo/']) - await comfyPage.setSetting('Comfy.NodeLibrary.BookmarksCustomization', { - 'foo/': { - icon: 'pi-folder', - color: '#007bff' + await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ + 'foo/' + ]) + await comfyPage.settings.setSetting( + 'Comfy.NodeLibrary.BookmarksCustomization', + { + 'foo/': { + icon: 'pi-folder', + color: '#007bff' + } } - }) + ) const tab = comfyPage.menu.nodeLibraryTab await tab.getFolder('foo').click({ button: 'right' }) await comfyPage.page.getByLabel('Delete').click() await comfyPage.nextFrame() expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + await comfyPage.settings.getSetting('Comfy.NodeLibrary.Bookmarks.V2') ).toEqual([]) expect( - await comfyPage.getSetting('Comfy.NodeLibrary.BookmarksCustomization') + await comfyPage.settings.getSetting( + 'Comfy.NodeLibrary.BookmarksCustomization' + ) ).toEqual({}) }) test('Can filter nodes in both trees', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ + await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ 'foo/', 'foo/KSamplerAdvanced', 'KSampler' @@ -336,8 +386,6 @@ test.describe('Node library sidebar', () => { const tab = comfyPage.menu.nodeLibraryTab await tab.nodeLibrarySearchBoxInput.fill('KSampler') - // Node search box is debounced and may take some time to update. - await comfyPage.page.waitForTimeout(1000) - expect(await tab.getNode('KSampler (Advanced)').count()).toBe(2) + await expect(tab.getNode('KSampler (Advanced)')).toHaveCount(2) }) }) diff --git a/browser_tests/tests/sidebar/workflows.spec.ts b/browser_tests/tests/sidebar/workflows.spec.ts index 86f37b23d..7ff026e53 100644 --- a/browser_tests/tests/sidebar/workflows.spec.ts +++ b/browser_tests/tests/sidebar/workflows.spec.ts @@ -4,8 +4,11 @@ import { comfyPageFixture as test } from '../../fixtures/ComfyPage' test.describe('Workflows sidebar', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') - await comfyPage.setSetting('Comfy.Workflow.WorkflowTabsPosition', 'Sidebar') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting( + 'Comfy.Workflow.WorkflowTabsPosition', + 'Sidebar' + ) // Open the sidebar const tab = comfyPage.menu.workflowsTab @@ -13,7 +16,7 @@ test.describe('Workflows sidebar', () => { }) test.afterEach(async ({ comfyPage }) => { - await comfyPage.setupWorkflowsDirectory({}) + await comfyPage.workflow.setupWorkflowsDirectory({}) }) test('Can create new blank workflow', async ({ comfyPage }) => { @@ -22,7 +25,7 @@ test.describe('Workflows sidebar', () => { '*Unsaved Workflow.json' ]) - await comfyPage.executeCommand('Comfy.NewBlankWorkflow') + await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow') expect(await tab.getOpenedWorkflowNames()).toEqual([ '*Unsaved Workflow.json', '*Unsaved Workflow (2).json' @@ -30,7 +33,7 @@ test.describe('Workflows sidebar', () => { }) test('Can show top level saved workflows', async ({ comfyPage }) => { - await comfyPage.setupWorkflowsDirectory({ + await comfyPage.workflow.setupWorkflowsDirectory({ 'workflow1.json': 'default.json', 'workflow2.json': 'default.json' }) @@ -50,20 +53,20 @@ test.describe('Workflows sidebar', () => { expect.arrayContaining(['workflow1.json']) ) - await comfyPage.executeCommand('Comfy.DuplicateWorkflow') + await comfyPage.command.executeCommand('Comfy.DuplicateWorkflow') expect(await tab.getOpenedWorkflowNames()).toEqual([ 'workflow1.json', '*workflow1 (Copy).json' ]) - await comfyPage.executeCommand('Comfy.DuplicateWorkflow') + await comfyPage.command.executeCommand('Comfy.DuplicateWorkflow') expect(await tab.getOpenedWorkflowNames()).toEqual([ 'workflow1.json', '*workflow1 (Copy).json', '*workflow1 (Copy) (2).json' ]) - await comfyPage.executeCommand('Comfy.DuplicateWorkflow') + await comfyPage.command.executeCommand('Comfy.DuplicateWorkflow') expect(await tab.getOpenedWorkflowNames()).toEqual([ 'workflow1.json', '*workflow1 (Copy).json', @@ -73,28 +76,30 @@ test.describe('Workflows sidebar', () => { }) test('Can open workflow after insert', async ({ comfyPage }) => { - await comfyPage.setupWorkflowsDirectory({ + await comfyPage.workflow.setupWorkflowsDirectory({ 'workflow1.json': 'nodes/single_ksampler.json' }) const tab = comfyPage.menu.workflowsTab await tab.open() - await comfyPage.executeCommand('Comfy.LoadDefaultWorkflow') - const originalNodeCount = (await comfyPage.getNodes()).length + await comfyPage.command.executeCommand('Comfy.LoadDefaultWorkflow') + const originalNodeCount = (await comfyPage.nodeOps.getNodes()).length await tab.insertWorkflow(tab.getPersistedItem('workflow1.json')) - await comfyPage.nextFrame() - expect((await comfyPage.getNodes()).length).toEqual(originalNodeCount + 1) + await expect + .poll(() => comfyPage.nodeOps.getNodes().then((n) => n.length)) + .toEqual(originalNodeCount + 1) await tab.getPersistedItem('workflow1.json').click() - await comfyPage.nextFrame() - expect((await comfyPage.getNodes()).length).toEqual(1) + await expect + .poll(() => comfyPage.nodeOps.getNodes().then((n) => n.length)) + .toEqual(1) }) test('Can rename nested workflow from opened workflow item', async ({ comfyPage }) => { - await comfyPage.setupWorkflowsDirectory({ + await comfyPage.workflow.setupWorkflowsDirectory({ foo: { 'bar.json': 'default.json' } @@ -116,7 +121,7 @@ test.describe('Workflows sidebar', () => { }) test('Can save workflow as', async ({ comfyPage }) => { - await comfyPage.executeCommand('Comfy.NewBlankWorkflow') + await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow') await comfyPage.menu.topbar.saveWorkflowAs('workflow3.json') expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([ '*Unsaved Workflow.json', @@ -134,17 +139,17 @@ test.describe('Workflows sidebar', () => { test('Exported workflow does not contain localized slot names', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('default') - const exportedWorkflow = await comfyPage.getExportedWorkflow({ + await comfyPage.workflow.loadWorkflow('default') + const exportedWorkflow = await comfyPage.workflow.getExportedWorkflow({ api: false }) expect(exportedWorkflow).toBeDefined() for (const node of exportedWorkflow.nodes) { - for (const slot of node.inputs) { + for (const slot of node.inputs ?? []) { expect(slot.localized_name).toBeUndefined() expect(slot.label).toBeUndefined() } - for (const slot of node.outputs) { + for (const slot of node.outputs ?? []) { expect(slot.localized_name).toBeUndefined() expect(slot.label).toBeUndefined() } @@ -154,7 +159,7 @@ test.describe('Workflows sidebar', () => { test('Can export same workflow with different locales', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('default') + await comfyPage.workflow.loadWorkflow('default') // Setup download listener before triggering the export const downloadPromise = comfyPage.page.waitForEvent('download') @@ -165,14 +170,14 @@ test.describe('Workflows sidebar', () => { expect(download.suggestedFilename()).toBe('exported_default.json') // Get the exported workflow content - const downloadedContent = await comfyPage.getExportedWorkflow({ + const downloadedContent = await comfyPage.workflow.getExportedWorkflow({ api: false }) - await comfyPage.setSetting('Comfy.Locale', 'zh') + await comfyPage.settings.setSetting('Comfy.Locale', 'zh') await comfyPage.setup() - const downloadedContentZh = await comfyPage.getExportedWorkflow({ + const downloadedContentZh = await comfyPage.workflow.getExportedWorkflow({ api: false }) @@ -199,7 +204,7 @@ test.describe('Workflows sidebar', () => { test('Can save temporary workflow with unmodified name', async ({ comfyPage }) => { - expect(await comfyPage.isCurrentWorkflowModified()).toBe(false) + expect(await comfyPage.workflow.isCurrentWorkflowModified()).toBe(false) await comfyPage.menu.topbar.saveWorkflow('Unsaved Workflow') // Should not trigger the overwrite dialog @@ -207,7 +212,7 @@ test.describe('Workflows sidebar', () => { await comfyPage.page.locator('.comfy-modal-content:visible').count() ).toBe(0) - expect(await comfyPage.isCurrentWorkflowModified()).toBe(false) + expect(await comfyPage.workflow.isCurrentWorkflowModified()).toBe(false) }) test('Can overwrite other workflows with save as', async ({ comfyPage }) => { @@ -238,12 +243,16 @@ test.describe('Workflows sidebar', () => { test('Does not report warning when switching between opened workflows', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('missing/missing_nodes') - await comfyPage.closeDialog() + await comfyPage.workflow.loadWorkflow('missing/missing_nodes') + await comfyPage.page + .locator('.p-dialog') + .getByRole('button', { name: 'Close' }) + .click({ force: true }) + await comfyPage.page.locator('.p-dialog').waitFor({ state: 'hidden' }) // Load blank workflow await comfyPage.menu.workflowsTab.open() - await comfyPage.executeCommand('Comfy.NewBlankWorkflow') + await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow') // Switch back to the missing_nodes workflow await comfyPage.menu.workflowsTab.switchToWorkflow('missing_nodes') @@ -271,14 +280,14 @@ test.describe('Workflows sidebar', () => { test('Can close saved workflow with command', async ({ comfyPage }) => { const tab = comfyPage.menu.workflowsTab await comfyPage.menu.topbar.saveWorkflow('workflow1.json') - await comfyPage.executeCommand('Workspace.CloseWorkflow') + await comfyPage.command.executeCommand('Workspace.CloseWorkflow') expect(await tab.getOpenedWorkflowNames()).toEqual([ '*Unsaved Workflow.json' ]) }) test('Can delete workflows (confirm disabled)', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Workflow.ConfirmDelete', false) + await comfyPage.settings.setSetting('Comfy.Workflow.ConfirmDelete', false) const { topbar, workflowsTab } = comfyPage.menu @@ -288,7 +297,8 @@ test.describe('Workflows sidebar', () => { await workflowsTab.getOpenedItem(filename).click({ button: 'right' }) await comfyPage.nextFrame() - await comfyPage.clickContextMenuItem('Delete') + await comfyPage.contextMenu.clickMenuItem('Delete') + await comfyPage.nextFrame() await expect(workflowsTab.getOpenedItem(filename)).not.toBeVisible() expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([ @@ -304,7 +314,8 @@ test.describe('Workflows sidebar', () => { expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([filename]) await workflowsTab.getOpenedItem(filename).click({ button: 'right' }) - await comfyPage.clickContextMenuItem('Delete') + await comfyPage.contextMenu.clickMenuItem('Delete') + await comfyPage.nextFrame() await comfyPage.confirmDialog.click('delete') @@ -315,7 +326,7 @@ test.describe('Workflows sidebar', () => { }) test('Can duplicate workflow from context menu', async ({ comfyPage }) => { - await comfyPage.setupWorkflowsDirectory({ + await comfyPage.workflow.setupWorkflowsDirectory({ 'workflow1.json': 'default.json' }) @@ -325,7 +336,8 @@ test.describe('Workflows sidebar', () => { await workflowsTab .getPersistedItem('workflow1.json') .click({ button: 'right' }) - await comfyPage.clickContextMenuItem('Duplicate') + await comfyPage.contextMenu.clickMenuItem('Duplicate') + await comfyPage.nextFrame() expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([ '*Unsaved Workflow.json', @@ -334,7 +346,7 @@ test.describe('Workflows sidebar', () => { }) test('Can drop workflow from workflows sidebar', async ({ comfyPage }) => { - await comfyPage.setupWorkflowsDirectory({ + await comfyPage.workflow.setupWorkflowsDirectory({ 'workflow1.json': 'default.json' }) @@ -345,7 +357,7 @@ test.describe('Workflows sidebar', () => { comfyPage.menu.workflowsTab.getPersistedItem('workflow1.json') await expect(workflowItem).toBeVisible({ timeout: 3000 }) - const nodeCount = await comfyPage.getGraphNodesCount() + const nodeCount = await comfyPage.nodeOps.getGraphNodesCount() // Get the bounding box of the canvas element const canvasBoundingBox = (await comfyPage.page @@ -366,7 +378,7 @@ test.describe('Workflows sidebar', () => { // Wait for nodes to be inserted after drag-drop with retryable assertion await expect - .poll(() => comfyPage.getGraphNodesCount(), { timeout: 3000 }) + .poll(() => comfyPage.nodeOps.getGraphNodesCount(), { timeout: 3000 }) .toBe(nodeCount * 2) }) }) diff --git a/browser_tests/tests/subgraph-rename-dialog.spec.ts b/browser_tests/tests/subgraph-rename-dialog.spec.ts index f917bdb9b..b0a36bae8 100644 --- a/browser_tests/tests/subgraph-rename-dialog.spec.ts +++ b/browser_tests/tests/subgraph-rename-dialog.spec.ts @@ -3,7 +3,6 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' // Constants -const INITIAL_NAME = 'initial_slot_name' const RENAMED_NAME = 'renamed_slot_name' const SECOND_RENAMED_NAME = 'second_renamed_name' @@ -14,26 +13,34 @@ const SELECTORS = { test.describe('Subgraph Slot Rename Dialog', { tag: '@subgraph' }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test('Shows current slot label (not stale) in rename dialog', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') - const subgraphNode = await comfyPage.getNodeRefById('2') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2') await subgraphNode.navigateIntoSubgraph() // Get initial slot label const initialInputLabel = await comfyPage.page.evaluate(() => { - const graph = window['app'].canvas.graph + const graph = window.app!.canvas.graph + if (!graph || !('inputNode' in graph)) return null return graph.inputs?.[0]?.label || graph.inputs?.[0]?.name || null }) + if (initialInputLabel === null) { + throw new Error( + 'Expected subgraph to have an input slot label for rightClickInputSlot' + ) + } + // First rename - await comfyPage.rightClickSubgraphInputSlot(initialInputLabel) - await comfyPage.clickLitegraphContextMenuItem('Rename Slot') + await comfyPage.subgraph.rightClickInputSlot(initialInputLabel) + await comfyPage.contextMenu.clickLitegraphMenuItem('Rename Slot') + await comfyPage.nextFrame() await comfyPage.page.waitForSelector(SELECTORS.promptDialog, { state: 'visible' @@ -55,7 +62,9 @@ test.describe('Subgraph Slot Rename Dialog', { tag: '@subgraph' }, () => { // Verify the rename worked const afterFirstRename = await comfyPage.page.evaluate(() => { - const graph = window['app'].canvas.graph + const graph = window.app!.canvas.graph + if (!graph || !('inputNode' in graph)) + return { label: null, name: null, displayName: null } const slot = graph.inputs?.[0] return { label: slot?.label || null, @@ -67,8 +76,9 @@ test.describe('Subgraph Slot Rename Dialog', { tag: '@subgraph' }, () => { // Now rename again - this is where the bug would show // We need to use the index-based approach since the method looks for slot.name - await comfyPage.rightClickSubgraphInputSlot() - await comfyPage.clickLitegraphContextMenuItem('Rename Slot') + await comfyPage.subgraph.rightClickInputSlot() + await comfyPage.contextMenu.clickLitegraphMenuItem('Rename Slot') + await comfyPage.nextFrame() await comfyPage.page.waitForSelector(SELECTORS.promptDialog, { state: 'visible' @@ -97,7 +107,8 @@ test.describe('Subgraph Slot Rename Dialog', { tag: '@subgraph' }, () => { // Verify the second rename worked const afterSecondRename = await comfyPage.page.evaluate(() => { - const graph = window['app'].canvas.graph + const graph = window.app!.canvas.graph + if (!graph || !('inputNode' in graph)) return null return graph.inputs?.[0]?.label || null }) expect(afterSecondRename).toBe(SECOND_RENAMED_NAME) @@ -106,20 +117,28 @@ test.describe('Subgraph Slot Rename Dialog', { tag: '@subgraph' }, () => { test('Shows current output slot label in rename dialog', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') - const subgraphNode = await comfyPage.getNodeRefById('2') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2') await subgraphNode.navigateIntoSubgraph() // Get initial output slot label const initialOutputLabel = await comfyPage.page.evaluate(() => { - const graph = window['app'].canvas.graph + const graph = window.app!.canvas.graph + if (!graph || !('inputNode' in graph)) return null return graph.outputs?.[0]?.label || graph.outputs?.[0]?.name || null }) + if (initialOutputLabel === null) { + throw new Error( + 'Expected subgraph to have an output slot label for rightClickOutputSlot' + ) + } + // First rename - await comfyPage.rightClickSubgraphOutputSlot(initialOutputLabel) - await comfyPage.clickLitegraphContextMenuItem('Rename Slot') + await comfyPage.subgraph.rightClickOutputSlot(initialOutputLabel) + await comfyPage.contextMenu.clickLitegraphMenuItem('Rename Slot') + await comfyPage.nextFrame() await comfyPage.page.waitForSelector(SELECTORS.promptDialog, { state: 'visible' @@ -141,8 +160,9 @@ test.describe('Subgraph Slot Rename Dialog', { tag: '@subgraph' }, () => { // Now rename again to check for stale content // We need to use the index-based approach since the method looks for slot.name - await comfyPage.rightClickSubgraphOutputSlot() - await comfyPage.clickLitegraphContextMenuItem('Rename Slot') + await comfyPage.subgraph.rightClickOutputSlot() + await comfyPage.contextMenu.clickLitegraphMenuItem('Rename Slot') + await comfyPage.nextFrame() await comfyPage.page.waitForSelector(SELECTORS.promptDialog, { state: 'visible' diff --git a/browser_tests/tests/subgraph.spec.ts b/browser_tests/tests/subgraph.spec.ts index 1719ba059..cc78b4953 100644 --- a/browser_tests/tests/subgraph.spec.ts +++ b/browser_tests/tests/subgraph.spec.ts @@ -18,7 +18,7 @@ const SELECTORS = { test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) // Helper to get subgraph slot count @@ -26,8 +26,11 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { comfyPage: typeof test.prototype.comfyPage, type: 'inputs' | 'outputs' ): Promise { - return await comfyPage.page.evaluate((slotType) => { - return window['app'].canvas.graph[slotType]?.length || 0 + return await comfyPage.page.evaluate((slotType: 'inputs' | 'outputs') => { + const graph = window.app!.canvas.graph + // isSubgraph check: subgraphs have isRootGraph === false + if (!graph || !('inputNode' in graph)) return 0 + return graph[slotType]?.length || 0 }, type) } @@ -36,7 +39,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { comfyPage: typeof test.prototype.comfyPage ): Promise { return await comfyPage.page.evaluate(() => { - return window['app'].canvas.graph.nodes?.length || 0 + return window.app!.canvas.graph!.nodes?.length || 0 }) } @@ -45,22 +48,22 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { comfyPage: typeof test.prototype.comfyPage ): Promise { return await comfyPage.page.evaluate(() => { - const graph = window['app'].canvas.graph + const graph = window.app!.canvas.graph return graph?.constructor?.name === 'Subgraph' }) } test.describe('I/O Slot Management', () => { test('Can add input slots to subgraph', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') - const subgraphNode = await comfyPage.getNodeRefById('2') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2') await subgraphNode.navigateIntoSubgraph() const initialCount = await getSubgraphSlotCount(comfyPage, 'inputs') - const vaeEncodeNode = await comfyPage.getNodeRefById('2') + const vaeEncodeNode = await comfyPage.nodeOps.getNodeRefById('2') - await comfyPage.connectFromSubgraphInput(vaeEncodeNode, 0) + await comfyPage.subgraph.connectFromInput(vaeEncodeNode, 0) await comfyPage.nextFrame() const finalCount = await getSubgraphSlotCount(comfyPage, 'inputs') @@ -68,15 +71,15 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { }) test('Can add output slots to subgraph', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') - const subgraphNode = await comfyPage.getNodeRefById('2') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2') await subgraphNode.navigateIntoSubgraph() const initialCount = await getSubgraphSlotCount(comfyPage, 'outputs') - const vaeEncodeNode = await comfyPage.getNodeRefById('2') + const vaeEncodeNode = await comfyPage.nodeOps.getNodeRefById('2') - await comfyPage.connectToSubgraphOutput(vaeEncodeNode, 0) + await comfyPage.subgraph.connectToOutput(vaeEncodeNode, 0) await comfyPage.nextFrame() const finalCount = await getSubgraphSlotCount(comfyPage, 'outputs') @@ -84,16 +87,17 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { }) test('Can remove input slots from subgraph', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') - const subgraphNode = await comfyPage.getNodeRefById('2') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2') await subgraphNode.navigateIntoSubgraph() const initialCount = await getSubgraphSlotCount(comfyPage, 'inputs') expect(initialCount).toBeGreaterThan(0) - await comfyPage.rightClickSubgraphInputSlot() - await comfyPage.clickLitegraphContextMenuItem('Remove Slot') + await comfyPage.subgraph.rightClickInputSlot() + await comfyPage.contextMenu.clickLitegraphMenuItem('Remove Slot') + await comfyPage.nextFrame() // Force re-render await comfyPage.canvas.click({ position: { x: 100, y: 100 } }) @@ -104,16 +108,17 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { }) test('Can remove output slots from subgraph', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') - const subgraphNode = await comfyPage.getNodeRefById('2') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2') await subgraphNode.navigateIntoSubgraph() const initialCount = await getSubgraphSlotCount(comfyPage, 'outputs') expect(initialCount).toBeGreaterThan(0) - await comfyPage.rightClickSubgraphOutputSlot() - await comfyPage.clickLitegraphContextMenuItem('Remove Slot') + await comfyPage.subgraph.rightClickOutputSlot() + await comfyPage.contextMenu.clickLitegraphMenuItem('Remove Slot') + await comfyPage.nextFrame() // Force re-render await comfyPage.canvas.click({ position: { x: 100, y: 100 } }) @@ -124,18 +129,20 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { }) test('Can rename I/O slots', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') - const subgraphNode = await comfyPage.getNodeRefById('2') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2') await subgraphNode.navigateIntoSubgraph() const initialInputLabel = await comfyPage.page.evaluate(() => { - const graph = window['app'].canvas.graph + const graph = window.app!.canvas.graph + if (!graph || !('inputNode' in graph)) return null return graph.inputs?.[0]?.label || null }) - await comfyPage.rightClickSubgraphInputSlot(initialInputLabel) - await comfyPage.clickLitegraphContextMenuItem('Rename Slot') + await comfyPage.subgraph.rightClickInputSlot(initialInputLabel!) + await comfyPage.contextMenu.clickLitegraphMenuItem('Rename Slot') + await comfyPage.nextFrame() await comfyPage.page.waitForSelector(SELECTORS.promptDialog, { state: 'visible' @@ -148,7 +155,8 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { await comfyPage.nextFrame() const newInputName = await comfyPage.page.evaluate(() => { - const graph = window['app'].canvas.graph + const graph = window.app!.canvas.graph + if (!graph || !('inputNode' in graph)) return null return graph.inputs?.[0]?.label || null }) @@ -157,17 +165,18 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { }) test('Can rename input slots via double-click', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') - const subgraphNode = await comfyPage.getNodeRefById('2') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2') await subgraphNode.navigateIntoSubgraph() const initialInputLabel = await comfyPage.page.evaluate(() => { - const graph = window['app'].canvas.graph + const graph = window.app!.canvas.graph + if (!graph || !('inputNode' in graph)) return null return graph.inputs?.[0]?.label || null }) - await comfyPage.doubleClickSubgraphInputSlot(initialInputLabel) + await comfyPage.subgraph.doubleClickInputSlot(initialInputLabel!) await comfyPage.page.waitForSelector(SELECTORS.promptDialog, { state: 'visible' @@ -180,7 +189,8 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { await comfyPage.nextFrame() const newInputName = await comfyPage.page.evaluate(() => { - const graph = window['app'].canvas.graph + const graph = window.app!.canvas.graph + if (!graph || !('inputNode' in graph)) return null return graph.inputs?.[0]?.label || null }) @@ -189,17 +199,18 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { }) test('Can rename output slots via double-click', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') - const subgraphNode = await comfyPage.getNodeRefById('2') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2') await subgraphNode.navigateIntoSubgraph() const initialOutputLabel = await comfyPage.page.evaluate(() => { - const graph = window['app'].canvas.graph + const graph = window.app!.canvas.graph + if (!graph || !('inputNode' in graph)) return null return graph.outputs?.[0]?.label || null }) - await comfyPage.doubleClickSubgraphOutputSlot(initialOutputLabel) + await comfyPage.subgraph.doubleClickOutputSlot(initialOutputLabel!) await comfyPage.page.waitForSelector(SELECTORS.promptDialog, { state: 'visible' @@ -213,7 +224,8 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { await comfyPage.nextFrame() const newOutputName = await comfyPage.page.evaluate(() => { - const graph = window['app'].canvas.graph + const graph = window.app!.canvas.graph + if (!graph || !('inputNode' in graph)) return null return graph.outputs?.[0]?.label || null }) @@ -224,19 +236,21 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { test('Right-click context menu still works alongside double-click', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') - const subgraphNode = await comfyPage.getNodeRefById('2') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2') await subgraphNode.navigateIntoSubgraph() const initialInputLabel = await comfyPage.page.evaluate(() => { - const graph = window['app'].canvas.graph + const graph = window.app!.canvas.graph + if (!graph || !('inputNode' in graph)) return null return graph.inputs?.[0]?.label || null }) // Test that right-click still works for renaming - await comfyPage.rightClickSubgraphInputSlot(initialInputLabel) - await comfyPage.clickLitegraphContextMenuItem('Rename Slot') + await comfyPage.subgraph.rightClickInputSlot(initialInputLabel!) + await comfyPage.contextMenu.clickLitegraphMenuItem('Rename Slot') + await comfyPage.nextFrame() await comfyPage.page.waitForSelector(SELECTORS.promptDialog, { state: 'visible' @@ -250,7 +264,8 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { await comfyPage.nextFrame() const newInputName = await comfyPage.page.evaluate(() => { - const graph = window['app'].canvas.graph + const graph = window.app!.canvas.graph + if (!graph || !('inputNode' in graph)) return null return graph.inputs?.[0]?.label || null }) @@ -261,20 +276,25 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { test('Can double-click on slot label text to rename', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') - const subgraphNode = await comfyPage.getNodeRefById('2') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2') await subgraphNode.navigateIntoSubgraph() const initialInputLabel = await comfyPage.page.evaluate(() => { - const graph = window['app'].canvas.graph + const graph = window.app!.canvas.graph + if (!graph || !('inputNode' in graph)) return null return graph.inputs?.[0]?.label || null }) // Use direct pointer event approach to double-click on label await comfyPage.page.evaluate(() => { - const app = window['app'] + const app = window.app! + const graph = app.canvas.graph + if (!graph || !('inputNode' in graph)) { + throw new Error('Expected to be in subgraph') + } const input = graph.inputs?.[0] if (!input?.labelPos) { @@ -285,13 +305,15 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { const testX = input.labelPos[0] const testY = input.labelPos[1] + // Create a minimal mock event with required properties + // Full PointerEvent creation is unnecessary for this test const leftClickEvent = { canvasX: testX, canvasY: testY, - button: 0, // Left mouse button + button: 0, preventDefault: () => {}, stopPropagation: () => {} - } + } as Parameters[0] const inputNode = graph.inputNode if (inputNode?.onPointerDown) { @@ -322,7 +344,8 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { await comfyPage.nextFrame() const newInputName = await comfyPage.page.evaluate(() => { - const graph = window['app'].canvas.graph + const graph = window.app!.canvas.graph + if (!graph || !('inputNode' in graph)) return null return graph.inputs?.[0]?.label || null }) @@ -332,9 +355,11 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { test('Can create widget from link with compressed target_slot', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/subgraph-compressed-target-slot') + await comfyPage.workflow.loadWorkflow( + 'subgraphs/subgraph-compressed-target-slot' + ) const step = await comfyPage.page.evaluate(() => { - return window['app'].graph.nodes[0].widgets[0].options.step + return window.app!.graph!.nodes[0].widgets![0].options.step }) expect(step).toBe(10) }) @@ -342,19 +367,17 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { test.describe('Subgraph Creation and Deletion', () => { test('Can create subgraph from selected nodes', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('default') + await comfyPage.workflow.loadWorkflow('default') - const initialNodeCount = await getGraphNodeCount(comfyPage) - - await comfyPage.ctrlA() + await comfyPage.keyboard.selectAll() await comfyPage.nextFrame() - const node = await comfyPage.getNodeRefById('5') + const node = await comfyPage.nodeOps.getNodeRefById('5') await node.convertToSubgraph() await comfyPage.nextFrame() const subgraphNodes = - await comfyPage.getNodeRefsByTitle(NEW_SUBGRAPH_TITLE) + await comfyPage.nodeOps.getNodeRefsByTitle(NEW_SUBGRAPH_TITLE) expect(subgraphNodes.length).toBe(1) const finalNodeCount = await getGraphNodeCount(comfyPage) @@ -362,9 +385,9 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { }) test('Can delete subgraph node', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') - const subgraphNode = await comfyPage.getNodeRefById('2') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2') expect(await subgraphNode.exists()).toBe(true) const initialNodeCount = await getGraphNodeCount(comfyPage) @@ -376,7 +399,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { const finalNodeCount = await getGraphNodeCount(comfyPage) expect(finalNodeCount).toBe(initialNodeCount - 1) - const deletedNode = await comfyPage.getNodeRefById('2') + const deletedNode = await comfyPage.nodeOps.getNodeRefById('2') expect(await deletedNode.exists()).toBe(false) }) @@ -384,9 +407,9 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { test('Can copy subgraph node by dragging + alt', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') - const subgraphNode = await comfyPage.getNodeRefById('2') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2') // Get position of subgraph node const subgraphPos = await subgraphNode.getPosition() @@ -404,7 +427,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { // Find all subgraph nodes const subgraphNodes = - await comfyPage.getNodeRefsByTitle(NEW_SUBGRAPH_TITLE) + await comfyPage.nodeOps.getNodeRefsByTitle(NEW_SUBGRAPH_TITLE) // Expect a second subgraph node to be created (2 total) expect(subgraphNodes.length).toBe(2) @@ -413,9 +436,9 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { test('Copying subgraph node by dragging + alt creates a new subgraph node with unique type', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') - const subgraphNode = await comfyPage.getNodeRefById('2') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2') // Get position of subgraph node const subgraphPos = await subgraphNode.getPosition() @@ -433,7 +456,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { // Find all subgraph nodes and expect all unique IDs const subgraphNodes = - await comfyPage.getNodeRefsByTitle(NEW_SUBGRAPH_TITLE) + await comfyPage.nodeOps.getNodeRefsByTitle(NEW_SUBGRAPH_TITLE) // Expect the second subgraph node to have a unique type const nodeType1 = await subgraphNodes[0].getType() @@ -445,21 +468,21 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { test.describe('Operations Inside Subgraphs', () => { test('Can copy and paste nodes in subgraph', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') - const subgraphNode = await comfyPage.getNodeRefById('2') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2') await subgraphNode.navigateIntoSubgraph() const initialNodeCount = await getGraphNodeCount(comfyPage) const nodesInSubgraph = await comfyPage.page.evaluate(() => { - const nodes = window['app'].canvas.graph.nodes + const nodes = window.app!.canvas.graph!.nodes return nodes?.[0]?.id || null }) expect(nodesInSubgraph).not.toBeNull() - const nodeToClone = await comfyPage.getNodeRefById( + const nodeToClone = await comfyPage.nodeOps.getNodeRefById( String(nodesInSubgraph) ) await nodeToClone.click('title') @@ -476,13 +499,13 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { }) test('Can undo and redo operations in subgraph', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') - const subgraphNode = await comfyPage.getNodeRefById('2') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2') await subgraphNode.navigateIntoSubgraph() // Add a node - await comfyPage.doubleClickCanvas() + await comfyPage.canvasOps.doubleClick() await comfyPage.searchBox.fillAndSelectFirstNode('Note') await comfyPage.nextFrame() @@ -490,14 +513,14 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { const initialCount = await getGraphNodeCount(comfyPage) // Undo - await comfyPage.ctrlZ() + await comfyPage.keyboard.undo() await comfyPage.nextFrame() const afterUndoCount = await getGraphNodeCount(comfyPage) expect(afterUndoCount).toBe(initialCount - 1) // Redo - await comfyPage.ctrlY() + await comfyPage.keyboard.redo() await comfyPage.nextFrame() const afterRedoCount = await getGraphNodeCount(comfyPage) @@ -507,16 +530,16 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { test.describe('Subgraph Navigation and UI', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') }) test('Breadcrumb updates when subgraph node title is changed', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/nested-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/nested-subgraph') await comfyPage.nextFrame() - const subgraphNode = await comfyPage.getNodeRefById('10') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('10') const nodePos = await subgraphNode.getPosition() const nodeSize = await subgraphNode.getSize() @@ -565,7 +588,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { test('DOM widget visibility persists through subgraph navigation', async ({ comfyPage }) => { - await comfyPage.loadWorkflow( + await comfyPage.workflow.loadWorkflow( 'subgraphs/subgraph-with-promoted-text-widget' ) await comfyPage.nextFrame() @@ -575,7 +598,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { await expect(parentTextarea).toBeVisible() await expect(parentTextarea).toHaveCount(1) - const subgraphNode = await comfyPage.getNodeRefById('11') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('11') expect(await subgraphNode.exists()).toBe(true) await subgraphNode.navigateIntoSubgraph() @@ -598,14 +621,14 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { test('DOM widget content is preserved through navigation', async ({ comfyPage }) => { - await comfyPage.loadWorkflow( + await comfyPage.workflow.loadWorkflow( 'subgraphs/subgraph-with-promoted-text-widget' ) const textarea = comfyPage.page.locator(SELECTORS.domWidget) await textarea.fill(TEST_WIDGET_CONTENT) - const subgraphNode = await comfyPage.getNodeRefById('11') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('11') await subgraphNode.navigateIntoSubgraph() const subgraphTextarea = comfyPage.page.locator(SELECTORS.domWidget) @@ -621,7 +644,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { test('DOM elements are cleaned up when subgraph node is removed', async ({ comfyPage }) => { - await comfyPage.loadWorkflow( + await comfyPage.workflow.loadWorkflow( 'subgraphs/subgraph-with-promoted-text-widget' ) @@ -630,7 +653,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { .count() expect(initialCount).toBe(1) - const subgraphNode = await comfyPage.getNodeRefById('11') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('11') await subgraphNode.click('title') await comfyPage.page.keyboard.press('Delete') @@ -646,23 +669,24 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { comfyPage }) => { // Enable new menu for breadcrumb navigation - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') const workflowName = 'subgraphs/subgraph-with-promoted-text-widget' - await comfyPage.loadWorkflow(workflowName) + await comfyPage.workflow.loadWorkflow(workflowName) const textareaCount = await comfyPage.page .locator(SELECTORS.domWidget) .count() expect(textareaCount).toBe(1) - const subgraphNode = await comfyPage.getNodeRefById('11') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('11') // Navigate into subgraph (method now handles retries internally) await subgraphNode.navigateIntoSubgraph() - await comfyPage.rightClickSubgraphInputSlot('text') - await comfyPage.clickLitegraphContextMenuItem('Remove Slot') + await comfyPage.subgraph.rightClickInputSlot('text') + await comfyPage.contextMenu.clickLitegraphMenuItem('Remove Slot') + await comfyPage.nextFrame() // Wait for breadcrumb to be visible await comfyPage.page.waitForSelector(SELECTORS.breadcrumb, { @@ -682,7 +706,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { // Check that the subgraph node has no widgets after removing the text slot const widgetCount = await comfyPage.page.evaluate(() => { - return window['app'].canvas.graph.nodes[0].widgets?.length || 0 + return window.app!.canvas.graph!.nodes[0].widgets?.length || 0 }) expect(widgetCount).toBe(0) @@ -691,7 +715,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { test('Multiple promoted widgets are handled correctly', async ({ comfyPage }) => { - await comfyPage.loadWorkflow( + await comfyPage.workflow.loadWorkflow( 'subgraphs/subgraph-with-multiple-promoted-widgets' ) @@ -700,7 +724,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { .count() expect(parentCount).toBeGreaterThan(1) - const subgraphNode = await comfyPage.getNodeRefById('11') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('11') await subgraphNode.navigateIntoSubgraph() const subgraphCount = await comfyPage.page @@ -720,15 +744,15 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { test.describe('Navigation Hotkeys', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') }) test('Navigation hotkey can be customized', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') await comfyPage.nextFrame() // Change the Exit Subgraph keybinding from Escape to Alt+Q - await comfyPage.setSetting('Comfy.Keybinding.NewBindings', [ + await comfyPage.settings.setSetting('Comfy.Keybinding.NewBindings', [ { commandId: 'Comfy.Graph.ExitSubgraph', combo: { @@ -740,7 +764,7 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { } ]) - await comfyPage.setSetting('Comfy.Keybinding.UnsetBindings', [ + await comfyPage.settings.setSetting('Comfy.Keybinding.UnsetBindings', [ { commandId: 'Comfy.Graph.ExitSubgraph', combo: { @@ -754,10 +778,12 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { // Reload the page await comfyPage.page.reload() - await comfyPage.page.waitForTimeout(1024) + await comfyPage.setup() + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.nextFrame() // Navigate into subgraph - const subgraphNode = await comfyPage.getNodeRefById('2') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2') await subgraphNode.navigateIntoSubgraph() await comfyPage.page.waitForSelector(SELECTORS.breadcrumb) @@ -780,10 +806,10 @@ test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => { test('Escape prioritizes closing dialogs over exiting subgraph', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('subgraphs/basic-subgraph') + await comfyPage.workflow.loadWorkflow('subgraphs/basic-subgraph') await comfyPage.nextFrame() - const subgraphNode = await comfyPage.getNodeRefById('2') + const subgraphNode = await comfyPage.nodeOps.getNodeRefById('2') await subgraphNode.navigateIntoSubgraph() await comfyPage.page.waitForSelector(SELECTORS.breadcrumb) diff --git a/browser_tests/tests/templates.spec.ts b/browser_tests/tests/templates.spec.ts index 334c761af..54f6360aa 100644 --- a/browser_tests/tests/templates.spec.ts +++ b/browser_tests/tests/templates.spec.ts @@ -15,8 +15,11 @@ async function checkTemplateFileExists( test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') - await comfyPage.setSetting('Comfy.Workflow.ShowMissingModelsWarning', false) + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting( + 'Comfy.Workflow.ShowMissingModelsWarning', + false + ) }) test('should have a JSON workflow file for each template', async ({ @@ -72,13 +75,13 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => { test('Can load template workflows', async ({ comfyPage }) => { // Clear the workflow await comfyPage.menu.workflowsTab.open() - await comfyPage.executeCommand('Comfy.NewBlankWorkflow') + await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow') await expect(async () => { - expect(await comfyPage.getGraphNodesCount()).toBe(0) + expect(await comfyPage.nodeOps.getGraphNodesCount()).toBe(0) }).toPass({ timeout: 250 }) // Load a template - await comfyPage.executeCommand('Comfy.BrowseTemplates') + await comfyPage.command.executeCommand('Comfy.BrowseTemplates') await expect(comfyPage.templates.content).toBeVisible() await comfyPage.page @@ -89,7 +92,7 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => { // Ensure we now have some nodes await expect(async () => { - expect(await comfyPage.getGraphNodesCount()).toBeGreaterThan(0) + expect(await comfyPage.nodeOps.getGraphNodesCount()).toBeGreaterThan(0) }).toPass({ timeout: 250 }) }) @@ -97,7 +100,7 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => { comfyPage }) => { // Set the tutorial as not completed to mark the user as a first-time user - await comfyPage.setSetting('Comfy.TutorialCompleted', false) + await comfyPage.settings.setSetting('Comfy.TutorialCompleted', false) // Load the page await comfyPage.setup({ clearStorage: true }) @@ -107,9 +110,9 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => { }) test('Uses proper locale files for templates', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Locale', 'fr') + await comfyPage.settings.setSetting('Comfy.Locale', 'fr') - await comfyPage.executeCommand('Comfy.BrowseTemplates') + await comfyPage.command.executeCommand('Comfy.BrowseTemplates') const dialog = comfyPage.page.getByRole('dialog').filter({ has: comfyPage.page.getByRole('heading', { name: 'Modèles', exact: true }) @@ -134,7 +137,7 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => { comfyPage }) => { // Set locale to a language that doesn't have a template file - await comfyPage.setSetting('Comfy.Locale', 'de') // German - no index.de.json exists + await comfyPage.settings.setSetting('Comfy.Locale', 'de') // German - no index.de.json exists // Wait for the German request (expected to 404) const germanRequestPromise = comfyPage.page.waitForRequest( @@ -161,7 +164,7 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => { ) // Load the templates dialog - await comfyPage.executeCommand('Comfy.BrowseTemplates') + await comfyPage.command.executeCommand('Comfy.BrowseTemplates') await expect(comfyPage.templates.content).toBeVisible() // Verify German was requested first, then English as fallback @@ -181,7 +184,7 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => { comfyPage }) => { // Open templates dialog - await comfyPage.executeCommand('Comfy.BrowseTemplates') + await comfyPage.command.executeCommand('Comfy.BrowseTemplates') await comfyPage.templates.content.waitFor({ state: 'visible' }) const templateGrid = comfyPage.page.locator( @@ -189,20 +192,20 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => { ) const nav = comfyPage.page.locator('header', { hasText: 'Templates' }) - await comfyPage.templates.waitForMinimumCardCount(1) + await comfyPage.templates.expectMinimumCardCount(1) await expect(templateGrid).toBeVisible() await expect(nav).toBeVisible() // Nav should be visible at desktop size const mobileSize = { width: 640, height: 800 } await comfyPage.page.setViewportSize(mobileSize) - await comfyPage.templates.waitForMinimumCardCount(1) + await comfyPage.templates.expectMinimumCardCount(1) await expect(templateGrid).toBeVisible() // Nav header is clipped by overflow-hidden parent at mobile size await expect(nav).not.toBeInViewport() const tabletSize = { width: 1024, height: 800 } await comfyPage.page.setViewportSize(tabletSize) - await comfyPage.templates.waitForMinimumCardCount(1) + await comfyPage.templates.expectMinimumCardCount(1) await expect(templateGrid).toBeVisible() await expect(nav).toBeVisible() // Nav should be visible at tablet size }) @@ -272,7 +275,7 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => { }) // Open templates dialog - await comfyPage.executeCommand('Comfy.BrowseTemplates') + await comfyPage.command.executeCommand('Comfy.BrowseTemplates') await expect(comfyPage.templates.content).toBeVisible() // Wait for cards to load diff --git a/browser_tests/tests/useSettingSearch.spec.ts b/browser_tests/tests/useSettingSearch.spec.ts index aa3481408..43ddd09e7 100644 --- a/browser_tests/tests/useSettingSearch.spec.ts +++ b/browser_tests/tests/useSettingSearch.spec.ts @@ -1,27 +1,37 @@ import { expect } from '@playwright/test' +import type { Settings } from '../../src/schemas/apiSchema' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +/** + * Type helper for test settings with arbitrary IDs. + * Extensions can register settings with any ID, but SettingParams.id + * is typed as keyof Settings for autocomplete. + */ +type TestSettingId = keyof Settings + test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Settings Search functionality', { tag: '@settings' }, () => { test.beforeEach(async ({ comfyPage }) => { // Register test settings to verify hidden/deprecated filtering await comfyPage.page.evaluate(() => { - window['app'].registerExtension({ + window.app!.registerExtension({ name: 'TestSettingsExtension', settings: [ { - id: 'TestHiddenSetting', + // Extensions can register arbitrary setting IDs + id: 'TestHiddenSetting' as TestSettingId, name: 'Test Hidden Setting', type: 'hidden', defaultValue: 'hidden_value', category: ['Test', 'Hidden'] }, { - id: 'TestDeprecatedSetting', + // Extensions can register arbitrary setting IDs + id: 'TestDeprecatedSetting' as TestSettingId, name: 'Test Deprecated Setting', type: 'text', defaultValue: 'deprecated_value', @@ -29,7 +39,8 @@ test.describe('Settings Search functionality', { tag: '@settings' }, () => { category: ['Test', 'Deprecated'] }, { - id: 'TestVisibleSetting', + // Extensions can register arbitrary setting IDs + id: 'TestVisibleSetting' as TestSettingId, name: 'Test Visible Setting', type: 'text', defaultValue: 'visible_value', @@ -109,19 +120,14 @@ test.describe('Settings Search functionality', { tag: '@settings' }, () => { const settingsDialog = comfyPage.page.locator('.settings-container') await expect(settingsDialog).toBeVisible() - // Get categories and click on different ones - const categories = comfyPage.page.locator( - '.settings-sidebar .p-listbox-option' - ) - const categoryCount = await categories.count() + // Click on a specific category (Appearance) to verify category switching + const appearanceCategory = comfyPage.page.getByRole('option', { + name: 'Appearance' + }) + await appearanceCategory.click() - if (categoryCount > 1) { - // Click on the second category - await categories.nth(1).click() - - // Verify the category is selected - await expect(categories.nth(1)).toHaveClass(/p-listbox-option-selected/) - } + // Verify the category is selected + await expect(appearanceCategory).toHaveClass(/p-listbox-option-selected/) }) test('settings content area is visible', async ({ comfyPage }) => { diff --git a/browser_tests/tests/versionMismatchWarnings.spec.ts b/browser_tests/tests/versionMismatchWarnings.spec.ts index 15c55873f..6c0a89034 100644 --- a/browser_tests/tests/versionMismatchWarnings.spec.ts +++ b/browser_tests/tests/versionMismatchWarnings.spec.ts @@ -37,8 +37,8 @@ test.describe('Version Mismatch Warnings', { tag: '@slow' }, () => { } test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') - await comfyPage.setSetting( + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting( 'Comfy.VersionCompatibility.DisableWarnings', false ) @@ -103,10 +103,9 @@ test.describe('Version Mismatch Warnings', { tag: '@slow' }, () => { await comfyPage.setup() // Locate the warning toast and dismiss it - const warningToast = comfyPage.page - .locator('div') - .filter({ hasText: 'Version Compatibility' }) - .nth(3) + const warningToast = comfyPage.page.locator('.p-toast-message').filter({ + hasText: 'Version Compatibility' + }) await warningToast.waitFor({ state: 'visible' }) const dismissButton = warningToast.getByRole('button', { name: 'Close' }) await dismissButton.click() diff --git a/browser_tests/tests/viewport.spec.ts b/browser_tests/tests/viewport.spec.ts index c0d65ca38..b7320ee0e 100644 --- a/browser_tests/tests/viewport.spec.ts +++ b/browser_tests/tests/viewport.spec.ts @@ -6,7 +6,9 @@ test.describe('Viewport', { tag: ['@screenshot', '@smoke', '@canvas'] }, () => { test('Fits view to nodes when saved viewport position is offscreen', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('viewport/default-viewport-saved-offscreen') + await comfyPage.workflow.loadWorkflow( + 'viewport/default-viewport-saved-offscreen' + ) // Wait a few frames for rendering to stabilize for (let i = 0; i < 5; i++) { diff --git a/browser_tests/tests/viewport.spec.ts-snapshots/viewport-fits-when-saved-offscreen-chromium-linux.png b/browser_tests/tests/viewport.spec.ts-snapshots/viewport-fits-when-saved-offscreen-chromium-linux.png index 1afdb11a2..f6ac2afd0 100644 Binary files a/browser_tests/tests/viewport.spec.ts-snapshots/viewport-fits-when-saved-offscreen-chromium-linux.png and b/browser_tests/tests/viewport.spec.ts-snapshots/viewport-fits-when-saved-offscreen-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/groups/groups.spec.ts b/browser_tests/tests/vueNodes/groups/groups.spec.ts index 19404f7d4..5f691bbee 100644 --- a/browser_tests/tests/vueNodes/groups/groups.spec.ts +++ b/browser_tests/tests/vueNodes/groups/groups.spec.ts @@ -7,8 +7,8 @@ const CREATE_GROUP_HOTKEY = 'Control+g' test.describe('Vue Node Groups', { tag: '@screenshot' }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) - await comfyPage.setSetting('Comfy.Minimap.ShowGroups', true) + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('Comfy.Minimap.ShowGroups', true) await comfyPage.vueNodes.waitForNodes() }) @@ -24,9 +24,9 @@ test.describe('Vue Node Groups', { tag: '@screenshot' }, () => { test('should allow fitting group to contents', async ({ comfyPage }) => { await comfyPage.setup() - await comfyPage.loadWorkflow('groups/oversized_group') - await comfyPage.ctrlA() - await comfyPage.executeCommand('Comfy.Graph.FitGroupToContents') + await comfyPage.workflow.loadWorkflow('groups/oversized_group') + await comfyPage.keyboard.selectAll() + await comfyPage.command.executeCommand('Comfy.Graph.FitGroupToContents') await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot( 'vue-groups-fit-to-contents.png' @@ -36,18 +36,20 @@ test.describe('Vue Node Groups', { tag: '@screenshot' }, () => { test('should move nested groups together when dragging outer group', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('groups/nested-groups-1-inner-node') + await comfyPage.workflow.loadWorkflow('groups/nested-groups-1-inner-node') // Get initial positions with null guards - const outerInitial = await comfyPage.getGroupPosition('Outer Group') - const innerInitial = await comfyPage.getGroupPosition('Inner Group') + const outerInitial = + await comfyPage.canvasOps.getGroupPosition('Outer Group') + const innerInitial = + await comfyPage.canvasOps.getGroupPosition('Inner Group') const initialOffsetX = innerInitial.x - outerInitial.x const initialOffsetY = innerInitial.y - outerInitial.y // Drag the outer group const dragDelta = { x: 100, y: 80 } - await comfyPage.dragGroup({ + await comfyPage.canvasOps.dragGroup({ name: 'Outer Group', deltaX: dragDelta.x, deltaY: dragDelta.y @@ -55,8 +57,10 @@ test.describe('Vue Node Groups', { tag: '@screenshot' }, () => { // Use retrying assertion to wait for positions to update await expect(async () => { - const outerFinal = await comfyPage.getGroupPosition('Outer Group') - const innerFinal = await comfyPage.getGroupPosition('Inner Group') + const outerFinal = + await comfyPage.canvasOps.getGroupPosition('Outer Group') + const innerFinal = + await comfyPage.canvasOps.getGroupPosition('Inner Group') const finalOffsetX = innerFinal.x - outerFinal.x const finalOffsetY = innerFinal.y - outerFinal.y diff --git a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png index 7b20ddbeb..81b6a32ba 100644 Binary files a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png and b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts b/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts index ed18ab367..c7f5ce92c 100644 --- a/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts +++ b/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts @@ -5,7 +5,7 @@ import { test.describe('Vue Nodes Canvas Pan', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.vueNodes.waitForNodes() }) @@ -13,7 +13,10 @@ test.describe('Vue Nodes Canvas Pan', () => { '@mobile Can pan with touch', { tag: '@screenshot' }, async ({ comfyPage }) => { - await comfyPage.panWithTouch({ x: 64, y: 64 }, { x: 256, y: 256 }) + await comfyPage.canvasOps.panWithTouch( + { x: 64, y: 64 }, + { x: 256, y: 256 } + ) await expect(comfyPage.canvas).toHaveScreenshot( 'vue-nodes-paned-with-touch.png' ) diff --git a/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts b/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts index 0b064dd81..e391c289d 100644 --- a/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts +++ b/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts @@ -5,8 +5,8 @@ import { test.describe('Vue Nodes Zoom', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) - await comfyPage.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 8) + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 8) await comfyPage.vueNodes.waitForNodes() }) @@ -26,7 +26,7 @@ test.describe('Vue Nodes Zoom', () => { // the node. The node should not capture the drag while drag-zooming. await comfyPage.page.keyboard.down('Control') await comfyPage.page.keyboard.down('Shift') - await comfyPage.dragAndDrop( + await comfyPage.canvasOps.dragAndDrop( { x: 200, y: 300 }, { x: nodeMidpointX, y: nodeMidpointY } ) diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts index b22baf5b1..ab41c2785 100644 --- a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts +++ b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts @@ -25,7 +25,7 @@ async function getInputLinkDetails( ) { return await page.evaluate( ([targetNodeId, targetSlot]) => { - const app = window['app'] + const app = window.app const graph = app?.canvas?.graph ?? app?.graph if (!graph) return null @@ -100,10 +100,10 @@ async function connectSlots( test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) // await comfyPage.setup() - await comfyPage.loadWorkflow('vueNodes/simple-triple') + await comfyPage.workflow.loadWorkflow('vueNodes/simple-triple') await comfyPage.vueNodes.waitForNodes() await fitToViewInstant(comfyPage) }) @@ -112,7 +112,9 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { comfyPage, comfyMouse }) => { - const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] + const samplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler') + )[0] expect(samplerNode).toBeTruthy() const slot = slotLocator(comfyPage.page, samplerNode.id, 0, false) @@ -142,8 +144,10 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { test('should create a link when dropping on a compatible slot', async ({ comfyPage }) => { - const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] - const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0] + const samplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler') + )[0] + const vaeNode = (await comfyPage.nodeOps.getNodeRefsByType('VAEDecode'))[0] expect(samplerNode && vaeNode).toBeTruthy() const samplerOutput = await samplerNode.getOutput(0) @@ -172,8 +176,12 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { test('should not create a link when slot types are incompatible', async ({ comfyPage }) => { - const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] - const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0] + const samplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler') + )[0] + const clipNode = ( + await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode') + )[0] expect(samplerNode && clipNode).toBeTruthy() const samplerOutput = await samplerNode.getOutput(0) @@ -200,7 +208,9 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { test('should not create a link when dropping onto a slot on the same node', async ({ comfyPage }) => { - const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] + const samplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler') + )[0] expect(samplerNode).toBeTruthy() const samplerOutput = await samplerNode.getOutput(0) @@ -221,8 +231,10 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { comfyPage, comfyMouse }) => { - const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] - const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0] + const samplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler') + )[0] + const vaeNode = (await comfyPage.nodeOps.getNodeRefsByType('VAEDecode'))[0] expect(samplerNode && vaeNode).toBeTruthy() const samplerOutputCenter = await getSlotCenter( comfyPage.page, @@ -258,8 +270,10 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { comfyPage, comfyMouse }) => { - const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] - const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0] + const samplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler') + )[0] + const vaeNode = (await comfyPage.nodeOps.getNodeRefsByType('VAEDecode'))[0] expect(samplerNode && vaeNode).toBeTruthy() const samplerOutput = await samplerNode.getOutput(0) @@ -315,8 +329,10 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { comfyPage, comfyMouse }) => { - const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] - const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0] + const samplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler') + )[0] + const vaeNode = (await comfyPage.nodeOps.getNodeRefsByType('VAEDecode'))[0] expect(samplerNode && vaeNode).toBeTruthy() const samplerOutput = await samplerNode.getOutput(0) @@ -398,8 +414,10 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { comfyPage, comfyMouse }) => { - const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] - const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0] + const samplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler') + )[0] + const vaeNode = (await comfyPage.nodeOps.getNodeRefsByType('VAEDecode'))[0] const samplerOutput = await samplerNode.getOutput(0) const vaeInput = await vaeNode.getInput(0) @@ -419,7 +437,7 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { // This avoids relying on an exact path hit-test position. await comfyPage.page.evaluate( ([targetNodeId, targetSlot, clientPoint]) => { - const app = window['app'] + const app = window.app const graph = app?.canvas?.graph ?? app?.graph if (!graph) throw new Error('Graph not available') const node = graph.getNodeById(targetNodeId) @@ -433,7 +451,7 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { if (!link) throw new Error('Link not found') // Convert the client/canvas pixel coordinates to graph space - const pos = app.canvas.ds.convertCanvasToOffset([ + const pos = app!.canvas.ds.convertCanvasToOffset([ clientPoint.x, clientPoint.y ]) @@ -483,8 +501,10 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { comfyPage, comfyMouse }) => { - const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] - const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0] + const samplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler') + )[0] + const vaeNode = (await comfyPage.nodeOps.getNodeRefsByType('VAEDecode'))[0] expect(samplerNode && vaeNode).toBeTruthy() const samplerOutput = await samplerNode.getOutput(0) @@ -505,7 +525,7 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { // This avoids relying on an exact path hit-test position. await comfyPage.page.evaluate( ([targetNodeId, targetSlot, clientPoint]) => { - const app = window['app'] + const app = window.app const graph = app?.canvas?.graph ?? app?.graph if (!graph) throw new Error('Graph not available') const node = graph.getNodeById(targetNodeId) @@ -519,7 +539,7 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { if (!link) throw new Error('Link not found') // Convert the client/canvas pixel coordinates to graph space - const pos = app.canvas.ds.convertCanvasToOffset([ + const pos = app!.canvas.ds.convertCanvasToOffset([ clientPoint.x, clientPoint.y ]) @@ -572,8 +592,12 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { comfyPage, comfyMouse }) => { - const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0] - const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] + const clipNode = ( + await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode') + )[0] + const samplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler') + )[0] expect(clipNode && samplerNode).toBeTruthy() // Step 1: Connect CLIP's only output (index 0) to KSampler's second input (index 1) @@ -642,8 +666,12 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { comfyPage, comfyMouse }) => { - const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0] - const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] + const clipNode = ( + await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode') + )[0] + const samplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler') + )[0] expect(clipNode && samplerNode).toBeTruthy() const clipOutput = await clipNode.getOutput(0) @@ -697,8 +725,12 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { comfyPage, comfyMouse }) => { - const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0] - const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] + const clipNode = ( + await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode') + )[0] + const samplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler') + )[0] expect(clipNode && samplerNode).toBeTruthy() // Start drag from CLIP output[0] @@ -746,8 +778,12 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { comfyPage, comfyMouse }) => { - const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0] - const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] + const clipNode = ( + await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode') + )[0] + const samplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler') + )[0] expect(clipNode && samplerNode).toBeTruthy() // Drag from CLIP output[0] to KSampler input[2] (third slot) which is the @@ -791,8 +827,12 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { test('should batch disconnect all links with ctrl+alt+click on slot', async ({ comfyPage }) => { - const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0] - const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] + const clipNode = ( + await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode') + )[0] + const samplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler') + )[0] expect(clipNode && samplerNode).toBeTruthy() await connectSlots( @@ -832,12 +872,14 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { comfyPage, comfyMouse }) => { - await comfyPage.setSetting( + await comfyPage.settings.setSetting( 'Comfy.LinkRelease.ActionShift', 'context menu' ) - const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] + const samplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler') + )[0] expect(samplerNode).toBeTruthy() const outputCenter = await getSlotCenter( @@ -864,7 +906,7 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { // Pinned endpoint should not change with mouse movement while menu is open const before = await comfyPage.page.evaluate(() => { - const snap = window['app']?.canvas?.linkConnector?.state?.snapLinksPos + const snap = window.app?.canvas?.linkConnector?.state?.snapLinksPos return Array.isArray(snap) ? [snap[0], snap[1]] : null }) expect(before).not.toBeNull() @@ -872,7 +914,7 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { // Move mouse elsewhere and verify snap position is unchanged await comfyMouse.move({ x: dropPos.x + 160, y: dropPos.y + 100 }) const after = await comfyPage.page.evaluate(() => { - const snap = window['app']?.canvas?.linkConnector?.state?.snapLinksPos + const snap = window.app?.canvas?.linkConnector?.state?.snapLinksPos return Array.isArray(snap) ? [snap[0], snap[1]] : null }) expect(after).toEqual(before) @@ -882,13 +924,15 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { comfyPage, comfyMouse }) => { - await comfyPage.setSetting( + await comfyPage.settings.setSetting( 'Comfy.LinkRelease.ActionShift', 'context menu' ) - await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default') + await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default') - const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] + const samplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler') + )[0] expect(samplerNode).toBeTruthy() const outputCenter = await getSlotCenter( @@ -909,7 +953,8 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { } // Open Search from the context menu - await comfyPage.clickContextMenuItem('Search') + await comfyPage.contextMenu.clickMenuItem('Search') + await comfyPage.nextFrame() // Search box opens with prefilled type filter based on link type (LATENT) await expect(comfyPage.searchBox.input).toBeVisible() @@ -928,7 +973,7 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { expect(await samplerOutput.getLinkCount()).toBe(1) // One of the VAEDecode nodes should have an incoming link on input[0] - const vaeNodes = await comfyPage.getNodeRefsByType('VAEDecode') + const vaeNodes = await comfyPage.nodeOps.getNodeRefsByType('VAEDecode') let linked = false for (const vae of vaeNodes) { const details = await getInputLinkDetails(comfyPage.page, vae.id, 0) @@ -945,9 +990,14 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { comfyPage, comfyMouse }) => { - await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'search box') + await comfyPage.settings.setSetting( + 'Comfy.LinkRelease.ActionShift', + 'search box' + ) - const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0] + const samplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler') + )[0] expect(samplerNode).toBeTruthy() const outputCenter = await getSlotCenter( @@ -980,7 +1030,7 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { const samplerOutput = await samplerNode.getOutput(0) expect(await samplerOutput.getLinkCount()).toBe(1) - const vaeNodes = await comfyPage.getNodeRefsByType('VAEDecode') + const vaeNodes = await comfyPage.nodeOps.getNodeRefsByType('VAEDecode') let linked = false for (const vae of vaeNodes) { const details = await getInputLinkDetails(comfyPage.page, vae.id, 0) @@ -999,24 +1049,28 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { comfyMouse }) => { // Setup workflow with a KSampler node - await comfyPage.executeCommand('Comfy.NewBlankWorkflow') - await comfyPage.waitForGraphNodes(0) - await comfyPage.executeCommand('Workspace.SearchBox.Toggle') + await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow') + await comfyPage.nodeOps.waitForGraphNodes(0) + await comfyPage.command.executeCommand('Workspace.SearchBox.Toggle') await comfyPage.nextFrame() await comfyPage.searchBox.fillAndSelectFirstNode('KSampler') - await comfyPage.waitForGraphNodes(1) + await comfyPage.nodeOps.waitForGraphNodes(1) // Convert the KSampler node to a subgraph - let ksamplerNode = (await comfyPage.getNodeRefsByType('KSampler'))?.[0] + let ksamplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler') + )?.[0] await comfyPage.vueNodes.selectNode(String(ksamplerNode.id)) - await comfyPage.executeCommand('Comfy.Graph.ConvertToSubgraph') + await comfyPage.command.executeCommand('Comfy.Graph.ConvertToSubgraph') // Enter the subgraph await comfyPage.vueNodes.enterSubgraph() await fitToViewInstant(comfyPage) // Get the KSampler node inside the subgraph - ksamplerNode = (await comfyPage.getNodeRefsByType('KSampler', true))?.[0] + ksamplerNode = ( + await comfyPage.nodeOps.getNodeRefsByType('KSampler', true) + )?.[0] const positiveInput = await ksamplerNode.getInput(1) const negativeInput = await ksamplerNode.getInput(2) @@ -1027,7 +1081,7 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => { true ) - const sourceSlot = await comfyPage.getSubgraphInputSlot() + const sourceSlot = await comfyPage.subgraph.getInputSlot() const calculatedSourcePos = await sourceSlot.getOpenSlotPosition() await comfyMouse.move(calculatedSourcePos) diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png index 0485e0302..2b08998a9 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png index e5b95630f..3946f61c8 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png index d47e5ece3..1394c6220 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png index e89433357..2e084e8e0 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png index d108f0803..b46314b4e 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png index 0f003c13e..8d7f9652d 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png index 67eaac039..fbaf84983 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png index 0f55f3a83..505275fd3 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts index 07b23e088..402b31466 100644 --- a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts +++ b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts @@ -7,9 +7,9 @@ import { fitToViewInstant } from '../../../../helpers/fitToView' test.describe('Vue Node Bring to Front', { tag: '@screenshot' }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) - await comfyPage.loadWorkflow('vueNodes/simple-triple') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.workflow.loadWorkflow('vueNodes/simple-triple') await comfyPage.vueNodes.waitForNodes() await fitToViewInstant(comfyPage) }) @@ -61,7 +61,7 @@ test.describe('Vue Node Bring to Front', { tag: '@screenshot' }, () => { if (!ksamplerHeader) throw new Error('KSampler header not found') // Drag KSampler on top of CLIP Text Encode - await comfyPage.dragAndDrop( + await comfyPage.canvasOps.dragAndDrop( { x: ksamplerHeader.x + 50, y: ksamplerHeader.y + 10 }, clipCenter ) @@ -108,7 +108,7 @@ test.describe('Vue Node Bring to Front', { tag: '@screenshot' }, () => { const vaeHeader = await comfyPage.page.getByText('VAE Decode').boundingBox() if (!vaeHeader) throw new Error('VAE Decode header not found') - await comfyPage.dragAndDrop( + await comfyPage.canvasOps.dragAndDrop( { x: vaeHeader.x + 50, y: vaeHeader.y + 10 }, { x: clipCenter.x - 50, y: clipCenter.y } ) diff --git a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts index fd74872e5..42f8557be 100644 --- a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts +++ b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts @@ -7,7 +7,7 @@ import type { Position } from '../../../../fixtures/types' test.describe('Vue Node Moving', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.vueNodes.waitForNodes() }) @@ -35,7 +35,7 @@ test.describe('Vue Node Moving', () => { async ({ comfyPage }) => { const loadCheckpointHeaderPos = await getLoadCheckpointHeaderPos(comfyPage) - await comfyPage.dragAndDrop(loadCheckpointHeaderPos, { + await comfyPage.canvasOps.dragAndDrop(loadCheckpointHeaderPos, { x: 256, y: 256 }) @@ -52,11 +52,11 @@ test.describe('Vue Node Moving', () => { { tag: '@screenshot' }, async ({ comfyPage }) => { // Disable minimap (gets in way of the node on small screens) - await comfyPage.setSetting('Comfy.Minimap.Visible', false) + await comfyPage.settings.setSetting('Comfy.Minimap.Visible', false) const loadCheckpointHeaderPos = await getLoadCheckpointHeaderPos(comfyPage) - await comfyPage.panWithTouch( + await comfyPage.canvasOps.panWithTouch( { x: 64, y: 64 diff --git a/browser_tests/tests/vueNodes/interactions/node/remove.spec.ts b/browser_tests/tests/vueNodes/interactions/node/remove.spec.ts index e7a610643..342b3fb2d 100644 --- a/browser_tests/tests/vueNodes/interactions/node/remove.spec.ts +++ b/browser_tests/tests/vueNodes/interactions/node/remove.spec.ts @@ -3,14 +3,14 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../../../../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Vue Nodes - Delete Key Interaction', () => { test.beforeEach(async ({ comfyPage }) => { // Enable Vue nodes rendering - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) - await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false) + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', false) await comfyPage.setup() }) @@ -24,7 +24,7 @@ test.describe('Vue Nodes - Delete Key Interaction', () => { expect(initialNodeCount).toBeGreaterThan(0) // Select all Vue nodes - await comfyPage.ctrlA() + await comfyPage.keyboard.selectAll() // Verify all Vue nodes are selected const selectedCount = await comfyPage.vueNodes.getSelectedNodeCount() @@ -83,7 +83,7 @@ test.describe('Vue Nodes - Delete Key Interaction', () => { test('Delete key does not delete node when typing in Vue node widgets', async ({ comfyPage }) => { - const initialNodeCount = await comfyPage.getGraphNodesCount() + const initialNodeCount = await comfyPage.nodeOps.getGraphNodesCount() // Find a text input widget in a Vue node const textWidget = comfyPage.page @@ -98,7 +98,7 @@ test.describe('Vue Nodes - Delete Key Interaction', () => { await textWidget.press('Delete') // Node count should remain the same - const finalNodeCount = await comfyPage.getGraphNodesCount() + const finalNodeCount = await comfyPage.nodeOps.getGraphNodesCount() expect(finalNodeCount).toBe(initialNodeCount) }) diff --git a/browser_tests/tests/vueNodes/interactions/node/rename.spec.ts b/browser_tests/tests/vueNodes/interactions/node/rename.spec.ts index 5cd6b2fea..942b642b2 100644 --- a/browser_tests/tests/vueNodes/interactions/node/rename.spec.ts +++ b/browser_tests/tests/vueNodes/interactions/node/rename.spec.ts @@ -2,11 +2,12 @@ import { comfyExpect as expect, comfyPageFixture as test } from '../../../../fixtures/ComfyPage' +import { TestIds } from '../../../../fixtures/selectors' test.describe('Vue Nodes Renaming', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false) - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', false) + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.setup() await comfyPage.vueNodes.waitForNodes() }) @@ -46,7 +47,9 @@ test.describe('Vue Nodes Renaming', () => { if (!nodeBbox) throw new Error('Node not found') await loadCheckpointNode.dblclick() - const editingTitleInput = comfyPage.page.getByTestId('node-title-input') + const editingTitleInput = comfyPage.page.getByTestId( + TestIds.node.titleInput + ) await expect(editingTitleInput).not.toBeVisible() }) }) diff --git a/browser_tests/tests/vueNodes/interactions/node/resize.spec.ts b/browser_tests/tests/vueNodes/interactions/node/resize.spec.ts index c8e015ffa..fc6108195 100644 --- a/browser_tests/tests/vueNodes/interactions/node/resize.spec.ts +++ b/browser_tests/tests/vueNodes/interactions/node/resize.spec.ts @@ -5,7 +5,7 @@ import { test.describe('Vue Node Resizing', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.vueNodes.waitForNodes() }) diff --git a/browser_tests/tests/vueNodes/interactions/node/select.spec.ts b/browser_tests/tests/vueNodes/interactions/node/select.spec.ts index 98b0a63f6..4541072e1 100644 --- a/browser_tests/tests/vueNodes/interactions/node/select.spec.ts +++ b/browser_tests/tests/vueNodes/interactions/node/select.spec.ts @@ -4,12 +4,12 @@ import { } from '../../../../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Vue Node Selection', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.vueNodes.waitForNodes() }) @@ -78,7 +78,7 @@ test.describe('Vue Node Selection', () => { const initialPos = await checkpointNodeHeader.boundingBox() if (!initialPos) throw new Error('Failed to get header position') - await comfyPage.dragAndDrop( + await comfyPage.canvasOps.dragAndDrop( { x: initialPos.x + 10, y: initialPos.y + 10 }, { x: initialPos.x + 100, y: initialPos.y + 100 } ) diff --git a/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts b/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts index fb9ff2872..0a88b3a97 100644 --- a/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts +++ b/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts @@ -8,10 +8,10 @@ const BYPASS_CLASS = /before:bg-bypass\/60/ test.describe('Vue Node Bypass', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') - await comfyPage.setSetting('Comfy.Minimap.Visible', false) - await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true) + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.Minimap.Visible', false) + await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', true) await comfyPage.vueNodes.waitForNodes() }) diff --git a/browser_tests/tests/vueNodes/nodeStates/collapse.spec.ts b/browser_tests/tests/vueNodes/nodeStates/collapse.spec.ts index 8e8e22995..bb3b722d6 100644 --- a/browser_tests/tests/vueNodes/nodeStates/collapse.spec.ts +++ b/browser_tests/tests/vueNodes/nodeStates/collapse.spec.ts @@ -5,9 +5,9 @@ import { test.describe('Vue Node Collapse', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false) - await comfyPage.setSetting('Comfy.EnableTooltips', true) - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', false) + await comfyPage.settings.setSetting('Comfy.EnableTooltips', true) + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.setup() await comfyPage.vueNodes.waitForNodes() }) diff --git a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts index 369c7e195..dfdd173b9 100644 --- a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts +++ b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts @@ -5,9 +5,9 @@ import { test.describe('Vue Node Custom Colors', { tag: '@screenshot' }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') - await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true) - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting('Comfy.Canvas.SelectionToolbox', true) + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.vueNodes.waitForNodes() }) @@ -31,7 +31,7 @@ test.describe('Vue Node Custom Colors', { tag: '@screenshot' }, () => { }) test('should load node colors from workflow', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('nodes/every_node_color') + await comfyPage.workflow.loadWorkflow('nodes/every_node_color') await expect(comfyPage.canvas).toHaveScreenshot( 'vue-node-custom-colors-dark-all-colors.png' ) @@ -40,8 +40,8 @@ test.describe('Vue Node Custom Colors', { tag: '@screenshot' }, () => { test('should show brightened node colors on light theme', async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.ColorPalette', 'light') - await comfyPage.loadWorkflow('nodes/every_node_color') + await comfyPage.settings.setSetting('Comfy.ColorPalette', 'light') + await comfyPage.workflow.loadWorkflow('nodes/every_node_color') await expect(comfyPage.canvas).toHaveScreenshot( 'vue-node-custom-colors-light-all-colors.png' ) diff --git a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-dark-all-colors-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-dark-all-colors-chromium-linux.png index ad7a79ae0..81d28ebc4 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-dark-all-colors-chromium-linux.png and b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-dark-all-colors-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/error.spec.ts b/browser_tests/tests/vueNodes/nodeStates/error.spec.ts index b8c718239..a9623039c 100644 --- a/browser_tests/tests/vueNodes/nodeStates/error.spec.ts +++ b/browser_tests/tests/vueNodes/nodeStates/error.spec.ts @@ -7,7 +7,7 @@ const ERROR_CLASS = /border-node-stroke-error/ test.describe('Vue Node Error', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.vueNodes.waitForNodes() }) @@ -15,7 +15,7 @@ test.describe('Vue Node Error', () => { comfyPage }) => { await comfyPage.setup() - await comfyPage.loadWorkflow('missing/missing_nodes') + await comfyPage.workflow.loadWorkflow('missing/missing_nodes') // Expect error state on missing unknown node const unknownNode = comfyPage.page.locator('[data-node-id]').filter({ @@ -28,7 +28,7 @@ test.describe('Vue Node Error', () => { comfyPage }) => { await comfyPage.setup() - await comfyPage.loadWorkflow('nodes/execution_error') + await comfyPage.workflow.loadWorkflow('nodes/execution_error') await comfyPage.runButton.click() const raiseErrorNode = comfyPage.vueNodes.getNodeByTitle('Raise Error') diff --git a/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts b/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts index e59d79cfd..b4b4fd8c4 100644 --- a/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts +++ b/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts @@ -8,7 +8,7 @@ const MUTE_OPACITY = '0.5' test.describe('Vue Node Mute', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.vueNodes.waitForNodes() }) diff --git a/browser_tests/tests/vueNodes/nodeStates/pin.spec.ts b/browser_tests/tests/vueNodes/nodeStates/pin.spec.ts index 27f1ad1ac..f8f174fae 100644 --- a/browser_tests/tests/vueNodes/nodeStates/pin.spec.ts +++ b/browser_tests/tests/vueNodes/nodeStates/pin.spec.ts @@ -8,7 +8,7 @@ const PIN_INDICATOR = '[data-testid="node-pin-indicator"]' test.describe('Vue Node Pin', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.vueNodes.waitForNodes() }) @@ -55,7 +55,7 @@ test.describe('Vue Node Pin', () => { // Try to drag the node const headerPos = await checkpointNodeHeader.boundingBox() if (!headerPos) throw new Error('Failed to get header position') - await comfyPage.dragAndDrop( + await comfyPage.canvasOps.dragAndDrop( { x: headerPos.x, y: headerPos.y }, { x: headerPos.x + 256, y: headerPos.y + 256 } ) @@ -71,7 +71,7 @@ test.describe('Vue Node Pin', () => { await comfyPage.page.keyboard.press(PIN_HOTKEY) // Try to drag the node again - await comfyPage.dragAndDrop( + await comfyPage.canvasOps.dragAndDrop( { x: headerPos.x, y: headerPos.y }, { x: headerPos.x + 256, y: headerPos.y + 256 } ) diff --git a/browser_tests/tests/vueNodes/widgets/int/integerWidget.spec.ts b/browser_tests/tests/vueNodes/widgets/int/integerWidget.spec.ts index ee6b6bbfb..90e1f17da 100644 --- a/browser_tests/tests/vueNodes/widgets/int/integerWidget.spec.ts +++ b/browser_tests/tests/vueNodes/widgets/int/integerWidget.spec.ts @@ -5,14 +5,14 @@ import { test.describe('Vue Integer Widget', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.setup() }) test('should be disabled and not allow changing value when link connected to slot', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('vueNodes/linked-int-widget') + await comfyPage.workflow.loadWorkflow('vueNodes/linked-int-widget') await comfyPage.vueNodes.waitForNodes() const seedWidget = comfyPage.vueNodes diff --git a/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts b/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts index 2ae178d22..c7186e2d6 100644 --- a/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts +++ b/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts @@ -5,7 +5,7 @@ import { test.describe('Vue Upload Widgets', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.vueNodes.waitForNodes() }) @@ -14,7 +14,7 @@ test.describe('Vue Upload Widgets', () => { { tag: '@screenshot' }, async ({ comfyPage }) => { await comfyPage.setup() - await comfyPage.loadWorkflow('widgets/all_load_widgets') + await comfyPage.workflow.loadWorkflow('widgets/all_load_widgets') await comfyPage.vueNodes.waitForNodes() await expect(comfyPage.canvas).toHaveScreenshot( diff --git a/browser_tests/tests/vueNodes/widgets/text/multilineStringWidget.spec.ts b/browser_tests/tests/vueNodes/widgets/text/multilineStringWidget.spec.ts index bb08232a2..8a81772a8 100644 --- a/browser_tests/tests/vueNodes/widgets/text/multilineStringWidget.spec.ts +++ b/browser_tests/tests/vueNodes/widgets/text/multilineStringWidget.spec.ts @@ -1,12 +1,12 @@ import { - type ComfyPage, comfyExpect as expect, comfyPageFixture as test } from '../../../../fixtures/ComfyPage' +import type { ComfyPage } from '../../../../fixtures/ComfyPage' test.describe('Vue Multiline String Widget', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.vueNodes.waitForNodes() }) diff --git a/browser_tests/tests/vueNodes/widgets/widgetReactivity.spec.ts b/browser_tests/tests/vueNodes/widgets/widgetReactivity.spec.ts index 6f3701c12..8a3548614 100644 --- a/browser_tests/tests/vueNodes/widgets/widgetReactivity.spec.ts +++ b/browser_tests/tests/vueNodes/widgets/widgetReactivity.spec.ts @@ -2,10 +2,11 @@ import { comfyExpect as expect, comfyPageFixture as test } from '../../../fixtures/ComfyPage' +import type { TestGraphAccess } from '../../../types/globals' test.describe('Vue Widget Reactivity', () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true) await comfyPage.vueNodes.waitForNodes() }) test('Should display added widgets', async ({ comfyPage }) => { @@ -13,18 +14,21 @@ test.describe('Vue Widget Reactivity', () => { 'css=[data-testid="node-body-4"] > .lg-node-widgets > div' ) await comfyPage.page.evaluate(() => { - const node = window['graph']._nodes_by_id['4'] - node.widgets.push(node.widgets[0]) + const graph = window.graph as TestGraphAccess + const node = graph._nodes_by_id['4'] + node.widgets!.push(node.widgets![0]) }) await expect(loadCheckpointNode).toHaveCount(2) await comfyPage.page.evaluate(() => { - const node = window['graph']._nodes_by_id['4'] - node.widgets[2] = node.widgets[0] + const graph = window.graph as TestGraphAccess + const node = graph._nodes_by_id['4'] + node.widgets![2] = node.widgets![0] }) await expect(loadCheckpointNode).toHaveCount(3) await comfyPage.page.evaluate(() => { - const node = window['graph']._nodes_by_id['4'] - node.widgets.splice(0, 0, node.widgets[0]) + const graph = window.graph as TestGraphAccess + const node = graph._nodes_by_id['4'] + node.widgets!.splice(0, 0, node.widgets![0]) }) await expect(loadCheckpointNode).toHaveCount(4) }) @@ -33,18 +37,21 @@ test.describe('Vue Widget Reactivity', () => { 'css=[data-testid="node-body-3"] > .lg-node-widgets > div' ) await comfyPage.page.evaluate(() => { - const node = window['graph']._nodes_by_id['3'] - node.widgets.pop() + const graph = window.graph as TestGraphAccess + const node = graph._nodes_by_id['3'] + node.widgets!.pop() }) await expect(loadCheckpointNode).toHaveCount(5) await comfyPage.page.evaluate(() => { - const node = window['graph']._nodes_by_id['3'] - node.widgets.length-- + const graph = window.graph as TestGraphAccess + const node = graph._nodes_by_id['3'] + node.widgets!.length-- }) await expect(loadCheckpointNode).toHaveCount(4) await comfyPage.page.evaluate(() => { - const node = window['graph']._nodes_by_id['3'] - node.widgets.splice(0, 1) + const graph = window.graph as TestGraphAccess + const node = graph._nodes_by_id['3'] + node.widgets!.splice(0, 1) }) await expect(loadCheckpointNode).toHaveCount(3) }) diff --git a/browser_tests/tests/widget.spec.ts b/browser_tests/tests/widget.spec.ts index a0dfb0a6e..fd8afb06a 100644 --- a/browser_tests/tests/widget.spec.ts +++ b/browser_tests/tests/widget.spec.ts @@ -1,33 +1,55 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +import { DefaultGraphPositions } from '../fixtures/constants/defaultGraphPositions' test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled') }) test.describe('Combo text widget', { tag: ['@screenshot', '@widget'] }, () => { test('Truncates text when resized', async ({ comfyPage }) => { - await comfyPage.resizeLoadCheckpointNode(0.2, 1) + await comfyPage.nodeOps.resizeNode( + DefaultGraphPositions.loadCheckpoint.pos, + DefaultGraphPositions.loadCheckpoint.size, + 0.2, + 1 + ) await expect(comfyPage.canvas).toHaveScreenshot( 'load-checkpoint-resized-min-width.png' ) await comfyPage.closeMenu() - await comfyPage.resizeKsamplerNode(0.2, 1) + await comfyPage.nodeOps.resizeNode( + DefaultGraphPositions.ksampler.pos, + DefaultGraphPositions.ksampler.size, + 0.2, + 1 + ) await expect(comfyPage.canvas).toHaveScreenshot( `ksampler-resized-min-width.png` ) }) test("Doesn't truncate when space still available", async ({ comfyPage }) => { - await comfyPage.resizeEmptyLatentNode(0.8, 0.8) + await comfyPage.nodeOps.resizeNode( + DefaultGraphPositions.emptyLatent.pos, + DefaultGraphPositions.emptyLatent.size, + 0.8, + 0.8 + ) await expect(comfyPage.canvas).toHaveScreenshot( 'empty-latent-resized-80-percent.png' ) }) test('Can revert to full text', async ({ comfyPage }) => { - await comfyPage.resizeLoadCheckpointNode(0.8, 1, true) + await comfyPage.nodeOps.resizeNode( + DefaultGraphPositions.loadCheckpoint.pos, + DefaultGraphPositions.loadCheckpoint.size, + 0.8, + 1, + true + ) await expect(comfyPage.canvas).toHaveScreenshot('resized-to-original.png') }) @@ -36,13 +58,15 @@ test.describe('Combo text widget', { tag: ['@screenshot', '@widget'] }, () => { }) => { const getComboValues = async () => comfyPage.page.evaluate(() => { - return window['app'].graph.nodes - .find((node) => node.title === 'Node With Optional Combo Input') - .widgets.find((widget) => widget.name === 'optional_combo_input') + return window + .app!.graph!.nodes.find( + (node) => node.title === 'Node With Optional Combo Input' + )! + .widgets!.find((widget) => widget.name === 'optional_combo_input')! .options.values }) - await comfyPage.loadWorkflow('inputs/optional_combo_input') + await comfyPage.workflow.loadWorkflow('inputs/optional_combo_input') const initialComboValues = await getComboValues() // Focus canvas @@ -52,16 +76,16 @@ test.describe('Combo text widget', { tag: ['@screenshot', '@widget'] }, () => { await comfyPage.page.keyboard.press('r') // Wait for nodes' widgets to be updated - await comfyPage.page.waitForTimeout(500) - - const refreshedComboValues = await getComboValues() - expect(refreshedComboValues).not.toEqual(initialComboValues) + await expect(async () => { + const refreshedComboValues = await getComboValues() + expect(refreshedComboValues).not.toEqual(initialComboValues) + }).toPass({ timeout: 5000 }) }) test('Should refresh combo values of nodes with v2 combo input spec', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('inputs/node_with_v2_combo_input') + await comfyPage.workflow.loadWorkflow('inputs/node_with_v2_combo_input') // click canvas to focus await comfyPage.page.mouse.click(400, 300) // press R to trigger refresh @@ -71,9 +95,12 @@ test.describe('Combo text widget', { tag: ['@screenshot', '@widget'] }, () => { await comfyPage.nextFrame() // get the combo widget's values const comboValues = await comfyPage.page.evaluate(() => { - return window['app'].graph.nodes - .find((node) => node.title === 'Node With V2 Combo Input') - .widgets.find((widget) => widget.name === 'combo_input').options.values + return window + .app!.graph!.nodes.find( + (node) => node.title === 'Node With V2 Combo Input' + )! + .widgets!.find((widget) => widget.name === 'combo_input')!.options + .values }) expect(comboValues).toEqual(['A', 'B']) }) @@ -81,9 +108,9 @@ test.describe('Combo text widget', { tag: ['@screenshot', '@widget'] }, () => { test.describe('Boolean widget', { tag: ['@screenshot', '@widget'] }, () => { test('Can toggle', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('widgets/boolean_widget') + await comfyPage.workflow.loadWorkflow('widgets/boolean_widget') await expect(comfyPage.canvas).toHaveScreenshot('boolean_widget.png') - const node = (await comfyPage.getFirstNodeRef())! + const node = (await comfyPage.nodeOps.getFirstNodeRef())! const widget = await node.getWidget(0) await widget.click() await expect(comfyPage.canvas).toHaveScreenshot( @@ -94,42 +121,42 @@ test.describe('Boolean widget', { tag: ['@screenshot', '@widget'] }, () => { test.describe('Slider widget', { tag: ['@screenshot', '@widget'] }, () => { test('Can drag adjust value', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('inputs/simple_slider') - const node = (await comfyPage.getFirstNodeRef())! + await comfyPage.workflow.loadWorkflow('inputs/simple_slider') + const node = (await comfyPage.nodeOps.getFirstNodeRef())! const widget = await node.getWidget(0) await comfyPage.page.evaluate(() => { - const widget = window['app'].graph.nodes[0].widgets[0] + const widget = window.app!.graph!.nodes[0].widgets![0] widget.callback = (value: number) => { - window['widgetValue'] = value + window.widgetValue = value } }) await widget.dragHorizontal(50) await expect(comfyPage.canvas).toHaveScreenshot('slider_widget_dragged.png') expect( - await comfyPage.page.evaluate(() => window['widgetValue']) + await comfyPage.page.evaluate(() => window.widgetValue) ).toBeDefined() }) }) test.describe('Number widget', { tag: ['@screenshot', '@widget'] }, () => { test('Can drag adjust value', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('widgets/seed_widget') + await comfyPage.workflow.loadWorkflow('widgets/seed_widget') - const node = (await comfyPage.getFirstNodeRef())! + const node = (await comfyPage.nodeOps.getFirstNodeRef())! const widget = await node.getWidget(0) await comfyPage.page.evaluate(() => { - const widget = window['app'].graph.nodes[0].widgets[0] + const widget = window.app!.graph!.nodes[0].widgets![0] widget.callback = (value: number) => { - window['widgetValue'] = value + window.widgetValue = value } }) await widget.dragHorizontal(50) await expect(comfyPage.canvas).toHaveScreenshot('seed_widget_dragged.png') expect( - await comfyPage.page.evaluate(() => window['widgetValue']) + await comfyPage.page.evaluate(() => window.widgetValue) ).toBeDefined() }) }) @@ -141,11 +168,11 @@ test.describe( test('Auto expand node when widget is added dynamically', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('nodes/single_ksampler') + await comfyPage.workflow.loadWorkflow('nodes/single_ksampler') await comfyPage.page.evaluate(() => { - window['graph'].nodes[0].addWidget('number', 'new_widget', 10) - window['graph'].setDirtyCanvas(true, true) + window.app!.graph!.nodes[0].addWidget('number', 'new_widget', 10, null) + window.app!.graph!.setDirtyCanvas(true, true) }) await expect(comfyPage.canvas).toHaveScreenshot( @@ -157,20 +184,20 @@ test.describe( test.describe('Image widget', { tag: ['@screenshot', '@widget'] }, () => { test('Can load image', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('widgets/load_image_widget') + await comfyPage.workflow.loadWorkflow('widgets/load_image_widget') await expect(comfyPage.canvas).toHaveScreenshot('load_image_widget.png') }) test('Can drag and drop image', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('widgets/load_image_widget') + await comfyPage.workflow.loadWorkflow('widgets/load_image_widget') // Get position of the load image node - const nodes = await comfyPage.getNodeRefsByType('LoadImage') + const nodes = await comfyPage.nodeOps.getNodeRefsByType('LoadImage') const loadImageNode = nodes[0] const { x, y } = await loadImageNode.getPosition() // Drag and drop image file onto the load image node - await comfyPage.dragAndDropFile('image32x32.webp', { + await comfyPage.dragDrop.dragAndDropFile('image32x32.webp', { dropPosition: { x, y } }) @@ -188,8 +215,8 @@ test.describe('Image widget', { tag: ['@screenshot', '@widget'] }, () => { test('Can change image by changing the filename combo value', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('widgets/load_image_widget') - const nodes = await comfyPage.getNodeRefsByType('LoadImage') + await comfyPage.workflow.loadWorkflow('widgets/load_image_widget') + const nodes = await comfyPage.nodeOps.getNodeRefsByType('LoadImage') const loadImageNode = nodes[0] // Click the combo widget used to select the image filename @@ -224,14 +251,14 @@ test.describe('Image widget', { tag: ['@screenshot', '@widget'] }, () => { image1.src = src const image2 = new Image() image2.src = src - const targetNode = graph.nodes[6] + const targetNode = graph!.nodes[6] targetNode.imgs = [image1, image2] targetNode.imageIndex = 1 - app.canvas.setDirty(true) + app!.canvas.setDirty(true) const x = targetNode.pos[0] + targetNode.size[0] - 41 - const y = targetNode.pos[1] + targetNode.widgets.at(-1).last_y + 30 - return app.canvasPosToClientPos([x, y]) + const y = targetNode.pos[1] + targetNode.widgets!.at(-1)!.last_y! + 30 + return app!.canvasPosToClientPos([x, y]) }) const clip = { x, y, width: 35, height: 35 } @@ -250,17 +277,17 @@ test.describe( test.skip('Shows preview of uploaded animated image', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('widgets/load_animated_webp') + await comfyPage.workflow.loadWorkflow('widgets/load_animated_webp') // Get position of the load animated webp node - const nodes = await comfyPage.getNodeRefsByType( + const nodes = await comfyPage.nodeOps.getNodeRefsByType( 'DevToolsLoadAnimatedImageTest' ) const loadAnimatedWebpNode = nodes[0] const { x, y } = await loadAnimatedWebpNode.getPosition() // Drag and drop image file onto the load animated webp node - await comfyPage.dragAndDropFile('animated_webp.webp', { + await comfyPage.dragDrop.dragAndDropFile('animated_webp.webp', { dropPosition: { x, y } }) @@ -279,17 +306,17 @@ test.describe( }) test('Can drag-and-drop animated webp image', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('widgets/load_animated_webp') + await comfyPage.workflow.loadWorkflow('widgets/load_animated_webp') // Get position of the load animated webp node - const nodes = await comfyPage.getNodeRefsByType( + const nodes = await comfyPage.nodeOps.getNodeRefsByType( 'DevToolsLoadAnimatedImageTest' ) const loadAnimatedWebpNode = nodes[0] const { x, y } = await loadAnimatedWebpNode.getPosition() // Drag and drop image file onto the load animated webp node - await comfyPage.dragAndDropFile('animated_webp.webp', { + await comfyPage.dragDrop.dragAndDropFile('animated_webp.webp', { dropPosition: { x, y }, waitForUpload: true }) @@ -301,23 +328,24 @@ test.describe( }) test('Can preview saved animated webp image', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('widgets/save_animated_webp') + await comfyPage.workflow.loadWorkflow('widgets/save_animated_webp') // Get position of the load animated webp node - const loadNodes = await comfyPage.getNodeRefsByType( + const loadNodes = await comfyPage.nodeOps.getNodeRefsByType( 'DevToolsLoadAnimatedImageTest' ) const loadAnimatedWebpNode = loadNodes[0] const { x, y } = await loadAnimatedWebpNode.getPosition() // Drag and drop image file onto the load animated webp node - await comfyPage.dragAndDropFile('animated_webp.webp', { + await comfyPage.dragDrop.dragAndDropFile('animated_webp.webp', { dropPosition: { x, y } }) await comfyPage.nextFrame() // Get the SaveAnimatedWEBP node - const saveNodes = await comfyPage.getNodeRefsByType('SaveAnimatedWEBP') + const saveNodes = + await comfyPage.nodeOps.getNodeRefsByType('SaveAnimatedWEBP') const saveAnimatedWebpNode = saveNodes[0] if (!saveAnimatedWebpNode) throw new Error('SaveAnimatedWEBP node not found') @@ -326,8 +354,8 @@ test.describe( await comfyPage.page.evaluate( ([loadId, saveId]) => { // Set the output of the SaveAnimatedWEBP node to equal the loader node's image - window['app'].nodeOutputs[saveId] = window['app'].nodeOutputs[loadId] - app.canvas.setDirty(true) + window.app!.nodeOutputs[saveId] = window.app!.nodeOutputs[loadId] + app!.canvas.setDirty(true) }, [loadAnimatedWebpNode.id, saveAnimatedWebpNode.id] ) @@ -340,7 +368,7 @@ test.describe( test.describe('Load audio widget', { tag: ['@screenshot', '@widget'] }, () => { test('Can load audio', async ({ comfyPage }) => { - await comfyPage.loadWorkflow('widgets/load_audio_widget') + await comfyPage.workflow.loadWorkflow('widgets/load_audio_widget') // Wait for the audio widget to be rendered in the DOM await comfyPage.page.waitForSelector('.comfy-audio', { state: 'attached' }) await comfyPage.nextFrame() @@ -353,13 +381,13 @@ test.describe('Unserialized widgets', { tag: '@widget' }, () => { comfyPage }) => { // Add workflow w/ LoadImage node, which contains file upload and image preview widgets (not serialized) - await comfyPage.loadWorkflow('widgets/load_image_widget') + await comfyPage.workflow.loadWorkflow('widgets/load_image_widget') // Move mouse and click to trigger the `graphEqual` check in `changeTracker.ts` await comfyPage.page.mouse.move(10, 10) await comfyPage.page.mouse.click(10, 10) // Expect the graph to not be modified - expect(await comfyPage.isCurrentWorkflowModified()).toBe(false) + expect(await comfyPage.workflow.isCurrentWorkflowModified()).toBe(false) }) }) diff --git a/browser_tests/tests/workflowTabThumbnail.spec.ts b/browser_tests/tests/workflowTabThumbnail.spec.ts index 1f4d9464d..bd8a5efe1 100644 --- a/browser_tests/tests/workflowTabThumbnail.spec.ts +++ b/browser_tests/tests/workflowTabThumbnail.spec.ts @@ -5,8 +5,11 @@ import type { ComfyPage } from '../fixtures/ComfyPage' test.describe('Workflow Tab Thumbnails', { tag: '@workflow' }, () => { test.beforeEach(async ({ comfyPage }) => { - await comfyPage.setSetting('Comfy.UseNewMenu', 'Top') - await comfyPage.setSetting('Comfy.Workflow.WorkflowTabsPosition', 'Topbar') + await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top') + await comfyPage.settings.setSetting( + 'Comfy.Workflow.WorkflowTabsPosition', + 'Topbar' + ) await comfyPage.setup() }) @@ -90,9 +93,9 @@ test.describe('Workflow Tab Thumbnails', { tag: '@workflow' }, () => { canvasArea!.x + canvasArea!.width - 100, 100 ) - await comfyPage.delay(300) // Wait for the popover to hide + await expect(comfyPage.page.locator('.workflow-popover-fade')).toBeHidden() - await comfyPage.rightClickCanvas(200, 200) + await comfyPage.canvasOps.rightClick(200, 200) await comfyPage.page.getByText('Add Node').click() await comfyPage.nextFrame() await comfyPage.page.getByText(category).click() diff --git a/browser_tests/tsconfig.json b/browser_tests/tsconfig.json index 46e5a9bcd..8e943db4b 100644 --- a/browser_tests/tsconfig.json +++ b/browser_tests/tsconfig.json @@ -5,7 +5,13 @@ "noEmit": true, "noUnusedLocals": true, "noUnusedParameters": true, - "resolveJsonModule": true + "resolveJsonModule": true, + "types": ["vite/client"] }, - "include": ["**/*.ts"] + "include": [ + "**/*.ts", + "../src/types/**/*.d.ts", + "../global.d.ts", + "types/**/*.d.ts" + ] } diff --git a/browser_tests/types/globals.d.ts b/browser_tests/types/globals.d.ts new file mode 100644 index 000000000..1d38a31a6 --- /dev/null +++ b/browser_tests/types/globals.d.ts @@ -0,0 +1,69 @@ +import type { LGraph } from '@/lib/litegraph/src/LGraph' +// eslint-disable-next-line unused-imports/no-unused-imports -- used in typeof +import type { LGraphBadge } from '@/lib/litegraph/src/LGraphBadge' +import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' +import type { LiteGraphGlobal } from '@/lib/litegraph/src/LiteGraphGlobal' +import type { ComfyApp } from '@/scripts/app' +import type { useWorkspaceStore } from '@/stores/workspaceStore' + +/** + * Helper type for accessing nodes by ID in browser tests. + * Provides typed access to graph internals without requiring `any`. + */ +export interface TestGraphAccess { + _nodes_by_id: Record +} + +interface AppReadiness { + featureFlagsReceived: boolean + apiInitialized: boolean + appInitialized: boolean +} + +interface CapturedMessages { + clientFeatureFlags: unknown + serverFeatureFlags: unknown +} + +declare global { + interface Window { + app?: ComfyApp + graph?: LGraph + LiteGraph?: LiteGraphGlobal + LGraphBadge?: typeof LGraphBadge + + // Test-specific globals used for assertions + foo?: boolean + TestCommand?: boolean + changeCount?: number + widgetValue?: unknown + + // Feature flags test globals + __capturedMessages?: CapturedMessages + __appReadiness?: AppReadiness + + /** + * WebSocket store used by test fixtures for mocking WebSocket connections. + * @see browser_tests/fixtures/ws.ts + */ + __ws__?: Record + } + + const app: ComfyApp | undefined + const graph: LGraph | undefined + const LiteGraph: LiteGraphGlobal | undefined + const LGraphBadge: typeof LGraphBadge | undefined +} + +/** + * Internal store type for browser test access. + * Used to access properties not exposed via the public ExtensionManager interface. + * + * @example + * ```ts + * await page.evaluate(() => { + * ;(window.app!.extensionManager as WorkspaceStore).workflow.syncWorkflows() + * }) + * ``` + */ +export type WorkspaceStore = ReturnType diff --git a/build/plugins/comfyAPIPlugin.ts b/build/plugins/comfyAPIPlugin.ts index 394914dbe..16135cbd7 100644 --- a/build/plugins/comfyAPIPlugin.ts +++ b/build/plugins/comfyAPIPlugin.ts @@ -9,7 +9,11 @@ interface ShimResult { const SKIP_WARNING_FILES = new Set(['scripts/app', 'scripts/api']) /** Files that will be removed in v1.34 */ -const DEPRECATED_FILES = ['scripts/ui', 'extensions/core/groupNode'] as const +const DEPRECATED_FILES = [ + 'scripts/ui', + 'extensions/core/groupNode', + 'extensions/core/nodeTemplates' +] as const function getWarningMessage( fileKey: string, diff --git a/docs/guidance/playwright.md b/docs/guidance/playwright.md index 7825249cf..4eefbc08d 100644 --- a/docs/guidance/playwright.md +++ b/docs/guidance/playwright.md @@ -14,6 +14,67 @@ See `docs/testing/*.md` for detailed patterns. - Prefer specific selectors (role, label, test-id) - Test across viewports +## Window Globals + +Browser tests access `window.app`, `window.graph`, and `window.LiteGraph` which are +optional in the main app types. In E2E tests, use non-null assertions (`!`): + +```typescript +window.app!.graph!.nodes +window.LiteGraph!.registered_node_types +``` + +This is the **only context** where non-null assertions are acceptable. + +**TODO:** Consolidate these references into a central utility (e.g., `getApp()`) that +performs proper runtime type checking, removing the need for scattered `!` assertions. + +## Type Assertions in E2E Tests + +E2E tests may use **specific** type assertions when needed, but **never** `as any`. + +### Acceptable Patterns + +```typescript +// ✅ Non-null assertions for window globals +window.app!.extensionManager + +// ✅ Specific type assertions with documentation +// Extensions can register arbitrary setting IDs +id: 'TestSetting' as TestSettingId + +// ✅ Test-local type helpers +type TestSettingId = keyof Settings +``` + +### Forbidden Patterns + +```typescript +// ❌ Never use `as any` +settings: testData as any + +// ❌ Never modify production types to satisfy test errors +// Don't add test settings to src/schemas/apiSchema.ts + +// ❌ Don't chain through unknown to bypass types +data as unknown as SomeType // Avoid; prefer explicit typings or helpers +``` + +### Accessing Internal State + +When tests need internal store properties (e.g., `.workflow`, `.focusMode`): + +```typescript +// ✅ Access stores directly in page.evaluate +await page.evaluate(() => { + const store = useWorkflowStore() + return store.activeWorkflow +}) + +// ❌ Don't change public API types to expose internals +// Keep app.extensionManager typed as ExtensionManager, not WorkspaceStore +``` + ## Test Tags Tags are respected by config: @@ -29,6 +90,6 @@ Tags are respected by config: ## Running Tests ```bash -pnpm test:browser # Run all E2E tests -pnpm test:browser -- --ui # Interactive UI mode +pnpm test:browser:local # Run all E2E tests +pnpm test:browser:local -- --ui # Interactive UI mode ``` diff --git a/docs/guidance/typescript.md b/docs/guidance/typescript.md index 9a6dd102b..17b2f3989 100644 --- a/docs/guidance/typescript.md +++ b/docs/guidance/typescript.md @@ -14,6 +14,28 @@ globs: - Type assertions are a last resort; they lead to brittle code - Avoid `@ts-expect-error` - fix the underlying issue instead +### Type Assertion Hierarchy + +When you must handle uncertain types, prefer these approaches in order: + +1. ✅ **No assertion** — Properly typed from the start +2. ✅ **Type narrowing** — `if ('prop' in obj)` or type guards +3. ⚠️ **Specific assertion** — `as SpecificType` when you truly know the type +4. ⚠️ **`unknown` with narrowing** — For genuinely unknown data +5. ❌ **`as any`** — FORBIDDEN + +### Zod Schema Rules + +- Never use `z.any()` — it disables validation and propagates `any` into types +- Use `z.unknown()` if the type is genuinely unknown, then narrow it +- Never add test-only settings/types to production schemas + +### Public API Contracts + +- Keep public API types stable (e.g., `ExtensionManager` interface) +- Don't expose internal implementation types (e.g., Pinia store internals) +- Reactive refs (`ComputedRef`) should be unwrapped before exposing + ## Utility Libraries - Use `es-toolkit` for utility functions (not lodash) diff --git a/package.json b/package.json index d4243e2a6..4cd32bb40 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "test:browser:local": "cross-env PLAYWRIGHT_LOCAL=1 pnpm test:browser", "test:unit": "nx run test", "typecheck": "vue-tsc --noEmit", + "typecheck:browser": "vue-tsc --project browser_tests/tsconfig.json", "typecheck:desktop": "nx run @comfyorg/desktop-ui:typecheck", "zipdist": "node scripts/zipdist.js", "clean": "nx reset" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a1ea5f0af..d742311b7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,8 +46,8 @@ catalogs: specifier: ^1.0.3 version: 1.0.3 '@playwright/test': - specifier: ^1.57.0 - version: 1.57.0 + specifier: ^1.58.1 + version: 1.58.1 '@primeuix/forms': specifier: 0.0.2 version: 0.0.2 @@ -527,7 +527,7 @@ importers: version: 22.2.6(@babel/traverse@7.28.5)(@zkochan/js-yaml@0.0.7)(eslint@9.39.1(jiti@2.6.1))(nx@22.2.6) '@nx/playwright': specifier: 'catalog:' - version: 22.2.6(@babel/traverse@7.28.5)(@playwright/test@1.57.0)(@zkochan/js-yaml@0.0.7)(eslint@9.39.1(jiti@2.6.1))(nx@22.2.6) + version: 22.2.6(@babel/traverse@7.28.5)(@playwright/test@1.58.1)(@zkochan/js-yaml@0.0.7)(eslint@9.39.1(jiti@2.6.1))(nx@22.2.6) '@nx/storybook': specifier: 'catalog:' version: 22.2.4(@babel/traverse@7.28.5)(@zkochan/js-yaml@0.0.7)(eslint@9.39.1(jiti@2.6.1))(nx@22.2.6)(storybook@10.1.9(@testing-library/dom@10.4.1)(prettier@3.7.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3) @@ -539,7 +539,7 @@ importers: version: 1.0.3(pinia@3.0.4(typescript@5.9.3)(vue@3.5.13(typescript@5.9.3))) '@playwright/test': specifier: 'catalog:' - version: 1.57.0 + version: 1.58.1 '@sentry/vite-plugin': specifier: 'catalog:' version: 4.6.0 @@ -2770,8 +2770,8 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@playwright/test@1.57.0': - resolution: {integrity: sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==} + '@playwright/test@1.58.1': + resolution: {integrity: sha512-6LdVIUERWxQMmUSSQi0I53GgCBYgM2RpGngCPY7hSeju+VrKjq3lvs7HpJoPbDiY5QM5EYRtRX5fvrinnMAz3w==} engines: {node: '>=18'} hasBin: true @@ -7020,13 +7020,13 @@ packages: pkg-types@2.3.0: resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} - playwright-core@1.57.0: - resolution: {integrity: sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==} + playwright-core@1.58.1: + resolution: {integrity: sha512-bcWzOaTxcW+VOOGBCQgnaKToLJ65d6AqfLVKEWvexyS3AS6rbXl+xdpYRMGSRBClPvyj44njOWoxjNdL/H9UNg==} engines: {node: '>=18'} hasBin: true - playwright@1.57.0: - resolution: {integrity: sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==} + playwright@1.58.1: + resolution: {integrity: sha512-+2uTZHxSCcxjvGc5C891LrS1/NlxglGxzrC4seZiVjcYVQfUa87wBL6rTDqzGjuoWNjnBzRqKmF6zRYGMvQUaQ==} engines: {node: '>=18'} hasBin: true @@ -10536,7 +10536,7 @@ snapshots: '@nx/nx-win32-x64-msvc@22.2.6': optional: true - '@nx/playwright@22.2.6(@babel/traverse@7.28.5)(@playwright/test@1.57.0)(@zkochan/js-yaml@0.0.7)(eslint@9.39.1(jiti@2.6.1))(nx@22.2.6)': + '@nx/playwright@22.2.6(@babel/traverse@7.28.5)(@playwright/test@1.58.1)(@zkochan/js-yaml@0.0.7)(eslint@9.39.1(jiti@2.6.1))(nx@22.2.6)': dependencies: '@nx/devkit': 22.2.6(nx@22.2.6) '@nx/eslint': 22.2.6(@babel/traverse@7.28.5)(@zkochan/js-yaml@0.0.7)(eslint@9.39.1(jiti@2.6.1))(nx@22.2.6) @@ -10544,7 +10544,7 @@ snapshots: minimatch: 9.0.3 tslib: 2.8.1 optionalDependencies: - '@playwright/test': 1.57.0 + '@playwright/test': 1.58.1 transitivePeerDependencies: - '@babel/traverse' - '@swc-node/register' @@ -10803,9 +10803,9 @@ snapshots: '@pkgr/core@0.2.9': {} - '@playwright/test@1.57.0': + '@playwright/test@1.58.1': dependencies: - playwright: 1.57.0 + playwright: 1.58.1 '@pnpm/config.env-replace@1.1.0': {} @@ -15637,11 +15637,11 @@ snapshots: exsolve: 1.0.8 pathe: 2.0.3 - playwright-core@1.57.0: {} + playwright-core@1.58.1: {} - playwright@1.57.0: + playwright@1.58.1: dependencies: - playwright-core: 1.57.0 + playwright-core: 1.58.1 optionalDependencies: fsevents: 2.3.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 68fd38f49..89e14f9cb 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -16,7 +16,7 @@ catalog: '@nx/storybook': 22.2.4 '@nx/vite': 22.2.6 '@pinia/testing': ^1.0.3 - '@playwright/test': ^1.57.0 + '@playwright/test': ^1.58.1 '@primeuix/forms': 0.0.2 '@primeuix/styled': 0.3.2 '@primeuix/utils': ^0.3.2 diff --git a/src/components/bottomPanel/tabs/shortcuts/ShortcutsList.vue b/src/components/bottomPanel/tabs/shortcuts/ShortcutsList.vue index d8992332b..3938facfb 100644 --- a/src/components/bottomPanel/tabs/shortcuts/ShortcutsList.vue +++ b/src/components/bottomPanel/tabs/shortcuts/ShortcutsList.vue @@ -1,6 +1,9 @@