Compare commits

...

222 Commits

Author SHA1 Message Date
Austin Mroz
72535eb88d Type cleanup, annotation as function, fix import
Naming now uniformly refers to the widget as an Annotated Number and the
widget is now implemented as a proper typescript class

annotatedNumber widgets can optionally have an annotation function set.
This allows for extensions to set annotations on ranges of values.

Fixed the app import incorrectly having an extension
2024-09-26 15:17:06 -05:00
Austin Mroz
330633355a Type correctness, widget conversion
Type checking is satisfied aside from the forced widget type

Widgets defined in python code as INT or FLOAT are now converted if the
options includes relevant keys. This grants functionality similar to the
multiline option on strings

Colors now correctly follow themes
2024-09-26 03:25:32 -05:00
Austin Mroz
9c3cf86381 POC for an annotated number widget 2024-09-26 03:25:28 -05: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
340 changed files with 17408 additions and 4491 deletions

View File

@@ -16,4 +16,4 @@ DEPLOY_COMFYUI_DIR=/home/ComfyUI/web
EXAMPLE_REPO_PATH=tests-ui/ComfyUI_examples
# Whether to enable minification of the frontend code.
ENABLE_MINIFY=true
ENABLE_MINIFY=true

View File

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

View File

@@ -2,7 +2,7 @@ name: Prettier Check
on:
pull_request:
branches: [ main, master ]
branches: [ main, master, dev* ]
jobs:
prettier:

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

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

