Compare commits

..

4 Commits

Author SHA1 Message Date
Chenlei Hu
5e6e34cfd3 Fix regex handling of folderName 2025-03-17 15:05:24 -04:00
Chenlei Hu
2a445f3f94 workaround placeholder filename 2025-03-17 14:49:21 -04:00
Chenlei Hu
f8dcb915aa nit 2025-03-17 14:12:49 -04:00
Chenlei Hu
11925ce345 Create folder support 2025-03-17 14:09:07 -04:00
597 changed files with 6506 additions and 27085 deletions

View File

@@ -8,15 +8,6 @@ const vue3CompositionApiBestPractices = [
"Use watch and watchEffect for side effects", "Use watch and watchEffect for side effects",
"Implement lifecycle hooks with onMounted, onUpdated, etc.", "Implement lifecycle hooks with onMounted, onUpdated, etc.",
"Utilize provide/inject for dependency injection", "Utilize provide/inject for dependency injection",
"Use vue 3.5 style of default prop declaration. Example:
const { nodes, showTotal = true } = defineProps<{
nodes: ApiNodeCost[]
showTotal?: boolean
}>()
",
"Organize vue component in <template> <script> <style> order",
] ]
// Folder structure // Folder structure
@@ -49,6 +40,4 @@ const additionalInstructions = `
7. Implement proper error handling 7. Implement proper error handling
8. Follow Vue 3 style guide and naming conventions 8. Follow Vue 3 style guide and naming conventions
9. Use Vite for fast development and building 9. Use Vite for fast development and building
10. Use vue-i18n in composition API for any string literals. Place new translation
entries in src/locales/en/main.json.
`; `;

View File

@@ -1,5 +1,4 @@
# Local development playwright target # Local development playwright target
# Note: Don't add a trailing / after the port
PLAYWRIGHT_TEST_URL=http://localhost:5173 PLAYWRIGHT_TEST_URL=http://localhost:5173
# PLAYWRIGHT_TEST_URL=http://localhost:8188 # PLAYWRIGHT_TEST_URL=http://localhost:8188

View File

@@ -30,7 +30,7 @@ jobs:
with: with:
repository: 'Comfy-Org/ComfyUI_devtools' repository: 'Comfy-Org/ComfyUI_devtools'
path: 'ComfyUI/custom_nodes/ComfyUI_devtools' path: 'ComfyUI/custom_nodes/ComfyUI_devtools'
ref: '49c8220be49120dbaff85f32813d854d6dff2d05' ref: '080e6d4af809a46852d1c4b7ed85f06e8a3a72be'
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:

2
.gitignore vendored
View File

@@ -38,7 +38,7 @@ tests-ui/workflows/examples
/playwright-report/ /playwright-report/
/blob-report/ /blob-report/
/playwright/.cache/ /playwright/.cache/
browser_tests/**/*-win32.png browser_tests/*/*-win32.png
.env .env

View File

@@ -9,8 +9,8 @@ module.exports = defineConfig({
entry: 'src/locales/en', entry: 'src/locales/en',
entryLocale: 'en', entryLocale: 'en',
output: 'src/locales', output: 'src/locales',
outputLocales: ['zh', 'ru', 'ja', 'ko', 'fr', 'es'], outputLocales: ['zh', 'ru', 'ja', 'ko', 'fr'],
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora. reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, controlnet, lora.
'latent' is the short form of 'latent space'. 'latent' is the short form of 'latent space'.
'mask' is in the context of image processing. 'mask' is in the context of image processing.
` `

View File

@@ -1,25 +0,0 @@
{
"recommendations": [
"austenc.tailwind-docs",
"bradlc.vscode-tailwindcss",
"davidanson.vscode-markdownlint",
"dbaeumer.vscode-eslint",
"eamodio.gitlens",
"esbenp.prettier-vscode",
"figma.figma-vscode-extension",
"github.vscode-github-actions",
"github.vscode-pull-request-github",
"hbenl.vscode-test-explorer",
"lokalise.i18n-ally",
"ms-playwright.playwright",
"vitest.explorer",
"vue.volar",
"sonarsource.sonarlint-vscode",
"deque-systems.vscode-axe-linter",
"kisstkondoros.vscode-codemetrics",
"donjayamanne.githistory",
"wix.vscode-import-cost",
"prograhammer.tslint-vue",
"antfu.vite"
]
}

View File

@@ -9,26 +9,15 @@ If `TEST_COMFYUI_DIR` in `.env` isn't set to your `(Comfy Path)/ComfyUI` directo
## Setup ## Setup
### ComfyUI devtools Clone <https://github.com/Comfy-Org/ComfyUI_devtools> to your `custom_nodes` directory.
Clone <https://github.com/Comfy-Org/ComfyUI_devtools> to your `custom_nodes` directory. ComfyUI_devtools adds additional API endpoints and nodes to ComfyUI for browser testing.
_ComfyUI_devtools adds additional API endpoints and nodes to ComfyUI for browser testing._
### Node.js & Playwright Prerequisites
Ensure you have Node.js v20 or later installed. Then, set up the Chromium test driver: Ensure you have Node.js v20 or later installed. Then, set up the Chromium test driver:
```bash ```bash
npx playwright install chromium --with-deps npx playwright install chromium --with-deps
``` ```
### Environment Variables
Ensure the environment variables in `.env` are set correctly according to your setup.
The `.env` file will not exist until you create it yourself.
A template with helpful information can be found in `.env_example`.
### Multiple Tests
If you are running Playwright tests in parallel or running the same test multiple times, the flag `--multi-user` must be added to the main ComfyUI process.
## Running Tests ## Running Tests
There are two ways to run the tests: There are two ways to run the tests:
@@ -45,6 +34,8 @@ There are two ways to run the tests:
``` ```
This opens a user interface where you can select specific tests to run and inspect the test execution timeline. This opens a user interface where you can select specific tests to run and inspect the test execution timeline.
To run the same test multiple times in Playwright's UI mode, you must launch the main ComfyUI process with the `--multi-user` flag.
![Playwright UI Mode](https://github.com/user-attachments/assets/6a1ebef0-90eb-4157-8694-f5ee94d03755) ![Playwright UI Mode](https://github.com/user-attachments/assets/6a1ebef0-90eb-4157-8694-f5ee94d03755)
## Screenshot Expectations ## Screenshot Expectations

View File

@@ -1,9 +1,9 @@
import type { Response } from '@playwright/test' import type { Response } from '@playwright/test'
import { expect, mergeTests } from '@playwright/test' import { expect, mergeTests } from '@playwright/test'
import type { StatusWsMessage } from '../../src/schemas/apiSchema.ts' import type { StatusWsMessage } from '../src/schemas/apiSchema.ts'
import { comfyPageFixture } from '../fixtures/ComfyPage.ts' import { comfyPageFixture } from './fixtures/ComfyPage'
import { webSocketFixture } from '../fixtures/ws.ts' import { webSocketFixture } from './fixtures/ws.ts'
const test = mergeTests(comfyPageFixture, webSocketFixture) const test = mergeTests(comfyPageFixture, webSocketFixture)

View File

@@ -1,126 +0,0 @@
{
"id": "51b9b184-770d-40ac-a478-8cc31667ff23",
"revision": 0,
"last_node_id": 5,
"last_link_id": 3,
"nodes": [
{
"id": 4,
"type": "KSampler",
"pos": [
867.4669799804688,
347.22369384765625
],
"size": [
315,
262
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": null
},
{
"name": "positive",
"type": "CONDITIONING",
"link": null
},
{
"name": "negative",
"type": "CONDITIONING",
"link": null
},
{
"name": "latent_image",
"type": "LATENT",
"link": null
},
{
"name": "steps",
"type": "INT",
"widget": {
"name": "steps"
},
"link": 3
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": null
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
0,
"randomize",
20,
8,
"euler",
"normal",
1
]
},
{
"id": 5,
"type": "PrimitiveInt",
"pos": [
443.0852355957031,
441.131591796875
],
"size": [
315,
82
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "INT",
"type": "INT",
"links": [
3
]
}
],
"properties": {
"Node name for S&R": "PrimitiveInt"
},
"widgets_values": [
0,
"randomize"
]
}
],
"links": [
[
3,
5,
0,
4,
5,
"INT"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1.9487171000000016,
"offset": [
-325.57196748514497,
-168.13150517966463
]
}
},
"version": 0.4
}

View File

@@ -1,53 +0,0 @@
{
"id": "9bcb9451-8319-492a-88d4-fb711d8c3d25",
"revision": 0,
"last_node_id": 6,
"last_link_id": 0,
"nodes": [
{
"id": 6,
"type": "DevToolsNodeWithDefaultInput",
"pos": [
8.39722728729248,
29.727279663085938
],
"size": [
315,
82
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"name": "float_input",
"shape": 7,
"type": "FLOAT",
"link": null
}
],
"outputs": [],
"properties": {
"Node name for S&R": "DevToolsNodeWithDefaultInput"
},
"widgets_values": [
0,
1,
0
]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 2.1600300525920346,
"offset": [
63.071794466403446,
75.18055335968394
]
}
},
"version": 0.4
}

View File

@@ -1,82 +0,0 @@
{
"last_node_id": 9,
"last_link_id": 13,
"nodes": [
{
"id": 3,
"type": "KSampler",
"pos": [
0,
30
],
"size": {
"0": 315,
"1": 262
},
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": null
},
{
"name": "positive",
"type": "CONDITIONING",
"link": null
},
{
"name": "negative",
"type": "CONDITIONING",
"link": null
},
{
"name": "latent_image",
"type": "LATENT",
"link": null
} ,
{
"name": "dynamic_input",
"type": "FLOAT",
"link": null,
"_meta": "Dynamically added input via frontend JS logic"
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
156680208700286,
"randomize",
20,
8,
"euler",
"normal",
1
]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [
0,
0
]
}
},
"version": 0.4
}

View File

@@ -1,74 +0,0 @@
{
"id": "51b9b184-770d-40ac-a478-8cc31667ff23",
"revision": 0,
"last_node_id": 2,
"last_link_id": 1,
"nodes": [
{
"id": 1,
"type": "CLIPTextEncode",
"pos": [904, 466],
"size": [400, 200],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "text",
"type": "STRING",
"widget": {
"name": "text"
},
"link": 1
},
{
"name": "clip",
"type": "CLIP",
"link": null
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": null
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [""]
},
{
"id": 2,
"type": "PrimitiveString",
"pos": [556.8589477539062, 472.94342041015625],
"size": [315, 58],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": [1]
}
],
"properties": {
"Node name for S&R": "PrimitiveString"
},
"widgets_values": ["foo"]
}
],
"links": [[1, 2, 0, 1, 0, "STRING"]],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1.7715610000000013,
"offset": [-388.521484375, -162.31336975097656]
}
},
"version": 0.4
}

View File

@@ -51,10 +51,7 @@
0.85, 0.85,
false, false,
false, false,
"", ""
{
"foo": "bar"
}
] ]
} }
], ],

View File

@@ -1,104 +0,0 @@
{
"last_node_id": 2,
"last_link_id": 1,
"nodes": [
{
"id": 2,
"type": "KSampler",
"pos": {
"0": 304.3653259277344,
"1": 42.15586471557617
},
"size": [
315,
262
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": null
},
{
"name": "positive",
"type": "CONDITIONING",
"link": null
},
{
"name": "negative",
"type": "CONDITIONING",
"link": null
},
{
"name": "latent_image",
"type": "LATENT",
"link": null
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": null,
"shape": 3
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
0,
"randomize",
20,
8,
"euler",
"normal",
1
]
},
{
"id": 1,
"type": "PrimitiveInt",
"pos": {
"0": 14,
"1": 43
},
"size": [
203.1999969482422,
40.368401303242536
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "value",
"type": "INT",
"links": [],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "Int"
},
"widgets_values": [10]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [
0,
0
]
}
},
"version": 0.4
}

Binary file not shown.

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test' import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage' import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Browser tab title', () => { test.describe('Browser tab title', () => {
test.describe('Beta Menu', () => { test.describe('Beta Menu', () => {

View File

@@ -2,7 +2,7 @@ import {
ComfyPage, ComfyPage,
comfyExpect as expect, comfyExpect as expect,
comfyPageFixture as test comfyPageFixture as test
} from '../fixtures/ComfyPage' } from './fixtures/ComfyPage'
async function beforeChange(comfyPage: ComfyPage) { async function beforeChange(comfyPage: ComfyPage) {
await comfyPage.page.evaluate(() => { await comfyPage.page.evaluate(() => {

View File

@@ -1,7 +1,7 @@
import { expect } from '@playwright/test' import { expect } from '@playwright/test'
import type { Palette } from '../../src/schemas/colorPaletteSchema' import type { Palette } from '../src/schemas/colorPaletteSchema'
import { comfyPageFixture as test } from '../fixtures/ComfyPage' import { comfyPageFixture as test } from './fixtures/ComfyPage'
const customColorPalettes: Record<string, Palette> = { const customColorPalettes: Record<string, Palette> = {
obsidian: { obsidian: {

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test' import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage' import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Keybindings', () => { test.describe('Keybindings', () => {
test('Should execute command', async ({ comfyPage }) => { test('Should execute command', async ({ comfyPage }) => {
@@ -32,7 +32,7 @@ test.describe('Keybindings', () => {
}) })
await comfyPage.executeCommand('TestCommand') await comfyPage.executeCommand('TestCommand')
expect(await comfyPage.getToastErrorCount()).toBe(1) await expect(comfyPage.page.locator('.p-toast')).toBeVisible()
}) })
test('Should handle async command errors', async ({ comfyPage }) => { test('Should handle async command errors', async ({ comfyPage }) => {
@@ -45,6 +45,6 @@ test.describe('Keybindings', () => {
}) })
await comfyPage.executeCommand('TestCommand') await comfyPage.executeCommand('TestCommand')
expect(await comfyPage.getToastErrorCount()).toBe(1) await expect(comfyPage.page.locator('.p-toast')).toBeVisible()
}) })
}) })

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test' import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage' import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Copy Paste', () => { test.describe('Copy Paste', () => {
test('Can copy and paste node', async ({ comfyPage }) => { test('Can copy and paste node', async ({ comfyPage }) => {

View File

@@ -1,7 +1,7 @@
import { Locator, expect } from '@playwright/test' import { Locator, expect } from '@playwright/test'
import type { Keybinding } from '../../src/schemas/keyBindingSchema' import type { Keybinding } from '../src/schemas/keyBindingSchema'
import { comfyPageFixture as test } from '../fixtures/ComfyPage' import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Load workflow warning', () => { test.describe('Load workflow warning', () => {
test('Should display a warning when loading a workflow with missing nodes', async ({ test('Should display a warning when loading a workflow with missing nodes', async ({
@@ -309,35 +309,3 @@ test.describe('Feedback dialog', () => {
await expect(feedbackHeader).not.toBeVisible() await expect(feedbackHeader).not.toBeVisible()
}) })
}) })
test.describe('Error dialog', () => {
test('Should display an error dialog when graph configure fails', async ({
comfyPage
}) => {
await comfyPage.page.evaluate(() => {
const graph = window['graph']
graph.configure = () => {
throw new Error('Error on configure!')
}
})
await comfyPage.loadWorkflow('default')
const errorDialog = comfyPage.page.locator('.comfy-error-report')
await expect(errorDialog).toBeVisible()
})
test('Should display an error dialog when prompt execution fails', async ({
comfyPage
}) => {
await comfyPage.page.evaluate(async () => {
const app = window['app']
app.api.queuePrompt = () => {
throw new Error('Error on queuePrompt!')
}
await app.queuePrompt(0)
})
const errorDialog = comfyPage.page.locator('.comfy-error-report')
await expect(errorDialog).toBeVisible()
})
})

View File

@@ -1,12 +1,12 @@
import { expect } from '@playwright/test' import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage' import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('DOM Widget', () => { test.describe('DOM Widget', () => {
test('Collapsed multiline textarea is not visible', async ({ comfyPage }) => { test('Collapsed multiline textarea is not visible', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('collapsed_multiline') await comfyPage.loadWorkflow('collapsed_multiline')
const textareaWidget = comfyPage.page.locator('.comfy-multiline-input')
await expect(textareaWidget).not.toBeVisible() expect(comfyPage.page.locator('.comfy-multiline-input')).not.toBeVisible()
}) })
test('Multiline textarea correctly collapses', async ({ comfyPage }) => { test('Multiline textarea correctly collapses', async ({ comfyPage }) => {

View File

@@ -1,7 +1,7 @@
import { expect } from '@playwright/test' import { expect } from '@playwright/test'
import { SettingParams } from '../../src/types/settingTypes' import { SettingParams } from '../src/types/settingTypes'
import { comfyPageFixture as test } from '../fixtures/ComfyPage' import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Topbar commands', () => { test.describe('Topbar commands', () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {

View File

@@ -214,10 +214,6 @@ export class ComfyPage {
`Failed to setup workflows directory: ${await resp.text()}` `Failed to setup workflows directory: ${await resp.text()}`
) )
} }
await this.page.evaluate(async () => {
await window['app'].extensionManager.workflow.syncWorkflows()
})
} }
async setupUser(username: string) { async setupUser(username: string) {
@@ -412,7 +408,7 @@ export class ComfyPage {
} }
async getVisibleToastCount() { async getVisibleToastCount() {
return await this.page.locator('.p-toast-message:visible').count() return await this.page.locator('.p-toast:visible').count()
} }
async clickTextEncodeNode1() { async clickTextEncodeNode1() {
@@ -463,14 +459,7 @@ export class ComfyPage {
await this.nextFrame() await this.nextFrame()
} }
async dragAndDropFile( async dragAndDropFile(fileName: string) {
fileName: string,
options: {
dropPosition?: Position
} = {}
) {
const { dropPosition = { x: 100, y: 100 } } = options
const filePath = this.assetPath(fileName) const filePath = this.assetPath(fileName)
// Read the file content // Read the file content
@@ -482,63 +471,38 @@ export class ComfyPage {
if (fileName.endsWith('.webp')) return 'image/webp' if (fileName.endsWith('.webp')) return 'image/webp'
if (fileName.endsWith('.webm')) return 'video/webm' if (fileName.endsWith('.webm')) return 'video/webm'
if (fileName.endsWith('.json')) return 'application/json' if (fileName.endsWith('.json')) return 'application/json'
if (fileName.endsWith('.glb')) return 'model/gltf-binary'
return 'application/octet-stream' return 'application/octet-stream'
} }
const fileType = getFileType(fileName) const fileType = getFileType(fileName)
await this.page.evaluate( await this.page.evaluate(
async ({ buffer, fileName, fileType, dropPosition }) => { async ({ buffer, fileName, fileType }) => {
const file = new File([new Uint8Array(buffer)], fileName, { const file = new File([new Uint8Array(buffer)], fileName, {
type: fileType type: fileType
}) })
const dataTransfer = new DataTransfer() const dataTransfer = new DataTransfer()
dataTransfer.items.add(file) dataTransfer.items.add(file)
const targetElement = document.elementFromPoint( const dropEvent = new DragEvent('drop', {
dropPosition.x,
dropPosition.y
)
if (!targetElement) {
console.error('No element found at drop position:', dropPosition)
return { success: false, error: 'No element at position' }
}
const eventOptions = {
bubbles: true, bubbles: true,
cancelable: true, cancelable: true,
dataTransfer, dataTransfer
clientX: dropPosition.x, })
clientY: dropPosition.y
}
const dragOverEvent = new DragEvent('dragover', eventOptions)
const dropEvent = new DragEvent('drop', eventOptions)
Object.defineProperty(dropEvent, 'preventDefault', { Object.defineProperty(dropEvent, 'preventDefault', {
value: () => {}, value: () => {},
writable: false writable: false
}) })
Object.defineProperty(dropEvent, 'stopPropagation', { Object.defineProperty(dropEvent, 'stopPropagation', {
value: () => {}, value: () => {},
writable: false writable: false
}) })
targetElement.dispatchEvent(dragOverEvent) document.dispatchEvent(dropEvent)
targetElement.dispatchEvent(dropEvent)
return {
success: true,
targetInfo: {
tagName: targetElement.tagName,
id: targetElement.id,
classList: Array.from(targetElement.classList)
}
}
}, },
{ buffer: [...new Uint8Array(buffer)], fileName, fileType, dropPosition } { buffer: [...new Uint8Array(buffer)], fileName, fileType }
) )
await this.nextFrame() await this.nextFrame()
@@ -589,20 +553,11 @@ export class ComfyPage {
await this.dragAndDrop(this.clipTextEncodeNode1InputSlot, this.emptySpace) await this.dragAndDrop(this.clipTextEncodeNode1InputSlot, this.emptySpace)
} }
async connectEdge( async connectEdge() {
options: { await this.dragAndDrop(
reverse?: boolean this.loadCheckpointNodeClipOutputSlot,
} = {} this.clipTextEncodeNode1InputSlot
) { )
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() { async adjustWidgetValue() {

View File

@@ -81,7 +81,7 @@ export class NodeWidgetReference {
if (!widget) throw new Error(`Widget ${index} not found.`) if (!widget) throw new Error(`Widget ${index} not found.`)
const [x, y, w, h] = node.getBounding() const [x, y, w, h] = node.getBounding()
return window['app'].canvasPosToClientPos([ return window['app'].canvas.ds.convertOffsetToCanvas([
x + w / 2, x + w / 2,
y + window['LiteGraph']['NODE_TITLE_HEIGHT'] + widget.last_y + 1 y + window['LiteGraph']['NODE_TITLE_HEIGHT'] + widget.last_y + 1
]) ])
@@ -94,36 +94,6 @@ export class NodeWidgetReference {
} }
} }
/**
* @returns The position of the widget's associated socket
*/
async getSocketPosition(): Promise<Position> {
const pos: [number, number] = await this.node.comfyPage.page.evaluate(
([id, index]) => {
const node = window['app'].graph.getNodeById(id)
if (!node) throw new Error(`Node ${id} not found.`)
const widget = node.widgets[index]
if (!widget) throw new Error(`Widget ${index} not found.`)
const slot = node.inputs.find(
(slot) => slot.widget?.name === widget.name
)
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']
])
},
[this.node.id, this.index] as const
)
return {
x: pos[0],
y: pos[1]
}
}
async click() { async click() {
await this.node.comfyPage.canvas.click({ await this.node.comfyPage.canvas.click({
position: await this.getPosition() position: await this.getPosition()
@@ -145,20 +115,8 @@ export class NodeWidgetReference {
} }
) )
} }
async getValue() {
return await this.node.comfyPage.page.evaluate(
([id, index]) => {
const node = window['app'].graph.getNodeById(id)
if (!node) throw new Error(`Node ${id} not found.`)
const widget = node.widgets[index]
if (!widget) throw new Error(`Widget ${index} not found.`)
return widget.value
},
[this.node.id, this.index] as const
)
}
} }
export class NodeReference { export class NodeReference {
constructor( constructor(
readonly id: NodeId, readonly id: NodeId,
@@ -280,7 +238,7 @@ export class NodeReference {
const targetWidget = await targetNode.getWidget(targetWidgetIndex) const targetWidget = await targetNode.getWidget(targetWidgetIndex)
await this.comfyPage.dragAndDrop( await this.comfyPage.dragAndDrop(
await originSlot.getPosition(), await originSlot.getPosition(),
await targetWidget.getSocketPosition() await targetWidget.getPosition()
) )
return originSlot return originSlot
} }

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test' import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage' import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Graph Canvas Menu', () => { test.describe('Graph Canvas Menu', () => {
test.beforeEach(async ({ comfyPage }) => { test.beforeEach(async ({ comfyPage }) => {

View File

@@ -1,7 +1,7 @@
import { expect } from '@playwright/test' import { expect } from '@playwright/test'
import { ComfyPage, comfyPageFixture as test } from '../fixtures/ComfyPage' import { ComfyPage, comfyPageFixture as test } from './fixtures/ComfyPage'
import type { NodeReference } from '../fixtures/utils/litegraphUtils' import type { NodeReference } from './fixtures/utils/litegraphUtils'
test.describe('Group Node', () => { test.describe('Group Node', () => {
test.describe('Node library sidebar', () => { test.describe('Node library sidebar', () => {

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test' import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage' import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Item Interaction', () => { test.describe('Item Interaction', () => {
test('Can select/delete all items', async ({ comfyPage }) => { test('Can select/delete all items', async ({ comfyPage }) => {
@@ -91,20 +91,15 @@ test.describe('Node Interaction', () => {
await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'no action') await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'no action')
}) })
// Test both directions of edge connection. test('Can disconnect/connect edge', async ({ comfyPage }) => {
;[{ reverse: false }, { reverse: true }].forEach(({ reverse }) => { await comfyPage.disconnectEdge()
test(`Can disconnect/connect edge ${reverse ? 'reverse' : 'normal'}`, async ({ await expect(comfyPage.canvas).toHaveScreenshot('disconnected-edge.png')
comfyPage await comfyPage.connectEdge()
}) => { // Move mouse to empty area to avoid slot highlight.
await comfyPage.disconnectEdge() await comfyPage.moveMouseToEmptyArea()
await expect(comfyPage.canvas).toHaveScreenshot('disconnected-edge.png') // Litegraph renders edge with a slight offset.
await comfyPage.connectEdge({ reverse }) await expect(comfyPage.canvas).toHaveScreenshot('default.png', {
// Move mouse to empty area to avoid slot highlight. maxDiffPixels: 50
await comfyPage.moveMouseToEmptyArea()
// Litegraph renders edge with a slight offset.
await expect(comfyPage.canvas).toHaveScreenshot('default.png', {
maxDiffPixels: 50
})
}) })
}) })

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

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