diff --git a/.env_example b/.env_example
index 3ddf31fa2e..7bebc69367 100644
--- a/.env_example
+++ b/.env_example
@@ -12,6 +12,10 @@ DEV_SERVER_COMFYUI_URL=http://127.0.0.1:8188
# to ComfyUI launch script to serve the custom web version.
DEPLOY_COMFYUI_DIR=/home/ComfyUI/web
+# The directory containing the ComfyUI installation used to run Playwright tests.
+# If you aren't using a separate install for testing, point this to your regular install.
+TEST_COMFYUI_DIR=/home/ComfyUI
+
# The directory containing the ComfyUI_examples repo used to extract test workflows.
EXAMPLE_REPO_PATH=tests-ui/ComfyUI_examples
diff --git a/.prettierrc b/.prettierrc
index 9d774965dc..f36bb1cd21 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -2,5 +2,6 @@
"singleQuote": true,
"tabWidth": 2,
"semi": false,
- "trailingComma": "none"
+ "trailingComma": "none",
+ "printWidth": 80
}
\ No newline at end of file
diff --git a/README.md b/README.md
index 749af62074..b7f0b63127 100644
--- a/README.md
+++ b/README.md
@@ -33,11 +33,11 @@
### Nightly Release
-Nightly releases are published daily at [https://github.com/Comfy-Org/ComfyUI_frontend/releases](https://github.com/Comfy-Org/ComfyUI_frontend/releases).
+Nightly releases are published daily at [https://github.com/Comfy-Org/ComfyUI_frontend/releases](https://github.com/Comfy-Org/ComfyUI_frontend/releases).
To use the latest nightly release, add the following command line argument to your ComfyUI launch script:
-```
+```bat
--front-end-version Comfy-Org/ComfyUI_frontend@latest
```
@@ -62,6 +62,23 @@ There will be a 2-day feature freeze before each stable release. During this per
### Major features
+
+ v1.3.7: Keybinding customization
+
+## Basic UI
+
+
+## Reset button
+
+
+## Edit Keybinding
+
+
+
+[rec.webm](https://github.com/user-attachments/assets/a3984ed9-eb28-4d47-86c0-7fc3efc2b5d0)
+
+
+
v1.2.4: Node library sidebar tab
@@ -90,6 +107,32 @@ https://github.com/user-attachments/assets/4bbca3ee-318f-4cf0-be32-a5a5541066cf
### QoL changes
+
+ v1.3.6: **Litegraph** Toggle link visibility
+
+[rec.webm](https://github.com/user-attachments/assets/34e460ac-fbbc-44ef-bfbb-99a84c2ae2be)
+
+
+
+
+ v1.3.4: **Litegraph** Auto widget to input conversion
+
+Dropping a link of correct type on node widget will automatically convert the widget to input.
+
+[rec.webm](https://github.com/user-attachments/assets/15cea0b0-b225-4bec-af50-2cdb16dc46bf)
+
+
+
+
+ v1.3.4: **Litegraph** Canvas pan mode
+
+The canvas becomes readonly in pan mode. Pan mode is activated by clicking the pan mode button on the canvas menu
+or by holding the space key.
+
+[rec.webm](https://github.com/user-attachments/assets/c7872532-a2ac-44c1-9e7d-9e03b5d1a80b)
+
+
+
v1.3.1: **Litegraph** Shift drag link to create a new link
@@ -97,6 +140,13 @@ https://github.com/user-attachments/assets/4bbca3ee-318f-4cf0-be32-a5a5541066cf
+
+ v1.2.62: **Litegraph** Show optional input slots as donuts
+
+
+
+
+
v1.2.44: **Litegraph** Double click group title to edit
diff --git a/browser_tests/ComfyPage.ts b/browser_tests/ComfyPage.ts
index 8bc2cca078..7c4d13644b 100644
--- a/browser_tests/ComfyPage.ts
+++ b/browser_tests/ComfyPage.ts
@@ -575,6 +575,10 @@ export class ComfyPage {
await this.nextFrame()
}
+ async getVisibleToastCount() {
+ return await this.page.locator('.p-toast:visible').count()
+ }
+
async clickTextEncodeNode1() {
await this.canvas.click({
position: {
diff --git a/browser_tests/README.md b/browser_tests/README.md
index 1a58c89885..b63db0ddbd 100644
--- a/browser_tests/README.md
+++ b/browser_tests/README.md
@@ -5,7 +5,7 @@ This document outlines the setup and usage of Playwright for testing the ComfyUI
## WARNING
The browser tests will change the ComfyUI backend state, such as user settings and saved workflows.
-Please backup your ComfyUI data before running the tests locally.
+If `TEST_COMFYUI_DIR` in `.env` isn't set to your `(Comfy Path)/ComfyUI` directory, these changes won't be automatically restored.
## Setup
diff --git a/browser_tests/assets/old_workflow_converted_input.json b/browser_tests/assets/old_workflow_converted_input.json
new file mode 100644
index 0000000000..8734433b1a
--- /dev/null
+++ b/browser_tests/assets/old_workflow_converted_input.json
@@ -0,0 +1,128 @@
+{
+ "last_node_id": 2,
+ "last_link_id": 1,
+ "nodes": [
+ {
+ "id": 1,
+ "type": "ControlNetApplyAdvanced",
+ "pos": {
+ "0": 449,
+ "1": 204
+ },
+ "size": [
+ 340.20001220703125,
+ 166
+ ],
+ "flags": {},
+ "order": 1,
+ "mode": 0,
+ "inputs": [
+ {
+ "name": "positive",
+ "type": "CONDITIONING",
+ "link": null
+ },
+ {
+ "name": "negative",
+ "type": "CONDITIONING",
+ "link": null
+ },
+ {
+ "name": "control_net",
+ "type": "CONTROL_NET",
+ "link": null
+ },
+ {
+ "name": "image",
+ "type": "IMAGE",
+ "link": null
+ },
+ {
+ "name": "strength",
+ "type": "FLOAT",
+ "link": 1,
+ "widget": {
+ "name": "strength"
+ }
+ }
+ ],
+ "outputs": [
+ {
+ "name": "positive",
+ "type": "CONDITIONING",
+ "links": null
+ },
+ {
+ "name": "negative",
+ "type": "CONDITIONING",
+ "links": null
+ }
+ ],
+ "properties": {
+ "Node name for S&R": "ControlNetApplyAdvanced"
+ },
+ "widgets_values": [
+ 1,
+ 0,
+ 1
+ ]
+ },
+ {
+ "id": 2,
+ "type": "PrimitiveNode",
+ "pos": {
+ "0": 177,
+ "1": 265
+ },
+ "size": [
+ 210,
+ 82
+ ],
+ "flags": {},
+ "order": 0,
+ "mode": 0,
+ "inputs": [],
+ "outputs": [
+ {
+ "name": "FLOAT",
+ "type": "FLOAT",
+ "links": [
+ 1
+ ],
+ "widget": {
+ "name": "strength"
+ }
+ }
+ ],
+ "properties": {
+ "Run widget replace on values": false
+ },
+ "widgets_values": [
+ 1,
+ "fixed"
+ ]
+ }
+ ],
+ "links": [
+ [
+ 1,
+ 2,
+ 0,
+ 1,
+ 4,
+ "FLOAT"
+ ]
+ ],
+ "groups": [],
+ "config": {},
+ "extra": {
+ "ds": {
+ "scale": 1,
+ "offset": {
+ "0": 47.541666666666515,
+ "1": 186.9375
+ }
+ }
+ },
+ "version": 0.4
+}
\ No newline at end of file
diff --git a/browser_tests/dialog.spec.ts b/browser_tests/dialog.spec.ts
index 7b46f95259..5e2d601f26 100644
--- a/browser_tests/dialog.spec.ts
+++ b/browser_tests/dialog.spec.ts
@@ -95,3 +95,34 @@ test.describe('Missing models warning', () => {
await expect(folderSelect).not.toBeVisible()
})
})
+
+test.describe('Settings', () => {
+ test.afterEach(async ({ comfyPage }) => {
+ // Restore default setting value
+ await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', 1.1)
+ })
+
+ test('@mobile Should be visible on mobile', async ({ comfyPage }) => {
+ await comfyPage.page.keyboard.press('Control+,')
+ const searchBox = comfyPage.page.locator('.settings-content')
+ await expect(searchBox).toBeVisible()
+ })
+
+ test('Can open settings with hotkey', async ({ comfyPage }) => {
+ await comfyPage.page.keyboard.down('ControlOrMeta')
+ await comfyPage.page.keyboard.press(',')
+ await comfyPage.page.keyboard.up('ControlOrMeta')
+ const settingsLocator = comfyPage.page.locator('.settings-container')
+ await expect(settingsLocator).toBeVisible()
+ await comfyPage.page.keyboard.press('Escape')
+ await expect(settingsLocator).not.toBeVisible()
+ })
+
+ test('Can change canvas zoom speed setting', async ({ comfyPage }) => {
+ const maxSpeed = 2.5
+ await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', maxSpeed)
+ test.step('Setting should persist', async () => {
+ expect(await comfyPage.getSetting('Comfy.Graph.ZoomSpeed')).toBe(maxSpeed)
+ })
+ })
+})
diff --git a/browser_tests/globalSetup.ts b/browser_tests/globalSetup.ts
new file mode 100644
index 0000000000..86626480f2
--- /dev/null
+++ b/browser_tests/globalSetup.ts
@@ -0,0 +1,20 @@
+import { FullConfig } from '@playwright/test'
+import { backupPath } from './utils/backupUtils'
+import dotenv from 'dotenv'
+
+dotenv.config()
+
+export default function globalSetup(config: FullConfig) {
+ if (!process.env.CI) {
+ if (process.env.TEST_COMFYUI_DIR) {
+ backupPath([process.env.TEST_COMFYUI_DIR, 'user'])
+ backupPath([process.env.TEST_COMFYUI_DIR, 'models'], {
+ renameAndReplaceWithScaffolding: true
+ })
+ } else {
+ console.warn(
+ 'Set TEST_COMFYUI_DIR in .env to prevent user data (settings, workflows, etc.) from being overwritten'
+ )
+ }
+ }
+}
diff --git a/browser_tests/globalTeardown.ts b/browser_tests/globalTeardown.ts
new file mode 100644
index 0000000000..8ebb713396
--- /dev/null
+++ b/browser_tests/globalTeardown.ts
@@ -0,0 +1,12 @@
+import { FullConfig } from '@playwright/test'
+import { restorePath } from './utils/backupUtils'
+import dotenv from 'dotenv'
+
+dotenv.config()
+
+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/groupNode.spec.ts b/browser_tests/groupNode.spec.ts
index e24255ce04..c722454735 100644
--- a/browser_tests/groupNode.spec.ts
+++ b/browser_tests/groupNode.spec.ts
@@ -252,4 +252,20 @@ test.describe('Group Node', () => {
})
})
})
+
+ test.describe('Keybindings', () => {
+ test('Convert to group node, no selection', async ({ comfyPage }) => {
+ expect(await comfyPage.getVisibleToastCount()).toBe(0)
+ await comfyPage.page.keyboard.press('Alt+g')
+ await comfyPage.page.waitForTimeout(300)
+ expect(await comfyPage.getVisibleToastCount()).toBe(1)
+ })
+ test('Convert to group node, selected 1 node', async ({ comfyPage }) => {
+ expect(await comfyPage.getVisibleToastCount()).toBe(0)
+ await comfyPage.clickTextEncodeNode1()
+ await comfyPage.page.keyboard.press('Alt+g')
+ await comfyPage.page.waitForTimeout(300)
+ expect(await comfyPage.getVisibleToastCount()).toBe(1)
+ })
+ })
})
diff --git a/browser_tests/interaction.spec.ts b/browser_tests/interaction.spec.ts
index 743884e02d..d542e117ca 100644
--- a/browser_tests/interaction.spec.ts
+++ b/browser_tests/interaction.spec.ts
@@ -12,9 +12,19 @@ test.describe('Node Interaction', () => {
})
test.describe('Node Selection', () => {
- test.afterEach(async ({ comfyPage }) => {
- // Deselect all nodes
- await comfyPage.clickEmptySpace()
+ const multiSelectModifiers = ['Control', 'Shift', 'Meta']
+
+ multiSelectModifiers.forEach((modifier) => {
+ test(`Can add multiple nodes to selection using ${modifier}+Click`, async ({
+ comfyPage
+ }) => {
+ const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode')
+ for (const node of clipNodes) {
+ await node.click('title', { modifiers: [modifier] })
+ }
+ const selectedNodeCount = await comfyPage.getSelectedGraphNodesCount()
+ expect(selectedNodeCount).toBe(clipNodes.length)
+ })
})
test('Can highlight selected', async ({ comfyPage }) => {
@@ -497,15 +507,3 @@ test.describe('Load duplicate workflow', () => {
expect(await comfyPage.getGraphNodesCount()).toBe(1)
})
})
-
-test.describe('Menu interactions', () => {
- test('Can open settings with hotkey', async ({ comfyPage }) => {
- await comfyPage.page.keyboard.down('ControlOrMeta')
- await comfyPage.page.keyboard.press(',')
- await comfyPage.page.keyboard.up('ControlOrMeta')
- const settingsLocator = comfyPage.page.locator('.settings-container')
- await expect(settingsLocator).toBeVisible()
- await comfyPage.page.keyboard.press('Escape')
- await expect(settingsLocator).not.toBeVisible()
- })
-})
diff --git a/browser_tests/menu.spec.ts b/browser_tests/menu.spec.ts
index 9633002748..f55f16257a 100644
--- a/browser_tests/menu.spec.ts
+++ b/browser_tests/menu.spec.ts
@@ -416,10 +416,9 @@ test.describe('Menu', () => {
const tab = comfyPage.menu.workflowsTab
await tab.open()
- expect(await tab.getTopLevelSavedWorkflowNames()).toEqual([
- 'workflow1.json',
- 'workflow2.json'
- ])
+ expect(await tab.getTopLevelSavedWorkflowNames()).toEqual(
+ expect.arrayContaining(['workflow1.json', 'workflow2.json'])
+ )
})
test('Does not report warning when switching between opened workflows', async ({
@@ -525,17 +524,4 @@ test.describe('Menu', () => {
expect(await comfyPage.getSetting('Comfy.UseNewMenu')).toBe('Top')
})
})
-
- test('Can change canvas zoom speed setting', async ({ comfyPage }) => {
- const [defaultSpeed, maxSpeed] = [1.1, 2.5]
- expect(await comfyPage.getSetting('Comfy.Graph.ZoomSpeed')).toBe(
- defaultSpeed
- )
- await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', maxSpeed)
- expect(await comfyPage.getSetting('Comfy.Graph.ZoomSpeed')).toBe(maxSpeed)
- await comfyPage.page.reload()
- await comfyPage.setup()
- expect(await comfyPage.getSetting('Comfy.Graph.ZoomSpeed')).toBe(maxSpeed)
- await comfyPage.setSetting('Comfy.Graph.ZoomSpeed', defaultSpeed)
- })
})
diff --git a/browser_tests/nodeDisplay.spec.ts b/browser_tests/nodeDisplay.spec.ts
index bbbf98ae59..05d0157ce3 100644
--- a/browser_tests/nodeDisplay.spec.ts
+++ b/browser_tests/nodeDisplay.spec.ts
@@ -32,4 +32,16 @@ test.describe('Optional input', () => {
// If the node's multiline text widget is visible, then it was loaded successfully
expect(comfyPage.page.locator('.comfy-multiline-input')).toHaveCount(1)
})
+ test('Old workflow with converted input', async ({ comfyPage }) => {
+ await comfyPage.loadWorkflow('old_workflow_converted_input')
+ const node = await comfyPage.getNodeRefById('1')
+ const inputs = await node.getProperty('inputs')
+ 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()
+ })
})
diff --git a/browser_tests/utils/backupUtils.ts b/browser_tests/utils/backupUtils.ts
new file mode 100644
index 0000000000..0d2ae1f4dc
--- /dev/null
+++ b/browser_tests/utils/backupUtils.ts
@@ -0,0 +1,69 @@
+import path from 'path'
+import fs from 'fs-extra'
+
+type PathParts = readonly [string, ...string[]]
+
+const getBackupPath = (originalPath: string): string => `${originalPath}.bak`
+
+const resolvePathIfExists = (pathParts: PathParts): string | null => {
+ const resolvedPath = path.resolve(...pathParts)
+ if (!fs.pathExistsSync(resolvedPath)) {
+ console.warn(`Path not found: ${resolvedPath}`)
+ return null
+ }
+ return resolvedPath
+}
+
+const createScaffoldingCopy = (srcDir: string, destDir: string) => {
+ // Get all items (files and directories) in the source directory
+ const items = fs.readdirSync(srcDir, { withFileTypes: true })
+
+ for (const item of items) {
+ const srcPath = path.join(srcDir, item.name)
+ const destPath = path.join(destDir, item.name)
+
+ if (item.isDirectory()) {
+ // Create the corresponding directory in the destination
+ fs.ensureDirSync(destPath)
+
+ // Recursively copy the directory structure
+ createScaffoldingCopy(srcPath, destPath)
+ }
+ }
+}
+
+export function backupPath(
+ pathParts: PathParts,
+ { renameAndReplaceWithScaffolding = false } = {}
+) {
+ const originalPath = resolvePathIfExists(pathParts)
+ if (!originalPath) return
+
+ const backupPath = getBackupPath(originalPath)
+ try {
+ if (renameAndReplaceWithScaffolding) {
+ // Rename the original path and create scaffolding in its place
+ fs.moveSync(originalPath, backupPath)
+ createScaffoldingCopy(backupPath, originalPath)
+ } else {
+ // Create a copy of the original path
+ fs.copySync(originalPath, backupPath)
+ }
+ } catch (error) {
+ console.error(`Failed to backup ${originalPath} from ${backupPath}`, error)
+ }
+}
+
+export function restorePath(pathParts: PathParts) {
+ const originalPath = resolvePathIfExists(pathParts)
+ if (!originalPath) return
+
+ const backupPath = getBackupPath(originalPath)
+ if (!fs.pathExistsSync(backupPath)) return
+
+ try {
+ fs.moveSync(backupPath, originalPath, { overwrite: true })
+ } catch (error) {
+ console.error(`Failed to restore ${originalPath} from ${backupPath}`, error)
+ }
+}
diff --git a/index.html b/index.html
index 07f29df3e2..5e6f66d545 100644
--- a/index.html
+++ b/index.html
@@ -36,7 +36,6 @@
-