Compare commits

...

575 Commits

Author SHA1 Message Date
filtered
782d93a7a0 Add awaits to various tests 2024-11-15 01:28:48 +11:00
filtered
7be14c5189 Add nodeTemplate tests
Test failure confirmed when links are not connected
2024-11-15 01:28:18 +11:00
Chenlei Hu
ee5c127146 1.3.43 (#1536) 2024-11-13 19:01:44 -05:00
Chenlei Hu
acba6097e0 Replace electron API mocks with actual electron API impl (#1535)
* link electron types locally

* Update electronAPI calls

* Fix source validation

* Payload to raw

* nit

* Update electron types
2024-11-13 17:20:18 -05:00
filtered
82d00a1bcf Update Template copy & paste (#1533)
* Split original clipboard functions out

* Add version check for templates

* Fix regression in use template undo steps
2024-11-13 17:04:31 -05:00
Chenlei Hu
b9224464c0 Fix reverse proxy (#1532) 2024-11-13 15:36:35 -05:00
Chenlei Hu
fba9a03df3 Lazy load setting dialog tabs (#1530) 2024-11-13 10:56:48 -05:00
Chenlei Hu
2fd624cd3d [skip ci] Update README.md (#1529)
Replace screenshot with actual logs for better accessibility.
2024-11-13 10:37:12 -05:00
Chenlei Hu
095fe2a175 Allow access of dev server in LAN for touch device testing (#1528) 2024-11-13 10:34:36 -05:00
Lasse Lauwerys
d838777e04 Touch support bug fixes (#1527)
* Improved touch support

* Fix touch support scaling error

* Fix touch scaling precision on all zoom levels

* Improved touch experiene, fixed zooming on textarea elements and fixed context menu.

* Minor bug fix
2024-11-13 10:14:11 -05:00
filtered
7e0d1d441d Flaky tests and observable state (#1526)
* Fix missing await

* Fix flaky tests - keyboard combos

Old code is causing playwright &/ changeTracker to add an undo step.  Using combo mode resolves flakiness until that can be investigated thoroughly.

* Restore skipped tests

* Fix flaky tests

* Async clean up

* Fix test always fails on retry

* Add TS types (tests)

* Fix flaky test

* Add observable busy state to workflow store

* Add workflow store busy wait to tests

* Rename test for clarity

* Fix flaky tests - use press() from locator API

Ref: https://playwright.dev/docs/api/class-keyboard#keyboard-press

* Fix flaky test - wait next frame

* Add delay between mouse events

Litegraph pointer handling is all custom coded, so a adding a delay between events for a bit of reality is actually beneficial.
2024-11-13 09:35:22 -05:00
Chenlei Hu
ddab149f16 1.3.42 (#1524) 2024-11-12 23:13:49 -05:00
Chenlei Hu
a73fdcd3bd Fix sidebar splitter state (#1523) 2024-11-12 23:12:56 -05:00
filtered
d6e0c197bd Decouple group node from Litegraph copy & paste (#1522)
* nit - Refactor

* Add old clipboard code to groupNode

* [Refactor] groupNode copy / paste functions

* Clarify function name
2024-11-12 23:11:04 -05:00
Chenlei Hu
3117d0fdc1 Fix loading of model library in non-electron env (#1521) 2024-11-12 22:38:29 -05:00
Chenlei Hu
96fda64b70 Fix queue button overlaped by pysssss.ImageFeed (#1520) 2024-11-12 21:35:14 -05:00
oto-ciulis-tt
e3d2c3a814 feat: Add download progress to sidebar (#1490)
* feat: Add download progress to sidebar

* Removing console log

* Lint fixes

* Updating UI

* Fixing lint error

* Fixing lint error

* Fixing lint error

* PR comments

* Reverting change

---------

Co-authored-by: Oto Ciulis <oto.ciulis@gmail.com>
2024-11-12 16:28:55 -05:00
Lasse Lauwerys
1a8900de1f Improved touch support (#1519)
* Improved touch support

* Fix touch support scaling error
2024-11-12 16:19:59 -05:00
Chenlei Hu
05ba526388 Type DOMWidget and DOMWidgetOptions (#1517)
* Type DOMWidget and DOMWidgetOptions

* Annotate widget value type
2024-11-12 13:35:24 -05:00
Chenlei Hu
4bc79181ae Move DOMClippingEnabled to coreSettings.ts (#1516)
* Move DOMClippingEnabled to coreSettings.ts

* nit
2024-11-12 12:01:44 -05:00
filtered
feafbf9cbf Litegraph Reroute Beta (#1421)
* Add Reroute support - ConnectingLinkImpl

Bonus: TS strict

* Add Reroute support

* Remove unused TS expect error

* Add reroute beta opt-in option

* Add settings option: Middle-click reroute node

* Add settings: Link Markers

* Move settings

* Update litegraph

---------

Co-authored-by: huchenlei <huchenlei@proton.me>
2024-11-12 11:46:14 -05:00
Chenlei Hu
40f9b881f3 1.3.41 (#1514) 2024-11-12 10:48:36 -05:00
Chenlei Hu
8236163fea Enable New UI by default (#1515)
nit

Add playwright test

nit

nit

nit
2024-11-12 10:48:26 -05:00
filtered
59b555b448 Fix multiline text input alignment & clipping (#1513)
* Simplify multiline widget scaling

* Fix multiline widget clipping

* Update test expectations [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-11-12 10:38:03 -05:00
Chenlei Hu
71eeee6744 Less padding on sidebar tabs for small screens (#1511)
* Reduce searchbox and tree padding for small screen

* Smaller buttons
2024-11-11 20:15:34 -05:00
Chenlei Hu
1ff6e27d9c Manage widget definitions with Pinia store (#1510)
* Fix compile

* nit

* Remove extensions.test

* nit
2024-11-11 17:23:52 -05:00
Chenlei Hu
64ef0f18b1 Fix welcome page welcome text selection (#1508) 2024-11-11 11:13:59 -05:00
Chenlei Hu
73bdbddf90 Fix rename open/bookmark workflow (#1507)
* Fix rename open/bookmark workflow

* nit

* Fix save as

* Add browser test
2024-11-11 11:06:41 -05:00
Chenlei Hu
a55833b3a6 1.3.40 (#1506) 2024-11-11 09:53:42 -05:00
filtered
43012eb1d1 Add settings option: Keep links on delete (#1504) 2024-11-10 21:33:48 -05:00
Chenlei Hu
d1e019589d [Electron] ComfyUI Desktop install wizard (#1503)
* Basic prototype

* Welcome screen animation

* nit

* Refactor structure

* Fix mocking

* Add tooltips

* i18n

* Add next button

* nit

* Stepper navigate

* Extract

* More i18n

* More i18n

* Polish MigrationPicker

* Polish settings step

* Add more i18n

* nit

* nit
2024-11-10 19:56:01 -05:00
filtered
7bc79edf3d Add back/forwards compatibility to schema validation (#1501)
* Allow future extensions of added schema objects

* Add explicit versioning to zod schemas

* Extend schema 0.4 with new fields in extras

- Allows Reroutes without using schema v1
- Reroute data is retained when using old versions - simply not displayed

* Add Reroute undo/redo support
2024-11-10 19:50:18 -05:00
Chenlei Hu
58ad01adfe Reland "Re-enable multiple-undo test" (#1499)
* Revert "Revert "Re-enable multiple-undo test (#1483)" (#1498)"

This reverts commit 5f1a9659e9.

* nit
2024-11-10 12:59:19 -05:00
Chenlei Hu
5f1a9659e9 Revert "Re-enable multiple-undo test (#1483)" (#1498)
This reverts commit 6c6c356c78.
2024-11-10 11:53:55 -05:00
Chenlei Hu
6c6c356c78 Re-enable multiple-undo test (#1483)
* Re-enable multiple-undo test

* nit
2024-11-10 10:57:15 -05:00
Chenlei Hu
893fd498df 1.3.39 (#1497) 2024-11-10 10:17:42 -05:00
Chenlei Hu
1ca388457d chore: update litegraph to 0.8.25 (#1496) 2024-11-10 10:16:35 -05:00
Chenlei Hu
69f0da06f8 Add update-litegraph script (#1495)
* Add update script

* nit
2024-11-10 10:15:46 -05:00
Chenlei Hu
d9a34872c3 [Electron] Add basic welcome screen (#1491)
* WIP

* Add LogTerminal

* Modify server startup view

* Add installView

* Add basic welcome screen and dev server setup

* nit

* nit

* nit

* nit

* nit
2024-11-10 09:41:32 -05:00
filtered
31fac3873c Litegraph: Reroute support (#1420)
* Add Litegraph schema v1 support

- LLink changed from array to object
- Add reroutes
- Graph state object
- Falls back to original schema if not validated
- Add version check to schema validation pass

* Fix test schema version detection

Resolves conflict with proposed schema v1
2024-11-09 15:46:34 -05:00
Chenlei Hu
8dc057517f 1.3.38 (#1488) 2024-11-09 10:55:35 -05:00
Chenlei Hu
4617e0fb1a Fix node badge on unknown color palette (#1487)
* Fix node badge on unknown color palette

* Update test expectations [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-11-09 10:55:25 -05:00
Chenlei Hu
f8ec87ddea Fix changeTracker modified state (#1481)
* Add jsondiffpatch

* Add logs

* Add graphDiff helper

* Fix changeTracker

* Add loglevel

* Add playwright test

* Fix jest test

* nit

* nit

* Fix test url

* nit
2024-11-08 22:24:35 -05:00
Chenlei Hu
c12f059940 Persist splitter state in localStorage (#1480) 2024-11-08 20:16:13 -05:00
Chenlei Hu
cc320e0f84 Change tooltip location to bottom for sidebar action buttons (#1479) 2024-11-08 18:56:44 -05:00
Chenlei Hu
acbc38ced4 Revert "Enable New UI by default (#1460)" (#1476)
This reverts commit f0b735f3dd.
2024-11-08 16:35:52 -05:00
oto-ciulis-tt
777a6d9ce3 feat:use electron api to download models (#1473)
* enh: Use electron API to download models

* Adding tooltips

* PR comments

---------

Co-authored-by: Oto Ciulis <oto.ciulis@gmail.com>
2024-11-08 15:59:35 -05:00
pythongosssss
7e0b87dd32 Live terminal output (#1347)
* Add live terminal output

* Fix scrolling

* Refactor loading

* Fallback to polling if endpoint fails

* Comment

* Move clientId to executionStore
Refactor types

* Remove polling
2024-11-08 15:38:21 -05:00
Zoltán Dócs
0161a670cf Fit view to bounds (#1474)
* fit view:
- fit view to canvas selection
- fit view to whole graph when nothing is selected
- add button to graph canvas menu
- assign default keybinding '.'

* Adjust on changed APIs

* Update litegraph

---------

Co-authored-by: huchenlei <huchenlei@proton.me>
2024-11-08 15:03:21 -05:00
Chenlei Hu
0eba49c536 Update litegraph (Animate to bounds) (#1475) 2024-11-08 14:12:58 -05:00
Chenlei Hu
1d9c3f00b7 Setting dialog responsive design for smaller screen size (screen width < 1536) (#1472)
* Smaller queue button

* Smaller dialog padding

* Adjust setting content

* Fix keybinding panel
2024-11-08 11:18:26 -05:00
Chenlei Hu
904408de01 1.3.37 (#1470) 2024-11-08 10:03:27 -05:00
Chenlei Hu
700336fcc7 Fix queue button icon layout shift (#1469) 2024-11-08 10:02:36 -05:00
Chenlei Hu
dd192777b7 Consistently use -1 for temporary file size (#1464) 2024-11-07 23:34:46 -05:00
Chenlei Hu
6b6992591b Update litegraph (#1462) 2024-11-07 20:20:05 -05:00
filtered
45380f7ca0 Fix TypeError thrown (#1461)
Missing node type + reroute linked
2024-11-07 20:17:16 -05:00
Chenlei Hu
f0b735f3dd Enable New UI by default (#1460)
* Enable New UI by default

* nit

* Add playwright test

* nit

* nit

* nit
2024-11-07 17:55:53 -05:00
Chenlei Hu
9568d63820 Add comfyui-electron-types dependency (#1459) 2024-11-07 16:05:26 -05:00
Chenlei Hu
073638672d Fix ('STRING',) input node (#1457)
* Fix ('STRING',) input node

* Update test expectations [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-11-07 13:03:54 -05:00
Chenlei Hu
8ae9210298 Always sort workflows tree (#1456)
* Always sort workflows tree

* nit

* nit

* Add tests

* nit

* nit
2024-11-07 11:29:29 -05:00
Chenlei Hu
daf94d74d5 1.3.36 (#1453) 2024-11-07 08:59:56 -05:00
Chenlei Hu
14b3d4c766 Fix loading of workflow bookmarks (#1452) 2024-11-07 08:58:28 -05:00
Chenlei Hu
40880dbb59 Move refresh button from actionbar to 'Edit' menu (#1451) 2024-11-07 08:47:46 -05:00
Chenlei Hu
aa4742e394 Update litegraph (#1447) 2024-11-06 21:51:03 -05:00
Chenlei Hu
0a7000328a Add menu button to toggle focus mode (#1446)
* Add focus mode toggle button

* handle menu position

* nit
2024-11-06 20:56:32 -05:00
dependabot[bot]
da7a49bb5c Bump happy-dom from 15.4.0 to 15.11.0 (#1445)
Bumps [happy-dom](https://github.com/capricorn86/happy-dom) from 15.4.0 to 15.11.0.
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v15.4.0...v15.11.0)

---
updated-dependencies:
- dependency-name: happy-dom
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-06 19:59:13 -05:00
Chenlei Hu
5e4439b905 [Electron] Add electron-specific setup page (#1444)
* Add dummy server start view

* Do external nav

* nit

* nit

* nit

* nit
2024-11-06 19:39:09 -05:00
Chenlei Hu
ea0883271e 1.3.35 (#1443) 2024-11-06 13:49:12 -05:00
Chenlei Hu
3d303c7693 Fix save workflow binding on Ctrl + S (#1442)
* Fix save workflow binding on Ctrl + S

* nit
2024-11-06 13:46:56 -05:00
Chenlei Hu
9f14edaf2b 1.3.34 (#1440) 2024-11-06 08:53:38 -05:00
Chenlei Hu
d1738b50d2 Update litegraph (Remove hardcode +1 size) (#1438)
* Update litegraph (Remove hardcode +1 size)

* Update test expectations [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-11-05 19:07:38 -05:00
Chenlei Hu
c560628f1f [Extension API] Register about panel badge (#1436)
* Custom about panel badge

* Add playwright test

* Update README

* nit

* nit

* nit

* nit
2024-11-05 19:06:38 -05:00
Chenlei Hu
c56533bb23 Workflow Management Reworked (#1406)
* Merge temp userfile

Basic migration

Remove deprecated isFavourite

Rename

nit

nit

Rework open/load

Refactor save

Refactor delete

Remove workflow dep on manager

WIP

Change map to record

Fix directory

nit

isActive

Move

nit

Add unload

Add close workflow

Remove workflowManager.closeWorkflow

nit

Remove workflowManager.storePrompt

move from commandStore

move more from commandStore

nit

Use workflowservice

nit

nit

implement setWorkflow

nit

Remove workflows.ts

Fix strict errors

nit

nit

Resolves circular dep

nit

nit

Fix workflow switching

Add openworkflowPaths

Fix store

Fix key

Serialize by default

Fix proxy

nit

Update path

Proper sync

Fix tabs

WIP

nit

Resolve merge conflict

Fix userfile store tests

Update jest test

Update tabs

patch tests

Fix changeTracker init

Move insert to service

nit

Fix insert

nit

Handle bookmark rename

Refactor tests

Add delete workflow

Add test on deleting workflow

Add closeWorkflow tests

nit

* Fix path

* Move load next/previous

* Move logic from store to service

* nit

* nit

* nit

* nit

* nit

* Add ChangeTracker.initialState

* ChangeTracker load/unload

* Remove app.changeWorkflow

* Hook to app.ts

* Changetracker restore

* nit

* nit

* nit

* Add debug logs

* Remove unnecessary checkState on graphLoad

* nit

* Fix strict

* Fix temp workflow name

* Track ismodified

* Fix reactivity

* nit

* Fix graph equal

* nit

* update test

* nit

* nit

* Fix modified state

* nit

* Fix modified state

* Sidebar force close

* tabs force close

* Fix save

* Add load remote workflow test

* Force save

* Add save test

* nit

* Correctly handle delete last opened workflow

* nit

* Fix workflow rename

* Fix save

* Fix tests

* Fix strict

* Update playwright tests

* Fix filename conflict handling

* nit

* Merge temporary and persisted ref

* Update playwright expectations

* nit

* nit

* Fix saveAs

* Add playwright test

* nit
2024-11-05 11:03:27 -05:00
Chenlei Hu
1387d7e627 1.3.33 (#1435) 2024-11-05 10:06:51 -05:00
Chenlei Hu
16f2e56d8e Handle errors from top action menu commands (#1432) 2024-11-04 22:50:19 -05:00
Chenlei Hu
75ffab2160 Fix user stuck in title editing state (#1430)
* Fix user stuck in title editing state

* Fix test
2024-11-04 21:59:40 -05:00
filtered
4c19e1ba3a Speed up E2E tests using fully parallel (#1429)
With flaky tests / async bugs all dealt with, fullyParallel can be restored.
2024-11-04 20:33:50 -05:00
Chenlei Hu
2161ae4e5b Pin selected items (Nodes + Groups) (#1427)
* Pin selected items (Nodes + Groups)

* Update litegraph

* Add playwright test

* nit

* Update test expectations [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-11-04 17:36:33 -05:00
Chenlei Hu
3148c90e28 [skip ci] Update README.md (#1425) 2024-11-04 09:16:55 -05:00
Chenlei Hu
497b2fba8d 1.3.32 (#1424) 2024-11-04 09:03:27 -05:00
Chenlei Hu
09d5e29f01 Create new branch in release script (#1423) 2024-11-04 09:02:33 -05:00
filtered
56b63ebab5 Update Litegraph API: Group move / select / titlebar (#1418)
* Litegraph: Group move / select

* Update litegraph

---------

Co-authored-by: huchenlei <huchenlei@proton.me>
2024-11-03 18:45:20 -05:00
filtered
3ba776e6ca Add Litegraph multi-select & group nesting (#1416)
* Allow nested groups

Pass all selected items (new litegraph feature) instead of just selected nodes.

* Allow nested groups - context menus

* Update litegraph

* Update litegraph (Select all / Delete selected)

* Add playwright test

* nit

* Update test expectations [skip ci]

---------

Co-authored-by: huchenlei <huchenlei@proton.me>
Co-authored-by: github-actions <github-actions@github.com>
2024-11-03 18:08:42 -05:00
Chenlei Hu
0a784d9236 Highlight splitter gutter on resizing (#1414) 2024-11-03 12:38:39 -05:00
Chenlei Hu
00df7b428f Animate goto node (#1412)
* Animate goto node

* Update litegraph (animateToNode)
2024-11-03 10:57:17 -05:00
filtered
27bacc36d4 Update tests with precise node positions / sizes (#1408)
* Update tests with precise node positions / sizes

* Fix test flakiness - missing await

* Fix test failures - async not awaited

* Update action

* Update test expectations [skip ci]

---------

Co-authored-by: huchenlei <huchenlei@proton.me>
Co-authored-by: github-actions <github-actions@github.com>
2024-11-03 10:42:54 -05:00
filtered
394df49208 Fix primitive size on load (#1407) 2024-11-03 09:29:39 -05:00
Chenlei Hu
38847e1079 1.3.31 (#1411) 2024-11-03 09:27:58 -05:00
Chenlei Hu
dd86417177 Disable flaky test on missing model download dialog (#1409) 2024-11-03 09:04:18 -05:00
filtered
1366c8cb44 Fix primitive resize when node size ref retained (#1405)
* Fix primitive resize when node size ref retained

Primitive assumes that setting node size property will replace the ref.  Minimal change.

* Use explicit variable names
2024-11-02 16:14:49 -04:00
Chenlei Hu
3a910f25e9 Track previous workflow name on Vue side (#1404) 2024-11-02 14:40:05 -04:00
Chenlei Hu
cc420b70a5 Add finally handler for rename tree node action (#1403)
* Add finally handler for rename tree node action

* nit
2024-11-02 11:39:15 -04:00
filtered
caa3ac2068 Add playwright concurrency - multi-user mode (#1400)
* Add playwright concurrency - multi-user mode

* Add extra server params

* nit

* Update to v2.1 action

---------

Co-authored-by: huchenlei <huchenlei@proton.me>
2024-11-02 09:51:24 -04:00
Chenlei Hu
8baaf380dc Split jest tests into fast and slow groups (#1401) 2024-11-01 22:39:42 -04:00
Chenlei Hu
d719a4e0fb Move exportWorkflow from menu to workflowService (#1399) 2024-11-01 19:44:21 -04:00
Chenlei Hu
d254559e20 [Refactor] Extract createTemporary (#1398)
* [Refactor] Extract createTemporary

* nit
2024-11-01 19:32:50 -04:00
pythongosssss
d701758663 Add support for hidden & advanced widgets (#1389)
* Add support for hidden & advanced widgets

* Fix

* Update package

* Remove ts-expect-error

* Fix test, tidy
2024-11-01 19:12:44 -04:00
Chenlei Hu
a11b78d1c3 Remove deprecated method isFavourite (#1397) 2024-11-01 19:12:04 -04:00
Chenlei Hu
dfb695be72 [Refactor] Rework userFileStore to match existing API on ComfyWorkflow (#1394)
* nit

* Move load

* nit

* nit

* Update store API

* nit

* nit

* Move api

* nit

* Update tests

* Add docs

* Add temp user file

* Implement save as

* Test saveAs
2024-10-31 21:58:00 -04:00
Chenlei Hu
2974b9257a 1.3.30 (#1393) 2024-10-31 19:17:19 -04:00
Chenlei Hu
d11d07334b Add npm release script to automatically create release PR (#1392)
* Add release script

* nit
2024-10-31 19:14:35 -04:00
Chenlei Hu
0c8fe41b84 Fix queue ResultItem schema (#1386) 2024-10-30 20:36:33 -04:00
filtered
ed0592d6e0 Update litegraph API - add @ts-expect-error (#1380)
* Update litegraph API - add @ts-expect-error

LG update removes some implicit any, exposing existing errors

* Update litegraph

---------

Co-authored-by: huchenlei <huchenlei@proton.me>
2024-10-30 20:05:04 -04:00
Chenlei Hu
94f4147f92 Fix double trigger of setting onChange callback (#1385)
* Fix onChange double trigger

* nit

* Add playwright test
2024-10-30 19:55:46 -04:00
Chenlei Hu
67ee8726ef 1.3.29 (#1383) 2024-10-30 16:34:49 -04:00
Chenlei Hu
e48c78541c Hide empty folders when searching in model library (#1382) 2024-10-30 16:27:35 -04:00
Chenlei Hu
bf7a9bf5eb Update litegraph (link snap to slot & highlight) (#1378)
* Update litegraph (link snap)

* Add settings

* nit

* Add playwright test

* Update test expectations [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-10-30 15:23:58 -04:00
filtered
3fb2d423ba Update litegraph API - 237 (#1376) 2024-10-30 13:43:38 -04:00
Chenlei Hu
74f7311585 Fix jest test mock (#1375) 2024-10-30 10:37:54 -04:00
Chenlei Hu
97c38583e9 Rename workspaceStateStore to workspaceStore (#1374) 2024-10-30 09:49:23 -04:00
Chenlei Hu
324eff93fd Update Litegraph API - canvas.state (#1372)
* Update Litegraph API - canvas.state

* Update litegraph

---------

Co-authored-by: filtered <176114999+webfiltered@users.noreply.github.com>
2024-10-29 22:07:04 -04:00
Chenlei Hu
4b1104f52c Add node id to execution error report (#1371) 2024-10-29 21:48:45 -04:00
Chenlei Hu
d702fc81a2 1.3.28 (#1370) 2024-10-29 20:30:07 -04:00
Chenlei Hu
2a94ab4423 Fix empty model library folder content (#1369) 2024-10-29 20:28:50 -04:00
Chenlei Hu
37f6c89383 1.3.27 (#1366) 2024-10-29 17:56:28 -04:00
Chenlei Hu
35fab0bef3 Focus mode (#1365)
* Menu hamburger

* Focus

* nit

* nit

* nit

* Update test expectations [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-10-29 17:25:18 -04:00
filtered
795e932b8f Fix husky pre-commit & type check only staged (#1361)
* Fix husky pre-commit on some Windows clients

https://github.com/typicode/husky/issues/1072#issuecomment-1784006332

* Limit commit type check to staged files

Adds tsc-files package (dev only) and its config

* Remove deprecated git add from lint-staged
2024-10-29 14:15:07 -04:00
Chenlei Hu
8dddffe840 Use browser download in missing model dialog (#1362)
* Remove custom backend download logic

* Add download hooks

* Download button

* Use browser download

* Update test
2024-10-29 14:07:16 -04:00
Chenlei Hu
1f91a88d7b Move linkRenderMode extension to core (#1359) 2024-10-29 11:00:10 -04:00
Chenlei Hu
10f43be911 Remove show model folder checkbox in missing model dialog (#1358)
* Remove show model folder checkbox in missing model dialog

* nit

* nit
2024-10-29 10:15:31 -04:00
Chenlei Hu
87517daf1f Restyle missing model warning dialog (#1354)
* Restyle missing model dialog

* nit

* nit

* nit

* nit
2024-10-29 09:26:02 -04:00
Chenlei Hu
739ebd3d04 Auto-expand model library tree on search (#1357) 2024-10-29 09:24:56 -04:00
Chenlei Hu
4582c71583 [Refactor] Rework modelStore and ModelLibrarySidebarTab (#1350)
* nit

* Rename

* nit

* Move load model folders to app level

* Various fixes

* nit

* nit

* wip

* nit

* nit

* nit

* Split

* nit

* Add back spinner

* nit

* nit

* Add refresh button

* nit

* nit

* Preserve model folder order

* Avoid order change on folder open
2024-10-28 21:23:53 -04:00
Chenlei Hu
757f0ced81 1.3.26 (#1353) 2024-10-28 19:59:43 -04:00
Chenlei Hu
a471a3f302 [skip ci] Add typecheck pre-commit hook (#1352) 2024-10-28 19:56:04 -04:00
Chenlei Hu
5a3a8d32ab Update litegraph 0.8.10 (#1351) 2024-10-28 19:49:48 -04:00
Chenlei Hu
44b109a449 Add type annotation for missingNodeTypes (#1349)
* Add type annotation for missingNodeTypes

* nit

* nit
2024-10-28 17:26:52 -04:00
Chenlei Hu
229896a4b7 Restyle missing node warning dialog (#1348)
* nit

* Restyle missing node warning dialog

* nit

* nit
2024-10-28 16:45:47 -04:00
Chenlei Hu
997b5ee819 Update litegraph (Fix group select) (#1342)
* Update litegraph (Fix group select)

* Update test expectations [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-10-27 22:05:18 -04:00
Chenlei Hu
ba99eca700 1.3.25 (#1341) 2024-10-27 20:07:47 -04:00
Chenlei Hu
82c369322d Handle invalid node def errors (#1340)
* nit

* Add error handling

* nit

* nit
2024-10-27 20:07:05 -04:00
Chenlei Hu
546c5dabc8 Update litegraph (Slot highlight) (#1339)
* Update litegraph (Slot highlight)

* Disable tooltip setting in playwright tests

* Update test expectations [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-10-27 19:31:22 -04:00
filtered
7d450adf93 Remove unused param - litegraph update (#1335) 2024-10-27 16:21:14 -04:00
Chenlei Hu
eed92864f2 Enable ts-strict for changeTracker (#1338)
* Fix app getter

* nit

* nit

* nit

* Fix rest of errors

* nit
2024-10-27 16:20:32 -04:00
Chenlei Hu
8fd7852740 Enable ts-strict for nodeBookmarkStore (#1336) 2024-10-27 15:06:37 -04:00
Chenlei Hu
2a927bb9ea [skip ci] Update README (Dev section) (#1334)
* Expand test section

* Add techstack section
2024-10-27 10:12:54 -04:00
Chenlei Hu
c566491ac7 Enable ts-strict for queueStore (#1333) 2024-10-27 10:07:09 -04:00
Zoltán Dócs
7729611a2a Bugfix node widgets wrongly being prepared for qeueing on saving the workflow (#1331)
- calling graphToPrompt() invokes beforeQueued() which should not happen when saving the workflow
2024-10-27 09:59:27 -04:00
Chenlei Hu
8861492655 Enable ts-strict for workflowStore (#1332) 2024-10-27 09:59:17 -04:00
Chenlei Hu
fa9d944b32 Convert pinia stores from options API to composition API (#1330)
* Convert toastStore

* Convert workspaceStateStore

* Convert settingStore

* Convert queueStore

* Convert modelToNodeStore

* Convert modelStore

* Convert dialogStore

* nit

* nit

* nit
2024-10-27 08:47:24 -04:00
Chenlei Hu
880437f3c0 1.3.24 (#1326) 2024-10-26 19:50:05 -04:00
Chenlei Hu
c1e960c83c Update litegraph (Multi-group selection) (#1325) 2024-10-26 18:26:59 -04:00
Chenlei Hu
571386c061 Enable ts-strict for api.ts (#1324) 2024-10-26 18:08:24 -04:00
Chenlei Hu
44d886a18b Enable ts-strict for ui/settings (#1323) 2024-10-26 17:25:15 -04:00
Chenlei Hu
92ac403679 Enable ts-strict for settingStore (#1322) 2024-10-26 17:07:17 -04:00
Chenlei Hu
dc3dab4e1c Enable ts-strict for commandStore (#1321) 2024-10-26 17:05:42 -04:00
Chenlei Hu
386594554e Enable ts-strict for executionStore (#1320)
* Fix listener types

* nit

* nit

* fix type
2024-10-26 17:00:18 -04:00
Chenlei Hu
02a951ad58 Enable ts-strict for nodeDefStore (#1319) 2024-10-26 16:02:07 -04:00
Chenlei Hu
92f0f4a21c Convert nodeDefStore to use composition API (#1318)
* Convert nodeDefStore to use composition API

* nit
2024-10-26 15:48:40 -04:00
Chenlei Hu
645897f8b8 [Refactor] Make node badge a vue component (#1317)
* [Refactor] Make node badge a vue component

* Simplify badge logic

* nit
2024-10-26 15:28:14 -04:00
Chenlei Hu
ef4179a06c Update litegraph to 0.8.6 (#1316)
* Update litegraph

* Remove ts-expect-error
2024-10-26 14:36:45 -04:00
Chenlei Hu
a3e4af40c1 1.3.23 (#1314) 2024-10-25 20:13:42 -04:00
Björn Söderqvist
1dedce5ec6 Enable ts-strict for colorUtil, contextMenuFilter and linkRenderMode (#1313)
* Add types for colorUtil.ts

* Add types for contextMenuFilter

* Add types to linkRenderMode.ts
2024-10-25 15:42:15 -04:00
Chenlei Hu
3a4b36fb31 Disallow node library bookmark folder name with / (#1311) 2024-10-25 12:35:53 -04:00
Chenlei Hu
25457d31d4 Revert "Fix playwright test on settings API (#1303)" (#1310)
This reverts commit 54e833502a.
2024-10-25 12:09:50 -04:00
Chenlei Hu
4f9dc830b6 Setup clean setting state before every playwright test (#1309)
* Use reload

* setting setup

* nit

* Remove setting cleanups

* Wait for frame

* nit

* nit
2024-10-25 11:25:17 -04:00
Chenlei Hu
12d421b42c Update litegraph (TypeScript LiteGraphGlobal) (#1308) 2024-10-25 08:45:04 -04:00
Chenlei Hu
16ebfd6171 Fix import of NodeReference in ComfyPage (#1306) 2024-10-25 08:32:12 -04:00
Chenlei Hu
59c999324e Split ComfyPage fixture (#1305)
* Split down page components

* Move litegraph utils

* nit
2024-10-25 08:29:02 -04:00
Chenlei Hu
624bcc75ab Move ComfyPage to fixtures folder (#1304) 2024-10-25 08:01:50 -04:00
Chenlei Hu
54e833502a Fix playwright test on settings API (#1303) 2024-10-25 07:53:23 -04:00
Chenlei Hu
c3242711c7 Update bug-report.yaml (Add settings section) (#1301) 2024-10-25 07:46:45 -04:00
Chenlei Hu
17391e4aad Handle potential undefined node data in NodeBookmarkTreeExplorer (#1300) 2024-10-25 07:41:43 -04:00
Chenlei Hu
48b840a88d 1.3.22 (#1297) 2024-10-24 20:03:40 -04:00
Chenlei Hu
377fed584f [Extension API] Register custom bottom panel tabs (#1296)
* Bottom panel API

* Update README
2024-10-24 19:58:44 -04:00
Chenlei Hu
14a6687cc9 Integrated terminal (#1295)
* Add terminal tab

* Add basic terminal

* Style terminal

* Add keybinding

* Auto scroll:

* Mock for jest test
2024-10-24 17:31:00 -04:00
Chenlei Hu
d142893244 Add bottom panel support (#1294)
* Add bottom panel

* Bottom panel store

* Extract ExtensionSlot component

* Tab rendering

* Add toggle button on top menu bar

* nit

* Add toggle button tooltip

* Add command
2024-10-24 15:15:19 -04:00
Chenlei Hu
957a767ed0 New settings API (#1292)
* Add settings API

* Add playwright test

* Update README
2024-10-24 10:26:01 -04:00
YANG Zhitao
3553c8e0d4 Enable ts-strict for slotDefaults.ts (#1290) 2024-10-24 07:31:45 -04:00
YANG Zhitao
05221f7961 Enable ts-strict for clipspace.ts (#1291) 2024-10-24 07:31:15 -04:00
Chenlei Hu
d113072a64 1.3.21 (#1289) 2024-10-23 20:38:50 -04:00
Chenlei Hu
afa619b7df Revert "Enable ts-strict for invertMenuScrolling.ts (#1264)" (#1288)
This reverts commit 9388ee0705.
2024-10-23 20:37:40 -04:00
Chenlei Hu
e25bbc19cb Revert "Enable ts-strict for contextMenuFilter.ts (#1263)" (#1287)
This reverts commit b655c5544d.
2024-10-23 20:37:03 -04:00
Chenlei Hu
0251bc9e6c 1.3.20 (#1285) 2024-10-23 19:53:33 -04:00
Chenlei Hu
7a3f20c57d Fix sidebar scrollpanel style (#1283) 2024-10-23 18:27:54 -04:00
Chenlei Hu
269fc7c8c9 Remove misleading console error on paste (#1282) 2024-10-23 14:59:24 -04:00
Chenlei Hu
db08f74d6a Update litegraph (TypeScript LgraphNode) (#1281) 2024-10-23 14:38:09 -04:00
Chenlei Hu
5db757ade2 Enable ts-strict for uploadImage.ts (#1280) 2024-10-23 13:59:47 -04:00
Chenlei Hu
59c03d2de5 [Refactor] Rename ModelStore to ModelFolder (#1244)
* Refactor click

* Rename ModelStore to ModelFolder
2024-10-23 12:04:49 -04:00
YANG Zhitao
b655c5544d Enable ts-strict for contextMenuFilter.ts (#1263)
* Enable ts-strict for contextMenuFilter.ts

* function instead of arrow function

* tackle @ts-expect-error at contextMenuFilter.ts by use class instead of function + prototype
2024-10-23 12:03:50 -04:00
YANG Zhitao
9388ee0705 Enable ts-strict for invertMenuScrolling.ts (#1264) 2024-10-23 12:03:33 -04:00
juju
f228ec29eb Enable ts-strict for editAttention.ts (#1269)
* fix typecheck errors

* revert tsconfig strict true
2024-10-23 12:02:52 -04:00
Yoland Yan
7239e94092 Mini lang (#1245) 2024-10-13 09:56:58 +02:00
Chenlei Hu
e33a5f7736 1.3.19 (#1243) 2024-10-12 17:21:59 -04:00
Chenlei Hu
c1c990e6f3 Properly show empty folder in model sidebar tab (#1242)
* Properly show empty folder in model sidebar tab

* nit

* nit
2024-10-12 17:02:58 -04:00
Chenlei Hu
a890756868 Enable ts-strict for modelStore.ts (#1241) 2024-10-12 16:38:27 -04:00
Chenlei Hu
015ee2df15 Revert "Enable ts-strict for colorUtil.ts (#1239)" (#1240)
This reverts commit c3b2697568.
2024-10-12 15:58:43 -04:00
Chenlei Hu
c3b2697568 Enable ts-strict for colorUtil.ts (#1239) 2024-10-12 14:11:32 -04:00
Chenlei Hu
dfcabd2834 Enable ts-strict for treeUtil.ts (#1238) 2024-10-12 14:10:08 -04:00
Chenlei Hu
634196cbd6 Enable ts-strict for dialogStore.ts (#1236) 2024-10-12 12:17:55 -04:00
Chenlei Hu
5611e90fda Add ts-strict-ignore plugin (#1235)
* Add ts-strict-ignore plugin

* nit

* Add to typecheck script
2024-10-12 11:56:49 -04:00
Chenlei Hu
c23d95f8f9 1.3.18 (#1232) 2024-10-11 16:27:50 -04:00
Chenlei Hu
c2377b62ac Fix sidebar tab bg color (#1229)
* Fix node library sidebar style

* Fix styles

* nit
2024-10-11 15:04:29 -04:00
Chenlei Hu
f328d4cd81 Hide system scrollbar for queue sidebar tab (#1227) 2024-10-11 13:59:51 -04:00
Chenlei Hu
fbc1482b90 Use scrollpanel in sidebar template (#1226)
* Use scrollpanel in sidebar template

* Migrate

* nit
2024-10-11 13:41:41 -04:00
Chenlei Hu
90e07af4f5 Update litegraph (TypeScript LGraphCanvas) (#1225) 2024-10-11 13:39:19 -04:00
AustinMroz
014c3f3172 Fix load workflow with converted widget (#1222)
f74973 introduced a regression where a node which has had an input
added, will clobber a converted input of in a workflow created before
the input was added.

The prior behaviour was also incorrect (the new input would not exist on
the node), but would often be runnable.

Keeping the position of the converted widget and adding the new input to
the end is an unfortunate compromise. Doing it the other way around
breaks primitive nodes
2024-10-11 12:20:03 -04:00
Chenlei Hu
419009424b Handle missing extraPngInfo field in queue (#1223) 2024-10-11 12:16:35 -04:00
Chenlei Hu
f599c9bcb8 Fix format (#1224) 2024-10-11 12:16:17 -04:00
Chenlei Hu
6c696cddb9 Fix dialog header component error (#1221) 2024-10-11 11:55:13 -04:00
Chenlei Hu
d787c21f8b [skip ci] Explicitly specify prettier printWidth (#1220) 2024-10-11 11:53:35 -04:00
Chenlei Hu
f96f08be32 Add ru to locale list (#1218) 2024-10-11 10:43:10 -04:00
Ioan
8ebb51b9a3 Update i18n.ts (#1214)
* Update i18n.ts

Added Russian translation

* Update i18n.ts
2024-10-11 09:14:04 -04:00
Chenlei Hu
60e1b82df6 Update litegraph (TypeScript LLink) (#1213)
* Update litegraph (TypeScript LLink)

* Remove ts-expect-error
2024-10-10 20:54:31 -04:00
Chenlei Hu
459afa158c Restyle dialog with scrollpanel (#1212) 2024-10-10 20:27:14 -04:00
Chenlei Hu
1c3d3b33f6 1.3.17 (#1211) 2024-10-10 16:52:01 -04:00
Chenlei Hu
f64365915b Mark app.showMissingNodeDialog private (#1210) 2024-10-10 16:35:29 -04:00
Chenlei Hu
ec8e6f79b3 Fix create group node command error states (#1209)
* Fix edge cases

* Add playwright test

* nit
2024-10-10 15:56:00 -04:00
Chenlei Hu
b89f467983 Add group node commands/keybindings (#1208)
* Add group node commands/keybindings

* Fix jest tests
2024-10-10 12:50:05 -04:00
Chenlei Hu
009dbcf8c7 Wrap pragmatic dnd API with hooks (#1207) 2024-10-10 10:53:49 -04:00
Chenlei Hu
4413fd248c Remove question mark badge on folders in model library tree (#1205) 2024-10-10 10:13:14 -04:00
Chenlei Hu
8962597e69 Update litegraph (TypeScript LGraph) (#1206) 2024-10-10 10:13:02 -04:00
Chenlei Hu
f4d4111fbd 1.3.16 (#1202) 2024-10-09 22:17:57 -04:00
Chenlei Hu
babac5a4a9 1.3.15 (#1201) 2024-10-09 22:15:06 -04:00
Chenlei Hu
f71595fcc9 Fix node def handling of undefined fields (#1199)
* Fix node def handling

* nit

* Add test
2024-10-09 22:11:27 -04:00
Chenlei Hu
59a5f5f5d0 Add help menu on command menu bar (#1197)
* Add help menu on command menu bar

* nit
2024-10-09 20:35:17 -04:00
Chenlei Hu
2d5faa7f3d Anchor floating actionbar to closest side when resizing window (#1195)
* Anchor floating actionbar to closest side when resizing window

* nit

* nit
2024-10-09 17:41:36 -04:00
Chenlei Hu
32fa950aa1 1.3.14 (#1194) 2024-10-09 16:41:38 -04:00
Chenlei Hu
82112c2c6e Move low priority init to idle task (#1192) 2024-10-09 16:22:58 -04:00
Chenlei Hu
f94bdc358b Disable node def validation by default (#1190)
* Add setting

* Make node def validation optional
2024-10-09 16:02:27 -04:00
Chenlei Hu
f6466d7062 Avoid calling settingStore.set when there is no legacy node bookmark (#1191)
* Avoid calling settingStore.set when there is no legacy node bookmark

* nit
2024-10-09 16:02:14 -04:00
Chenlei Hu
1c5fd2465e Move vitejs/plugin-vue to devDep (#1189) 2024-10-09 15:16:43 -04:00
Chenlei Hu
e99329cff5 Remove class-transformer dependency (#1187)
* nit

* Fix test

* Remove class-transformer and its deps

* nit

* Fix invalid type for dummy node
2024-10-09 15:10:19 -04:00
bymyself
fabcbaec82 Restore backend state when Playwright test finishes (#1168)
* Restore backend state when Playwright test finishes. Resolve #1094

* Add warning when env var not set

* Rename and replace with scaffolding option for models dir

* Rename

* Define another env var [skip ci]

* Fix paths [skip ci]

* Update README.md

---------

Co-authored-by: Chenlei Hu <huchenlei@proton.me>
2024-10-09 15:05:19 -04:00
Chenlei Hu
c8f50509ed Fix VHS advanced preview html video type (#1186)
* Fix VHS advanced preview html video type

* nit
2024-10-09 15:02:02 -04:00
Chenlei Hu
165604bb80 Support VHS advanced preview in queue sidebar tab (#1183)
* Map VHS video type

* Advance preview format

* nit

* View VHS advanced preview

* Disable result gallery vitest

* Proper disable
2024-10-09 12:42:00 -04:00
Chenlei Hu
829bce1c8c Load Keybinding and Extension panel async (#1179) 2024-10-08 22:20:30 -04:00
Chenlei Hu
c3b82165fa 1.3.13 (#1178) 2024-10-08 20:14:56 -04:00
Chenlei Hu
d673a521d8 Add always snap to grid setting (#1177)
* Always snap to grid

* Ban pysssss.SnapToGrid

* nit
2024-10-08 20:12:23 -04:00
Chenlei Hu
ee88a79bc3 1.3.12 (#1175) 2024-10-08 17:12:53 -04:00
Chenlei Hu
5f3afa3776 Supports VHS video outputs in queue sidebar tab (#1174)
* Properly identify gifs

* Detect VHS video

* Basic video support in queue

* Video in lightbox

* Preview button

* nit

* Fix vitest
2024-10-08 17:10:44 -04:00
Chenlei Hu
3cafc10c2b Fix pan mode icon display (#1173) 2024-10-08 14:24:39 -04:00
Chenlei Hu
2cb1cea196 Make LGraphCanvas shallowReactive (#1169)
* Make LGraphCanvas shallowReactive

* Restore canvas options after creation
2024-10-08 14:07:15 -04:00
Chenlei Hu
482da21ba7 Remove state check on continuous keydown (#1171)
* Remove state check on continuous keydown

* nit
2024-10-08 11:01:08 -04:00
bymyself
bf80340310 Add test on settings visibility on mobile (#1164)
* Add test on settings dialog visibility on mobile

* Consolidate settings-dialog tests

* Simplify zoom speed setting tests
2024-10-08 09:43:16 -04:00
bymyself
5ef15c0daf Add aria labels to dialogs (#1167) 2024-10-08 09:41:28 -04:00
bymyself
62be958d47 Add tests on node multi-select (#1163) 2024-10-08 09:11:22 -04:00
Chenlei Hu
1ba236bbce Simplify node tooltip lifecycle (#1162) 2024-10-07 22:13:52 -04:00
Chenlei Hu
a4e08f60fe Extract theme toggle as command (#1161)
* Extract theme toggle as command

* nit
2024-10-07 21:54:58 -04:00
Chenlei Hu
5ba1d1a3f7 Show sidebar toggle keybinding shortcut on sidebar icon tooltip (#1160) 2024-10-07 21:44:38 -04:00
Chenlei Hu
58dd15a662 Fix core sidebar tab toggle command register (#1159) 2024-10-07 21:32:30 -04:00
Chenlei Hu
50a6ee27a0 Refactor core sidebar tab registration (#1158)
* Refactor sidebar tab register

* Register core tabs
2024-10-07 21:23:52 -04:00
Chenlei Hu
23952d9751 Show queue front icon when shift is pressed (#1157)
* Move shiftDown state to workspaceStateStore

* Queue front state
2024-10-07 19:54:00 -04:00
Chenlei Hu
2b26514190 1.3.11 (#1154) 2024-10-07 17:27:25 -04:00
Chenlei Hu
f8343d0f93 Fix flaky playwright test (#1152) 2024-10-07 17:11:20 -04:00
Chenlei Hu
cc17bee945 Manage app.ts litegraph keybindings (#1151)
* Manage app.ts litegraph keybindings

* nit
2024-10-07 16:50:58 -04:00
Alex "mcmonkey" Goodwin
ff1ca268a4 Model Library sidebar: allow searching metadata (#1148)
* Model Library sidebar: allow searching metadata

title, description, etc

* don't use vue stuff inside of vue because vue doesn't support vue

very cool

* remove old import

* and that one
2024-10-07 14:50:45 -04:00
Chenlei Hu
99c948f578 Update README (#1149) 2024-10-07 14:46:43 -04:00
Alex "mcmonkey" Goodwin
d68a1116dc ModelToNodeStore minor fix (#1147) 2024-10-07 13:13:15 -04:00
Chenlei Hu
dee1ec1a2a Update Litegraph (TypeScript conversion) (#1145)
* Fix various type errors

* Fix rest of ts errors

* update litegraph

* nit
2024-10-07 11:31:54 -04:00
bymyself
9cbfc9856b Fix widget/input conversion on text widgets (#1129)
* Support converting dynamically created text widgets to input

* Fix array contains

* Add test wait

* Try to fix test only failing in CI

* Fix test: Disable conversion option nesting in contextmenu
2024-10-07 11:22:28 -04:00
bymyself
a95a6f9b47 Fix saved workflow cleanup in menu tests (#1142)
* Fix saved workflow cleanup in menu tests

* Clear workflow dir before each test
2024-10-07 09:35:15 -04:00
Chenlei Hu
c83ce863d7 Rework command menu extension API (#1144)
* Rework command menu API

* Update test

* Update README

* Prevent register other extension's command
2024-10-06 23:31:57 -04:00
Chenlei Hu
05aa78372b Add keybinding search (#1143) 2024-10-06 22:40:20 -04:00
Chenlei Hu
38e3dcbaeb Add frontend extension management panel (#1141)
* Manage register of extension in pinia

* Add disabled extensions setting

* nit

* Disable extension

* Add virtual divider

* Basic extension panel

* Style cell

* nit

* Fix loading

* inactive rules

* nit

* Calculate changes

* nit

* Experimental setting guard
2024-10-06 22:15:33 -04:00
Chenlei Hu
cfa763962e Update playwright test fixture (#1139)
* Update playwright test fixture

* fix resolve

* nit

* Wait dialog close
2024-10-06 21:21:55 -04:00
Chenlei Hu
8c156cc651 Replace window.dialog with prompt dialog in workflows.ts (#1138) 2024-10-06 19:31:46 -04:00
Chenlei Hu
c7aabecc0e 1.3.10 (#1137) 2024-10-06 17:25:09 -04:00
Chenlei Hu
defacf3398 Remove unused code (#1136) 2024-10-06 16:22:48 -04:00
Chenlei Hu
7f2920644e Revert "Remove model library searchbox (#1133)" (#1135)
This reverts commit 1b3cc4de1a.
2024-10-06 16:02:14 -04:00
Chenlei Hu
c92ff79231 Add workflow tab tooltip to show full path (#1134) 2024-10-06 12:10:54 -04:00
bymyself
3c70c1e463 Show keybinding on topbar dropdown menus (#1127)
* Show keybinding on topbar dropdown menus, resolve #1092

* Add text-muted to tailwind config

* Add Playwright test

* Preserve Primevue classes in menu item template

* Extend MenuItem

* Revert adding undo/redo to core keybindings

* Change test selector

* refactor

* Extract as component

* refactor

* nit

* fix extension API

---------

Co-authored-by: huchenlei <huchenlei@proton.me>
2024-10-06 12:08:16 -04:00
Chenlei Hu
1b3cc4de1a Remove model library searchbox (#1133) 2024-10-06 11:44:38 -04:00
Chenlei Hu
b97331cbab Restyle SettingGroup (#1125)
* Setting group CSS

* nit

* nit

* Dim label color

* nit

* Set width
2024-10-05 20:34:15 -04:00
Chenlei Hu
b7287dbb22 1.3.9 (#1124) 2024-10-05 18:09:51 -04:00
Chenlei Hu
2c90735bb1 Restore top/bottom menu location setting (#1123)
* Rename floating to top

* Adjust teleport target

* Fix dropdown direction for bottom menubar

* Fix z-index
2024-10-05 18:08:48 -04:00
Chenlei Hu
4d5fbeff45 Add eslint-plugin-unused-imports (#1121) 2024-10-05 16:36:02 -04:00
Chenlei Hu
ad55722662 Docking action bar on top menu bar (#1119)
* Teleport when docked

* Docking logic

* Remove unnecessary v-show

* Docked panel style

* Drop zone highlight

* Rename test

* Add playwright test
2024-10-05 14:06:29 -04:00
Chenlei Hu
9c118c8e37 Add '- ComfyUI' suffix on browser title (#1118)
* Add '- ComfyUI' suffix on browser title

* Update test expectations
2024-10-05 11:35:17 -04:00
Chenlei Hu
267660a661 Extract QueueButton as component (#1117) 2024-10-05 10:07:50 -04:00
filtered
f2017291d6 Prevent converted widget being duplicated (#1115) 2024-10-05 08:49:17 -04:00
Chenlei Hu
4cc69544b5 Replace window.alert with toast alert (#1112)
* Replace window.alert with toast alert

* Mock jest
2024-10-04 22:00:44 -04:00
Chenlei Hu
2649d72d3f Refactor sidebarTabStore (#1111) 2024-10-04 21:20:35 -04:00
Chenlei Hu
a852b8e6e1 [skip ci] Add ersionAdded to newly added commands (#1110) 2024-10-04 20:41:05 -04:00
Chenlei Hu
6deb994235 Add command to switch opened workflow tabs (#1109) 2024-10-04 20:33:16 -04:00
Chenlei Hu
b30d285025 Add toggle command for each sidebar tab registered (#1108)
* Add toggle command for each sidebar tab registered

* nit
2024-10-04 20:22:10 -04:00
Chenlei Hu
18476d28dc 1.3.8 (#1107) 2024-10-04 16:32:46 -04:00
Chenlei Hu
57a4cb9036 Add default toast error handling for command execution (#1106)
* Error handling execute command

* Cleanup

* Add playwright test

* Mock i18n in jest test

* Reduce test func timeout
2024-10-04 16:28:08 -04:00
Chenlei Hu
ebc71b0e46 Add PromptDialog to replace window.prompt (#1104)
* Save file prompt dialog

* Don't download if dialog dismissed

* refactor

* style dialog

* nit

* Autofocus
2024-10-04 15:33:27 -04:00
Chenlei Hu
39d68bcdc4 Remove new default workflow button in workflows sidebar (#1100) 2024-10-04 11:37:37 -04:00
Chenlei Hu
e20126a254 Rename AppMenu to Actionbar (#1099)
* Rename AppMenu to Actionbar

* nit

* nit
2024-10-04 06:49:06 -04:00
Chenlei Hu
416fd0aed6 Restyle action bar (#1098) 2024-10-04 06:30:55 -04:00
Zoltán Dócs
661b8081c1 Show in-progress preview of the running task in the queue (#1091) 2024-10-03 18:48:06 -04:00
Chenlei Hu
64b5f4e7d5 1.3.7 (#1090) 2024-10-03 17:04:54 -04:00
Chenlei Hu
1775d43d90 Support keybinding customization (#1081)
* Basic keybinding panel

nit

Make row selectable

Reduce padding

Better key seq render

Show actions on demand

Turn off autocomplete

nit

Persist keybindings

Autofocus

Fix set unsetted keybinding bug

Refactor

Add reset button

Add back default keybinding logic

Report key conflict error

Adjust style

fix bug

Highlight modified keybindings

* Set current editing command's id as dialog header
2024-10-03 16:58:56 -04:00
Chenlei Hu
142882a8ff Move keybinds to coreKeybindings (#1078)
* Refactor core keybinds

* Prevent default

* Add playwright test
2024-10-03 11:25:53 -04:00
Robin Huang
65cad74eba Add model download URL and change to use safetensors. (#1076) 2024-10-03 09:08:26 -04:00
Chenlei Hu
77aaa38a92 [Extension API] Custom commands and keybindings (#1075)
* Add keybinding schema

* nit

* Keybinding store

* nit

* wip

* Bind condition on ComfyCommand

* Add settings

* nit

* Revamp keybinding store

* Add tests

* Add load keybinding

* load extension keybindings

* Load extension commands

* Handle keybindings

* test

* Keybinding playwright test

* Update README

* nit

* Remove log

* Remove system stats fromt logging.ts
2024-10-02 21:38:04 -04:00
Chenlei Hu
ea3d8cf728 1.3.6 (#1073) 2024-10-02 16:02:37 -04:00
Alex "mcmonkey" Goodwin
b3a624a572 Allow dragging model library outputs onto existing nodes (#1004)
* allow multiple compatible node registrations for model type

* allow dragging model library outputs onto existing nodes

* easier registration

* add alt loaders for checkpoint and lora
2024-10-02 15:53:19 -04:00
bymyself
a737be7e16 Fix group node copy paste (#1069)
* Fix group node copy paste

* nit

---------

Co-authored-by: huchenlei <huchenlei@proton.me>
2024-10-02 15:51:33 -04:00
Acly
aca2194892 Emit graph changed event after modifying a widget value via keyboard (#1072) 2024-10-02 15:28:42 -04:00
Chenlei Hu
3a2b2f9e15 Add toggle link visibility button on canvas menu (#1070)
* Basic link visibility toggle

* Icon change

* nit

* Update litegraph

* nit

* Add playwright test

* Update test expectations [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-10-02 15:23:37 -04:00
Chenlei Hu
a19f713c57 Revert "Hide Comfy.Workflow.WorkflowTabsPosition (#984)" (#1071)
This reverts commit a41f3b1ac6.
2024-10-02 15:14:23 -04:00
Chenlei Hu
8b2ef3c352 Use jpg for template workflow thumbnail (#1066) 2024-10-02 12:11:09 -04:00
pythongosssss
861bcabd66 Add support for multiple changes in a single ChangeTracker state (#1022)
* wip

* Add tests

* Update package

* remove logs

* nit

* nit

---------

Co-authored-by: huchenlei <huchenlei@proton.me>
2024-10-02 11:53:54 -04:00
Alex "mcmonkey" Goodwin
cc2b64df52 add filename in model preview popup (#1005)
* add filename in model preview popup

for #1003

* user setting for model name fomat in the tree

* add a tooltip for the setting to explain what things mean

* more explicit file_name naming

* touch of additional text in the tooltip
2024-10-02 10:59:01 -04:00
Alex "mcmonkey" Goodwin
a7a0035b0e allow custom bypass color (#993)
* allow custom bypass color

* shove the not-litegraph-color-def into litegraph

* prettier just does this to be funny i swear
2024-10-02 10:49:59 -04:00
bymyself
31b1aeeb69 Add test for selecting nodes on mac (#1055)
* Add test for selecting nodes on mac

* Deselect nodes in teardown

* Fix unstable test. Remove test on unimplemented feature
2024-10-02 10:42:57 -04:00
Chenlei Hu
3f10fd53bd 1.3.5 (#1061) 2024-10-01 16:39:36 -04:00
Chenlei Hu
0194d76722 Make max batch count configurable (#1060) 2024-10-01 15:42:12 -04:00
Chenlei Hu
98a0291bbd Remove update-main action (#1058) 2024-10-01 13:14:53 -04:00
Chenlei Hu
f9fc36f0ed Cleanup zip file on release (#1057) 2024-10-01 13:13:18 -04:00
pythongosssss
a2bd2a9bae Implement creating inputs by dragging link to widget (#1021)
* Implement creating inputs by dragging link to widget

* Update litegraph

* Add playwright test

* Update test expectations [skip ci]

---------

Co-authored-by: huchenlei <huchenlei@proton.me>
Co-authored-by: github-actions <github-actions@github.com>
2024-10-01 12:53:38 -04:00
Chenlei Hu
c42222cf0d Update litegraph (Pan when dragging link) (#1056)
* Update litegraph (Pan when dragging link)

* Update test expectations [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-10-01 10:54:28 -04:00
Chenlei Hu
271a4979b7 [Skip CI] Remove outdated roadmap (#1052) 2024-09-30 20:25:35 -04:00
Chenlei Hu
1c980397b8 1.3.4 (#1051) 2024-09-30 20:21:52 -04:00
Chenlei Hu
5d957a05b9 Replace locking/unlocking of canvas with select/pan mode (#1050)
* Add dragging canvas state

* Change icon

* Add playwright test

* tooltip change

* Enable for legacy menu

* Update litegraph

* Hide canvas menu for test
2024-09-30 20:20:49 -04:00
pythongosssss
f75f774ddb Graph canvas menu (#1023)
* add graph canvas menu

* Move to corner

* Remove action bar reset zoom button

* nit

* Add setting

---------

Co-authored-by: huchenlei <chenlei.hu@mail.utoronto.ca>
2024-09-30 16:06:43 -04:00
bymyself
224c0080ee Fix topbar submenu width on mobile/tablet (#1047)
* Fix topbar submenu width on tablet

* Add Playwright test

* Use better selector

* nit

---------

Co-authored-by: Chenlei Hu <chenlei.hu@mail.utoronto.ca>
2024-09-30 15:21:09 -04:00
bymyself
6ea5fea1a7 Fix menu drag on touch device (#1046) 2024-09-30 15:15:58 -04:00
bymyself
04e1344676 Fix closing saved workflows (#1049)
* Fix workflow save. Resolves #996 Resolves #1048

* Add test on closing tabs

* Add test on closing open workflows in sidebar
2024-09-30 15:09:24 -04:00
Chenlei Hu
0117964ca5 1.3.3 (#1036) 2024-09-28 11:22:19 +09:00
Chenlei Hu
9d110d39b2 Make action bar draggable (#1035)
* Basic draggable

* Nowrap

* Prevent double reset

* Persist position

* nit

* nit

* Window resize adjustment

* Fix playwright test
2024-09-28 11:21:38 +09:00
Chenlei Hu
8d7693e5ad 1.3.2 (#1024) 2024-09-27 15:31:48 +09:00
Yoland Yan
ec9a30d269 Minor: change app-menu bottom spacing (#1020) 2024-09-27 15:10:13 +09:00
Chenlei Hu
56fc2dd753 Update litegraph (Canvas readonly attr change event) (#1019) 2024-09-27 15:00:54 +09:00
Chenlei Hu
9050591ff9 Update litegraph (Drag cursor shape) (#1014) 2024-09-27 10:51:52 +09:00
Alex "mcmonkey" Goodwin
0cf21b190c don't remove invalid dropdown values (#998)
for #963
2024-09-27 09:03:55 +09:00
Alex "mcmonkey" Goodwin
2531ec178e minor css improvement with editable text (#990) 2024-09-27 08:26:24 +09:00
Alex "mcmonkey" Goodwin
81119acaf2 add a hotkey for settings (#991)
* add a hotkey for settings

for #942

* playwright test for settings menu hotkey

* make hotkey intercompatible with both old and new UI
2024-09-27 08:25:56 +09:00
Chenlei Hu
05f999903d 1.3.1 (#989) 2024-09-26 16:23:35 +09:00
Chenlei Hu
66c02d1e3a Revert "Mark Comfy.Workflow.ShowMissingModelsWarning as stable & enabled by d…" (#988)
This reverts commit cdaa0bda5b.
2024-09-26 16:19:09 +09:00
Chenlei Hu
a05df99a8a Update litegraph (Link copy) (#986)
* Update litegraph (Link copy)

* Update readme
2024-09-26 16:06:59 +09:00
Chenlei Hu
e200e2f89c Add model info for flux workflow template (#987) 2024-09-26 16:06:37 +09:00
Chenlei Hu
cdaa0bda5b Mark Comfy.Workflow.ShowMissingModelsWarning as stable & enabled by default (#985) 2024-09-26 15:10:17 +09:00
Chenlei Hu
a41f3b1ac6 Hide Comfy.Workflow.WorkflowTabsPosition (#984) 2024-09-26 15:10:03 +09:00
Chenlei Hu
3585cb69f5 Allow extension register custom topbar menu command (#982)
* Refactor command store

* Rename coreMenuStore to menuStore

* Extension API to register command

* Update README

* Add playwright test
2024-09-26 10:44:15 +09:00
Chenlei Hu
a53f0ba4db 1.3.0 (#981) 2024-09-26 08:51:13 +09:00
huchenlei
7300f6edc2 Format 2024-09-26 08:48:51 +09:00
christian-byrne
1126eaa346 Add test workflow 2024-09-26 08:48:51 +09:00
christian-byrne
7e5d82d0e8 Use visibility of textarea to determine if loaded correctly 2024-09-26 08:48:51 +09:00
christian-byrne
2b2b1cdb85 Add Playwright test 2024-09-26 08:48:51 +09:00
christian-byrne
0b7c1609fd Fix nodes with only optional inputs 2024-09-26 08:48:51 +09:00
Chenlei Hu
7760f91a56 Revert "Disable broken playwright test by backend change (#967)"
This reverts commit da651eee6f.
2024-09-25 19:20:08 +09:00
七海千秋
58d8ab40c4 add missing i18n messages
added all missing chinese messages
2024-09-25 19:18:59 +09:00
Chenlei Hu
4ab3aa9a39 Backward compatibility with extension injections on legacy menu bar (#970)
* Compatible to legacy top menu extensions

* Rework css

* nit
2024-09-25 16:01:50 +09:00
AustinMroz
9199639320 Reset FileInput value after load (#958)
When a file is browsed for, the fileInput remains associated with the
chosen file even after the associated workflow is loaded. If a user
attempts to load the same file again, the onchange event does not fire
and loading fails silently. This is fixed by clearing the fileInput
after the workflow has been loaded.

Out of an abundance of caution, the onchange is made async to ensure the
fileInput isn't cleared until after the workflow has loaded.

Add a test to check if the same workflow file can be loaded
consecutively.
2024-09-25 16:01:50 +09:00
Chenlei Hu
5ee0fd3519 Rework queue button (#968)
* Move queue button to right side

* Rework split button

* Group

* Remove unused code

* x2 buttons

* Use primevue divider

* adjust style

* Add tooltip

* Update test

* Add clearing pending tasks button to queue bar

* Fix state

* Dropdown list fix
2024-09-25 16:01:50 +09:00
AustinMroz
59976ea357 Reduce SearchBox margins for lower resolutions. (#959)
On lower screen widths, the SearchBox would scale itself down instead of
reducing the fairly wide margins. This wastes space and has reduces the
usability of the search box contents itself by cutting off information
(such as the experimental badge) on nodes with medium or longer titles

This is not without side effects, so further adjustments may be needed.
Currently, the searchbox is slightly offset to the right even for wide
screens and the adjustments are disabled for very small screens (<=768)
such that the preview is offscreen, but the entirety of the searchbox is
properly displayed down to 512
2024-09-25 16:01:50 +09:00
Chenlei Hu
62bddded37 Move clipspace from action bar to topbar dropdown menu (#956) 2024-09-25 16:01:50 +09:00
Alex "mcmonkey" Goodwin
35a7c81fd8 initial download-folder-selector interface (#890)
* initial download-folder-selector interface

* use primevue select

* add a folder select visibility checkbox

* slightly reduce indirection

* fix up select box updating

* revert bad upstream changes

* cleanup

* allow localhost sourced models in ui side

(for testing purposes only basically, but does no harm in deployed envs)

* add screenshot expectations to test

* Update test expectations [skip ci]

* add testing of folder select

* fix test

* don't exclude folder selector when there's only 1

since the checkbox covers that better anyway

* oo - fix checkbox

* Update test expectations [skip ci]

* testing - don't expect screenshots :(

* experimental new test code

* toHaveClass is silly

* add // comments documenting intent of allowedSources

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-09-25 16:01:50 +09:00
AustinMroz
30469a6d88 Support redo with Ctrl+Shift+Z (#957) 2024-09-25 16:01:50 +09:00
Alex "mcmonkey" Goodwin
d2d645f74b better badges for empty/loading model library folders (#953)
* better badges for empty/loading model library folders

for #945

* fix total count on loaded nodes

* fix test break

* additional test fix

* use a null prop fallback instead of having to explicitly calc

* patch
2024-09-25 16:01:50 +09:00
Chenlei Hu
4e5bcd6a3b Migrate deprecated setting values (#954) 2024-09-25 16:01:50 +09:00
Chenlei Hu
6a8287e192 Show opened workflows as topbar tabs (#952)
* Basic tab switching

* Closing tabs

* Style buttons

* wip

* Fix scroll style

* Add setting

* Add playwright test

* Add unsaved status

* nit
2024-09-25 16:01:50 +09:00
Alex "mcmonkey" Goodwin
7b344d5629 Fix routing (#939)
* reinstate working routing code and remove broken code

* forward object_info

* remove object_info bit
2024-09-25 16:01:50 +09:00
Alex "mcmonkey" Goodwin
35579e644e don't show redundant model previews (#949)
for #944
2024-09-25 16:01:50 +09:00
Chenlei Hu
1bc78a716e Fix action bar commands (#946) 2024-09-25 16:01:50 +09:00
Chenlei Hu
0d28c108d2 Add topbar dropdown menu (#937)
* Add basic menu

* Add workflows/edit to menu bar

* Add command store

* Fix z-index

* Fix beta menu setting switch

* nit

* Drop to center

* Fix command invocation
2024-09-25 16:01:50 +09:00
Alex "mcmonkey" Goodwin
6a158d46b8 [Draft] Model library sidebar tab (#837)
* basic/empty model library sidebar tab

in-progress

* make it actually list out models

* extremely primitive search impl

* list out available folders

(incomplete list atm)

* load list dynamically

* nice lil loading icon

* that's not doing anything

* run autoformatter

* fix up some absolute vue shenanigans

* swap to pi-box

* is_fake_object

* i think apply the tailwind thingo

* trim '.safetensors' from end of display title

* oop

* after load, retain title if no new title is given

* is_load_requested to prevent duplication

* dirty initial model metadata load & preview

based on node preview code

* update model store tests

* initial image icon for model lib

* i hate this

* better empty spacer

* add api handler for '/models'

* load model folders list instead of hardcoding

* add a 'no content' placeholder for empty folders

* autoformat

* autoload model metadata

* error handling on metadata loading

* larger model icons

* click a model to spawn a node for it

* draggable model nodes

* add a setting for whether to autoload or not

* autoformat will be the death of me

* cleanup promise code

* make the model preview actually half-decent

* revert bad unchecked change

* put registration back
2024-09-25 16:01:50 +09:00
pythongosssss
bf7652227a Workflow templates (#938)
* Add template gallery

* Add simple test

* Add examples

* Enable floating menu in test
2024-09-25 16:01:50 +09:00
Chenlei Hu
2aaee5c331 Remove support of Top/Bottom in menu positions (#933)
* Remove support of Top/Bottom in menu positions

* Update menu positions in test setting

* nit
2024-09-25 16:01:50 +09:00
huchenlei
fa2884f9b2 Resolve merge conflict 2024-09-25 16:01:50 +09:00
ArtificialLab
9c7ea5bd87 Fix routing (#929)
* fix router and move graph related parts to GraphView.vue

* (fix) add back child element in UnloadWindowConfirmDialog

* (cleanup) remove empty callback

* (fix) routing issue when base url is not webroot

* add back DEV_SERVER_COMFYUI_URL
2024-09-25 16:01:50 +09:00
Chenlei Hu
5e51ae37cf Relands "Fix routing and layout issue" (#931)
* Revert "Revert "Fix routing and layout issue (#923)" (#930)"

This reverts commit 1e2dfea173.

* Fix merge conflicts
2024-09-25 16:01:50 +09:00
Chenlei Hu
4fa3a38f98 Revert "Fix routing and layout issue (#923)" (#930)
This reverts commit 94db2e90da.
2024-09-25 16:01:50 +09:00
ArtificialLab
d735513e60 Fix routing and layout issue (#923)
* fix router and move graph related parts to GraphView.vue

* (fix) add back child element in UnloadWindowConfirmDialog

* (cleanup) remove empty callback
2024-09-25 16:01:50 +09:00
filtered
38c2ec7532 Fix workflow search cannot find uppercase letters (#908) 2024-09-25 16:01:50 +09:00
Chenlei Hu
f4d4cc3439 Move workflow dropdown to sidebar tab (#893)
* Initial move to sidebar

Remove broken CSS

Move action buttons

Migrate open workflows

Add basic browse

WIP

Add insert support

Remove legacy workflow manager

Remove unused CSS

Reorder

Remove legacy workflow UI

nit

* Support bookmark

Add workflow bookmark store

nit

Add back bookmark functionality

Correctly load bookmarks

nit

Fix many other issues

Fix this binding

style divider

* Extract tree leaf component

* Hide bookmark section when no bookmarks

* nit

* Fix save

* Add workflows searchbox

* Add search support

* Show total opened

* Add basic test

* Add more tests

* Fix redo/undo test

* Temporarily disable browser tab title test
2024-09-25 16:01:50 +09:00
Chenlei Hu
4ae066c57d Proxy ComfyWorkflow objects (#869)
* Proxy ComfyWorkflow objects

* nit
2024-09-25 16:01:50 +09:00
pythongosssss
2d1ff64951 Floating menu option (#726)
* Add floating menu

* Fix

* Updates

* Add auto-queue change test

* Fix
2024-09-25 16:01:50 +09:00
Chenlei Hu
73a7f7dae0 1.2.64 (#972) 2024-09-25 15:56:09 +09:00
Chenlei Hu
cf6367b649 Fix node bypass color (#971)
* Fix node bypass color

* Update test expectations [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-09-25 15:54:05 +09:00
bymyself
84fc0e9205 Fix group node naming compatibility (#969)
* Convery legacy group node names in workflow

* Add Playwright test

* Remove hardcoded strings
2024-09-25 15:25:08 +09:00
bymyself
941f71faea Move groupnode test teardown logic to afterEach (#965) 2024-09-25 11:53:52 +09:00
Chenlei Hu
da651eee6f Disable broken playwright test by backend change (#967) 2024-09-25 11:41:11 +09:00
Chenlei Hu
eed00f97f9 Disable jest test on audio_stable_audio_example.flac.json (#966)
* Disable jest test on audio_stable_audio_example.flac.json

* nit

* nit
2024-09-25 11:33:23 +09:00
Chenlei Hu
2387a5e9bd 1.2.63 (#955) 2024-09-24 16:30:45 +09:00
bymyself
6b9c1b70ba Fix group node bookmarking (#950)
* Resolves #926 group node bookmark

* Remove expect outside scope of test

* Update unit tests

* Update group node manager path separators

* Update group node path sepator in fixture
2024-09-24 16:26:02 +09:00
bymyself
b21c0f59f9 Apply node opacity setting to all node colors (#947)
* Apply opacity to node colors. Resolves #928

* Handle default and custom colors all in draw handler

* Add colorUtil unit tests

* Add Playwright test

* Remove comment

* Revert colorPalette.ts changes

* Remove unused imports

* Fix typo

* Update test expectations [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-09-24 16:19:53 +09:00
dependabot[bot]
5d8e8a2486 Bump rollup from 4.22.0 to 4.22.4 (#951)
Bumps [rollup](https://github.com/rollup/rollup) from 4.22.0 to 4.22.4.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.22.0...v4.22.4)

---
updated-dependencies:
- dependency-name: rollup
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-24 14:57:46 +09:00
Alex "mcmonkey" Goodwin
423df92ff8 allow browser tests update off main branch (#940)
why was this blocked??
2024-09-23 19:43:04 +09:00
Chenlei Hu
04a950d7f5 More robust group node playwright test (#935)
* More robust group node playwright test

* nit

* nit
2024-09-23 14:30:45 +09:00
Chenlei Hu
65560604a8 Revert "test to validate subrouting (#927)" (#936)
This reverts commit 6a3dbe08de.
2024-09-23 14:30:17 +09:00
Chenlei Hu
78dea484c9 Fix type for Comfy.Sidebar.Size in apiTypes (#932) 2024-09-23 11:31:18 +09:00
Alex "mcmonkey" Goodwin
6a3dbe08de test to validate subrouting (#927)
* test to validate subrouting

* Update test expectations [skip ci]

* core tests need to prep the page

* Update test expectations [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-09-23 09:24:49 +09:00
Chenlei Hu
9aa976fdf0 Show total/free ram in about page (#924) 2024-09-22 17:40:40 +09:00
Chenlei Hu
39eeda8430 1.2.62 (#922) 2024-09-22 16:40:00 +09:00
Chenlei Hu
2878952b1d Makes forceInput node input slot correctly reflect option/required state (#921)
* Correctly style optional force input input slot

* Add force input playwright test

* Update test expectations [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-09-22 16:35:56 +09:00
pythongosssss
223a1f677b Fix links being lost after manage group node (#916)
* Fix links being lost after manage group node

* Change to use groupnodebuilder

* Make test more reliable
2024-09-22 16:17:39 +09:00
Chenlei Hu
7b4b40db5b Update litegraph (Slot style) (#919)
* Update litegraph (Slot style)

* Update test expectations [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-09-22 16:15:39 +09:00
Chenlei Hu
1052603a17 Revert "Any keyboard layout for Ctrl + V, Z, Y... (#763)" (#920)
This reverts commit 23796d9040.
2024-09-22 16:14:59 +09:00
pythongosssss
4ee1b23e9b Exclude litegraph from being cached (#918) 2024-09-22 15:25:52 +09:00
bymyself
326e0748c0 Add node opacity setting (#909)
* Add node opacity setting

* Add colorUtil unit test

* Add playwright test

* Update test expectations [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-09-22 15:18:38 +09:00
ArtificialLab
ea0f74a9f6 Cleanup (#915)
* (update) cleanup:
- move reflect to main.ts
- add config.ts with comfy frontend version
- cleanup index.html and App.vue

* (fix) lint doesn't like branch assignments

* (fix) properly add __COMFYUI_FRONTEND_VERSION__ to ts globals
2024-09-22 10:12:54 +04:00
Chenlei Hu
cdaac0d9bb 1.2.61 (#913) 2024-09-22 12:14:13 +09:00
Chenlei Hu
f749734863 Make optional node input's slot hollow circle (#912)
* Use hollow circle for optional input

* nit

* Show hollow shape for optional input

* Add playwright tests

* Update litegraph

* Update test expectations [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-09-22 12:12:48 +09:00
bymyself
a15c4d1612 Fix audio widget serialize option (#910) 2024-09-22 11:54:07 +09:00
bymyself
290bf52fc5 Fix frontend node tooltip error (#911) 2024-09-22 11:52:35 +09:00
dmx
529e889d0e (move) treenode style to style.css 2024-09-22 06:09:56 +04:00
dmx
5a5a69de17 (UI) NodeTree 2024-09-22 06:04:45 +04:00
dmx
194549a4b0 (clean) CSS in App.vue 2024-09-22 06:03:54 +04:00
bymyself
4052fc55f3 Fix node preview styles (#903)
* Use colorPalette to style node previews

* Use widget text secondary color for description

* Remove unused css

* nit

---------

Co-authored-by: huchenlei <chenlei.hu@mail.utoronto.ca>
2024-09-22 09:05:06 +09:00
Chenlei Hu
82d03b5c1b Add colorPalette cleanup for playwright test (#907) 2024-09-22 08:53:01 +09:00
filtered
c7f123766e Add TS types / merge ComfyLGraphNode (#902)
* Add TS type for LGraphNodeConstructor

* Add TS type & move shared prop to parent

* Add TS types - Comfy augmentations

* nit - TS type

* Merge ComfyLGNode into existing augmentations

* nit - fix missed explicit type on import
2024-09-21 18:18:27 +09:00
filtered
88acabb355 Fix TS type on InputSpec (#901) 2024-09-21 14:12:39 +09:00
Chenlei Hu
e5f1eb8609 Update browser tests README (#900) 2024-09-21 10:49:29 +09:00
Chenlei Hu
eb7ab0860d 1.2.60 (#896) 2024-09-20 19:52:09 +09:00
Chenlei Hu
9ed3545b95 Fix frontend node conflicting with node badge (#895) 2024-09-20 19:45:34 +09:00
Chenlei Hu
d223f3865b 1.2.59 (#892) 2024-09-20 09:25:50 +09:00
Chenlei Hu
4538db86cf Add node execution progress to browser title (#891)
* Add node execution progress to browser title

* nit

* nit
2024-09-20 09:22:09 +09:00
dependabot[bot]
3931cae044 Bump vite from 5.3.3 to 5.4.6 (#889)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.3.3 to 5.4.6.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.6/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.6/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-19 20:11:34 +09:00
Chenlei Hu
810a63f808 Support async hooks in TreeExplorerNode (#888)
* Support async hooks in TreeExplorerNode

* rebase

* nit

* Fix component test failure

* Add edit vitest

* Add more tests

* Add component test
2024-09-19 20:10:43 +09:00
Chenlei Hu
609984d400 No selection on tree node if selectionKeys prop is not set (#887) 2024-09-19 16:48:56 +09:00
Chenlei Hu
a57c958058 Bind extra context menu items on TreeExplorerNode interface (#886) 2024-09-19 14:51:07 +09:00
Chenlei Hu
b6dbe8f07b Shorten node source package name by remove ComfyUI prefix/suffix (#883)
* Shorten node source package name by remove ComfyUI prefix/suffix

* Update test expectations [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-09-19 12:40:05 +09:00
Chenlei Hu
29d69338ef 1.2.58 (#882) 2024-09-19 12:00:45 +09:00
Chenlei Hu
98de010811 Fix node searchbox filter removal (#881) 2024-09-19 11:58:29 +09:00
Chenlei Hu
63302a6634 Fix sorting on type filter + empty query (#880)
* Fix sorting on type filter + empty query

* nit

* nit
2024-09-19 11:22:23 +09:00
Chenlei Hu
8568e037bf Sort search result by node frequency (#879)
* Sort search result by node frequency

* Fix jest test
2024-09-19 10:09:54 +09:00
Chenlei Hu
6c4143ca94 Show node by frequency on empty query (#878) 2024-09-19 09:35:22 +09:00
Chenlei Hu
efa2fa269d 1.2.57 (#868) 2024-09-18 09:38:27 +09:00
Chenlei Hu
a2cf6a7be2 Refactor TreeExplorer (Add handleClick hook) (#867)
* Refactor TreeExplorer (Add handleClick hook)

* nit
2024-09-18 09:36:21 +09:00
bymyself
e493473c35 Add tests on using group nodes in library sidebar (#864)
* Add tests on adding group node from library sidebar

* Improve test name clarity
2024-09-18 09:09:35 +09:00
MaraScott
415a2e7fa5 add square pointer (#848)
* add square pointer

* create enum + refactorize to create init_shape and draw_shape methods
2024-09-17 17:24:07 +09:00
Chenlei Hu
ba9a3b4a9b Move workflows management to pinia (#862) 2024-09-17 17:15:20 +09:00
bymyself
174c52958f Add test on mobile canvas panning (#863)
* Add test on mobile canvas panning

* Update test expectations [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-09-17 17:15:05 +09:00
Chenlei Hu
4e41db2d6a [Beta Menu] Shows unsaved state on browser tab title (#860)
* [Beta Menu] Shows unsaved state on browser tab title

* Proper state management

* Add playwright test

* Fix browser tests
2024-09-17 16:14:06 +09:00
bymyself
e8daebdc0c Add group nodes to search and node library (#861)
* Register group nodes in nodeDefStore

* Add playwright tests

* Update test expectations [skip ci]

* Mock nodeDefStore in group node unit test

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-09-17 16:06:58 +09:00
Chenlei Hu
582acd7bd1 1.2.56 (#859) 2024-09-17 11:17:26 +09:00
Chenlei Hu
48fe14e263 [Beta Menu] Show active workflow name on browser tab title (#857) 2024-09-17 11:11:52 +09:00
bymyself
f9fd0f59ff Add nullcheck to snap-to-grid setting (#858) 2024-09-17 11:11:39 +09:00
Chenlei Hu
3fe4b4b856 Add generation progress to browser tab title (#855) 2024-09-17 10:31:29 +09:00
bymyself
c510b344af Allow zero as input slider min/max (#854) 2024-09-17 09:43:32 +09:00
Chenlei Hu
980dd285ad Revert move floating menu to Vue (#853) 2024-09-17 09:33:25 +09:00
Chenlei Hu
2b60244e4a Move setting declarations from ui to coreSettings (#847)
* Move setting declarations from ui to coreSettings

* nit

* nit

* Move effect to vue component
2024-09-16 17:47:47 +09:00
Chenlei Hu
45a866f194 Fix ComfyUI class setup procedure (#846) 2024-09-16 17:14:11 +09:00
Chenlei Hu
091b8a74fb Mock settingStore (#845) 2024-09-16 16:25:32 +09:00
Chenlei Hu
74fa4a2c2d Move inlined settings in settingsStore to a separate file (#844) 2024-09-16 14:46:06 +09:00
Chenlei Hu
327b67a022 Move floating menu to a Vue component (#843)
* Move floating menu to a Vue component

* nit

* Fix jest tests
2024-09-16 14:26:46 +09:00
Chenlei Hu
d0a4db5f4f Update litegraph (Copy connection by shift drag from path) (#841)
* Add playwright tests

* Update lg

* nit

* nit

* Skip tests

* Update test expectations [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-09-16 12:02:48 +09:00
Chenlei Hu
861eaa155f Refactor test fixture dnd (#840)
* Refactor test fixture dnd

* nit
2024-09-16 10:13:25 +09:00
Chenlei Hu
3550e7f7f1 Show bookmark icon on booked mark nodes in node search box (#839) 2024-09-15 17:31:44 +09:00
Chenlei Hu
7d25d976d1 Extract search option as a Vue component (#838) 2024-09-15 17:15:19 +09:00
Chenlei Hu
7025e321de 1.2.55 (#836) 2024-09-15 11:22:27 +09:00
huchenlei
429fa75fcc Update litegraph (Fix group right click) 2024-09-15 11:03:34 +09:00
Chenlei Hu
347563adf9 Move json format functions to formatUtil (#834) 2024-09-15 09:46:34 +09:00
Chenlei Hu
9bdb3c0332 1.2.54 (#830) 2024-09-14 17:12:50 +09:00
Chenlei Hu
12c699cc87 Update litegraph (Getters) (#829)
* Update litegraph (Getters)

* Update
2024-09-14 17:10:57 +09:00
Chenlei Hu
588cfeca4b Replace ComfyApp.runningNodeId with executionStore.executingNodeId (#828)
* Replace ComfyApp.runningNodeId with executionStore.executingNodeId

* nit
2024-09-14 16:01:37 +09:00
Chenlei Hu
f983f42c45 Add executionStore (#827)
* Extract execution store

* Fix executing nodes highlight

* nit

* nit

* nit
2024-09-14 15:15:15 +09:00
Chenlei Hu
fef780a72f Make useTreeExpansion hook accept expandedKeys as param (#826) 2024-09-14 11:27:38 +09:00
Chenlei Hu
ebdcd92977 Extract error handling with toast message as hook (#825) 2024-09-14 11:25:08 +09:00
filtered
c98ea5ba01 Use LiteGraph validation for node search->create (#822)
Adds LiteGraph type to augmentation until LG types are auto-generated
Removes @ts-expect-error
2024-09-14 08:36:48 +09:00
filtered
48f84a46cd Add apiTypes present in docs but missing in zod (#821)
* Add apiTypes present in docs but missing in zod

* Fix prettier check
2024-09-14 08:35:32 +09:00
filtered
9483cfe915 Add graceful correction when widgets undef. (#820)
Fixes crash on load of workflow where `node.widgets` has no `.find()`
2024-09-14 08:33:43 +09:00
Chenlei Hu
862e2c2607 1.2.53 (#818) 2024-09-13 20:59:16 +09:00
Chenlei Hu
a08ec196c7 Fix frontend-only node freezing litegraph (#817) 2024-09-13 20:58:19 +09:00
Chenlei Hu
17db1e6074 Rework userFileStore (#815)
* Rework userFileStore

* nit

* Add back unittests
2024-09-13 17:40:08 +09:00
Chenlei Hu
65a8dbb7e0 1.2.52 (#814) 2024-09-13 16:25:01 +09:00
Chenlei Hu
efd8b5c19d Add playwright test for custom color palette (#812)
* Add playwright test for custom color palette

* nit

* Fix leaked side effect

* nit

* Update test expectations [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-09-13 13:52:16 +09:00
Chenlei Hu
0a188aaf72 Disable zoom when editing titles (#813) 2024-09-13 11:42:24 +09:00
Chenlei Hu
eb45cca031 Pin searchbox at top when node library scrolls (#811)
* minor style fix

* nit

* Pin searchbox at top when node library scrolls
2024-09-13 10:50:06 +09:00
Chenlei Hu
d8d6fa86e4 Add button to clear pending tasks (#810) 2024-09-13 10:23:28 +09:00
Chenlei Hu
880ac4fa5a Add node lifecycle badge text (#809) 2024-09-13 10:04:36 +09:00
Chenlei Hu
7d3b8dc44c Make \n correctly displayed on error message (#805) 2024-09-13 09:04:21 +09:00
Chenlei Hu
1230d92b37 1.2.51 (#804) 2024-09-12 20:23:04 +09:00
Chenlei Hu
8889c4de4a Fix node def registeration (#803) 2024-09-12 20:17:21 +09:00
Chenlei Hu
637f5b501e Add about panel in settings dialog (#799)
* basic about page

* Remove frontend version from setting dialog header

* Style about page

* basic system stats

* Basic styling

* Reword

* Format memory amount
2024-09-12 17:31:19 +09:00
Chenlei Hu
d2b3e325a4 1.2.50 (#798) 2024-09-12 17:16:54 +09:00
Chenlei Hu
c99ca004b4 Fix badge position on collapsed nodes (#797)
* Update litegraph (Proper collapsed node handling)

* Add playwright test

* Update test expectations [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-09-12 17:15:20 +09:00
Chenlei Hu
fa9a415c62 Fix litegraph crash on using custom colorPalette (#795) 2024-09-12 16:56:17 +09:00
Chenlei Hu
da3271fe57 Move InputSlider to common/ (#794) 2024-09-12 15:19:36 +09:00
Chenlei Hu
358c0ce83c Update litegraph (Pin/Unpin selected nodes) (#791)
* Update litegraph (Pin/Unpin selected nodes)

* Checkout head_ref first

* nit

* Update test expectations [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-09-12 15:04:43 +09:00
Chenlei Hu
110c007912 Update litegraph (Proper ContextMenu export) (#790) 2024-09-12 14:09:00 +09:00
Chenlei Hu
fdb01c06f2 Split playwright tests to multiple runners by project (#789) 2024-09-12 11:35:36 +09:00
Chenlei Hu
ca6bf7d054 Split jest unit test and playwright test into different actions (#787)
* Split jest unit test and playwright test into different actions

* Use composite action

* Add tag v1
2024-09-12 10:54:50 +09:00
Chenlei Hu
14f5019556 1.2.49 (#788) 2024-09-12 10:16:59 +09:00
Chenlei Hu
80ca1808f0 Node source/id badge (#781)
* Add basic node badge

* Node source badge

* Prevent manager badge rendering

* Update litegraph (Badge support)

* Add playwright tests

* Separate nodes

* nit

* Checkout devtools repo for browser test expectation CI

* Fix failing unittests

* Rename setting

* Hide all badges in playwright tests

* Handle group node

* Update test expectations [skip ci]

* Fix unittest

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-09-12 09:36:06 +09:00
Chenlei Hu
f2a30ec197 Fix missing model dialog test (#782) 2024-09-11 21:00:32 +09:00
Chenlei Hu
b8bdba0bcc Fix tailwindcss in NoResultsPlaceholder.vue (#780) 2024-09-11 17:38:42 +09:00
Chenlei Hu
baf0bc8de4 Run all playwright tests under dpr=2 (#779)
* Run all playwright tests under dpr=2

* Update test expectations [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-09-11 11:33:40 +09:00
Chenlei Hu
8ce7b515a3 Fix ComfyExtension types (#778) 2024-09-11 10:43:01 +09:00
Chenlei Hu
06a05cb283 Fix loading large workflow embedded in webp (#777)
* Fix loading large workflow embedded in webp

* Update test expectations [skip ci]

* nit

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-09-11 10:12:01 +09:00
bymyself
15758101aa Fix searchbox popover on touch devices (#773)
* Add delay on touch pointer event when opening searchbox

* Add playwright mobile test
2024-09-11 08:55:04 +09:00
ArtificialLab
05b3ad2f59 Front stack primary updates and improvements (#757)
* (fix) index.html formating for prettier

* (add) proper icon management
- on-demand icons auto importing
- handle all available icon sets (https://icones.js.org)

* (fix) proper css management

* (add) front stack improvement:
- implement vue router
- prepare for App.vue simplification
- proper management of views and layouts
- fix Tailwind CSS and prepare for overall css cleaning

* (fix) move back user.css to public dir

* (fix) remove user.css import from main.ts
2024-09-11 08:53:54 +09:00
bymyself
90abf9744c Document registerCustomNodes (#772) 2024-09-10 12:43:39 +09:00
filtered
0e01bb3c07 Fix reroute to wildcard & multi-typed slots (#769)
Use the same type check the rest of the connection process uses.
2024-09-09 17:53:47 +09:00
Chenlei Hu
8b77dde55a [skip ci] Update litegraph dev guide (#770) 2024-09-09 17:51:44 +09:00
Chenlei Hu
a41de30dc5 Fix tailwind css setup (#768)
* Fix tailwind css setup

* Install as dev dep

* Uninstall primeui tailwind
2024-09-09 16:56:32 +09:00
Chenlei Hu
534ea17816 1.2.48 (#767) 2024-09-09 16:20:01 +09:00
Chenlei Hu
913582c7cd [Chore] Update primevue (#766) 2024-09-09 16:14:20 +09:00
Chenlei Hu
3779878b57 Update litegraph (Bug fixes) (#765) 2024-09-09 16:11:28 +09:00
Chenlei Hu
023299cf1a Fix loading workflow from of edited webp (#764)
* Fix loading workflow from of edited webp

* Update test expectations [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-09-09 16:07:15 +09:00
Chenlei Hu
23796d9040 Any keyboard layout for Ctrl + V, Z, Y... (#763)
* Any keyboard layout for Ctrl + V, Z, Y...

`if` conditioning changed for better support any keyboard layout for shortcuts

* Format

---------

Co-authored-by: Khachatur Avanesian <jailbreakvideo@gmail.com>
2024-09-09 10:17:14 +09:00
bymyself
21c3883cc7 Improve beta menu nav accessibility (#762)
* Add ARIA labels to beta menu btns without text

* Adjust test locator
2024-09-09 09:49:43 +09:00
bymyself
616e295262 Improve searchbox accessibility (#760)
* Set field key for search result options label

* Add playwright test

* Add role attr to search dialog
2024-09-09 09:49:02 +09:00
bymyself
c201e86b97 Improve sidebar accessibility (#759)
* Add ARIA label to sidebar buttons

* Add component test

* Add generalized component tests
2024-09-09 09:48:29 +09:00
bymyself
61ee43aa6f [skip ci] Fix search box props type (#753) 2024-09-07 02:34:47 -04:00
bymyself
08a1fd0056 Dismiss gallery lightbox on background click (#752)
* Dismiss gallery on background click

* Add vitest tests
2024-09-07 02:32:56 -04:00
Chenlei Hu
56f3842045 1.2.47 (#747) 2024-09-05 11:41:00 -04:00
Chenlei Hu
81bc0fd9cb Release script (#746)
* Update main repo release script

* update readme [skip ci]
2024-09-05 11:40:05 -04:00
Chenlei Hu
38c957d3a9 Fix load of string node id workflow (#744)
* Update litegraph

* Fix string node id

* Add playwright test

* Update test expectations [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-09-05 11:16:41 -04:00
Chenlei Hu
9d855d637e Ignore missing dialog (#743)
* Do not report missing nodes/models warning repeatedly

* Add playwright tests

* cast finalOptions, add comments to interface

* Use old menu in tests to not break top left click methods

* Assert no dialog on undo and on redo separately

* nit

* nit

---------

Co-authored-by: christian-byrne <abolkonsky.rem@gmail.com>
2024-09-05 11:00:41 -04:00
bymyself
743683c01d Add logging setting display name (#742) 2024-09-05 10:39:22 -04:00
Chenlei Hu
720e7e112d 1.2.46 (#738) 2024-09-04 20:29:14 -04:00
Chenlei Hu
ce157afeac Disable minify on release dist (#737) 2024-09-04 20:28:38 -04:00
Chenlei Hu
95701ab761 Add keyboard shortcut for pin/unpin node (#736)
* Add keyboard shortcut for pin/unpin node

* Add playwright test

* Add nextFrame calls

* Keyboard event on canvas

* disable test
2024-09-04 20:26:10 -04:00
Alex "mcmonkey" Goodwin
060e61f0db initial model store (#674)
* initial model store

* refactor the 'modelstoreserviceimpl' to pinia

* pepper in some reactive

the inner ModelStore (per-folder) can't be pinia because its made of temporary instances, but it can be reactive

* use refs in metadata class

* remove 'reactive'

* remove ref too

* add simple unit tests for modelStore

* make things worse via autoformatting

* move mock impls to a function
2024-09-04 19:59:40 -04:00
Chenlei Hu
6c7fb5041d Replace locking with pining in core (#734)
* Replace locking with pin in core

* Add extra expectation

* Update litegraph

* Update test expectations [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-09-04 19:59:06 -04:00
Chenlei Hu
25a3c30fef [skip ci] Update README (#733) 2024-09-04 10:31:12 -04:00
Chenlei Hu
287bd7ddd0 Add test on text widget popover dismiss (#732)
* Add test on text widget popover dismiss (#719)

* nit

* Update test expectations [skip ci]

---------

Co-authored-by: bymyself <abolkonsky.rem@gmail.com>
Co-authored-by: github-actions <github-actions@github.com>
2024-09-04 10:31:01 -04:00
Chenlei Hu
b396d1a9fe Trigger searchbox on group body db click (#731) 2024-09-04 10:16:12 -04:00
Chenlei Hu
ada8500d21 1.2.45 (#727) 2024-09-03 20:17:34 -04:00
Chenlei Hu
0f32ab334a Update litegraph (Group highlight option) (#725) 2024-09-03 15:34:01 -04:00
Chenlei Hu
36cdebcad1 Restore context menu for new searchbox (#724)
* Searchbox revamp

* nit

* nit

* Add playwright test

* Update litegraph

* Rename setting

* Update test expectations [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-09-03 13:28:26 -04:00
Chenlei Hu
974a7ef63f Store shallowRef of litegraph canvas (#722)
* Store shallowRef of litegraph canvas

* nit
2024-09-03 10:11:31 -04:00
bymyself
b49b19c9b0 [skip ci] Add ToastMessageOptions docs to README (#721) 2024-09-03 08:10:23 -04:00
bymyself
a5d93f6910 Update Settings type with new fields (#718) 2024-09-02 22:50:56 -04:00
Chenlei Hu
8a99124470 1.2.44 (#716) 2024-09-02 20:31:01 -04:00
Chenlei Hu
4a230f720e Edit group name on group creation (With Ctrl + g) (#715)
* Editor store

* Merge editors

* nit

* Edit on group creation

* nit
2024-09-02 20:20:40 -04:00
Chenlei Hu
4ad1e67ebf Double click group title to edit (#714)
* Double click group title to edit

* Add playwright test

* Update test expectations [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-09-02 18:01:02 -04:00
Chenlei Hu
80e4384644 Eager init nodeSearchService (#713) 2024-09-02 15:40:10 -04:00
Chenlei Hu
51b7467012 Auto transforms bugged node pos (#712) 2024-09-02 14:41:45 -04:00
Chenlei Hu
9d69db6db7 Update litegraph (Fix node.pos serialization) (#711) 2024-09-02 14:34:58 -04:00
Chenlei Hu
e733b87f22 Add beforeRegisterVueAppNodeDefs hook (#709)
* Add beforeRegisterVueAppNodeDefs hook

* Remove min-char constraint on node library search
2024-09-02 11:06:16 -04:00
Chenlei Hu
adcef7d2f4 Add locale setting (#708) 2024-09-02 10:01:00 -04:00
mijuku233
8ba5da14bc Custom background image (#698) 2024-09-01 19:46:55 -04:00
Chenlei Hu
c181bf1f55 1.2.43 (#705) 2024-09-01 19:01:16 -04:00
Chenlei Hu
d9a7537169 Fix top level bookmark migration (#706) 2024-09-01 18:55:56 -04:00
Chenlei Hu
75e91137f0 Display node display_name instead of name (#704) 2024-09-01 18:33:19 -04:00
Chenlei Hu
a4a298924e Show node id name in node searchbox (#703)
* Show node unique name in node searchbox

* nit
2024-09-01 18:12:27 -04:00
Chenlei Hu
14da8433f7 Migrate node library sidebar to use unique name instead of display name (#702)
* Migrate node library sidebar to use unique name instead of display name

* Break word
2024-09-01 17:52:53 -04:00
Chenlei Hu
ff2d160230 Reduce divider margin in node library (#701) 2024-09-01 14:49:31 -04:00
Chenlei Hu
b0b5f92940 Add setting to control tree explorer item padding (#700) 2024-09-01 14:36:15 -04:00
Chenlei Hu
d04dbcd2c1 [Major Refactor] Use TreeExplorer on nodeLibrarySidebarTab (#699)
* Basic move

* Add back node bookmark

* Move node preview

* Fix drag node to canvas

* Restore click node to add to canvas

* Split bookmark tree and library tree

* Migrate rename and delete context menu

* Fix expanded keys

* Split components

* Support extra menu items

* Context menu only for folder

* Migrate add folder

* Handle drop

* Store color customization

* remove extra padding

* Do not show context menu if no item

* Hide divider if no bookmark

* Sort bookmarks alphabetically default

* nit

* proper edit

* Update test selectors

* Auto expand on item drop

* nit

* Fix tests

* Search also searches bookmarks tree

* Add serach playwright test
2024-09-01 14:03:15 -04:00
huchenlei
5383f97eba Add tree explorer tree node test 2024-08-31 21:10:32 -04:00
huchenlei
bc7da487e8 Add drop handler 2024-08-31 21:10:32 -04:00
huchenlei
86e7c12e27 Add draggable/droppable flags 2024-08-31 21:10:32 -04:00
huchenlei
50f1ca8eaf Add extra interfaces 2024-08-31 21:10:32 -04:00
huchenlei
280b43fd58 Merge folder and node impl 2024-08-31 21:10:32 -04:00
huchenlei
488f0d82b4 More refactor 2024-08-31 21:10:32 -04:00
huchenlei
bc3ec65967 Move 2024-08-31 21:10:32 -04:00
huchenlei
61342edba0 1.2.42 2024-08-31 09:56:05 -04:00
huchenlei
9247aec03a nit 2024-08-31 09:54:36 -04:00
huchenlei
0e88308571 Remove github button in error dialog 2024-08-31 09:54:36 -04:00
huchenlei
380cbdd5fc 1.2.41 2024-08-30 21:27:06 -04:00
huchenlei
68d6b1f172 Add confirm dialog on window close 2024-08-30 16:51:10 -04:00
Chenlei Hu
70d5e98c73 Update github action run conditions (#682) 2024-08-30 15:32:26 -04:00
Chenlei Hu
9009e784f9 Add component test (Vitest) (#681)
* Add component test (Vitest)

* Fix compile error
2024-08-30 15:32:26 -04:00
Chenlei Hu
877e500510 Update litegraph (ES6 LGraphCanvas) (#679) 2024-08-30 15:32:26 -04:00
Chenlei Hu
aee2afee36 1.2.40 (#685) 2024-08-29 21:30:58 -04:00
Chenlei Hu
f42609c966 Add support for extra system stats in error report (#684)
* Add support for extra system stats in error report

* Add toast on error
2024-08-29 21:29:33 -04:00
pythongosssss
aaea05a37b Sync pr fix clip path when using new menu (#184)
* Sync pr fix clip path when using new menu

* Enable test

* Update outdated test image

* Update test image
2024-08-29 18:02:10 -04:00
pythongosssss
d0067719b8 Fix primitive resize (#683)
* Fix primitive resize on load
Fixes #676

* Add test

* Add playwright test

* Update test expectations [skip ci]

---------

Co-authored-by: huchenlei <chenlei.hu@mail.utoronto.ca>
Co-authored-by: github-actions <github-actions@github.com>
2024-08-29 18:01:57 -04:00
Chenlei Hu
e59ed85cc0 1.2.39 (#675) 2024-08-28 20:22:18 -04:00
Alex "mcmonkey" Goodwin
d5b4311e24 run formatter and add .js to gitattributes (#673)
* run formatter and add .js to gitattributes

* remove npm format change
2024-08-28 17:21:07 -04:00
Alex "mcmonkey" Goodwin
6fed739402 add a git attributes file (#672)
set all ts and vue scripts as LF on Windows as there are scripts (eg the prettier format script) that will break with windows format
2024-08-28 17:03:38 -04:00
Chenlei Hu
b25c5259bd Fix node title editor's width when node is collapsed (#671)
* Fix node title editor's width when node is collapsed

* nit
2024-08-28 16:35:01 -04:00
Chenlei Hu
fb5bb57b0d Mark show missing models warning as experimental (#670)
* Mark show missing models warning as experimental

* nit
2024-08-28 16:22:40 -04:00
Chenlei Hu
986827cb91 Update litegraph (ES6 LGraphNode) (#669) 2024-08-28 15:45:45 -04:00
Chenlei Hu
6f9d2bfa17 Relands "Add support for LiteGraph to convert to classes" (#667)
* Revert "Revert "Add support for LiteGraph to convert to classes (#334)" (#386)"

This reverts commit d607f6c7f7.

* Format
2024-08-28 15:35:05 -04:00
Chenlei Hu
4e8acf6c77 Add setting to disable double click node title edit (#668) 2024-08-28 13:58:33 -04:00
Chenlei Hu
493805d0ee Update README (#664) 2024-08-28 12:00:13 -04:00
Chenlei Hu
2c174b5956 Group selected nodes by Ctrl + g (#663)
* Ctrl + g to group selected noes

* Add playwright test

* nit

* Move button loc

* Update test expectations [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-08-28 11:49:41 -04:00
Chenlei Hu
5c2cb00cd6 Replace autocomplete with select as node filter option input (#659)
* Replace autocomplete with select as node filter option input

* i18n
2024-08-27 21:45:44 -04:00
pythongosssss
968f417061 Node library search filters (#636)
* Add search filters to node library

* Fix

* Dont close on add

* Fix wildcard

---------

Co-authored-by: Chenlei Hu <chenlei.hu@mail.utoronto.ca>
2024-08-27 21:17:34 -04:00
Chenlei Hu
fef9395a2c Add format check CI task (#658) 2024-08-27 20:32:33 -04:00
Chenlei Hu
698754b835 1.2.38 (#657) 2024-08-27 18:23:10 -04:00
filtered
29d21348ca Fix image grid render consistency (#654)
- Makes image grids of non-uniform-size images fill the space the same as uniform image batches
- Re-uses existing code
- Removes workaround
- "Supports image sizes up to 1 terapixel"
2024-08-27 18:22:14 -04:00
Chenlei Hu
09d8f2a502 Missing model download dialog test (#656)
* Test prep

* Add missing model dialog test

* Basic test of download model

* Add comment

* Adjust setting in test

* Change download dir to not interfere with other tests
2024-08-27 17:34:32 -04:00
Chenlei Hu
50b418113c Double click node title to trigger edit (#655)
* Update litegraph

* Double click edit node title

* Update

* Auto select all

* Update litegraph

* Add playwright test

* Update readme

* Update test expectations [skip ci]

---------

Co-authored-by: github-actions <github-actions@github.com>
2024-08-27 11:38:39 -04:00
Chenlei Hu
6ab92f28db Fix dialog maximization (#652) 2024-08-27 09:58:09 -04:00
Alex "mcmonkey" Goodwin
b19cbd9111 Model Downloader Improvements (#650)
* use new download_path value in download api

from https://github.com/comfyanonymous/ComfyUI/pull/4621

* add in-UI refusal for ckpt files
2024-08-27 08:56:57 -04:00
pythongosssss
9cdefca481 Enhancements to the queue image feed (#646)
* Enhancements to the queue image feed
 - Change flat list icon
 - Add cover/contain mode
 - Add right click -> go to node
 - Add go to node link on detail

* Add loading spinner

* resolve comments

---------

Co-authored-by: huchenlei <chenlei.hu@mail.utoronto.ca>
2024-08-26 21:57:23 -04:00
Chenlei Hu
84662ada9e 1.2.37 (#649) 2024-08-26 21:36:40 -04:00
Chenlei Hu
26b02979a1 1.2.36 (#648) 2024-08-26 21:31:38 -04:00
Chenlei Hu
0795c3041c Bookmark folder icon customization (#647)
* Add bookmark customization support

* WIP

* Fix bugs

* Fix color update

* Handle rename and delete of customization

* nit

* Add custom color picker

* Computed final color

* i18n

* Remove cancel button as dialog already has it

* Add playwright test
2024-08-26 21:30:38 -04:00
pythongosssss
c604209f40 Fix crash connecting to node with no slots (#644)
* Fix crash connecting to node with no slots

* Add playwright test

* Update test expectations [skip ci]

---------

Co-authored-by: huchenlei <chenlei.hu@mail.utoronto.ca>
Co-authored-by: github-actions <github-actions@github.com>
2024-08-26 18:29:52 -04:00
Chenlei Hu
96d129e8a6 Fix edit attention test (#640) 2024-08-26 11:31:46 -04:00
Chenlei Hu
a69858c87a Fix unbookmark node in node library (#639)
* Add unbookmark test

* Fix unbookmark
2024-08-26 11:04:08 -04:00
Chenlei Hu
03ac6eea19 Add playwright undo tests (#638) 2024-08-26 10:47:37 -04:00
filtered
f9ae5aaa0f Fix textarea undo history cleared by editAttention (#635) 2024-08-26 10:34:17 -04:00
Chenlei Hu
a5cdebe1a8 Adjust toast location to avoid overlap with side toolbar and menu (#637) 2024-08-26 10:32:34 -04:00
549 changed files with 30763 additions and 8122 deletions

View File

@@ -12,8 +12,12 @@ 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
# Whether to enable minification of the frontend code.
ENABLE_MINIFY=true
ENABLE_MINIFY=true

7
.gitattributes vendored Normal file
View File

@@ -0,0 +1,7 @@
# Default
* text=auto
# Force TS to LF to make the unixy scripts not break on Windows
*.ts text eol=lf
*.vue text eol=lf
*.js text eol=lf

View File

@@ -50,6 +50,12 @@ body:
description: 'Please copy the output from your browser logs here. You can access this by pressing F12 to toggle the developer tools, then navigating to the Console tab.'
validations:
required: true
- type: textarea
attributes:
label: Setting JSON
description: 'Please upload the setting file here. The setting file is located at `user/default/comfy.settings.json`'
validations:
required: true
- type: dropdown
id: browsers
attributes:

View File

@@ -1,11 +1,6 @@
name: ESLint
on:
push:
branches:
- main
- master
- 'dev*'
pull_request:
branches:
- main

23
.github/workflows/format.yaml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: Prettier Check
on:
pull_request:
branches: [ main, master, dev* ]
jobs:
prettier:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: lts/*
- name: Install dependencies
run: npm ci
- name: Run Prettier check
run: npx prettier --check './**/*.{js,ts,tsx,vue}'

View File

@@ -5,50 +5,13 @@ name: Update Playwright Expectations
on:
pull_request:
types: [ labeled ]
branches: [ main, master ]
jobs:
test:
runs-on: ubuntu-latest
if: github.event.label.name == 'New Browser Test Expectations'
steps:
- name: Checkout ComfyUI
uses: actions/checkout@v4
with:
repository: "comfyanonymous/ComfyUI"
path: "ComfyUI"
ref: master
- name: Checkout ComfyUI_frontend
uses: actions/checkout@v4
with:
repository: "Comfy-Org/ComfyUI_frontend"
path: "ComfyUI_frontend"
ref: ${{ github.head_ref }}
- uses: actions/setup-node@v3
with:
node-version: lts/*
- uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install requirements
run: |
python -m pip install --upgrade pip
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
pip install -r requirements.txt
pip install wait-for-it
working-directory: ComfyUI
- name: Build & Install ComfyUI_frontend
run: |
npm ci
npm run build
rm -rf ../ComfyUI/web/*
mv dist/* ../ComfyUI/web/
working-directory: ComfyUI_frontend
- name: Start ComfyUI server
run: |
python main.py --cpu &
wait-for-it --service 127.0.0.1:8188 -t 600
working-directory: ComfyUI
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.1
- name: Install Playwright Browsers
run: npx playwright install chromium --with-deps
working-directory: ComfyUI_frontend
@@ -72,6 +35,8 @@ jobs:
run: |
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@github.com'
git fetch origin ${{ github.head_ref }}
git checkout -B ${{ github.head_ref }} origin/${{ github.head_ref }}
git add browser_tests
git commit -m "Update test expectations [skip ci]"
git push origin HEAD:${{ github.head_ref }}

View File

@@ -5,7 +5,6 @@ on:
branches:
- main
- master
- 'dev*'
pull_request:
branches:
- main
@@ -13,80 +12,64 @@ on:
- 'dev*'
jobs:
test:
jest-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout ComfyUI
uses: actions/checkout@v4
with:
repository: "comfyanonymous/ComfyUI"
path: "ComfyUI"
ref: master
- name: Checkout ComfyUI_frontend
uses: actions/checkout@v4
with:
repository: "Comfy-Org/ComfyUI_frontend"
path: "ComfyUI_frontend"
- name: Checkout ComfyUI_devtools
uses: actions/checkout@v4
with:
repository: "Comfy-Org/ComfyUI_devtools"
path: "ComfyUI/custom_nodes/ComfyUI_devtools"
- name: Get commit message
id: commit-message
run: echo "::set-output name=message::$(git log -1 --pretty=%B)"
working-directory: ComfyUI_frontend
- name: Checkout ComfyUI_examples
uses: actions/checkout@v4
with:
repository: "comfyanonymous/ComfyUI_examples"
path: "ComfyUI_frontend/tests-ui/ComfyUI_examples"
ref: master
- name: Skip CI
if: contains(steps.commit-message.outputs.message, '[skip ci]')
run: echo "Skipping CI as commit contains '[skip ci]'"
continue-on-error: true
working-directory: ComfyUI_frontend
- uses: actions/setup-node@v3
with:
node-version: lts/*
- uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install requirements
run: |
python -m pip install --upgrade pip
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
pip install -r requirements.txt
pip install wait-for-it
working-directory: ComfyUI
- name: Build & Install ComfyUI_frontend
run: |
npm ci
npm run build
rm -rf ../ComfyUI/web/*
mv dist/* ../ComfyUI/web/
working-directory: ComfyUI_frontend
- name: Start ComfyUI server
run: |
python main.py --cpu &
wait-for-it --service 127.0.0.1:8188 -t 600
working-directory: ComfyUI
- name: Run UI tests
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.1
- name: Run Jest tests
run: |
npm run test:generate
npm run test:generate:examples
npm test -- --verbose
working-directory: ComfyUI_frontend
playwright-tests-chromium:
runs-on: ubuntu-latest
steps:
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.1
- name: Install Playwright Browsers
run: npx playwright install chromium --with-deps
working-directory: ComfyUI_frontend
- name: Run Playwright tests
run: npx playwright test
- name: Run Playwright tests (Chromium)
run: npx playwright test --project=chromium
working-directory: ComfyUI_frontend
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
name: playwright-report-chromium
path: ComfyUI_frontend/playwright-report/
retention-days: 30
playwright-tests-chromium-2x:
runs-on: ubuntu-latest
steps:
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.1
- name: Install Playwright Browsers
run: npx playwright install chromium --with-deps
working-directory: ComfyUI_frontend
- name: Run Playwright tests (Chromium 2x)
run: npx playwright test --project=chromium-2x
working-directory: ComfyUI_frontend
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-chromium-2x
path: ComfyUI_frontend/playwright-report/
retention-days: 30
playwright-tests-mobile-chrome:
runs-on: ubuntu-latest
steps:
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.1
- name: Install Playwright Browsers
run: npx playwright install chromium --with-deps
working-directory: ComfyUI_frontend
- name: Run Playwright tests (Mobile Chrome)
run: npx playwright test --project=mobile-chrome
working-directory: ComfyUI_frontend
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-mobile-chrome
path: ComfyUI_frontend/playwright-report/
retention-days: 30

View File

@@ -1,53 +0,0 @@
name: Update Main Repo from PR
on:
pull_request:
types: [labeled]
jobs:
update-main-repo:
if: github.event.label.name == 'Update Main Repo'
runs-on: ubuntu-latest
steps:
- name: Checkout frontend repo PR
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: lts/*
- name: Install dependencies
run: npm ci
- name: Build project
run: npm run build
- name: Checkout ComfyUI
uses: actions/checkout@v4
with:
repository: "comfyanonymous/ComfyUI"
path: ComfyUI
ref: master
- name: Copy compiled assets
run: |
rm -rf ./ComfyUI/web/*
cp -R dist/* ./ComfyUI/web/
- name: Create Pull Request
uses: peter-evans/create-pull-request@v5
with:
token: ${{ secrets.PAT }}
commit-message: 'Update frontend assets from PR #${{ github.event.pull_request.number }}'
title: 'Update frontend assets from PR #${{ github.event.pull_request.number }}'
body: |
This PR updates the compiled frontend assets from PR #${{ github.event.pull_request.number }} in the frontend repo.
Frontend PR: ${{ github.event.pull_request.html_url }}
branch: update-frontend-assets-pr-${{ github.event.pull_request.number }}
base: main
path: ComfyUI

31
.github/workflows/vitest.yaml vendored Normal file
View File

@@ -0,0 +1,31 @@
name: Vitest Tests
on:
push:
branches:
- main
- master
- 'dev*'
pull_request:
branches:
- main
- master
- 'dev*'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: lts/*
- name: Install dependencies
run: npm ci
- name: Run Vitest tests
run: npm run test:component

1
.gitignore vendored
View File

@@ -23,6 +23,7 @@ dist-ssr
*.njsproj
*.sln
*.sw?
components.d.ts
# Ignore test data.
tests-ui/data/*

View File

@@ -1 +1,5 @@
npx lint-staged
if [[ "$OS" == "Windows_NT" ]]; then
npx.cmd lint-staged
else
npx lint-staged
fi

View File

@@ -2,5 +2,6 @@
"singleQuote": true,
"tabWidth": 2,
"semi": false,
"trailingComma": "none"
"trailingComma": "none",
"printWidth": 80
}

370
README.md
View File

@@ -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,31 @@ There will be a 2-day feature freeze before each stable release. During this per
### Major features
<details>
<summary>v1.3.22: Integrated server terminal</summary>
Press Ctrl + ` to toggle integrated terminal.
https://github.com/user-attachments/assets/eddedc6a-07a3-4a83-9475-63b3977f6d94
</details>
<details>
<summary>v1.3.7: Keybinding customization</summary>
## Basic UI
![image](https://github.com/user-attachments/assets/c84a1609-3880-48e0-a746-011f36beda68)
## Reset button
![image](https://github.com/user-attachments/assets/4d2922da-bb4f-4f90-8017-a8e4a0db07c7)
## Edit Keybinding
![image](https://github.com/user-attachments/assets/77626b7a-cb46-48f8-9465-e03120aac66a)
![image](https://github.com/user-attachments/assets/79131a4e-75c6-4715-bd11-c6aaed887779)
[rec.webm](https://github.com/user-attachments/assets/a3984ed9-eb28-4d47-86c0-7fc3efc2b5d0)
</details>
<details>
<summary>v1.2.4: Node library sidebar tab</summary>
@@ -90,6 +115,79 @@ https://github.com/user-attachments/assets/4bbca3ee-318f-4cf0-be32-a5a5541066cf
### QoL changes
<details>
<summary>v1.3.32: **Litegraph** Nested group</summary>
https://github.com/user-attachments/assets/f51adeb1-028e-40af-81e4-0ac13075198a
</details>
<details>
<summary>v1.3.24: **Litegraph** Group selection</summary>
https://github.com/user-attachments/assets/e6230a94-411e-4fba-90cb-6c694200adaa
</details>
<details>
<summary>v1.3.6: **Litegraph** Toggle link visibility</summary>
[rec.webm](https://github.com/user-attachments/assets/34e460ac-fbbc-44ef-bfbb-99a84c2ae2be)
</details>
<details>
<summary>v1.3.4: **Litegraph** Auto widget to input conversion</summary>
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)
</details>
<details>
<summary>v1.3.4: **Litegraph** Canvas pan mode</summary>
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)
</details>
<details>
<summary>v1.3.1: **Litegraph** Shift drag link to create a new link</summary>
[rec.webm](https://github.com/user-attachments/assets/7e73aaf9-79e2-4c3c-a26a-911cba3b85e4)
</details>
<details>
<summary>v1.2.62: **Litegraph** Show optional input slots as donuts</summary>
![GYEIRidb0AYGO-v](https://github.com/user-attachments/assets/e6cde0b6-654b-4afd-a117-133657a410b1)
</details>
<details>
<summary>v1.2.44: **Litegraph** Double click group title to edit</summary>
https://github.com/user-attachments/assets/5bf0e2b6-8b3a-40a7-b44f-f0879e9ad26f
</details>
<details>
<summary>v1.2.39: **Litegraph** Group selected nodes with Ctrl + G</summary>
https://github.com/user-attachments/assets/7805dc54-0854-4a28-8bcd-4b007fa01151
</details>
<details>
<summary>v1.2.38: **Litegraph** Double click node title to edit</summary>
https://github.com/user-attachments/assets/d61d5d0e-f200-4153-b293-3e3f6a212b30
</details>
<details>
<summary>v1.2.7: **Litegraph** drags multiple links with shift pressed</summary>
@@ -115,7 +213,173 @@ https://github.com/user-attachments/assets/c142c43f-2fe9-4030-8196-b3bfd4c6977d
https://github.com/user-attachments/assets/5696a89d-4a47-4fcc-9e8c-71e1264943f2
</details>
### Node developers API
### Developer APIs
<details>
<summary>v1.3.34: Register about panel badges</summary>
```js
app.registerExtension({
name: 'TestExtension1',
aboutPageBadges: [
{
label: 'Test Badge',
url: 'https://example.com',
icon: 'pi pi-box'
}
]
})
```
![image](https://github.com/user-attachments/assets/099e77ee-16ad-4141-b2fc-5e9d5075188b)
</details>
<details>
<summary>v1.3.22: Register bottom panel tabs</summary>
```js
app.registerExtension({
name: 'TestExtension',
bottomPanelTabs: [
{
id: 'TestTab',
title: 'Test Tab',
type: 'custom',
render: (el) => {
el.innerHTML = '<div>Custom tab</div>'
}
}
]
})
```
![image](https://github.com/user-attachments/assets/2114f8b8-2f55-414b-b027-78e61c870b64)
</details>
<details>
<summary>v1.3.22: New settings API</summary>
Legacy settings API.
```js
// Register a new setting
app.ui.settings.addSetting({
id: 'TestSetting',
name: 'Test Setting',
type: 'text',
defaultValue: 'Hello, world!'
})
// Get the value of a setting
const value = app.ui.settings.getSettingValue('TestSetting')
// Set the value of a setting
app.ui.settings.setSettingValue('TestSetting', 'Hello, universe!')
```
New settings API.
```js
// Register a new setting
app.registerExtension({
name: 'TestExtension1',
settings: [
{
id: 'TestSetting',
name: 'Test Setting',
type: 'text',
defaultValue: 'Hello, world!'
}
]
})
// Get the value of a setting
const value = app.extensionManager.setting.get('TestSetting')
// Set the value of a setting
app.extensionManager.setting.set('TestSetting', 'Hello, universe!')
```
</details>
<details>
<summary>v1.3.7: Register commands and keybindings</summary>
Extensions can call the following API to register commands and keybindings. Do
note that keybindings defined in core cannot be overwritten, and some keybindings
are reserved by the browser.
```js
app.registerExtension({
name: 'TestExtension1',
commands: [
{
id: 'TestCommand',
function: () => {
alert('TestCommand')
}
}
],
keybindings: [
{
combo: { key: 'k' },
commandId: 'TestCommand'
}
]
})
```
</details>
<details>
<summary>v1.3.1: Extension API to register custom topbar menu items</summary>
Extensions can call the following API to register custom topbar menu items.
```js
app.registerExtension({
name: 'TestExtension1',
commands: [
{
id: 'foo-id',
label: 'foo',
function: () => {
alert(1)
}
}
],
menuCommands: [
{
path: ['ext', 'ext2'],
commands: ['foo-id']
}
]
})
```
![image](https://github.com/user-attachments/assets/ae7b082f-7ce9-4549-a446-4563567102fe)
</details>
<details>
<summary>v1.2.27: Extension API to add toast message</summary>i
Extensions can call the following API to add toast messages.
```js
app.extensionManager.toast.add({
severity: 'info',
summary: 'Loaded!',
detail: 'Extension loaded!',
life: 3000
})
```
Documentation of all supported options can be found here: <https://primevue.org/toast/#api.toast.interfaces.ToastMessageOptions>
![image](https://github.com/user-attachments/assets/de02cd7e-cd81-43d1-a0b0-bccef92ff487)
</details>
<details>
<summary>v1.2.4: Extension API to register custom sidebar tab</summary>
@@ -141,50 +405,16 @@ We will support custom icons later.
![image](https://github.com/user-attachments/assets/7bff028a-bf91-4cab-bf97-55c243b3f5e0)
</details>
<details>
<summary>v1.2.27: Extension API to add toast message</summary>
Extensions can call the following API to add toast messages.
```js
app.extensionManager.toast.add({
severity: 'info',
summary: 'Loaded!',
detail: 'Extension loaded!'
})
```
![image](https://github.com/user-attachments/assets/de02cd7e-cd81-43d1-a0b0-bccef92ff487)
</details>
## Road Map
### What has been done
- Migrate all code to TypeScript with minimal change modification to the original logic.
- Bundle all code with Vite's rollup build.
- Added a shim layer to be backward compatible with the existing extension system. <https://github.com/huchenlei/ComfyUI_frontend/pull/15>
- Front-end dev server.
- Zod schema for input validation on ComfyUI workflow.
- Make litegraph a npm dependency. <https://github.com/Comfy-Org/ComfyUI_frontend/pull/89>
- Introduce Vue to start managing part of the UI.
- Easy install and version management (<https://github.com/comfyanonymous/ComfyUI/pull/3897>).
- Better node management. Sherlock <https://github.com/Nuked88/ComfyUI-N-Sidebar>.
### What to be done
- Replace the existing ComfyUI front-end impl
- Remove `@ts-ignore`s.
- Turn on `strict` on `tsconfig.json`.
- Add more widget types for node developers.
- LLM streaming node.
- Linear mode (Similar to InvokeAI's linear mode).
- Keybinding settings management. Register keybindings API for custom nodes.
- New extensions API for adding UI-related features.
## Development
### Tech Stack
- [Vue 3](https://vuejs.org/) with [TypeScript](https://www.typescriptlang.org/)
- [Pinia](https://pinia.vuejs.org/) for state management
- [PrimeVue](https://primevue.org/) with [TailwindCSS](https://tailwindcss.com/) for UI
- [Litegraph](https://github.com/Comfy-Org/litegraph.js) for node editor
- [zod](https://zod.dev/) for schema validation
### Git pre-commit hooks
Run `npm run prepare` to install Git pre-commit hooks. Currently, the pre-commit
@@ -195,11 +425,31 @@ hook is used to auto-format code on commit.
Note: The dev server will NOT load any extension from the ComfyUI server. Only
core extensions will be loaded.
- Run `npm install` to install the necessary packages
- Start local ComfyUI backend at `localhost:8188`
- Run `npm run dev` to start the dev server
- Run `npm run dev:electron` to start the dev server with electron API mocked
### Test
#### Access dev server on touch devices
After you start the dev server, you should see following logs:
```
> comfyui-frontend@1.3.42 dev
> vite
VITE v5.4.6 ready in 488 ms
➜ Local: http://localhost:5173/
➜ Network: http://172.21.80.1:5173/
➜ Network: http://192.168.2.20:5173/
➜ press h + enter to show help
```
Make sure your desktop machine and touch device are on the same network. On your touch device,
navigate to `http://<server_ip>:5173` (e.g. `http://192.168.2.20:5173` here), to access the ComfyUI frontend.
### Unit Test
- `git clone https://github.com/comfyanonymous/ComfyUI_examples.git` to `tests-ui/ComfyUI_examples` or the EXAMPLE_REPO_PATH location specified in .env
- `npm i` to install all dependencies
@@ -207,11 +457,35 @@ core extensions will be loaded.
- `npm run test:generate:examples` to extract the example workflows
- `npm run test` to execute all unit tests.
### Component Test
Component test verifies Vue components in `src/components/`.
- `npm run test:component` to execute all component tests.
### Playwright Test
Playwright test verifies the whole app. See <https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/browser_tests/README.md> for details.
### LiteGraph
This repo is using litegraph package hosted on https://github.com/Comfy-Org/litegraph.js. Any changes to litegraph should be submitted in that repo instead.
This repo is using litegraph package hosted on <https://github.com/Comfy-Org/litegraph.js>. Any changes to litegraph should be submitted in that repo instead.
### Test litegraph changes
- Run `npm link` in the local litegraph repo.
- Run `npm link @comfyorg/litegraph` in this repo.
This will replace the litegraph package in this repo with the local litegraph repo.
## Deploy
- Option 1: Set `DEPLOY_COMFYUI_DIR` in `.env` and run `npm run deploy`.
- Option 2: Copy everything under `dist/` to `ComfyUI/web/` in your ComfyUI checkout manually.
## Publish release to ComfyUI main repo
Run following command to publish a release to ComfyUI main repo. The script will create a new branch and do a commit to `web/` folder by checkout `dist.zip`
from GitHub release.
- `python scripts/main_repo_release.py <path_to_comfyui_main_repo> <version>`

View File

@@ -1,511 +0,0 @@
import type { Page, Locator } from '@playwright/test'
import { test as base } from '@playwright/test'
import dotenv from 'dotenv'
dotenv.config()
interface Position {
x: number
y: number
}
interface Size {
width: number
height: number
}
class ComfyNodeSearchBox {
public readonly input: Locator
public readonly dropdown: Locator
constructor(public readonly page: Page) {
this.input = page.locator(
'.comfy-vue-node-search-container input[type="text"]'
)
this.dropdown = page.locator(
'.comfy-vue-node-search-container .p-autocomplete-list'
)
}
async fillAndSelectFirstNode(
nodeName: string,
options?: { suggestionIndex: number }
) {
await this.input.waitFor({ state: 'visible' })
await this.input.fill(nodeName)
await this.dropdown.waitFor({ state: 'visible' })
// Wait for some time for the auto complete list to update.
// The auto complete list is debounced and may take some time to update.
await this.page.waitForTimeout(500)
await this.dropdown
.locator('li')
.nth(options?.suggestionIndex || 0)
.click()
}
}
class NodeLibrarySidebarTab {
public readonly tabId: string = 'node-library'
constructor(public readonly page: Page) {}
get tabButton() {
return this.page.locator(`.${this.tabId}-tab-button`)
}
get selectedTabButton() {
return this.page.locator(
`.${this.tabId}-tab-button.side-bar-button-selected`
)
}
get nodeLibraryTree() {
return this.page.locator('.node-lib-tree')
}
get nodePreview() {
return this.page.locator('.node-lib-node-preview')
}
get tabContainer() {
return this.page.locator('.sidebar-content-container')
}
get newFolderButton() {
return this.tabContainer.locator('.new-folder-button')
}
async open() {
if (await this.selectedTabButton.isVisible()) {
return
}
await this.tabButton.click()
await this.nodeLibraryTree.waitFor({ state: 'visible' })
}
folderSelector(folderName: string) {
return `.p-tree-node-content:has(> .node-lib-tree-node-label:has(.folder-label:has-text("${folderName}")))`
}
getFolder(folderName: string) {
return this.page.locator(this.folderSelector(folderName))
}
nodeSelector(nodeName: string) {
return `.p-tree-node-content:has(> .node-lib-tree-node-label:has(.node-label:has-text("${nodeName}")))`
}
getNode(nodeName: string) {
return this.page.locator(this.nodeSelector(nodeName))
}
}
class ComfyMenu {
public readonly sideToolbar: Locator
public readonly themeToggleButton: Locator
constructor(public readonly page: Page) {
this.sideToolbar = page.locator('.side-tool-bar-container')
this.themeToggleButton = page.locator('.comfy-vue-theme-toggle')
}
get nodeLibraryTab() {
return new NodeLibrarySidebarTab(this.page)
}
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 }
)
setTimeout(resolve, 5000)
})
})
}
async getThemeId() {
return await this.page.evaluate(async () => {
return await window['app'].ui.settings.getSettingValue(
'Comfy.ColorPalette'
)
})
}
}
export class ComfyPage {
public readonly url: string
// All canvas position operations are based on default view of canvas.
public readonly canvas: Locator
public readonly widgetTextBox: Locator
// Buttons
public readonly resetViewButton: Locator
public readonly queueButton: Locator
// Inputs
public readonly workflowUploadInput: Locator
// Components
public readonly searchBox: ComfyNodeSearchBox
public readonly menu: ComfyMenu
constructor(public readonly page: Page) {
this.url = process.env.PLAYWRIGHT_TEST_URL || 'http://localhost:8188'
this.canvas = page.locator('#graph-canvas')
this.widgetTextBox = page.getByPlaceholder('text').nth(1)
this.resetViewButton = page.getByRole('button', { name: 'Reset View' })
this.queueButton = page.getByRole('button', { name: 'Queue Prompt' })
this.workflowUploadInput = page.locator('#comfy-file-input')
this.searchBox = new ComfyNodeSearchBox(page)
this.menu = new ComfyMenu(page)
}
async getGraphNodesCount(): Promise<number> {
return await this.page.evaluate(() => {
return window['app']?.graph?._nodes?.length || 0
})
}
async setup() {
await this.goto()
// Unify font for consistent screenshots.
await this.page.addStyleTag({
url: 'https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap'
})
await this.page.addStyleTag({
url: 'https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap'
})
await this.page.addStyleTag({
content: `
* {
font-family: 'Roboto Mono', 'Noto Color Emoji';
}`
})
await this.page.waitForFunction(() => document.fonts.ready)
await this.page.waitForFunction(
() => window['app'] !== undefined && window['app'].vueAppReady
)
await this.page.evaluate(() => {
window['app']['canvas'].show_info = false
})
await this.nextFrame()
// Reset view to force re-rendering of canvas. So that info fields like fps
// become hidden.
await this.resetView()
}
async setSetting(settingId: string, settingValue: any) {
return await this.page.evaluate(
async ({ id, value }) => {
await window['app'].ui.settings.setSettingValueAsync(id, value)
},
{ id: settingId, value: settingValue }
)
}
async getSetting(settingId: string) {
return await this.page.evaluate(async (id) => {
return await window['app'].ui.settings.getSettingValue(id)
}, settingId)
}
async reload() {
await this.page.reload({ timeout: 15000 })
await this.setup()
}
async goto() {
await this.page.goto(this.url)
}
async nextFrame() {
await this.page.evaluate(() => {
return new Promise<number>(requestAnimationFrame)
})
}
async delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
async loadWorkflow(workflowName: string) {
await this.workflowUploadInput.setInputFiles(
`./browser_tests/assets/${workflowName}.json`
)
await this.nextFrame()
}
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 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)
await this.page.mouse.up()
await this.nextFrame()
}
async dragNode2() {
await this.dragAndDrop({ x: 622, y: 400 }, { x: 622, y: 300 })
await this.nextFrame()
}
async disconnectEdge() {
// CLIP input anchor
await this.page.mouse.move(427, 198)
await this.page.mouse.down()
await this.page.mouse.move(427, 98)
await this.page.mouse.up()
// Move out the way to avoid highlight of menu item.
await this.page.mouse.move(10, 10)
await this.nextFrame()
}
async connectEdge() {
// CLIP output anchor on Load Checkpoint Node.
await this.page.mouse.move(332, 509)
await this.page.mouse.down()
// CLIP input anchor on CLIP Text Encode Node.
await this.page.mouse.move(427, 198)
await this.page.mouse.up()
await this.nextFrame()
}
async adjustWidgetValue() {
// Adjust Empty Latent Image's width input.
const page = this.page
await page.locator('#graph-canvas').click({
position: {
x: 724,
y: 645
}
})
await page.locator('input[type="text"]').click()
await page.locator('input[type="text"]').fill('128')
await page.locator('input[type="text"]').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()
// TEMPORARY HACK: Multiple pans open the search menu, so cheat and keep it closed.
// TODO: Fix that (double-click at not-the-same-coordinations should not open the menu)
await this.page.keyboard.press('Escape')
await this.page.mouse.move(offset.x + safeSpot.x, offset.y + safeSpot.y)
await this.page.mouse.up()
await this.nextFrame()
}
async rightClickCanvas() {
await this.page.mouse.click(10, 10, { button: 'right' })
await this.nextFrame()
}
async doubleClickCanvas() {
await this.page.mouse.dblclick(10, 10)
await this.nextFrame()
}
async clickEmptyLatentNode() {
await this.canvas.click({
position: {
x: 724,
y: 625
}
})
this.page.mouse.move(10, 10)
await this.nextFrame()
}
async rightClickEmptyLatentNode() {
await this.canvas.click({
position: {
x: 724,
y: 645
},
button: 'right'
})
this.page.mouse.move(10, 10)
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 ctrlC() {
await this.page.keyboard.down('Control')
await this.page.keyboard.press('KeyC')
await this.page.keyboard.up('Control')
await this.nextFrame()
}
async ctrlV() {
await this.page.keyboard.down('Control')
await this.page.keyboard.press('KeyV')
await this.page.keyboard.up('Control')
await this.nextFrame()
}
async closeMenu() {
await this.page.click('button.comfy-close-menu-btn')
await this.nextFrame()
}
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
}
await this.dragAndDrop(bottomRight, target)
await this.nextFrame()
if (revertAfter) {
await this.dragAndDrop(target, bottomRight)
await this.nextFrame()
}
}
async resizeKsamplerNode(
percentX: number,
percentY: number,
revertAfter: boolean = false
) {
const ksamplerPos = {
x: 864,
y: 157
}
const ksamplerSize = {
width: 315,
height: 292
}
this.resizeNode(ksamplerPos, ksamplerSize, percentX, percentY, revertAfter)
}
async resizeLoadCheckpointNode(
percentX: number,
percentY: number,
revertAfter: boolean = false
) {
const loadCheckpointPos = {
x: 25,
y: 440
}
const loadCheckpointSize = {
width: 320,
height: 120
}
this.resizeNode(
loadCheckpointPos,
loadCheckpointSize,
percentX,
percentY,
revertAfter
)
}
async resizeEmptyLatentNode(
percentX: number,
percentY: number,
revertAfter: boolean = false
) {
const emptyLatentPos = {
x: 475,
y: 580
}
const emptyLatentSize = {
width: 303,
height: 132
}
this.resizeNode(
emptyLatentPos,
emptyLatentSize,
percentX,
percentY,
revertAfter
)
}
}
export const comfyPageFixture = base.extend<{ comfyPage: ComfyPage }>({
comfyPage: async ({ page }, use) => {
const comfyPage = new ComfyPage(page)
await comfyPage.setup()
await use(comfyPage)
}
})

View File

@@ -2,8 +2,16 @@
This document outlines the setup and usage of Playwright for testing the ComfyUI_frontend project.
## WARNING
The browser tests will change the ComfyUI backend state, such as user settings and saved workflows.
If `TEST_COMFYUI_DIR` in `.env` isn't set to your `(Comfy Path)/ComfyUI` directory, these changes won't be automatically restored.
## Setup
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.
Ensure you have Node.js v20 or later installed. Then, set up the Chromium test driver:
```bash

View File

@@ -0,0 +1,125 @@
import type { Response } from '@playwright/test'
import type { StatusWsMessage } from '../src/types/apiTypes.ts'
import { expect, mergeTests } from '@playwright/test'
import { comfyPageFixture } from './fixtures/ComfyPage'
import { webSocketFixture } from './fixtures/ws.ts'
const test = mergeTests(comfyPageFixture, webSocketFixture)
test.describe('Actionbar', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
})
/**
* This test ensures that the autoqueue change mode can only queue one change at a time
*/
test('Does not auto-queue multiple changes at a time', async ({
comfyPage,
ws
}) => {
// Enable change auto-queue mode
const queueOpts = await comfyPage.actionbar.queueButton.toggleOptions()
expect(await queueOpts.getMode()).toBe('disabled')
await queueOpts.setMode('change')
await comfyPage.nextFrame()
expect(await queueOpts.getMode()).toBe('change')
await comfyPage.actionbar.queueButton.toggleOptions()
// Intercept the prompt queue endpoint
let promptNumber = 0
comfyPage.page.route('**/api/prompt', async (route, req) => {
await new Promise((r) => setTimeout(r, 100))
route.fulfill({
status: 200,
body: JSON.stringify({
prompt_id: promptNumber,
number: ++promptNumber,
node_errors: {},
// Include the request data to validate which prompt was queued so we can validate the width
__request: req.postDataJSON()
})
})
})
// Start watching for a message to prompt
const requestPromise = comfyPage.page.waitForResponse('**/api/prompt')
// 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(
(n) => n.type === 'EmptyLatentImage'
)
node.widgets[0].value = value
window[
'app'
].extensionManager.workflow.activeWorkflow.changeTracker.checkState()
}, value)
}
// Trigger a status websocket message
const triggerStatus = async (queueSize: number) => {
await ws.trigger({
type: 'status',
data: {
status: {
exec_info: {
queue_remaining: queueSize
}
}
}
} as StatusWsMessage)
}
// Extract the width from the queue response
const getQueuedWidth = async (resp: Promise<Response>) => {
const obj = await (await resp).json()
return obj['__request']['prompt']['5']['inputs']['width']
}
// Trigger a bunch of changes
const START = 32
const END = 64
for (let i = START; i <= END; i += 8) {
await triggerChange(i)
}
// Ensure the queued width is the first value
expect(
await getQueuedWidth(requestPromise),
'the first queued prompt should be the first change width'
).toBe(START)
// Ensure that no other changes are queued
await expect(
comfyPage.page.waitForResponse('**/api/prompt', { timeout: 250 })
).rejects.toThrow()
expect(
promptNumber,
'only 1 prompt should have been queued even though there were multiple changes'
).toBe(1)
// Trigger a status update so auto-queue re-runs
await triggerStatus(1)
await triggerStatus(0)
// Ensure the queued width is the last queued value
expect(
await getQueuedWidth(comfyPage.page.waitForResponse('**/api/prompt')),
'last queued prompt width should be the last change'
).toBe(END)
expect(promptNumber, 'queued prompt count should be 2').toBe(2)
})
test('Can dock actionbar into top menu', async ({ comfyPage }) => {
await comfyPage.page.dragAndDrop(
'.actionbar .drag-handle',
'.comfyui-menu',
{
targetPosition: { x: 0, y: 0 }
}
)
expect(await comfyPage.actionbar.isDocked()).toBe(true)
})
})

View File

@@ -0,0 +1,135 @@
{
"last_node_id": 9,
"last_link_id": 9,
"nodes": [
{
"id": 7,
"type": "CLIPTextEncode",
"pos": [413, 389],
"size": [425.27801513671875, 180.6060791015625],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [{ "name": "clip", "type": "CLIP", "link": 5 }],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [6],
"slot_index": 0
}
],
"properties": {},
"widgets_values": ["text, watermark"]
},
{
"id": 6,
"type": "CLIPTextEncode",
"pos": [415, 186],
"size": [422.84503173828125, 164.31304931640625],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [{ "name": "clip", "type": "CLIP", "link": 3 }],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [4],
"slot_index": 0
}
],
"properties": {},
"widgets_values": [
"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,"
]
},
{
"id": 5,
"type": "EmptyLatentImage",
"pos": [473, 609],
"size": [315, 106],
"flags": {},
"order": 1,
"mode": 0,
"outputs": [{ "name": "LATENT", "type": "LATENT", "links": [2], "slot_index": 0 }],
"properties": {},
"widgets_values": [512, 512, 1]
},
{
"id": 3,
"type": "KSampler",
"pos": [863, 186],
"size": [315, 262],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{ "name": "model", "type": "MODEL", "link": 1 },
{ "name": "positive", "type": "CONDITIONING", "link": 4 },
{ "name": "negative", "type": "CONDITIONING", "link": 6 },
{ "name": "latent_image", "type": "LATENT", "link": 2 }
],
"outputs": [{ "name": "LATENT", "type": "LATENT", "links": [7], "slot_index": 0 }],
"properties": {},
"widgets_values": [156680208700286, true, 20, 8, "euler", "normal", 1]
},
{
"id": 8,
"type": "VAEDecode",
"pos": [1209, 188],
"size": [210, 46],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{ "name": "samples", "type": "LATENT", "link": 7 },
{ "name": "vae", "type": "VAE", "link": 8 }
],
"outputs": [{ "name": "IMAGE", "type": "IMAGE", "links": [9], "slot_index": 0 }],
"properties": {}
},
{
"id": 9,
"type": "SaveImage",
"pos": [1451, 189],
"size": [210, 26],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [{ "name": "images", "type": "IMAGE", "link": 9 }],
"properties": {}
},
{
"id": 4,
"type": "CheckpointLoaderSimple",
"pos": [26, 474],
"size": [315, 98],
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{ "name": "MODEL", "type": "MODEL", "links": [1], "slot_index": 0 },
{ "name": "CLIP", "type": "CLIP", "links": [3, 5], "slot_index": 1 },
{ "name": "VAE", "type": "VAE", "links": [8], "slot_index": 2 }
],
"properties": {},
"widgets_values": ["v1-5-pruned-emaonly.ckpt"]
}
],
"links": [
[1, 4, 0, 3, 0, "MODEL"],
[2, 5, 0, 3, 3, "LATENT"],
[3, 4, 1, 6, 0, "CLIP"],
[4, 6, 0, 3, 1, "CONDITIONING"],
[5, 4, 1, 7, 0, "CLIP"],
[6, 7, 0, 3, 2, "CONDITIONING"],
[7, 3, 0, 8, 0, "LATENT"],
[8, 4, 2, 8, 1, "VAE"],
[9, 8, 0, 9, 0, "IMAGE"]
],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

View File

@@ -0,0 +1,504 @@
{
"last_node_id": 13,
"last_link_id": 9,
"nodes": [
{
"id": 3,
"type": "KSampler",
"pos": {
"0": 863,
"1": 186
},
"size": {
"0": 315,
"1": 262
},
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 1
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 4
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 6
},
{
"name": "latent_image",
"type": "LATENT",
"link": 2
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
7
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
156680208700286,
"randomize",
20,
8,
"euler",
"normal",
1
],
"color": "#432",
"bgcolor": "#653"
},
{
"id": 4,
"type": "CheckpointLoaderSimple",
"pos": {
"0": 36,
"1": 172
},
"size": {
"0": 315,
"1": 98
},
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
1
],
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": [
3,
5
],
"slot_index": 1
},
{
"name": "VAE",
"type": "VAE",
"links": [
8
],
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"Stable-diffusion/v1-5-pruned-emaonly.safetensors"
],
"color": "#322",
"bgcolor": "#533"
},
{
"id": 5,
"type": "EmptyLatentImage",
"pos": {
"0": 473,
"1": 609
},
"size": {
"0": 315,
"1": 106
},
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
2
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage"
},
"widgets_values": [
512,
512,
1
],
"color": "#323",
"bgcolor": "#535"
},
{
"id": 6,
"type": "CLIPTextEncode",
"pos": {
"0": 415,
"1": 186
},
"size": {
"0": 422.84503173828125,
"1": 164.31304931640625
},
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 3
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
4
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,"
],
"color": "#233",
"bgcolor": "#355"
},
{
"id": 7,
"type": "CLIPTextEncode",
"pos": {
"0": 413,
"1": 389
},
"size": {
"0": 425.27801513671875,
"1": 180.6060791015625
},
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 5
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
6
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"text, watermark"
],
"color": "#323",
"bgcolor": "#535"
},
{
"id": 8,
"type": "VAEDecode",
"pos": {
"0": 866,
"1": 502
},
"size": {
"0": 210,
"1": 46
},
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 7
},
{
"name": "vae",
"type": "VAE",
"link": 8
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
9
],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEDecode"
},
"widgets_values": [],
"color": "#222",
"bgcolor": "#000"
},
{
"id": 9,
"type": "SaveImage",
"pos": {
"0": 857,
"1": 611
},
"size": [
214.2000732421875,
59.4000244140625
],
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 9
}
],
"outputs": [],
"properties": {},
"widgets_values": [
"ComfyUI"
]
},
{
"id": 10,
"type": "CheckpointLoaderSimple",
"pos": {
"0": 42,
"1": 329
},
"size": {
"0": 315,
"1": 98
},
"flags": {},
"order": 2,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": null
},
{
"name": "CLIP",
"type": "CLIP",
"links": null
},
{
"name": "VAE",
"type": "VAE",
"links": null
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"Stable-diffusion/v1-5-pruned-emaonly.safetensors"
],
"color": "#332922",
"bgcolor": "#593930"
},
{
"id": 11,
"type": "CheckpointLoaderSimple",
"pos": {
"0": 40,
"1": 494
},
"size": {
"0": 315,
"1": 98
},
"flags": {},
"order": 3,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": null
},
{
"name": "CLIP",
"type": "CLIP",
"links": null
},
{
"name": "VAE",
"type": "VAE",
"links": null
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"Stable-diffusion/v1-5-pruned-emaonly.safetensors"
],
"color": "#223",
"bgcolor": "#335"
},
{
"id": 13,
"type": "ImageScale",
"pos": {
"0": 42,
"1": 650
},
"size": {
"0": 315,
"1": 130
},
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": null
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null
}
],
"properties": {
"Node name for S&R": "ImageScale"
},
"widgets_values": [
"nearest-exact",
512,
512,
"disabled"
],
"color": "#2a363b",
"bgcolor": "#3f5159"
}
],
"links": [
[
1,
4,
0,
3,
0,
"MODEL"
],
[
2,
5,
0,
3,
3,
"LATENT"
],
[
3,
4,
1,
6,
0,
"CLIP"
],
[
4,
6,
0,
3,
1,
"CONDITIONING"
],
[
5,
4,
1,
7,
0,
"CLIP"
],
[
6,
7,
0,
3,
2,
"CONDITIONING"
],
[
7,
3,
0,
8,
0,
"LATENT"
],
[
8,
4,
2,
8,
1,
"VAE"
],
[
9,
8,
0,
9,
0,
"IMAGE"
]
],
"groups": [],
"config": {},
"extra": {},
"version": 0.4
}

View File

@@ -5,10 +5,10 @@
{
"id": 14,
"type": "PreviewImage",
"pos": [
858,
-41
],
"pos": {
"0": 300,
"1": 60
},
"size": {
"0": 213.8594970703125,
"1": 50.65289306640625
@@ -23,6 +23,7 @@
"link": 15
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewImage"
}
@@ -30,10 +31,10 @@
{
"id": 17,
"type": "DevToolsErrorRaiseNode",
"pos": [
477,
-40
],
"pos": {
"0": 20,
"1": 60
},
"size": {
"0": 210,
"1": 26
@@ -41,6 +42,7 @@
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
@@ -71,10 +73,10 @@
"config": {},
"extra": {
"ds": {
"scale": 1.2100000000000006,
"scale": 1,
"offset": [
-266.1038310281165,
337.94335447664554
117.20766722169206,
472.69035116826046
]
}
},

View File

@@ -0,0 +1,62 @@
{
"last_node_id": 5,
"last_link_id": 0,
"nodes": [
{
"id": 5,
"type": "DevToolsNodeWithForceInput",
"pos": {
"0": 9,
"1": 39
},
"size": {
"0": 315,
"1": 106
},
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"name": "int_input",
"type": "INT",
"link": null,
"widget": {
"name": "int_input"
}
},
{
"name": "float_input",
"type": "FLOAT",
"link": null,
"widget": {
"name": "float_input"
},
"shape": 7
}
],
"outputs": [],
"properties": {
"Node name for S&R": "DevToolsNodeWithForceInput"
},
"widgets_values": [
0,
1,
0
]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [
0,
0
]
}
},
"version": 0.4
}

View File

@@ -0,0 +1,404 @@
{
"last_node_id": 10,
"last_link_id": 9,
"nodes": [
{
"id": 10,
"type": "workflow>group_node",
"pos": {
"0": 26,
"1": 186
},
"size": {
"0": 400,
"1": 390
},
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [],
"properties": {
"Node name for S&R": "workflow>group_node"
},
"widgets_values": [
512,
512,
1,
"v1-5-pruned-emaonly.ckpt",
"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,",
"text, watermark",
156680208700286,
"randomize",
20,
8,
"euler",
"normal",
1,
"ComfyUI"
]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"groupNodes": {
"group_node": {
"nodes": [
{
"id": -1,
"type": "EmptyLatentImage",
"pos": {
"0": 473,
"1": 609
},
"size": {
"0": 315,
"1": 106
},
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage"
},
"widgets_values": [
512,
512,
1
],
"index": 0
},
{
"id": -1,
"type": "CheckpointLoaderSimple",
"pos": {
"0": 26,
"1": 474
},
"size": {
"0": 315,
"1": 98
},
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [],
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": [],
"slot_index": 1
},
{
"name": "VAE",
"type": "VAE",
"links": [],
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"v1-5-pruned-emaonly.ckpt"
],
"index": 1
},
{
"id": -1,
"type": "CLIPTextEncode",
"pos": {
"0": 415,
"1": 186
},
"size": {
"0": 422.84503173828125,
"1": 164.31304931640625
},
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": null
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,"
],
"index": 2
},
{
"id": -1,
"type": "CLIPTextEncode",
"pos": {
"0": 413,
"1": 389
},
"size": {
"0": 425.27801513671875,
"1": 180.6060791015625
},
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": null
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"text, watermark"
],
"index": 3
},
{
"id": -1,
"type": "KSampler",
"pos": {
"0": 863,
"1": 186
},
"size": {
"0": 315,
"1": 262
},
"flags": {},
"order": 4,
"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": [],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
156680208700286,
"randomize",
20,
8,
"euler",
"normal",
1
],
"index": 4
},
{
"id": -1,
"type": "VAEDecode",
"pos": {
"0": 1209,
"1": 188
},
"size": {
"0": 210,
"1": 46
},
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": null
},
{
"name": "vae",
"type": "VAE",
"link": null
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "VAEDecode"
},
"index": 5
},
{
"id": -1,
"type": "SaveImage",
"pos": {
"0": 1451,
"1": 189
},
"size": {
"0": 210,
"1": 58
},
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": null
}
],
"outputs": [],
"properties": {},
"widgets_values": [
"ComfyUI"
],
"index": 6
}
],
"links": [
[
1,
1,
2,
0,
4,
"CLIP"
],
[
1,
1,
3,
0,
4,
"CLIP"
],
[
1,
0,
4,
0,
4,
"MODEL"
],
[
2,
0,
4,
1,
6,
"CONDITIONING"
],
[
3,
0,
4,
2,
7,
"CONDITIONING"
],
[
0,
0,
4,
3,
5,
"LATENT"
],
[
4,
0,
5,
0,
3,
"LATENT"
],
[
1,
2,
5,
1,
4,
"VAE"
],
[
5,
0,
6,
0,
8,
"IMAGE"
]
],
"external": []
}
}
},
"version": 0.4
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

View File

@@ -0,0 +1,252 @@
{
"last_node_id": 15,
"last_link_id": 9,
"nodes": [
{
"id": 15,
"type": "workflow/hello",
"pos": {
"0": 566,
"1": 316
},
"size": {
"0": 468.5999755859375,
"1": 582
},
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": null,
"label": "model"
},
{
"name": "positive",
"type": "CONDITIONING",
"link": null,
"label": "positive"
},
{
"name": "negative",
"type": "CONDITIONING",
"link": null,
"label": "negative"
},
{
"name": "latent_image",
"type": "LATENT",
"link": null,
"label": "latent_image"
},
{
"name": "KSampler model",
"type": "MODEL",
"link": null,
"label": "KSampler model"
},
{
"name": "KSampler positive",
"type": "CONDITIONING",
"link": null,
"label": "KSampler positive"
},
{
"name": "KSampler negative",
"type": "CONDITIONING",
"link": null,
"label": "KSampler negative"
},
{
"name": "KSampler latent_image",
"type": "LATENT",
"link": null,
"label": "KSampler latent_image"
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": null,
"shape": 3,
"label": "LATENT"
},
{
"name": "KSampler LATENT",
"type": "LATENT",
"links": null,
"shape": 3,
"label": "KSampler LATENT"
}
],
"properties": {
"Node name for S&R": "workflow/hello"
},
"widgets_values": [
"enable",
0,
"randomize",
20,
8,
"euler",
"normal",
0,
10000,
"disable",
0,
"randomize",
20,
8,
"euler",
"normal",
1
]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"groupNodes": {
"hello": {
"nodes": [
{
"id": -1,
"type": "KSamplerAdvanced",
"pos": {
"0": 351.3332824707031,
"1": 577.3333129882812
},
"size": {
"0": 315,
"1": 334
},
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": null,
"label": "model"
},
{
"name": "positive",
"type": "CONDITIONING",
"link": null,
"label": "positive"
},
{
"name": "negative",
"type": "CONDITIONING",
"link": null,
"label": "negative"
},
{
"name": "latent_image",
"type": "LATENT",
"link": null,
"label": "latent_image"
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": null,
"shape": 3,
"label": "LATENT"
}
],
"properties": {
"Node name for S&R": "KSamplerAdvanced"
},
"widgets_values": [
"enable",
0,
"randomize",
20,
8,
"euler",
"normal",
0,
10000,
"disable"
],
"index": 0
},
{
"id": -1,
"type": "KSampler",
"pos": {
"0": 636,
"1": 427
},
"size": {
"0": 315,
"1": 262
},
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": null,
"label": "model"
},
{
"name": "positive",
"type": "CONDITIONING",
"link": null,
"label": "positive"
},
{
"name": "negative",
"type": "CONDITIONING",
"link": null,
"label": "negative"
},
{
"name": "latent_image",
"type": "LATENT",
"link": null,
"label": "latent_image"
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": null,
"shape": 3,
"label": "LATENT"
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
0,
"randomize",
20,
8,
"euler",
"normal",
1
],
"index": 1
}
],
"links": [],
"external": []
}
}
},
"version": 0.4
}

View File

@@ -0,0 +1,25 @@
{
"last_node_id": 0,
"last_link_id": 0,
"nodes": [],
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [
0,
0
]
}
},
"models": [
{
"name": "fake_model.safetensors",
"url": "http://localhost:8188/api/devtools/fake_model.safetensors",
"directory": "clip"
}
],
"version": 0.4
}

View File

@@ -0,0 +1,227 @@
{
"last_node_id": 3,
"last_link_id": 0,
"nodes": [
{
"id": 2,
"type": "KSampler",
"pos": {
"0": 420,
"1": 130
},
"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
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": null
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
0,
"randomize",
20,
8,
"euler",
"normal",
1
]
},
{
"id": 3,
"type": "KSampler",
"pos": {
"0": 820,
"1": 130
},
"size": {
"0": 315,
"1": 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
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": null
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
0,
"randomize",
20,
8,
"euler",
"normal",
1
]
},
{
"id": 1,
"type": "KSampler",
"pos": {
"0": 30,
"1": 130
},
"size": {
"0": 315,
"1": 262
},
"flags": {},
"order": 2,
"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
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
0,
"randomize",
20,
8,
"euler",
"normal",
1
]
}
],
"links": [],
"groups": [
{
"id": 0,
"title": "Group",
"bounding": [
406.9701232910156,
59.079444885253906,
335,
345.6000061035156
],
"color": "#3f789e",
"font_size": 24,
"flags": {}
},
{
"id": 3,
"title": "Group Parent",
"bounding": [
796.9703979492188,
14.796443939208984,
355,
399.20001220703125
],
"color": "#3f789e",
"font_size": 24,
"flags": {}
},
{
"id": 2,
"title": "Group Child",
"bounding": [
806.9703979492188,
58.39643096923828,
335,
345.6000061035156
],
"color": "#3f789e",
"font_size": 24,
"flags": {}
}
],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [
0,
0
]
}
},
"version": 0.4
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

View File

@@ -0,0 +1,10 @@
[
{
"name": "Three Nodes Template",
"data": "{\"nodes\":[{\"id\":7,\"type\":\"CLIPTextEncode\",\"pos\":[413,389],\"size\":[425.27801513671875,180.6060791015625],\"flags\":{},\"order\":3,\"mode\":0,\"inputs\":[{\"name\":\"clip\",\"type\":\"CLIP\",\"link\":null}],\"outputs\":[{\"name\":\"CONDITIONING\",\"type\":\"CONDITIONING\",\"links\":[],\"slot_index\":0}],\"properties\":{\"Node name for S&R\":\"CLIPTextEncode\"},\"widgets_values\":[\"text, watermark\"]},{\"id\":6,\"type\":\"CLIPTextEncode\",\"pos\":[415,186],\"size\":[422.84503173828125,164.31304931640625],\"flags\":{},\"order\":2,\"mode\":0,\"inputs\":[{\"name\":\"clip\",\"type\":\"CLIP\",\"link\":null}],\"outputs\":[{\"name\":\"CONDITIONING\",\"type\":\"CONDITIONING\",\"links\":[],\"slot_index\":0}],\"properties\":{\"Node name for S&R\":\"CLIPTextEncode\"},\"widgets_values\":[\"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,\"]},{\"id\":4,\"type\":\"CheckpointLoaderSimple\",\"pos\":[26,474],\"size\":[315,98],\"flags\":{},\"order\":1,\"mode\":0,\"inputs\":[],\"outputs\":[{\"name\":\"MODEL\",\"type\":\"MODEL\",\"links\":[],\"slot_index\":0},{\"name\":\"CLIP\",\"type\":\"CLIP\",\"links\":[],\"slot_index\":1},{\"name\":\"VAE\",\"type\":\"VAE\",\"links\":[],\"slot_index\":2}],\"properties\":{\"Node name for S&R\":\"CheckpointLoaderSimple\"},\"widgets_values\":[\"v1-5-pruned-emaonly.ckpt\"]}],\"groups\":[],\"reroutes\":[],\"links\":[{\"id\":5,\"origin_id\":4,\"origin_slot\":1,\"target_id\":7,\"target_slot\":0,\"type\":\"CLIP\"},{\"id\":3,\"origin_id\":4,\"origin_slot\":1,\"target_id\":6,\"target_slot\":0,\"type\":\"CLIP\"}]}"
},
{
"name": "Completely empty template",
"data": "{\"nodes\":[],\"groups\":[],\"reroutes\":[],\"links\":[]}"
}
]

View File

@@ -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
}

View File

@@ -0,0 +1,44 @@
{
"last_node_id": 11,
"last_link_id": 9,
"nodes": [
{
"id": 11,
"type": "DevToolsNodeWithOnlyOptionalInput",
"pos": {
"0": 150,
"1": 464.2916564941406
},
"size": {
"0": 400,
"1": 200
},
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": null,
"shape": 7,
"label": "clip"
}
],
"outputs": [],
"properties": {
"Node name for S&R": "DevToolsNodeWithOnlyOptionalInput"
},
"widgets_values": [
""
]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"groupNodes": {}
},
"version": 0.4
}

View File

@@ -0,0 +1,57 @@
{
"last_node_id": 5,
"last_link_id": 0,
"nodes": [
{
"id": 5,
"type": "DevToolsNodeWithOptionalInput",
"pos": {
"0": 19,
"1": 46
},
"size": {
"0": 302.4000244140625,
"1": 46
},
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"name": "required_input",
"type": "IMAGE",
"link": null
},
{
"name": "optional_input",
"type": "IMAGE",
"link": null,
"shape": 7
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null
}
],
"properties": {
"Node name for S&R": "DevToolsNodeWithOptionalInput"
}
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [
0,
0
]
}
},
"version": 0.4
}

View File

@@ -0,0 +1,56 @@
{
"last_node_id": 5,
"last_link_id": 0,
"nodes": [
{
"id": 5,
"type": "DevToolsNodeWithOptionalInput",
"pos": {
"0": 19,
"1": 46
},
"size": {
"0": 302.4000244140625,
"1": 46
},
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"name": "required_input",
"type": "IMAGE",
"link": null
},
{
"name": "optional_input",
"type": "IMAGE",
"link": null
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null
}
],
"properties": {
"Node name for S&R": "DevToolsNodeWithOptionalInput"
}
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [
0,
0
]
}
},
"version": 0.4
}

View File

@@ -0,0 +1,57 @@
{
"last_node_id": 5,
"last_link_id": 0,
"nodes": [
{
"id": 5,
"type": "DevToolsNodeWithOptionalInput",
"pos": {
"0": 19,
"1": 46
},
"size": {
"0": 302.4000244140625,
"1": 46
},
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"name": "required_input",
"type": "IMAGE",
"link": null
},
{
"name": "optional_input",
"type": "IMAGE",
"link": null,
"shape": 6
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null
}
],
"properties": {
"Node name for S&R": "DevToolsNodeWithOptionalInput"
}
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [
0,
0
]
}
},
"version": 0.4
}

View File

@@ -0,0 +1,145 @@
{
"last_node_id": 2,
"last_link_id": 1,
"nodes": [
{
"id": 2,
"type": "KSampler",
"pos": {
"0": 521.0906982421875,
"1": 40.999996185302734,
"2": 0,
"3": 0,
"4": 0,
"5": 0,
"6": 0,
"7": 0,
"8": 0,
"9": 0
},
"size": {
"0": 315,
"1": 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",
"link": 1,
"widget": {
"name": "steps"
}
}
],
"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": "PrimitiveNode",
"pos": {
"0": 15,
"1": 46,
"2": 0,
"3": 0,
"4": 0,
"5": 0,
"6": 0,
"7": 0,
"8": 0,
"9": 0
},
"size": [
446.96645387135936,
108.34243389566905
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "INT",
"type": "INT",
"links": [
1
],
"slot_index": 0,
"widget": {
"name": "steps"
}
}
],
"properties": {
"Run widget replace on values": false
},
"widgets_values": [
20,
"fixed"
]
}
],
"links": [
[
1,
1,
0,
2,
4,
"INT"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [
0,
0
]
}
},
"version": 0.4
}

View File

@@ -0,0 +1,104 @@
{
"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": "PrimitiveNode",
"pos": {
"0": 14,
"1": 43
},
"size": [
203.1999969482422,
40.368401303242536
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "connect to widget input",
"type": "*",
"links": [],
"slot_index": 0
}
],
"properties": {
"Run widget replace on values": false
},
"widgets_values": []
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [
0,
0
]
}
},
"version": 0.4
}

View File

@@ -0,0 +1,88 @@
{
"last_node_id": 9,
"last_link_id": 13,
"nodes": [
{
"id": 3,
"type": "KSampler",
"pos": {
"0": 10.321063995361328,
"1": 73.14462280273438
},
"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
}
],
"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": [
{
"title": "Group",
"bounding": [
0,
0,
335,
346
],
"color": "#3f789e",
"font_size": 24
}
],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [
0,
0
]
}
},
"version": 0.4
}

View File

@@ -0,0 +1,30 @@
{
"last_node_id": 9,
"last_link_id": 13,
"nodes": [],
"links": [],
"groups": [
{
"title": "Group",
"bounding": [
0,
0,
335,
346
],
"color": "#3f789e",
"font_size": 24
}
],
"config": {},
"extra": {
"ds": {
"scale": 1.2100000000000006,
"offset": [
104.34159172650945,
241.35965953210126
]
}
},
"version": 0.4
}

View File

@@ -0,0 +1,76 @@
{
"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
}
],
"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

@@ -0,0 +1,46 @@
{
"last_node_id": 9,
"last_link_id": 9,
"nodes": [
{
"id": 9,
"type": "SaveImage",
"pos": {
"0": 64,
"1": 104
},
"size": {
"0": 210,
"1": 58
},
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": null
}
],
"outputs": [],
"properties": {},
"widgets_values": [
"ComfyUI"
]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [
0,
0
]
}
},
"version": 0.4
}

View File

@@ -0,0 +1,42 @@
{
"last_node_id": 1,
"last_link_id": 0,
"nodes": [
{
"id": 1,
"type": "DevToolsNodeWithStringInput",
"pos": [
15,
48
],
"size": [
315,
58
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [],
"properties": {
"Node name for S&R": "DevToolsNodeWithStringInput"
},
"widgets_values": [
""
]
}
],
"links": [],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [
0,
0
]
}
},
"version": 0.4
}

View File

@@ -0,0 +1,377 @@
{
"last_node_id": 0,
"last_link_id": 18,
"nodes": [
{
"id": "CheckpointLoaderSimple.0",
"type": "CheckpointLoaderSimple",
"pos": {
"0": 100,
"1": 130
},
"size": {
"0": 315,
"1": 98
},
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [
12
],
"shape": 3
},
{
"name": "CLIP",
"type": "CLIP",
"links": [
10,
11
],
"shape": 3
},
{
"name": "VAE",
"type": "VAE",
"links": [
17
],
"shape": 3
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": [
"v1-5-pruned-emaonly.ckpt"
]
},
{
"id": "CLIPTextEncode.0",
"type": "CLIPTextEncode",
"pos": {
"0": 515,
"1": 130
},
"size": {
"0": 400,
"1": 200
},
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 10
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
13
],
"shape": 3
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,"
]
},
{
"id": "CLIPTextEncode.1",
"type": "CLIPTextEncode",
"pos": {
"0": 515,
"1": 460
},
"size": {
"0": 400,
"1": 200
},
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 11
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [
14
],
"shape": 3
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode"
},
"widgets_values": [
"text, watermark"
]
},
{
"id": "EmptyLatentImage.0",
"type": "EmptyLatentImage",
"pos": {
"0": 100,
"1": 358
},
"size": {
"0": 315,
"1": 106
},
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
15
],
"shape": 3
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage"
},
"widgets_values": [
512,
512,
1
]
},
{
"id": "KSampler.0",
"type": "KSampler",
"pos": {
"0": 1015,
"1": 130
},
"size": {
"0": 315,
"1": 262
},
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 12
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 13
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 14
},
{
"name": "latent_image",
"type": "LATENT",
"link": 15
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [
16
],
"shape": 3
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [
3,
"randomize",
20,
8,
"euler",
"normal",
1
]
},
{
"id": "VAEDecode.0",
"type": "VAEDecode",
"pos": {
"0": 1430,
"1": 130
},
"size": {
"0": 210,
"1": 46
},
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 16
},
{
"name": "vae",
"type": "VAE",
"link": 17
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
18
],
"shape": 3
}
],
"properties": {
"Node name for S&R": "VAEDecode"
}
},
{
"id": "SaveImage.0",
"type": "SaveImage",
"pos": {
"0": 1740,
"1": 130
},
"size": {
"0": 315,
"1": 58
},
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 18
}
],
"outputs": [],
"properties": {},
"widgets_values": [
"ComfyUI"
]
}
],
"links": [
[
10,
"CheckpointLoaderSimple.0",
1,
"CLIPTextEncode.0",
0,
"CLIP"
],
[
11,
"CheckpointLoaderSimple.0",
1,
"CLIPTextEncode.1",
0,
"CLIP"
],
[
12,
"CheckpointLoaderSimple.0",
0,
"KSampler.0",
0,
"MODEL"
],
[
13,
"CLIPTextEncode.0",
0,
"KSampler.0",
1,
"CONDITIONING"
],
[
14,
"CLIPTextEncode.1",
0,
"KSampler.0",
2,
"CONDITIONING"
],
[
15,
"EmptyLatentImage.0",
0,
"KSampler.0",
3,
"LATENT"
],
[
16,
"KSampler.0",
0,
"VAEDecode.0",
0,
"LATENT"
],
[
17,
"CheckpointLoaderSimple.0",
2,
"VAEDecode.0",
1,
"VAE"
],
[
18,
"VAEDecode.0",
0,
"SaveImage.0",
0,
"IMAGE"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [
149.9747408641311,
383.8593224280729
]
}
},
"version": 0.4
}

View File

@@ -0,0 +1,6 @@
[
{
"name": "vintageClipboard Template",
"data": "{\"nodes\":[{\"id\":-1,\"type\":\"CheckpointLoaderSimple\",\"pos\":[26,474],\"size\":[315,98],\"flags\":{},\"order\":1,\"mode\":0,\"inputs\":[],\"outputs\":[{\"name\":\"MODEL\",\"type\":\"MODEL\",\"links\":[],\"slot_index\":0},{\"name\":\"CLIP\",\"type\":\"CLIP\",\"links\":[],\"slot_index\":1},{\"name\":\"VAE\",\"type\":\"VAE\",\"links\":[],\"slot_index\":2}],\"properties\":{\"Node name for S&R\":\"CheckpointLoaderSimple\"},\"widgets_values\":[\"v1-5-pruned-emaonly.ckpt\"]},{\"id\":-1,\"type\":\"CLIPTextEncode\",\"pos\":[415,186],\"size\":[422.84503173828125,164.31304931640625],\"flags\":{},\"order\":2,\"mode\":0,\"inputs\":[{\"name\":\"clip\",\"type\":\"CLIP\",\"link\":null}],\"outputs\":[{\"name\":\"CONDITIONING\",\"type\":\"CONDITIONING\",\"links\":[],\"slot_index\":0}],\"properties\":{\"Node name for S&R\":\"CLIPTextEncode\"},\"widgets_values\":[\"beautiful scenery nature glass bottle landscape, , purple galaxy bottle,\"]},{\"id\":-1,\"type\":\"CLIPTextEncode\",\"pos\":[413,389],\"size\":[425.27801513671875,180.6060791015625],\"flags\":{},\"order\":3,\"mode\":0,\"inputs\":[{\"name\":\"clip\",\"type\":\"CLIP\",\"link\":null}],\"outputs\":[{\"name\":\"CONDITIONING\",\"type\":\"CONDITIONING\",\"links\":[],\"slot_index\":0}],\"properties\":{\"Node name for S&R\":\"CLIPTextEncode\"},\"widgets_values\":[\"text, watermark\"]}],\"links\":[[0,1,1,0,4],[0,1,2,0,4]]}"
}
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

View File

@@ -0,0 +1,51 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Browser tab title', () => {
test.describe('Beta Menu', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.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
})
expect(await comfyPage.page.title()).toBe(`${workflowName} - ComfyUI`)
})
// Failing on CI
// Cannot reproduce locally
test.skip('Can display workflow name with unsaved changes', async ({
comfyPage
}) => {
const workflowName = await comfyPage.page.evaluate(async () => {
return window['app'].extensionManager.workflow.activeWorkflow.filename
})
expect(await comfyPage.page.title()).toBe(`${workflowName} - ComfyUI`)
await comfyPage.menu.topbar.saveWorkflow('test')
expect(await comfyPage.page.title()).toBe('test - ComfyUI')
const textBox = comfyPage.widgetTextBox
await textBox.fill('Hello World')
await comfyPage.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()
})
})
})
test.describe('Legacy Menu', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test('Can display default title', async ({ comfyPage }) => {
expect(await comfyPage.page.title()).toBe('ComfyUI')
})
})
})

View File

@@ -0,0 +1,172 @@
import {
ComfyPage,
comfyPageFixture as test,
comfyExpect as expect
} from './fixtures/ComfyPage'
import type { useWorkspaceStore } from '../src/stores/workspaceStore'
type WorkspaceStore = ReturnType<typeof useWorkspaceStore>
async function beforeChange(comfyPage: ComfyPage) {
await comfyPage.page.evaluate(() => {
window['app'].canvas.emitBeforeChange()
})
}
async function afterChange(comfyPage: ComfyPage) {
await comfyPage.page.evaluate(() => {
window['app'].canvas.emitAfterChange()
})
}
test.describe('Change Tracker', () => {
test.describe('Undo/Redo', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setupWorkflowsDirectory({})
})
test('Can undo multiple operations', async ({ comfyPage }) => {
function isModified() {
return comfyPage.page.evaluate(async () => {
return !!(window['app'].extensionManager as WorkspaceStore).workflow
.activeWorkflow?.isModified
})
}
function getUndoQueueSize() {
return comfyPage.page.evaluate(() => {
const workflow = (window['app'].extensionManager as WorkspaceStore)
.workflow.activeWorkflow
return workflow?.changeTracker.undoQueue.length
})
}
function getRedoQueueSize() {
return comfyPage.page.evaluate(() => {
const workflow = (window['app'].extensionManager as WorkspaceStore)
.workflow.activeWorkflow
return workflow?.changeTracker.redoQueue.length
})
}
expect(await getUndoQueueSize()).toBe(0)
expect(await 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 isModified()).toBe(false)
// TODO(huchenlei): Investigate why saving the workflow is causing the
// undo queue to be triggered.
expect(await getUndoQueueSize()).toBe(1)
expect(await getRedoQueueSize()).toBe(0)
const node = (await comfyPage.getFirstNodeRef())!
await node.click('collapse')
await expect(node).toBeCollapsed()
expect(await isModified()).toBe(true)
expect(await getUndoQueueSize()).toBe(2)
expect(await getRedoQueueSize()).toBe(0)
await comfyPage.ctrlB()
await expect(node).toBeBypassed()
expect(await isModified()).toBe(true)
expect(await getUndoQueueSize()).toBe(3)
expect(await getRedoQueueSize()).toBe(0)
await comfyPage.ctrlZ()
await expect(node).not.toBeBypassed()
expect(await isModified()).toBe(true)
expect(await getUndoQueueSize()).toBe(2)
expect(await getRedoQueueSize()).toBe(1)
await comfyPage.ctrlZ()
await expect(node).not.toBeCollapsed()
expect(await isModified()).toBe(false)
expect(await getUndoQueueSize()).toBe(1)
expect(await getRedoQueueSize()).toBe(2)
})
})
test('Can group multiple change actions into a single transaction', async ({
comfyPage
}) => {
const node = (await comfyPage.getFirstNodeRef())!
expect(node).toBeTruthy()
await expect(node).not.toBeCollapsed()
await expect(node).not.toBeBypassed()
// Make changes outside set
// Bypass + collapse node
await node.click('collapse')
await comfyPage.ctrlB()
await expect(node).toBeCollapsed()
await expect(node).toBeBypassed()
// Undo, undo, ensure both changes undone
await comfyPage.ctrlZ()
await expect(node).not.toBeBypassed()
await expect(node).toBeCollapsed()
await comfyPage.ctrlZ()
await expect(node).not.toBeBypassed()
await expect(node).not.toBeCollapsed()
// Run again, but within a change transaction
await beforeChange(comfyPage)
await node.click('collapse')
await comfyPage.ctrlB()
await expect(node).toBeCollapsed()
await expect(node).toBeBypassed()
// End transaction
await afterChange(comfyPage)
// Ensure undo reverts both changes
await comfyPage.ctrlZ()
await expect(node).not.toBeBypassed()
await expect(node).not.toBeCollapsed()
})
test('Can nest multiple change transactions without adding undo steps', async ({
comfyPage
}) => {
const node = (await comfyPage.getFirstNodeRef())!
const bypassAndPin = async () => {
await beforeChange(comfyPage)
await comfyPage.ctrlB()
await expect(node).toBeBypassed()
await comfyPage.page.keyboard.press('KeyP')
await comfyPage.nextFrame()
await expect(node).toBePinned()
await afterChange(comfyPage)
}
const collapse = async () => {
await beforeChange(comfyPage)
await node.click('collapse', { moveMouseToEmptyArea: true })
await expect(node).toBeCollapsed()
await afterChange(comfyPage)
}
const multipleChanges = async () => {
await beforeChange(comfyPage)
// Call other actions that uses begin/endChange
await collapse()
await bypassAndPin()
await afterChange(comfyPage)
}
await multipleChanges()
await comfyPage.ctrlZ()
await expect(node).not.toBeBypassed()
await expect(node).not.toBePinned()
await expect(node).not.toBeCollapsed()
await comfyPage.ctrlY()
await expect(node).toBeBypassed()
await expect(node).toBePinned()
await expect(node).toBeCollapsed()
})
})

View File

@@ -0,0 +1,239 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
const customColorPalettes = {
obsidian: {
version: 102,
id: 'obsidian',
name: 'Obsidian',
colors: {
node_slot: {
CLIP: '#FFD500',
CLIP_VISION: '#A8DADC',
CLIP_VISION_OUTPUT: '#ad7452',
CONDITIONING: '#FFA931',
CONTROL_NET: '#6EE7B7',
IMAGE: '#64B5F6',
LATENT: '#FF9CF9',
MASK: '#81C784',
MODEL: '#B39DDB',
STYLE_MODEL: '#C2FFAE',
VAE: '#FF6E6E',
TAESD: '#DCC274',
PIPE_LINE: '#7737AA',
PIPE_LINE_SDXL: '#7737AA',
INT: '#29699C',
XYPLOT: '#74DA5D',
X_Y: '#38291f'
},
litegraph_base: {
BACKGROUND_IMAGE:
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII=',
CLEAR_BACKGROUND_COLOR: '#222222',
NODE_TITLE_COLOR: 'rgba(255,255,255,.75)',
NODE_SELECTED_TITLE_COLOR: '#FFF',
NODE_TEXT_SIZE: 14,
NODE_TEXT_COLOR: '#b8b8b8',
NODE_SUBTEXT_SIZE: 12,
NODE_DEFAULT_COLOR: 'rgba(0,0,0,.8)',
NODE_DEFAULT_BGCOLOR: 'rgba(22,22,22,.8)',
NODE_DEFAULT_BOXCOLOR: 'rgba(255,255,255,.75)',
NODE_DEFAULT_SHAPE: 'box',
NODE_BOX_OUTLINE_COLOR: '#236692',
DEFAULT_SHADOW_COLOR: 'rgba(0,0,0,0)',
DEFAULT_GROUP_FONT: 24,
WIDGET_BGCOLOR: '#242424',
WIDGET_OUTLINE_COLOR: '#333',
WIDGET_TEXT_COLOR: '#a3a3a8',
WIDGET_SECONDARY_TEXT_COLOR: '#97979c',
LINK_COLOR: '#9A9',
EVENT_LINK_COLOR: '#A86',
CONNECTING_LINK_COLOR: '#AFA'
},
comfy_base: {
'fg-color': '#fff',
'bg-color': '#242424',
'comfy-menu-bg': 'rgba(24,24,24,.9)',
'comfy-input-bg': '#262626',
'input-text': '#ddd',
'descrip-text': '#999',
'drag-text': '#ccc',
'error-text': '#ff4444',
'border-color': '#29292c',
'tr-even-bg-color': 'rgba(28,28,28,.9)',
'tr-odd-bg-color': 'rgba(19,19,19,.9)'
}
}
},
obsidian_dark: {
version: 102,
id: 'obsidian_dark',
name: 'Obsidian Dark',
colors: {
node_slot: {
CLIP: '#FFD500',
CLIP_VISION: '#A8DADC',
CLIP_VISION_OUTPUT: '#ad7452',
CONDITIONING: '#FFA931',
CONTROL_NET: '#6EE7B7',
IMAGE: '#64B5F6',
LATENT: '#FF9CF9',
MASK: '#81C784',
MODEL: '#B39DDB',
STYLE_MODEL: '#C2FFAE',
VAE: '#FF6E6E',
TAESD: '#DCC274',
PIPE_LINE: '#7737AA',
PIPE_LINE_SDXL: '#7737AA',
INT: '#29699C',
XYPLOT: '#74DA5D',
X_Y: '#38291f'
},
litegraph_base: {
BACKGROUND_IMAGE:
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAACXBIWXMAAAsTAAALEwEAmpwYAAAGlmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4xLWMwMDEgNzkuMTQ2Mjg5OSwgMjAyMy8wNi8yNS0yMDowMTo1NSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAyMy0xMS0xM1QwMDoxODowMiswMTowMCIgeG1wOk1vZGlmeURhdGU9IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOmIyYzRhNjA5LWJmYTctYTg0MC1iOGFlLTk3MzE2ZjM1ZGIyNyIgeG1wTU06RG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjk0ZmNlZGU4LTE1MTctZmQ0MC04ZGU3LWYzOTgxM2E3ODk5ZiIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOjIzMWIxMGIwLWI0ZmItMDI0ZS1iMTJlLTMwNTMwM2NkMDdjOCI+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNyZWF0ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6MjMxYjEwYjAtYjRmYi0wMjRlLWIxMmUtMzA1MzAzY2QwN2M4IiBzdEV2dDp3aGVuPSIyMDIzLTExLTEzVDAwOjE4OjAyKzAxOjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgMjUuMSAoV2luZG93cykiLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjQ4OWY1NzlmLTJkNjUtZWQ0Zi04OTg0LTA4NGE2MGE1ZTMzNSIgc3RFdnQ6d2hlbj0iMjAyMy0xMS0xNVQwMjowNDo1OSswMTowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIDI1LjEgKFdpbmRvd3MpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpiMmM0YTYwOS1iZmE3LWE4NDAtYjhhZS05NzMxNmYzNWRiMjciIHN0RXZ0OndoZW49IjIwMjMtMTEtMTVUMDI6MDQ6NTkrMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNS4xIChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4OTe6GAAAAx0lEQVR42u3WMQoAIQxFwRzJys77X8vSLiRgITif7bYbgrwYc/mKXyBoY4VVBgsWLFiwYFmOlTv+9jfDOjHmr8u6eVkGCxYsWLBgmc5S8ApewXvgYRksWLBgKXidpeBdloL3wMOCBctgwVLwCl7BuyyDBQsWLFiwTGcpeAWv4D3wsAwWLFiwFLzOUvAuS8F74GHBgmWwYCl4Ba/gXZbBggULFixYprMUvIJX8B54WAYLFixYCl5nKXiXpeA98LBgwTJYsGC9tg1o8f4TTtqzNQAAAABJRU5ErkJggg==',
CLEAR_BACKGROUND_COLOR: '#000',
NODE_TITLE_COLOR: 'rgba(255,255,255,.75)',
NODE_SELECTED_TITLE_COLOR: '#FFF',
NODE_TEXT_SIZE: 14,
NODE_TEXT_COLOR: '#b8b8b8',
NODE_SUBTEXT_SIZE: 12,
NODE_DEFAULT_COLOR: 'rgba(0,0,0,.8)',
NODE_DEFAULT_BGCOLOR: 'rgba(22,22,22,.8)',
NODE_DEFAULT_BOXCOLOR: 'rgba(255,255,255,.75)',
NODE_DEFAULT_SHAPE: 'box',
NODE_BOX_OUTLINE_COLOR: '#236692',
DEFAULT_SHADOW_COLOR: 'rgba(0,0,0,0)',
DEFAULT_GROUP_FONT: 24,
WIDGET_BGCOLOR: '#242424',
WIDGET_OUTLINE_COLOR: '#333',
WIDGET_TEXT_COLOR: '#a3a3a8',
WIDGET_SECONDARY_TEXT_COLOR: '#97979c',
LINK_COLOR: '#9A9',
EVENT_LINK_COLOR: '#A86',
CONNECTING_LINK_COLOR: '#AFA'
},
comfy_base: {
'fg-color': '#fff',
'bg-color': '#242424',
'comfy-menu-bg': 'rgba(24,24,24,.9)',
'comfy-input-bg': '#262626',
'input-text': '#ddd',
'descrip-text': '#999',
'drag-text': '#ccc',
'error-text': '#ff4444',
'border-color': '#29292c',
'tr-even-bg-color': 'rgba(28,28,28,.9)',
'tr-odd-bg-color': 'rgba(19,19,19,.9)'
}
}
}
}
test.describe('Color Palette', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.CustomColorPalettes', customColorPalettes)
})
test('Can show custom color palette', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.ColorPalette', 'custom_obsidian_dark')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'custom-color-palette-obsidian-dark.png'
)
await comfyPage.setSetting('Comfy.ColorPalette', 'dark')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('default-color-palette.png')
})
})
test.describe('Node Color Adjustments', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.loadWorkflow('every_node_color')
})
test('should adjust opacity via node opacity setting', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Node.Opacity', 0.5)
await comfyPage.page.waitForTimeout(128)
// 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.page.waitForTimeout(128)
await comfyPage.page.mouse.move(8, 8)
await expect(comfyPage.canvas).toHaveScreenshot('node-opacity-1.png')
})
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.nextFrame()
await comfyPage.page.mouse.move(0, 0)
await expect(comfyPage.canvas).toHaveScreenshot(
'node-opacity-0.2-arc-theme.png'
)
})
test('should not serialize color adjustments in workflow', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Node.Opacity', 0.5)
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
const saveWorkflowInterval = 1000
await comfyPage.page.waitForTimeout(saveWorkflowInterval)
const workflow = await comfyPage.page.evaluate(() => {
return localStorage.getItem('workflow')
})
for (const node of JSON.parse(workflow).nodes) {
if (node.bgcolor) expect(node.bgcolor).not.toMatch(/hsla/)
if (node.color) expect(node.color).not.toMatch(/hsla/)
}
})
test('should lighten node colors when switching to light theme', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('node-lightened-colors.png')
})
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 node?.clickContextMenuOption('Colors')
})
test('should persist color adjustments when changing custom node colors', async ({
comfyPage
}) => {
await comfyPage.page
.locator('.litemenu-entry.submenu span:has-text("red")')
.click()
await expect(comfyPage.canvas).toHaveScreenshot(
'node-opacity-0.3-color-changed.png'
)
})
test('should persist color adjustments when removing custom node color', async ({
comfyPage
}) => {
await comfyPage.page
.locator('.litemenu-entry.submenu span:has-text("No color")')
.click()
await expect(comfyPage.canvas).toHaveScreenshot(
'node-opacity-0.3-color-removed.png'
)
})
})
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

View File

@@ -0,0 +1,49 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Keybindings', () => {
test('Should execute command', async ({ comfyPage }) => {
await comfyPage.registerCommand('TestCommand', () => {
window['foo'] = true
})
await comfyPage.executeCommand('TestCommand')
expect(await comfyPage.page.evaluate(() => window['foo'])).toBe(true)
})
test('Should execute async command', async ({ comfyPage }) => {
await comfyPage.registerCommand('TestCommand', async () => {
await new Promise<void>((resolve) =>
setTimeout(() => {
window['foo'] = true
resolve()
}, 5)
)
})
await comfyPage.executeCommand('TestCommand')
expect(await comfyPage.page.evaluate(() => window['foo'])).toBe(true)
})
test('Should handle command errors', async ({ comfyPage }) => {
await comfyPage.registerCommand('TestCommand', () => {
throw new Error('Test error')
})
await comfyPage.executeCommand('TestCommand')
await expect(comfyPage.page.locator('.p-toast')).toBeVisible()
})
test('Should handle async command errors', async ({ comfyPage }) => {
await comfyPage.registerCommand('TestCommand', async () => {
await new Promise<void>((resolve, reject) =>
setTimeout(() => {
reject(new Error('Test error'))
}, 5)
)
})
await comfyPage.executeCommand('TestCommand')
await expect(comfyPage.page.locator('.p-toast')).toBeVisible()
})
})

View File

@@ -1,5 +1,5 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from './ComfyPage'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Copy Paste', () => {
test('Can copy and paste node', async ({ comfyPage }) => {
@@ -15,9 +15,9 @@ test.describe('Copy Paste', () => {
await textBox.click()
const originalString = await textBox.inputValue()
await textBox.selectText()
await comfyPage.ctrlC()
await comfyPage.ctrlV()
await comfyPage.ctrlV()
await comfyPage.ctrlC(null)
await comfyPage.ctrlV(null)
await comfyPage.ctrlV(null)
const resultString = await textBox.inputValue()
expect(resultString).toBe(originalString + originalString)
})
@@ -31,7 +31,7 @@ test.describe('Copy Paste', () => {
y: 643
}
})
await comfyPage.ctrlC()
await comfyPage.ctrlC(null)
// KSampler's seed
await comfyPage.canvas.click({
position: {
@@ -39,7 +39,7 @@ test.describe('Copy Paste', () => {
y: 281
}
})
await comfyPage.ctrlV()
await comfyPage.ctrlV(null)
await comfyPage.page.keyboard.press('Enter')
await expect(comfyPage.canvas).toHaveScreenshot('copied-widget-value.png')
})
@@ -51,14 +51,14 @@ test.describe('Copy Paste', () => {
comfyPage
}) => {
await comfyPage.clickEmptyLatentNode()
await comfyPage.ctrlC()
await comfyPage.ctrlC(null)
const textBox = comfyPage.widgetTextBox
await textBox.click()
await textBox.inputValue()
await textBox.selectText()
await comfyPage.ctrlC()
await comfyPage.ctrlV()
await comfyPage.ctrlV()
await comfyPage.ctrlC(null)
await comfyPage.ctrlV(null)
await comfyPage.ctrlV(null)
await expect(comfyPage.canvas).toHaveScreenshot(
'paste-in-text-area-with-node-previously-copied.png'
)
@@ -69,10 +69,10 @@ test.describe('Copy Paste', () => {
await textBox.click()
await textBox.inputValue()
await textBox.selectText()
await comfyPage.ctrlC()
await comfyPage.ctrlC(null)
// Unfocus textbox.
await comfyPage.page.mouse.click(10, 10)
await comfyPage.ctrlV()
await comfyPage.ctrlV(null)
await expect(comfyPage.canvas).toHaveScreenshot('no-node-copied.png')
})
@@ -86,4 +86,23 @@ test.describe('Copy Paste', () => {
await comfyPage.page.keyboard.up('Alt')
await expect(comfyPage.canvas).toHaveScreenshot('drag-copy-copied-node.png')
})
test('Can undo paste multiple nodes as single action', async ({
comfyPage
}) => {
const initialCount = await comfyPage.getGraphNodesCount()
expect(initialCount).toBeGreaterThan(1)
await comfyPage.canvas.click()
await comfyPage.ctrlA()
await comfyPage.page.mouse.move(10, 10)
await comfyPage.ctrlC()
await comfyPage.ctrlV()
const pasteCount = await comfyPage.getGraphNodesCount()
expect(pasteCount).toBe(initialCount * 2)
await comfyPage.ctrlZ()
const undoCount = await comfyPage.getGraphNodesCount()
expect(undoCount).toBe(initialCount)
})
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 101 KiB

View File

@@ -1,5 +1,5 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from './ComfyPage'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Load workflow warning', () => {
test('Should display a warning when loading a workflow with missing nodes', async ({
@@ -13,15 +13,89 @@ test.describe('Load workflow warning', () => {
})
})
test('Does not report warning on undo/redo', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default')
await comfyPage.loadWorkflow('missing_nodes')
await comfyPage.closeDialog()
// Make a change to the graph
await comfyPage.doubleClickCanvas()
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler')
// Undo and redo the change
await comfyPage.ctrlZ()
await expect(comfyPage.page.locator('.comfy-missing-nodes')).not.toBeVisible()
await comfyPage.ctrlY()
await expect(comfyPage.page.locator('.comfy-missing-nodes')).not.toBeVisible()
})
test.describe('Execution error', () => {
test('Should display an error message when an execution error occurs', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('execution_error')
await comfyPage.queueButton.click()
await comfyPage.nextFrame()
// Wait for the element with the .comfy-execution-error selector to be visible
const executionError = comfyPage.page.locator('.comfy-error-report')
await expect(executionError).toBeVisible()
})
})
test.describe('Missing models warning', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Workflow.ShowMissingModelsWarning', true)
await comfyPage.page.evaluate((url: string) => {
return fetch(`${url}/api/devtools/cleanup_fake_model`)
}, comfyPage.url)
})
// Flaky test after parallelization
// https://github.com/Comfy-Org/ComfyUI_frontend/pull/1400
test.skip('Should display a warning when missing models are found', async ({
comfyPage
}) => {
// The fake_model.safetensors is served by
// https://github.com/Comfy-Org/ComfyUI_devtools/blob/main/__init__.py
await comfyPage.loadWorkflow('missing_models')
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
await expect(missingModelsWarning).toBeVisible()
const downloadButton = comfyPage.page.getByLabel('Download')
await expect(downloadButton).toBeVisible()
const downloadPromise = comfyPage.page.waitForEvent('download')
await downloadButton.click()
const download = await downloadPromise
expect(download.suggestedFilename()).toBe('fake_model.safetensors')
})
})
test.describe('Settings', () => {
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)
await test.step('Setting should persist', async () => {
expect(await comfyPage.getSetting('Comfy.Graph.ZoomSpeed')).toBe(maxSpeed)
})
})
})

View File

@@ -0,0 +1,160 @@
import { expect, Locator } from '@playwright/test'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Topbar commands', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
})
test('Should allow registering topbar commands', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
window['app'].registerExtension({
name: 'TestExtension1',
commands: [
{
id: 'foo',
label: 'foo-command',
function: () => {
window['foo'] = true
}
}
],
menuCommands: [
{
path: ['ext'],
commands: ['foo']
}
]
})
})
await comfyPage.menu.topbar.triggerTopbarCommand(['ext', 'foo-command'])
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.page.evaluate(() => {
window['app'].registerExtension({
name: 'TestExtension1',
menuCommands: [
{
path: ['ext'],
commands: ['foo']
}
]
})
})
const menuItem = comfyPage.menu.topbar.getMenuItem('ext')
expect(await menuItem.count()).toBe(0)
})
test('Should allow registering keybindings', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
const app = window['app']
app.registerExtension({
name: 'TestExtension1',
commands: [
{
id: 'TestCommand',
function: () => {
window['TestCommand'] = true
}
}
],
keybindings: [
{
combo: { key: 'k' },
commandId: 'TestCommand'
}
]
})
})
await comfyPage.page.keyboard.press('k')
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({
name: 'TestExtension1',
settings: [
{
id: 'TestSetting',
name: 'Test Setting',
type: 'text',
defaultValue: 'Hello, world!',
onChange: () => {
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!')
await comfyPage.setSetting('TestSetting', 'Hello, universe!')
expect(await comfyPage.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({
name: 'TestExtension1',
settings: [
{
id: 'Comfy.TestSetting',
name: 'Test Setting',
type: 'boolean',
defaultValue: false,
onChange: () => {
window['changeCount'] = (window['changeCount'] ?? 0) + 1
}
}
]
})
})
expect(await comfyPage.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)
})
})
test.describe('About panel', () => {
test('Should allow adding badges', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
window['app'].registerExtension({
name: 'TestExtension1',
aboutPageBadges: [
{
label: 'Test Badge',
url: 'https://example.com',
icon: 'pi pi-box'
}
]
})
})
await comfyPage.settingDialog.open()
await comfyPage.settingDialog.goToAboutPanel()
const badge = comfyPage.page.locator('.about-badge').last()
expect(badge).toBeDefined()
expect(await badge.textContent()).toContain('Test Badge')
})
})
})

View File

@@ -0,0 +1,886 @@
import type { Page, Locator, APIRequestContext } from '@playwright/test'
import { expect } from '@playwright/test'
import { test as base } from '@playwright/test'
import { ComfyActionbar } from '../helpers/actionbar'
import dotenv from 'dotenv'
dotenv.config()
import * as fs from 'fs'
import { NodeBadgeMode } from '../../src/types/nodeSource'
import type { NodeId } from '../../src/types/comfyWorkflow'
import type { KeyCombo } from '../../src/types/keyBindingTypes'
import { ComfyTemplates } from '../helpers/templates'
import { ComfyNodeSearchBox } from './components/ComfyNodeSearchBox'
import {
NodeLibrarySidebarTab,
WorkflowsSidebarTab
} from './components/SidebarTab'
import { Topbar } from './components/Topbar'
import { NodeReference } from './utils/litegraphUtils'
import type { Position, Size } from './types'
import { SettingDialog } from './components/SettingDialog'
class ComfyMenu {
public readonly sideToolbar: Locator
public readonly themeToggleButton: Locator
public readonly saveButton: Locator
constructor(public readonly page: Page) {
this.sideToolbar = page.locator('.side-tool-bar-container')
this.themeToggleButton = page.locator('.comfy-vue-theme-toggle')
this.saveButton = page
.locator('button[title="Save the current workflow"]')
.nth(0)
}
get nodeLibraryTab() {
return new NodeLibrarySidebarTab(this.page)
}
get workflowsTab() {
return new WorkflowsSidebarTab(this.page)
}
get topbar() {
return new Topbar(this.page)
}
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 }
)
setTimeout(resolve, 5000)
})
})
}
async getThemeId() {
return await this.page.evaluate(async () => {
return await window['app'].ui.settings.getSettingValue(
'Comfy.ColorPalette'
)
})
}
}
type FolderStructure = {
[key: string]: FolderStructure | string
}
export class ComfyPage {
public readonly url: string
// All canvas position operations are based on default view of canvas.
public readonly canvas: Locator
public readonly widgetTextBox: Locator
public readonly contextMenu: Locator
// Buttons
public readonly resetViewButton: Locator
public readonly queueButton: Locator
// Inputs
public readonly workflowUploadInput: Locator
// Components
public readonly searchBox: ComfyNodeSearchBox
public readonly menu: ComfyMenu
public readonly actionbar: ComfyActionbar
public readonly templates: ComfyTemplates
public readonly settingDialog: SettingDialog
/** Worker index to test user ID */
public readonly userIds: string[] = []
/** Test user ID for the current context */
get id() {
return this.userIds[comfyPageFixture.info().parallelIndex]
}
constructor(
public readonly page: Page,
public readonly request: APIRequestContext
) {
this.url = process.env.PLAYWRIGHT_TEST_URL || 'http://localhost:8188'
this.canvas = page.locator('#graph-canvas')
this.widgetTextBox = page.getByPlaceholder('text').nth(1)
this.contextMenu = page.locator('.litegraph.litecontextmenu')
this.resetViewButton = page.getByRole('button', { name: 'Reset View' })
this.queueButton = page.getByRole('button', { name: 'Queue Prompt' })
this.workflowUploadInput = page.locator('#comfy-file-input')
this.searchBox = new ComfyNodeSearchBox(page)
this.menu = new ComfyMenu(page)
this.actionbar = new ComfyActionbar(page)
this.templates = new ComfyTemplates(page)
this.settingDialog = new SettingDialog(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<number> {
return await this.page.evaluate(() => {
return window['app']?.graph?.nodes?.length || 0
})
}
async getSelectedGraphNodesCount(): Promise<number> {
return await this.page.evaluate(() => {
return (
window['app']?.graph?.nodes?.filter(
(node: any) => node.is_selected === true
).length || 0
)
})
}
async getGraphSelectedItemsCount(): Promise<number | undefined> {
return await this.page.evaluate(() => {
return window['app']?.canvas?.selectedItems?.size
})
}
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()}`
)
}
}
async setupUser(username: string) {
const res = await this.request.get(`${this.url}/api/users`)
if (res.status() !== 200)
throw new Error(`Failed to retrieve users: ${await res.text()}`)
const apiRes = await res.json()
const user = Object.entries(apiRes?.users ?? {}).find(
([, name]) => name === username
)
const id = user?.[0]
return id ? id : await this.createUser(username)
}
async createUser(username: string) {
const resp = await this.request.post(`${this.url}/api/users`, {
data: { username }
})
if (resp.status() !== 200)
throw new Error(`Failed to create user: ${await resp.text()}`)
return await resp.json()
}
async clearNodeTemplates() {
const resp = await this.request.delete(
`${this.url}/api/userdata/comfy.templates.json`,
{
headers: { 'Comfy-User': this.id }
}
)
const status = resp.status()
if (status !== 204 && status !== 404)
throw new Error(`Failed to delete node templates: ${await resp.text()}`)
}
async setNodeTemplates(fileName: string) {
const path = this.assetPath(fileName)
const data = fs.readFileSync(path, 'utf-8')
const resp = await this.request.post(
`${this.url}/api/userdata/comfy.templates.json`,
{
headers: {
'Comfy-User': this.id,
overwrite: 'true',
full_info: 'true'
},
data
}
)
if (resp.status() !== 200)
throw new Error(`Failed to upload node templates: ${await resp.text()}`)
}
async setupSettings(settings: Record<string, any>) {
const resp = await this.request.post(
`${this.url}/api/devtools/set_settings`,
{
data: settings
}
)
if (resp.status() !== 200) {
throw new Error(`Failed to setup settings: ${await resp.text()}`)
}
}
async setup() {
await this.goto()
await this.page.evaluate((id) => {
localStorage.clear()
sessionStorage.clear()
localStorage.setItem('Comfy.userId', id)
}, this.id)
await this.goto()
// Unify font for consistent screenshots.
await this.page.addStyleTag({
url: 'https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap'
})
await this.page.addStyleTag({
url: 'https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&family=Roboto+Mono:ital,wght@0,100..700;1,100..700&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap'
})
await this.page.addStyleTag({
content: `
* {
font-family: 'Roboto Mono', 'Noto Color Emoji';
}`
})
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
)
await this.page.waitForSelector('.p-blockui-mask', { state: 'hidden' })
await this.nextFrame()
}
public assetPath(fileName: string) {
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<void>)
) {
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 reload() {
await this.page.reload({ timeout: 15000 })
await this.setup()
}
async goto() {
await this.page.goto(this.url)
}
async nextFrame() {
await this.page.evaluate(() => {
return new Promise<number>(requestAnimationFrame)
})
}
async delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms))
}
async loadWorkflow(workflowName: string) {
await this.workflowUploadInput.setInputFiles(
this.assetPath(`${workflowName}.json`)
)
await this.nextFrame()
}
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.page.locator('.p-toast:visible').count()
}
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,
modifierKey?: 'ControlOrMeta' | 'Control' | 'Alt' | 'Shift'
) {
if (modifierKey) await this.page.keyboard.down(modifierKey)
await this.page.mouse.move(source.x, source.y)
await this.page.mouse.down()
await this.page.mouse.move(target.x, target.y)
await this.page.mouse.up()
if (modifierKey) await this.page.keyboard.up(modifierKey)
await this.nextFrame()
}
async dragAndDropFile(fileName: string) {
const filePath = this.assetPath(fileName)
// Read the file content
const buffer = fs.readFileSync(filePath)
// Get file type
const getFileType = (fileName: string) => {
if (fileName.endsWith('.png')) return 'image/png'
if (fileName.endsWith('.webp')) return 'image/webp'
if (fileName.endsWith('.json')) return 'application/json'
return 'application/octet-stream'
}
const fileType = getFileType(fileName)
await this.page.evaluate(
async ({ buffer, fileName, fileType }) => {
const file = new File([new Uint8Array(buffer)], fileName, {
type: fileType
})
const dataTransfer = new DataTransfer()
dataTransfer.items.add(file)
const dropEvent = new DragEvent('drop', {
bubbles: true,
cancelable: true,
dataTransfer
})
Object.defineProperty(dropEvent, 'preventDefault', {
value: () => {},
writable: false
})
Object.defineProperty(dropEvent, 'stopPropagation', {
value: () => {},
writable: false
})
document.dispatchEvent(dropEvent)
},
{ buffer: [...new Uint8Array(buffer)], fileName, fileType }
)
await this.nextFrame()
}
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 }
}
async disconnectEdge() {
await this.dragAndDrop(this.clipTextEncodeNode1InputSlot, this.emptySpace)
}
async connectEdge() {
await this.dragAndDrop(
this.loadCheckpointNodeClipOutputSlot,
this.clipTextEncodeNode1InputSlot
)
}
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()
// TEMPORARY HACK: Multiple pans open the search menu, so cheat and keep it closed.
// TODO: Fix that (double-click at not-the-same-coordinations should not open the menu)
await this.page.keyboard.press('Escape')
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() {
await this.canvas.click({
position: { x: 10, y: 10 },
button: 'right'
})
await expect(this.contextMenu).toBeVisible()
}
async doubleClickCanvas() {
await this.page.mouse.dblclick(10, 10, { delay: 5 })
await this.nextFrame()
}
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 clickContextMenuItem(name: string): Promise<void> {
await this.page.getByRole('menuitem', { name }).click()
}
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()
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 convertAllNodesToGroupNode(groupNodeName: string) {
this.page.on('dialog', async (dialog) => {
await dialog.accept(groupNodeName)
})
await this.canvas.press('Control+a')
const node = await this.getFirstNodeRef()
await node!.clickContextMenuOption('Convert to Group Node')
await this.nextFrame()
}
async convertOffsetToCanvas(pos: [number, number]) {
return this.page.evaluate((pos) => {
return window['app'].canvas.ds.convertOffsetToCanvas(pos)
}, pos)
}
async getNodeRefById(id: NodeId) {
return new NodeReference(id, this)
}
async getNodeRefsByType(type: string): Promise<NodeReference[]> {
return Promise.all(
(
await this.page.evaluate((type) => {
return window['app'].graph.nodes
.filter((n) => n.type === type)
.map((n) => n.id)
}, type)
).map((id: NodeId) => this.getNodeRefById(id))
)
}
async getFirstNodeRef(): Promise<NodeReference | null> {
const id = await this.page.evaluate(() => {
return window['app'].graph.nodes[0]?.id
})
if (!id) return null
return this.getNodeRefById(id)
}
async moveMouseToEmptyArea() {
await this.page.mouse.move(10, 10)
}
}
export const comfyPageFixture = base.extend<{ comfyPage: ComfyPage }>({
comfyPage: async ({ page, request }, use) => {
const comfyPage = new ComfyPage(page, request)
const { parallelIndex } = comfyPageFixture.info()
const username = `playwright-test-${parallelIndex}`
const userId = await comfyPage.setupUser(username)
comfyPage.userIds[parallelIndex] = userId
await comfyPage.setupSettings({
'Comfy.UseNewMenu': 'Disabled',
// Hide canvas menu/info by default.
'Comfy.Graph.CanvasInfo': false,
'Comfy.Graph.CanvasMenu': false,
// Hide all badges by default.
'Comfy.NodeBadge.NodeIdBadgeMode': NodeBadgeMode.None,
'Comfy.NodeBadge.NodeSourceBadgeMode': NodeBadgeMode.None,
// Disable tooltips by default to avoid flakiness.
'Comfy.EnableTooltips': false,
'Comfy.userId': userId
})
await comfyPage.setup()
await use(comfyPage)
}
})
const makeMatcher = function <T>(
getValue: (node: NodeReference) => Promise<T> | T,
type: string
) {
return async function (
node: NodeReference,
options?: { timeout?: number; intervals?: number[] }
) {
const value = await getValue(node)
let assertion = expect(
value,
'Node is ' + (this.isNot ? '' : 'not ') + type
)
if (this.isNot) {
assertion = assertion.not
}
await expect(async () => {
assertion.toBeTruthy()
}).toPass({ timeout: 250, ...options })
return {
pass: !this.isNot,
message: () => 'Node is ' + (this.isNot ? 'not ' : '') + type
}
}
}
export const comfyExpect = expect.extend({
toBePinned: makeMatcher((n) => n.isPinned(), 'pinned'),
toBeBypassed: makeMatcher((n) => n.isBypassed(), 'bypassed'),
toBeCollapsed: makeMatcher((n) => n.isCollapsed(), 'collapsed')
})

View File

@@ -0,0 +1,79 @@
import { Locator, Page } from '@playwright/test'
export class ComfyNodeSearchFilterSelectionPanel {
constructor(public readonly page: Page) {}
async selectFilterType(filterType: string) {
await this.page
.locator(
`.filter-type-select .p-togglebutton-label:has-text("${filterType}")`
)
.click()
}
async selectFilterValue(filterValue: string) {
await this.page.locator('.filter-value-select .p-select-dropdown').click()
await this.page
.locator(
`.p-select-overlay .p-select-list .p-select-option-label:text-is("${filterValue}")`
)
.click()
}
async addFilter(filterValue: string, filterType: string) {
await this.selectFilterType(filterType)
await this.selectFilterValue(filterValue)
await this.page.locator('.p-button-label:has-text("Add")').click()
}
}
export class ComfyNodeSearchBox {
public readonly input: Locator
public readonly dropdown: Locator
public readonly filterSelectionPanel: ComfyNodeSearchFilterSelectionPanel
constructor(public readonly page: Page) {
this.input = page.locator(
'.comfy-vue-node-search-container input[type="text"]'
)
this.dropdown = page.locator(
'.comfy-vue-node-search-container .p-autocomplete-list'
)
this.filterSelectionPanel = new ComfyNodeSearchFilterSelectionPanel(page)
}
get filterButton() {
return this.page.locator('.comfy-vue-node-search-container ._filter-button')
}
async fillAndSelectFirstNode(
nodeName: string,
options?: { suggestionIndex: number }
) {
await this.input.waitFor({ state: 'visible' })
await this.input.fill(nodeName)
await this.dropdown.waitFor({ state: 'visible' })
// Wait for some time for the auto complete list to update.
// The auto complete list is debounced and may take some time to update.
await this.page.waitForTimeout(500)
await this.dropdown
.locator('li')
.nth(options?.suggestionIndex || 0)
.click()
}
async addFilter(filterValue: string, filterType: string) {
await this.filterButton.click()
await this.filterSelectionPanel.addFilter(filterValue, filterType)
}
get filterChips() {
return this.page.locator(
'.comfy-vue-node-search-container .p-autocomplete-chip-item'
)
}
async removeFilter(index: number) {
await this.filterChips.nth(index).locator('.p-chip-remove-icon').click()
}
}

View File

@@ -0,0 +1,40 @@
import { Page } from '@playwright/test'
export class SettingDialog {
constructor(public readonly page: Page) {}
async open() {
const button = this.page.locator('button.comfy-settings-btn:visible')
await button.click()
await this.page.waitForSelector('div.settings-container')
}
/**
* Set the value of a text setting
* @param id - The id of the setting
* @param value - The value to set
*/
async setStringSetting(id: string, value: string) {
const settingInputDiv = this.page.locator(
`div.settings-container div[id="${id}"]`
)
await settingInputDiv.locator('input').fill(value)
}
/**
* Toggle the value of a boolean setting
* @param id - The id of the setting
*/
async toggleBooleanSetting(id: string) {
const settingInputDiv = this.page.locator(
`div.settings-container div[id="${id}"]`
)
await settingInputDiv.locator('input').click()
}
async goToAboutPanel() {
const aboutButton = this.page.locator('li[aria-label="About"]')
await aboutButton.click()
await this.page.waitForSelector('div.about-container')
}
}

View File

@@ -0,0 +1,139 @@
import { Locator, Page } from '@playwright/test'
class SidebarTab {
constructor(
public readonly page: Page,
public readonly tabId: string
) {}
get tabButton() {
return this.page.locator(`.${this.tabId}-tab-button`)
}
get selectedTabButton() {
return this.page.locator(
`.${this.tabId}-tab-button.side-bar-button-selected`
)
}
async open() {
if (await this.selectedTabButton.isVisible()) {
return
}
await this.tabButton.click()
}
}
export class NodeLibrarySidebarTab extends SidebarTab {
constructor(public readonly page: Page) {
super(page, 'node-library')
}
get nodeLibrarySearchBoxInput() {
return this.page.locator('.node-lib-search-box input[type="text"]')
}
get nodeLibraryTree() {
return this.page.locator('.node-lib-tree-explorer')
}
get nodePreview() {
return this.page.locator('.node-lib-node-preview')
}
get tabContainer() {
return this.page.locator('.sidebar-content-container')
}
get newFolderButton() {
return this.tabContainer.locator('.new-folder-button')
}
async open() {
await super.open()
await this.nodeLibraryTree.waitFor({ state: 'visible' })
}
async close() {
if (!this.tabButton.isVisible()) {
return
}
await this.tabButton.click()
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}")))`
}
getNode(nodeName: string) {
return this.page.locator(this.nodeSelector(nodeName))
}
}
export class WorkflowsSidebarTab extends SidebarTab {
constructor(public readonly page: Page) {
super(page, 'workflows')
}
get browseGalleryButton() {
return this.page.locator('.browse-templates-button')
}
get newBlankWorkflowButton() {
return this.page.locator('.new-blank-workflow-button')
}
get openWorkflowButton() {
return this.page.locator('.open-workflow-button')
}
async getOpenedWorkflowNames() {
return await this.page
.locator('.comfyui-workflows-open .node-label')
.allInnerTexts()
}
async getTopLevelSavedWorkflowNames() {
return await this.page
.locator('.comfyui-workflows-browse .node-label')
.allInnerTexts()
}
async switchToWorkflow(workflowName: string) {
const workflowLocator = this.getOpenedItem(workflowName)
await workflowLocator.click()
await this.page.waitForTimeout(300)
}
getOpenedItem(name: string) {
return this.page.locator('.comfyui-workflows-open .node-label', {
hasText: name
})
}
getPersistedItem(name: string) {
return this.page.locator('.comfyui-workflows-browse .node-label', {
hasText: name
})
}
async renameWorkflow(locator: Locator, newName: string) {
await locator.click({ button: 'right' })
await this.page
.locator('.p-contextmenu-item-content', { hasText: 'Rename' })
.click()
await this.page.keyboard.type(newName)
await this.page.keyboard.press('Enter')
await this.page.waitForTimeout(300)
}
}

View File

@@ -0,0 +1,85 @@
import { Locator, Page } from '@playwright/test'
export class Topbar {
constructor(public readonly page: Page) {}
async getTabNames(): Promise<string[]> {
return await this.page
.locator('.workflow-tabs .workflow-label')
.allInnerTexts()
}
async openSubmenuMobile() {
await this.page.locator('.p-menubar-mobile .p-menubar-button').click()
}
getMenuItem(itemLabel: string): Locator {
return this.page.locator(`.p-menubar-item-label:text-is("${itemLabel}")`)
}
getWorkflowTab(tabName: string): Locator {
return this.page
.locator(`.workflow-tabs .workflow-label:has-text("${tabName}")`)
.locator('..')
}
async closeWorkflowTab(tabName: string) {
const tab = this.getWorkflowTab(tabName)
await tab.locator('.close-button').click({ force: true })
}
getSaveDialog(): Locator {
return this.page.locator('.p-dialog-content input')
}
saveWorkflow(workflowName: string): Promise<void> {
return this._saveWorkflow(workflowName, 'Save')
}
saveWorkflowAs(workflowName: string): Promise<void> {
return this._saveWorkflow(workflowName, 'Save As')
}
async _saveWorkflow(workflowName: string, command: 'Save' | 'Save As') {
await this.triggerTopbarCommand(['Workflow', command])
await this.getSaveDialog().fill(workflowName)
await this.page.keyboard.press('Enter')
// Wait for workflow service to finish saving
await this.page.waitForFunction(
() => !window['app'].extensionManager.workflow.isBusy,
undefined,
{ timeout: 3000 }
)
// Wait for the dialog to close.
await this.getSaveDialog().waitFor({ state: 'hidden', timeout: 500 })
}
async triggerTopbarCommand(path: string[]) {
if (path.length < 2) {
throw new Error('Path is too short')
}
const tabName = path[0]
const topLevelMenu = this.page.locator(
`.top-menubar .p-menubar-item-label:text-is("${tabName}")`
)
await topLevelMenu.waitFor({ state: 'visible' })
await topLevelMenu.click()
for (let i = 1; i < path.length; i++) {
const commandName = path[i]
const menuItem = this.page
.locator(
`.top-menubar .p-menubar-submenu .p-menubar-item:has-text("${commandName}")`
)
.first()
await menuItem.waitFor({ state: 'visible' })
await menuItem.hover()
if (i === path.length - 1) {
await menuItem.click()
}
}
}
}

View File

@@ -0,0 +1,9 @@
export interface Position {
x: number
y: number
}
export interface Size {
width: number
height: number
}

View File

@@ -0,0 +1,257 @@
import { ManageGroupNode } from '../../helpers/manageGroupNode'
import type { NodeId } from '../../../src/types/comfyWorkflow'
import type { Page } from '@playwright/test'
import type { ComfyPage } from '../ComfyPage'
import type { Position, Size } from '../types'
export class NodeSlotReference {
constructor(
readonly type: 'input' | 'output',
readonly index: number,
readonly node: NodeReference
) {}
async getPosition() {
const pos: [number, number] = await this.node.comfyPage.page.evaluate(
([type, id, index]) => {
const node = window['app'].graph.getNodeById(id)
if (!node) throw new Error(`Node ${id} not found.`)
return window['app'].canvas.ds.convertOffsetToCanvas(
node.getConnectionPos(type === 'input', index)
)
},
[this.type, this.node.id, this.index] as const
)
return {
x: pos[0],
y: pos[1]
}
}
async getLinkCount() {
return await this.node.comfyPage.page.evaluate(
([type, id, index]) => {
const node = window['app'].graph.getNodeById(id)
if (!node) throw new Error(`Node ${id} not found.`)
if (type === 'input') {
return node.inputs[index].link == null ? 0 : 1
}
return node.outputs[index].links?.length ?? 0
},
[this.type, this.node.id, this.index] as const
)
}
async removeLinks() {
await this.node.comfyPage.page.evaluate(
([type, id, index]) => {
const node = window['app'].graph.getNodeById(id)
if (!node) throw new Error(`Node ${id} not found.`)
if (type === 'input') {
node.disconnectInput(index)
} else {
node.disconnectOutput(index)
}
},
[this.type, this.node.id, this.index] as const
)
}
}
export class NodeWidgetReference {
constructor(
readonly index: number,
readonly node: NodeReference
) {}
async getPosition(): 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 [x, y, w, h] = node.getBounding()
return window['app'].canvas.ds.convertOffsetToCanvas([
x + w / 2,
y + window['LiteGraph']['NODE_TITLE_HEIGHT'] + widget.last_y + 1
])
},
[this.node.id, this.index] as const
)
return {
x: pos[0],
y: pos[1]
}
}
}
export class NodeReference {
constructor(
readonly id: NodeId,
readonly comfyPage: ComfyPage
) {}
async exists(): Promise<boolean> {
return await this.comfyPage.page.evaluate((id) => {
const node = window['app'].graph.getNodeById(id)
return !!node
}, this.id)
}
getType(): Promise<string> {
return this.getProperty('type')
}
async getPosition(): Promise<Position> {
const pos = await this.comfyPage.convertOffsetToCanvas(
await this.getProperty<[number, number]>('pos')
)
return {
x: pos[0],
y: pos[1]
}
}
async getBounding(): Promise<Position & Size> {
const [x, y, width, height]: [number, number, number, number] =
await this.comfyPage.page.evaluate((id) => {
const node = window['app'].graph.getNodeById(id)
if (!node) throw new Error('Node not found')
return node.getBounding()
}, this.id)
return {
x,
y,
width,
height
}
}
async getSize(): Promise<Size> {
const size = await this.getProperty<[number, number]>('size')
return {
width: size[0],
height: size[1]
}
}
async getFlags(): Promise<{ collapsed?: boolean; pinned?: boolean }> {
return await this.getProperty('flags')
}
async isPinned() {
return !!(await this.getFlags()).pinned
}
async isCollapsed() {
return !!(await this.getFlags()).collapsed
}
async isBypassed() {
return (await this.getProperty<number | null | undefined>('mode')) === 4
}
async getProperty<T>(prop: string): Promise<T> {
return await this.comfyPage.page.evaluate(
([id, prop]) => {
const node = window['app'].graph.getNodeById(id)
if (!node) throw new Error('Node not found')
return node[prop]
},
[this.id, prop] as const
)
}
async getOutput(index: number) {
return new NodeSlotReference('output', index, this)
}
async getInput(index: number) {
return new NodeSlotReference('input', index, this)
}
async getWidget(index: number) {
return new NodeWidgetReference(index, this)
}
async click(
position: 'title' | 'collapse',
options?: Parameters<Page['click']>[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 }
break
case 'collapse':
clickPos = { x: nodePos.x + 5, y: nodePos.y - 10 }
break
default:
throw new Error(`Invalid click position ${position}`)
}
const moveMouseToEmptyArea = options?.moveMouseToEmptyArea
if (options) {
delete options.moveMouseToEmptyArea
}
await this.comfyPage.canvas.click({
...options,
position: clickPos
})
await this.comfyPage.nextFrame()
if (moveMouseToEmptyArea) {
await this.comfyPage.moveMouseToEmptyArea()
}
}
async copy() {
await this.click('title')
await this.comfyPage.ctrlC()
await this.comfyPage.nextFrame()
}
async connectWidget(
originSlotIndex: number,
targetNode: NodeReference,
targetWidgetIndex: number
) {
const originSlot = await this.getOutput(originSlotIndex)
const targetWidget = await targetNode.getWidget(targetWidgetIndex)
await this.comfyPage.dragAndDrop(
await originSlot.getPosition(),
await targetWidget.getPosition()
)
return originSlot
}
async connectOutput(
originSlotIndex: number,
targetNode: NodeReference,
targetSlotIndex: number
) {
const originSlot = await this.getOutput(originSlotIndex)
const targetSlot = await targetNode.getInput(targetSlotIndex)
await this.comfyPage.dragAndDrop(
await originSlot.getPosition(),
await targetSlot.getPosition()
)
return originSlot
}
async getContextMenuOptionNames() {
await this.click('title', { button: 'right' })
const ctx = this.comfyPage.page.locator('.litecontextmenu')
return await ctx.locator('.litemenu-entry').allInnerTexts()
}
async clickContextMenuOption(optionText: string) {
await this.click('title', { button: 'right' })
const ctx = this.comfyPage.page.locator('.litecontextmenu')
await ctx.getByText(optionText).click()
}
async convertToGroupNode(groupNodeName: string = 'GroupNode') {
this.comfyPage.page.once('dialog', async (dialog) => {
await dialog.accept(groupNodeName)
})
await this.clickContextMenuOption('Convert to Group Node')
await this.comfyPage.nextFrame()
const nodes = await this.comfyPage.getNodeRefsByType(
`workflow>${groupNodeName}`
)
if (nodes.length !== 1) {
throw new Error(`Did not find single group node (found=${nodes.length})`)
}
return nodes[0]
}
async manageGroupNode() {
await this.clickContextMenuOption('Manage Group Node')
await this.comfyPage.nextFrame()
return new ManageGroupNode(
this.comfyPage.page,
this.comfyPage.page.locator('.comfy-group-manage')
)
}
}

View File

@@ -0,0 +1,51 @@
import { test as base } from '@playwright/test'
export const webSocketFixture = base.extend<{
ws: { trigger(data: any, url?: string): Promise<void> }
}>({
ws: [
async ({ page }, use) => {
// Each time a page loads, to catch navigations
page.on('load', async () => {
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<string, WebSocket> = ((window as any).__ws__ = {})
window.WebSocket = class extends window.WebSocket {
constructor() {
// @ts-expect-error
super(...arguments)
store[this.url] = this
}
}
})
})
await use({
async trigger(data, url) {
// Trigger a websocket event on the page
await page.evaluate(
function ([data, url]) {
if (!url) {
// If no URL specified, use page URL
const u = new URL(window.location.toString())
u.protocol = 'ws:'
u.pathname = '/'
url = u.toString() + 'ws'
}
const ws: WebSocket = (window as any).__ws__[url]
ws.dispatchEvent(
new MessageEvent('message', {
data
})
)
},
[JSON.stringify(data), url]
)
}
})
},
// We need this to run automatically as the first thing so it adds handlers as soon as the page loads
{ auto: true }
]
})

View File

@@ -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'
)
}
}
}

View File

@@ -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'])
}
}

View File

@@ -0,0 +1,38 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Graph Canvas Menu', () => {
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)
})
test('Can toggle link visibility', async ({ comfyPage }) => {
// Note: `Comfy.Graph.CanvasMenu` is disabled in comfyPage setup.
// so no cleanup is needed.
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true)
const button = comfyPage.page.getByTestId('toggle-link-visibility-button')
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
})
expect(await comfyPage.getSetting('Comfy.LinkRenderMode')).toBe(
hiddenLinkRenderMode
)
await button.click()
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'canvas-with-visible-links.png'
)
expect(await comfyPage.getSetting('Comfy.LinkRenderMode')).not.toBe(
hiddenLinkRenderMode
)
})
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View File

@@ -0,0 +1,259 @@
import { expect } from '@playwright/test'
import { ComfyPage, comfyPageFixture as test } from './fixtures/ComfyPage'
import type { NodeReference } from './fixtures/utils/litegraphUtils'
test.describe('Group Node', () => {
test.describe('Node library sidebar', () => {
const groupNodeName = 'DefautWorkflowGroupNode'
const groupNodeCategory = 'group nodes>workflow'
const groupNodeBookmarkName = `workflow>${groupNodeName}`
let libraryTab
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
libraryTab = comfyPage.menu.nodeLibraryTab
await comfyPage.convertAllNodesToGroupNode(groupNodeName)
await libraryTab.open()
})
test('Is added to node library sidebar', async ({ comfyPage }) => {
expect(await libraryTab.getFolder('group nodes').count()).toBe(1)
})
test('Can be added to canvas using node library sidebar', async ({
comfyPage
}) => {
const initialNodeCount = await comfyPage.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)
})
test('Can be bookmarked and unbookmarked', async ({ comfyPage }) => {
await libraryTab.getFolder(groupNodeCategory).click()
await libraryTab
.getNode(groupNodeName)
.locator('.bookmark-button')
.click()
// Verify the node is added to the bookmarks tab
expect(
await comfyPage.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)
// Unbookmark the node
await libraryTab
.getNode(groupNodeName)
.locator('.bookmark-button')
.first()
.click()
// Verify the node is removed from the bookmarks tab
expect(
await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
).toHaveLength(0)
})
test('Displays preview on bookmark hover', async ({ comfyPage }) => {
await libraryTab.getFolder(groupNodeCategory).click()
await libraryTab
.getNode(groupNodeName)
.locator('.bookmark-button')
.click()
await comfyPage.page.hover('.p-tree-node-label.tree-explorer-node-label')
expect(await comfyPage.page.isVisible('.node-lib-node-preview')).toBe(
true
)
await libraryTab
.getNode(groupNodeName)
.locator('.bookmark-button')
.first()
.click()
})
})
test('Can be added to canvas using search', async ({ comfyPage }) => {
const groupNodeName = 'DefautWorkflowGroupNode'
await comfyPage.convertAllNodesToGroupNode(groupNodeName)
await comfyPage.doubleClickCanvas()
await comfyPage.nextFrame()
await comfyPage.searchBox.fillAndSelectFirstNode(groupNodeName)
await expect(comfyPage.canvas).toHaveScreenshot(
'group-node-copy-added-from-search.png'
)
})
test('Displays tooltip on title hover', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.EnableTooltips', true)
await comfyPage.convertAllNodesToGroupNode('Group Node')
await comfyPage.page.mouse.move(47, 173)
const tooltipTimeout = 500
await comfyPage.page.waitForTimeout(tooltipTimeout + 16)
await expect(comfyPage.page.locator('.node-tooltip')).toBeVisible()
})
test('Reconnects inputs after configuration changed via manage dialog save', async ({
comfyPage
}) => {
const expectSingleNode = async (type: string) => {
const nodes = await comfyPage.getNodeRefsByType(type)
expect(nodes).toHaveLength(1)
return nodes[0]
}
const latent = await expectSingleNode('EmptyLatentImage')
const sampler = await expectSingleNode('KSampler')
// Remove existing link
const samplerInput = await sampler.getInput(0)
await samplerInput.removeLinks()
// Group latent + sampler
await latent.click('title', {
modifiers: ['Shift']
})
await sampler.click('title', {
modifiers: ['Shift']
})
const groupNode = await sampler.convertToGroupNode()
// Connect node to group
const ckpt = await expectSingleNode('CheckpointLoaderSimple')
const input = await ckpt.connectOutput(0, groupNode, 0)
expect(await input.getLinkCount()).toBe(1)
// Modify the group node via manage dialog
const manage = await groupNode.manageGroupNode()
await manage.selectNode('KSampler')
await manage.changeTab('Inputs')
await manage.setLabel('model', 'test')
await manage.save()
await manage.close()
// Ensure the link is still present
expect(await input.getLinkCount()).toBe(1)
})
test('Loads from a workflow using the legacy path separator ("/")', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('legacy_group_node')
expect(await comfyPage.getGraphNodesCount()).toBe(1)
await expect(
comfyPage.page.locator('.comfy-missing-nodes')
).not.toBeVisible()
})
test.describe('Copy and paste', () => {
let groupNode: NodeReference | null
const WORKFLOW_NAME = 'group_node_v1.3.3'
const GROUP_NODE_CATEGORY = 'group nodes>workflow'
const GROUP_NODE_PREFIX = 'workflow>'
const GROUP_NODE_NAME = 'group_node' // Node name in given workflow
const GROUP_NODE_TYPE = `${GROUP_NODE_PREFIX}${GROUP_NODE_NAME}`
const isRegisteredLitegraph = async (comfyPage: ComfyPage) => {
return await comfyPage.page.evaluate((nodeType: string) => {
return !!window['LiteGraph'].registered_node_types[nodeType]
}, GROUP_NODE_TYPE)
}
const isRegisteredNodeDefStore = async (comfyPage: ComfyPage) => {
const groupNodesFolderCt = await comfyPage.menu.nodeLibraryTab
.getFolder(GROUP_NODE_CATEGORY)
.count()
return groupNodesFolderCt === 1
}
const verifyNodeLoaded = async (
comfyPage: ComfyPage,
expectedCount: number
) => {
expect(await comfyPage.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)
await comfyPage.menu.nodeLibraryTab.open()
groupNode = await comfyPage.getFirstNodeRef()
if (!groupNode)
throw new Error(`Group node not found in workflow ${WORKFLOW_NAME}`)
await groupNode.copy()
})
test('Copies and pastes group node within the same workflow', async ({
comfyPage
}) => {
await comfyPage.ctrlV()
await verifyNodeLoaded(comfyPage, 2)
})
test('Copies and pastes group node after clearing workflow', async ({
comfyPage
}) => {
await comfyPage.menu.topbar.triggerTopbarCommand([
'Edit',
'Clear Workflow'
])
await comfyPage.ctrlV()
await verifyNodeLoaded(comfyPage, 1)
})
test('Copies and pastes group node into a newly created blank workflow', async ({
comfyPage
}) => {
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
await comfyPage.ctrlV()
await verifyNodeLoaded(comfyPage, 1)
})
test('Copies and pastes group node across different workflows', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('default')
await comfyPage.ctrlV()
await verifyNodeLoaded(comfyPage, 1)
})
test('Serializes group node after copy and paste across workflows', async ({
comfyPage
}) => {
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
await comfyPage.ctrlV()
const currentGraphState = await comfyPage.page.evaluate(() =>
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),
currentGraphState
)
await comfyPage.nextFrame()
await verifyNodeLoaded(comfyPage, 1)
})
})
})
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)
})
})
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

View File

@@ -0,0 +1,49 @@
import type { Page, Locator } from '@playwright/test'
import type { AutoQueueMode } from '../../src/stores/queueStore'
export class ComfyActionbar {
public readonly root: Locator
public readonly queueButton: ComfyQueueButton
constructor(public readonly page: Page) {
this.root = page.locator('.actionbar')
this.queueButton = new ComfyQueueButton(this)
}
async isDocked() {
const className = await this.root.getAttribute('class')
return className?.includes('is-docked') ?? false
}
}
class ComfyQueueButton {
public readonly root: Locator
public readonly primaryButton: Locator
public readonly dropdownButton: Locator
constructor(public readonly actionbar: ComfyActionbar) {
this.root = actionbar.root.getByTestId('queue-button')
this.primaryButton = this.root.locator('.p-splitbutton-button')
this.dropdownButton = this.root.locator('.p-splitbutton-dropdown')
}
public async toggleOptions() {
await this.dropdownButton.click()
return new ComfyQueueButtonOptions(this.actionbar.page)
}
}
class ComfyQueueButtonOptions {
constructor(public readonly page: Page) {}
public async setMode(mode: AutoQueueMode) {
await this.page.evaluate((mode) => {
window['app'].extensionManager.queueSettings.mode = mode
}, mode)
}
public async getMode() {
return await this.page.evaluate(() => {
return window['app'].extensionManager.queueSettings.mode
})
}
}

View File

@@ -0,0 +1,37 @@
import { Locator, Page } from '@playwright/test'
export class ManageGroupNode {
footer: Locator
constructor(
readonly page: Page,
readonly root: Locator
) {
this.footer = root.locator('footer')
}
async setLabel(name: string, label: string) {
const active = this.root.locator('.comfy-group-manage-node-page.active')
const input = active.getByPlaceholder(name)
await input.fill(label)
}
async save() {
await this.footer.getByText('Save').click()
}
async close() {
await this.footer.getByText('Close').click()
}
async selectNode(name: string) {
const list = this.root.locator('.comfy-group-manage-list-items')
const item = list.getByText(name)
await item.click()
}
async changeTab(name: 'Inputs' | 'Widgets' | 'Outputs') {
const header = this.root.locator('.comfy-group-manage-node header')
const tab = header.getByText(name)
await tab.click()
}
}

View File

@@ -0,0 +1,12 @@
import { Locator, Page } from '@playwright/test'
export class ComfyTemplates {
readonly content: Locator
constructor(readonly page: Page) {
this.content = page.getByTestId('template-workflows-content')
}
async loadTemplate(id: string) {
await this.content.getByTestId(`template-workflow-${id}`).click()
}
}

View File

@@ -1,5 +1,26 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from './ComfyPage'
import { comfyPageFixture as test } from './fixtures/ComfyPage'
test.describe('Item Interaction', () => {
test('Can select/delete all items', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('mixed_graph_items')
await comfyPage.canvas.press('Control+a')
await expect(comfyPage.canvas).toHaveScreenshot('selected-all.png')
await comfyPage.canvas.press('Delete')
await expect(comfyPage.canvas).toHaveScreenshot('deleted-all.png')
})
test('Can pin/unpin items with keyboard shortcut', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('mixed_graph_items')
await comfyPage.canvas.press('Control+a')
await comfyPage.canvas.press('KeyP')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('pinned-all.png')
await comfyPage.canvas.press('KeyP')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('unpinned-all.png')
})
})
test.describe('Node Interaction', () => {
test('Can enter prompt', async ({ comfyPage }) => {
@@ -11,32 +32,128 @@ test.describe('Node Interaction', () => {
await expect(textBox).toHaveValue('Hello World 2')
})
test('Can highlight selected', async ({ comfyPage }) => {
await expect(comfyPage.canvas).toHaveScreenshot('default.png')
await comfyPage.clickTextEncodeNode1()
await expect(comfyPage.canvas).toHaveScreenshot('selected-node1.png')
await comfyPage.clickTextEncodeNode2()
await expect(comfyPage.canvas).toHaveScreenshot('selected-node2.png')
test.describe('Node Selection', () => {
const multiSelectModifiers = ['Control', 'Shift', 'Meta'] as const
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 }) => {
await expect(comfyPage.canvas).toHaveScreenshot('default.png')
await comfyPage.clickTextEncodeNode1()
await expect(comfyPage.canvas).toHaveScreenshot('selected-node1.png')
await comfyPage.clickTextEncodeNode2()
await expect(comfyPage.canvas).toHaveScreenshot('selected-node2.png')
})
test('Can drag-select nodes with Meta (mac)', async ({ comfyPage }) => {
const clipNodes = await comfyPage.getNodeRefsByType('CLIPTextEncode')
const clipNode1Pos = await clipNodes[0].getPosition()
const clipNode2Pos = await clipNodes[1].getPosition()
const offset = 64
await comfyPage.page.keyboard.down('Meta')
await comfyPage.dragAndDrop(
{
x: Math.min(clipNode1Pos.x, clipNode2Pos.x) - offset,
y: Math.min(clipNode1Pos.y, clipNode2Pos.y) - offset
},
{
x: Math.max(clipNode1Pos.x, clipNode2Pos.x) + offset,
y: Math.max(clipNode1Pos.y, clipNode2Pos.y) + offset
}
)
await comfyPage.page.keyboard.up('Meta')
expect(await comfyPage.getSelectedGraphNodesCount()).toBe(
clipNodes.length
)
})
})
// Flaky. See https://github.com/comfyanonymous/ComfyUI/issues/3866
test.skip('Can drag node', async ({ comfyPage }) => {
test('Can drag node', async ({ comfyPage }) => {
await comfyPage.dragNode2()
await expect(comfyPage.canvas).toHaveScreenshot('dragged-node1.png')
})
test('Can disconnect/connect edge', async ({ comfyPage }) => {
await comfyPage.disconnectEdge()
// Close search menu popped up.
await comfyPage.page.keyboard.press('Escape')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'disconnected-edge-with-menu.png'
)
await comfyPage.connectEdge()
// Litegraph renders edge with a slight offset.
await expect(comfyPage.canvas).toHaveScreenshot('default.png', {
maxDiffPixels: 50
test.describe('Edge Interaction', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.LinkRelease.Action', 'no action')
await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'no action')
})
test('Can disconnect/connect edge', async ({ comfyPage }) => {
await comfyPage.disconnectEdge()
await expect(comfyPage.canvas).toHaveScreenshot('disconnected-edge.png')
await comfyPage.connectEdge()
// Move mouse to empty area to avoid slot highlight.
await comfyPage.moveMouseToEmptyArea()
// Litegraph renders edge with a slight offset.
await expect(comfyPage.canvas).toHaveScreenshot('default.png', {
maxDiffPixels: 50
})
})
// Chromium 2x cannot move link.
// See https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/10876381315/job/30176211513
test.skip('Can move link', async ({ comfyPage }) => {
await comfyPage.dragAndDrop(
comfyPage.clipTextEncodeNode1InputSlot,
comfyPage.emptySpace
)
await expect(comfyPage.canvas).toHaveScreenshot('disconnected-edge.png')
await comfyPage.dragAndDrop(
comfyPage.clipTextEncodeNode2InputSlot,
comfyPage.clipTextEncodeNode1InputSlot
)
await expect(comfyPage.canvas).toHaveScreenshot('moved-link.png')
})
// Copy link is not working on CI at all
// Chromium 2x recognize it as dragging canvas.
// Chromium triggers search box after link release. The link is indeed copied.
// See https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/10876381315/job/30176211513
test.skip('Can copy link by shift-drag existing link', async ({
comfyPage
}) => {
await comfyPage.dragAndDrop(
comfyPage.clipTextEncodeNode1InputSlot,
comfyPage.emptySpace
)
await expect(comfyPage.canvas).toHaveScreenshot('disconnected-edge.png')
await comfyPage.page.keyboard.down('Shift')
await comfyPage.dragAndDrop(
comfyPage.clipTextEncodeNode2InputLinkPath,
comfyPage.clipTextEncodeNode1InputSlot
)
await comfyPage.page.keyboard.up('Shift')
await expect(comfyPage.canvas).toHaveScreenshot('copied-link.png')
})
test('Auto snap&highlight when dragging link over node', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Node.AutoSnapLinkToSlot', true)
await comfyPage.setSetting('Comfy.Node.SnapHighlightsNode', true)
await comfyPage.page.mouse.move(
comfyPage.clipTextEncodeNode1InputSlot.x,
comfyPage.clipTextEncodeNode1InputSlot.y
)
await comfyPage.page.mouse.down()
await comfyPage.page.mouse.move(
comfyPage.clipTextEncodeNode2InputSlot.x,
comfyPage.clipTextEncodeNode2InputSlot.y
)
await expect(comfyPage.canvas).toHaveScreenshot('snapped-highlighted.png')
})
})
@@ -114,12 +231,15 @@ test.describe('Node Interaction', () => {
)
})
test('Can close prompt dialog with canvas click', async ({ comfyPage }) => {
test('Can close prompt dialog with canvas click (number widget)', async ({
comfyPage
}) => {
const numberWidgetPos = {
x: 724,
y: 645
}
await comfyPage.canvas.click({
position: {
x: 724,
y: 645
}
position: numberWidgetPos
})
await expect(comfyPage.canvas).toHaveScreenshot('prompt-dialog-opened.png')
// Wait for 1s so that it does not trigger the search box by double click.
@@ -132,6 +252,115 @@ test.describe('Node Interaction', () => {
})
await expect(comfyPage.canvas).toHaveScreenshot('prompt-dialog-closed.png')
})
test('Can close prompt dialog with canvas click (text widget)', async ({
comfyPage
}) => {
const textWidgetPos = {
x: 167,
y: 143
}
await comfyPage.loadWorkflow('single_save_image_node')
await comfyPage.canvas.click({
position: textWidgetPos
})
await expect(comfyPage.canvas).toHaveScreenshot(
'prompt-dialog-opened-text.png'
)
await comfyPage.page.waitForTimeout(1000)
await comfyPage.canvas.click({
position: {
x: 10,
y: 10
}
})
await expect(comfyPage.canvas).toHaveScreenshot(
'prompt-dialog-closed-text.png'
)
})
test('Can double click node title to edit', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('single_ksampler')
await comfyPage.canvas.dblclick({
position: {
x: 50,
y: 10
},
delay: 5
})
await comfyPage.page.keyboard.type('Hello World')
await comfyPage.page.keyboard.press('Enter')
await expect(comfyPage.canvas).toHaveScreenshot('node-title-edited.png')
})
test('Double click node body does not trigger edit', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('single_ksampler')
await comfyPage.canvas.dblclick({
position: {
x: 50,
y: 50
},
delay: 5
})
expect(await comfyPage.page.locator('.node-title-editor').count()).toBe(0)
})
test('Can group selected nodes', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.GroupSelectedNodes.Padding', 10)
await comfyPage.select2Nodes()
await comfyPage.page.keyboard.down('Control')
await comfyPage.page.keyboard.press('KeyG')
await comfyPage.page.keyboard.up('Control')
await comfyPage.nextFrame()
// Confirm group title
await comfyPage.page.keyboard.press('Enter')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('group-selected-nodes.png')
})
// Somehow this test fails on GitHub Actions. It works locally.
// https://github.com/Comfy-Org/ComfyUI_frontend/pull/736
test.skip('Can pin/unpin nodes with keyboard shortcut', async ({
comfyPage
}) => {
await comfyPage.select2Nodes()
await comfyPage.canvas.press('KeyP')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('nodes-pinned.png')
await comfyPage.canvas.press('KeyP')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('nodes-unpinned.png')
})
test('Can bypass/unbypass nodes with keyboard shortcut', async ({
comfyPage
}) => {
await comfyPage.select2Nodes()
await comfyPage.canvas.press('Control+b')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('nodes-bypassed.png')
await comfyPage.canvas.press('Control+b')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('nodes-unbypassed.png')
})
})
test.describe('Group Interaction', () => {
test('Can double click group title to edit', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('single_group')
await comfyPage.canvas.dblclick({
position: {
x: 50,
y: 10
},
delay: 5
})
await comfyPage.page.keyboard.type('Hello World')
await comfyPage.page.keyboard.press('Enter')
await expect(comfyPage.canvas).toHaveScreenshot('group-title-edited.png')
})
})
test.describe('Canvas Interaction', () => {
@@ -201,6 +430,52 @@ test.describe('Canvas Interaction', () => {
await expect(comfyPage.canvas).toHaveScreenshot('panned.png')
})
test('Cursor style changes when panning', async ({ comfyPage }) => {
const getCursorStyle = async () => {
return await comfyPage.page.evaluate(() => {
return (
document.getElementById('graph-canvas')!.style.cursor || 'default'
)
})
}
await comfyPage.page.mouse.move(10, 10)
expect(await getCursorStyle()).toBe('default')
await comfyPage.page.mouse.down()
expect(await getCursorStyle()).toBe('grabbing')
await comfyPage.page.mouse.up()
expect(await getCursorStyle()).toBe('default')
await comfyPage.page.keyboard.down('Space')
expect(await getCursorStyle()).toBe('grab')
await comfyPage.page.mouse.down()
expect(await getCursorStyle()).toBe('grabbing')
await comfyPage.page.mouse.up()
expect(await getCursorStyle()).toBe('grab')
await comfyPage.page.keyboard.up('Space')
expect(await getCursorStyle()).toBe('default')
})
test('Can pan when dragging a link', async ({ comfyPage }) => {
const posSlot1 = comfyPage.clipTextEncodeNode1InputSlot
await comfyPage.page.mouse.move(posSlot1.x, posSlot1.y)
await comfyPage.page.mouse.down()
const posEmpty = comfyPage.emptySpace
await comfyPage.page.mouse.move(posEmpty.x, posEmpty.y)
await expect(comfyPage.canvas).toHaveScreenshot('dragging-link1.png')
await comfyPage.page.keyboard.down('Space')
await comfyPage.page.mouse.move(posEmpty.x + 100, posEmpty.y + 100)
// Canvas should be panned.
await expect(comfyPage.canvas).toHaveScreenshot(
'panning-when-dragging-link.png'
)
await comfyPage.page.keyboard.up('Space')
await comfyPage.page.mouse.move(posEmpty.x, posEmpty.y)
// Should be back to dragging link mode when space is released.
await expect(comfyPage.canvas).toHaveScreenshot('dragging-link2.png')
await comfyPage.page.mouse.up()
})
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 })
@@ -216,4 +491,66 @@ test.describe('Canvas Interaction', () => {
await comfyPage.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 expect(comfyPage.canvas).toHaveScreenshot('panned-touch.png')
})
})
test.describe('Widget Interaction', () => {
test('Undo text input', async ({ comfyPage }) => {
const textBox = comfyPage.widgetTextBox
await textBox.click()
await textBox.fill('')
await expect(textBox).toHaveValue('')
await textBox.fill('Hello World')
await expect(textBox).toHaveValue('Hello World')
await comfyPage.ctrlZ(null)
await expect(textBox).toHaveValue('')
})
test('Undo attention edit', async ({ comfyPage }) => {
await comfyPage.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 expect(textBox).toHaveValue('(1girl:1.05)')
await comfyPage.ctrlZ(null)
await expect(textBox).toHaveValue('1girl')
})
})
test.describe('Load workflow', () => {
test('Can load workflow with string node id', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('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('string_input')
await expect(comfyPage.canvas).toHaveScreenshot('string_input.png')
})
})
test.describe('Load duplicate workflow', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
})
test('A workflow can be loaded multiple times in a row', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('single_ksampler')
await comfyPage.menu.workflowsTab.open()
await comfyPage.menu.workflowsTab.newBlankWorkflowButton.click()
await comfyPage.loadWorkflow('single_ksampler')
expect(await comfyPage.getGraphNodesCount()).toBe(1)
})
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

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