Compare commits

..

33 Commits

Author SHA1 Message Date
bymyself
c4468a14c5 fix(cloud): disable legacy node templates feature on cloud
The legacy node templates feature (accessed via canvas right-click menu)
causes false-positive errors on cloud. This change conditionally loads
the extension only for non-cloud builds.

Refs: COM-14105
Amp-Thread-ID: https://ampcode.com/threads/T-019c0c5f-e384-7733-9081-9460dc8debf5
Co-authored-by: Amp <amp@ampcode.com>
2026-01-29 18:07:18 -08:00
Alexander Brown
82bacb82a7 test: add Playwright test tags for filtering (@smoke, @slow, @screenshot, domains) (#8441)
## Summary

Adds structured test tags to all 54 Playwright test files to enable
flexible test filtering during development and CI.

## Tags Added

| Tag | Count | Purpose |
|-----|-------|---------|
| `@screenshot` | 32 files | Tests with visual assertions
(`toHaveScreenshot`) |
| `@smoke` | 5 files | Quick essential tests for fast validation |
| `@slow` | 5 files | Long-running tests (templates, subgraph,
featureFlags) |
| `@canvas` | 15 files | Canvas/graph rendering tests |
| `@node` | 10 files | Node behavior tests |
| `@ui` | 8 files | UI component tests |
| `@widget` | 5 files | Widget-specific tests |
| `@workflow` | 3 files | Workflow operations |
| `@subgraph` | 1 file | Subgraph functionality |
| `@keyboard` | 2 files | Keyboard shortcuts |
| `@settings` | 2 files | Settings/preferences |

## Usage Examples

```bash
# Quick validation (~16 tests, ~30s)
pnpm test:browser -- --grep @smoke

# Skip slow tests for faster CI feedback
pnpm test:browser -- --grep-invert @slow

# Skip visual tests (useful for local development without snapshots)
pnpm test:browser -- --grep-invert @screenshot

# Run only canvas-related tests
pnpm test:browser -- --grep @canvas

# Combine filters
pnpm test:browser -- --grep @smoke --grep-invert @screenshot
```

## Implementation Details

- Uses Playwright's native tag syntax: `test.describe('Name', { tag:
'@tag' }, ...)`
- Tags inherit from describe blocks to child tests
- Preserves existing project-level tags: `@mobile`, `@2x`, `@0.5x`
- Multiple tags supported: `{ tag: ['@screenshot', '@smoke'] }`

## Test Plan

- [x] All existing tests pass unchanged
- [x] Tag filtering works with `--grep` and `--grep-invert`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8441-test-add-Playwright-test-tags-for-filtering-smoke-slow-screenshot-domains-2f76d73d36508184990ec859c8fd7629)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: GitHub Action <action@github.com>
2026-01-29 16:34:56 -08:00
Christian Byrne
80ccc13659 feat: add Chatterbox model support for Cloud asset browser (#8418)
## Summary

Adds support for creating Chatterbox TTS nodes when clicking Chatterbox
models in the Cloud asset browser.

## Changes

### modelToNodeStore.ts
- Add `findProvidersWithFallback()` helper for hierarchical model type
lookups (e.g., `parent/child` falls back to `parent`)
- Register 4 Chatterbox model directories with empty widget keys:
  - `chatterbox/chatterbox` → `FL_ChatterboxTTS`
  - `chatterbox/chatterbox_turbo` → `FL_ChatterboxTurboTTS`
- `chatterbox/chatterbox_multilingual` → `FL_ChatterboxMultilingualTTS`
  - `chatterbox/chatterbox_vc` → `FL_ChatterboxVC`

### createModelNodeFromAsset.ts
- Skip widget assignment when `provider.key` is empty (for nodes that
auto-load models without a widget selector)

### Tests
- Add tests for hierarchical fallback behavior
- Add tests for empty widget key (auto-load nodes)
- Add Chatterbox node types to mock data

## Notes

- Empty `key` convention: Chatterbox nodes auto-load their models and
don't have a model selector widget, so we register them with `key: ''`
and skip the widget assignment step
- Hierarchical fallback only goes one level deep (`a/b/c` → `a`, not
`a/b`)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8418-feat-add-Chatterbox-model-support-for-Cloud-asset-browser-2f76d73d365081be822bc369b155f099)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Subagent 5 <subagent@example.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: GitHub Action <action@github.com>
2026-01-29 16:26:51 -08:00
AustinMroz
dce6fd1040 Fix invalid keybind flash (#8435)
Previously the save keybind action would
- apply the new keybind
- wait for a network request to persist the change
- close the dialogue regardless of the results of the above changes

During this network request, the dialog would show a warning that the
keybind is invalid because the dialogue "contains a modified keybind
which conflicts with an existing keybind"
<img width="506" height="261" alt="image"
src="https://github.com/user-attachments/assets/e46150ce-9349-4f8e-b3b5-fb0b20dd3db9"
/>


This PR changes the order these actions are applied in.
- The dialogue is immediately closed
- The keybinding is updated if valid
- The keybinding is persisted.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8435-Fix-invalid-keybind-flash-2f76d73d3650815c9657f35e77d331fe)
by [Unito](https://www.unito.io)
2026-01-29 16:26:07 -08:00
AustinMroz
e2625a4055 Fix paste-with-links breaking autogrow connections (#8442)
The extra `linf` check was made in 878c8c0f39. I'm no longer able to
replicate the cloning bug the check was introduced for.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8442-Fix-paste-with-links-breaking-autogrow-connections-2f76d73d3650817aa99cc3b9e4e6412c)
by [Unito](https://www.unito.io)
2026-01-29 16:25:16 -08:00
Alexander Brown
a4cf9a1ca8 Fix: Hide Jobs in Assets Panel when Queue V2 is disabled. (#8450)
## Summary

See Title.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8450-Fix-Hide-Jobs-in-Assets-Panel-when-Queue-V2-is-disabled-2f86d73d3650810c8155c1fea92fc0aa)
by [Unito](https://www.unito.io)
2026-01-29 16:23:16 -08:00
Jin Yi
d437b96238 [bugfix] Fix shift+click deselection in asset panel (#8396)
## Summary
Fix shift+click range selection not properly deselecting assets when
selecting a smaller range, and improve selection performance.

## Changes
- **Bug Fix**: Shift+click now replaces selection with the new range
instead of combining with existing selection
- **Performance**: Remove unnecessary `.every()` check in `setSelection`
(O(n) → O(1))
- **Tests**: Add 23 unit tests for asset selection logic

## Test Plan
- [x] Click 1st asset → only 1st selected
- [x] Shift+click 3rd asset → items 1-3 selected
- [x] Shift+click 1st asset → only 1st selected (was broken, now fixed)
- [x] All 23 new unit tests pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8396-bugfix-Fix-shift-click-deselection-in-asset-panel-2f76d73d3650814ca060d1e6a40cf6d4)
by [Unito](https://www.unito.io)
2026-01-29 16:21:08 -08:00
Christian Byrne
f1cf8073d6 fix: garbage collect subgraph definitions when SubgraphNode is removed (#8187)
## Summary

When removing a SubgraphNode via `LGraph.remove()`:
- Fire `onRemoved` for all nodes inside the subgraph
- Fire `onNodeRemoved` callback on the subgraph for each inner node
- Remove subgraph definition from `rootGraph.subgraphs` when no other
nodes reference it (checks both root graph nodes and nodes inside other
subgraphs)

This ensures proper cleanup of subgraph definitions and lifecycle
callbacks for nested nodes when subgraph nodes are deleted.

## Changes

### LGraph.ts
Added SubgraphNode-specific cleanup in `remove()` method that:
1. Iterates inner nodes and fires their `onRemoved` callbacks
2. Fires `onNodeRemoved` on the subgraph for downstream listeners (e.g.,
minimap)
3. Garbage collects the subgraph definition when no other nodes
reference it

### SubgraphNode.ts
Fixed `graph` property to match `LGraphNode` lifecycle contract.
Previously it was declared as `override readonly graph` via constructor
parameter promotion, which prevented `LGraph.remove()` from setting
`node.graph = null`. Changed to a regular mutable property with null
guard in `rootGraph` getter.

### LGraph.test.ts
Added 4 tests:
- `removing SubgraphNode fires onRemoved for inner nodes`
- `removing SubgraphNode fires onNodeRemoved callback`
- `subgraph definition is removed when last referencing node is removed`
- `subgraph definition is retained when other nodes still reference it`

## Related

- Fixes #8145
- Part of the subgraph lifecycle cleanup plan (Slice 2: Definition
garbage collection)
2026-01-29 16:19:25 -08:00
Jin Yi
d7654baebf [feat] Show context-appropriate empty state messages in Manager tabs (#8415)
## Summary
Shows tab-specific empty state messages in Node Manager instead of
generic "No search results found" message.

## Changes
- Added computed properties to determine empty state messages based on
current tab and search state
- Display tab-specific messages when a tab is empty without active
search (e.g., "No Missing Nodes" for Missing tab)
- Fall back to search-related messages only when there's an active
search query
- Added Korean translations for empty state messages

| Tab | Empty State Title |
|-----|-------------------|
| All Installed | No Extensions Installed |
| Update Available | All Up to Date |
| Conflicting | No Conflicts Detected |
| Workflow | No Extensions in Workflow |
| Missing | No Missing Nodes |

## Review Focus
- Verify i18n key structure matches existing patterns

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8415-feat-Show-context-appropriate-empty-state-messages-in-Manager-tabs-2f76d73d3650817ab8a0d41b45df3411)
by [Unito](https://www.unito.io)
2026-01-29 16:16:28 -08:00
Comfy Org PR Bot
a032e50721 1.39.2 (#8447)
Patch version increment to 1.39.2

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8447-1-39-2-2f86d73d3650819e8daccc9c5fbc3a3b)
by [Unito](https://www.unito.io)

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2026-01-29 16:12:49 -08:00
Christian Byrne
d9ea36a1d0 refactor: change asset cache from nodeType-keyed to category-keyed (#8433)
## Summary

Refactors the model assets cache in `assetsStore.ts` to be keyed by
category (e.g., 'checkpoints', 'loras') instead of nodeType (e.g.,
'CheckpointLoaderSimple').

## Changes

- Rename `modelStateByKey` to `modelStateByCategory`
- Add `resolveCategory()` helper to translate nodeType to category for
cache lookup
- Multiple node types sharing the same category now share one cache
entry
- Add `invalidateCategory()` method for cache invalidation
- Maintain backwards-compatible public API accepting nodeType
- Update tests for new category-keyed behavior

## Benefits

1. **Deduplication**: Same category = same cache entry = single API call
2. **Simple invalidation**: Delete asset with tag 'checkpoints' then
invalidate cache
3. **Cleaner mental model**: Store to View reactive flow works naturally

## Testing

- All existing tests pass with updates
- Added new tests for category-keyed cache sharing, invalidateCategory,
and unknown node type handling

## Part of Stack

This is **PR 1 of 2** in a stacked PR series:
1. **This PR**: Refactor asset cache to category-keyed (architectural
improvement)
2. **[PR 2
#8434](https://github.com/Comfy-Org/ComfyUI_frontend/pull/8434)**: Fix
deletion invalidation

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8433-refactor-change-asset-cache-from-nodeType-keyed-to-category-keyed-2f76d73d365081999b7fda12c9706ab5)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Subagent 5 <subagent@example.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: GitHub Action <action@github.com>
2026-01-29 16:03:29 -08:00
AustinMroz
eceed972f5 Fix Help Center display in linear mode (#8438)
The Help Center popover wasn't working in linear mode because the
`#graph-canvas-container` is hidden. This is fixed by instead setting
the target to body. This matches the [default behaviour used by portals
in
rekai-ui](https://github.com/unovue/reka-ui/blob/v2/packages/core/src/Teleport/Teleport.vue)
<img width="476" height="677" alt="image"
src="https://github.com/user-attachments/assets/cca46648-3bce-4b72-a5be-3727e2358217"
/>

I've got a working branch for moving the Help Center to our reka-ui
Popover component, but that'll require a much larger surface area of
code changes.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8438-Fix-Help-Center-display-in-linear-mode-2f76d73d36508112abedf1160f7c3a90)
by [Unito](https://www.unito.io)
2026-01-29 16:01:38 -08:00
Christian Byrne
72a6af4b9e make new queue panel disabled by default (#8444)
## Summary

Was enabled by default for 1.38 during nightly, but should be reverted
back now.

In the future we can now just use `isNightly` flag for this (ref:
https://github.com/Comfy-Org/ComfyUI_frontend/pull/8149)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8444-make-new-queue-panel-disabled-by-default-2f76d73d365081139b54f76d9325101d)
by [Unito](https://www.unito.io)
2026-01-29 23:56:54 +00:00
Rizumu Ayaka
fc38f16543 fix: default image input for the template is displayed as empty on dropdown selection (#8276)
The image input for nodes loaded from templates appears empty in the
Properties Panel.

When the widget's current value (saved in the template) is not in the
available file list returned by the server, the selectedSet is empty,
causing a placeholder to be displayed instead of the actual value.

Added a missingValueItem computed property in WidgetSelectDropdown.vue.
When the current value is not in inputItems or outputItems, it creates a
fallback item and adds it to dropdownItems.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8276-fix-default-image-input-for-the-template-is-displayed-as-empty-on-dropdown-selection-2f16d73d3650817eaad5e4e33637fb74)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: github-actions <github-actions@github.com>
2026-01-29 14:19:52 -08:00
Christian Byrne
faad2c03de feat: increase allowed batch count (on Run button) on cloud (from 4 to 32) (#8436)
## Summary

cloud.comfy.org now suports up to 100 queued jobs at a time
([details](https://x.com/ComfyUI/status/2016622139722572032?s=20)). We
can increase the batch count limit to 32. Possible downside is cloud
having to reject larger number of jobs over the 100 limit if someone go
to 32 and clicks 4+ times. This setting was configurable anyway before,
so this is mostly a QoL change.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8436-feat-increase-allowed-batch-count-on-Run-button-on-cloud-from-4-to-32-2f76d73d365081728650fabefc394046)
by [Unito](https://www.unito.io)
2026-01-29 13:54:52 -08:00
Christian Byrne
d4cec49db5 fix: use merge-multiple for snapshot artifact download (#8432)
## Summary

Fixes the snapshot merge failure introduced by PR #8377
(actions/download-artifact v4→v7 upgrade).

## Root Cause

The v5+ release of `download-artifact` changed behavior: when a
`pattern` matches only a **single artifact**, files are extracted
directly to `path/` without the artifact name subdirectory. When only
one shard had changes, the merge loop couldn't find the expected
`snapshots-shard-*/` directories.

## Fix

Use `merge-multiple: true` — the documented pattern for combining
sharded artifacts. This merges all matched artifacts directly into the
target path, eliminating directory structure assumptions.

## Testing

This fix can be validated by re-running the workflow on [PR
#8276](https://github.com/Comfy-Org/ComfyUI_frontend/pull/8276) after
merge.

---
- Fixes snapshot update workflow regression from #8377

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8432-fix-use-merge-multiple-for-snapshot-artifact-download-2f76d73d3650810b97fdfe28cd3c7694)
by [Unito](https://www.unito.io)

Co-authored-by: Subagent 5 <subagent@example.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-01-29 12:58:22 -08:00
AustinMroz
af8433fb3d Support widget specific contextmenu options in vue (#8431)
<img width="614" height="485" alt="image"
src="https://github.com/user-attachments/assets/2a635dec-8bed-4fab-9881-5e6057d482e1"
/>

These options were defined in `litegraphService`. While the existing
code for defining options is reused (to ensure there's no implementation
drift) these extra widget options use the litegraph format for context
menu options and do not belong in `useSelectionMenuOptions`. They have
been moved out of `useLitegraphService` (good), but left in
`litegraphService` (not great)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8431-Support-widget-specific-contextmenu-options-in-vue-2f76d73d3650814fb20fca352dc81e3b)
by [Unito](https://www.unito.io)
2026-01-29 11:38:41 -08:00
Johnpaul Chiwetelu
cabd08f0ec Road to No explicit any: Group 8 (part 6) test files (#8344)
## Summary

This PR removes unsafe type assertions ("as unknown as Type") from test
files and improves type safety across the codebase.

### Key Changes

#### Type Safety Improvements
- Removed all instances of "as unknown as" patterns from test files
- Used proper factory functions from litegraphTestUtils instead of
custom mocks
- Made incomplete mocks explicit using Partial<T> types
- Fixed DialogStore mocking with proper interface exports
- Improved type safety with satisfies operator where applicable

#### App Parameter Removal
- **Removed the unused `app` parameter from all ComfyExtension interface
methods**
- The app parameter was always undefined at runtime as it was never
passed from invokeExtensions
- Affected methods: init, setup, addCustomNodeDefs,
beforeRegisterNodeDef, beforeRegisterVueAppNodeDefs,
registerCustomNodes, loadedGraphNode, nodeCreated, beforeConfigureGraph,
afterConfigureGraph

##### Breaking Change Analysis
Verified via Sourcegraph that this is NOT a breaking change:
- Searched all 10 affected methods across GitHub repositories
- Only one external repository
([drawthingsai/draw-things-comfyui](https://github.com/drawthingsai/draw-things-comfyui))
declares the app parameter in their extension methods
- That repository never actually uses the app parameter (just declares
it in the function signature)
- All other repositories already omit the app parameter
- Search queries used:
- [init method
search](https://sourcegraph.com/search?q=context:global+repo:%5Egithub%5C.com/.*+lang:typescript+%22init%28app%22+-repo:Comfy-Org/ComfyUI_frontend&patternType=standard)
- [setup method
search](https://sourcegraph.com/search?q=context:global+repo:%5Egithub%5C.com/.*+lang:typescript+%22setup%28app%22+-repo:Comfy-Org/ComfyUI_frontend&patternType=standard)
  - Similar searches for all 10 methods confirmed no usage

### Files Changed

Test files:
-
src/components/settings/widgets/__tests__/WidgetInputNumberInput.test.ts
- src/services/keybindingService.escape.test.ts  
- src/services/keybindingService.forwarding.test.ts
- src/utils/__tests__/newUserService.test.ts →
src/utils/__tests__/useNewUserService.test.ts
- src/services/jobOutputCache.test.ts
-
src/renderer/extensions/vueNodes/widgets/composables/useRemoteWidget.test.ts
-
src/renderer/extensions/vueNodes/widgets/composables/useIntWidget.test.ts
-
src/renderer/extensions/vueNodes/widgets/composables/useFloatWidget.test.ts

Source files:
- src/types/comfy.ts - Removed app parameter from ComfyExtension
interface
- src/services/extensionService.ts - Improved type safety with
FunctionPropertyNames helper
- src/scripts/metadata/isobmff.ts - Fixed extractJson return type per
review
- src/extensions/core/*.ts - Updated extension implementations
- src/scripts/app.ts - Updated app initialization

### Testing
- All existing tests pass
- Type checking passes  
- ESLint/oxlint checks pass
- No breaking changes for external repositories

Part of the "Road to No Explicit Any" initiative.

### Previous PRs in this series:
- Part 2: #7401
- Part 3: #7935
- Part 4: #7970
- Part 5: #8064
- Part 6: #8083
- Part 7: #8092
- Part 8 Group 1: #8253
- Part 8 Group 2: #8258
- Part 8 Group 3: #8304
- Part 8 Group 4: #8314
- Part 8 Group 5: #8329
- Part 8 Group 6: #8344 (this PR)
2026-01-29 11:03:17 -08:00
AustinMroz
868180eb28 Disable logs button in sidebar on cloud (#8429)
Since cloud doesn't currently provide logs, this button was just causing
confusion.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8429-Disable-logs-button-in-sidebar-on-cloud-2f76d73d365081a4b909dd9a105e381e)
by [Unito](https://www.unito.io)
2026-01-29 09:58:25 -08:00
Alexander Brown
c51916d103 Chore: Add workflow dispatch to E2E (#8422)
Also remove old branches from the ignore

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8422-Chore-Add-workflow-dispatch-to-E2E-2f76d73d365081d0940dceaa37231ca7)
by [Unito](https://www.unito.io)
2026-01-29 08:47:18 -08:00
Comfy Org PR Bot
d2ff7d518a 1.39.1 (#8382)
Patch version increment to 1.39.1

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8382-1-39-1-2f76d73d36508126840cdc74593952e5)
by [Unito](https://www.unito.io)

---------

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Austin Mroz <austin@comfy.org>
2026-01-29 00:31:49 -08:00
AustinMroz
44baadd7ca Implement clickable badges (#8401)
Adds an `onClick` handler to LGraphBadge

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8401-Implement-clickable-badges-2f76d73d365081b3b23fc1eaa3bc65b8)
by [Unito](https://www.unito.io)
2026-01-29 00:04:28 -08:00
AustinMroz
3866fe7eaa Fix flake hidream test (#8406)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8406-Fix-flake-hidream-test-2f76d73d3650814daae8fb7775fa3073)
by [Unito](https://www.unito.io)
2026-01-29 00:02:30 -08:00
Jin Yi
4debbf8268 [bugfix] Disable install button when already installed version is selected (#8412)
## Summary
Prevent re-installing an already installed version by disabling the
Install button and the version option in the selector.

## Changes
- Add `isVersionInstalled()` function to check if a version is already
installed
- Add `isInstallDisabled` computed to disable Install button when
selected version is installed
- Add `option-disabled="isInstalled"` to Listbox to prevent selecting
installed versions

Fixes the issue where users could trigger duplicate install operations
by selecting the currently installed version.

🤖 Generated with [Claude Code](https://claude.ai/code)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8412-bugfix-Disable-install-button-when-already-installed-version-is-selected-2f76d73d365081cb859ff98a3d1c64e6)
by [Unito](https://www.unito.io)
2026-01-28 23:58:12 -08:00
Christian Byrne
23a5baef43 feat: add category support for blueprints and protect global blueprints (#8378)
## Summary

This PR adds two related features for subgraph blueprints:

### 1. Protect Global Blueprints from Deletion
- Added `isGlobalBlueprint()` helper that distinguishes blueprints by
`python_module` field
- User blueprints have `python_module: 'blueprint'`
- Global (installed) blueprints have `python_module` set to the node
pack name
- Guarded `deleteBlueprint()` to show a warning toast for global
blueprints
- Hidden delete menu in node library for global blueprints

### 2. Category Support for Blueprints
- User blueprints now use category `Subgraph Blueprints/User`
- Global blueprints use `Subgraph Blueprints/{category}` if category is
provided, otherwise `Subgraph Blueprints`
- Extended `GlobalSubgraphData.info` type with optional `category` field
- Added `category` to `zSubgraphDefinition` schema

## Files Changed
- `src/stores/subgraphStore.ts` - Core logic for both features
- `src/stores/subgraphStore.test.ts` - Tests for `isGlobalBlueprint`
- `src/components/sidebar/tabs/nodeLibrary/NodeTreeLeaf.vue` - Hide
delete menu for global blueprints
- `src/scripts/api.ts` - Extended `GlobalSubgraphData` type
- `src/platform/workflow/validation/schemas/workflowSchema.ts` - Added
category to schema
- `src/locales/en/main.json` - Added translation key

## Testing
-  `pnpm typecheck` passed
-  `pnpm lint` passed

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8378-feat-add-category-support-for-blueprints-and-protect-global-blueprints-2f66d73d36508137aa67c2d88c358b69)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Subagent 5 <subagent@example.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: AustinMroz <austin@comfy.org>
2026-01-28 23:30:48 -08:00
Christian Byrne
0faf2220b8 fix: dragging (e.g., when selecting text) in Markdown note causes node to drag (#8413)
## Summary

When attempting to select text inside Vue node Markdown widget's
textarea (edit mode), the node would drag instead of text being
selected.

Root cause: WidgetMarkdown.vue's Textarea only had @click.stop and
@keydown.stop, but was missing pointer event modifiers. The pointerdown
event bubbled up to LGraphNode.vue which initiated node drag.

*Fix*: Add @pointerdown.capture.stop, @pointermove.capture.stop, and
@pointerup.capture.stop to match the pattern used in WidgetTextarea.vue.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8413-fix-dragging-e-g-when-selecting-text-in-Markdown-note-causes-node-to-drag-2f76d73d3650816dbf9bdf893775c3d4)
by [Unito](https://www.unito.io)

Co-authored-by: Subagent 5 <subagent@example.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-01-28 23:18:23 -08:00
Jin Yi
65ff23c5af [bugfix] Fix manager missing node tab with shared composable (#8409) 2026-01-29 06:23:47 +00:00
Alexander Brown
6ce60a11a4 test: use createTestingPinia instead of createPinia (#8376)
Replace \createPinia\ with \createTestingPinia({ stubActions: false })\
from \@pinia/testing\ across 45 test files for proper test isolation.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8376-test-use-createTestingPinia-instead-of-createPinia-2f66d73d36508137a9f0daffcddc86f7)
by [Unito](https://www.unito.io)

Co-authored-by: Amp <amp@ampcode.com>
2026-01-28 22:21:38 -08:00
Christian Byrne
3b5d124029 fix: use getAuthHeader in createCustomer to support API key auth (#8408)
## Summary

Fixes authentication failure when using API key authentication on
staging server after frontend update to 1.33.10.

<img width="1160" height="709" alt="image"
src="https://github.com/user-attachments/assets/fe56866d-1819-419e-9f53-35a123d764c3"
/>


## Changes

- **What**: Changed `createCustomer()` to use `getAuthHeader()` instead
of `getFirebaseAuthHeader()`, allowing API key users to authenticate
successfully

## Review Focus

- Verify `getAuthHeader()` correctly falls back to API key when no
Firebase token exists
- Backend `/customers` endpoint supports `X-API-KEY` header (per cloud
PR #1766)

Fixes COM-12398

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8408-fix-use-getAuthHeader-in-createCustomer-to-support-API-key-auth-2f76d73d3650819994e3e6d3ed9f3dfa)
by [Unito](https://www.unito.io)

Co-authored-by: Subagent 5 <subagent@example.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-01-28 22:12:28 -08:00
Alexander Brown
bd4920febc Chore: Actions updates and cleanup (#8377)
## Summary

...

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8377-WIP-Chore-Actions-updates-and-cleanup-2f66d73d3650818483a8dffa32a6f245)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-01-28 21:22:39 -08:00
Jin Yi
bd916096ac fix: add null check in getCanvasCenter to prevent crash on asset insert (#8399)
## Summary
Adds null check in `getCanvasCenter()` to prevent crash when inserting
asset as node before canvas is fully initialized.

## Changes
- **What**: Added optional chaining for `app.canvas?.ds?.visible_area`
with fallback to `[0, 0]`

## Review Focus
- Simple defensive fix - returns origin position if canvas not ready

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8399-fix-add-null-check-in-getCanvasCenter-to-prevent-crash-on-asset-insert-2f76d73d365081e88c08ef40ea9e7b78)
by [Unito](https://www.unito.io)
2026-01-29 13:19:51 +09:00
guill
9be853f6b5 feat: support dev-only nodes (#8359)
## Summary

Support `dev_only` property to node definitions that hides nodes from
search and menus unless dev mode is enabled. Dev-only nodes display a
"DEV" badge when visible.

This functionality is primarily intended to support unit-testing nodes
on Comfy Cloud, but also has other uses.

## Changes

- **What**: Nodes flagged as dev_only in the node schema will only
appear in search and menus if Dev Mode is on.

## Screenshots (if applicable)

With Dev Mode off:
<img width="2189" height="1003" alt="image"
src="https://github.com/user-attachments/assets/a08e1fd7-dca9-4ce1-9964-5f4f3b7b95ac"
/>

With Dev Mode on:
<img width="2201" height="1066" alt="image"
src="https://github.com/user-attachments/assets/7fe6cd1f-f774-4f48-b604-a528e286b584"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8359-feat-support-dev-only-nodes-2f66d73d36508102839ee7cd66a26129)
by [Unito](https://www.unito.io)
2026-01-28 19:41:45 -08:00
Christian Byrne
2103dcc788 fix: increase Vue node resize handle size for better usability (#8391)
## Summary

Increases the resize handle size on Vue nodes to improve usability,
especially when nodes are selected.

## Changes

- **What**: Increased resize handle from 12px to 20px and offset it
slightly outside the node boundary to avoid overlap with selection
outline

## Review Focus

The resize handle was too small and became harder to grab when the node
was selected (the 2px outline rendered outside the box, visually
obscuring the corner). This fix increases the hit area and positions it
to extend beyond the node edge.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8391-fix-increase-Vue-node-resize-handle-size-for-better-usability-2f76d73d36508136b2aac51bc0d53551)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Subagent 5 <subagent@example.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: GitHub Action <action@github.com>
2026-01-28 19:15:40 -08:00
237 changed files with 4969 additions and 1896 deletions

1
.gitattributes vendored
View File

@@ -11,6 +11,7 @@
*.ts text eol=lf
*.vue text eol=lf
*.yaml text eol=lf
*.yml text eol=lf
# Generated files
packages/registry-types/src/comfyRegistryTypes.ts linguist-generated=true

View File

@@ -104,14 +104,14 @@ runs:
- name: Find existing comment
id: find
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
with:
issue-number: ${{ inputs.issue-number || github.event.pull_request.number }}
comment-author: github-actions[bot]
body-includes: ${{ steps.build.outputs.marker_search }}
- name: Post or update comment
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
with:
issue-number: ${{ inputs.issue-number || github.event.pull_request.number }}
comment-id: ${{ steps.find.outputs.comment-id }}

View File

@@ -16,7 +16,7 @@ runs:
# Checkout ComfyUI repo, install the dev_tools node and start server
- name: Checkout ComfyUI
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
repository: 'comfyanonymous/ComfyUI'
path: 'ComfyUI'
@@ -33,7 +33,7 @@ runs:
fi
- name: Setup Python
uses: actions/setup-python@v4
uses: actions/setup-python@v6
with:
python-version: '3.10'

View File

@@ -12,29 +12,17 @@ runs:
# Install pnpm, Node.js, build frontend
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: 'lts/*'
cache: 'pnpm'
cache-dependency-path: './pnpm-lock.yaml'
# Restore tool caches before running any build/lint operations
- name: Restore tool output cache
uses: actions/cache/restore@v4
with:
path: |
./.cache
./tsconfig.tsbuildinfo
key: tool-cache-${{ runner.os }}-${{ hashFiles('./pnpm-lock.yaml') }}-${{ hashFiles('./src/**/*.{ts,vue,js,mts}', './*.config.*') }}
restore-keys: |
tool-cache-${{ runner.os }}-${{ hashFiles('./pnpm-lock.yaml') }}-
tool-cache-${{ runner.os }}-
- name: Install dependencies
shell: bash
run: pnpm install --frozen-lockfile

View File

@@ -11,7 +11,7 @@ runs:
echo "playwright-version=$PLAYWRIGHT_VERSION" >> $GITHUB_OUTPUT
- name: Cache Playwright Browsers
uses: actions/cache@v4
uses: actions/cache@v5 # v5.0.2
id: cache-playwright-browsers
with:
path: '~/.cache/ms-playwright'

View File

@@ -13,15 +13,15 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: lts/*
cache: 'pnpm'
@@ -36,7 +36,7 @@ jobs:
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
- name: Create Pull Request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: '[chore] Update electron-types to ${{ steps.get-version.outputs.NEW_VERSION }}'

View File

@@ -18,15 +18,15 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: lts/*
cache: 'pnpm'
@@ -35,7 +35,7 @@ jobs:
run: pnpm install --frozen-lockfile
- name: Checkout ComfyUI-Manager repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
repository: Comfy-Org/ComfyUI-Manager
path: ComfyUI-Manager
@@ -86,7 +86,7 @@ jobs:
- name: Create Pull Request
if: steps.check-changes.outputs.changed == 'true'
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: '[chore] Update ComfyUI-Manager API types from ComfyUI-Manager@${{ steps.manager-info.outputs.commit }}'

View File

@@ -17,15 +17,15 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: lts/*
cache: 'pnpm'
@@ -34,7 +34,7 @@ jobs:
run: pnpm install --frozen-lockfile
- name: Checkout comfy-api repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
repository: Comfy-Org/comfy-api
path: comfy-api
@@ -87,7 +87,7 @@ jobs:
- name: Create Pull Request
if: steps.check-changes.outputs.changed == 'true'
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: '[chore] Update Comfy Registry API types from comfy-api@${{ steps.api-info.outputs.commit }}'

View File

@@ -13,6 +13,6 @@ jobs:
json-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Validate JSON syntax
run: ./scripts/cicd/check-json.sh

View File

@@ -18,23 +18,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout PR
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ !github.event.pull_request.head.repo.fork && github.head_ref || github.ref }}
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Setup frontend
uses: ./.github/actions/setup-frontend
- name: Run ESLint with auto-fix
run: pnpm lint:fix
@@ -73,7 +62,7 @@ jobs:
- name: Comment on PR about auto-fix
if: steps.verify-changed-files.outputs.changed == 'true' && github.event.pull_request.head.repo.full_name == github.repository
continue-on-error: true
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
github.rest.issues.createComment({
@@ -86,7 +75,7 @@ jobs:
- name: Comment on PR about manual fix needed
if: steps.verify-changed-files.outputs.changed == 'true' && github.event.pull_request.head.repo.full_name != github.repository
continue-on-error: true
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
github.rest.issues.createComment({

View File

@@ -16,10 +16,10 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: '3.11'

View File

@@ -17,21 +17,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4.1.0
with:
version: 10
- name: Install Node.js
uses: actions/setup-node@v5
with:
node-version: '24.x'
cache: pnpm
- name: Install dependencies
run: pnpm install
- name: Setup frontend
uses: ./.github/actions/setup-frontend
- name: Build project
run: pnpm build
@@ -46,7 +35,7 @@ jobs:
echo ${{ github.base_ref }} > ./temp/size/base.txt
- name: Upload size data
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: size-data
path: temp/size

View File

@@ -31,11 +31,11 @@ jobs:
echo "Is forked: ${{ github.event.workflow_run.head_repository.full_name != github.event.workflow_run.repository.full_name }}"
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Get PR Number
id: pr
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
const { data: prs } = await github.rest.pulls.list({
@@ -68,7 +68,7 @@ jobs:
- name: Download and Deploy Reports
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}

View File

@@ -5,8 +5,8 @@ on:
push:
branches: [main, master, core/*, desktop/*]
pull_request:
branches-ignore:
[wip/*, draft/*, temp/*, vue-nodes-migration, sno-playwright-*]
branches-ignore: [wip/*, draft/*, temp/*]
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Setup frontend
uses: ./.github/actions/setup-frontend
with:
@@ -25,7 +25,7 @@ jobs:
# Upload only built dist/ (containerized test jobs will pnpm install without cache)
- name: Upload built frontend
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: frontend-dist
path: dist/
@@ -51,9 +51,9 @@ jobs:
shardTotal: [8]
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Download built frontend
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
name: frontend-dist
path: dist/
@@ -72,7 +72,7 @@ jobs:
PLAYWRIGHT_BLOB_OUTPUT_DIR: ./blob-report
- name: Upload blob report
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
if: ${{ !cancelled() }}
with:
name: blob-report-chromium-${{ matrix.shardIndex }}
@@ -98,9 +98,9 @@ jobs:
browser: [chromium-2x, chromium-0.5x, mobile-chrome]
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Download built frontend
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
name: frontend-dist
path: dist/
@@ -128,7 +128,7 @@ jobs:
pnpm exec playwright merge-reports --reporter=json ./blob-report
- name: Upload Playwright report
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
if: always()
with:
name: playwright-report-${{ matrix.browser }}
@@ -141,16 +141,13 @@ jobs:
runs-on: ubuntu-latest
if: ${{ !cancelled() }}
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Download blob reports
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
path: ./all-blob-reports
pattern: blob-report-chromium-*
@@ -165,7 +162,7 @@ jobs:
pnpm dlx @playwright/test merge-reports --reporter=json ./all-blob-reports
- name: Upload HTML report
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: playwright-report-chromium
path: ./playwright-report/
@@ -183,7 +180,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Get start time
id: start-time
@@ -210,10 +207,10 @@ jobs:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Download all playwright reports
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
pattern: playwright-report-*
path: reports

View File

@@ -31,11 +31,11 @@ jobs:
echo "Is forked: ${{ github.event.workflow_run.head_repository.full_name != github.event.workflow_run.repository.full_name }}"
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Get PR Number
id: pr
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
const { data: prs } = await github.rest.pulls.list({
@@ -68,7 +68,7 @@ jobs:
- name: Download and Deploy Storybook
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed' && github.event.workflow_run.conclusion == 'success'
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}

View File

@@ -14,7 +14,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Post starting comment
env:
@@ -36,21 +36,10 @@ jobs:
workflow-url: ${{ steps.workflow-url.outputs.url }}
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Setup frontend
uses: ./.github/actions/setup-frontend
- name: Build Storybook
run: pnpm build-storybook
@@ -69,7 +58,7 @@ jobs:
- name: Upload Storybook build
if: success() && github.event.pull_request.head.repo.fork == false
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: storybook-static
path: storybook-static/
@@ -86,27 +75,16 @@ jobs:
chromatic-storybook-url: ${{ steps.chromatic.outputs.storybookUrl }}
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0 # Required for Chromatic baseline
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Setup frontend
uses: ./.github/actions/setup-frontend
- name: Build Storybook and run Chromatic
id: chromatic
uses: chromaui/action@latest
uses: chromaui/action@07791f8243f4cb2698bf4d00426baf4b2d1cb7e0 # v13.3.5
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
buildScriptName: build-storybook
@@ -136,11 +114,11 @@ jobs:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Download Storybook build
if: needs.storybook-build.outputs.conclusion == 'success'
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
name: storybook-static
path: storybook-static
@@ -170,7 +148,7 @@ jobs:
pull-requests: write
steps:
- name: Update comment with Chromatic URLs
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
const buildUrl = '${{ needs.chromatic-deployment.outputs.chromatic-build-url }}';

View File

@@ -16,21 +16,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Setup frontend
uses: ./.github/actions/setup-frontend
- name: Run Vitest tests
run: pnpm test:unit

View File

@@ -0,0 +1,21 @@
name: Validate Action SHA Pins
on:
pull_request:
paths:
- '.github/workflows/**'
- '.github/actions/**'
- '.pinact.yaml'
permissions:
contents: read
jobs:
validate-pins:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: suzuki-shunsuke/pinact-action@3d49c6412901042473ffa78becddab1aea46bbea # v1.3.1
with:
skip_push: 'true'

View File

@@ -17,10 +17,10 @@ jobs:
yaml-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: '3.x'

View File

@@ -18,12 +18,12 @@ jobs:
steps:
- name: Checkout merge commit
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
# Setup playwright environment
- name: Setup ComfyUI Frontend

View File

@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
# Setup playwright environment with custom node repository
- name: Setup ComfyUI Server (without launching)
@@ -36,7 +36,7 @@ jobs:
# Install the custom node repository
- name: Checkout custom node repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
repository: ${{ inputs.owner }}/${{ inputs.repository }}
path: 'ComfyUI/custom_nodes/${{ inputs.repository }}'
@@ -113,7 +113,7 @@ jobs:
git commit -m "Update locales"
- name: Install SSH key For PUSH
uses: shimataro/ssh-key-action@d4fffb50872869abe2d9a9098a6d9c5aa7d16be4
uses: shimataro/ssh-key-action@d4fffb50872869abe2d9a9098a6d9c5aa7d16be4 # v2.7.0
with:
# PR private key from action server
key: ${{ secrets.PR_SSH_PRIVATE_KEY }}

View File

@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
# Setup playwright environment
- name: Setup ComfyUI Server (and start)
uses: ./.github/actions/setup-comfyui-server
@@ -40,7 +40,7 @@ jobs:
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- name: Create Pull Request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: 'Update locales for node definitions'

View File

@@ -64,7 +64,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0

View File

@@ -23,18 +23,18 @@ jobs:
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: refs/pull/${{ github.event.pull_request.number }}/head
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'pnpm'
@@ -44,7 +44,7 @@ jobs:
pnpm install -g typescript @vue/compiler-sfc
- name: Run Claude PR Review
uses: anthropics/claude-code-action@v1.0.6
uses: anthropics/claude-code-action@ff34ce0ff04a470bd3fa56c1ef391c8f1c19f8e9 # v1.0.38
with:
label_trigger: 'claude-review'
prompt: |

View File

@@ -33,24 +33,13 @@ jobs:
github.event_name == 'workflow_dispatch'
)
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4.1.0
with:
version: 10
- name: Install Node.js
uses: actions/setup-node@v5
with:
node-version: '24.x'
cache: pnpm
- name: Install dependencies
run: pnpm install
- name: Setup frontend
uses: ./.github/actions/setup-frontend
- name: Download size data
uses: dawidd6/action-download-artifact@v11
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
with:
name: size-data
run_id: ${{ github.event_name == 'workflow_dispatch' && inputs.run_id || github.event.workflow_run.id }}
@@ -75,7 +64,7 @@ jobs:
fi
- name: Download previous size data
uses: dawidd6/action-download-artifact@v11
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
with:
branch: ${{ steps.pr-base.outputs.content }}
workflow: ci-size-data.yaml
@@ -89,12 +78,12 @@ jobs:
- name: Read size report
id: size-report
uses: juliangruber/read-file-action@v1
uses: juliangruber/read-file-action@b549046febe0fe86f8cb4f93c24e284433f9ab58 # v1.1.7
with:
path: ./size-report.md
- name: Create or update PR comment
uses: actions-cool/maintain-one-comment@v3
uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3.2.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
number: ${{ steps.pr-number.outputs.content }}

View File

@@ -38,7 +38,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Find Update Comment
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
id: 'find-update-comment'
with:
issue-number: ${{ steps.pr-info.outputs.pr-number }}
@@ -46,7 +46,7 @@ jobs:
body-includes: 'Updating Playwright Expectations'
- name: Add Starting Reaction
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
with:
comment-id: ${{ steps.find-update-comment.outputs.comment-id }}
issue-number: ${{ steps.pr-info.outputs.pr-number }}
@@ -56,7 +56,7 @@ jobs:
reactions: eyes
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ steps.pr-info.outputs.branch }}
- name: Setup frontend
@@ -66,7 +66,7 @@ jobs:
# Upload built dist/ (containerized test jobs will pnpm install without cache)
- name: Upload built frontend
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: frontend-dist
path: dist/
@@ -91,11 +91,11 @@ jobs:
shardTotal: [4]
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ needs.setup.outputs.branch }}
- name: Download built frontend
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
name: frontend-dist
path: dist/
@@ -149,7 +149,7 @@ jobs:
# Upload ONLY the changed files from this shard
- name: Upload changed snapshots
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
if: steps.changed-snapshots.outputs.has-changes == 'true'
with:
name: snapshots-shard-${{ matrix.shardIndex }}
@@ -157,7 +157,7 @@ jobs:
retention-days: 1
- name: Upload test report
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
if: always()
with:
name: playwright-report-shard-${{ matrix.shardIndex }}
@@ -170,17 +170,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ needs.setup.outputs.branch }}
# Download all changed snapshot files from shards
- name: Download snapshot artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
pattern: snapshots-shard-*
path: ./downloaded-snapshots
merge-multiple: false
merge-multiple: true
- name: List downloaded files
run: |
@@ -206,13 +206,13 @@ jobs:
echo "MERGING CHANGED SNAPSHOTS"
echo "=========================================="
# Check if any artifacts were downloaded
# Check if any artifacts were downloaded (merge-multiple puts files directly in path)
if [ ! -d "./downloaded-snapshots" ]; then
echo "No snapshot artifacts to merge"
echo "=========================================="
echo "MERGE COMPLETE"
echo "=========================================="
echo "Shards merged: 0"
echo "Files merged: 0"
exit 0
fi
@@ -222,37 +222,29 @@ jobs:
exit 1
fi
merged_count=0
# Count files to merge
file_count=$(find ./downloaded-snapshots -type f | wc -l)
# For each shard's changed files, copy them directly
for shard_dir in ./downloaded-snapshots/snapshots-shard-*/; do
if [ ! -d "$shard_dir" ]; then
continue
fi
if [ "$file_count" -eq 0 ]; then
echo "No snapshot files found in downloaded artifacts"
echo "=========================================="
echo "MERGE COMPLETE"
echo "=========================================="
echo "Files merged: 0"
exit 0
fi
shard_name=$(basename "$shard_dir")
file_count=$(find "$shard_dir" -type f | wc -l)
echo "Merging $file_count snapshot file(s)..."
if [ "$file_count" -eq 0 ]; then
echo " $shard_name: no files"
continue
fi
echo "Processing $shard_name ($file_count file(s))..."
# Copy files directly, preserving directory structure
# Since files are already in correct structure (no browser_tests/ prefix), just copy them all
cp -v -r "$shard_dir"* browser_tests/ 2>&1 | sed 's/^/ /'
merged_count=$((merged_count + 1))
echo " ✓ Merged"
echo ""
done
# Copy all files directly, preserving directory structure
# With merge-multiple: true, files are directly in ./downloaded-snapshots/ without shard subdirs
cp -v -r ./downloaded-snapshots/* browser_tests/ 2>&1 | sed 's/^/ /'
echo ""
echo "=========================================="
echo "MERGE COMPLETE"
echo "=========================================="
echo "Shards merged: $merged_count"
echo "Files merged: $file_count"
- name: Show changes
run: |
@@ -301,7 +293,7 @@ jobs:
echo "✓ Commit and push successful"
- name: Add Done Reaction
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
if: github.event_name == 'issue_comment' && steps.commit.outputs.has-changes == 'true'
with:
comment-id: ${{ needs.setup.outputs.comment-id }}

View File

@@ -20,13 +20,13 @@ jobs:
dist_tag: ${{ steps.dist.outputs.dist_tag }}
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: '24.x'
@@ -71,7 +71,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout merge commit
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}
fetch-depth: 2

View File

@@ -77,19 +77,19 @@ jobs:
fi
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ steps.resolve_ref.outputs.ref }}
fetch-depth: 1
persist-credentials: false
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: '24.x'
cache: 'pnpm'

View File

@@ -61,13 +61,13 @@ jobs:
steps:
- name: Checkout ComfyUI_frontend
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
path: frontend
- name: Checkout ComfyUI (sparse)
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
repository: Comfy-Org/ComfyUI
sparse-checkout: |
@@ -75,12 +75,12 @@ jobs:
path: comfyui
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: lts/*
@@ -169,7 +169,7 @@ jobs:
steps:
- name: Checkout ComfyUI fork
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
repository: ${{ inputs.comfyui_fork || 'Comfy-Org/ComfyUI' }}
token: ${{ secrets.PR_GH_TOKEN }}

View File

@@ -18,13 +18,13 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ secrets.PR_GH_TOKEN || secrets.GITHUB_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: 'lts/*'

View File

@@ -19,12 +19,12 @@ jobs:
is_prerelease: ${{ steps.check_prerelease.outputs.is_prerelease }}
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: 'lts/*'
cache: 'pnpm'
@@ -55,7 +55,7 @@ jobs:
pnpm build
pnpm zipdist
- name: Upload dist artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: dist-files
path: |
@@ -66,16 +66,13 @@ jobs:
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Download dist artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
name: dist-files
- name: Create release
id: create_release
uses: >-
softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@@ -98,13 +95,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Download dist artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
name: dist-files
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v6
with:
python-version: '3.x'
- name: Install build dependencies
@@ -119,8 +116,7 @@ jobs:
env:
COMFYUI_FRONTEND_VERSION: ${{ needs.build.outputs.version }}
- name: Publish pypi package
uses: >-
pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with:
password: ${{ secrets.PYPI_TOKEN }}
packages-dir: comfyui_frontend_package/dist
@@ -147,7 +143,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout merge commit
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}
fetch-depth: 2

View File

@@ -69,18 +69,18 @@ jobs:
fi
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ steps.resolve_ref.outputs.ref }}
fetch-depth: 1
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: 'lts/*'
cache: 'pnpm'

View File

@@ -15,12 +15,12 @@ jobs:
version: ${{ steps.current_version.outputs.version }}
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: 'lts/*'
cache: 'pnpm'
@@ -40,7 +40,7 @@ jobs:
pnpm build
pnpm zipdist
- name: Upload dist artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: dist-files
path: |
@@ -52,13 +52,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Download dist artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
name: dist-files
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v6
with:
python-version: '3.x'
- name: Install build dependencies
@@ -73,7 +73,7 @@ jobs:
env:
COMFYUI_FRONTEND_VERSION: ${{ format('{0}.dev{1}', needs.build.outputs.version, inputs.devVersion) }}
- name: Publish pypi package
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with:
password: ${{ secrets.PYPI_TOKEN }}
packages-dir: comfyui_frontend_package/dist

View File

@@ -65,7 +65,7 @@ jobs:
- name: Close stale nightly version bump PRs
if: github.event_name == 'schedule'
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
github-token: ${{ github.token }}
script: |
@@ -118,7 +118,7 @@ jobs:
core.info(`Closed ${closed.length} stale PR(s).`)
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ steps.prepared-inputs.outputs.branch }}
fetch-depth: 0
@@ -142,12 +142,12 @@ jobs:
echo "✅ Branch '$BRANCH' exists"
- name: Install pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: lts/*
@@ -180,7 +180,7 @@ jobs:
echo "capitalised=${CAPITALISED_TYPE@u}" >> "$GITHUB_OUTPUT"
- name: Create Pull Request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: '[release] Increment version to ${{ steps.bump-version.outputs.NEW_VERSION }}'

View File

@@ -29,7 +29,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ github.event.inputs.branch }}
fetch-depth: 0
@@ -51,12 +51,12 @@ jobs:
echo "✅ Branch '$BRANCH' exists"
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: '24.x'
cache: 'pnpm'
@@ -79,7 +79,7 @@ jobs:
echo "capitalised=${VERSION_TYPE@u}" >> $GITHUB_OUTPUT
- name: Create Pull Request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: '[release] Increment desktop-ui to ${{ steps.bump-version.outputs.NEW_VERSION }}'

View File

@@ -22,18 +22,18 @@ jobs:
timeout-minutes: 45
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
fetch-depth: 50
ref: main
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'pnpm'
@@ -49,7 +49,7 @@ jobs:
fi
- name: Run Claude Documentation Review
uses: anthropics/claude-code-action@v1.0.6
uses: anthropics/claude-code-action@ff34ce0ff04a470bd3fa56c1ef391c8f1c19f8e9 # v1.0.38
with:
prompt: |
Is all documentation still 100% accurate?
@@ -130,7 +130,7 @@ jobs:
- name: Create or Update Pull Request
if: steps.check_changes.outputs.has_changes == 'true'
uses: peter-evans/create-pull-request@v7
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: 'docs: weekly documentation accuracy update'

24
.pinact.yaml Normal file
View File

@@ -0,0 +1,24 @@
# pinact configuration
# https://github.com/suzuki-shunsuke/pinact
version: 3
files:
- pattern: .github/workflows/*.yaml
- pattern: .github/actions/**/*.yaml
# Actions that don't need SHA pinning (official GitHub actions are trusted)
ignore_actions:
- name: actions/cache
ref: v5
- name: actions/checkout
ref: v6
- name: actions/setup-node
ref: v6
- name: actions/setup-python
ref: v6
- name: actions/upload-artifact
ref: v6
- name: actions/download-artifact
ref: v7
- name: actions/github-script
ref: v8

View File

@@ -8,3 +8,6 @@ rules:
line-length: disable
document-start: disable
truthy: disable
comments:
min-spaces-from-content: 1

View File

@@ -7,7 +7,7 @@ import { webSocketFixture } from '../fixtures/ws.ts'
const test = mergeTests(comfyPageFixture, webSocketFixture)
test.describe('Actionbar', () => {
test.describe('Actionbar', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
})

View File

@@ -2,7 +2,7 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Bottom Panel Shortcuts', () => {
test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
})

View File

@@ -2,7 +2,7 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Browser tab title', () => {
test.describe('Browser tab title', { tag: '@smoke' }, () => {
test.describe('Beta Menu', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')

View File

@@ -19,7 +19,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Change Tracker', () => {
test.describe('Change Tracker', { tag: '@workflow' }, () => {
test.describe('Undo/Redo', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')

View File

@@ -151,7 +151,7 @@ const customColorPalettes: Record<string, Palette> = {
}
}
test.describe('Color Palette', () => {
test.describe('Color Palette', { tag: ['@screenshot', '@settings'] }, () => {
test('Can show custom color palette', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.CustomColorPalettes', customColorPalettes)
// Reload to apply the new setting. Setting Comfy.CustomColorPalettes directly
@@ -194,104 +194,110 @@ test.describe('Color Palette', () => {
})
})
test.describe('Node Color Adjustments', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.loadWorkflow('nodes/every_node_color')
})
test('should adjust opacity via node opacity setting', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Node.Opacity', 0.5)
// Drag mouse to force canvas to redraw
await comfyPage.page.mouse.move(0, 0)
await expect(comfyPage.canvas).toHaveScreenshot('node-opacity-0.5.png')
await comfyPage.setSetting('Comfy.Node.Opacity', 1.0)
await comfyPage.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')
await comfyPage.nextFrame()
const parsed = await (
await comfyPage.page.waitForFunction(
() => {
const workflow = localStorage.getItem('workflow')
if (!workflow) return null
try {
const data = JSON.parse(workflow)
return Array.isArray(data?.nodes) ? data : null
} catch {
return null
}
},
{ timeout: 3000 }
)
).jsonValue()
expect(parsed.nodes).toBeDefined()
expect(Array.isArray(parsed.nodes)).toBe(true)
for (const node of parsed.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.describe(
'Node Color Adjustments',
{ tag: ['@screenshot', '@settings'] },
() => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.loadWorkflow('nodes/every_node_color')
})
test('should adjust opacity via node opacity setting', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Node.Opacity', 0.5)
// Drag mouse to force canvas to redraw
await comfyPage.page.mouse.move(0, 0)
await expect(comfyPage.canvas).toHaveScreenshot('node-opacity-0.5.png')
await comfyPage.setSetting('Comfy.Node.Opacity', 1.0)
await comfyPage.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')
await comfyPage.setSetting('Comfy.Node.Opacity', 0.3)
const node = await comfyPage.getFirstNodeRef()
await node?.clickContextMenuOption('Colors')
await comfyPage.nextFrame()
const parsed = await (
await comfyPage.page.waitForFunction(
() => {
const workflow = localStorage.getItem('workflow')
if (!workflow) return null
try {
const data = JSON.parse(workflow)
return Array.isArray(data?.nodes) ? data : null
} catch {
return null
}
},
{ timeout: 3000 }
)
).jsonValue()
expect(parsed.nodes).toBeDefined()
expect(Array.isArray(parsed.nodes)).toBe(true)
for (const node of parsed.nodes) {
if (node.bgcolor) expect(node.bgcolor).not.toMatch(/hsla/)
if (node.color) expect(node.color).not.toMatch(/hsla/)
}
})
test('should persist color adjustments when changing custom node colors', async ({
test('should lighten node colors when switching to light theme', async ({
comfyPage
}) => {
await comfyPage.page
.locator('.litemenu-entry.submenu span:has-text("red")')
.click()
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'node-opacity-0.3-color-changed.png'
'node-lightened-colors.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'
)
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'
)
})
})
})
})
}
)

View File

@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Keybindings', () => {
test.describe('Keybindings', { tag: '@keyboard' }, () => {
test('Should execute command', async ({ comfyPage }) => {
await comfyPage.registerCommand('TestCommand', () => {
window['foo'] = true

View File

@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Copy Paste', () => {
test.describe('Copy Paste', { tag: ['@screenshot', '@workflow'] }, () => {
test('Can copy and paste node', async ({ comfyPage }) => {
await comfyPage.clickEmptyLatentNode()
await comfyPage.page.mouse.move(10, 10)

View File

@@ -22,7 +22,7 @@ async function verifyCustomIconSvg(iconElement: Locator) {
expect(decodedSvg).toContain("<svg xmlns='http://www.w3.org/2000/svg'")
}
test.describe('Custom Icons', () => {
test.describe('Custom Icons', { tag: '@settings' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
})

View File

@@ -8,7 +8,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Load workflow warning', () => {
test.describe('Load workflow warning', { tag: '@ui' }, () => {
test('Should display a warning when loading a workflow with missing nodes', async ({
comfyPage
}) => {

View File

@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('DOM Widget', () => {
test.describe('DOM Widget', { tag: '@widget' }, () => {
test('Collapsed multiline textarea is not visible', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/collapsed_multiline')
const textareaWidget = comfyPage.page.locator('.comfy-multiline-input')
@@ -29,12 +29,16 @@ test.describe('DOM Widget', () => {
await expect(lastMultiline).not.toBeVisible()
})
test('Position update when entering focus mode', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.executeCommand('Workspace.ToggleFocusMode')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('focus-mode-on.png')
})
test(
'Position update when entering focus mode',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.executeCommand('Workspace.ToggleFocusMode')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('focus-mode-on.png')
}
)
// No DOM widget should be created by creation of interim LGraphNode objects.
test('Copy node with DOM widget by dragging + alt', async ({ comfyPage }) => {

View File

@@ -6,40 +6,48 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Execution', () => {
test('Report error on unconnected slot', async ({ comfyPage }) => {
await comfyPage.disconnectEdge()
await comfyPage.clickEmptySpace()
test.describe('Execution', { tag: ['@smoke', '@workflow'] }, () => {
test(
'Report error on unconnected slot',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.disconnectEdge()
await comfyPage.clickEmptySpace()
await comfyPage.executeCommand('Comfy.QueuePrompt')
await expect(comfyPage.page.locator('.comfy-error-report')).toBeVisible()
await comfyPage.page.locator('.p-dialog-close-button').click()
await comfyPage.page.locator('.comfy-error-report').waitFor({
state: 'hidden'
})
await expect(comfyPage.canvas).toHaveScreenshot(
'execution-error-unconnected-slot.png'
)
})
await comfyPage.executeCommand('Comfy.QueuePrompt')
await expect(comfyPage.page.locator('.comfy-error-report')).toBeVisible()
await comfyPage.page.locator('.p-dialog-close-button').click()
await comfyPage.page.locator('.comfy-error-report').waitFor({
state: 'hidden'
})
await expect(comfyPage.canvas).toHaveScreenshot(
'execution-error-unconnected-slot.png'
)
}
)
})
test.describe('Execute to selected output nodes', () => {
test('Execute to selected output nodes', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('execution/partial_execution')
const input = await comfyPage.getNodeRefById(3)
const output1 = await comfyPage.getNodeRefById(1)
const output2 = await comfyPage.getNodeRefById(4)
expect(await (await input.getWidget(0)).getValue()).toBe('foo')
expect(await (await output1.getWidget(0)).getValue()).toBe('')
expect(await (await output2.getWidget(0)).getValue()).toBe('')
await output1.click('title')
await comfyPage.executeCommand('Comfy.QueueSelectedOutputNodes')
await expect(async () => {
test.describe(
'Execute to selected output nodes',
{ tag: ['@smoke', '@workflow'] },
() => {
test('Execute to selected output nodes', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('execution/partial_execution')
const input = await comfyPage.getNodeRefById(3)
const output1 = await comfyPage.getNodeRefById(1)
const output2 = await comfyPage.getNodeRefById(4)
expect(await (await input.getWidget(0)).getValue()).toBe('foo')
expect(await (await output1.getWidget(0)).getValue()).toBe('foo')
expect(await (await output1.getWidget(0)).getValue()).toBe('')
expect(await (await output2.getWidget(0)).getValue()).toBe('')
}).toPass({ timeout: 2_000 })
})
})
await output1.click('title')
await comfyPage.executeCommand('Comfy.QueueSelectedOutputNodes')
await expect(async () => {
expect(await (await input.getWidget(0)).getValue()).toBe('foo')
expect(await (await output1.getWidget(0)).getValue()).toBe('foo')
expect(await (await output2.getWidget(0)).getValue()).toBe('')
}).toPass({ timeout: 2_000 })
})
}
)

View File

@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Feature Flags', () => {
test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
test('Client and server exchange feature flags on connection', async ({
comfyPage
}) => {

View File

@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Graph', () => {
test.describe('Graph', { tag: ['@smoke', '@canvas'] }, () => {
// Should be able to fix link input slot index after swap the input order
// Ref: https://github.com/Comfy-Org/ComfyUI_frontend/issues/3348
test('Fix link input slots', async ({ comfyPage }) => {

View File

@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Graph Canvas Menu', () => {
test.describe('Graph Canvas Menu', { tag: ['@screenshot', '@canvas'] }, () => {
test.beforeEach(async ({ comfyPage }) => {
// Set link render mode to spline to make sure it's not affected by other tests'
// side effects.
@@ -15,29 +15,33 @@ test.describe('Graph Canvas Menu', () => {
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true)
})
test('Can toggle link visibility', async ({ comfyPage }) => {
const button = comfyPage.page.getByTestId('toggle-link-visibility-button')
await button.click()
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'canvas-with-hidden-links.png'
)
const hiddenLinkRenderMode = await comfyPage.page.evaluate(() => {
return window['LiteGraph'].HIDDEN_LINK
})
expect(await comfyPage.getSetting('Comfy.LinkRenderMode')).toBe(
hiddenLinkRenderMode
)
test(
'Can toggle link visibility',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
const button = comfyPage.page.getByTestId('toggle-link-visibility-button')
await button.click()
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'canvas-with-hidden-links.png'
)
const hiddenLinkRenderMode = await comfyPage.page.evaluate(() => {
return window['LiteGraph'].HIDDEN_LINK
})
expect(await comfyPage.getSetting('Comfy.LinkRenderMode')).toBe(
hiddenLinkRenderMode
)
await button.click()
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'canvas-with-visible-links.png'
)
expect(await comfyPage.getSetting('Comfy.LinkRenderMode')).not.toBe(
hiddenLinkRenderMode
)
})
await button.click()
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'canvas-with-visible-links.png'
)
expect(await comfyPage.getSetting('Comfy.LinkRenderMode')).not.toBe(
hiddenLinkRenderMode
)
}
)
test('Toggle minimap button is clickable and has correct test id', async ({
comfyPage

View File

@@ -8,7 +8,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Group Node', () => {
test.describe('Group Node', { tag: '@node' }, () => {
test.describe('Node library sidebar', () => {
const groupNodeName = 'DefautWorkflowGroupNode'
const groupNodeCategory = 'group nodes>workflow'
@@ -89,16 +89,20 @@ test.describe('Group Node', () => {
// does not have a v-model on the query, so we cannot observe the raw
// query update, and thus cannot set the spinning state between the raw query
// update and the debounced search update.
test.skip('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.skip(
'Can be added to canvas using search',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
const groupNodeName = 'DefautWorkflowGroupNode'
await comfyPage.convertAllNodesToGroupNode(groupNodeName)
await comfyPage.doubleClickCanvas()
await comfyPage.nextFrame()
await comfyPage.searchBox.fillAndSelectFirstNode(groupNodeName)
await expect(comfyPage.canvas).toHaveScreenshot(
'group-node-copy-added-from-search.png'
)
}
)
test('Displays tooltip on title hover', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.EnableTooltips', true)

View File

@@ -13,7 +13,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Item Interaction', () => {
test.describe('Item Interaction', { tag: ['@screenshot', '@node'] }, () => {
test('Can select/delete all items', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('groups/mixed_graph_items')
await comfyPage.canvas.press('Control+a')
@@ -60,13 +60,17 @@ test.describe('Node Interaction', () => {
})
})
test('@2x Can highlight selected', async ({ comfyPage }) => {
await expect(comfyPage.canvas).toHaveScreenshot('default.png')
await comfyPage.clickTextEncodeNode1()
await expect(comfyPage.canvas).toHaveScreenshot('selected-node1.png')
await comfyPage.clickTextEncodeNode2()
await expect(comfyPage.canvas).toHaveScreenshot('selected-node2.png')
})
test(
'@2x Can highlight selected',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await expect(comfyPage.canvas).toHaveScreenshot('default.png')
await comfyPage.clickTextEncodeNode1()
await expect(comfyPage.canvas).toHaveScreenshot('selected-node1.png')
await comfyPage.clickTextEncodeNode2()
await expect(comfyPage.canvas).toHaveScreenshot('selected-node2.png')
}
)
const dragSelectNodes = async (
comfyPage: ComfyPage,
@@ -150,12 +154,12 @@ test.describe('Node Interaction', () => {
})
})
test('Can drag node', async ({ comfyPage }) => {
test('Can drag node', { tag: '@screenshot' }, async ({ comfyPage }) => {
await comfyPage.dragNode2()
await expect(comfyPage.canvas).toHaveScreenshot('dragged-node1.png')
})
test.describe('Edge Interaction', () => {
test.describe('Edge Interaction', { tag: '@screenshot' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.LinkRelease.Action', 'no action')
await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'no action')
@@ -222,12 +226,18 @@ test.describe('Node Interaction', () => {
})
})
test('Can adjust widget value', async ({ comfyPage }) => {
await comfyPage.adjustWidgetValue()
await expect(comfyPage.canvas).toHaveScreenshot('adjusted-widget-value.png')
})
test(
'Can adjust widget value',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.adjustWidgetValue()
await expect(comfyPage.canvas).toHaveScreenshot(
'adjusted-widget-value.png'
)
}
)
test('Link snap to slot', async ({ comfyPage }) => {
test('Link snap to slot', { tag: '@screenshot' }, async ({ comfyPage }) => {
await comfyPage.loadWorkflow('links/snap_to_slot')
await expect(comfyPage.canvas).toHaveScreenshot('snap_to_slot.png')
@@ -244,57 +254,67 @@ test.describe('Node Interaction', () => {
await expect(comfyPage.canvas).toHaveScreenshot('snap_to_slot_linked.png')
})
test('Can batch move links by drag with shift', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('links/batch_move_links')
await expect(comfyPage.canvas).toHaveScreenshot('batch_move_links.png')
test(
'Can batch move links by drag with shift',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.loadWorkflow('links/batch_move_links')
await expect(comfyPage.canvas).toHaveScreenshot('batch_move_links.png')
const outputSlot1Pos = {
x: 304,
y: 127
const outputSlot1Pos = {
x: 304,
y: 127
}
const outputSlot2Pos = {
x: 307,
y: 310
}
await comfyPage.page.keyboard.down('Shift')
await comfyPage.dragAndDrop(outputSlot1Pos, outputSlot2Pos)
await comfyPage.page.keyboard.up('Shift')
await expect(comfyPage.canvas).toHaveScreenshot(
'batch_move_links_moved.png'
)
}
const outputSlot2Pos = {
x: 307,
y: 310
)
test(
'Can batch disconnect links with ctrl+alt+click',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
const loadCheckpointClipSlotPos = {
x: 332,
y: 508
}
await comfyPage.canvas.click({
modifiers: ['Control', 'Alt'],
position: loadCheckpointClipSlotPos
})
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'batch-disconnect-links-disconnected.png'
)
}
)
await comfyPage.page.keyboard.down('Shift')
await comfyPage.dragAndDrop(outputSlot1Pos, outputSlot2Pos)
await comfyPage.page.keyboard.up('Shift')
await expect(comfyPage.canvas).toHaveScreenshot(
'batch_move_links_moved.png'
)
})
test('Can batch disconnect links with ctrl+alt+click', async ({
comfyPage
}) => {
const loadCheckpointClipSlotPos = {
x: 332,
y: 508
test(
'Can toggle dom widget node open/closed',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await expect(comfyPage.canvas).toHaveScreenshot('default.png')
await comfyPage.clickTextEncodeNodeToggler()
await expect(comfyPage.canvas).toHaveScreenshot(
'text-encode-toggled-off.png'
)
await comfyPage.delay(1000)
await comfyPage.clickTextEncodeNodeToggler()
await expect(comfyPage.canvas).toHaveScreenshot(
'text-encode-toggled-back-open.png'
)
}
await comfyPage.canvas.click({
modifiers: ['Control', 'Alt'],
position: loadCheckpointClipSlotPos
})
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'batch-disconnect-links-disconnected.png'
)
})
test('Can toggle dom widget node open/closed', async ({ comfyPage }) => {
await expect(comfyPage.canvas).toHaveScreenshot('default.png')
await comfyPage.clickTextEncodeNodeToggler()
await expect(comfyPage.canvas).toHaveScreenshot(
'text-encode-toggled-off.png'
)
await comfyPage.delay(1000)
await comfyPage.clickTextEncodeNodeToggler()
await expect(comfyPage.canvas).toHaveScreenshot(
'text-encode-toggled-back-open.png'
)
})
)
test('Can close prompt dialog with canvas click (number widget)', async ({
comfyPage
@@ -341,19 +361,23 @@ test.describe('Node Interaction', () => {
await expect(legacyPrompt).toBeHidden()
})
test('Can double click node title to edit', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('nodes/single_ksampler')
await comfyPage.canvas.dblclick({
position: {
x: 50,
y: 10
},
delay: 5
})
await comfyPage.page.keyboard.type('Hello World')
await comfyPage.page.keyboard.press('Enter')
await expect(comfyPage.canvas).toHaveScreenshot('node-title-edited.png')
})
test(
'Can double click node title to edit',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.loadWorkflow('nodes/single_ksampler')
await comfyPage.canvas.dblclick({
position: {
x: 50,
y: 10
},
delay: 5
})
await comfyPage.page.keyboard.type('Hello World')
await comfyPage.page.keyboard.press('Enter')
await expect(comfyPage.canvas).toHaveScreenshot('node-title-edited.png')
}
)
test('Double click node body does not trigger edit', async ({
comfyPage
@@ -369,29 +393,41 @@ test.describe('Node Interaction', () => {
expect(await comfyPage.page.locator('.node-title-editor').count()).toBe(0)
})
test('Can group selected nodes', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.GroupSelectedNodes.Padding', 10)
await comfyPage.select2Nodes()
await comfyPage.page.keyboard.down('Control')
await comfyPage.page.keyboard.press('KeyG')
await comfyPage.page.keyboard.up('Control')
await comfyPage.nextFrame()
// Confirm group title
await comfyPage.page.keyboard.press('Enter')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('group-selected-nodes.png')
})
test(
'Can group selected nodes',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.GroupSelectedNodes.Padding', 10)
await comfyPage.select2Nodes()
await comfyPage.page.keyboard.down('Control')
await comfyPage.page.keyboard.press('KeyG')
await comfyPage.page.keyboard.up('Control')
await comfyPage.nextFrame()
// Confirm group title
await comfyPage.page.keyboard.press('Enter')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'group-selected-nodes.png'
)
}
)
test('Can fit group to contents', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('groups/oversized_group')
await comfyPage.ctrlA()
await comfyPage.nextFrame()
await comfyPage.executeCommand('Comfy.Graph.FitGroupToContents')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('group-fit-to-contents.png')
})
test(
'Can fit group to contents',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.loadWorkflow('groups/oversized_group')
await comfyPage.ctrlA()
await comfyPage.nextFrame()
await comfyPage.executeCommand('Comfy.Graph.FitGroupToContents')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'group-fit-to-contents.png'
)
}
)
test('Can pin/unpin nodes', async ({ comfyPage }) => {
test('Can pin/unpin nodes', { tag: '@screenshot' }, async ({ comfyPage }) => {
await comfyPage.select2Nodes()
await comfyPage.executeCommand('Comfy.Canvas.ToggleSelectedNodes.Pin')
await comfyPage.nextFrame()
@@ -401,20 +437,22 @@ test.describe('Node Interaction', () => {
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(
'Can bypass/unbypass nodes with keyboard shortcut',
{ tag: '@screenshot' },
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.describe('Group Interaction', { tag: '@screenshot' }, () => {
test('Can double click group title to edit', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('groups/single_group')
await comfyPage.canvas.dblclick({
@@ -430,7 +468,7 @@ test.describe('Group Interaction', () => {
})
})
test.describe('Canvas Interaction', () => {
test.describe('Canvas Interaction', { tag: '@screenshot' }, () => {
test('Can zoom in/out', async ({ comfyPage }) => {
await comfyPage.zoom(-100)
await expect(comfyPage.canvas).toHaveScreenshot('zoomed-in.png')
@@ -632,7 +670,7 @@ test.describe('Widget Interaction', () => {
})
})
test.describe('Load workflow', () => {
test.describe('Load workflow', { tag: '@screenshot' }, () => {
test('Can load workflow with string node id', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('nodes/string_node_id')
await expect(comfyPage.canvas).toHaveScreenshot('string_node_id.png')
@@ -824,7 +862,7 @@ test.describe('Viewport settings', () => {
})
})
test.describe('Canvas Navigation', () => {
test.describe('Canvas Navigation', { tag: '@screenshot' }, () => {
test.describe('Legacy Mode', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Canvas.NavigationMode', 'legacy')

View File

@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Keybindings', () => {
test.describe('Keybindings', { tag: '@keyboard' }, () => {
test('Should not trigger non-modifier keybinding when typing in input fields', async ({
comfyPage
}) => {

View File

@@ -14,7 +14,7 @@ function listenForEvent(): Promise<Event> {
})
}
test.describe('Canvas Event', () => {
test.describe('Canvas Event', { tag: '@canvas' }, () => {
test('Emit litegraph:canvas empty-release', async ({ comfyPage }) => {
const eventPromise = comfyPage.page.evaluate(listenForEvent)
const disconnectPromise = comfyPage.disconnectEdge()

View File

@@ -6,46 +6,50 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Load Workflow in Media', () => {
const fileNames = [
'workflow.webp',
'edited_workflow.webp',
'no_workflow.webp',
'large_workflow.webp',
'workflow_prompt_parameters.png',
'workflow.webm',
// Skipped due to 3d widget unstable visual result.
// 3d widget shows grid after fully loaded.
// 'workflow.glb',
'workflow.mp4',
'workflow.mov',
'workflow.m4v',
'workflow.svg'
// TODO: Re-enable after fixing test asset to use core nodes only
// Currently opens missing nodes dialog which is outside scope of AVIF loading functionality
// 'workflow.avif'
]
fileNames.forEach(async (fileName) => {
test(`Load workflow in ${fileName} (drop from filesystem)`, async ({
comfyPage
}) => {
await comfyPage.dragAndDropFile(`workflowInMedia/${fileName}`)
await expect(comfyPage.canvas).toHaveScreenshot(`${fileName}.png`)
test.describe(
'Load Workflow in Media',
{ tag: ['@screenshot', '@workflow'] },
() => {
const fileNames = [
'workflow.webp',
'edited_workflow.webp',
'no_workflow.webp',
'large_workflow.webp',
'workflow_prompt_parameters.png',
'workflow.webm',
// Skipped due to 3d widget unstable visual result.
// 3d widget shows grid after fully loaded.
// 'workflow.glb',
'workflow.mp4',
'workflow.mov',
'workflow.m4v',
'workflow.svg'
// TODO: Re-enable after fixing test asset to use core nodes only
// Currently opens missing nodes dialog which is outside scope of AVIF loading functionality
// 'workflow.avif'
]
fileNames.forEach(async (fileName) => {
test(`Load workflow in ${fileName} (drop from filesystem)`, async ({
comfyPage
}) => {
await comfyPage.dragAndDropFile(`workflowInMedia/${fileName}`)
await expect(comfyPage.canvas).toHaveScreenshot(`${fileName}.png`)
})
})
})
const urls = [
'https://comfyanonymous.github.io/ComfyUI_examples/hidream/hidream_dev_example.png'
]
urls.forEach(async (url) => {
test(`Load workflow from URL ${url} (drop from different browser tabs)`, async ({
comfyPage
}) => {
await comfyPage.dragAndDropURL(url)
const readableName = url.split('/').pop()
await expect(comfyPage.canvas).toHaveScreenshot(
`dropped_workflow_url_${readableName}.png`
)
const urls = [
'https://comfyanonymous.github.io/ComfyUI_examples/hidream/hidream_dev_example.png'
]
urls.forEach(async (url) => {
test(`Load workflow from URL ${url} (drop from different browser tabs)`, async ({
comfyPage
}) => {
await comfyPage.dragAndDropURL(url)
const readableName = url.split('/').pop()
await expect(comfyPage.canvas).toHaveScreenshot(
`dropped_workflow_url_${readableName}.png`
)
})
})
})
})
}
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 135 KiB

View File

@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('LOD Threshold', () => {
test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => {
test('Should switch to low quality mode at correct zoom threshold', async ({
comfyPage
}) => {
@@ -149,53 +149,55 @@ test.describe('LOD Threshold', () => {
expect(state.scale).toBeLessThan(0.2) // Very zoomed out
})
test('Should show visual difference between LOD on and off', async ({
comfyPage
}) => {
// Load a workflow with text-heavy nodes for clear visual difference
await comfyPage.loadWorkflow('default')
test(
'Should show visual difference between LOD on and off',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
// Load a workflow with text-heavy nodes for clear visual difference
await comfyPage.loadWorkflow('default')
// Set zoom level clearly below the threshold to ensure LOD activates
const targetZoom = 0.4 // Well below default threshold of ~0.571
// Set zoom level clearly below the threshold to ensure LOD activates
const targetZoom = 0.4 // Well below default threshold of ~0.571
// Zoom to target level
await comfyPage.page.evaluate((zoom) => {
window['app'].canvas.ds.scale = zoom
window['app'].canvas.setDirty(true, true)
}, targetZoom)
await comfyPage.nextFrame()
// Zoom to target level
await comfyPage.page.evaluate((zoom) => {
window['app'].canvas.ds.scale = zoom
window['app'].canvas.setDirty(true, true)
}, targetZoom)
await comfyPage.nextFrame()
// Take snapshot with LOD active (default 8px setting)
await expect(comfyPage.canvas).toHaveScreenshot(
'lod-comparison-low-quality.png'
)
// Take snapshot with LOD active (default 8px setting)
await expect(comfyPage.canvas).toHaveScreenshot(
'lod-comparison-low-quality.png'
)
const lowQualityState = await comfyPage.page.evaluate(() => {
const canvas = window['app'].canvas
return {
lowQuality: canvas.low_quality,
scale: canvas.ds.scale
}
})
expect(lowQualityState.lowQuality).toBe(true)
const lowQualityState = await comfyPage.page.evaluate(() => {
const canvas = window['app'].canvas
return {
lowQuality: canvas.low_quality,
scale: canvas.ds.scale
}
})
expect(lowQualityState.lowQuality).toBe(true)
// Disable LOD to see high quality at same zoom
await comfyPage.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 0)
await comfyPage.nextFrame()
// Disable LOD to see high quality at same zoom
await comfyPage.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 0)
await comfyPage.nextFrame()
// Take snapshot with LOD disabled (full quality at same zoom)
await expect(comfyPage.canvas).toHaveScreenshot(
'lod-comparison-high-quality.png'
)
// Take snapshot with LOD disabled (full quality at same zoom)
await expect(comfyPage.canvas).toHaveScreenshot(
'lod-comparison-high-quality.png'
)
const highQualityState = await comfyPage.page.evaluate(() => {
const canvas = window['app'].canvas
return {
lowQuality: canvas.low_quality,
scale: canvas.ds.scale
}
})
expect(highQualityState.lowQuality).toBe(false)
expect(highQualityState.scale).toBeCloseTo(targetZoom, 2)
})
const highQualityState = await comfyPage.page.evaluate(() => {
const canvas = window['app'].canvas
return {
lowQuality: canvas.low_quality,
scale: canvas.ds.scale
}
})
expect(highQualityState.lowQuality).toBe(false)
expect(highQualityState.scale).toBeCloseTo(targetZoom, 2)
}
)
})

View File

@@ -2,7 +2,7 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Menu', () => {
test.describe('Menu', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
})

View File

@@ -2,7 +2,7 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Minimap', () => {
test.describe('Minimap', { tag: '@canvas' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting('Comfy.Minimap.Visible', true)

View File

@@ -1,35 +1,39 @@
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { expect } from '@playwright/test'
test.describe('Mobile Baseline Snapshots', () => {
test('@mobile empty canvas', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.ConfirmClear', false)
await comfyPage.executeCommand('Comfy.ClearWorkflow')
await expect(async () => {
expect(await comfyPage.getGraphNodesCount()).toBe(0)
}).toPass({ timeout: 256 })
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('mobile-empty-canvas.png')
})
test.describe(
'Mobile Baseline Snapshots',
{ tag: ['@mobile', '@screenshot'] },
() => {
test('@mobile empty canvas', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.ConfirmClear', false)
await comfyPage.executeCommand('Comfy.ClearWorkflow')
await expect(async () => {
expect(await comfyPage.getGraphNodesCount()).toBe(0)
}).toPass({ timeout: 256 })
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('mobile-empty-canvas.png')
})
test('@mobile default workflow', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('default')
await expect(comfyPage.canvas).toHaveScreenshot(
'mobile-default-workflow.png'
)
})
test('@mobile default workflow', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('default')
await expect(comfyPage.canvas).toHaveScreenshot(
'mobile-default-workflow.png'
)
})
test('@mobile settings dialog', async ({ comfyPage }) => {
await comfyPage.settingDialog.open()
await comfyPage.nextFrame()
test('@mobile settings dialog', async ({ comfyPage }) => {
await comfyPage.settingDialog.open()
await comfyPage.nextFrame()
await expect(comfyPage.settingDialog.root).toHaveScreenshot(
'mobile-settings-dialog.png',
{
mask: [
comfyPage.settingDialog.root.getByTestId('current-user-indicator')
]
}
)
})
})
await expect(comfyPage.settingDialog.root).toHaveScreenshot(
'mobile-settings-dialog.png',
{
mask: [
comfyPage.settingDialog.root.getByTestId('current-user-indicator')
]
}
)
})
}
)

View File

@@ -8,7 +8,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Node Badge', () => {
test.describe('Node Badge', { tag: ['@screenshot', '@smoke', '@node'] }, () => {
test('Can add badge', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
const LGraphBadge = window['LGraphBadge']
@@ -66,50 +66,60 @@ test.describe('Node Badge', () => {
})
})
test.describe('Node source badge', () => {
Object.values(NodeBadgeMode).forEach(async (mode) => {
test(`Shows node badges (${mode})`, async ({ comfyPage }) => {
// Execution error workflow has both custom node and core node.
await comfyPage.loadWorkflow('nodes/execution_error')
await comfyPage.setSetting('Comfy.NodeBadge.NodeSourceBadgeMode', mode)
await comfyPage.setSetting('Comfy.NodeBadge.NodeIdBadgeMode', mode)
await comfyPage.nextFrame()
await comfyPage.resetView()
await expect(comfyPage.canvas).toHaveScreenshot(`node-badge-${mode}.png`)
test.describe(
'Node source badge',
{ tag: ['@screenshot', '@smoke', '@node'] },
() => {
Object.values(NodeBadgeMode).forEach(async (mode) => {
test(`Shows node badges (${mode})`, async ({ comfyPage }) => {
// Execution error workflow has both custom node and core node.
await comfyPage.loadWorkflow('nodes/execution_error')
await comfyPage.setSetting('Comfy.NodeBadge.NodeSourceBadgeMode', mode)
await comfyPage.setSetting('Comfy.NodeBadge.NodeIdBadgeMode', mode)
await comfyPage.nextFrame()
await comfyPage.resetView()
await expect(comfyPage.canvas).toHaveScreenshot(
`node-badge-${mode}.png`
)
})
})
})
})
}
)
test.describe('Node badge color', () => {
test('Can show node badge with unknown color palette', async ({
comfyPage
}) => {
await comfyPage.setSetting(
'Comfy.NodeBadge.NodeIdBadgeMode',
NodeBadgeMode.ShowAll
)
await comfyPage.setSetting('Comfy.ColorPalette', 'unknown')
await comfyPage.nextFrame()
// Click empty space to trigger canvas re-render.
await comfyPage.clickEmptySpace()
await expect(comfyPage.canvas).toHaveScreenshot(
'node-badge-unknown-color-palette.png'
)
})
test.describe(
'Node badge color',
{ tag: ['@screenshot', '@smoke', '@node'] },
() => {
test('Can show node badge with unknown color palette', async ({
comfyPage
}) => {
await comfyPage.setSetting(
'Comfy.NodeBadge.NodeIdBadgeMode',
NodeBadgeMode.ShowAll
)
await comfyPage.setSetting('Comfy.ColorPalette', 'unknown')
await comfyPage.nextFrame()
// Click empty space to trigger canvas re-render.
await comfyPage.clickEmptySpace()
await expect(comfyPage.canvas).toHaveScreenshot(
'node-badge-unknown-color-palette.png'
)
})
test('Can show node badge with light color palette', async ({
comfyPage
}) => {
await comfyPage.setSetting(
'Comfy.NodeBadge.NodeIdBadgeMode',
NodeBadgeMode.ShowAll
)
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
await comfyPage.nextFrame()
// Click empty space to trigger canvas re-render.
await comfyPage.clickEmptySpace()
await expect(comfyPage.canvas).toHaveScreenshot(
'node-badge-light-color-palette.png'
)
})
})
test('Can show node badge with light color palette', async ({
comfyPage
}) => {
await comfyPage.setSetting(
'Comfy.NodeBadge.NodeIdBadgeMode',
NodeBadgeMode.ShowAll
)
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
await comfyPage.nextFrame()
// Click empty space to trigger canvas re-render.
await comfyPage.clickEmptySpace()
await expect(comfyPage.canvas).toHaveScreenshot(
'node-badge-light-color-palette.png'
)
})
}
)

View File

@@ -8,7 +8,7 @@ test.beforeEach(async ({ comfyPage }) => {
// If an input is optional by node definition, it should be shown as
// a hollow circle no matter what shape it was defined in the workflow JSON.
test.describe('Optional input', () => {
test.describe('Optional input', { tag: ['@screenshot', '@node'] }, () => {
test('No shape specified', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/optional_input_no_shape')
await expect(comfyPage.canvas).toHaveScreenshot('optional_input.png')

View File

@@ -23,7 +23,7 @@ async function selectNodeWithPan(comfyPage: ComfyPage, nodeRef: NodeReference) {
await nodeRef.click('title')
}
test.describe('Node Help', () => {
test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setup()
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')

View File

@@ -7,7 +7,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Node search box', () => {
test.describe('Node search box', { tag: '@node' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.LinkRelease.Action', 'search box')
await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'search box')
@@ -46,14 +46,14 @@ test.describe('Node search box', () => {
await expect(comfyPage.searchBox.input).toBeVisible()
})
test('Can add node', async ({ comfyPage }) => {
test('Can add node', { tag: '@screenshot' }, async ({ comfyPage }) => {
await comfyPage.doubleClickCanvas()
await expect(comfyPage.searchBox.input).toHaveCount(1)
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler')
await expect(comfyPage.canvas).toHaveScreenshot('added-node.png')
})
test('Can auto link node', async ({ comfyPage }) => {
test('Can auto link node', { tag: '@screenshot' }, async ({ comfyPage }) => {
await comfyPage.disconnectEdge()
// Select the second item as the first item is always reroute
await comfyPage.searchBox.fillAndSelectFirstNode('CLIPTextEncode', {
@@ -62,41 +62,47 @@ test.describe('Node search box', () => {
await expect(comfyPage.canvas).toHaveScreenshot('auto-linked-node.png')
})
test('Can auto link batch moved node', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('links/batch_move_links')
test(
'Can auto link batch moved node',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.loadWorkflow('links/batch_move_links')
const outputSlot1Pos = {
x: 304,
y: 127
const outputSlot1Pos = {
x: 304,
y: 127
}
const emptySpacePos = {
x: 5,
y: 5
}
await comfyPage.page.keyboard.down('Shift')
await comfyPage.dragAndDrop(outputSlot1Pos, emptySpacePos)
await comfyPage.page.keyboard.up('Shift')
// Select the second item as the first item is always reroute
await comfyPage.searchBox.fillAndSelectFirstNode('Load Checkpoint', {
suggestionIndex: 0
})
await expect(comfyPage.canvas).toHaveScreenshot(
'auto-linked-node-batch.png'
)
}
const emptySpacePos = {
x: 5,
y: 5
)
test(
'Link release connecting to node with no slots',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.disconnectEdge()
await expect(comfyPage.searchBox.input).toHaveCount(1)
await comfyPage.page.locator('.p-chip-remove-icon').click()
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler')
await expect(comfyPage.canvas).toHaveScreenshot(
'added-node-no-connection.png'
)
}
await comfyPage.page.keyboard.down('Shift')
await comfyPage.dragAndDrop(outputSlot1Pos, emptySpacePos)
await comfyPage.page.keyboard.up('Shift')
// Select the second item as the first item is always reroute
await comfyPage.searchBox.fillAndSelectFirstNode('Load Checkpoint', {
suggestionIndex: 0
})
await expect(comfyPage.canvas).toHaveScreenshot(
'auto-linked-node-batch.png'
)
})
test('Link release connecting to node with no slots', async ({
comfyPage
}) => {
await comfyPage.disconnectEdge()
await expect(comfyPage.searchBox.input).toHaveCount(1)
await comfyPage.page.locator('.p-chip-remove-icon').click()
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler')
await expect(comfyPage.canvas).toHaveScreenshot(
'added-node-no-connection.png'
)
})
)
test('Has correct aria-labels on search results', async ({ comfyPage }) => {
const node = 'Load Checkpoint'
@@ -251,40 +257,45 @@ test.describe('Node search box', () => {
})
})
test.describe('Release context menu', () => {
test.describe('Release context menu', { tag: '@node' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.LinkRelease.Action', 'context menu')
await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'search box')
await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default')
})
test('Can trigger on link release', async ({ comfyPage }) => {
await comfyPage.disconnectEdge()
const contextMenu = comfyPage.page.locator('.litecontextmenu')
// Wait for context menu with correct title (slot name | slot type)
// The title shows the output slot name and type from the disconnected link
await expect(contextMenu.locator('.litemenu-title')).toContainText(
'CLIP | CLIP'
)
await comfyPage.page.mouse.move(10, 10)
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'link-release-context-menu.png'
)
})
test(
'Can trigger on link release',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.disconnectEdge()
const contextMenu = comfyPage.page.locator('.litecontextmenu')
// Wait for context menu with correct title (slot name | slot type)
// The title shows the output slot name and type from the disconnected link
await expect(contextMenu.locator('.litemenu-title')).toContainText(
'CLIP | CLIP'
)
await comfyPage.page.mouse.move(10, 10)
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'link-release-context-menu.png'
)
}
)
test('Can search and add node from context menu', async ({
comfyPage,
comfyMouse
}) => {
await comfyPage.disconnectEdge()
await comfyMouse.move({ x: 10, y: 10 })
await comfyPage.clickContextMenuItem('Search')
await comfyPage.searchBox.fillAndSelectFirstNode('CLIP Prompt')
await expect(comfyPage.canvas).toHaveScreenshot(
'link-context-menu-search.png'
)
})
test(
'Can search and add node from context menu',
{ tag: '@screenshot' },
async ({ comfyPage, comfyMouse }) => {
await comfyPage.disconnectEdge()
await comfyMouse.move({ x: 10, y: 10 })
await comfyPage.clickContextMenuItem('Search')
await comfyPage.searchBox.fillAndSelectFirstNode('CLIP Prompt')
await expect(comfyPage.canvas).toHaveScreenshot(
'link-context-menu-search.png'
)
}
)
test('Existing user (pre-1.24.1) gets context menu by default on link release', async ({
comfyPage

View File

@@ -6,8 +6,8 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Note Node', () => {
test('Can load node nodes', async ({ comfyPage }) => {
test.describe('Note Node', { tag: '@node' }, () => {
test('Can load node nodes', { tag: '@screenshot' }, async ({ comfyPage }) => {
await comfyPage.loadWorkflow('nodes/note_nodes')
await expect(comfyPage.canvas).toHaveScreenshot('note_nodes.png')
})

View File

@@ -7,7 +7,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Primitive Node', () => {
test.describe('Primitive Node', { tag: ['@screenshot', '@node'] }, () => {
test('Can load with correct size', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('primitive/primitive_node')
await expect(comfyPage.canvas).toHaveScreenshot('primitive_node.png')

View File

@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Record Audio Node', () => {
test.describe('Record Audio Node', { tag: '@screenshot' }, () => {
test('should add a record audio node and take a screenshot', async ({
comfyPage
}) => {

View File

@@ -3,7 +3,7 @@ import { expect } from '@playwright/test'
import type { ComfyPage } from '../fixtures/ComfyPage'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Remote COMBO Widget', () => {
test.describe('Remote COMBO Widget', { tag: '@widget' }, () => {
const mockOptions = ['d', 'c', 'b', 'a']
const addRemoteWidgetNode = async (

View File

@@ -3,7 +3,7 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { getMiddlePoint } from '../fixtures/utils/litegraphUtils'
test.describe('Reroute Node', () => {
test.describe('Reroute Node', { tag: ['@screenshot', '@node'] }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
})
@@ -38,92 +38,96 @@ test.describe('Reroute Node', () => {
})
})
test.describe('LiteGraph Native Reroute Node', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.setSetting('LiteGraph.Reroute.SplineOffset', 80)
})
test.describe(
'LiteGraph Native Reroute Node',
{ tag: ['@screenshot', '@node'] },
() => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.setSetting('LiteGraph.Reroute.SplineOffset', 80)
})
test('loads from workflow', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('reroute/native_reroute')
await expect(comfyPage.canvas).toHaveScreenshot('native_reroute.png')
})
test('loads from workflow', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('reroute/native_reroute')
await expect(comfyPage.canvas).toHaveScreenshot('native_reroute.png')
})
test('@2x @0.5x Can add reroute by alt clicking on link', async ({
comfyPage
}) => {
const loadCheckpointNode = (
await comfyPage.getNodeRefsByTitle('Load Checkpoint')
)[0]
const clipEncodeNode = (
await comfyPage.getNodeRefsByTitle('CLIP Text Encode (Prompt)')
)[0]
test('@2x @0.5x Can add reroute by alt clicking on link', async ({
comfyPage
}) => {
const loadCheckpointNode = (
await comfyPage.getNodeRefsByTitle('Load Checkpoint')
)[0]
const clipEncodeNode = (
await comfyPage.getNodeRefsByTitle('CLIP Text Encode (Prompt)')
)[0]
const slot1 = await loadCheckpointNode.getOutput(1)
const slot2 = await clipEncodeNode.getInput(0)
const middlePoint = getMiddlePoint(
await slot1.getPosition(),
await slot2.getPosition()
)
const slot1 = await loadCheckpointNode.getOutput(1)
const slot2 = await clipEncodeNode.getInput(0)
const middlePoint = getMiddlePoint(
await slot1.getPosition(),
await slot2.getPosition()
)
await comfyPage.page.keyboard.down('Alt')
await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y)
await comfyPage.page.keyboard.up('Alt')
await comfyPage.page.keyboard.down('Alt')
await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y)
await comfyPage.page.keyboard.up('Alt')
await expect(comfyPage.canvas).toHaveScreenshot(
'native_reroute_alt_click.png'
)
})
await expect(comfyPage.canvas).toHaveScreenshot(
'native_reroute_alt_click.png'
)
})
test('Can add reroute by clicking middle of link context menu', async ({
comfyPage
}) => {
const loadCheckpointNode = (
await comfyPage.getNodeRefsByTitle('Load Checkpoint')
)[0]
const clipEncodeNode = (
await comfyPage.getNodeRefsByTitle('CLIP Text Encode (Prompt)')
)[0]
test('Can add reroute by clicking middle of link context menu', async ({
comfyPage
}) => {
const loadCheckpointNode = (
await comfyPage.getNodeRefsByTitle('Load Checkpoint')
)[0]
const clipEncodeNode = (
await comfyPage.getNodeRefsByTitle('CLIP Text Encode (Prompt)')
)[0]
const slot1 = await loadCheckpointNode.getOutput(1)
const slot2 = await clipEncodeNode.getInput(0)
const middlePoint = getMiddlePoint(
await slot1.getPosition(),
await slot2.getPosition()
)
const slot1 = await loadCheckpointNode.getOutput(1)
const slot2 = await clipEncodeNode.getInput(0)
const middlePoint = getMiddlePoint(
await slot1.getPosition(),
await slot2.getPosition()
)
await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y)
await comfyPage.page
.locator('.litecontextmenu .litemenu-entry', { hasText: 'Add Reroute' })
.click()
await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y)
await comfyPage.page
.locator('.litecontextmenu .litemenu-entry', { hasText: 'Add Reroute' })
.click()
await expect(comfyPage.canvas).toHaveScreenshot(
'native_reroute_context_menu.png'
)
})
await expect(comfyPage.canvas).toHaveScreenshot(
'native_reroute_context_menu.png'
)
})
test('Can delete link that is connected to two reroutes', async ({
comfyPage
}) => {
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/4695
await comfyPage.loadWorkflow(
'reroute/single-native-reroute-default-workflow'
)
test('Can delete link that is connected to two reroutes', async ({
comfyPage
}) => {
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/4695
await comfyPage.loadWorkflow(
'reroute/single-native-reroute-default-workflow'
)
// To find the clickable midpoint button, we use the hardcoded value from the browser logs
// since the link is a bezier curve and not a straight line.
const middlePoint = { x: 359.4188232421875, y: 468.7716979980469 }
// To find the clickable midpoint button, we use the hardcoded value from the browser logs
// since the link is a bezier curve and not a straight line.
const middlePoint = { x: 359.4188232421875, y: 468.7716979980469 }
// Click the middle point of the link to open the context menu.
await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y)
// Click the middle point of the link to open the context menu.
await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y)
// Click the "Delete" context menu option.
await comfyPage.page
.locator('.litecontextmenu .litemenu-entry', { hasText: 'Delete' })
.click()
// Click the "Delete" context menu option.
await comfyPage.page
.locator('.litecontextmenu .litemenu-entry', { hasText: 'Delete' })
.click()
await expect(comfyPage.canvas).toHaveScreenshot(
'native_reroute_delete_from_midpoint_context_menu.png'
)
})
})
await expect(comfyPage.canvas).toHaveScreenshot(
'native_reroute_delete_from_midpoint_context_menu.png'
)
})
}
)

View File

@@ -7,43 +7,49 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Canvas Right Click Menu', () => {
test('Can add node', async ({ comfyPage }) => {
await comfyPage.rightClickCanvas()
await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png')
await comfyPage.page.getByText('Add Node').click()
await comfyPage.nextFrame()
await comfyPage.page.getByText('loaders').click()
await comfyPage.nextFrame()
await comfyPage.page.getByText('Load VAE').click()
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('add-node-node-added.png')
})
test.describe(
'Canvas Right Click Menu',
{ tag: ['@screenshot', '@ui'] },
() => {
test('Can add node', async ({ comfyPage }) => {
await comfyPage.rightClickCanvas()
await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png')
await comfyPage.page.getByText('Add Node').click()
await comfyPage.nextFrame()
await comfyPage.page.getByText('loaders').click()
await comfyPage.nextFrame()
await comfyPage.page.getByText('Load VAE').click()
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('add-node-node-added.png')
})
test('Can add group', async ({ comfyPage }) => {
await comfyPage.rightClickCanvas()
await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png')
await comfyPage.page.getByText('Add Group', { exact: true }).click()
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('add-group-group-added.png')
})
test('Can add group', async ({ comfyPage }) => {
await comfyPage.rightClickCanvas()
await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png')
await comfyPage.page.getByText('Add Group', { exact: true }).click()
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'add-group-group-added.png'
)
})
test('Can convert to group node', async ({ comfyPage }) => {
await comfyPage.select2Nodes()
await expect(comfyPage.canvas).toHaveScreenshot('selected-2-nodes.png')
await comfyPage.rightClickCanvas()
await comfyPage.clickContextMenuItem('Convert to Group Node (Deprecated)')
await comfyPage.promptDialogInput.fill('GroupNode2CLIP')
await comfyPage.page.keyboard.press('Enter')
await comfyPage.promptDialogInput.waitFor({ state: 'hidden' })
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'right-click-node-group-node.png'
)
})
})
test('Can convert to group node', async ({ comfyPage }) => {
await comfyPage.select2Nodes()
await expect(comfyPage.canvas).toHaveScreenshot('selected-2-nodes.png')
await comfyPage.rightClickCanvas()
await comfyPage.clickContextMenuItem('Convert to Group Node (Deprecated)')
await comfyPage.promptDialogInput.fill('GroupNode2CLIP')
await comfyPage.page.keyboard.press('Enter')
await comfyPage.promptDialogInput.waitFor({ state: 'hidden' })
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'right-click-node-group-node.png'
)
})
}
)
test.describe('Node Right Click Menu', () => {
test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => {
test('Can open properties panel', async ({ comfyPage }) => {
await comfyPage.rightClickEmptyLatentNode()
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')

View File

@@ -10,7 +10,7 @@ test.beforeEach(async ({ comfyPage }) => {
const BLUE_COLOR = 'rgb(51, 51, 85)'
const RED_COLOR = 'rgb(85, 51, 51)'
test.describe('Selection Toolbox', () => {
test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
})

View File

@@ -7,178 +7,190 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Selection Toolbox - More Options Submenus', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
await comfyPage.loadWorkflow('nodes/single_ksampler')
await comfyPage.nextFrame()
await comfyPage.selectNodes(['KSampler'])
await comfyPage.nextFrame()
})
const openMoreOptions = async (comfyPage: ComfyPage) => {
const ksamplerNodes = await comfyPage.getNodeRefsByTitle('KSampler')
if (ksamplerNodes.length === 0) {
throw new Error('No KSampler nodes found')
}
// Drag the KSampler to the center of the screen
const nodePos = await ksamplerNodes[0].getPosition()
const viewportSize = comfyPage.page.viewportSize()
const centerX = viewportSize.width / 3
const centerY = viewportSize.height / 2
await comfyPage.dragAndDrop(
{ x: nodePos.x, y: nodePos.y },
{ x: centerX, y: centerY }
)
await comfyPage.nextFrame()
await ksamplerNodes[0].click('title')
await comfyPage.nextFrame()
await expect(comfyPage.page.locator('.selection-toolbox')).toBeVisible({
timeout: 5000
})
const moreOptionsBtn = comfyPage.page.locator(
'[data-testid="more-options-button"]'
)
await expect(moreOptionsBtn).toBeVisible({ timeout: 3000 })
await comfyPage.page.click('[data-testid="more-options-button"]')
await comfyPage.nextFrame()
const menuOptionsVisible = await comfyPage.page
.getByText('Rename')
.isVisible({ timeout: 2000 })
.catch(() => false)
if (menuOptionsVisible) {
return
}
await moreOptionsBtn.click({ force: true })
await comfyPage.nextFrame()
const menuOptionsVisibleAfterClick = await comfyPage.page
.getByText('Rename')
.isVisible({ timeout: 2000 })
.catch(() => false)
if (menuOptionsVisibleAfterClick) {
return
}
throw new Error('Could not open More Options menu - popover not showing')
}
test('opens Node Info from More Options menu', async ({ comfyPage }) => {
await openMoreOptions(comfyPage)
const nodeInfoButton = comfyPage.page.getByText('Node Info', {
exact: true
})
await expect(nodeInfoButton).toBeVisible()
await nodeInfoButton.click()
await comfyPage.nextFrame()
})
test('changes node shape via Shape submenu', async ({ comfyPage }) => {
const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
const initialShape = await nodeRef.getProperty<number>('shape')
await openMoreOptions(comfyPage)
await comfyPage.page.getByText('Shape', { exact: true }).hover()
await expect(comfyPage.page.getByText('Box', { exact: true })).toBeVisible({
timeout: 5000
})
await comfyPage.page.getByText('Box', { exact: true }).click()
await comfyPage.nextFrame()
const newShape = await nodeRef.getProperty<number>('shape')
expect(newShape).not.toBe(initialShape)
expect(newShape).toBe(1)
})
test('changes node color via Color submenu swatch', async ({ comfyPage }) => {
const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
const initialColor = await nodeRef.getProperty<string | undefined>('color')
await openMoreOptions(comfyPage)
await comfyPage.page.getByText('Color', { exact: true }).click()
const blueSwatch = comfyPage.page.locator('[title="Blue"]')
await expect(blueSwatch.first()).toBeVisible({ timeout: 5000 })
await blueSwatch.first().click()
await comfyPage.nextFrame()
const newColor = await nodeRef.getProperty<string | undefined>('color')
expect(newColor).toBe('#223')
if (initialColor) {
expect(newColor).not.toBe(initialColor)
}
})
test('renames a node using Rename action', async ({ comfyPage }) => {
const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
await openMoreOptions(comfyPage)
await comfyPage.page
.getByText('Rename', { exact: true })
.click({ force: true })
const input = comfyPage.page.locator(
'.group-title-editor.node-title-editor .editable-text input'
)
await expect(input).toBeVisible()
await input.fill('RenamedNode')
await input.press('Enter')
await comfyPage.nextFrame()
const newTitle = await nodeRef.getProperty<string>('title')
expect(newTitle).toBe('RenamedNode')
})
test('closes More Options menu when clicking outside', async ({
comfyPage
}) => {
await openMoreOptions(comfyPage)
const renameItem = comfyPage.page.getByText('Rename', { exact: true })
await expect(renameItem).toBeVisible({ timeout: 5000 })
// Wait for multiple frames to allow PrimeVue's outside click handler to initialize
for (let i = 0; i < 30; i++) {
test.describe(
'Selection Toolbox - More Options Submenus',
{ tag: '@ui' },
() => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
await comfyPage.loadWorkflow('nodes/single_ksampler')
await comfyPage.nextFrame()
await comfyPage.selectNodes(['KSampler'])
await comfyPage.nextFrame()
})
const openMoreOptions = async (comfyPage: ComfyPage) => {
const ksamplerNodes = await comfyPage.getNodeRefsByTitle('KSampler')
if (ksamplerNodes.length === 0) {
throw new Error('No KSampler nodes found')
}
// Drag the KSampler to the center of the screen
const nodePos = await ksamplerNodes[0].getPosition()
const viewportSize = comfyPage.page.viewportSize()
const centerX = viewportSize.width / 3
const centerY = viewportSize.height / 2
await comfyPage.dragAndDrop(
{ x: nodePos.x, y: nodePos.y },
{ x: centerX, y: centerY }
)
await comfyPage.nextFrame()
await ksamplerNodes[0].click('title')
await comfyPage.nextFrame()
await expect(comfyPage.page.locator('.selection-toolbox')).toBeVisible({
timeout: 5000
})
const moreOptionsBtn = comfyPage.page.locator(
'[data-testid="more-options-button"]'
)
await expect(moreOptionsBtn).toBeVisible({ timeout: 3000 })
await comfyPage.page.click('[data-testid="more-options-button"]')
await comfyPage.nextFrame()
const menuOptionsVisible = await comfyPage.page
.getByText('Rename')
.isVisible({ timeout: 2000 })
.catch(() => false)
if (menuOptionsVisible) {
return
}
await moreOptionsBtn.click({ force: true })
await comfyPage.nextFrame()
const menuOptionsVisibleAfterClick = await comfyPage.page
.getByText('Rename')
.isVisible({ timeout: 2000 })
.catch(() => false)
if (menuOptionsVisibleAfterClick) {
return
}
throw new Error('Could not open More Options menu - popover not showing')
}
await comfyPage.page
.locator('#graph-canvas')
.click({ position: { x: 0, y: 50 }, force: true })
test('opens Node Info from More Options menu', async ({ comfyPage }) => {
await openMoreOptions(comfyPage)
const nodeInfoButton = comfyPage.page.getByText('Node Info', {
exact: true
})
await expect(nodeInfoButton).toBeVisible()
await nodeInfoButton.click()
await comfyPage.nextFrame()
})
await comfyPage.nextFrame()
await expect(
comfyPage.page.getByText('Rename', { exact: true })
).not.toBeVisible()
})
test('changes node shape via Shape submenu', async ({ comfyPage }) => {
const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
const initialShape = await nodeRef.getProperty<number>('shape')
test('closes More Options menu when clicking the button again (toggle)', async ({
comfyPage
}) => {
await openMoreOptions(comfyPage)
await expect(
comfyPage.page.getByText('Rename', { exact: true })
).toBeVisible({ timeout: 5000 })
await openMoreOptions(comfyPage)
await comfyPage.page.getByText('Shape', { exact: true }).hover()
await expect(
comfyPage.page.getByText('Box', { exact: true })
).toBeVisible({
timeout: 5000
})
await comfyPage.page.getByText('Box', { exact: true }).click()
await comfyPage.nextFrame()
await comfyPage.page.evaluate(() => {
const btn = document.querySelector('[data-testid="more-options-button"]')
if (btn) {
const event = new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window,
detail: 1
})
btn.dispatchEvent(event)
const newShape = await nodeRef.getProperty<number>('shape')
expect(newShape).not.toBe(initialShape)
expect(newShape).toBe(1)
})
test('changes node color via Color submenu swatch', async ({
comfyPage
}) => {
const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
const initialColor = await nodeRef.getProperty<string | undefined>(
'color'
)
await openMoreOptions(comfyPage)
await comfyPage.page.getByText('Color', { exact: true }).click()
const blueSwatch = comfyPage.page.locator('[title="Blue"]')
await expect(blueSwatch.first()).toBeVisible({ timeout: 5000 })
await blueSwatch.first().click()
await comfyPage.nextFrame()
const newColor = await nodeRef.getProperty<string | undefined>('color')
expect(newColor).toBe('#223')
if (initialColor) {
expect(newColor).not.toBe(initialColor)
}
})
await comfyPage.nextFrame()
await expect(
comfyPage.page.getByText('Rename', { exact: true })
).not.toBeVisible()
})
})
test('renames a node using Rename action', async ({ comfyPage }) => {
const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
await openMoreOptions(comfyPage)
await comfyPage.page
.getByText('Rename', { exact: true })
.click({ force: true })
const input = comfyPage.page.locator(
'.group-title-editor.node-title-editor .editable-text input'
)
await expect(input).toBeVisible()
await input.fill('RenamedNode')
await input.press('Enter')
await comfyPage.nextFrame()
const newTitle = await nodeRef.getProperty<string>('title')
expect(newTitle).toBe('RenamedNode')
})
test('closes More Options menu when clicking outside', async ({
comfyPage
}) => {
await openMoreOptions(comfyPage)
const renameItem = comfyPage.page.getByText('Rename', { exact: true })
await expect(renameItem).toBeVisible({ timeout: 5000 })
// Wait for multiple frames to allow PrimeVue's outside click handler to initialize
for (let i = 0; i < 30; i++) {
await comfyPage.nextFrame()
}
await comfyPage.page
.locator('#graph-canvas')
.click({ position: { x: 0, y: 50 }, force: true })
await comfyPage.nextFrame()
await expect(
comfyPage.page.getByText('Rename', { exact: true })
).not.toBeVisible()
})
test('closes More Options menu when clicking the button again (toggle)', async ({
comfyPage
}) => {
await openMoreOptions(comfyPage)
await expect(
comfyPage.page.getByText('Rename', { exact: true })
).toBeVisible({ timeout: 5000 })
await comfyPage.page.evaluate(() => {
const btn = document.querySelector(
'[data-testid="more-options-button"]'
)
if (btn) {
const event = new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window,
detail: 1
})
btn.dispatchEvent(event)
}
})
await comfyPage.nextFrame()
await expect(
comfyPage.page.getByText('Rename', { exact: true })
).not.toBeVisible()
})
}
)

View File

@@ -12,7 +12,7 @@ const SELECTORS = {
promptDialog: '.graphdialog input'
} as const
test.describe('Subgraph Slot Rename Dialog', () => {
test.describe('Subgraph Slot Rename Dialog', { tag: '@subgraph' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})

View File

@@ -16,7 +16,7 @@ const SELECTORS = {
domWidget: '.comfy-multiline-input'
} as const
test.describe('Subgraph Operations', () => {
test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})

View File

@@ -13,7 +13,7 @@ async function checkTemplateFileExists(
return response.ok()
}
test.describe('Templates', () => {
test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting('Comfy.Workflow.ShowMissingModelsWarning', false)
@@ -207,109 +207,114 @@ test.describe('Templates', () => {
await expect(nav).toBeVisible() // Nav should be visible at tablet size
})
test('template cards descriptions adjust height dynamically', async ({
comfyPage
}) => {
// Setup test by intercepting templates response to inject cards with varying description lengths
await comfyPage.page.route('**/templates/index.json', async (route, _) => {
const response = [
{
moduleName: 'default',
title: 'Test Templates',
type: 'image',
templates: [
test(
'template cards descriptions adjust height dynamically',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
// Setup test by intercepting templates response to inject cards with varying description lengths
await comfyPage.page.route(
'**/templates/index.json',
async (route, _) => {
const response = [
{
name: 'short-description',
title: 'Short Description',
mediaType: 'image',
mediaSubtype: 'webp',
description: 'This is a short description.'
},
{
name: 'medium-description',
title: 'Medium Description',
mediaType: 'image',
mediaSubtype: 'webp',
description:
'This is a medium length description that should take up two lines on most displays.'
},
{
name: 'long-description',
title: 'Long Description',
mediaType: 'image',
mediaSubtype: 'webp',
description:
'This is a much longer description that should definitely wrap to multiple lines. It contains enough text to demonstrate how the cards handle varying amounts of content while maintaining a consistent layout grid.'
moduleName: 'default',
title: 'Test Templates',
type: 'image',
templates: [
{
name: 'short-description',
title: 'Short Description',
mediaType: 'image',
mediaSubtype: 'webp',
description: 'This is a short description.'
},
{
name: 'medium-description',
title: 'Medium Description',
mediaType: 'image',
mediaSubtype: 'webp',
description:
'This is a medium length description that should take up two lines on most displays.'
},
{
name: 'long-description',
title: 'Long Description',
mediaType: 'image',
mediaSubtype: 'webp',
description:
'This is a much longer description that should definitely wrap to multiple lines. It contains enough text to demonstrate how the cards handle varying amounts of content while maintaining a consistent layout grid.'
}
]
}
]
await route.fulfill({
status: 200,
body: JSON.stringify(response),
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-store'
}
})
}
]
await route.fulfill({
status: 200,
body: JSON.stringify(response),
headers: {
'Content-Type': 'application/json',
)
// Mock the thumbnail images to avoid 404s
await comfyPage.page.route('**/templates/**.webp', async (route) => {
const headers = {
'Content-Type': 'image/webp',
'Cache-Control': 'no-store'
}
await route.fulfill({
status: 200,
path: 'browser_tests/assets/example.webp',
headers
})
})
})
// Mock the thumbnail images to avoid 404s
await comfyPage.page.route('**/templates/**.webp', async (route) => {
const headers = {
'Content-Type': 'image/webp',
'Cache-Control': 'no-store'
}
await route.fulfill({
status: 200,
path: 'browser_tests/assets/example.webp',
headers
})
})
// Open templates dialog
await comfyPage.executeCommand('Comfy.BrowseTemplates')
await expect(comfyPage.templates.content).toBeVisible()
// Open templates dialog
await comfyPage.executeCommand('Comfy.BrowseTemplates')
await expect(comfyPage.templates.content).toBeVisible()
// Wait for cards to load
await expect(
comfyPage.page.locator(
'[data-testid="template-workflow-short-description"]'
)
).toBeVisible({ timeout: 5000 })
// Wait for cards to load
await expect(
comfyPage.page.locator(
// Verify all three cards with different descriptions are visible
const shortDescCard = comfyPage.page.locator(
'[data-testid="template-workflow-short-description"]'
)
).toBeVisible({ timeout: 5000 })
const mediumDescCard = comfyPage.page.locator(
'[data-testid="template-workflow-medium-description"]'
)
const longDescCard = comfyPage.page.locator(
'[data-testid="template-workflow-long-description"]'
)
// Verify all three cards with different descriptions are visible
const shortDescCard = comfyPage.page.locator(
'[data-testid="template-workflow-short-description"]'
)
const mediumDescCard = comfyPage.page.locator(
'[data-testid="template-workflow-medium-description"]'
)
const longDescCard = comfyPage.page.locator(
'[data-testid="template-workflow-long-description"]'
)
await expect(shortDescCard).toBeVisible()
await expect(mediumDescCard).toBeVisible()
await expect(longDescCard).toBeVisible()
await expect(shortDescCard).toBeVisible()
await expect(mediumDescCard).toBeVisible()
await expect(longDescCard).toBeVisible()
// Verify descriptions are visible and have line-clamp class
// The description is in a p tag with text-muted class
const shortDesc = shortDescCard.locator('p.text-muted.line-clamp-2')
const mediumDesc = mediumDescCard.locator('p.text-muted.line-clamp-2')
const longDesc = longDescCard.locator('p.text-muted.line-clamp-2')
// Verify descriptions are visible and have line-clamp class
// The description is in a p tag with text-muted class
const shortDesc = shortDescCard.locator('p.text-muted.line-clamp-2')
const mediumDesc = mediumDescCard.locator('p.text-muted.line-clamp-2')
const longDesc = longDescCard.locator('p.text-muted.line-clamp-2')
await expect(shortDesc).toContainText('short description')
await expect(mediumDesc).toContainText('medium length description')
await expect(longDesc).toContainText('much longer description')
await expect(shortDesc).toContainText('short description')
await expect(mediumDesc).toContainText('medium length description')
await expect(longDesc).toContainText('much longer description')
// Verify grid layout maintains consistency
const templateGrid = comfyPage.page.locator(
'[data-testid="template-workflows-content"]'
)
await expect(templateGrid).toBeVisible()
await expect(templateGrid).toHaveScreenshot(
'template-grid-varying-content.png'
)
})
// Verify grid layout maintains consistency
const templateGrid = comfyPage.page.locator(
'[data-testid="template-workflows-content"]'
)
await expect(templateGrid).toBeVisible()
await expect(templateGrid).toHaveScreenshot(
'template-grid-varying-content.png'
)
}
)
})

View File

@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Settings Search functionality', () => {
test.describe('Settings Search functionality', { tag: '@settings' }, () => {
test.beforeEach(async ({ comfyPage }) => {
// Register test settings to verify hidden/deprecated filtering
await comfyPage.page.evaluate(() => {

View File

@@ -5,7 +5,7 @@ import { userSelectPageFixture as test } from '../fixtures/UserSelectPage'
/**
* Expects ComfyUI backend to be launched with `--multi-user` flag.
*/
test.describe('User Select View', () => {
test.describe('User Select View', { tag: '@settings' }, () => {
test.beforeEach(async ({ userSelectPage, page }) => {
await page.goto(userSelectPage.url)
await page.evaluate(() => {

View File

@@ -3,7 +3,7 @@ import { expect } from '@playwright/test'
import type { SystemStats } from '../../src/schemas/apiSchema'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Version Mismatch Warnings', () => {
test.describe('Version Mismatch Warnings', { tag: '@slow' }, () => {
const ALWAYS_AHEAD_OF_INSTALLED_VERSION = '100.100.100'
const ALWAYS_BEHIND_INSTALLED_VERSION = '0.0.0'

View File

@@ -2,7 +2,7 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Viewport', () => {
test.describe('Viewport', { tag: ['@screenshot', '@smoke', '@canvas'] }, () => {
test('Fits view to nodes when saved viewport position is offscreen', async ({
comfyPage
}) => {

View File

@@ -5,7 +5,7 @@ import {
const CREATE_GROUP_HOTKEY = 'Control+g'
test.describe('Vue Node Groups', () => {
test.describe('Vue Node Groups', { tag: '@screenshot' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.setSetting('Comfy.Minimap.ShowGroups', true)

View File

@@ -9,10 +9,14 @@ test.describe('Vue Nodes Canvas Pan', () => {
await comfyPage.vueNodes.waitForNodes()
})
test('@mobile Can pan with touch', async ({ comfyPage }) => {
await comfyPage.panWithTouch({ x: 64, y: 64 }, { x: 256, y: 256 })
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-nodes-paned-with-touch.png'
)
})
test(
'@mobile Can pan with touch',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.panWithTouch({ x: 64, y: 64 }, { x: 256, y: 256 })
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-nodes-paned-with-touch.png'
)
}
)
})

View File

@@ -10,25 +10,30 @@ test.describe('Vue Nodes Zoom', () => {
await comfyPage.vueNodes.waitForNodes()
})
test('should not capture drag while zooming with ctrl+shift+drag', async ({
comfyPage
}) => {
const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
const nodeBoundingBox = await checkpointNode.boundingBox()
if (!nodeBoundingBox) throw new Error('Node bounding box not available')
test(
'should not capture drag while zooming with ctrl+shift+drag',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
const checkpointNode =
comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
const nodeBoundingBox = await checkpointNode.boundingBox()
if (!nodeBoundingBox) throw new Error('Node bounding box not available')
const nodeMidpointX = nodeBoundingBox.x + nodeBoundingBox.width / 2
const nodeMidpointY = nodeBoundingBox.y + nodeBoundingBox.height / 2
const nodeMidpointX = nodeBoundingBox.x + nodeBoundingBox.width / 2
const nodeMidpointY = nodeBoundingBox.y + nodeBoundingBox.height / 2
// Start the Ctrl+Shift drag-to-zoom on the canvas and continue dragging over
// the node. The node should not capture the drag while drag-zooming.
await comfyPage.page.keyboard.down('Control')
await comfyPage.page.keyboard.down('Shift')
await comfyPage.dragAndDrop(
{ x: 200, y: 300 },
{ x: nodeMidpointX, y: nodeMidpointY }
)
// Start the Ctrl+Shift drag-to-zoom on the canvas and continue dragging over
// the node. The node should not capture the drag while drag-zooming.
await comfyPage.page.keyboard.down('Control')
await comfyPage.page.keyboard.down('Shift')
await comfyPage.dragAndDrop(
{ x: 200, y: 300 },
{ x: nodeMidpointX, y: nodeMidpointY }
)
await expect(comfyPage.canvas).toHaveScreenshot('zoomed-in-ctrl-shift.png')
})
await expect(comfyPage.canvas).toHaveScreenshot(
'zoomed-in-ctrl-shift.png'
)
}
)
})

View File

@@ -98,7 +98,7 @@ async function connectSlots(
await nextFrame()
}
test.describe('Vue Node Link Interaction', () => {
test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)

View File

@@ -5,7 +5,7 @@ import {
import type { ComfyPage } from '../../../../fixtures/ComfyPage'
import { fitToViewInstant } from '../../../../helpers/fitToView'
test.describe('Vue Node Bring to Front', () => {
test.describe('Vue Node Bring to Front', { tag: '@screenshot' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)

View File

@@ -1,8 +1,8 @@
import {
type ComfyPage,
comfyExpect as expect,
comfyPageFixture as test
} from '../../../../fixtures/ComfyPage'
import type { ComfyPage } from '../../../../fixtures/ComfyPage'
import type { Position } from '../../../../fixtures/types'
test.describe('Vue Node Moving', () => {
@@ -29,39 +29,47 @@ test.describe('Vue Node Moving', () => {
expect(diffY).toBeGreaterThan(0)
}
test('should allow moving nodes by dragging', async ({ comfyPage }) => {
const loadCheckpointHeaderPos = await getLoadCheckpointHeaderPos(comfyPage)
await comfyPage.dragAndDrop(loadCheckpointHeaderPos, {
x: 256,
y: 256
})
test(
'should allow moving nodes by dragging',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
const loadCheckpointHeaderPos =
await getLoadCheckpointHeaderPos(comfyPage)
await comfyPage.dragAndDrop(loadCheckpointHeaderPos, {
x: 256,
y: 256
})
const newHeaderPos = await getLoadCheckpointHeaderPos(comfyPage)
await expectPosChanged(loadCheckpointHeaderPos, newHeaderPos)
const newHeaderPos = await getLoadCheckpointHeaderPos(comfyPage)
await expectPosChanged(loadCheckpointHeaderPos, newHeaderPos)
await expect(comfyPage.canvas).toHaveScreenshot('vue-node-moved-node.png')
})
await expect(comfyPage.canvas).toHaveScreenshot('vue-node-moved-node.png')
}
)
test('@mobile should allow moving nodes by dragging on touch devices', async ({
comfyPage
}) => {
// Disable minimap (gets in way of the node on small screens)
await comfyPage.setSetting('Comfy.Minimap.Visible', false)
test(
'@mobile should allow moving nodes by dragging on touch devices',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
// Disable minimap (gets in way of the node on small screens)
await comfyPage.setSetting('Comfy.Minimap.Visible', false)
const loadCheckpointHeaderPos = await getLoadCheckpointHeaderPos(comfyPage)
await comfyPage.panWithTouch(
{
x: 64,
y: 64
},
loadCheckpointHeaderPos
)
const loadCheckpointHeaderPos =
await getLoadCheckpointHeaderPos(comfyPage)
await comfyPage.panWithTouch(
{
x: 64,
y: 64
},
loadCheckpointHeaderPos
)
const newHeaderPos = await getLoadCheckpointHeaderPos(comfyPage)
await expectPosChanged(loadCheckpointHeaderPos, newHeaderPos)
const newHeaderPos = await getLoadCheckpointHeaderPos(comfyPage)
await expectPosChanged(loadCheckpointHeaderPos, newHeaderPos)
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-node-moved-node-touch.png'
)
})
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-node-moved-node-touch.png'
)
}
)
})

View File

@@ -15,22 +15,25 @@ test.describe('Vue Node Bypass', () => {
await comfyPage.vueNodes.waitForNodes()
})
test('should allow toggling bypass on a selected node with hotkey', async ({
comfyPage
}) => {
await comfyPage.page.getByText('Load Checkpoint').click()
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
test(
'should allow toggling bypass on a selected node with hotkey',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.page.getByText('Load Checkpoint').click()
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-node-bypassed-state.png'
)
const checkpointNode =
comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-node-bypassed-state.png'
)
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS)
})
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS)
}
)
test('should allow toggling bypass on multiple selected nodes with hotkey', async ({
comfyPage

View File

@@ -3,7 +3,7 @@ import {
comfyPageFixture as test
} from '../../../fixtures/ComfyPage'
test.describe('Vue Node Custom Colors', () => {
test.describe('Vue Node Custom Colors', { tag: '@screenshot' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)

View File

@@ -12,19 +12,24 @@ test.describe('Vue Node Mute', () => {
await comfyPage.vueNodes.waitForNodes()
})
test('should allow toggling mute on a selected node with hotkey', async ({
comfyPage
}) => {
await comfyPage.page.getByText('Load Checkpoint').click()
await comfyPage.page.keyboard.press(MUTE_HOTKEY)
test(
'should allow toggling mute on a selected node with hotkey',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.page.getByText('Load Checkpoint').click()
await comfyPage.page.keyboard.press(MUTE_HOTKEY)
const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
await expect(checkpointNode).toHaveCSS('opacity', MUTE_OPACITY)
await expect(comfyPage.canvas).toHaveScreenshot('vue-node-muted-state.png')
const checkpointNode =
comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
await expect(checkpointNode).toHaveCSS('opacity', MUTE_OPACITY)
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-node-muted-state.png'
)
await comfyPage.page.keyboard.press(MUTE_HOTKEY)
await expect(checkpointNode).not.toHaveCSS('opacity', MUTE_OPACITY)
})
await comfyPage.page.keyboard.press(MUTE_HOTKEY)
await expect(checkpointNode).not.toHaveCSS('opacity', MUTE_OPACITY)
}
)
test('should allow toggling mute on multiple selected nodes with hotkey', async ({
comfyPage

View File

@@ -9,13 +9,17 @@ test.describe('Vue Upload Widgets', () => {
await comfyPage.vueNodes.waitForNodes()
})
test('should hide canvas-only upload buttons', async ({ comfyPage }) => {
await comfyPage.setup()
await comfyPage.loadWorkflow('widgets/all_load_widgets')
await comfyPage.vueNodes.waitForNodes()
test(
'should hide canvas-only upload buttons',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.setup()
await comfyPage.loadWorkflow('widgets/all_load_widgets')
await comfyPage.vueNodes.waitForNodes()
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-nodes-upload-widgets.png'
)
})
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-nodes-upload-widgets.png'
)
}
)
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 80 KiB

View File

@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Combo text widget', () => {
test.describe('Combo text widget', { tag: ['@screenshot', '@widget'] }, () => {
test('Truncates text when resized', async ({ comfyPage }) => {
await comfyPage.resizeLoadCheckpointNode(0.2, 1)
await expect(comfyPage.canvas).toHaveScreenshot(
@@ -79,7 +79,7 @@ test.describe('Combo text widget', () => {
})
})
test.describe('Boolean widget', () => {
test.describe('Boolean widget', { tag: ['@screenshot', '@widget'] }, () => {
test('Can toggle', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/boolean_widget')
await expect(comfyPage.canvas).toHaveScreenshot('boolean_widget.png')
@@ -92,7 +92,7 @@ test.describe('Boolean widget', () => {
})
})
test.describe('Slider widget', () => {
test.describe('Slider widget', { tag: ['@screenshot', '@widget'] }, () => {
test('Can drag adjust value', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/simple_slider')
const node = (await comfyPage.getFirstNodeRef())!
@@ -113,7 +113,7 @@ test.describe('Slider widget', () => {
})
})
test.describe('Number widget', () => {
test.describe('Number widget', { tag: ['@screenshot', '@widget'] }, () => {
test('Can drag adjust value', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/seed_widget')
@@ -134,22 +134,28 @@ test.describe('Number widget', () => {
})
})
test.describe('Dynamic widget manipulation', () => {
test('Auto expand node when widget is added dynamically', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('nodes/single_ksampler')
test.describe(
'Dynamic widget manipulation',
{ tag: ['@screenshot', '@widget'] },
() => {
test('Auto expand node when widget is added dynamically', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('nodes/single_ksampler')
await comfyPage.page.evaluate(() => {
window['graph'].nodes[0].addWidget('number', 'new_widget', 10)
window['graph'].setDirtyCanvas(true, true)
await comfyPage.page.evaluate(() => {
window['graph'].nodes[0].addWidget('number', 'new_widget', 10)
window['graph'].setDirtyCanvas(true, true)
})
await expect(comfyPage.canvas).toHaveScreenshot(
'ksampler_widget_added.png'
)
})
}
)
await expect(comfyPage.canvas).toHaveScreenshot('ksampler_widget_added.png')
})
})
test.describe('Image widget', () => {
test.describe('Image widget', { tag: ['@screenshot', '@widget'] }, () => {
test('Can load image', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/load_image_widget')
await expect(comfyPage.canvas).toHaveScreenshot('load_image_widget.png')
@@ -236,99 +242,103 @@ test.describe('Image widget', () => {
})
})
test.describe('Animated image widget', () => {
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/3718
test.skip('Shows preview of uploaded animated image', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('widgets/load_animated_webp')
test.describe(
'Animated image widget',
{ tag: ['@screenshot', '@widget'] },
() => {
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/3718
test.skip('Shows preview of uploaded animated image', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('widgets/load_animated_webp')
// Get position of the load animated webp node
const nodes = await comfyPage.getNodeRefsByType(
'DevToolsLoadAnimatedImageTest'
)
const loadAnimatedWebpNode = nodes[0]
const { x, y } = await loadAnimatedWebpNode.getPosition()
// Get position of the load animated webp node
const nodes = await comfyPage.getNodeRefsByType(
'DevToolsLoadAnimatedImageTest'
)
const loadAnimatedWebpNode = nodes[0]
const { x, y } = await loadAnimatedWebpNode.getPosition()
// Drag and drop image file onto the load animated webp node
await comfyPage.dragAndDropFile('animated_webp.webp', {
dropPosition: { x, y }
// Drag and drop image file onto the load animated webp node
await comfyPage.dragAndDropFile('animated_webp.webp', {
dropPosition: { x, y }
})
// Expect the image preview to change automatically
await expect(comfyPage.canvas).toHaveScreenshot(
'animated_image_preview_drag_and_dropped.png'
)
// Move mouse and click on canvas to trigger render
await comfyPage.page.mouse.click(64, 64)
// Expect the image preview to change to the next frame of the animation
await expect(comfyPage.canvas).toHaveScreenshot(
'animated_image_preview_drag_and_dropped_next_frame.png'
)
})
// Expect the image preview to change automatically
await expect(comfyPage.canvas).toHaveScreenshot(
'animated_image_preview_drag_and_dropped.png'
)
test('Can drag-and-drop animated webp image', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/load_animated_webp')
// Move mouse and click on canvas to trigger render
await comfyPage.page.mouse.click(64, 64)
// Get position of the load animated webp node
const nodes = await comfyPage.getNodeRefsByType(
'DevToolsLoadAnimatedImageTest'
)
const loadAnimatedWebpNode = nodes[0]
const { x, y } = await loadAnimatedWebpNode.getPosition()
// Expect the image preview to change to the next frame of the animation
await expect(comfyPage.canvas).toHaveScreenshot(
'animated_image_preview_drag_and_dropped_next_frame.png'
)
})
// Drag and drop image file onto the load animated webp node
await comfyPage.dragAndDropFile('animated_webp.webp', {
dropPosition: { x, y },
waitForUpload: true
})
test('Can drag-and-drop animated webp image', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/load_animated_webp')
// Get position of the load animated webp node
const nodes = await comfyPage.getNodeRefsByType(
'DevToolsLoadAnimatedImageTest'
)
const loadAnimatedWebpNode = nodes[0]
const { x, y } = await loadAnimatedWebpNode.getPosition()
// Drag and drop image file onto the load animated webp node
await comfyPage.dragAndDropFile('animated_webp.webp', {
dropPosition: { x, y },
waitForUpload: true
// Expect the filename combo value to be updated
const fileComboWidget = await loadAnimatedWebpNode.getWidget(0)
const filename = await fileComboWidget.getValue()
expect(filename).toContain('animated_webp.webp')
})
// Expect the filename combo value to be updated
const fileComboWidget = await loadAnimatedWebpNode.getWidget(0)
const filename = await fileComboWidget.getValue()
expect(filename).toContain('animated_webp.webp')
})
test('Can preview saved animated webp image', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/save_animated_webp')
test('Can preview saved animated webp image', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/save_animated_webp')
// Get position of the load animated webp node
const loadNodes = await comfyPage.getNodeRefsByType(
'DevToolsLoadAnimatedImageTest'
)
const loadAnimatedWebpNode = loadNodes[0]
const { x, y } = await loadAnimatedWebpNode.getPosition()
// Get position of the load animated webp node
const loadNodes = await comfyPage.getNodeRefsByType(
'DevToolsLoadAnimatedImageTest'
)
const loadAnimatedWebpNode = loadNodes[0]
const { x, y } = await loadAnimatedWebpNode.getPosition()
// Drag and drop image file onto the load animated webp node
await comfyPage.dragAndDropFile('animated_webp.webp', {
dropPosition: { x, y }
})
await comfyPage.nextFrame()
// Drag and drop image file onto the load animated webp node
await comfyPage.dragAndDropFile('animated_webp.webp', {
dropPosition: { x, y }
// Get the SaveAnimatedWEBP node
const saveNodes = await comfyPage.getNodeRefsByType('SaveAnimatedWEBP')
const saveAnimatedWebpNode = saveNodes[0]
if (!saveAnimatedWebpNode)
throw new Error('SaveAnimatedWEBP node not found')
// Simulate the graph executing
await comfyPage.page.evaluate(
([loadId, saveId]) => {
// Set the output of the SaveAnimatedWEBP node to equal the loader node's image
window['app'].nodeOutputs[saveId] = window['app'].nodeOutputs[loadId]
app.canvas.setDirty(true)
},
[loadAnimatedWebpNode.id, saveAnimatedWebpNode.id]
)
await expect(
comfyPage.page.locator('.dom-widget').locator('img')
).toHaveCount(2)
})
await comfyPage.nextFrame()
}
)
// Get the SaveAnimatedWEBP node
const saveNodes = await comfyPage.getNodeRefsByType('SaveAnimatedWEBP')
const saveAnimatedWebpNode = saveNodes[0]
if (!saveAnimatedWebpNode)
throw new Error('SaveAnimatedWEBP node not found')
// Simulate the graph executing
await comfyPage.page.evaluate(
([loadId, saveId]) => {
// Set the output of the SaveAnimatedWEBP node to equal the loader node's image
window['app'].nodeOutputs[saveId] = window['app'].nodeOutputs[loadId]
app.canvas.setDirty(true)
},
[loadAnimatedWebpNode.id, saveAnimatedWebpNode.id]
)
await expect(
comfyPage.page.locator('.dom-widget').locator('img')
).toHaveCount(2)
})
})
test.describe('Load audio widget', () => {
test.describe('Load audio widget', { tag: ['@screenshot', '@widget'] }, () => {
test('Can load audio', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/load_audio_widget')
// Wait for the audio widget to be rendered in the DOM
@@ -338,7 +348,7 @@ test.describe('Load audio widget', () => {
})
})
test.describe('Unserialized widgets', () => {
test.describe('Unserialized widgets', { tag: '@widget' }, () => {
test('Unserialized widgets values do not mark graph as modified', async ({
comfyPage
}) => {

View File

@@ -1,8 +1,9 @@
import { expect } from '@playwright/test'
import { type ComfyPage, comfyPageFixture as test } from '../fixtures/ComfyPage'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import type { ComfyPage } from '../fixtures/ComfyPage'
test.describe('Workflow Tab Thumbnails', () => {
test.describe('Workflow Tab Thumbnails', { tag: '@workflow' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting('Comfy.Workflow.WorkflowTabsPosition', 'Topbar')

View File

@@ -18,7 +18,8 @@ Basic setup for testing Pinia stores:
```typescript
// Example from: tests-ui/tests/store/workflowStore.test.ts
import { createPinia, setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useWorkflowStore } from '@/domains/workflow/ui/stores/workflowStore'
@@ -27,8 +28,8 @@ describe('useWorkflowStore', () => {
let store: ReturnType<typeof useWorkflowStore>
beforeEach(() => {
// Create a fresh pinia and activate it for each test
setActivePinia(createPinia())
// Create a fresh testing pinia and activate it for each test
setActivePinia(createTestingPinia({ stubActions: false }))
// Initialize the store
store = useWorkflowStore()

View File

@@ -1,6 +1,6 @@
{
"name": "@comfyorg/comfyui-frontend",
"version": "1.39.0",
"version": "1.39.2",
"private": true,
"description": "Official front-end implementation of ComfyUI",
"homepage": "https://comfy.org",

View File

@@ -265,18 +265,15 @@ function cancelEdit() {
}
async function saveKeybinding() {
if (currentEditingCommand.value && newBindingKeyCombo.value) {
const updated = keybindingStore.updateKeybindingOnCommand(
new KeybindingImpl({
commandId: currentEditingCommand.value.id,
combo: newBindingKeyCombo.value
})
)
if (updated) {
await keybindingService.persistUserKeybindings()
}
}
const commandId = currentEditingCommand.value?.id
const combo = newBindingKeyCombo.value
cancelEdit()
if (!combo || commandId == undefined) return
const updated = keybindingStore.updateKeybindingOnCommand(
new KeybindingImpl({ commandId, combo })
)
if (updated) await keybindingService.persistUserKeybindings()
}
async function resetKeybinding(commandData: ICommandData) {

View File

@@ -1,6 +1,6 @@
<template>
<!-- Help Center Popup positioned within canvas area -->
<Teleport to="#graph-canvas-container">
<Teleport to="body">
<div
v-if="isHelpCenterVisible"
class="help-center-popup"

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