Compare commits

..

100 Commits

Author SHA1 Message Date
bymyself
97547434b0 [feat] Import-based API versioning architecture
- Replace metadata-based approach with import-based version selection
- Add dual endpoint fetching strategy (object_info + v3/object_info)
- Implement proxy-based bidirectional data synchronization
- Create version-specific type definitions (v1, v1.2, v3)
- Add data transformation pipeline between API versions
- Update extension service for version-aware invocation

This provides type-safe API versioning where extensions choose their
API version through imports, ensuring compile-time safety and zero
breaking changes for existing extensions.
2025-07-08 23:47:45 -07:00
Alexander Piskun
c7877dbd18 fix(float-precision): correct float widget rounding (#4291)
Signed-off-by: Alexander Piskun <bigcat88@icloud.com>
2025-07-06 15:40:27 -07:00
Terry Jia
4cbcded820 add defaultsByInstallVersion (#4354) 2025-07-06 13:28:58 -07:00
Comfy Org PR Bot
469594e5cc [chore] Update litegraph to 0.16.5 (#4365)
Co-authored-by: webfiltered <176114999+webfiltered@users.noreply.github.com>
2025-07-06 13:38:06 +00:00
Christian Byrne
191b4574b9 [fix] Add dynamic pricing for API nodes with quantity parameters (#4362) 2025-07-05 20:50:33 -07:00
filtered
0c4339f652 [TS] Update callbacks using CanvasMouseEvent #1104 (#4358) 2025-07-05 11:53:44 -07:00
RickyHuang
35556eb674 fIx: side toolbar tab tooltip not reactive when changing locale (#4213)
Co-authored-by: Huang Yun Qi <yun-qi.huang@ubisoft.com>
2025-07-05 02:54:23 -07:00
Christian Byrne
92b65ca00e [fix] Remove optional designation from issue report details field (#4355) 2025-07-04 20:34:47 -07:00
Christian Byrne
8f4e807468 [fix] move i18n pre-commit check inside Windows conditional block (#4353) 2025-07-04 17:25:04 -07:00
Terry Jia
c1db367422 add installedVersion (#4337)
Co-authored-by: bymyself <cbyrne@comfy.org>
2025-07-04 16:52:18 -07:00
Christian Byrne
3b435e337e [fix] Add dynamic pricing for Ideogram nodes based on num_images parameter (#4351) 2025-07-04 16:13:33 -07:00
Terry Jia
ee5088551e Vue expose (#4265) 2025-07-03 21:35:24 -07:00
Christian Byrne
44bbfa9f39 [feat] Implement getNodeByComfyNodeName API integration (#4343) 2025-07-03 17:59:21 -07:00
Comfy Org PR Bot
1b4ad61e7f [chore] Update Comfy Registry API types from comfy-api@4b0dc99 (#4340)
Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2025-07-03 12:42:23 -07:00
Christian Byrne
7befec5b17 Add unused i18n keys detection to pre-commit hook (#4328) 2025-07-03 10:53:56 -07:00
Comfy Org PR Bot
a51c09893f 1.24.0-1 (#4336)
Co-authored-by: webfiltered <176114999+webfiltered@users.noreply.github.com>
2025-07-02 19:52:05 -07:00
Comfy Org PR Bot
f290c00a61 [chore] Update litegraph to 0.16.4 (#4335)
Co-authored-by: webfiltered <176114999+webfiltered@users.noreply.github.com>
2025-07-02 17:57:33 -07:00
bmcomfy
a45753486d [System Pop Up] Improve help center menu behavior and Electron compatibility (#4330) 2025-07-02 16:13:13 -07:00
Christian Byrne
5cc1a8dea2 [test] Add release notification browser tests (#4311)
Co-authored-by: Claude <noreply@anthropic.com>
2025-07-02 01:29:09 -07:00
Christian Byrne
959ab3b3ec [feat] Add ESLint i18n enforcement and fix hardcoded strings (#4327)
Co-authored-by: github-actions <github-actions@github.com>
2025-07-02 00:41:46 -07:00
Terry Jia
35ff882ff2 [3d] better solution to support reading extra resource/texture (#4209) 2025-07-01 21:25:18 -07:00
filtered
f57f97cfcd [TS] Remove frontend-only typing from litegraph (#4325) 2025-07-01 20:07:05 -07:00
Christian Byrne
8f825c066b [docs] add code quality guidelines for i18n, async cleanup, and error handling (#4305) 2025-07-01 17:13:55 -07:00
Comfy Org PR Bot
e6f90e3101 1.24.0-0 (#4321)
Co-authored-by: webfiltered <176114999+webfiltered@users.noreply.github.com>
2025-07-01 22:06:28 +00:00
filtered
d68391a80a [CI] Fix prerelease version tag not set (#4322) 2025-07-01 15:06:08 -07:00
filtered
df710945c9 [CI] Skip i18n in unrelated PRs (#4320) 2025-07-01 14:53:48 -07:00
filtered
8d6360074d Use prerelease flag for draft releases (#4319) 2025-07-01 14:51:24 -07:00
filtered
26c106c3e4 Allow prerelease using version bump action (#4318) 2025-07-01 14:29:55 -07:00
Comfy Org PR Bot
d92c282439 [chore] Update litegraph to 0.16.3 (#4316)
Co-authored-by: webfiltered <176114999+webfiltered@users.noreply.github.com>
2025-07-01 13:40:21 -07:00
Comfy Org PR Bot
bf3dcc83a0 [chore] Update litegraph to 0.16.2 (#4315)
Co-authored-by: webfiltered <176114999+webfiltered@users.noreply.github.com>
2025-07-01 12:58:53 -07:00
filtered
6470a0bbd9 [CodeHealth] Follow-up on #4288 - code style / async (#4308) 2025-07-01 09:46:24 -07:00
ComfyUI Wiki
c75015c5b8 Fix helper menu issues and align with the design. (#4261) 2025-06-30 21:36:09 -04:00
Christian Byrne
64a2a5b3ae [fix] Mock release API in browser tests to prevent UI interference (#4310) 2025-06-30 17:36:07 -07:00
filtered
fada8bf9cf Follow-up on #4256 (#4307) 2025-06-30 12:25:55 -07:00
Terry Jia
5bbed91295 usage log table (#4288)
Co-authored-by: github-actions <github-actions@github.com>
2025-06-30 12:13:01 -07:00
Christian Byrne
eb8b67dd9d [docs] Update nested README files with comprehensive component listings (#4303) 2025-06-29 19:55:21 -07:00
Christian Byrne
d6a8f98327 [docs] add component communication best practices (#4302) 2025-06-29 18:25:28 -07:00
filtered
8457768a41 [Test] Update test expectations to match core changes to scheduler (#4293)
Co-authored-by: github-actions <github-actions@github.com>
2025-06-28 21:58:31 -07:00
Comfy Org PR Bot
3ae7faa8c5 [chore] Update litegraph to 0.16.1 (#4292)
Co-authored-by: webfiltered <176114999+webfiltered@users.noreply.github.com>
2025-06-28 17:08:56 -07:00
filtered
a7fb685290 Add Subgraphs (#3905)
Co-authored-by: github-actions <github-actions@github.com>
2025-06-28 15:37:23 -07:00
Christian Byrne
7620bb9063 [bugfix] Handle backend error messages with appended content (#4283) 2025-06-27 13:47:21 -07:00
bmcomfy
2d2cec2e79 [System Pop Up] Add help center with release notifications and "What's New" popup (#4256)
Co-authored-by: github-actions <github-actions@github.com>
2025-06-26 14:11:15 -07:00
Comfy Org PR Bot
c2ae40bab5 1.23.4 (#4281)
Co-authored-by: webfiltered <176114999+webfiltered@users.noreply.github.com>
2025-06-26 08:39:41 -07:00
filtered
10fbf7e847 Use scaled FLUX weights by default (#4280) 2025-06-26 08:38:30 -07:00
Comfy Org PR Bot
0bbfc44bc7 1.23.3 (#4279)
Co-authored-by: webfiltered <176114999+webfiltered@users.noreply.github.com>
2025-06-26 08:12:00 -07:00
filtered
a9b7ed2a53 Prevent video output nodes from showing edit model button (#4278) 2025-06-26 06:15:09 -07:00
filtered
35ee8f2d92 Only show edit image icon on valid nodes (#4277) 2025-06-26 03:00:05 -07:00
filtered
9a3530dc3a Add initial edit model button (static) (#4276)
Co-authored-by: github-actions <github-actions@github.com>
2025-06-26 00:34:38 -07:00
Comfy Org PR Bot
4c177121a6 [chore] Update Comfy Registry API types from comfy-api@065aded (#4274)
Co-authored-by: bmcomfy <214909599+bmcomfy@users.noreply.github.com>
2025-06-25 23:57:37 +00:00
Jin Yi
63181a1ddd [Manager] Standardize Card Aspect Ratios & Enhance UI (#4271)
Co-authored-by: Claude <noreply@anthropic.com>
2025-06-25 12:34:19 -07:00
Jin Yi
e17ca7ce71 fix: node migration TypeError (#4260) 2025-06-25 03:01:40 -07:00
Comfy Org PR Bot
77d2cae301 1.23.2 (#4266)
Co-authored-by: webfiltered <176114999+webfiltered@users.noreply.github.com>
2025-06-25 00:48:39 +00:00
Comfy Org PR Bot
164a4c4c25 [chore] Update Comfy Registry API types from comfy-api@af72ba5 (#4264)
Co-authored-by: bmcomfy <214909599+bmcomfy@users.noreply.github.com>
2025-06-24 14:57:41 -07:00
Jin Yi
47145ce4b8 [Manager] Modal UI Adjustment (Align with Design) (#4222) 2025-06-23 21:30:56 -07:00
Christian Byrne
6cf77a9814 [Manager] Fix bug: installed packs metadata not re-fetched after installations (#4254) 2025-06-23 04:37:50 -07:00
Christian Byrne
886e4908d4 [Manager] Fix flush timing issue when switching tabs (#4253) 2025-06-23 03:49:47 -07:00
Christian Byrne
24cbc41832 [Manager] Fix bug: opening modal when last focused tab was 'Installed' always shows empty list (#4252) 2025-06-23 02:41:15 -07:00
Christian Byrne
a80a939324 Fix: virtual grid scrolling bug when container is rendered with emtpy items (switching tabs) (#4251) 2025-06-23 00:13:46 -07:00
Christian Byrne
8e2d7cabba Fix bug: drag-and-drop, copy-paste, and upload don't work in nodes that specify upload folder that isn't 'input' (#4186) 2025-06-22 20:18:36 -07:00
Christian Byrne
e8dd26ff59 [Manager] Fix: When using registry search provider, results not properly paginated' (#4249) 2025-06-22 20:05:37 -07:00
Christian Byrne
3a1bd1829a [feat] Add auto-refresh on task completion for RemoteWidget nodes (#4191)
Co-authored-by: filtered <176114999+webfiltered@users.noreply.github.com>
2025-06-22 17:30:24 -07:00
ComfyUI Wiki
2f9dcd1669 Fix: fix typo in Lite Graph settings (#4245) 2025-06-22 08:32:09 +00:00
filtered
e23547dd5a [TS] Remove expect-error (type fix) (#4235) 2025-06-21 20:52:35 -07:00
Comfy Org PR Bot
f0f40bc39b 1.23.1 (#4234)
Co-authored-by: webfiltered <176114999+webfiltered@users.noreply.github.com>
2025-06-21 18:37:51 +00:00
Christian Byrne
4b32786ef5 [Manager] Update Algolia mappings (#4230)
Co-authored-by: Claude <noreply@anthropic.com>
2025-06-21 11:09:14 -07:00
Comfy Org PR Bot
9942b17388 [chore] Update Comfy Registry API types from comfy-api@4286a10 (#4229)
Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2025-06-20 15:47:04 -07:00
Christian Byrne
b99214bf5e [feat] Show version-specific missing core nodes in workflow warnings (#4227)
Co-authored-by: github-actions <github-actions@github.com>
2025-06-20 15:33:47 -07:00
Christian Byrne
2ef760c599 [Manager] Keep progress dialog on top using priority system (#4225) 2025-06-20 15:22:42 -07:00
Christian Byrne
429ab6c365 [Manager] Fix "total nodes" count when selecting multiple packs (#4228) 2025-06-20 15:20:26 -07:00
ComfyUI Wiki
b7693ae9f5 Fix typo in 3D settings (#4224) 2025-06-20 13:26:40 -07:00
filtered
ebedf1074d [CI] Fix intermittent actions/cache errors (#4220) 2025-06-18 03:55:05 -07:00
filtered
0832347f47 [CI] Fix intermittent failure when using actions/cache (#4219) 2025-06-18 01:24:42 -07:00
filtered
c745af0f25 [Test] Fix vitest scope overlaps playwright tests (#4218) 2025-06-18 01:08:30 -07:00
Comfy Org PR Bot
8c05266b83 1.23.0 (#4217)
Co-authored-by: webfiltered <176114999+webfiltered@users.noreply.github.com>
2025-06-18 00:32:54 -07:00
Jin Yi
fa14ec52f4 [Manager] Impletent “Install All” button (#4196)
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: filtered <176114999+webfiltered@users.noreply.github.com>
Co-authored-by: Christian Byrne <cbyrne@comfy.org>
Co-authored-by: Terry Jia <terryjia88@gmail.com>
Co-authored-by: comfy-waifu <comfywaifu.ai@gmail.com>
Co-authored-by: Comfy Org PR Bot <snomiao+comfy-pr@gmail.com>
2025-06-18 10:52:24 +09:00
Christian Byrne
ec9da0b6c5 [refactor] Add ResultItemType and improve image upload typing (#4200) 2025-06-16 14:31:24 -07:00
Christian Byrne
98bb1df436 [refactor] introduce frontend type augmentation pattern (#4192) 2025-06-16 11:32:07 -07:00
Christian Byrne
75077fe9ed [Manager] Add registry search fallback with gateway pattern (#4187) 2025-06-15 17:22:05 -07:00
filtered
d5ecfb2c99 Revert "[refactor] Refactor and type image upload options" (#4190) 2025-06-15 12:17:54 -07:00
Christian Byrne
3211875084 [refactor] Refactor and type image upload options (#4185) 2025-06-15 12:07:26 -07:00
Christian Byrne
a6bd04f951 [Manager] Make dialog closeable with button and hotkey (#4179)
Co-authored-by: github-actions <github-actions@github.com>
2025-06-14 15:21:43 -07:00
Christian Byrne
5b32d2aad0 [Manager] Persist/Restore Manager UI state (#4180) 2025-06-14 15:19:56 -07:00
Christian Byrne
23ba7e6501 [Manager] Fix version selector popover not closing when selecting different pack (#4176) 2025-06-14 15:06:32 -07:00
Comfy Org PR Bot
1e2b16f14d 1.22.2 (#4170)
Co-authored-by: webfiltered <176114999+webfiltered@users.noreply.github.com>
2025-06-14 07:19:53 +00:00
Christian Byrne
ec27d50333 [Manager] Fix selection state race condition during pack data merge (#4165) 2025-06-13 23:46:53 -07:00
Christian Byrne
693e156ab2 [Manager] Update PackCard styling to match Figma design (#4164) 2025-06-13 22:27:34 -07:00
comfy-waifu
8274df5075 Fixed favicon some progress frames not found - by ComfyWaifu 🤍 (#4143) 2025-06-13 21:59:35 -07:00
Christian Byrne
55bf36564d [Manager] Fix card selection highlight z-index and border radius issues (#4160) 2025-06-13 21:19:11 -07:00
Christian Byrne
48ac4a2b36 [Manager] Fix race condition in pack selection (#4158) 2025-06-14 03:53:06 +00:00
Christian Byrne
c9c1275e4c [Manager] Add enable/disable toggle for installed node packs (#4157) 2025-06-13 20:43:38 -07:00
Terry Jia
78ebc54ebe [3d] bugfix for preview manager (#4147) 2025-06-13 17:34:45 -07:00
Christian Byrne
88f2cc7847 [Manager] Refactor search result types (#4154) 2025-06-13 15:08:55 -07:00
Christian Byrne
7907e206da [Types] Remove outdated type intersection (#4146) 2025-06-13 14:08:59 -07:00
Christian Byrne
c4fa3dfe5a [Manager] Fix: fetch repeated infitely if no node packs installed (#4145) 2025-06-13 13:57:03 -07:00
filtered
587d7a19a1 [TS] Improve various types / remove assertions (#4148) 2025-06-13 01:46:50 -07:00
Jin Yi
9ca705381c Update fallback banner layout (#4141)
Co-authored-by: github-actions <github-actions@github.com>
2025-06-12 11:04:55 -07:00
Christian Byrne
a937ac59ad Revert Algolia proxy changes (#4133) 2025-06-11 06:41:35 -07:00
duckcomfy
995979a4e1 feat: add keyboard shortcut to move selected nodes (unbound by default) (#4066)
Co-authored-by: duckcomfy <a@a.a>
Co-authored-by: filtered <176114999+webfiltered@users.noreply.github.com>
2025-06-11 06:35:49 -07:00
Comfy Org PR Bot
c02ac95815 [chore] Update Comfy Registry API types from comfy-api@34a03c4 (#4123)
Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2025-06-10 05:01:45 -07:00
Christian Byrne
d01926b043 [Dev] Add note to Claude memory about common mistake with url prefixes (#4118) 2025-06-09 07:14:26 -07:00
370 changed files with 17328 additions and 14069 deletions

View File

@@ -1,111 +0,0 @@
Apply performance monitoring concepts from performance-test-guide.md to the specified test file: $ARGUMENTS
## Task Overview
Transform browser tests to include performance monitoring for canvas, node, and widget operations following the established performance testing patterns.
## Instructions
<analysis_phase>
1. **Read the target test file** specified in $ARGUMENTS
2. **Analyze test operations** to identify which ones should have performance monitoring based on the guide criteria:
-**Monitor**: Node operations, widget interactions, canvas operations, graph operations, background operations
-**Skip**: UI chrome elements, dialogs/modals, floating menus, gallery/template views
3. **Review existing test structure** to understand the test flow and key operations
</analysis_phase>
<implementation_phase>
4. **Add performance monitoring** following these steps:
**a. Import and setup:**
- Add `import { PerformanceMonitor } from '../helpers/performanceMonitor'`
- Add `@perf` tag to test name
- Initialize PerformanceMonitor with `comfyPage.page`
- Create descriptive kebab-case test name
- Call `startMonitoring(testName)`
**b. Wrap appropriate operations:**
- Use `measureOperation()` for node operations (creating, selecting, dragging, copying, deleting)
- Use `measureOperation()` for widget interactions (input changes, clicks, value modifications)
- Use `measureOperation()` for canvas operations (panning, zooming, selections, connections)
- Use `measureOperation()` for graph operations (loading workflows, undo/redo, batch operations)
- Use `markEvent()` for logical boundaries and state transitions
- Group related operations when they represent a single user action
- Keep assertions and expectations outside performance measurements
**c. Apply appropriate patterns:**
- **User Interaction Sequence**: Separate click, type, submit operations
- **Copy/Paste Operations**: Separate select, copy, paste with before/after marks
- **Drag Operations**: Separate start-drag, drag-to-position, drop
**d. Finalize:**
- Call `finishMonitoring(testName)` at the end
- Ensure all async operations are properly wrapped
</implementation_phase>
<naming_conventions>
- **Test names**: kebab-case, descriptive (e.g., 'copy-paste-multiple-nodes')
- **Operation names**: kebab-case, action-focused (e.g., 'click-node', 'drag-to-position')
- **Event marks**: kebab-case, state-focused (e.g., 'before-paste', 'after-render')
</naming_conventions>
<quality_guidelines>
- **Balance granularity**: Don't wrap every line, focus on meaningful operations
- **Maintain readability**: Wrapped code should remain clear and understandable
- **Preserve test logic**: Don't change test functionality, only add monitoring
- **Keep consistency**: Use similar operation names across similar tests
- **Group intelligently**: Combine related operations that represent single user actions
</quality_guidelines>
## Expected Output
Transform the test file to include:
1. Performance monitor import and initialization
2. `@perf` tag in test name
3. Appropriate `measureOperation()` wrapping for qualifying operations
4. `markEvent()` calls for logical boundaries
5. `finishMonitoring()` call at the end
6. Preserved test assertions and expectations outside performance measurements
Show the complete transformed test file with clear before/after comparison if the changes are substantial.
## Example Transformation Reference
Follow this pattern for transformation:
**Before:**
```typescript
test('Can copy and paste node', async ({ comfyPage }) => {
await comfyPage.clickEmptyLatentNode()
await comfyPage.ctrlC()
await comfyPage.ctrlV()
await expect(comfyPage.canvas).toHaveScreenshot('copied-node.png')
})
```
**After:**
```typescript
test('@perf Can copy and paste node', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'copy-paste-node'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('click-node', async () => {
await comfyPage.clickEmptyLatentNode()
})
await perfMonitor.measureOperation('copy-node', async () => {
await comfyPage.ctrlC()
})
await perfMonitor.measureOperation('paste-node', async () => {
await comfyPage.ctrlV()
})
await expect(comfyPage.canvas).toHaveScreenshot('copied-node.png')
await perfMonitor.finishMonitoring(testName)
})
```
Now apply these concepts to the test file: $ARGUMENTS

View File

@@ -1,53 +0,0 @@
# Create a Vue Widget for ComfyUI
Your task is to create a new Vue widget for ComfyUI based on the widget specification: $ARGUMENTS
## Instructions
Follow the comprehensive guide in `vue-widget-conversion/vue-widget-guide.md` to create the widget. This guide contains step-by-step instructions, examples from actual PRs, and best practices.
### Key Steps to Follow:
1. **Understand the Widget Type**
- Analyze what type of widget is needed: $ARGUMENTS
- Identify the data type (string, number, array, object, etc.)
- Determine if it needs special behaviors (execution state awareness, dynamic management, etc.)
2. **Component Creation**
- Create Vue component in `src/components/graph/widgets/`
- REQUIRED: Use PrimeVue components (reference `vue-widget-conversion/primevue-components.md`)
- Use Composition API with `<script setup>`
- Implement proper v-model binding with `defineModel`
3. **Composable Pattern**
- Always create widget constructor composable in `src/composables/widgets/`
- Only create node-level composable in `src/composables/node/` if the widget needs dynamic management
- Follow the dual composable pattern explained in the guide
4. **Registration**
- Register in `src/scripts/widgets.ts`
- Use appropriate widget type name
5. **Testing**
- Create unit tests for composables
- Test with actual nodes that use the widget
### Important Requirements:
- **Always use PrimeVue components** - Check `vue-widget-conversion/primevue-components.md` for available components
- Use TypeScript with proper types
- Follow Vue 3 Composition API patterns
- Use Tailwind CSS for styling (no custom CSS unless absolutely necessary)
- Implement proper error handling and validation
- Consider performance (use v-show vs v-if appropriately)
### Before Starting:
1. First read through the entire guide at `vue-widget-conversion/vue-widget-guide.md`
2. Check existing widget implementations for similar patterns
3. Identify which PrimeVue component(s) best fit the widget requirements
### Widget Specification to Implement:
$ARGUMENTS
Begin by analyzing the widget requirements and proposing an implementation plan based on the guide.

View File

@@ -3,6 +3,12 @@ name: Update Locales
on:
pull_request:
branches: [ main, master, dev* ]
paths-ignore:
- '.github/**'
- '.husky/**'
- '.vscode/**'
- 'browser_tests/**'
- 'tests-ui/**'
jobs:
update-locales:

View File

@@ -15,6 +15,7 @@ jobs:
contains(github.event.pull_request.labels.*.name, 'Release')
outputs:
version: ${{ steps.current_version.outputs.version }}
is_prerelease: ${{ steps.check_prerelease.outputs.is_prerelease }}
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -24,6 +25,15 @@ jobs:
- name: Get current version
id: current_version
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
- name: Check if prerelease
id: check_prerelease
run: |
VERSION=${{ steps.current_version.outputs.version }}
if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+- ]]; then
echo "is_prerelease=true" >> $GITHUB_OUTPUT
else
echo "is_prerelease=false" >> $GITHUB_OUTPUT
fi
- name: Build project
env:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
@@ -62,9 +72,9 @@ jobs:
dist.zip
tag_name: v${{ needs.build.outputs.version }}
target_commitish: ${{ github.event.pull_request.base.ref }}
make_latest: ${{ github.event.pull_request.base.ref == 'main' }}
draft: ${{ github.event.pull_request.base.ref != 'main' }}
prerelease: false
make_latest: ${{ github.event.pull_request.base.ref == 'main' && needs.build.outputs.is_prerelease == 'false' }}
draft: ${{ github.event.pull_request.base.ref != 'main' || needs.build.outputs.is_prerelease == 'true' }}
prerelease: ${{ needs.build.outputs.is_prerelease == 'true' }}
generate_release_notes: true
publish_pypi:

View File

@@ -46,8 +46,8 @@ jobs:
id: cache-key
run: echo "key=$(date +%s)" >> $GITHUB_OUTPUT
- name: Cache setup
uses: actions/cache@v3
- name: Save cache
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684
with:
path: |
ComfyUI
@@ -62,9 +62,13 @@ jobs:
matrix:
browser: [chromium, chromium-2x, mobile-chrome]
steps:
- name: Wait for cache propagation
run: sleep 10
- name: Restore cached setup
uses: actions/cache@v3
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684
with:
fail-on-cache-miss: true
path: |
ComfyUI
ComfyUI_frontend

View File

@@ -8,10 +8,12 @@ on:
required: true
default: 'patch'
type: 'choice'
options:
- patch
- minor
- major
options: [patch, minor, major, prepatch, preminor, premajor, prerelease]
pre_release:
description: Pre-release ID (suffix)
required: false
default: ''
type: string
jobs:
bump-version:
@@ -33,19 +35,25 @@ jobs:
- name: Bump version
id: bump-version
run: |
npm version ${{ github.event.inputs.version_type }} --no-git-tag-version
npm version ${{ github.event.inputs.version_type }} --preid ${{ github.event.inputs.pre_release }} --no-git-tag-version
NEW_VERSION=$(node -p "require('./package.json').version")
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
- name: Format PR string
id: capitalised
run: |
CAPITALISED_TYPE=${{ github.event.inputs.version_type }}
echo "capitalised=${CAPITALISED_TYPE@u}" >> $GITHUB_OUTPUT
- name: Create Pull Request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: '[release] Bump version to ${{ steps.bump-version.outputs.NEW_VERSION }}'
title: '${{ steps.bump-version.outputs.NEW_VERSION }}'
commit-message: '[release] Increment version to ${{ steps.bump-version.outputs.NEW_VERSION }}'
title: ${{ steps.bump-version.outputs.NEW_VERSION }}
body: |
Automated version bump to ${{ steps.bump-version.outputs.NEW_VERSION }}
${{ steps.capitalised.outputs.capitalised }} version increment to ${{ steps.bump-version.outputs.NEW_VERSION }}
branch: version-bump-${{ steps.bump-version.outputs.NEW_VERSION }}
base: main
labels: |
Release
Release

View File

@@ -1,5 +1,9 @@
if [[ "$OS" == "Windows_NT" ]]; then
npx.cmd lint-staged
# Check for unused i18n keys in staged files
npx.cmd tsx scripts/check-unused-i18n-keys.ts
else
npx lint-staged
# Check for unused i18n keys in staged files
npx tsx scripts/check-unused-i18n-keys.ts
fi

View File

@@ -1,8 +1,9 @@
- use npm run to see what commands are available
- use `npm run` to see what commands are available
- For component communication, prefer Vue's event-based pattern (emit/@event-name) for state changes and notifications; use defineExpose with refs only for imperative operations that need direct control (like form.validate(), modal.open(), or editor.focus()); events promote loose coupling and are better for reusable components, while exposed methods are acceptable for tightly-coupled component pairs or when wrapping third-party libraries that require imperative APIs
- After making code changes, follow this general process: (1) Create unit tests, component tests, browser tests (if appropriate for each), (2) run unit tests, component tests, and browser tests until passing, (3) run typecheck, lint, format (with prettier) -- you can use `npm run` command to see the scripts available, (4) check if any READMEs (including nested) or documentation needs to be updated, (5) Decide whether the changes are worth adding new content to the external documentation for (or would requires changes to the external documentation) at https://docs.comfy.org, then present your suggestion
- When referencing PrimeVue, you can get all the docs here: https://primevue.org. Do this instead of making up or inferring names of Components
- When trying to set tailwind classes for dark theme, use "dark-theme:" prefix rather than "dark:"
- Never add lines to PR descriptions that say "Generated with Claude Code"
- Never add lines to PR descriptions or commit messages that say "Generated with Claude Code"
- When making PR names and commit messages, if you are going to add a prefix like "docs:", "feat:", "bugfix:", use square brackets around the prefix term and do not use a colon (e.g., should be "[docs]" rather than "docs:").
- When I reference GitHub Repos related to Comfy-Org, you should proactively fetch or read the associated information in the repo. To do so, you should exhaust all options: (1) Check if we have a local copy of the repo, (2) Use the GitHub API to fetch the information; you may want to do this IN ADDITION to the other options, especially for reading specific branches/PRs/comments/reviews/metadata, and (3) curl the GitHub website and parse the html or json responses
- For information about ComfyUI, ComfyUI_frontend, or ComfyUI-Manager, you can web search or download these wikis: https://deepwiki.com/Comfy-Org/ComfyUI-Manager, https://deepwiki.com/Comfy-Org/ComfyUI_frontend/1-overview, https://deepwiki.com/comfyanonymous/ComfyUI/2-core-architecture
@@ -17,7 +18,6 @@
- Use the Vue 3 Composition API instead of the Options API when writing Vue components. An exception is when overriding or extending a PrimeVue component for compatibility, you may use the Options API.
- when we are solving an issue we know the link/number for, we should add "Fixes #n" (where n is the issue number) to the PR description.
- Never write css if you can accomplish the same thing with tailwind utility classes
- Use setup() function for component logic
- Utilize ref and reactive for reactive state
- Implement computed properties with computed()
- Use watch and watchEffect for side effects
@@ -27,14 +27,12 @@
- Use Tailwind CSS for styling
- Leverage VueUse functions for performance-enhancing styles
- Use lodash for utility functions
- Use TypeScript for type safety
- Implement proper props and emits definitions
- Utilize Vue 3's Teleport component when needed
- Use Suspense for async components
- Implement proper error handling
- Follow Vue 3 style guide and naming conventions
- Use Vite for fast development and building
- Use vue-i18n in composition API for any string literals. Place new translation entries in src/locales/en/main.json.
- IMPORTANT: Use vue-i18n for ALL user-facing strings - no hard-coded text in services/utilities. Place new translation entries in src/locales/en/main.json
- Avoid using `@ts-expect-error` to work around type issues. We needed to employ it to migrate to TypeScript, but it should not be viewed as an accepted practice or standard.
- DO NOT use deprecated PrimeVue components. Use these replacements instead:
* `Dropdown` → Use `Select` (import from 'primevue/select')
@@ -54,3 +52,7 @@
- Templates: `api.fileURL('/templates/default.json')`
- Extensions: `api.fileURL(extensionPath)` for loading JS modules
- Any static assets that exist in the public directory
- When implementing code that outputs raw HTML (e.g., using v-html directive), always ensure dynamic content has been properly sanitized with DOMPurify or validated through trusted sources. Prefer Vue templates over v-html when possible.
- For any async operations (API calls, timers, etc), implement cleanup/cancellation in component unmount to prevent memory leaks
- Extract complex template conditionals into separate components or computed properties
- Error messages should be actionable and user-friendly (e.g., "Failed to load data. Please refresh the page." instead of "Unknown error")

View File

@@ -29,6 +29,16 @@ A template with helpful information can be found in `.env_example`.
### Multiple Tests
If you are running Playwright tests in parallel or running the same test multiple times, the flag `--multi-user` must be added to the main ComfyUI process.
### Release API Mocking
By default, all tests mock the release API (`api.comfy.org/releases`) to prevent release notification popups from interfering with test execution. This is necessary because the release notifications can appear over UI elements and block test interactions.
To test with real release data, you can disable mocking:
```typescript
await comfyPage.setup({ mockReleases: false });
```
For tests that specifically need to test release functionality, see the example in `tests/releaseNotifications.spec.ts`.
## Running Tests
There are multiple ways to run the tests:

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -10,7 +10,6 @@ import type { KeyCombo } from '../../src/schemas/keyBindingSchema'
import type { useWorkspaceStore } from '../../src/stores/workspaceStore'
import { NodeBadgeMode } from '../../src/types/nodeSource'
import { ComfyActionbar } from '../helpers/actionbar'
import { PerformanceMonitor } from '../helpers/performanceMonitor'
import { ComfyTemplates } from '../helpers/templates'
import { ComfyMouse } from './ComfyMouse'
import { ComfyNodeSearchBox } from './components/ComfyNodeSearchBox'
@@ -144,7 +143,6 @@ export class ComfyPage {
public readonly templates: ComfyTemplates
public readonly settingDialog: SettingDialog
public readonly confirmDialog: ConfirmDialog
public readonly performanceMonitor: PerformanceMonitor
/** Worker index to test user ID */
public readonly userIds: string[] = []
@@ -172,7 +170,6 @@ export class ComfyPage {
this.templates = new ComfyTemplates(page)
this.settingDialog = new SettingDialog(page)
this.confirmDialog = new ConfirmDialog(page)
this.performanceMonitor = new PerformanceMonitor(page)
}
convertLeafToContent(structure: FolderStructure): FolderStructure {
@@ -271,8 +268,34 @@ export class ComfyPage {
return this._history
}
async setup({ clearStorage = true }: { clearStorage?: boolean } = {}) {
async setup({
clearStorage = true,
mockReleases = true
}: {
clearStorage?: boolean
mockReleases?: boolean
} = {}) {
await this.goto()
// Mock release endpoint to prevent changelog popups
if (mockReleases) {
await this.page.route('**/releases**', async (route) => {
const url = route.request().url()
if (
url.includes('api.comfy.org') ||
url.includes('stagingapi.comfy.org')
) {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([])
})
} else {
await route.continue()
}
})
}
if (clearStorage) {
await this.page.evaluate((id) => {
localStorage.clear()
@@ -1049,6 +1072,8 @@ export class ComfyPage {
}
}
export const testComfySnapToGridGridSize = 50
export const comfyPageFixture = base.extend<{
comfyPage: ComfyPage
comfyMouse: ComfyMouse
@@ -1061,14 +1086,6 @@ export const comfyPageFixture = base.extend<{
const userId = await comfyPage.setupUser(username)
comfyPage.userIds[parallelIndex] = userId
// Enable performance monitoring for @perf tagged tests
const isPerformanceTest = testInfo.title.includes('@perf')
// console.log('test info', testInfo)
if (isPerformanceTest) {
console.log('Enabling performance monitoring')
// PerformanceMonitor.enable()
}
try {
await comfyPage.setupSettings({
'Comfy.UseNewMenu': 'Disabled',
@@ -1083,30 +1100,19 @@ export const comfyPageFixture = base.extend<{
'Comfy.EnableTooltips': false,
'Comfy.userId': userId,
// Set tutorial completed to true to avoid loading the tutorial workflow.
'Comfy.TutorialCompleted': true
'Comfy.TutorialCompleted': true,
'Comfy.SnapToGrid.GridSize': testComfySnapToGridGridSize
})
} catch (e) {
console.error(e)
}
if (isPerformanceTest) {
// Start performance monitoring just before test execution
console.log('Starting performance monitoring')
await comfyPage.performanceMonitor.startMonitoring(testInfo.title)
}
await comfyPage.setup()
await use(comfyPage)
// Cleanup performance monitoring and collect final metrics
if (isPerformanceTest) {
console.log('Finishing performance monitoring')
await comfyPage.performanceMonitor.finishMonitoring(testInfo.title)
}
},
comfyMouse: async ({ comfyPage }, use) => {
const comfyMouse = new ComfyMouse(comfyPage)
void use(comfyMouse)
await use(comfyMouse)
}
})

View File

@@ -1,30 +1,13 @@
import { FullConfig } from '@playwright/test'
import dotenv from 'dotenv'
import { PerformanceMonitor } from './helpers/performanceMonitor'
import { restorePath } from './utils/backupUtils'
dotenv.config()
export default async function globalTeardown(config: FullConfig) {
console.log('🧹 Global teardown starting...')
// Always try to save performance metrics (handles temp files from workers)
try {
const filePath = await PerformanceMonitor.saveMetricsToFile()
console.log(`✅ Performance metrics saved successfully to: ${filePath}`)
} catch (error) {
console.error(
'❌ Failed to save performance metrics in global teardown:',
error
)
}
// Existing teardown logic
export default function globalTeardown(config: FullConfig) {
if (!process.env.CI && process.env.TEST_COMFYUI_DIR) {
restorePath([process.env.TEST_COMFYUI_DIR, 'user'])
restorePath([process.env.TEST_COMFYUI_DIR, 'models'])
}
console.log('🧹 Global teardown completed')
}

View File

@@ -1,346 +0,0 @@
import type { Page, TestInfo } from '@playwright/test'
import * as fs from 'fs'
import * as path from 'path'
export interface PerformanceMetrics {
testName: string
timestamp: number
branch?: string
memoryUsage: {
usedJSHeapSize: number
totalJSHeapSize: number
jsHeapSizeLimit: number
} | null
timing: {
loadStart?: number
domContentLoaded?: number
loadComplete?: number
firstPaint?: number
firstContentfulPaint?: number
largestContentfulPaint?: number
}
customMetrics: Record<string, number>
}
export interface PerformanceRunSummary {
runId: string
timestamp: number
branch: string
gitCommit?: string
environment: {
nodeVersion: string
playwrightVersion: string
os: string
}
testMetrics: PerformanceMetrics[]
}
export class PerformanceMonitor {
private metrics: PerformanceMetrics[] = []
private static allMetrics: PerformanceMetrics[] = []
constructor(
private page: Page,
private testInfo?: TestInfo
) {}
async startMonitoring(testName: string) {
await this.page.evaluate((testName) => {
// Initialize performance monitoring
window.perfMonitor = {
testName,
startTime: performance.now(),
marks: new Map(),
measures: new Map()
}
// Mark test start
performance.mark(`${testName}-start`)
// Set up performance observer to capture paint metrics
if ('PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (
entry.entryType === 'paint' ||
entry.entryType === 'largest-contentful-paint'
) {
window.perfMonitor?.measures.set(entry.name, entry.startTime)
}
}
})
observer.observe({ entryTypes: ['paint', 'largest-contentful-paint'] })
}
}, testName)
}
async markEvent(eventName: string) {
await this.page.evaluate((eventName) => {
if (window.perfMonitor) {
const markName = `${window.perfMonitor.testName}-${eventName}`
performance.mark(markName)
window.perfMonitor.marks.set(
eventName,
performance.now() - window.perfMonitor.startTime
)
}
}, eventName)
}
async measureOperation<T>(
operationName: string,
operation: () => Promise<T>
): Promise<T> {
await this.markEvent(`${operationName}-start`)
const result = await operation()
await this.markEvent(`${operationName}-end`)
// Create performance measure
await this.page.evaluate((operationName) => {
if (window.perfMonitor) {
const testName = window.perfMonitor.testName
const startMark = `${testName}-${operationName}-start`
const endMark = `${testName}-${operationName}-end`
try {
performance.measure(`${operationName}`, startMark, endMark)
const measure = performance.getEntriesByName(`${operationName}`)[0]
window.perfMonitor.measures.set(operationName, measure.duration)
} catch (e) {
console.warn('Failed to create performance measure:', e)
}
}
}, operationName)
return result
}
async collectMetrics(
testName: string,
branch: string = 'unknown'
): Promise<PerformanceMetrics | null> {
const metrics = await this.page.evaluate(
({ testName, branch }) => {
if (!window.perfMonitor) return null
// Collect all performance data
const navigationEntry = performance.getEntriesByType(
'navigation'
)[0] as PerformanceNavigationTiming
const paintEntries = performance.getEntriesByType('paint')
const lcpEntries = performance.getEntriesByType(
'largest-contentful-paint'
)
const timing: any = {}
if (navigationEntry) {
timing.loadStart = navigationEntry.loadEventStart
timing.domContentLoaded = navigationEntry.domContentLoadedEventEnd
timing.loadComplete = navigationEntry.loadEventEnd
}
paintEntries.forEach((entry) => {
if (entry.name === 'first-paint') {
timing.firstPaint = entry.startTime
} else if (entry.name === 'first-contentful-paint') {
timing.firstContentfulPaint = entry.startTime
}
})
if (lcpEntries.length > 0) {
timing.largestContentfulPaint =
lcpEntries[lcpEntries.length - 1].startTime
}
const customMetrics: Record<string, number> = {}
window.perfMonitor.measures.forEach((value, key) => {
customMetrics[key] = value
})
return {
testName,
timestamp: Date.now(),
branch,
memoryUsage: performance.memory
? {
usedJSHeapSize: performance.memory.usedJSHeapSize,
totalJSHeapSize: performance.memory.totalJSHeapSize,
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit
}
: null,
timing,
customMetrics
}
},
{ testName, branch }
)
if (metrics) {
this.metrics.push(metrics)
PerformanceMonitor.allMetrics.push(metrics)
// Write individual metric file immediately for worker persistence
try {
const tempDir = path.join(process.cwd(), 'test-results', '.perf-temp')
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true })
}
const tempFile = path.join(
tempDir,
`metric-${Date.now()}-${Math.random().toString(36).substr(2, 9)}.json`
)
fs.writeFileSync(tempFile, JSON.stringify(metrics, null, 2))
} catch (error) {
console.warn('Failed to write temp metric file:', error)
}
console.log('PERFORMANCE_METRICS:', JSON.stringify(metrics))
console.log(
`📈 Total metrics collected so far: ${PerformanceMonitor.allMetrics.length}`
)
}
return metrics
}
async finishMonitoring(testName: string) {
await this.markEvent('test-end')
await this.collectMetrics(testName, 'vue-widget/perf-test')
console.log('Finishing performance monitoring')
// Print all metrics
console.log('PERFORMANCE_METRICS:', JSON.stringify(this.metrics))
// Cleanup
await this.page.evaluate(() => {
if (window.perfMonitor) {
delete window.perfMonitor
}
})
}
getAllMetrics(): PerformanceMetrics[] {
return this.metrics
}
static getAllCollectedMetrics(): PerformanceMetrics[] {
return PerformanceMonitor.allMetrics
}
static clearAllMetrics() {
PerformanceMonitor.allMetrics = []
}
static async saveMetricsToFile(outputPath?: string): Promise<string> {
// This runs in Node.js context (global teardown), not browser
if (typeof window !== 'undefined') {
throw new Error(
'saveMetricsToFile should only be called from Node.js context'
)
}
// Collect metrics from temp files (handles worker persistence)
const allMetrics: PerformanceMetrics[] = []
const tempDir = path.join(process.cwd(), 'test-results', '.perf-temp')
if (fs.existsSync(tempDir)) {
const tempFiles = fs
.readdirSync(tempDir)
.filter((f) => f.startsWith('metric-') && f.endsWith('.json'))
for (const file of tempFiles) {
try {
const content = fs.readFileSync(path.join(tempDir, file), 'utf8')
const metric = JSON.parse(content)
allMetrics.push(metric)
} catch (error) {
console.warn(`Failed to read temp metric file ${file}:`, error)
}
}
// Clean up temp files
try {
fs.rmSync(tempDir, { recursive: true, force: true })
} catch (error) {
console.warn('Failed to clean up temp directory:', error)
}
}
// Also include any metrics from static array (fallback)
allMetrics.push(...PerformanceMonitor.allMetrics)
const defaultPath = path.join(process.cwd(), 'test-results', 'performance')
const resultsDir = outputPath || defaultPath
// Ensure directory exists
if (!fs.existsSync(resultsDir)) {
fs.mkdirSync(resultsDir, { recursive: true })
}
const runId = `run-${new Date().toISOString().replace(/[:.]/g, '-')}`
const branch =
process.env.GIT_BRANCH ||
process.env.GITHUB_HEAD_REF ||
process.env.GITHUB_REF_NAME ||
'unknown'
// Get Playwright version more safely
let playwrightVersion = 'unknown'
try {
playwrightVersion = require('@playwright/test/package.json').version
} catch {
// Fallback if package.json not accessible
playwrightVersion = 'unknown'
}
const summary: PerformanceRunSummary = {
runId,
timestamp: Date.now(),
branch,
gitCommit: process.env.GITHUB_SHA || process.env.GIT_COMMIT,
environment: {
nodeVersion: process.version,
playwrightVersion,
os: process.platform
},
testMetrics: allMetrics
}
const fileName = `${runId}.json`
const filePath = path.join(resultsDir, fileName)
try {
fs.writeFileSync(filePath, JSON.stringify(summary, null, 2))
console.log(`\n📊 Performance metrics saved to: ${filePath}`)
console.log(`📈 Total tests measured: ${allMetrics.length}`)
// Also create/update a latest.json for easy access
const latestPath = path.join(resultsDir, 'latest.json')
fs.writeFileSync(latestPath, JSON.stringify(summary, null, 2))
return filePath
} catch (error) {
console.error('Failed to save performance metrics:', error)
throw error
}
}
}
// Extend window type for TypeScript
declare global {
interface Window {
perfMonitor?: {
testName: string
startTime: number
marks: Map<string, number>
measures: Map<string, number>
}
}
// Chrome-specific performance.memory extension
interface Performance {
memory?: {
usedJSHeapSize: number
totalJSHeapSize: number
jsHeapSizeLimit: number
}
}
}

View File

@@ -1,7 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { PerformanceMonitor } from '../helpers/performanceMonitor'
test.describe('Background Image Upload', () => {
test.beforeEach(async ({ comfyPage }) => {
@@ -45,14 +44,9 @@ test.describe('Background Image Upload', () => {
await expect(clearButton).toBeDisabled() // Should be disabled when no image
})
test('@perf should upload image file and set as background', async ({
test('should upload image file and set as background', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'upload-background-image-file'
await perfMonitor.startMonitoring(testName)
// Open settings dialog
await comfyPage.page.keyboard.press('Control+,')
@@ -69,18 +63,16 @@ test.describe('Background Image Upload', () => {
'button:has(.pi-upload)'
)
// Set up file upload handler and upload
await perfMonitor.measureOperation('trigger-file-upload', async () => {
const fileChooserPromise = comfyPage.page.waitForEvent('filechooser')
await uploadButton.click()
const fileChooser = await fileChooserPromise
await fileChooser.setFiles(comfyPage.assetPath('image32x32.webp'))
})
// Set up file upload handler
const fileChooserPromise = comfyPage.page.waitForEvent('filechooser')
await uploadButton.click()
const fileChooser = await fileChooserPromise
// Upload the test image
await fileChooser.setFiles(comfyPage.assetPath('image32x32.webp'))
// Wait for upload to complete and verify the setting was updated
await perfMonitor.measureOperation('process-uploaded-file', async () => {
await comfyPage.page.waitForTimeout(500) // Give time for file reading
})
await comfyPage.page.waitForTimeout(500) // Give time for file reading
// Verify the URL input now has an API URL
const urlInput = backgroundImageSetting.locator('input[type="text"]')
@@ -96,18 +88,11 @@ test.describe('Background Image Upload', () => {
'Comfy.Canvas.BackgroundImage'
)
expect(settingValue).toMatch(/^\/api\/view\?.*subfolder=backgrounds/)
await perfMonitor.finishMonitoring(testName)
})
test('@perf should accept URL input for background image', async ({
test('should accept URL input for background image', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'input-background-image-url'
await perfMonitor.startMonitoring(testName)
const testImageUrl = 'https://example.com/test-image.png'
// Open settings dialog
@@ -121,13 +106,12 @@ test.describe('Background Image Upload', () => {
const backgroundImageSetting = comfyPage.page.locator(
'#Comfy\\.Canvas\\.BackgroundImage'
)
// Enter URL in the input field
const urlInput = backgroundImageSetting.locator('input[type="text"]')
await perfMonitor.measureOperation('input-url-text', async () => {
await urlInput.fill(testImageUrl)
await urlInput.blur()
})
await urlInput.fill(testImageUrl)
// Trigger blur event to ensure the value is set
await urlInput.blur()
// Verify clear button is now enabled
const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)')
@@ -138,24 +122,15 @@ test.describe('Background Image Upload', () => {
'Comfy.Canvas.BackgroundImage'
)
expect(settingValue).toBe(testImageUrl)
await perfMonitor.finishMonitoring(testName)
})
test('@perf should clear background image when clear button is clicked', async ({
test('should clear background image when clear button is clicked', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'clear-background-image'
await perfMonitor.startMonitoring(testName)
const testImageUrl = 'https://example.com/test-image.png'
// First set a background image
await perfMonitor.measureOperation('set-initial-setting', async () => {
await comfyPage.setSetting('Comfy.Canvas.BackgroundImage', testImageUrl)
})
await comfyPage.setSetting('Comfy.Canvas.BackgroundImage', testImageUrl)
// Open settings dialog
await comfyPage.page.keyboard.press('Control+,')
@@ -177,9 +152,7 @@ test.describe('Background Image Upload', () => {
await expect(clearButton).toBeEnabled()
// Click the clear button
await perfMonitor.measureOperation('click-clear-button', async () => {
await clearButton.click()
})
await clearButton.click()
// Verify the input is now empty
await expect(urlInput).toHaveValue('')
@@ -192,8 +165,6 @@ test.describe('Background Image Upload', () => {
'Comfy.Canvas.BackgroundImage'
)
expect(settingValue).toBe('')
await perfMonitor.finishMonitoring(testName)
})
test('should show tooltip on upload and clear buttons', async ({
@@ -240,14 +211,9 @@ test.describe('Background Image Upload', () => {
await expect(clearTooltip).toBeVisible()
})
test('@perf should maintain reactive updates between URL input and clear button state', async ({
test('should maintain reactive updates between URL input and clear button state', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'reactive-widget-updates'
await perfMonitor.startMonitoring(testName)
// Open settings dialog
await comfyPage.page.keyboard.press('Control+,')
@@ -266,30 +232,20 @@ test.describe('Background Image Upload', () => {
await expect(clearButton).toBeDisabled()
// Type some text - clear button should become enabled
await perfMonitor.measureOperation('input-partial-text', async () => {
await urlInput.fill('test')
})
await urlInput.fill('test')
await expect(clearButton).toBeEnabled()
// Clear the text manually - clear button should become disabled again
await perfMonitor.measureOperation('clear-input-manually', async () => {
await urlInput.fill('')
})
await urlInput.fill('')
await expect(clearButton).toBeDisabled()
// Add text again - clear button should become enabled
await perfMonitor.measureOperation('input-full-url', async () => {
await urlInput.fill('https://example.com/image.png')
})
await urlInput.fill('https://example.com/image.png')
await expect(clearButton).toBeEnabled()
// Use clear button - should clear input and disable itself
await perfMonitor.measureOperation('clear-via-button', async () => {
await clearButton.click()
})
await clearButton.click()
await expect(urlInput).toHaveValue('')
await expect(clearButton).toBeDisabled()
await perfMonitor.finishMonitoring(testName)
})
})

View File

@@ -3,7 +3,6 @@ import {
comfyExpect as expect,
comfyPageFixture as test
} from '../fixtures/ComfyPage'
import { PerformanceMonitor } from '../helpers/performanceMonitor'
async function beforeChange(comfyPage: ComfyPage) {
await comfyPage.page.evaluate(() => {
@@ -23,161 +22,93 @@ test.describe('Change Tracker', () => {
await comfyPage.setupWorkflowsDirectory({})
})
test.skip('@perf Can undo multiple operations', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'undo-multiple-operations'
await perfMonitor.startMonitoring(testName)
test('Can undo multiple operations', async ({ comfyPage }) => {
expect(await comfyPage.getUndoQueueSize()).toBe(0)
expect(await comfyPage.getRedoQueueSize()).toBe(0)
// Save, confirm no errors & workflow modified flag removed
await perfMonitor.measureOperation('save-workflow', async () => {
await comfyPage.menu.topbar.saveWorkflow('undo-redo-test')
})
await comfyPage.menu.topbar.saveWorkflow('undo-redo-test')
expect(await comfyPage.getToastErrorCount()).toBe(0)
expect(await comfyPage.isCurrentWorkflowModified()).toBe(false)
expect(await comfyPage.getUndoQueueSize()).toBe(0)
expect(await comfyPage.getRedoQueueSize()).toBe(0)
const node = (await comfyPage.getFirstNodeRef())!
await perfMonitor.measureOperation('click-node-title', async () => {
await node.click('title')
})
await perfMonitor.measureOperation('collapse-node', async () => {
await node.click('collapse')
})
await node.click('title')
await node.click('collapse')
await expect(node).toBeCollapsed()
expect(await comfyPage.isCurrentWorkflowModified()).toBe(true)
expect(await comfyPage.getUndoQueueSize()).toBe(1)
expect(await comfyPage.getRedoQueueSize()).toBe(0)
await perfMonitor.measureOperation('bypass-node', async () => {
await comfyPage.ctrlB()
})
await comfyPage.ctrlB()
await expect(node).toBeBypassed()
expect(await comfyPage.isCurrentWorkflowModified()).toBe(true)
expect(await comfyPage.getUndoQueueSize()).toBe(2)
expect(await comfyPage.getRedoQueueSize()).toBe(0)
await perfMonitor.markEvent('before-undo-operations')
await perfMonitor.measureOperation('undo-bypass', async () => {
await comfyPage.ctrlZ()
})
await comfyPage.ctrlZ()
await expect(node).not.toBeBypassed()
expect(await comfyPage.isCurrentWorkflowModified()).toBe(true)
expect(await comfyPage.getUndoQueueSize()).toBe(1)
expect(await comfyPage.getRedoQueueSize()).toBe(1)
await perfMonitor.measureOperation('undo-collapse', async () => {
await comfyPage.ctrlZ()
})
await comfyPage.ctrlZ()
await expect(node).not.toBeCollapsed()
expect(await comfyPage.isCurrentWorkflowModified()).toBe(false)
expect(await comfyPage.getUndoQueueSize()).toBe(0)
expect(await comfyPage.getRedoQueueSize()).toBe(2)
await perfMonitor.finishMonitoring(testName)
})
})
test('@perf Can group multiple change actions into a single transaction', async ({
test('Can group multiple change actions into a single transaction', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'group-change-transactions'
await perfMonitor.startMonitoring(testName)
const node = (await comfyPage.getFirstNodeRef())!
expect(node).toBeTruthy()
await expect(node).not.toBeCollapsed()
await expect(node).not.toBeBypassed()
await perfMonitor.markEvent('individual-changes-start')
// Make changes outside set
// Bypass + collapse node
await perfMonitor.measureOperation('click-node-title', async () => {
await node.click('title')
})
await perfMonitor.measureOperation('collapse-node', async () => {
await node.click('collapse')
})
await perfMonitor.measureOperation('bypass-node', async () => {
await comfyPage.ctrlB()
})
await node.click('title')
await node.click('collapse')
await comfyPage.ctrlB()
await expect(node).toBeCollapsed()
await expect(node).toBeBypassed()
// Undo, undo, ensure both changes undone
await perfMonitor.measureOperation('undo-bypass', async () => {
await comfyPage.ctrlZ()
})
await comfyPage.ctrlZ()
await expect(node).not.toBeBypassed()
await expect(node).toBeCollapsed()
await perfMonitor.measureOperation('undo-collapse', async () => {
await comfyPage.ctrlZ()
})
await comfyPage.ctrlZ()
await expect(node).not.toBeBypassed()
await expect(node).not.toBeCollapsed()
// Prevent clicks registering a double-click
await perfMonitor.measureOperation('click-empty-space', async () => {
await comfyPage.clickEmptySpace()
})
await perfMonitor.measureOperation('click-node-title-again', async () => {
await node.click('title')
})
await perfMonitor.markEvent('transaction-changes-start')
await comfyPage.clickEmptySpace()
await node.click('title')
// Run again, but within a change transaction
await perfMonitor.measureOperation('begin-change-transaction', async () => {
await beforeChange(comfyPage)
})
await beforeChange(comfyPage)
await perfMonitor.measureOperation('collapse-in-transaction', async () => {
await node.click('collapse')
})
await perfMonitor.measureOperation('bypass-in-transaction', async () => {
await comfyPage.ctrlB()
})
await node.click('collapse')
await comfyPage.ctrlB()
await expect(node).toBeCollapsed()
await expect(node).toBeBypassed()
// End transaction
await perfMonitor.measureOperation('end-change-transaction', async () => {
await afterChange(comfyPage)
})
await afterChange(comfyPage)
// Ensure undo reverts both changes
await perfMonitor.measureOperation('undo-transaction', async () => {
await comfyPage.ctrlZ()
})
await comfyPage.ctrlZ()
await expect(node).not.toBeBypassed()
await expect(node).not.toBeCollapsed()
await perfMonitor.finishMonitoring(testName)
})
test('@perf Can nest multiple change transactions without adding undo steps', async ({
test('Can nest multiple change transactions without adding undo steps', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'nested-change-transactions'
await perfMonitor.startMonitoring(testName)
const node = (await comfyPage.getFirstNodeRef())!
const bypassAndPin = async () => {
await beforeChange(comfyPage)
@@ -205,67 +136,32 @@ test.describe('Change Tracker', () => {
await afterChange(comfyPage)
}
await perfMonitor.measureOperation(
'execute-nested-transactions',
async () => {
await multipleChanges()
}
)
await multipleChanges()
await perfMonitor.measureOperation('undo-all-changes', async () => {
await comfyPage.ctrlZ()
})
await comfyPage.ctrlZ()
await expect(node).not.toBeBypassed()
await expect(node).not.toBePinned()
await expect(node).not.toBeCollapsed()
await perfMonitor.measureOperation('redo-all-changes', async () => {
await comfyPage.ctrlY()
})
await comfyPage.ctrlY()
await expect(node).toBeBypassed()
await expect(node).toBePinned()
await expect(node).toBeCollapsed()
await perfMonitor.finishMonitoring(testName)
})
// Skip because fails with vue widget nodes (reason not investigated)
test.skip('@perf Can detect changes in workflow.extra', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'detect-workflow-extra-changes'
await perfMonitor.startMonitoring(testName)
test('Can detect changes in workflow.extra', async ({ comfyPage }) => {
expect(await comfyPage.getUndoQueueSize()).toBe(0)
await perfMonitor.measureOperation('modify-workflow-extra', async () => {
await comfyPage.page.evaluate(() => {
window['app'].graph.extra.foo = 'bar'
})
await comfyPage.page.evaluate(() => {
window['app'].graph.extra.foo = 'bar'
})
// Click empty space to trigger a change detection.
await perfMonitor.measureOperation('trigger-change-detection', async () => {
await comfyPage.clickEmptySpace()
})
await comfyPage.clickEmptySpace()
expect(await comfyPage.getUndoQueueSize()).toBe(1)
await perfMonitor.finishMonitoring(testName)
})
test('@perf Ignores changes in workflow.ds', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'ignore-workflow-ds-changes'
await perfMonitor.startMonitoring(testName)
test('Ignores changes in workflow.ds', async ({ comfyPage }) => {
expect(await comfyPage.getUndoQueueSize()).toBe(0)
await perfMonitor.measureOperation('pan-canvas', async () => {
await comfyPage.pan({ x: 10, y: 10 })
})
await comfyPage.pan({ x: 10, y: 10 })
expect(await comfyPage.getUndoQueueSize()).toBe(0)
await perfMonitor.finishMonitoring(testName)
})
})

View File

@@ -1,7 +1,6 @@
import { Page, expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { PerformanceMonitor } from '../helpers/performanceMonitor'
interface ChatHistoryEntry {
prompt: string
@@ -43,66 +42,49 @@ test.describe('Chat History Widget', () => {
await comfyPage.page.waitForSelector('.pi-pencil')
})
// Skip because fails with vue widget nodes (reason not investigated)
test.skip('@perf displays chat history when receiving display_component message', async ({
test('displays chat history when receiving display_component message', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'display-chat-history-component'
await perfMonitor.startMonitoring(testName)
// Verify the chat history is displayed correctly
await expect(comfyPage.page.getByText('Hello')).toBeVisible()
await expect(comfyPage.page.getByText('World')).toBeVisible()
await perfMonitor.finishMonitoring(testName)
})
test.skip('@perf handles message editing interaction', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'message-editing-interaction'
await perfMonitor.startMonitoring(testName)
test('handles message editing interaction', async ({ comfyPage }) => {
// Get first node's ID
await perfMonitor.measureOperation('setup-node-widgets', async () => {
nodeId = await comfyPage.page.evaluate(() => {
const node = window['app'].graph.nodes[0]
nodeId = await comfyPage.page.evaluate(() => {
const node = window['app'].graph.nodes[0]
// Make sure the node has a prompt widget (for editing functionality)
if (!node.widgets) {
node.widgets = []
}
// Make sure the node has a prompt widget (for editing functionality)
if (!node.widgets) {
node.widgets = []
}
// Add a prompt widget if it doesn't exist
if (!node.widgets.find((w) => w.name === 'prompt')) {
node.widgets.push({
name: 'prompt',
type: 'text',
value: 'Original prompt'
})
}
// Add a prompt widget if it doesn't exist
if (!node.widgets.find((w) => w.name === 'prompt')) {
node.widgets.push({
name: 'prompt',
type: 'text',
value: 'Original prompt'
})
}
return node.id
})
return node.id
})
await perfMonitor.measureOperation('render-chat-history', async () => {
await renderChatHistory(comfyPage.page, [
{
prompt: 'Message 1',
response: 'Response 1',
response_id: '123'
},
{
prompt: 'Message 2',
response: 'Response 2',
response_id: '456'
}
])
await comfyPage.page.waitForSelector('.pi-pencil')
})
await renderChatHistory(comfyPage.page, [
{
prompt: 'Message 1',
response: 'Response 1',
response_id: '123'
},
{
prompt: 'Message 2',
response: 'Response 2',
response_id: '456'
}
])
await comfyPage.page.waitForSelector('.pi-pencil')
const originalTextAreaInput = await comfyPage.page
.getByPlaceholder('text')
@@ -110,73 +92,48 @@ test.describe('Chat History Widget', () => {
.inputValue()
// Click edit button on first message
await perfMonitor.measureOperation('click-edit-button', async () => {
await comfyPage.page.getByLabel('Edit').first().click()
await comfyPage.nextFrame()
})
await comfyPage.page.getByLabel('Edit').first().click()
await comfyPage.nextFrame()
// Verify cancel button appears
await expect(comfyPage.page.getByLabel('Cancel')).toBeVisible()
// Click cancel edit
await perfMonitor.measureOperation('click-cancel-button', async () => {
await comfyPage.page.getByLabel('Cancel').click()
})
await comfyPage.page.getByLabel('Cancel').click()
// Verify prompt input is restored
await expect(comfyPage.page.getByPlaceholder('text').nth(1)).toHaveValue(
originalTextAreaInput
)
await perfMonitor.finishMonitoring(testName)
})
test.skip('@perf handles real-time updates to chat history', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'real-time-chat-history-updates'
await perfMonitor.startMonitoring(testName)
test('handles real-time updates to chat history', async ({ comfyPage }) => {
// Send initial history
await perfMonitor.measureOperation('render-initial-history', async () => {
await renderChatHistory(comfyPage.page, [
{
prompt: 'Initial message',
response: 'Initial response',
response_id: '123'
}
])
await comfyPage.page.waitForSelector('.pi-pencil')
})
await perfMonitor.markEvent('before-history-update')
await renderChatHistory(comfyPage.page, [
{
prompt: 'Initial message',
response: 'Initial response',
response_id: '123'
}
])
await comfyPage.page.waitForSelector('.pi-pencil')
// Update history with additional messages
await perfMonitor.measureOperation('update-chat-history', async () => {
await renderChatHistory(comfyPage.page, [
{
prompt: 'Follow-up',
response: 'New response',
response_id: '456'
}
])
await comfyPage.page.waitForSelector('.pi-pencil')
})
await renderChatHistory(comfyPage.page, [
{
prompt: 'Follow-up',
response: 'New response',
response_id: '456'
}
])
await comfyPage.page.waitForSelector('.pi-pencil')
// Move mouse over the canvas to force update
await perfMonitor.measureOperation('trigger-canvas-update', async () => {
await comfyPage.page.mouse.move(100, 100)
await comfyPage.nextFrame()
})
await perfMonitor.markEvent('after-canvas-update')
await comfyPage.page.mouse.move(100, 100)
await comfyPage.nextFrame()
// Verify new messages appear
await expect(comfyPage.page.getByText('Follow-up')).toBeVisible()
await expect(comfyPage.page.getByText('New response')).toBeVisible()
await perfMonitor.finishMonitoring(testName)
})
})

View File

@@ -2,7 +2,6 @@ import { expect } from '@playwright/test'
import type { Palette } from '../../src/schemas/colorPaletteSchema'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { PerformanceMonitor } from '../helpers/performanceMonitor'
const customColorPalettes: Record<string, Palette> = {
obsidian: {
@@ -149,99 +148,45 @@ const customColorPalettes: Record<string, Palette> = {
}
test.describe('Color Palette', () => {
test('@perf Can show custom color palette', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'show-custom-color-palette'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('set-custom-palettes', async () => {
await comfyPage.setSetting(
'Comfy.CustomColorPalettes',
customColorPalettes
)
})
test('Can show custom color palette', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.CustomColorPalettes', customColorPalettes)
// Reload to apply the new setting. Setting Comfy.CustomColorPalettes directly
// doesn't update the store immediately.
await perfMonitor.measureOperation('reload-page', async () => {
await comfyPage.setup()
})
await perfMonitor.measureOperation('load-workflow', async () => {
await comfyPage.loadWorkflow('every_node_color')
})
await perfMonitor.measureOperation(
'apply-obsidian-dark-palette',
async () => {
await comfyPage.setSetting('Comfy.ColorPalette', 'obsidian_dark')
}
)
await comfyPage.setup()
await comfyPage.loadWorkflow('every_node_color')
await comfyPage.setSetting('Comfy.ColorPalette', 'obsidian_dark')
await expect(comfyPage.canvas).toHaveScreenshot(
'custom-color-palette-obsidian-dark-all-colors.png'
)
await perfMonitor.measureOperation('apply-light-red-palette', async () => {
await comfyPage.setSetting('Comfy.ColorPalette', 'light_red')
await comfyPage.nextFrame()
})
await comfyPage.setSetting('Comfy.ColorPalette', 'light_red')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'custom-color-palette-light-red.png'
)
await perfMonitor.measureOperation(
'apply-default-dark-palette',
async () => {
await comfyPage.setSetting('Comfy.ColorPalette', 'dark')
await comfyPage.nextFrame()
}
)
await comfyPage.setSetting('Comfy.ColorPalette', 'dark')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('default-color-palette.png')
await perfMonitor.finishMonitoring(testName)
})
test.skip('@perf Can add custom color palette', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'add-custom-color-palette'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('add-palette-via-api', async () => {
await comfyPage.page.evaluate((p) => {
window['app'].extensionManager.colorPalette.addCustomColorPalette(p)
}, customColorPalettes.obsidian_dark)
})
test('Can add custom color palette', async ({ comfyPage }) => {
await comfyPage.page.evaluate((p) => {
window['app'].extensionManager.colorPalette.addCustomColorPalette(p)
}, customColorPalettes.obsidian_dark)
expect(await comfyPage.getToastErrorCount()).toBe(0)
await perfMonitor.measureOperation('apply-custom-palette', async () => {
await comfyPage.setSetting('Comfy.ColorPalette', 'obsidian_dark')
await comfyPage.nextFrame()
})
await comfyPage.setSetting('Comfy.ColorPalette', 'obsidian_dark')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'custom-color-palette-obsidian-dark.png'
)
// Legacy `custom_` prefix is still supported
await perfMonitor.measureOperation(
'apply-custom-palette-legacy-prefix',
async () => {
await comfyPage.setSetting('Comfy.ColorPalette', 'custom_obsidian_dark')
await comfyPage.nextFrame()
}
)
await comfyPage.setSetting('Comfy.ColorPalette', 'custom_obsidian_dark')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'custom-color-palette-obsidian-dark.png'
)
await perfMonitor.finishMonitoring(testName)
})
})
@@ -250,121 +195,58 @@ test.describe('Node Color Adjustments', () => {
await comfyPage.loadWorkflow('every_node_color')
})
test('@perf should adjust opacity via node opacity setting', async ({
test('should adjust opacity via node opacity setting', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'adjust-node-opacity'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('set-opacity-0-5', async () => {
await comfyPage.setSetting('Comfy.Node.Opacity', 0.5)
await comfyPage.page.waitForTimeout(128)
})
await comfyPage.setSetting('Comfy.Node.Opacity', 0.5)
await comfyPage.page.waitForTimeout(128)
// Drag mouse to force canvas to redraw
await perfMonitor.measureOperation('redraw-canvas', async () => {
await comfyPage.page.mouse.move(0, 0)
})
await comfyPage.page.mouse.move(0, 0)
await expect(comfyPage.canvas).toHaveScreenshot('node-opacity-0.5.png')
await perfMonitor.measureOperation('set-opacity-1-0', async () => {
await comfyPage.setSetting('Comfy.Node.Opacity', 1.0)
await comfyPage.page.waitForTimeout(128)
})
await perfMonitor.measureOperation(
'redraw-canvas-full-opacity',
async () => {
await comfyPage.page.mouse.move(8, 8)
}
)
await comfyPage.setSetting('Comfy.Node.Opacity', 1.0)
await comfyPage.page.waitForTimeout(128)
await comfyPage.page.mouse.move(8, 8)
await expect(comfyPage.canvas).toHaveScreenshot('node-opacity-1.png')
await perfMonitor.finishMonitoring(testName)
})
test('@perf should persist color adjustments when changing themes', async ({
test('should persist color adjustments when changing themes', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'persist-opacity-across-themes'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('set-opacity-and-theme', async () => {
await comfyPage.setSetting('Comfy.Node.Opacity', 0.2)
await comfyPage.setSetting('Comfy.ColorPalette', 'arc')
await comfyPage.nextFrame()
})
await perfMonitor.measureOperation('redraw-canvas-with-theme', async () => {
await comfyPage.page.mouse.move(0, 0)
})
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'
)
await perfMonitor.finishMonitoring(testName)
})
test('@perf should not serialize color adjustments in workflow', async ({
test('should not serialize color adjustments in workflow', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'workflow-serialization-color-adjustments'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('apply-color-settings', async () => {
await comfyPage.setSetting('Comfy.Node.Opacity', 0.5)
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
})
await comfyPage.setSetting('Comfy.Node.Opacity', 0.5)
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
const saveWorkflowInterval = 1000
await perfMonitor.measureOperation('wait-for-workflow-save', async () => {
await comfyPage.page.waitForTimeout(saveWorkflowInterval)
await comfyPage.page.waitForTimeout(saveWorkflowInterval)
const workflow = await comfyPage.page.evaluate(() => {
return localStorage.getItem('workflow')
})
let workflow: string | null
await perfMonitor.measureOperation(
'get-workflow-from-storage',
async () => {
workflow = await comfyPage.page.evaluate(() => {
return localStorage.getItem('workflow')
})
}
)
for (const node of JSON.parse(workflow! ?? '{}').nodes) {
for (const node of JSON.parse(workflow ?? '{}').nodes) {
if (node.bgcolor) expect(node.bgcolor).not.toMatch(/hsla/)
if (node.color) expect(node.color).not.toMatch(/hsla/)
}
await perfMonitor.finishMonitoring(testName)
})
// Skip because fails with vue widget nodes (reason not investigated)
test.skip('@perf should lighten node colors when switching to light theme', async ({
test('should lighten node colors when switching to light theme', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'lighten-colors-light-theme'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('apply-light-theme', async () => {
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
await comfyPage.nextFrame()
})
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('node-lightened-colors.png')
await perfMonitor.finishMonitoring(testName)
})
test.describe('Context menu color adjustments', () => {
@@ -375,48 +257,26 @@ test.describe('Node Color Adjustments', () => {
await node?.clickContextMenuOption('Colors')
})
test('@perf should persist color adjustments when changing custom node colors', async ({
test('should persist color adjustments when changing custom node colors', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'persist-opacity-color-change'
await perfMonitor.startMonitoring(testName)
// Context menu interaction - monitor the node color change operation
await perfMonitor.measureOperation('select-red-color', async () => {
await comfyPage.page
.locator('.litemenu-entry.submenu span:has-text("red")')
.click()
})
await comfyPage.page
.locator('.litemenu-entry.submenu span:has-text("red")')
.click()
await expect(comfyPage.canvas).toHaveScreenshot(
'node-opacity-0.3-color-changed.png'
)
await perfMonitor.finishMonitoring(testName)
})
test('@perf should persist color adjustments when removing custom node color', async ({
test('should persist color adjustments when removing custom node color', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'persist-opacity-color-removal'
await perfMonitor.startMonitoring(testName)
// Context menu interaction - monitor the node color removal operation
await perfMonitor.measureOperation('remove-node-color', async () => {
await comfyPage.page
.locator('.litemenu-entry.submenu span:has-text("No color")')
.click()
})
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'
)
await perfMonitor.finishMonitoring(testName)
})
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 133 KiB

View File

@@ -1,7 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { PerformanceMonitor } from '../helpers/performanceMonitor'
test.describe('Copy Paste', () => {
test('Can copy and paste node', async ({ comfyPage }) => {
@@ -12,290 +11,107 @@ test.describe('Copy Paste', () => {
await expect(comfyPage.canvas).toHaveScreenshot('copied-node.png')
})
test('@perf Can copy and paste node with link', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'copy-paste-node-with-link'
await perfMonitor.startMonitoring(testName)
// Click node with performance tracking
await perfMonitor.measureOperation('click-text-encode-node', async () => {
await comfyPage.clickTextEncodeNode1()
})
// Mouse move with performance tracking
await perfMonitor.measureOperation('mouse-move', async () => {
await comfyPage.page.mouse.move(10, 10)
})
// Copy operation with performance tracking
await perfMonitor.measureOperation('copy-operation', async () => {
await comfyPage.ctrlC()
})
// Mark before paste
await perfMonitor.markEvent('before-paste')
// Paste operation with performance tracking
await perfMonitor.measureOperation('paste-operation', async () => {
await comfyPage.page.keyboard.press('Control+Shift+V')
})
// Mark after paste
await perfMonitor.markEvent('after-paste')
await perfMonitor.finishMonitoring(testName)
test('Can copy and paste node with link', async ({ comfyPage }) => {
await comfyPage.clickTextEncodeNode1()
await comfyPage.page.mouse.move(10, 10)
await comfyPage.ctrlC()
await comfyPage.page.keyboard.press('Control+Shift+V')
await expect(comfyPage.canvas).toHaveScreenshot('copied-node-with-link.png')
})
test('@perf Can copy and paste text', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'copy-paste-text'
await perfMonitor.startMonitoring(testName)
test('Can copy and paste text', async ({ comfyPage }) => {
const textBox = comfyPage.widgetTextBox
await perfMonitor.measureOperation('click-textbox', async () => {
await textBox.click()
})
let originalString: string
await perfMonitor.measureOperation('get-input-value', async () => {
originalString = await textBox.inputValue()
})
await perfMonitor.measureOperation('select-text', async () => {
await textBox.selectText()
})
await perfMonitor.measureOperation('copy-text', async () => {
await comfyPage.ctrlC(null)
})
await perfMonitor.measureOperation('paste-text-first', async () => {
await comfyPage.ctrlV(null)
})
await perfMonitor.measureOperation('paste-text-second', async () => {
await comfyPage.ctrlV(null)
})
await textBox.click()
const originalString = await textBox.inputValue()
await textBox.selectText()
await comfyPage.ctrlC(null)
await comfyPage.ctrlV(null)
await comfyPage.ctrlV(null)
const resultString = await textBox.inputValue()
expect(resultString).toBe(originalString! + originalString!)
await perfMonitor.finishMonitoring(testName)
expect(resultString).toBe(originalString + originalString)
})
// skip reason: fails, did not investigate why
test.skip('@perf Can copy and paste widget value', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'copy-paste-widget-value'
await perfMonitor.startMonitoring(testName)
test('Can copy and paste widget value', async ({ comfyPage }) => {
// Copy width value (512) from empty latent node to KSampler's seed.
// KSampler's seed
await perfMonitor.measureOperation('click-ksampler-seed', async () => {
await comfyPage.canvas.click({
position: {
x: 1005,
y: 281
}
})
await comfyPage.canvas.click({
position: {
x: 1005,
y: 281
}
})
await perfMonitor.measureOperation('copy-widget-value', async () => {
await comfyPage.ctrlC(null)
})
await comfyPage.ctrlC(null)
// Empty latent node's width
await perfMonitor.measureOperation('click-empty-latent-width', async () => {
await comfyPage.canvas.click({
position: {
x: 718,
y: 643
}
})
await comfyPage.canvas.click({
position: {
x: 718,
y: 643
}
})
await perfMonitor.measureOperation('paste-widget-value', async () => {
await comfyPage.ctrlV(null)
})
await perfMonitor.measureOperation('confirm-with-enter', async () => {
await comfyPage.page.keyboard.press('Enter')
})
await perfMonitor.finishMonitoring(testName)
await comfyPage.ctrlV(null)
await comfyPage.page.keyboard.press('Enter')
await expect(comfyPage.canvas).toHaveScreenshot('copied-widget-value.png')
})
/**
* https://github.com/Comfy-Org/ComfyUI_frontend/issues/98
*/
test('@perf Paste in text area with node previously copied', async ({
test('Paste in text area with node previously copied', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'paste-text-with-node-copied'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('click-empty-latent-node', async () => {
await comfyPage.clickEmptyLatentNode()
})
await perfMonitor.measureOperation('copy-node', async () => {
await comfyPage.ctrlC(null)
})
await comfyPage.clickEmptyLatentNode()
await comfyPage.ctrlC(null)
const textBox = comfyPage.widgetTextBox
await perfMonitor.measureOperation('click-textbox', async () => {
await textBox.click()
})
await perfMonitor.measureOperation('get-input-value', async () => {
await textBox.inputValue()
})
await perfMonitor.measureOperation('select-text', async () => {
await textBox.selectText()
})
await perfMonitor.measureOperation('copy-text', async () => {
await comfyPage.ctrlC(null)
})
await perfMonitor.measureOperation('paste-text-first', async () => {
await comfyPage.ctrlV(null)
})
await perfMonitor.measureOperation('paste-text-second', async () => {
await comfyPage.ctrlV(null)
})
await perfMonitor.finishMonitoring(testName)
await textBox.click()
await textBox.inputValue()
await textBox.selectText()
await comfyPage.ctrlC(null)
await comfyPage.ctrlV(null)
await comfyPage.ctrlV(null)
await expect(comfyPage.canvas).toHaveScreenshot(
'paste-in-text-area-with-node-previously-copied.png'
)
})
test('@perf Copy text area does not copy node', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'copy-text-no-node'
await perfMonitor.startMonitoring(testName)
test('Copy text area does not copy node', async ({ comfyPage }) => {
const textBox = comfyPage.widgetTextBox
await perfMonitor.measureOperation('click-textbox', async () => {
await textBox.click()
})
await perfMonitor.measureOperation('get-input-value', async () => {
await textBox.inputValue()
})
await perfMonitor.measureOperation('select-text', async () => {
await textBox.selectText()
})
await perfMonitor.measureOperation('copy-text', async () => {
await comfyPage.ctrlC(null)
})
await textBox.click()
await textBox.inputValue()
await textBox.selectText()
await comfyPage.ctrlC(null)
// Unfocus textbox.
await perfMonitor.measureOperation('unfocus-textbox', async () => {
await comfyPage.page.mouse.click(10, 10)
})
await perfMonitor.measureOperation('paste-attempt', async () => {
await comfyPage.ctrlV(null)
})
await perfMonitor.finishMonitoring(testName)
await comfyPage.page.mouse.click(10, 10)
await comfyPage.ctrlV(null)
await expect(comfyPage.canvas).toHaveScreenshot('no-node-copied.png')
})
test('@perf Copy node by dragging + alt', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'copy-node-drag-alt'
await perfMonitor.startMonitoring(testName)
test('Copy node by dragging + alt', async ({ comfyPage }) => {
// TextEncodeNode1
await perfMonitor.measureOperation('mouse-move-to-node', async () => {
await comfyPage.page.mouse.move(618, 191)
})
await perfMonitor.markEvent('alt-key-down')
await comfyPage.page.mouse.move(618, 191)
await comfyPage.page.keyboard.down('Alt')
await perfMonitor.measureOperation('mouse-down', async () => {
await comfyPage.page.mouse.down()
})
await perfMonitor.measureOperation('drag-node', async () => {
await comfyPage.page.mouse.move(100, 100)
})
await perfMonitor.measureOperation('mouse-up', async () => {
await comfyPage.page.mouse.up()
})
await perfMonitor.markEvent('alt-key-up')
await comfyPage.page.mouse.down()
await comfyPage.page.mouse.move(100, 100)
await comfyPage.page.mouse.up()
await comfyPage.page.keyboard.up('Alt')
await perfMonitor.finishMonitoring(testName)
await expect(comfyPage.canvas).toHaveScreenshot('drag-copy-copied-node.png')
})
// skip reason: fails, did not investigate why
test.skip('@perf Can undo paste multiple nodes as single action', async ({
test('Can undo paste multiple nodes as single action', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'undo-paste-multiple-nodes'
const initialCount = await comfyPage.getGraphNodesCount()
expect(initialCount).toBeGreaterThan(1)
await comfyPage.canvas.click()
await comfyPage.ctrlA()
await comfyPage.page.mouse.move(10, 10)
await comfyPage.ctrlC()
await comfyPage.ctrlV()
await perfMonitor.startMonitoring(testName)
const pasteCount = await comfyPage.getGraphNodesCount()
expect(pasteCount).toBe(initialCount * 2)
let initialCount: number
await perfMonitor.measureOperation('get-initial-count', async () => {
initialCount = await comfyPage.getGraphNodesCount()
})
expect(initialCount!).toBeGreaterThan(1)
await perfMonitor.measureOperation('click-canvas', async () => {
await comfyPage.canvas.click()
})
await perfMonitor.measureOperation('select-all', async () => {
await comfyPage.ctrlA()
})
await perfMonitor.measureOperation('mouse-move', async () => {
await comfyPage.page.mouse.move(10, 10)
})
await perfMonitor.measureOperation('copy-all-nodes', async () => {
await comfyPage.ctrlC()
})
await perfMonitor.measureOperation('paste-all-nodes', async () => {
await comfyPage.ctrlV()
})
let pasteCount: number
await perfMonitor.measureOperation('get-paste-count', async () => {
pasteCount = await comfyPage.getGraphNodesCount()
})
expect(pasteCount!).toBe(initialCount! * 2)
await perfMonitor.measureOperation('undo-paste', async () => {
await comfyPage.ctrlZ()
})
let undoCount: number
await perfMonitor.measureOperation('get-undo-count', async () => {
undoCount = await comfyPage.getGraphNodesCount()
})
expect(undoCount!).toBe(initialCount!)
await perfMonitor.finishMonitoring(testName)
await comfyPage.ctrlZ()
const undoCount = await comfyPage.getGraphNodesCount()
expect(undoCount).toBe(initialCount)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 100 KiB

View File

@@ -2,7 +2,6 @@ import { Locator, expect } from '@playwright/test'
import type { Keybinding } from '../../src/schemas/keyBindingSchema'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { PerformanceMonitor } from '../helpers/performanceMonitor'
test.describe('Load workflow warning', () => {
test('Should display a warning when loading a workflow with missing nodes', async ({
@@ -16,89 +15,46 @@ test.describe('Load workflow warning', () => {
})
})
test.skip('@perf Does not report warning on undo/redo', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'undo-redo-no-warning'
await perfMonitor.startMonitoring(testName)
test('Does not report warning on undo/redo', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default')
await perfMonitor.measureOperation('load-workflow', async () => {
await comfyPage.loadWorkflow('missing_nodes')
})
await comfyPage.loadWorkflow('missing_nodes')
await comfyPage.closeDialog()
// Make a change to the graph
await perfMonitor.measureOperation('add-node-sequence', async () => {
await comfyPage.doubleClickCanvas()
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler')
})
await comfyPage.doubleClickCanvas()
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler')
// Undo and redo the change
await perfMonitor.measureOperation('undo-operation', async () => {
await comfyPage.ctrlZ()
})
await comfyPage.ctrlZ()
await expect(comfyPage.page.locator('.comfy-missing-nodes')).not.toBeVisible()
await perfMonitor.measureOperation('redo-operation', async () => {
await comfyPage.ctrlY()
})
await comfyPage.ctrlY()
await expect(comfyPage.page.locator('.comfy-missing-nodes')).not.toBeVisible()
await perfMonitor.finishMonitoring(testName)
})
test.describe('Execution error', () => {
test.skip('@perf Should display an error message when an execution error occurs', async ({
test('Should display an error message when an execution error occurs', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'execution-error-display'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('load-workflow', async () => {
await comfyPage.loadWorkflow('execution_error')
})
await perfMonitor.measureOperation('queue-execution', async () => {
await comfyPage.queueButton.click()
await comfyPage.nextFrame()
})
await comfyPage.loadWorkflow('execution_error')
await comfyPage.queueButton.click()
await comfyPage.nextFrame()
// Wait for the element with the .comfy-execution-error selector to be visible
const executionError = comfyPage.page.locator('.comfy-error-report')
await expect(executionError).toBeVisible()
await perfMonitor.finishMonitoring(testName)
})
test.skip('@perf Can display Issue Report form', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'issue-report-form-display'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('load-workflow', async () => {
await comfyPage.loadWorkflow('execution_error')
})
await perfMonitor.measureOperation('queue-execution', async () => {
await comfyPage.queueButton.click()
await comfyPage.nextFrame()
})
test('Can display Issue Report form', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('execution_error')
await comfyPage.queueButton.click()
await comfyPage.nextFrame()
await comfyPage.page.getByLabel('Help Fix This').click()
const issueReportForm = comfyPage.page.getByText(
'Submit Error Report (Optional)'
)
await expect(issueReportForm).toBeVisible()
await perfMonitor.finishMonitoring(testName)
})
})
@@ -399,29 +355,18 @@ test.describe('Error dialog', () => {
})
test.describe('Signin dialog', () => {
// Skip because fails with vue widget nodes (reason not investigated)
test.skip('@perf Paste content to signin dialog should not paste node on canvas', async ({
test('Paste content to signin dialog should not paste node on canvas', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'signin-dialog-paste-isolation'
await perfMonitor.startMonitoring(testName)
const nodeNum = (await comfyPage.getNodes()).length
await comfyPage.clickEmptyLatentNode()
await comfyPage.ctrlC()
await perfMonitor.measureOperation('copy-node-sequence', async () => {
await comfyPage.clickEmptyLatentNode()
await comfyPage.ctrlC()
})
await perfMonitor.measureOperation('widget-text-operations', async () => {
const textBox = comfyPage.widgetTextBox
await textBox.click()
await textBox.fill('test_password')
await textBox.press('Control+a')
await textBox.press('Control+c')
})
const textBox = comfyPage.widgetTextBox
await textBox.click()
await textBox.fill('test_password')
await textBox.press('Control+a')
await textBox.press('Control+c')
await comfyPage.page.evaluate(() => {
window['app'].extensionManager.dialog.showSignInDialog()
@@ -433,7 +378,5 @@ test.describe('Signin dialog', () => {
await expect(input).toHaveValue('test_password')
expect(await comfyPage.getNodes()).toHaveLength(nodeNum)
await perfMonitor.finishMonitoring(testName)
})
})

View File

@@ -1,35 +1,15 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { PerformanceMonitor } from '../helpers/performanceMonitor'
test.describe('DOM Widget', () => {
test('@perf Collapsed multiline textarea is not visible', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'collapsed-multiline-textarea-visibility'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('load-workflow', async () => {
await comfyPage.loadWorkflow('collapsed_multiline')
})
test('Collapsed multiline textarea is not visible', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('collapsed_multiline')
const textareaWidget = comfyPage.page.locator('.comfy-multiline-input')
await expect(textareaWidget).not.toBeVisible()
await perfMonitor.finishMonitoring(testName)
})
test.skip('@perf Multiline textarea correctly collapses', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'multiline-textarea-collapse'
await perfMonitor.startMonitoring(testName)
test('Multiline textarea correctly collapses', async ({ comfyPage }) => {
const multilineTextAreas = comfyPage.page.locator('.comfy-multiline-input')
const firstMultiline = multilineTextAreas.first()
const lastMultiline = multilineTextAreas.last()
@@ -37,91 +17,34 @@ test.describe('DOM Widget', () => {
await expect(firstMultiline).toBeVisible()
await expect(lastMultiline).toBeVisible()
let nodes: any[]
await perfMonitor.measureOperation('get-nodes-by-type', async () => {
nodes = await comfyPage.getNodeRefsByType('CLIPTextEncode')
})
await perfMonitor.markEvent('before-collapse')
await perfMonitor.measureOperation('collapse-all-nodes', async () => {
for (const node of nodes!) {
await node.click('collapse')
}
})
await perfMonitor.markEvent('after-collapse')
const nodes = await comfyPage.getNodeRefsByType('CLIPTextEncode')
for (const node of nodes) {
await node.click('collapse')
}
await expect(firstMultiline).not.toBeVisible()
await expect(lastMultiline).not.toBeVisible()
await perfMonitor.finishMonitoring(testName)
})
// Skip because fails with vue widget nodes (reason not investigated)
test.skip('@perf Position update when entering focus mode', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'focus-mode-position-update'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('set-menu-setting', async () => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
})
await perfMonitor.measureOperation('toggle-focus-mode', async () => {
await comfyPage.executeCommand('Workspace.ToggleFocusMode')
})
await perfMonitor.measureOperation('wait-frame-update', async () => {
await comfyPage.nextFrame()
})
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')
await perfMonitor.finishMonitoring(testName)
})
// No DOM widget should be created by creation of interim LGraphNode objects.
test.skip('@perf Copy node with DOM widget by dragging + alt', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'copy-node-alt-drag'
await perfMonitor.startMonitoring(testName)
let initialCount: number
await perfMonitor.measureOperation('get-initial-widget-count', async () => {
initialCount = await comfyPage.getDOMWidgetCount()
})
await perfMonitor.markEvent('before-copy-operation')
test('Copy node with DOM widget by dragging + alt', async ({ comfyPage }) => {
const initialCount = await comfyPage.getDOMWidgetCount()
// TextEncodeNode1
await perfMonitor.measureOperation('position-mouse-on-node', async () => {
await comfyPage.page.mouse.move(618, 191)
})
await comfyPage.page.mouse.move(618, 191)
await comfyPage.page.keyboard.down('Alt')
await comfyPage.page.mouse.down()
await comfyPage.page.mouse.move(100, 100)
await comfyPage.page.mouse.up()
await comfyPage.page.keyboard.up('Alt')
await perfMonitor.measureOperation('alt-drag-copy', async () => {
await comfyPage.page.keyboard.down('Alt')
await comfyPage.page.mouse.down()
await comfyPage.page.mouse.move(100, 100)
await comfyPage.page.mouse.up()
await comfyPage.page.keyboard.up('Alt')
})
await perfMonitor.markEvent('after-copy-operation')
let finalCount: number
await perfMonitor.measureOperation('get-final-widget-count', async () => {
finalCount = await comfyPage.getDOMWidgetCount()
})
expect(finalCount!).toBe(initialCount! + 1)
await perfMonitor.finishMonitoring(testName)
const finalCount = await comfyPage.getDOMWidgetCount()
expect(finalCount).toBe(initialCount + 1)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 95 KiB

View File

@@ -1,7 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { PerformanceMonitor } from '../helpers/performanceMonitor'
test.describe('Graph Canvas Menu', () => {
test.beforeEach(async ({ comfyPage }) => {
@@ -10,28 +9,14 @@ test.describe('Graph Canvas Menu', () => {
await comfyPage.setSetting('Comfy.LinkRenderMode', 2)
})
test('@perf Can toggle link visibility', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'toggle-link-visibility'
await perfMonitor.startMonitoring(testName)
test('Can toggle link visibility', async ({ comfyPage }) => {
// Note: `Comfy.Graph.CanvasMenu` is disabled in comfyPage setup.
// so no cleanup is needed.
await perfMonitor.measureOperation('enable-canvas-menu', async () => {
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true)
})
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true)
const button = comfyPage.page.getByTestId('toggle-link-visibility-button')
await perfMonitor.markEvent('before-hide-links')
await perfMonitor.measureOperation('hide-links', async () => {
await button.click()
await comfyPage.nextFrame()
})
await perfMonitor.markEvent('after-hide-links')
// Screenshot assertions and validations stay outside performance monitoring
await button.click()
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'canvas-with-hidden-links.png'
)
@@ -42,21 +27,13 @@ test.describe('Graph Canvas Menu', () => {
hiddenLinkRenderMode
)
await perfMonitor.markEvent('before-show-links')
await perfMonitor.measureOperation('show-links', async () => {
await button.click()
await comfyPage.nextFrame()
})
await perfMonitor.markEvent('after-show-links')
// Screenshot assertions and validations stay outside performance monitoring
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 perfMonitor.finishMonitoring(testName)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View File

@@ -2,7 +2,6 @@ import { expect } from '@playwright/test'
import { ComfyPage, comfyPageFixture as test } from '../fixtures/ComfyPage'
import type { NodeReference } from '../fixtures/utils/litegraphUtils'
import { PerformanceMonitor } from '../helpers/performanceMonitor'
test.describe('Group Node', () => {
test.describe('Node library sidebar', () => {
@@ -18,51 +17,29 @@ test.describe('Group Node', () => {
await libraryTab.open()
})
test('Is added to node library sidebar', async ({ comfyPage }) => {
test.skip('Is added to node library sidebar', async ({ comfyPage }) => {
expect(await libraryTab.getFolder('group nodes').count()).toBe(1)
})
test('@perf Can be added to canvas using node library sidebar', async ({
test.skip('Can be added to canvas using node library sidebar', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'add-group-node-from-library'
await perfMonitor.startMonitoring(testName)
const initialNodeCount = await comfyPage.getGraphNodesCount()
// Add group node from node library sidebar
await perfMonitor.measureOperation('expand-category-folder', async () => {
await libraryTab.getFolder(groupNodeCategory).click()
})
await perfMonitor.measureOperation('add-node-from-library', async () => {
await libraryTab.getNode(groupNodeName).click()
})
await libraryTab.getFolder(groupNodeCategory).click()
await libraryTab.getNode(groupNodeName).click()
// Verify the node is added to the canvas
expect(await comfyPage.getGraphNodesCount()).toBe(initialNodeCount + 1)
await perfMonitor.finishMonitoring(testName)
})
test('@perf Can be bookmarked and unbookmarked', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'bookmark-unbookmark-group-node'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('expand-category-folder', async () => {
await libraryTab.getFolder(groupNodeCategory).click()
})
await perfMonitor.measureOperation('bookmark-node', async () => {
await libraryTab
.getNode(groupNodeName)
.locator('.bookmark-button')
.click()
})
test.skip('Can be bookmarked and unbookmarked', async ({ comfyPage }) => {
await libraryTab.getFolder(groupNodeCategory).click()
await libraryTab
.getNode(groupNodeName)
.locator('.bookmark-button')
.click()
// Verify the node is added to the bookmarks tab
expect(
@@ -72,23 +49,19 @@ test.describe('Group Node', () => {
expect(await libraryTab.getNode(groupNodeName).count()).not.toBe(0)
// Unbookmark the node
await perfMonitor.measureOperation('unbookmark-node', async () => {
await libraryTab
.getNode(groupNodeName)
.locator('.bookmark-button')
.first()
.click()
})
await libraryTab
.getNode(groupNodeName)
.locator('.bookmark-button')
.first()
.click()
// Verify the node is removed from the bookmarks tab
expect(
await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
).toHaveLength(0)
await perfMonitor.finishMonitoring(testName)
})
test('Displays preview on bookmark hover', async ({ comfyPage }) => {
test.skip('Displays preview on bookmark hover', async ({ comfyPage }) => {
await libraryTab.getFolder(groupNodeCategory).click()
await libraryTab
.getNode(groupNodeName)
@@ -122,38 +95,18 @@ test.describe('Group Node', () => {
)
})
test.skip('@perf Displays tooltip on title hover', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'group-node-tooltip-display'
await perfMonitor.startMonitoring(testName)
test.skip('Displays tooltip on title hover', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.EnableTooltips', true)
await perfMonitor.measureOperation('convert-to-group-node', async () => {
await comfyPage.convertAllNodesToGroupNode('Group Node')
})
await perfMonitor.measureOperation('hover-for-tooltip', async () => {
await comfyPage.page.mouse.move(47, 173)
const tooltipTimeout = 500
await comfyPage.page.waitForTimeout(tooltipTimeout + 16)
})
await comfyPage.convertAllNodesToGroupNode('Group Node')
await comfyPage.page.mouse.move(47, 173)
const tooltipTimeout = 500
await comfyPage.page.waitForTimeout(tooltipTimeout + 16)
await expect(comfyPage.page.locator('.node-tooltip')).toBeVisible()
await perfMonitor.finishMonitoring(testName)
})
// Skip because fails with vue widget nodes (reason not investigated)
test.skip('@perf Manage group opens with the correct group selected', async ({
test.skip('Manage group opens with the correct group selected', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'manage-group-node-selection'
await perfMonitor.startMonitoring(testName)
const makeGroup = async (name, type1, type2) => {
const node1 = (await comfyPage.getNodeRefsByType(type1))[0]
const node2 = (await comfyPage.getNodeRefsByType(type2))[0]
@@ -164,44 +117,21 @@ test.describe('Group Node', () => {
return await node2.convertToGroupNode(name)
}
let group1
await perfMonitor.measureOperation('create-first-group', async () => {
group1 = await makeGroup('g1', 'CLIPTextEncode', 'CheckpointLoaderSimple')
})
let group2
await perfMonitor.measureOperation('create-second-group', async () => {
group2 = await makeGroup('g2', 'EmptyLatentImage', 'KSampler')
})
let manage1
await perfMonitor.measureOperation('open-first-manage-dialog', async () => {
manage1 = await group1.manageGroupNode()
await comfyPage.nextFrame()
})
expect(await manage1.getSelectedNodeType()).toBe('g1')
await perfMonitor.measureOperation(
'close-first-manage-dialog',
async () => {
await manage1.close()
}
const group1 = await makeGroup(
'g1',
'CLIPTextEncode',
'CheckpointLoaderSimple'
)
const group2 = await makeGroup('g2', 'EmptyLatentImage', 'KSampler')
const manage1 = await group1.manageGroupNode()
await comfyPage.nextFrame()
expect(await manage1.getSelectedNodeType()).toBe('g1')
await manage1.close()
await expect(manage1.root).not.toBeVisible()
let manage2
await perfMonitor.measureOperation(
'open-second-manage-dialog',
async () => {
manage2 = await group2.manageGroupNode()
}
)
const manage2 = await group2.manageGroupNode()
expect(await manage2.getSelectedNodeType()).toBe('g2')
await perfMonitor.finishMonitoring(testName)
})
test('Preserves hidden input configuration when containing duplicate node types', async ({
@@ -235,14 +165,9 @@ test.describe('Group Node', () => {
expect(visibleInputCount).toBe(2)
})
test.skip('@perf Reconnects inputs after configuration changed via manage dialog save', async ({
test.skip('Reconnects inputs after configuration changed via manage dialog save', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'reconnect-inputs-after-config-change'
await perfMonitor.startMonitoring(testName)
const expectSingleNode = async (type: string) => {
const nodes = await comfyPage.getNodeRefsByType(type)
expect(nodes).toHaveLength(1)
@@ -250,65 +175,30 @@ test.describe('Group Node', () => {
}
const latent = await expectSingleNode('EmptyLatentImage')
const sampler = await expectSingleNode('KSampler')
// Remove existing link
const samplerInput = await sampler.getInput(0)
await perfMonitor.measureOperation('remove-existing-links', async () => {
await samplerInput.removeLinks()
})
await samplerInput.removeLinks()
// Group latent + sampler
await perfMonitor.measureOperation('select-nodes-for-group', async () => {
await latent.click('title', {
modifiers: ['Shift']
})
await sampler.click('title', {
modifiers: ['Shift']
})
await latent.click('title', {
modifiers: ['Shift']
})
let groupNode
await perfMonitor.measureOperation('convert-to-group-node', async () => {
groupNode = await sampler.convertToGroupNode()
await sampler.click('title', {
modifiers: ['Shift']
})
const groupNode = await sampler.convertToGroupNode()
// Connect node to group
const ckpt = await expectSingleNode('CheckpointLoaderSimple')
let input
await perfMonitor.measureOperation('connect-nodes', async () => {
input = await ckpt.connectOutput(0, groupNode, 0)
})
const input = await ckpt.connectOutput(0, groupNode, 0)
expect(await input.getLinkCount()).toBe(1)
// Modify the group node via manage dialog
await perfMonitor.markEvent('before-manage-dialog')
let manage
await perfMonitor.measureOperation('open-manage-dialog', async () => {
manage = await groupNode.manageGroupNode()
})
await perfMonitor.measureOperation(
'configure-in-manage-dialog',
async () => {
await manage.selectNode('KSampler')
await manage.changeTab('Inputs')
await manage.setLabel('model', 'test')
await manage.save()
}
)
await perfMonitor.measureOperation('close-manage-dialog', async () => {
await manage.close()
})
await perfMonitor.markEvent('after-manage-dialog')
const manage = await groupNode.manageGroupNode()
await manage.selectNode('KSampler')
await manage.changeTab('Inputs')
await manage.setLabel('model', 'test')
await manage.save()
await manage.close()
// Ensure the link is still present
expect(await input.getLinkCount()).toBe(1)
await perfMonitor.finishMonitoring(testName)
})
test('Loads from a workflow using the legacy path separator ("/")', async ({
@@ -364,131 +254,57 @@ test.describe('Group Node', () => {
await groupNode.copy()
})
test('@perf Copies and pastes group node within the same workflow', async ({
test('Copies and pastes group node within the same workflow', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'copy-paste-group-node-same-workflow'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('paste-group-node', async () => {
await comfyPage.ctrlV()
})
await comfyPage.ctrlV()
await verifyNodeLoaded(comfyPage, 2)
await perfMonitor.finishMonitoring(testName)
})
test('@perf Copies and pastes group node after clearing workflow', async ({
test('Copies and pastes group node after clearing workflow', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'copy-paste-group-node-after-clear'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('clear-workflow', async () => {
await comfyPage.menu.topbar.triggerTopbarCommand([
'Edit',
'Clear Workflow'
])
})
await perfMonitor.measureOperation('paste-group-node', async () => {
await comfyPage.ctrlV()
})
await comfyPage.menu.topbar.triggerTopbarCommand([
'Edit',
'Clear Workflow'
])
await comfyPage.ctrlV()
await verifyNodeLoaded(comfyPage, 1)
await perfMonitor.finishMonitoring(testName)
})
test('@perf Copies and pastes group node into a newly created blank workflow', async ({
test('Copies and pastes group node into a newly created blank workflow', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'copy-paste-group-node-new-workflow'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('create-new-workflow', async () => {
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
})
await perfMonitor.measureOperation('paste-group-node', async () => {
await comfyPage.ctrlV()
})
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
await comfyPage.ctrlV()
await verifyNodeLoaded(comfyPage, 1)
await perfMonitor.finishMonitoring(testName)
})
test('@perf Copies and pastes group node across different workflows', async ({
test('Copies and pastes group node across different workflows', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'copy-paste-group-node-different-workflow'
await comfyPage.loadWorkflow('default')
await comfyPage.ctrlV()
await verifyNodeLoaded(comfyPage, 1)
})
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation(
'load-different-workflow',
async () => {
await comfyPage.loadWorkflow('default')
}
test('Serializes group node after copy and paste across workflows', async ({
comfyPage
}) => {
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
await comfyPage.ctrlV()
const currentGraphState = await comfyPage.page.evaluate(() =>
window['app'].graph.serialize()
)
await perfMonitor.measureOperation('paste-group-node', async () => {
await comfyPage.ctrlV()
})
await verifyNodeLoaded(comfyPage, 1)
await perfMonitor.finishMonitoring(testName)
})
test('@perf Serializes group node after copy and paste across workflows', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'serialize-group-node-cross-workflow'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation('create-new-workflow', async () => {
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
})
await perfMonitor.measureOperation('paste-group-node', async () => {
await comfyPage.ctrlV()
})
let currentGraphState
await perfMonitor.measureOperation('serialize-graph', async () => {
currentGraphState = await comfyPage.page.evaluate(() =>
window['app'].graph.serialize()
)
})
await test.step('Load workflow containing a group node pasted from a different workflow', async () => {
await perfMonitor.measureOperation(
'load-serialized-workflow',
async () => {
await comfyPage.page.evaluate(
(workflow) => window['app'].loadGraphData(workflow),
currentGraphState
)
await comfyPage.nextFrame()
}
await comfyPage.page.evaluate(
(workflow) => window['app'].loadGraphData(workflow),
currentGraphState
)
await comfyPage.nextFrame()
await verifyNodeLoaded(comfyPage, 1)
})
await perfMonitor.finishMonitoring(testName)
})
})
@@ -499,31 +315,12 @@ test.describe('Group Node', () => {
await comfyPage.page.waitForTimeout(300)
expect(await comfyPage.getVisibleToastCount()).toBe(1)
})
test('@perf Convert to group node, selected 1 node', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'convert-single-node-to-group-keybinding'
await perfMonitor.startMonitoring(testName)
test('Convert to group node, selected 1 node', async ({ comfyPage }) => {
expect(await comfyPage.getVisibleToastCount()).toBe(0)
await perfMonitor.measureOperation('select-node', async () => {
await comfyPage.clickTextEncodeNode1()
})
await perfMonitor.measureOperation(
'trigger-group-keybinding',
async () => {
await comfyPage.page.keyboard.press('Alt+g')
await comfyPage.page.waitForTimeout(300)
}
)
await comfyPage.clickTextEncodeNode1()
await comfyPage.page.keyboard.press('Alt+g')
await comfyPage.page.waitForTimeout(300)
expect(await comfyPage.getVisibleToastCount()).toBe(1)
await perfMonitor.finishMonitoring(testName)
})
})
})

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 93 KiB

View File

@@ -1,7 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { PerformanceMonitor } from '../helpers/performanceMonitor'
function listenForEvent(): Promise<Event> {
return new Promise<Event>((resolve) => {
@@ -12,50 +11,27 @@ function listenForEvent(): Promise<Event> {
}
test.describe('Canvas Event', () => {
test.skip('@perf Emit litegraph:canvas empty-release', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'canvas-empty-release'
await perfMonitor.startMonitoring(testName)
test('Emit litegraph:canvas empty-release', async ({ comfyPage }) => {
const eventPromise = comfyPage.page.evaluate(listenForEvent)
await perfMonitor.measureOperation('disconnect-edge', async () => {
await comfyPage.disconnectEdge()
})
const disconnectPromise = comfyPage.disconnectEdge()
const event = await eventPromise
await disconnectPromise
expect(event).not.toBeNull()
// No further check on event content as the content is dropped by
// playwright for some reason.
// See https://github.com/microsoft/playwright/issues/31580
await perfMonitor.finishMonitoring(testName)
})
// Skip because fails with vue widget nodes (reason not investigated)
test.skip('@perf Emit litegraph:canvas empty-double-click', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'canvas-double-click'
await perfMonitor.startMonitoring(testName)
test('Emit litegraph:canvas empty-double-click', async ({ comfyPage }) => {
const eventPromise = comfyPage.page.evaluate(listenForEvent)
await perfMonitor.measureOperation('double-click-canvas', async () => {
await comfyPage.doubleClickCanvas()
})
const doubleClickPromise = comfyPage.doubleClickCanvas()
const event = await eventPromise
await doubleClickPromise
expect(event).not.toBeNull()
// No further check on event content as the content is dropped by
// playwright for some reason.
// See https://github.com/microsoft/playwright/issues/31580
await perfMonitor.finishMonitoring(testName)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View File

@@ -3,200 +3,109 @@ import { expect } from '@playwright/test'
import type { ComfyApp } from '../../src/scripts/app'
import { NodeBadgeMode } from '../../src/types/nodeSource'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { PerformanceMonitor } from '../helpers/performanceMonitor'
test.describe('Node Badge', () => {
test('@perf Can add badge', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'add-single-badge'
test('Can add badge', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
const LGraphBadge = window['LGraphBadge']
const app = window['app'] as ComfyApp
const graph = app.graph
const nodes = graph.nodes
await perfMonitor.startMonitoring(testName)
for (const node of nodes) {
node.badges = [new LGraphBadge({ text: 'Test Badge' })]
}
await perfMonitor.measureOperation('add-badge-to-nodes', async () => {
await comfyPage.page.evaluate(() => {
const LGraphBadge = window['LGraphBadge']
const app = window['app'] as ComfyApp
const graph = app.graph
const nodes = graph.nodes
for (const node of nodes) {
node.badges = [new LGraphBadge({ text: 'Test Badge' })]
}
graph.setDirtyCanvas(true, true)
})
graph.setDirtyCanvas(true, true)
})
await expect(comfyPage.canvas).toHaveScreenshot('node-badge.png')
await perfMonitor.finishMonitoring(testName)
})
test('@perf Can add multiple badges', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'add-multiple-badges'
test('Can add multiple badges', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
const LGraphBadge = window['LGraphBadge']
const app = window['app'] as ComfyApp
const graph = app.graph
const nodes = graph.nodes
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation(
'add-multiple-badges-to-nodes',
async () => {
await comfyPage.page.evaluate(() => {
const LGraphBadge = window['LGraphBadge']
const app = window['app'] as ComfyApp
const graph = app.graph
const nodes = graph.nodes
for (const node of nodes) {
node.badges = [
new LGraphBadge({ text: 'Test Badge 1' }),
new LGraphBadge({ text: 'Test Badge 2' })
]
}
graph.setDirtyCanvas(true, true)
})
for (const node of nodes) {
node.badges = [
new LGraphBadge({ text: 'Test Badge 1' }),
new LGraphBadge({ text: 'Test Badge 2' })
]
}
)
graph.setDirtyCanvas(true, true)
})
await expect(comfyPage.canvas).toHaveScreenshot('node-badge-multiple.png')
await perfMonitor.finishMonitoring(testName)
})
test('@perf Can add badge left-side', async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'add-badge-left-position'
test('Can add badge left-side', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
const LGraphBadge = window['LGraphBadge']
const app = window['app'] as ComfyApp
const graph = app.graph
const nodes = graph.nodes
await perfMonitor.startMonitoring(testName)
for (const node of nodes) {
node.badges = [new LGraphBadge({ text: 'Test Badge' })]
// @ts-expect-error - Enum value
node.badgePosition = 'top-left'
}
await perfMonitor.measureOperation('add-badge-with-position', async () => {
await comfyPage.page.evaluate(() => {
const LGraphBadge = window['LGraphBadge']
const app = window['app'] as ComfyApp
const graph = app.graph
const nodes = graph.nodes
for (const node of nodes) {
node.badges = [new LGraphBadge({ text: 'Test Badge' })]
// @ts-expect-error - Enum value
node.badgePosition = 'top-left'
}
graph.setDirtyCanvas(true, true)
})
graph.setDirtyCanvas(true, true)
})
await expect(comfyPage.canvas).toHaveScreenshot('node-badge-left.png')
await perfMonitor.finishMonitoring(testName)
})
})
test.describe('Node source badge', () => {
Object.values(NodeBadgeMode).forEach(async (mode) => {
test(`@perf Shows node badges (${mode})`, async ({ comfyPage }) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = `node-source-badge-${mode}`
await perfMonitor.startMonitoring(testName)
test(`Shows node badges (${mode})`, async ({ comfyPage }) => {
// Execution error workflow has both custom node and core node.
await perfMonitor.measureOperation('load-workflow', async () => {
await comfyPage.loadWorkflow('execution_error')
})
await perfMonitor.measureOperation(
'configure-badge-settings',
async () => {
await comfyPage.setSetting(
'Comfy.NodeBadge.NodeSourceBadgeMode',
mode
)
await comfyPage.setSetting('Comfy.NodeBadge.NodeIdBadgeMode', mode)
}
)
await perfMonitor.measureOperation('render-badges', async () => {
await comfyPage.nextFrame()
await comfyPage.resetView()
})
await comfyPage.loadWorkflow('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`)
await perfMonitor.finishMonitoring(testName)
})
})
})
test.describe('Node badge color', () => {
test('@perf Can show node badge with unknown color palette', async ({
test('Can show node badge with unknown color palette', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'node-badge-unknown-color-palette'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation(
'configure-badge-and-palette',
async () => {
await comfyPage.setSetting(
'Comfy.NodeBadge.NodeIdBadgeMode',
NodeBadgeMode.ShowAll
)
await comfyPage.setSetting('Comfy.ColorPalette', 'unknown')
}
await comfyPage.setSetting(
'Comfy.NodeBadge.NodeIdBadgeMode',
NodeBadgeMode.ShowAll
)
await perfMonitor.measureOperation(
'render-with-unknown-palette',
async () => {
await comfyPage.nextFrame()
// Click empty space to trigger canvas re-render.
await comfyPage.clickEmptySpace()
}
)
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'
)
await perfMonitor.finishMonitoring(testName)
})
test('@perf Can show node badge with light color palette', async ({
test('Can show node badge with light color palette', async ({
comfyPage
}) => {
const perfMonitor = new PerformanceMonitor(comfyPage.page)
const testName = 'node-badge-light-color-palette'
await perfMonitor.startMonitoring(testName)
await perfMonitor.measureOperation(
'configure-badge-and-light-palette',
async () => {
await comfyPage.setSetting(
'Comfy.NodeBadge.NodeIdBadgeMode',
NodeBadgeMode.ShowAll
)
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
}
await comfyPage.setSetting(
'Comfy.NodeBadge.NodeIdBadgeMode',
NodeBadgeMode.ShowAll
)
await perfMonitor.measureOperation(
'render-with-light-palette',
async () => {
await comfyPage.nextFrame()
// Click empty space to trigger canvas re-render.
await comfyPage.clickEmptySpace()
}
)
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'
)
await perfMonitor.finishMonitoring(testName)
})
})

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