@@ -90,6 +90,13 @@ https://github.com/user-attachments/assets/4bbca3ee-318f-4cf0-be32-a5a5541066cf
### QoL changes
<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>
@@ -130,6 +137,37 @@ https://github.com/user-attachments/assets/c142c43f-2fe9-4030-8196-b3bfd4c6977d
</details>
### Node developers API
<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.extensionManager.menu.registerTopbarCommands(["ext", "ext2"], [{id:"foo", label: "foo", function: () => alert(1)}])
```
![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>
@@ -155,22 +193,6 @@ 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
@@ -184,18 +206,17 @@ We will support custom icons later.
- 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>.
- Replace the existing ComfyUI front-end implementation. <https://github.com/comfyanonymous/ComfyUI/pull/4379>
### 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
@@ -223,9 +244,24 @@ core extensions will be loaded.
### 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 uninstall @comfyorg/litegraph` in this 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,7 +1,14 @@
import type { Page, Locator } from '@playwright/test'
import type { Page, Locator, APIRequestContext } from '@playwright/test'
import { expect } from '@playwright/test'
import { test as base } from '@playwright/test'
import { ComfyAppMenu } from './helpers/appMenu'
import dotenv from 'dotenv'
dotenv.config()
import * as fs from 'fs'
import { NodeBadgeMode } from '../src/types/nodeSource'
import { NodeId } from '../src/types/comfyWorkflow'
import { ManageGroupNode } from './helpers/manageGroupNode'
import { ComfyTemplates } from './helpers/templates'
interface Position {
x: number
@@ -13,9 +20,37 @@ interface Size {
height: number
}
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()
}
}
class ComfyNodeSearchBox {
public readonly input: Locator
public readonly dropdown: Locator
public readonly filterSelectionPanel: ComfyNodeSearchFilterSelectionPanel
constructor(public readonly page: Page) {
this.input = page.locator(
@@ -24,6 +59,11 @@ class ComfyNodeSearchBox {
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(
@@ -41,11 +81,28 @@ class ComfyNodeSearchBox {
.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()
}
}
class NodeLibrarySidebarTab {
public readonly tabId: string = 'node-library'
constructor(public readonly page: Page) {}
class SidebarTab {
constructor(
public readonly page: Page,
public readonly tabId: string
) {}
get tabButton() {
return this.page.locator(`.${this.tabId}-tab-button`)
@@ -57,8 +114,25 @@ class NodeLibrarySidebarTab {
)
}
async open() {
if (await this.selectedTabButton.isVisible()) {
return
}
await this.tabButton.click()
}
}
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')
return this.page.locator('.node-lib-tree-explorer')
}
get nodePreview() {
@@ -74,16 +148,21 @@ class NodeLibrarySidebarTab {
}
async open() {
if (await this.selectedTabButton.isVisible()) {
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: 'visible' })
await this.nodeLibraryTree.waitFor({ state: 'hidden' })
}
folderSelector(folderName: string) {
return `.p-tree-node-content:has(> .node-lib-tree-node-label:has(.folder-label:has-text("${folderName}")))`
return `.p-tree-node-content:has(> .tree-explorer-node-label:has(.tree-folder .node-label:has-text("${folderName}")))`
}
getFolder(folderName: string) {
@@ -91,7 +170,7 @@ class NodeLibrarySidebarTab {
}
nodeSelector(nodeName: string) {
return `.p-tree-node-content:has(> .node-lib-tree-node-label:has(.node-label:has-text("${nodeName}")))`
return `.p-tree-node-content:has(> .tree-explorer-node-label:has(.tree-leaf .node-label:has-text("${nodeName}")))`
}
getNode(nodeName: string) {
@@ -99,19 +178,125 @@ class NodeLibrarySidebarTab {
}
}
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 browseWorkflowsButton() {
return this.page.locator('.browse-workflows-button')
}
get newDefaultWorkflowButton() {
return this.page.locator('.new-default-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.page.locator(
'.comfyui-workflows-open .node-label',
{ hasText: workflowName }
)
await workflowLocator.click()
await this.page.waitForTimeout(300)
}
}
class Topbar {
constructor(public readonly page: Page) {}
async getTabNames(): Promise<string[]> {
return await this.page
.locator('.workflow-tabs .workflow-label')
.allInnerTexts()
}
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:has-text("${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}")`
)
await menuItem.waitFor({ state: 'visible' })
await menuItem.hover()
if (i === path.length - 1) {
await menuItem.click()
}
}
}
}
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)
}
async saveWorkflow(name: string) {
const acceptDialog = async (dialog) => {
await dialog.accept(name)
}
this.page.on('dialog', acceptDialog)
await this.saveButton.click()
// Wait a moment to ensure the dialog has been handled
await this.page.waitForTimeout(300)
// Remove the dialog listener
this.page.off('dialog', acceptDialog)
}
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(() => {
@@ -136,6 +321,10 @@ class ComfyMenu {
}
}
type FolderStructure = {
[key: string]: FolderStructure | string
}
export class ComfyPage {
public readonly url: string
// All canvas position operations are based on default view of canvas.
@@ -152,8 +341,13 @@ export class ComfyPage {
// Components
public readonly searchBox: ComfyNodeSearchBox
public readonly menu: ComfyMenu
public readonly appMenu: ComfyAppMenu
public readonly templates: ComfyTemplates
constructor(public readonly page: Page) {
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)
@@ -162,16 +356,55 @@ export class ComfyPage {
this.workflowUploadInput = page.locator('#comfy-file-input')
this.searchBox = new ComfyNodeSearchBox(page)
this.menu = new ComfyMenu(page)
this.appMenu = new ComfyAppMenu(page)
this.templates = new ComfyTemplates(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
return window['app']?.graph?.nodes?.length || 0
})
}
async setup() {
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/default/workflows'
}
}
)
if (resp.status() !== 200) {
throw new Error(
`Failed to setup workflows directory: ${await resp.text()}`
)
}
}
async setup({ resetView = true } = {}) {
await this.goto()
await this.page.evaluate(() => {
localStorage.clear()
sessionStorage.clear()
})
// 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'
@@ -193,9 +426,22 @@ export class ComfyPage {
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()
if (resetView) {
// Reset view to force re-rendering of canvas. So that info fields like fps
// become hidden.
await this.resetView()
}
// Hide all badges by default.
await this.setSetting('Comfy.NodeBadge.NodeIdBadgeMode', NodeBadgeMode.None)
await this.setSetting(
'Comfy.NodeBadge.NodeSourceBadgeMode',
NodeBadgeMode.None
)
}
public assetPath(fileName: string) {
return `./browser_tests/assets/${fileName}`
}
async setSetting(settingId: string, settingValue: any) {
@@ -234,7 +480,7 @@ export class ComfyPage {
async loadWorkflow(workflowName: string) {
await this.workflowUploadInput.setInputFiles(
`./browser_tests/assets/${workflowName}.json`
this.assetPath(`${workflowName}.json`)
)
await this.nextFrame()
}
@@ -296,30 +542,93 @@ export class ComfyPage {
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() {
// 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()
await this.dragAndDrop(this.clipTextEncodeNode1InputSlot, this.emptySpace)
}
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()
await this.dragAndDrop(
this.loadCheckpointNodeClipOutputSlot,
this.clipTextEncodeNode1InputSlot
)
}
async adjustWidgetValue() {
@@ -357,6 +666,24 @@ export class ComfyPage {
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.page.mouse.click(10, 10, { button: 'right' })
await this.nextFrame()
@@ -420,6 +747,13 @@ export class ComfyPage {
await this.nextFrame()
}
async ctrlY() {
await this.page.keyboard.down('Control')
await this.page.keyboard.press('KeyY')
await this.page.keyboard.up('Control')
await this.nextFrame()
}
async ctrlArrowUp() {
await this.page.keyboard.down('Control')
await this.page.keyboard.press('ArrowUp')
@@ -439,6 +773,11 @@ export class ComfyPage {
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,
@@ -521,11 +860,199 @@ export class ComfyPage {
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 (
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)
}
}
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
)
}
}
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 getSize(): Promise<Size> {
const size = await this.getProperty<[number, number]>('size')
return {
width: size[0],
height: size[1]
}
}
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 click(position: 'title', options?: Parameters<Page['click']>[1]) {
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
default:
throw new Error(`Invalid click position ${position}`)
}
await this.comfyPage.canvas.click({
...options,
position: clickPos
})
await this.comfyPage.nextFrame()
}
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 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')
)
}
}
export const comfyPageFixture = base.extend<{ comfyPage: ComfyPage }>({
comfyPage: async ({ page }, use) => {
const comfyPage = new ComfyPage(page)
comfyPage: async ({ page, request }, use) => {
const comfyPage = new ComfyPage(page, request)
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.
Please backup your ComfyUI data before running the tests locally.
## 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,116 @@
import type { Response } from '@playwright/test'
import type { StatusWsMessage } from '../src/types/apiTypes.ts'
import { expect, mergeTests } from '@playwright/test'
import { comfyPageFixture } from './ComfyPage'
import { webSocketFixture } from './fixtures/ws.ts'
const test = mergeTests(comfyPageFixture, webSocketFixture)
test.describe('AppMenu', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Floating')
})
test.afterEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
/**
* 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.appMenu.queueButton.toggleOptions()
expect(await queueOpts.getMode()).toBe('disabled')
await queueOpts.setMode('change')
await comfyPage.nextFrame()
expect(await queueOpts.getMode()).toBe('change')
await comfyPage.appMenu.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'].workflowManager.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)
})
})

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
}

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
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

View File

@@ -0,0 +1,57 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from './ComfyPage'
test.describe('Browser tab title', () => {
test.describe('Beta Menu', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Floating')
})
test.afterEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test('Can display workflow name', async ({ comfyPage }) => {
const workflowName = await comfyPage.page.evaluate(async () => {
return window['app'].workflowManager.activeWorkflow.name
})
// Note: unsaved workflow name is always prepended with "*".
expect(await comfyPage.page.title()).toBe(`*${workflowName}`)
})
// Broken by https://github.com/Comfy-Org/ComfyUI_frontend/pull/893
// Release blocker for v1.3.0
test.skip('Can display workflow name with unsaved changes', async ({
comfyPage
}) => {
const workflowName = await comfyPage.page.evaluate(async () => {
return window['app'].workflowManager.activeWorkflow.name
})
// Note: unsaved workflow name is always prepended with "*".
expect(await comfyPage.page.title()).toBe(`*${workflowName}`)
await comfyPage.menu.saveWorkflow('test')
expect(await comfyPage.page.title()).toBe('test')
const textBox = comfyPage.widgetTextBox
await textBox.fill('Hello World')
await comfyPage.clickEmptySpace()
expect(await comfyPage.page.title()).toBe(`*test`)
// Delete the saved workflow for cleanup.
await comfyPage.page.evaluate(async () => {
window['app'].workflowManager.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,250 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from './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.afterEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.CustomColorPalettes', {})
await comfyPage.setSetting('Comfy.ColorPalette', 'dark')
await comfyPage.setSetting('Comfy.Node.Opacity', 1.0)
})
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.afterEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Node.Opacity', 1.0)
await comfyPage.setSetting('Comfy.ColorPalette', 'dark')
})
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: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 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: 157 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

View File

@@ -13,6 +13,23 @@ 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
@@ -28,34 +45,53 @@ test.describe('Execution error', () => {
test.describe('Missing models warning', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Workflow.ModelDownload.AllowedSources', [
'http://localhost:8188'
])
await comfyPage.setSetting('Comfy.Workflow.ModelDownload.AllowedSuffixes', [
'.safetensors'
])
await comfyPage.setSetting('Comfy.Workflow.ShowMissingModelsWarning', true)
await comfyPage.page.evaluate((url: string) => {
return fetch(`${url}/api/devtools/cleanup_fake_model`)
}, comfyPage.url)
})
test('Should display a warning when missing models are found', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Workflow.ShowMissingModelsWarning', true)
// The fake_model.safetensors is served by
// https://github.com/Comfy-Org/ComfyUI_devtools/blob/main/__init__.py
await comfyPage.loadWorkflow('missing_models')
// Wait for the element with the .comfy-missing-models selector to be visible
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
await expect(missingModelsWarning).toBeVisible()
// Click the download button
const downloadButton = comfyPage.page.getByLabel('Download')
await expect(downloadButton).toBeVisible()
await downloadButton.click()
// Wait for the element with the .download-complete selector to be visible
const downloadComplete = comfyPage.page.locator('.download-complete')
await expect(downloadComplete).toBeVisible()
})
test('Can configure download folder', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('missing_models')
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
await expect(missingModelsWarning).toBeVisible()
const folderSelectToggle = comfyPage.page.locator(
'.model-path-select-checkbox'
)
const folderSelect = comfyPage.page.locator('.model-path-select')
await expect(folderSelectToggle).toBeVisible()
await expect(folderSelect).not.toBeVisible()
await folderSelectToggle.click() // show the selectors
await expect(folderSelect).toBeVisible()
await folderSelect.click() // open dropdown
await expect(folderSelect).toHaveClass(/p-select-open/)
await folderSelect.click() // close the dropdown
await expect(folderSelect).not.toHaveClass(/p-select-open/)
await folderSelectToggle.click() // hide the selectors
await expect(folderSelect).not.toBeVisible()
})
})

View File

@@ -0,0 +1,32 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from './ComfyPage'
test.describe('Topbar commands', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Floating')
})
test.afterEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test('Should allow registering topbar commands', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
window['app'].extensionManager.menu.registerTopbarCommands(
['ext'],
[
{
id: 'foo',
label: 'foo',
function: () => {
window['foo'] = true
}
}
]
)
})
await comfyPage.menu.topbar.triggerTopbarCommand(['ext', 'foo'])
expect(await comfyPage.page.evaluate(() => window['foo'])).toBe(true)
})
})

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,151 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from './ComfyPage'
test.describe('Group Node', () => {
test.afterEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
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', 'Floating')
libraryTab = comfyPage.menu.nodeLibraryTab
await comfyPage.convertAllNodesToGroupNode(groupNodeName)
await libraryTab.open()
})
test.afterEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [])
await libraryTab.close()
})
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.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)
expect(comfyPage.page.locator('.comfy-missing-nodes')).not.toBeVisible()
})
})

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,44 @@
import type { Page, Locator } from '@playwright/test'
import type { AutoQueueMode } from '../../src/stores/queueStore'
export class ComfyAppMenu {
public readonly root: Locator
public readonly queueButton: ComfyQueueButton
constructor(public readonly page: Page) {
this.root = page.locator('.app-menu')
this.queueButton = new ComfyQueueButton(this)
}
}
class ComfyQueueButton {
public readonly root: Locator
public readonly primaryButton: Locator
public readonly dropdownButton: Locator
constructor(public readonly appMenu: ComfyAppMenu) {
this.root = appMenu.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.appMenu.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

@@ -19,24 +19,61 @@ test.describe('Node Interaction', () => {
await expect(comfyPage.canvas).toHaveScreenshot('selected-node2.png')
})
// 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()
// 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')
})
})
@@ -114,12 +151,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.
@@ -133,6 +173,32 @@ 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({
@@ -166,8 +232,52 @@ test.describe('Node Interaction', () => {
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
}
})
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', () => {
@@ -252,6 +362,12 @@ 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', () => {
@@ -279,3 +395,30 @@ test.describe('Widget Interaction', () => {
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.describe('Load duplicate workflow', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Floating')
})
test.afterEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
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.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

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