Compare commits

..

33 Commits

Author SHA1 Message Date
snomiao
2e6bf4896e [feat] Add comprehensive caching to CI/CD workflows
- Add ESLint cache (.eslintcache) to auto-fix and fork PR workflows
- Add npm cache to all Node.js setup steps across workflows
- Add Vite build cache (node_modules/.vite) to improve build performance
- Add Playwright browser cache with version-specific keys
- Optimize test-ui, vitest, release, and auto-fix workflows

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-14 13:45:29 +00:00
snomiao
b87a251e6b fix: Replace manual workflow setup with Comfy-Org setup action for Playwright tests
- Use Comfy-Org/ComfyUI_frontend_setup_action@v2.3 instead of manual Node.js setup
- Add working-directory: ComfyUI_frontend to all workflow steps
- Use original i18n workflow commit strategy instead of stefanzweifel/git-auto-commit-action
- Remove test code that was triggering workflow failures
- This fixes the Playwright test failures by properly setting up ComfyUI backend

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-14 12:51:40 +00:00
snomiao
1aa2a66b01 fix: Replace raw text with i18n translation key to test workflow 2025-08-14 11:59:34 +00:00
snomiao
253853be6f kick-format 2025-08-14 11:53:49 +00:00
snomiao
7f86f3fec1 e Merge branch 'sno-lint-issue-write--merge-with-locales-update' of github.com:Comfy-Org/ComfyUI_frontend into sno-lint-issue-write--merge-with-locales-update 2025-08-14 11:48:14 +00:00
snomiao
9a381415e5 chore(auto-fix-and-update.yaml): update comments to clarify job purposes and actions for better understanding 2025-08-14 11:48:09 +00:00
snomiao
b61ecef7fd [auto-fix] Apply ESLint and Prettier fixe 2025-08-14 11:43:04 +00:00
snomiao
aab6c0c954 kick-format-modify-main-ts 2025-08-14 11:42:47 +00:00
snomiao
8afc0b9012 kick-format 2025-08-14 11:42:47 +00:00
snomiao
3a869d98da Merge branch 'sno-lint-issue-write--merge-with-locales-update' of github.com:Comfy-Org/ComfyUI_frontend into sno-lint-issue-write--merge-with-locales-update 2025-08-14 11:41:24 +00:00
snomiao
3021b60dc4 [feat] Merge commit-back workflows into unified auto-fix-and-update workflow
- Combines lint-and-format and locale update workflows to avoid git conflicts
- Uses stefanzweifel/git-auto-commit-action@v5 for reliable commits
- Includes proper permissions (contents: write, pull-requests: write, issues: write)
- Handles both main repo PRs (with auto-commit) and fork PRs (with guidance comments)
- Updates locales only when relevant code changes are detected
- Single commit strategy prevents conflicts between different auto-fix operations

Resolves conflicts mentioned in PR #4940 comments where multiple commit-back
workflows would interfere with each other.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-14 11:39:48 +00:00
snomiao
1643baf761 feat: Add issues write permission to workflows to avoid lint and i18n workflow conflicts
- Add issues: write permission to lint-and-format.yaml
- Add issues: write permission to i18n.yaml
- Prevents workflow conflicts when updating PR statuses or comments
2025-08-14 11:39:48 +00:00
snomiao
8d4846770f [auto-fix] Apply ESLint and Prettier fixe 2025-08-14 11:39:48 +00:00
snomiao
a88d46bdb7 use auto-commit-action to handle pullrequest-ref name 2025-08-14 11:39:48 +00:00
snomiao
39686ef0f6 kick-format-modify-main-ts 2025-08-14 11:39:48 +00:00
snomiao
943359e559 kick-format 2025-08-14 11:39:48 +00:00
snomiao
a7dd696cbe [feat] Add issues: write permission to lint-and-format workflow
The lint-and-format workflow creates comments on PRs, which requires
the issues: write permission to function properly.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-14 11:39:48 +00:00
snomiao
405b97c5ef [feat] Merge commit-back workflows into unified auto-fix-and-update workflow
- Combines lint-and-format and locale update workflows to avoid git conflicts
- Uses stefanzweifel/git-auto-commit-action@v5 for reliable commits
- Includes proper permissions (contents: write, pull-requests: write, issues: write)
- Handles both main repo PRs (with auto-commit) and fork PRs (with guidance comments)
- Updates locales only when relevant code changes are detected
- Single commit strategy prevents conflicts between different auto-fix operations

Resolves conflicts mentioned in PR #4940 comments where multiple commit-back
workflows would interfere with each other.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-14 10:47:03 +00:00
Alexander Piskun
e27b44bdac fix pricing for KlingImage2VideoNode (#4957)
## Summary

Following up #4938 where I forgot to add pricing for new model in the
`KlingImage2VideoNode`.

## Screenshots (if applicable)

<img width="1461" height="1228" alt="Screenshot from 2025-08-13
09-15-21"
src="https://github.com/user-attachments/assets/01be8ab9-820b-4112-9a54-1ce4f23de4eb"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-4957-fix-pricing-for-KlingImage2VideoNode-24e6d73d36508122b40ede36fdd50115)
by [Unito](https://www.unito.io)
2025-08-14 10:45:33 +00:00
Christian Byrne
01a0ebc8c8 [feat] Restore group node conversion menu with deprecated label (#4967)
## Summary
- Partially reverts commit c84218d6 to restore group node functionality
in context menus
- Adds "(Deprecated)" label to indicate the feature is deprecated
- Fixes TypeError when right-clicking on group nodes
- Re-enables tests that were disabled when the feature was removed

## Changes
1. **Restored context menu options** - Added back "Convert to Group Node
(Deprecated)" and "Manage Group Nodes" menu items
2. **Fixed null reference error** - Added null-safe operator to prevent
errors when right-clicking group nodes
3. **Re-enabled tests** - Restored 7 tests that were disabled in commit
586f8824

## Test plan
- [x] Right-click on canvas → verify "Convert to Group Node
(Deprecated)" appears
- [x] Right-click on nodes → verify the same menu option appears
- [x] Select multiple nodes and use the menu option → verify conversion
works
- [x] Right-click on group nodes → verify no errors occur
- [x] Run browser tests → verify all re-enabled tests pass

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-4967-feat-Restore-group-node-conversion-menu-with-deprecated-label-24e6d73d36508149a6f2dbef47223e94)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions <github-actions@github.com>
2025-08-14 10:45:33 +00:00
Christian Byrne
cd970bd29d fix: Add guards for _listenerController.abort() calls in SubgraphNode (#4968)
This fix adds guards before calling `_listenerController.abort()` to
prevent runtime errors when loading workflows. The guards check that
`_listenerController` exists and has an `abort` function before calling
it, matching the pattern used in Comfy-Org/litegraph.js#1134.

Fixes https://github.com/Comfy-Org/ComfyUI_frontend/issues/4907

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-4968-fix-Add-guards-for-_listenerController-abort-calls-in-SubgraphNode-24e6d73d3650813ebeeed69ee676faeb)
by [Unito](https://www.unito.io)
2025-08-14 10:45:33 +00:00
Terry Jia
ad7d23177e show group self color in minimap (#4954)
a tiny fix that show group self color in minimap when checking node
color

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-4954-show-group-self-color-in-minimap-24e6d73d3650812dbc58e9b134805f2d)
by [Unito](https://www.unito.io)
2025-08-14 10:45:32 +00:00
AustinMroz
ccaeab1541 Bundled subgraph fixes (#4964)
### Group support for subgraph unpacking
The unpacking code would silently delete groups (the cosmetic colored
rectangles). They are now correctly transferred.
### Fix subgraph node position on conversion to subgraph
Converting to subgraph will no longer cause nodes to inch upwards

![subgraph-conversion-positioning](https://github.com/user-attachments/assets/e120c3f9-5602-4dba-9075-c1eadb534f9a)
### Make unpacking use same positioning calcs as conversion
Non trivial, but unpacking is now a proper inverse for conversion.

![subgraph-conversion-inverse](https://github.com/user-attachments/assets/4fcaffca-1c97-4d71-93f7-1af569b1c941)
### Clean up old output links when unpacking
Unpacked nodes were left with dangling outputs. This would cause
cascading issues later, such as when consecutively unpacking nested
subgraphs.
### Minor refactoring for code clarity

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-4964-Bundled-subgraph-fixes-24e6d73d365081d3a043ef1531d9d38a)
by [Unito](https://www.unito.io)
2025-08-14 10:45:32 +00:00
pythongosssss
3024dcc41b Update side toolbar menu (#4946)
Side toolbar menu UI updates

## Summary

- Currently the template modal is very hidden. Many users do not find it
- The current icons are quite aleatory 

## Changes

 **What**: 
- Add templates shortcut button
- Add item label in normal size
- Use custom icon

Critical design decisions or edge cases that need attention:
- Sidebar tabs registered using custom icons will have their associated
command registed with an undefined icon (currently only string icons are
accepted, not components). I couldn't see anywhere directly using this
icon, but we should consider autogenerating an icon font so we can use
classes for our custom icons (or locating and updating locations to
support both icon types)

## Screenshots (if applicable)
Normal mode:
<img width="621" height="1034" alt="image"
src="https://github.com/user-attachments/assets/c1d1cee2-004e-4ff8-b3fa-197329b0d2ae"
/>

Small mode:
<img width="176" height="325" alt="image"
src="https://github.com/user-attachments/assets/3824b8f6-bc96-4e62-aece-f0265113d2e3"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-4946-Update-side-toolbar-menu-24d6d73d365081c5b2bdc0ee8b61dc50)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-08-14 10:45:32 +00:00
snomiao
c5581c9393 [feat] Add alternative package manager lockfiles to .gitignore (#4961)
## Summary
- Add bun.lock, bun.lockb, pnpm-lock.yaml, and yarn.lock to .gitignore
- Allows users to use faster package managers (Bun, pnpm) without making
git status dirty
- Maintains npm as the default while supporting developer choice of
package manager

## Test plan
- [x] Verify .gitignore changes are correct
- [ ] Test that creating these lockfiles doesn't show in git status
- [ ] Confirm existing npm functionality remains unaffected

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

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-4961-feat-Add-alternative-package-manager-lockfiles-to-gitignore-24e6d73d3650817c8fa4fb8e94df5ac6)
by [Unito](https://www.unito.io)

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-14 10:45:32 +00:00
snomiao
fe0054d9dd [feat] Add Linux core dump to .gitignore (#4960)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions <github-actions@github.com>
2025-08-14 10:45:32 +00:00
snomiao
b4bf72b2b7 Merge branch 'main' into sno-lint-issue-write 2025-08-13 15:56:38 +09:00
snomiao
45b43aa093 feat: Add issues write permission to workflows to avoid lint and i18n workflow conflicts
- Add issues: write permission to lint-and-format.yaml
- Add issues: write permission to i18n.yaml
- Prevents workflow conflicts when updating PR statuses or comments
2025-08-13 06:50:54 +00:00
snomiao
ec2888a116 [auto-fix] Apply ESLint and Prettier fixe 2025-08-13 05:25:41 +00:00
snomiao
0f747f3c5d use auto-commit-action to handle pullrequest-ref name 2025-08-13 05:21:29 +00:00
snomiao
7ee33e2892 kick-format-modify-main-ts 2025-08-12 11:52:25 +00:00
snomiao
73f71fc018 kick-format 2025-08-12 11:48:32 +00:00
snomiao
6b085ea8c4 [feat] Add issues: write permission to lint-and-format workflow
The lint-and-format workflow creates comments on PRs, which requires
the issues: write permission to function properly.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-12 06:49:15 +00:00
294 changed files with 2498 additions and 23244 deletions

View File

@@ -111,7 +111,50 @@ echo "Last stable release: $LAST_STABLE"
```
7. **HUMAN ANALYSIS**: Review change summary and verify scope
### Step 3: Breaking Change Analysis
### Step 3: Version Preview
**Version Preview:**
- Current: `${CURRENT_VERSION}`
- Proposed: Show exact version number
- **CONFIRMATION REQUIRED**: Proceed with version `X.Y.Z`?
### Step 4: Security and Dependency Audit
1. Run security audit:
```bash
npm audit --audit-level moderate
```
2. Check for known vulnerabilities in dependencies
3. Scan for hardcoded secrets or credentials:
```bash
git log -p ${BASE_TAG}..HEAD | grep -iE "(password|key|secret|token)" || echo "No sensitive data found"
```
4. Verify no sensitive data in recent commits
5. **SECURITY REVIEW**: Address any critical findings before proceeding?
### Step 5: Pre-Release Testing
1. Run complete test suite:
```bash
npm run test:unit
npm run test:component
```
2. Run type checking:
```bash
npm run typecheck
```
3. Run linting (may have issues with missing packages):
```bash
npm run lint || echo "Lint issues - verify if critical"
```
4. Test build process:
```bash
npm run build
npm run build:types
```
5. **QUALITY GATE**: All tests and builds passing?
### Step 6: Breaking Change Analysis
1. Analyze API changes in:
- Public TypeScript interfaces
@@ -126,7 +169,7 @@ echo "Last stable release: $LAST_STABLE"
3. Generate breaking change summary
4. **COMPATIBILITY REVIEW**: Breaking changes documented and justified?
### Step 4: Analyze Dependency Updates
### Step 7: Analyze Dependency Updates
1. **Check significant dependency updates:**
```bash
@@ -152,117 +195,7 @@ echo "Last stable release: $LAST_STABLE"
done
```
### Step 5: Generate GTM Feature Summary
1. **Collect PR data for analysis:**
```bash
# Get list of PR numbers from commits
PR_NUMBERS=$(git log ${BASE_TAG}..HEAD --oneline --no-merges --first-parent | \
grep -oE "#[0-9]+" | tr -d '#' | sort -u)
# Save PR data for each PR
echo "[" > prs-${NEW_VERSION}.json
first=true
for PR in $PR_NUMBERS; do
[[ "$first" == true ]] && first=false || echo "," >> prs-${NEW_VERSION}.json
gh pr view $PR --json number,title,author,body,labels 2>/dev/null >> prs-${NEW_VERSION}.json || echo "{}" >> prs-${NEW_VERSION}.json
done
echo "]" >> prs-${NEW_VERSION}.json
```
2. **Analyze for GTM-worthy features:**
```
<task>
Review these PRs to identify features worthy of marketing attention.
A feature is GTM-worthy if it meets ALL of these criteria:
- Introduces a NEW capability users didn't have before (not just improvements)
- Would be a compelling reason for users to upgrade to this version
- Can be demonstrated visually or has clear before/after comparison
- Affects a significant portion of the user base
NOT GTM-worthy:
- Bug fixes (even important ones)
- Minor UI tweaks or color changes
- Performance improvements without user-visible impact
- Internal refactoring
- Small convenience features
- Features that only improve existing functionality marginally
For each GTM-worthy feature, note:
- PR number, title, and author
- Media links from the PR description
- One compelling sentence on why users should care
If there are no GTM-worthy features, just say "No marketing-worthy features in this release."
</task>
PR data: [contents of prs-${NEW_VERSION}.json]
```
3. **Generate GTM notification:**
```bash
# Save to gtm-summary-${NEW_VERSION}.md based on analysis
# If GTM-worthy features exist, include them with testing instructions
# If not, note that this is a maintenance/bug fix release
# Check if notification is needed
if grep -q "No marketing-worthy features" gtm-summary-${NEW_VERSION}.md; then
echo "✅ No GTM notification needed for this release"
echo "📄 Summary saved to: gtm-summary-${NEW_VERSION}.md"
else
echo "📋 GTM summary saved to: gtm-summary-${NEW_VERSION}.md"
echo "📤 Share this file in #gtm channel to notify the team"
fi
```
### Step 6: Version Preview
**Version Preview:**
- Current: `${CURRENT_VERSION}`
- Proposed: Show exact version number based on analysis:
- Major version if breaking changes detected
- Minor version if new features added
- Patch version if only bug fixes
- **CONFIRMATION REQUIRED**: Proceed with version `X.Y.Z`?
### Step 7: Security and Dependency Audit
1. Run security audit:
```bash
npm audit --audit-level moderate
```
2. Check for known vulnerabilities in dependencies
3. Scan for hardcoded secrets or credentials:
```bash
git log -p ${BASE_TAG}..HEAD | grep -iE "(password|key|secret|token)" || echo "No sensitive data found"
```
4. Verify no sensitive data in recent commits
5. **SECURITY REVIEW**: Address any critical findings before proceeding?
### Step 8: Pre-Release Testing
1. Run complete test suite:
```bash
npm run test:unit
npm run test:component
```
2. Run type checking:
```bash
npm run typecheck
```
3. Run linting (may have issues with missing packages):
```bash
npm run lint || echo "Lint issues - verify if critical"
```
4. Test build process:
```bash
npm run build
npm run build:types
```
5. **QUALITY GATE**: All tests and builds passing?
### Step 9: Generate Comprehensive Release Notes
### Step 8: Generate Comprehensive Release Notes
1. Extract commit messages since base release:
```bash
@@ -324,7 +257,7 @@ echo "Last stable release: $LAST_STABLE"
- Ensure consistent bullet format: `- Description (#PR_NUMBER)`
5. **CONTENT REVIEW**: Release notes follow standard format?
### Step 10: Create Version Bump PR
### Step 9: Create Version Bump PR
**For standard version bumps (patch/minor/major):**
```bash
@@ -370,7 +303,7 @@ echo "Workflow triggered. Waiting for PR creation..."
```
4. **PR REVIEW**: Version bump PR created with standardized release notes?
### Step 11: Critical Release PR Verification
### Step 10: Critical Release PR Verification
1. **CRITICAL**: Verify PR has "Release" label:
```bash
@@ -392,7 +325,7 @@ echo "Workflow triggered. Waiting for PR creation..."
```
7. **FINAL CODE REVIEW**: Release label present and no [skip ci]?
### Step 12: Pre-Merge Validation
### Step 11: Pre-Merge Validation
1. **Review Requirements**: Release PRs require approval
2. Monitor CI checks - watch for update-locales
@@ -400,7 +333,7 @@ echo "Workflow triggered. Waiting for PR creation..."
4. Check no new commits to main since PR creation
5. **DEPLOYMENT READINESS**: Ready to merge?
### Step 13: Execute Release
### Step 12: Execute Release
1. **FINAL CONFIRMATION**: Merge PR to trigger release?
2. Merge the Release PR:
@@ -433,7 +366,7 @@ echo "Workflow triggered. Waiting for PR creation..."
gh run watch ${WORKFLOW_RUN_ID}
```
### Step 14: Enhance GitHub Release
### Step 13: Enhance GitHub Release
1. Wait for automatic release creation:
```bash
@@ -461,7 +394,7 @@ echo "Workflow triggered. Waiting for PR creation..."
gh release view v${NEW_VERSION}
```
### Step 15: Verify Multi-Channel Distribution
### Step 14: Verify Multi-Channel Distribution
1. **GitHub Release:**
```bash
@@ -499,7 +432,7 @@ echo "Workflow triggered. Waiting for PR creation..."
4. **DISTRIBUTION VERIFICATION**: All channels published successfully?
### Step 16: Post-Release Monitoring Setup
### Step 15: Post-Release Monitoring Setup
1. **Monitor immediate release health:**
```bash
@@ -569,49 +502,11 @@ echo "Workflow triggered. Waiting for PR creation..."
## Files Generated
- \`release-notes-${NEW_VERSION}.md\` - Comprehensive release notes
- \`post-release-checklist.md\` - Follow-up tasks
- \`gtm-summary-${NEW_VERSION}.md\` - Marketing team notification
EOF
```
4. **RELEASE COMPLETION**: All post-release setup completed?
### Step 17: Create Release Summary
1. **Create comprehensive release summary:**
```bash
cat > release-summary-${NEW_VERSION}.md << EOF
# Release Summary: ComfyUI Frontend v${NEW_VERSION}
**Released:** $(date)
**Type:** ${VERSION_TYPE}
**Duration:** ~${RELEASE_DURATION} minutes
**Release Commit:** ${RELEASE_COMMIT}
## Metrics
- **Commits Included:** ${COMMITS_COUNT}
- **Contributors:** ${CONTRIBUTORS_COUNT}
- **Files Changed:** ${FILES_CHANGED}
- **Lines Added/Removed:** +${LINES_ADDED}/-${LINES_REMOVED}
## Distribution Status
- ✅ GitHub Release: Published
- ✅ PyPI Package: Available
- ✅ npm Types: Available
## Next Steps
- Monitor for 24-48 hours
- Address any critical issues immediately
- Plan next release cycle
## Files Generated
- \`release-notes-${NEW_VERSION}.md\` - Comprehensive release notes
- \`post-release-checklist.md\` - Follow-up tasks
- \`gtm-summary-${NEW_VERSION}.md\` - Marketing team notification
EOF
```
2. **RELEASE COMPLETION**: All steps completed successfully?
## Advanced Safety Features
### Rollback Procedures

View File

@@ -1,6 +0,0 @@
# .git-blame-ignore-revs
# List of commits to ignore in git blame
# Format: <commit hash> # <description>
# npm run format on litegraph merge (10,672 insertions, 7,327 deletions across 129 files)
c53f197de2a3e0fa66b16dedc65c131235c1c4b6

View File

@@ -0,0 +1,206 @@
name: Auto-fix and Update
on:
pull_request:
branches-ignore: [wip/*, draft/*, temp/*]
permissions:
contents: write
pull-requests: write
issues: write
jobs:
auto-fix-and-update:
# Only run on PRs from the same repository (not forks) to avoid permission issues, auto fix any lint and formats, update locales, and commit the changes back
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- name: Setup ComfyUI Frontend and Backend
uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.3
# Step 1: Cache ESLint for better performance
- name: Cache ESLint
uses: actions/cache@v4
with:
path: ComfyUI_frontend/.eslintcache
key: ${{ runner.os }}-eslint-${{ hashFiles('ComfyUI_frontend/eslint.config.js', 'ComfyUI_frontend/package-lock.json') }}
restore-keys: |
${{ runner.os }}-eslint-
# Step 2: Run lint and format fixes
- name: Run ESLint with auto-fix
run: npm run lint:fix
working-directory: ComfyUI_frontend
- name: Run Prettier with auto-format
run: npm run format
working-directory: ComfyUI_frontend
# Step 3: Update locales (only if there are relevant changes)
- name: Check if locale updates needed
id: check-locale-changes
run: |
# Check if there are changes to files that would affect locales
if git diff --name-only origin/${{ github.base_ref }}..HEAD | grep -E '\.(vue|ts|tsx)$' | grep -v test | head -1; then
echo "locale_updates_needed=true" >> $GITHUB_OUTPUT
else
echo "locale_updates_needed=false" >> $GITHUB_OUTPUT
fi
working-directory: ComfyUI_frontend
- name: Install Playwright Browsers
if: steps.check-locale-changes.outputs.locale_updates_needed == 'true'
run: npx playwright install chromium --with-deps
working-directory: ComfyUI_frontend
- name: Start dev server
if: steps.check-locale-changes.outputs.locale_updates_needed == 'true'
run: npm run dev:electron &
working-directory: ComfyUI_frontend
- name: Update en.json
if: steps.check-locale-changes.outputs.locale_updates_needed == 'true'
run: npm run collect-i18n -- scripts/collect-i18n-general.ts
env:
PLAYWRIGHT_TEST_URL: http://localhost:5173
working-directory: ComfyUI_frontend
- name: Update translations
if: steps.check-locale-changes.outputs.locale_updates_needed == 'true'
run: npm run locale
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
working-directory: ComfyUI_frontend
# Step 4: Check for any changes from both lint-format and locale updates
- name: Check for changes
id: verify-changed-files
run: |
if [ -n "$(git status --porcelain)" ]; then
echo "changed=true" >> $GITHUB_OUTPUT
# Show what changed for debugging
echo "Changed files:"
git status --porcelain
else
echo "changed=false" >> $GITHUB_OUTPUT
fi
working-directory: ComfyUI_frontend
# Step 5: Commit all changes in a single commit
- name: Commit auto-fixes and locale updates
if: steps.verify-changed-files.outputs.changed == 'true'
run: |
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@github.com'
git fetch origin ${{ github.head_ref }}
# Stash any local changes before checkout
git stash -u
git checkout -B ${{ github.head_ref }} origin/${{ github.head_ref }}
# Apply the stashed changes if any
git stash pop || true
git add .
git diff --staged --quiet || git commit -m "[auto-fix] Apply ESLint, Prettier fixes and update locales"
git push origin HEAD:${{ github.head_ref }}
working-directory: ComfyUI_frontend
# Step 6: Run final validation
- name: Final validation
run: |
npm run lint
npm run format:check
npm run knip
working-directory: ComfyUI_frontend
# Step 7: Comment on PR about what was fixed
- name: Comment on PR about auto-fixes
if: steps.verify-changed-files.outputs.changed == 'true'
uses: actions/github-script@v7
with:
script: |
let changes = [];
if ('${{ steps.check-locale-changes.outputs.locale_updates_needed }}' === 'true') {
changes.push('Locale updates');
}
// Always include lint/format as we ran them
changes.push('ESLint auto-fixes', 'Prettier formatting');
const changesText = changes.join('\n- ');
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## 🔧 Auto-fixes Applied
This PR has been automatically updated with the following changes:
- ${changesText}
**⚠️ Important**: Your local branch is now behind. Run \`git pull\` before making additional changes to avoid conflicts.`
});
# Separate job for fork PRs that can't auto-commit, we only check lint and formats, and do not commit back
fork-pr-check:
if: github.event.pull_request.head.repo.full_name != github.repository
runs-on: ubuntu-latest
steps:
- name: Checkout PR
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Cache ESLint
uses: actions/cache@v4
with:
path: .eslintcache
key: ${{ runner.os }}-eslint-fork-${{ hashFiles('eslint.config.js', 'package-lock.json') }}
restore-keys: |
${{ runner.os }}-eslint-fork-
- name: Check linting and formatting
id: check-lint-format
run: |
# Run checks and capture exit codes
npm run lint || echo "lint_failed=true" >> $GITHUB_OUTPUT
npm run format:check || echo "format_failed=true" >> $GITHUB_OUTPUT
npm run knip || echo "knip_failed=true" >> $GITHUB_OUTPUT
- name: Comment on fork PR about manual fixes needed
if: steps.check-lint-format.outputs.lint_failed == 'true' || steps.check-lint-format.outputs.format_failed == 'true' || steps.check-lint-format.outputs.knip_failed == 'true'
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## ⚠️ Linting/Formatting Issues Found
This PR has linting or formatting issues that need to be fixed.
**Since this PR is from a fork, auto-fix cannot be applied automatically.**
### Option 1: Set up pre-commit hooks (recommended)
Run this once to automatically format code on every commit:
\`\`\`bash
npm run prepare
\`\`\`
### Option 2: Fix manually
Run these commands and push the changes:
\`\`\`bash
npm run lint:fix
npm run format
\`\`\`
See [CONTRIBUTING.md](https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/CONTRIBUTING.md#git-pre-commit-hooks) for more details.`
});

View File

@@ -1,96 +0,0 @@
name: 'Chromatic'
# - [Automate Chromatic with GitHub Actions • Chromatic docs]( https://www.chromatic.com/docs/github-actions/ )
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
chromatic-deployment:
runs-on: ubuntu-latest
permissions:
pull-requests: write
issues: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Required for Chromatic baseline
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Get current time
id: current-time
run: echo "time=$(date -u '+%m/%d/%Y, %I:%M:%S %p')" >> $GITHUB_OUTPUT
- name: Comment PR - Build Started
if: github.event_name == 'pull_request'
uses: edumserrano/find-create-or-update-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
body-includes: '<!-- STORYBOOK_BUILD_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: append
body: |
<!-- STORYBOOK_BUILD_STATUS -->
## 🎨 Storybook Build Status
🔄 **Building Storybook and running visual tests...**
⏳ Build started at: ${{ steps.current-time.outputs.time }} UTC
---
*This comment will be updated when the build completes*
- name: Install dependencies
run: npm ci
- name: Build Storybook and run Chromatic
id: chromatic
uses: chromaui/action@latest
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
buildScriptName: build-storybook
autoAcceptChanges: 'main' # Auto-accept changes on main branch
exitOnceUploaded: true # Don't wait for UI tests to complete
- name: Get completion time
id: completion-time
if: always()
run: echo "time=$(date -u '+%m/%d/%Y, %I:%M:%S %p')" >> $GITHUB_OUTPUT
- name: Comment PR - Build Complete
if: github.event_name == 'pull_request' && always()
uses: edumserrano/find-create-or-update-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
body-includes: '<!-- STORYBOOK_BUILD_STATUS -->'
comment-author: 'github-actions[bot]'
edit-mode: replace
body: |
<!-- STORYBOOK_BUILD_STATUS -->
## 🎨 Storybook Build Status
${{ steps.chromatic.outcome == 'success' && '✅' || '❌' }} **${{ steps.chromatic.outcome == 'success' && 'Build completed successfully!' || 'Build failed!' }}**
⏰ Completed at: ${{ steps.completion-time.outputs.time }} UTC
### 📊 Build Summary
- **Components**: ${{ steps.chromatic.outputs.componentCount || '0' }}
- **Stories**: ${{ steps.chromatic.outputs.testCount || '0' }}
- **Visual changes**: ${{ steps.chromatic.outputs.changeCount || '0' }}
- **Errors**: ${{ steps.chromatic.outputs.errorCount || '0' }}
### 🔗 Links
${{ steps.chromatic.outputs.buildUrl && format('- [📸 View Chromatic Build]({0})', steps.chromatic.outputs.buildUrl) || '' }}
${{ steps.chromatic.outputs.storybookUrl && format('- [📖 Preview Storybook]({0})', steps.chromatic.outputs.storybookUrl) || '' }}
---
${{ steps.chromatic.outcome == 'success' && '🎉 Your Storybook is ready for review!' || '⚠️ Please check the workflow logs for error details.' }}

View File

@@ -1,51 +0,0 @@
name: Update Locales
on:
pull_request:
branches: [ main, master, dev* ]
paths-ignore:
- '.github/**'
- '.husky/**'
- '.vscode/**'
- 'browser_tests/**'
- 'tests-ui/**'
jobs:
update-locales:
# Don't run on fork PRs
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
steps:
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v2.3
- name: Install Playwright Browsers
run: npx playwright install chromium --with-deps
working-directory: ComfyUI_frontend
- name: Start dev server
# Run electron dev server as it is a superset of the web dev server
# We do want electron specific UIs to be translated.
run: npm run dev:electron &
working-directory: ComfyUI_frontend
- name: Update en.json
run: npm run collect-i18n -- scripts/collect-i18n-general.ts
env:
PLAYWRIGHT_TEST_URL: http://localhost:5173
working-directory: ComfyUI_frontend
- name: Update translations
run: npm run locale
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
working-directory: ComfyUI_frontend
- name: Commit updated locales
run: |
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@github.com'
git fetch origin ${{ github.head_ref }}
# Stash any local changes before checkout
git stash -u
git checkout -B ${{ github.head_ref }} origin/${{ github.head_ref }}
# Apply the stashed changes if any
git stash pop || true
git add src/locales/
git diff --staged --quiet || git commit -m "Update locales [skip ci]"
git push origin HEAD:${{ github.head_ref }}
working-directory: ComfyUI_frontend

View File

@@ -1,83 +0,0 @@
name: Lint and Format
on:
pull_request:
branches-ignore: [wip/*, draft/*, temp/*]
permissions:
contents: write
pull-requests: write
jobs:
lint-and-format:
runs-on: ubuntu-latest
steps:
- name: Checkout PR
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run ESLint with auto-fix
run: npm run lint:fix
- name: Run Prettier with auto-format
run: npm run format
- name: Check for changes
id: verify-changed-files
run: |
if [ -n "$(git status --porcelain)" ]; then
echo "changed=true" >> $GITHUB_OUTPUT
else
echo "changed=false" >> $GITHUB_OUTPUT
fi
- name: Commit changes
if: steps.verify-changed-files.outputs.changed == 'true' && github.event.pull_request.head.repo.full_name == github.repository
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add .
git commit -m "[auto-fix] Apply ESLint and Prettier fixes"
git push
- name: Final validation
run: |
npm run lint
npm run format:check
npm run knip
- name: Comment on PR about auto-fix
if: steps.verify-changed-files.outputs.changed == 'true' && github.event.pull_request.head.repo.full_name == github.repository
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '## 🔧 Auto-fixes Applied\n\nThis PR has been automatically updated to fix linting and formatting issues.\n\n**⚠️ Important**: Your local branch is now behind. Run `git pull` before making additional changes to avoid conflicts.\n\n### Changes made:\n- ESLint auto-fixes\n- Prettier formatting'
})
- name: Comment on PR about manual fix needed
if: steps.verify-changed-files.outputs.changed == 'true' && github.event.pull_request.head.repo.full_name != github.repository
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '## ⚠️ Linting/Formatting Issues Found\n\nThis PR has linting or formatting issues that need to be fixed.\n\n**Since this PR is from a fork, auto-fix cannot be applied automatically.**\n\n### Option 1: Set up pre-commit hooks (recommended)\nRun this once to automatically format code on every commit:\n```bash\nnpm run prepare\n```\n\n### Option 2: Fix manually\nRun these commands and push the changes:\n```bash\nnpm run lint:fix\nnpm run format\n```\n\nSee [CONTRIBUTING.md](https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/CONTRIBUTING.md#git-pre-commit-hooks) for more details.'
})

View File

@@ -22,6 +22,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: 'npm'
- name: Get current version
id: current_version
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
@@ -34,6 +35,13 @@ jobs:
else
echo "is_prerelease=false" >> $GITHUB_OUTPUT
fi
- name: Cache Vite build
uses: actions/cache@v4
with:
path: node_modules/.vite
key: ${{ runner.os }}-vite-release-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-vite-release-
- name: Build project
env:
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
@@ -117,6 +125,7 @@ jobs:
with:
node-version: 'lts/*'
registry-url: https://registry.npmjs.org
cache: 'npm'
- run: npm ci
- run: npm run build:types
- name: Publish package

View File

@@ -35,6 +35,16 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'npm'
cache-dependency-path: ComfyUI_frontend/package-lock.json
- name: Cache Vite build
uses: actions/cache@v4
with:
path: ComfyUI_frontend/node_modules/.vite
key: ${{ runner.os }}-vite-build-${{ hashFiles('ComfyUI_frontend/package-lock.json') }}
restore-keys: |
${{ runner.os }}-vite-build-
- name: Build ComfyUI_frontend
run: |
@@ -92,7 +102,23 @@ jobs:
wait-for-it --service 127.0.0.1:8188 -t 600
working-directory: ComfyUI
- name: Get Playwright version
id: playwright-version
run: echo "version=$(npm list @playwright/test --depth=0 --json | jq -r '.dependencies["@playwright/test"].version')" >> $GITHUB_OUTPUT
working-directory: ComfyUI_frontend
- name: Cache Playwright browsers
uses: actions/cache@v4
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.version }}-chromium
restore-keys: |
${{ runner.os }}-playwright-${{ steps.playwright-version.outputs.version }}-
${{ runner.os }}-playwright-
- name: Install Playwright Browsers
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: npx playwright install chromium --with-deps
working-directory: ComfyUI_frontend

View File

@@ -17,10 +17,19 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Cache Vite
uses: actions/cache@v4
with:
path: node_modules/.vite
key: ${{ runner.os }}-vite-${{ hashFiles('package-lock.json') }}
restore-keys: |
${{ runner.os }}-vite-
- name: Run Vitest tests
run: |
npm run test:component

4
.gitignore vendored
View File

@@ -72,7 +72,3 @@ vite.config.mts.timestamp-*.mjs
# Linux core dumps
./core
*storybook.log
storybook-static

View File

@@ -13,10 +13,6 @@ module.exports = defineConfig({
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream.
'latent' is the short form of 'latent space'.
'mask' is in the context of image processing.
IMPORTANT Chinese Translation Guidelines:
- For 'zh' locale: Use ONLY Simplified Chinese characters (简体中文). Common examples: 节点 (not 節點), 画布 (not 畫布), 图像 (not 圖像), 选择 (not 選擇), 减小 (not 減小).
- For 'zh-TW' locale: Use ONLY Traditional Chinese characters (繁體中文) with Taiwan-specific terminology.
- NEVER mix Simplified and Traditional Chinese characters within the same locale.
Note: For Traditional Chinese (Taiwan), use Taiwan-specific terminology and traditional characters.
`
});

View File

@@ -1,197 +0,0 @@
# Storybook Development Guidelines for Claude
## Quick Commands
- `npm run storybook`: Start Storybook development server
- `npm run build-storybook`: Build static Storybook
- `npm run test:component`: Run component tests (includes Storybook components)
## Development Workflow for Storybook
1. **Creating New Stories**:
- Place `*.stories.ts` files alongside components
- Follow the naming pattern: `ComponentName.stories.ts`
- Use realistic mock data that matches ComfyUI schemas
2. **Testing Stories**:
- Verify stories render correctly in Storybook UI
- Test different component states and edge cases
- Ensure proper theming and styling
3. **Code Quality**:
- Run `npm run typecheck` to verify TypeScript
- Run `npm run lint` to check for linting issues
- Follow existing story patterns and conventions
## Story Creation Guidelines
### Basic Story Structure
```typescript
import type { Meta, StoryObj } from '@storybook/vue3'
import ComponentName from './ComponentName.vue'
const meta: Meta<typeof ComponentName> = {
title: 'Category/ComponentName',
component: ComponentName,
parameters: {
layout: 'centered' // or 'fullscreen', 'padded'
}
}
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
// Component props
}
}
```
### Mock Data Patterns
For ComfyUI components, use realistic mock data:
```typescript
// Node definition mock
const mockNodeDef = {
input: {
required: {
prompt: ["STRING", { multiline: true }]
}
},
output: ["CONDITIONING"],
output_is_list: [false],
category: "conditioning"
}
// Component instance mock
const mockComponent = {
id: "1",
type: "CLIPTextEncode",
// ... other properties
}
```
### Common Story Variants
Always include these story variants when applicable:
- **Default**: Basic component with minimal props
- **WithData**: Component with realistic data
- **Loading**: Component in loading state
- **Error**: Component with error state
- **LongContent**: Component with edge case content
- **Empty**: Component with no data
### Storybook-Specific Code Patterns
#### Store Access
```typescript
// In stories, access stores through the setup function
export const WithStore: Story = {
render: () => ({
setup() {
const store = useMyStore()
return { store }
},
template: '<MyComponent :data="store.data" />'
})
}
```
#### Event Testing
```typescript
export const WithEvents: Story = {
args: {
onUpdate: fn() // Use Storybook's fn() for action logging
}
}
```
## Configuration Notes
### Vue App Setup
The Storybook preview is configured with:
- Pinia stores initialized
- PrimeVue with ComfyUI theme
- i18n internationalization
- All necessary CSS imports
### Build Configuration
- Vite integration with proper alias resolution
- Manual chunking for better performance
- TypeScript support with strict checking
- CSS processing for Vue components
## Troubleshooting
### Common Issues
1. **Import Errors**: Verify `@/` alias is working correctly
2. **Missing Styles**: Ensure CSS imports are in `preview.ts`
3. **Store Errors**: Check store initialization in setup
4. **Type Errors**: Use proper TypeScript types for story args
### Debug Commands
```bash
# Check TypeScript issues
npm run typecheck
# Lint Storybook files
npm run lint .storybook/
# Build to check for production issues
npm run build-storybook
```
## File Organization
```
.storybook/
├── main.ts # Core configuration
├── preview.ts # Global setup and decorators
├── README.md # User documentation
└── CLAUDE.md # This file - Claude guidelines
src/
├── components/
│ └── MyComponent/
│ ├── MyComponent.vue
│ └── MyComponent.stories.ts
```
## Integration with ComfyUI
### Available Context
Stories have access to:
- All ComfyUI stores (widgetStore, colorPaletteStore, etc.)
- PrimeVue components with ComfyUI theming
- Internationalization system
- ComfyUI CSS variables and styling
### Testing Components
When testing ComfyUI-specific components:
1. Use realistic node definitions and data structures
2. Test with different node types (sampling, conditioning, etc.)
3. Verify proper CSS theming and dark/light modes
4. Check component behavior with various input combinations
### Performance Considerations
- Use manual chunking for large dependencies
- Minimize bundle size by avoiding unnecessary imports
- Leverage Storybook's lazy loading capabilities
- Profile build times and optimize as needed
## Best Practices
1. **Keep Stories Focused**: Each story should demonstrate one specific use case
2. **Use Descriptive Names**: Story names should clearly indicate what they show
3. **Document Complex Props**: Use JSDoc comments for complex prop types
4. **Test Edge Cases**: Create stories for unusual but valid use cases
5. **Maintain Consistency**: Follow established patterns in existing stories

View File

@@ -1,173 +0,0 @@
# Storybook Configuration for ComfyUI Frontend
## What is Storybook?
Storybook is a frontend workshop for building UI components and pages in isolation. It allows developers to:
- Build components independently from the main application
- Test components with different props and states
- Document component APIs and usage patterns
- Share components across teams and projects
- Catch visual regressions through visual testing
## Storybook vs Other Testing Tools
| Tool | Purpose | Use Case |
|------|---------|----------|
| **Storybook** | Component isolation & documentation | Developing, testing, and showcasing individual UI components |
| **Playwright** | End-to-end testing | Full user workflow testing across multiple pages |
| **Vitest** | Unit testing | Testing business logic, utilities, and component behavior |
| **Vue Testing Library** | Component testing | Testing component interactions and DOM output |
### When to Use Storybook
**✅ Use Storybook for:**
- Developing new UI components in isolation
- Creating component documentation and examples
- Testing different component states and props
- Sharing components with designers and stakeholders
- Visual regression testing
- Building a component library or design system
**❌ Don't use Storybook for:**
- Testing complex user workflows (use Playwright)
- Testing business logic (use Vitest)
- Integration testing between components (use Vue Testing Library)
## How to Use Storybook
### Development Commands
```bash
# Start Storybook development server
npm run storybook
# Build static Storybook for deployment
npm run build-storybook
```
### Creating Stories
Stories are located alongside components in `src/` directories with the pattern `*.stories.ts`:
```typescript
// MyComponent.stories.ts
import type { Meta, StoryObj } from '@storybook/vue3'
import MyComponent from './MyComponent.vue'
const meta: Meta<typeof MyComponent> = {
title: 'Components/MyComponent',
component: MyComponent,
parameters: {
layout: 'centered'
}
}
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {
args: {
title: 'Hello World'
}
}
export const WithVariant: Story = {
args: {
title: 'Variant Example',
variant: 'secondary'
}
}
```
### Available Features
- **Vue 3 Support**: Full Vue 3 composition API and reactivity
- **PrimeVue Integration**: All PrimeVue components and theming
- **ComfyUI Theming**: Custom ComfyUI theme preset applied
- **Pinia Stores**: Access to application stores for components that need state
- **TypeScript**: Full TypeScript support with proper type checking
- **CSS/SCSS**: Component styling support
- **Auto-documentation**: Automatic prop tables and component documentation
- **Chromatic Integration**: Automated visual regression testing for component stories
## Development Tips
### Best Practices
1. **Keep Stories Simple**: Each story should demonstrate one specific use case
2. **Use Realistic Data**: Use data that resembles real application usage
3. **Document Edge Cases**: Create stories for loading states, errors, and edge cases
4. **Group Related Stories**: Use consistent naming and grouping for related components
### Component Testing Strategy
```typescript
// Example: Testing different component states
export const Loading: Story = {
args: {
isLoading: true
}
}
export const Error: Story = {
args: {
error: 'Failed to load data'
}
}
export const WithLongText: Story = {
args: {
description: 'Very long description that might cause layout issues...'
}
}
```
### Debugging Tips
- Use browser DevTools to inspect component behavior
- Check the Storybook console for Vue warnings or errors
- Use the Controls addon to dynamically change props
- Leverage the Actions addon to test event handling
## Configuration Files
- **`main.ts`**: Core Storybook configuration and Vite integration
- **`preview.ts`**: Global decorators, parameters, and Vue app setup
- **`manager.ts`**: Storybook UI customization (if needed)
## Chromatic Visual Testing
This project uses [Chromatic](https://chromatic.com) for automated visual regression testing of Storybook components.
### How It Works
- **Automated Testing**: Every push to `main` and `sno-storybook` branches triggers Chromatic builds
- **Pull Request Reviews**: PRs against `main` branch include visual diffs for component changes
- **Baseline Management**: Changes on `main` branch are automatically accepted as new baselines
- **Cross-browser Testing**: Tests across multiple browsers and viewports
### Viewing Results
1. Check the GitHub Actions tab for Chromatic workflow status
2. Click on the Chromatic build link in PR comments to review visual changes
3. Accept or reject visual changes directly in the Chromatic UI
### Best Practices for Visual Testing
- **Consistent Stories**: Ensure stories render consistently across different environments
- **Meaningful Names**: Use descriptive story names that clearly indicate the component state
- **Edge Cases**: Include stories for loading, error, and empty states
- **Realistic Data**: Use data that closely resembles production usage
## Integration with ComfyUI
This Storybook setup includes:
- ComfyUI-specific theming and styling
- Pre-configured Pinia stores for state management
- Internationalization (i18n) support
- PrimeVue component library integration
- Proper alias resolution for `@/` imports
For component-specific examples, see the NodePreview stories in `src/components/node/`.

View File

@@ -1,96 +0,0 @@
import type { StorybookConfig } from '@storybook/vue3-vite'
import { FileSystemIconLoader } from 'unplugin-icons/loaders'
import IconsResolver from 'unplugin-icons/resolver'
import Icons from 'unplugin-icons/vite'
import Components from 'unplugin-vue-components/vite'
import type { InlineConfig } from 'vite'
const config: StorybookConfig = {
stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: ['@storybook/addon-docs'],
framework: {
name: '@storybook/vue3-vite',
options: {}
},
async viteFinal(config) {
// Use dynamic import to avoid CJS deprecation warning
const { mergeConfig } = await import('vite')
// Filter out any plugins that might generate import maps
if (config.plugins) {
config.plugins = config.plugins.filter((plugin: any) => {
if (plugin && plugin.name && plugin.name.includes('import-map')) {
return false
}
return true
})
}
return mergeConfig(config, {
// Replace plugins entirely to avoid inheritance issues
plugins: [
// Only include plugins we explicitly need for Storybook
Icons({
compiler: 'vue3',
customCollections: {
comfy: FileSystemIconLoader(
process.cwd() + '/src/assets/icons/custom'
)
}
}),
Components({
dts: false, // Disable dts generation in Storybook
resolvers: [
IconsResolver({
customCollections: ['comfy']
})
],
dirs: [
process.cwd() + '/src/components',
process.cwd() + '/src/layout',
process.cwd() + '/src/views'
],
deep: true,
extensions: ['vue']
})
// Note: Explicitly NOT including generateImportMapPlugin to avoid externalization
],
server: {
allowedHosts: true
},
resolve: {
alias: {
'@': process.cwd() + '/src'
}
},
build: {
rollupOptions: {
external: () => {
// Don't externalize any modules in Storybook build
// This ensures PrimeVue and other dependencies are bundled
return false
},
onwarn: (warning, warn) => {
// Suppress specific warnings
if (
warning.code === 'UNUSED_EXTERNAL_IMPORT' &&
warning.message?.includes('resolveComponent')
) {
return
}
// Suppress Storybook font asset warnings
if (
warning.code === 'UNRESOLVED_IMPORT' &&
warning.message?.includes('nunito-sans')
) {
return
}
warn(warning)
}
},
chunkSizeWarningLimit: 1000
}
} satisfies InlineConfig)
}
}
export default config

View File

@@ -1,34 +0,0 @@
<style>
body {
overflow-y: auto !important;
transition: background-color 0.3s ease, color 0.3s ease;
}
/* Light theme default */
body {
background-color: #ffffff;
color: #1a1a1a;
}
/* Dark theme styles */
body.dark-theme,
.dark-theme body {
background-color: #0a0a0a;
color: #e5e5e5;
}
/* Ensure Storybook canvas follows theme */
.sb-show-main {
transition: background-color 0.3s ease;
}
.dark-theme .sb-show-main,
.dark-theme .docs-story {
background-color: #0a0a0a !important;
}
/* Fix for Storybook controls panel in dark mode */
.dark-theme .docblock-argstable-body {
color: #e5e5e5;
}
</style>

View File

@@ -1,104 +0,0 @@
import { definePreset } from '@primevue/themes'
import Aura from '@primevue/themes/aura'
import { setup } from '@storybook/vue3'
import type { Preview } from '@storybook/vue3-vite'
import { createPinia } from 'pinia'
import 'primeicons/primeicons.css'
import PrimeVue from 'primevue/config'
import ConfirmationService from 'primevue/confirmationservice'
import ToastService from 'primevue/toastservice'
import Tooltip from 'primevue/tooltip'
import '../src/assets/css/style.css'
import { i18n } from '../src/i18n'
import '../src/lib/litegraph/public/css/litegraph.css'
import { useWidgetStore } from '../src/stores/widgetStore'
import { useColorPaletteStore } from '../src/stores/workspace/colorPaletteStore'
const ComfyUIPreset = definePreset(Aura, {
semantic: {
// @ts-expect-error fix me
primary: Aura['primitive'].blue
}
})
// Setup Vue app for Storybook
setup((app) => {
app.directive('tooltip', Tooltip)
const pinia = createPinia()
app.use(pinia)
// Initialize stores
useColorPaletteStore(pinia)
useWidgetStore(pinia)
app.use(i18n)
app.use(PrimeVue, {
theme: {
preset: ComfyUIPreset,
options: {
prefix: 'p',
cssLayer: {
name: 'primevue',
order: 'primevue, tailwind-utilities'
},
darkModeSelector: '.dark-theme, :root:has(.dark-theme)'
}
}
})
app.use(ConfirmationService)
app.use(ToastService)
})
// Dark theme decorator
export const withTheme = (Story: any, context: any) => {
const theme = context.globals.theme || 'light'
// Apply theme class to document root
if (theme === 'dark') {
document.documentElement.classList.add('dark-theme')
document.body.classList.add('dark-theme')
} else {
document.documentElement.classList.remove('dark-theme')
document.body.classList.remove('dark-theme')
}
return Story()
}
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i
}
},
backgrounds: {
default: 'light',
values: [
{ name: 'light', value: '#ffffff' },
{ name: 'dark', value: '#0a0a0a' }
]
}
},
globalTypes: {
theme: {
name: 'Theme',
description: 'Global theme for components',
defaultValue: 'light',
toolbar: {
icon: 'circlehollow',
items: [
{ value: 'light', icon: 'sun', title: 'Light' },
{ value: 'dark', icon: 'moon', title: 'Dark' }
],
showName: true,
dynamicTitle: true
}
}
},
decorators: [withTheme]
}
export default preview

View File

@@ -61,6 +61,8 @@ Run `npm run prepare` to install Git pre-commit hooks. Currently, the pre-commit
### Dev Server
Note: The dev server will NOT load any extension from the ComfyUI server. Only core extensions will be loaded.
- Start local ComfyUI backend at `localhost:8188`
- Run `npm run dev` to start the dev server
- Run `npm run dev:electron` to start the dev server with electron API mocked
@@ -87,10 +89,6 @@ After you start the dev server, you should see following logs:
Make sure your desktop machine and touch device are on the same network. On your touch device,
navigate to `http://<server_ip>:5173` (e.g. `http://192.168.2.20:5173` here), to access the ComfyUI frontend.
> ⚠️ IMPORTANT:
The dev server will NOT load JavaScript extensions from custom nodes. Only core extensions (built into the frontend) will be loaded. This is because the shim system that allows custom node JavaScript to import frontend modules only works in production builds. Python custom nodes still function normally. See [Extension Development Guide](docs/extensions/development.md) for details and workarounds. And See [Extension Overview](docs/extensions/README.md) for extensions overview.
## Development Workflow
### Architecture Decision Records

View File

@@ -529,10 +529,6 @@ For detailed development setup, testing procedures, and technical information, p
See [locales/README.md](src/locales/README.md) for details.
### Storybook
See [.storybook/README.md](.storybook/README.md) for component development and visual testing documentation.
## Troubleshooting
For comprehensive troubleshooting and technical support, please refer to our official documentation:

View File

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

Before

Width:  |  Height:  |  Size: 260 KiB

After

Width:  |  Height:  |  Size: 260 KiB

View File

Before

Width:  |  Height:  |  Size: 152 B

After

Width:  |  Height:  |  Size: 152 B

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 75 KiB

View File

Before

Width:  |  Height:  |  Size: 200 KiB

After

Width:  |  Height:  |  Size: 200 KiB

View File

@@ -50,7 +50,7 @@ export class Topbar {
workflowName: string,
command: 'Save' | 'Save As' | 'Export'
) {
await this.triggerTopbarCommand(['File', command])
await this.triggerTopbarCommand(['Workflow', command])
await this.getSaveDialog().fill(workflowName)
await this.page.keyboard.press('Enter')
@@ -72,8 +72,8 @@ export class Topbar {
}
async triggerTopbarCommand(path: string[]) {
if (path.length < 1) {
throw new Error('Path cannot be empty')
if (path.length < 2) {
throw new Error('Path is too short')
}
const menu = await this.openTopbarMenu()
@@ -85,13 +85,6 @@ export class Topbar {
.locator('.p-tieredmenu-item')
.filter({ has: topLevelMenuItem })
await topLevelMenu.waitFor({ state: 'visible' })
// Handle top-level commands (like "New")
if (path.length === 1) {
await topLevelMenuItem.click()
return
}
await topLevelMenu.hover()
let currentMenu = topLevelMenu

View File

@@ -154,7 +154,7 @@ test.describe('Color Palette', () => {
// doesn't update the store immediately.
await comfyPage.setup()
await comfyPage.loadWorkflow('nodes/every_node_color')
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'
@@ -192,7 +192,7 @@ test.describe('Color Palette', () => {
test.describe('Node Color Adjustments', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.loadWorkflow('nodes/every_node_color')
await comfyPage.loadWorkflow('every_node_color')
})
test('should adjust opacity via node opacity setting', async ({

View File

@@ -7,7 +7,7 @@ test.describe('Load workflow warning', () => {
test('Should display a warning when loading a workflow with missing nodes', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('missing/missing_nodes')
await comfyPage.loadWorkflow('missing_nodes')
// Wait for the element with the .comfy-missing-nodes selector to be visible
const missingNodesWarning = comfyPage.page.locator('.comfy-missing-nodes')
@@ -17,7 +17,7 @@ test.describe('Load workflow warning', () => {
test('Should display a warning when loading a workflow with missing nodes in subgraphs', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('missing/missing_nodes_in_subgraph')
await comfyPage.loadWorkflow('missing_nodes_in_subgraph')
// Wait for the element with the .comfy-missing-nodes selector to be visible
const missingNodesWarning = comfyPage.page.locator('.comfy-missing-nodes')
@@ -33,7 +33,7 @@ test.describe('Load workflow warning', () => {
test('Does not report warning on undo/redo', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default')
await comfyPage.loadWorkflow('missing/missing_nodes')
await comfyPage.loadWorkflow('missing_nodes')
await comfyPage.closeDialog()
// Make a change to the graph
@@ -51,7 +51,7 @@ test.describe('Execution error', () => {
test('Should display an error message when an execution error occurs', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('nodes/execution_error')
await comfyPage.loadWorkflow('execution_error')
await comfyPage.queueButton.click()
await comfyPage.nextFrame()
@@ -61,7 +61,7 @@ test.describe('Execution error', () => {
})
test('Can display Issue Report form', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('nodes/execution_error')
await comfyPage.loadWorkflow('execution_error')
await comfyPage.queueButton.click()
await comfyPage.nextFrame()
@@ -84,7 +84,7 @@ test.describe('Missing models warning', () => {
test('Should display a warning when missing models are found', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('missing/missing_models')
await comfyPage.loadWorkflow('missing_models')
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
await expect(missingModelsWarning).toBeVisible()
@@ -97,7 +97,7 @@ test.describe('Missing models warning', () => {
comfyPage
}) => {
// Load workflow that has a node with models metadata at the node level
await comfyPage.loadWorkflow('missing/missing_models_from_node_properties')
await comfyPage.loadWorkflow('missing_models_from_node_properties')
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
await expect(missingModelsWarning).toBeVisible()
@@ -142,7 +142,7 @@ test.describe('Missing models warning', () => {
{ times: 1 }
)
await comfyPage.loadWorkflow('missing/missing_models')
await comfyPage.loadWorkflow('missing_models')
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
await expect(missingModelsWarning).not.toBeVisible()
@@ -153,7 +153,7 @@ test.describe('Missing models warning', () => {
}) => {
// This tests the scenario where outdated model metadata exists in the workflow
// but the actual selected models (widget values) have changed
await comfyPage.loadWorkflow('missing/model_metadata_widget_mismatch')
await comfyPage.loadWorkflow('model_metadata_widget_mismatch')
// The missing models warning should NOT appear
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
@@ -167,7 +167,7 @@ test.describe('Missing models warning', () => {
}) => {
// The fake_model.safetensors is served by
// https://github.com/Comfy-Org/ComfyUI_devtools/blob/main/__init__.py
await comfyPage.loadWorkflow('missing/missing_models')
await comfyPage.loadWorkflow('missing_models')
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
await expect(missingModelsWarning).toBeVisible()
@@ -190,7 +190,7 @@ test.describe('Missing models warning', () => {
'Comfy.Workflow.ShowMissingModelsWarning',
true
)
await comfyPage.loadWorkflow('missing/missing_models')
await comfyPage.loadWorkflow('missing_models')
checkbox = comfyPage.page.getByLabel("Don't show this again")
closeButton = comfyPage.page.getByLabel('Close')

View File

@@ -4,7 +4,7 @@ import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('DOM Widget', () => {
test('Collapsed multiline textarea is not visible', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/collapsed_multiline')
await comfyPage.loadWorkflow('collapsed_multiline')
const textareaWidget = comfyPage.page.locator('.comfy-multiline-input')
await expect(textareaWidget).not.toBeVisible()
})

View File

@@ -6,7 +6,7 @@ test.describe('Graph', () => {
// Should be able to fix link input slot index after swap the input order
// Ref: https://github.com/Comfy-Org/ComfyUI_frontend/issues/3348
test('Fix link input slots', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/input_order_swap')
await comfyPage.loadWorkflow('input_order_swap')
expect(
await comfyPage.page.evaluate(() => {
return window['app'].graph.links.get(1)?.target_slot
@@ -15,7 +15,7 @@ test.describe('Graph', () => {
})
test('Validate workflow links', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('links/bad_link')
await comfyPage.loadWorkflow('bad_link')
await expect(comfyPage.getVisibleToastCount()).resolves.toBe(2)
})
})

View File

@@ -137,9 +137,7 @@ test.describe('Group Node', () => {
test('Preserves hidden input configuration when containing duplicate node types', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow(
'groupnodes/group_node_identical_nodes_hidden_inputs'
)
await comfyPage.loadWorkflow('group_node_identical_nodes_hidden_inputs')
await comfyPage.nextFrame()
const groupNodeId = 19
@@ -206,7 +204,7 @@ test.describe('Group Node', () => {
test('Loads from a workflow using the legacy path separator ("/")', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('groupnodes/legacy_group_node')
await comfyPage.loadWorkflow('legacy_group_node')
expect(await comfyPage.getGraphNodesCount()).toBe(1)
await expect(
comfyPage.page.locator('.comfy-missing-nodes')
@@ -215,7 +213,7 @@ test.describe('Group Node', () => {
test.describe('Copy and paste', () => {
let groupNode: NodeReference | null
const WORKFLOW_NAME = 'groupnodes/group_node_v1.3.3'
const WORKFLOW_NAME = 'group_node_v1.3.3'
const GROUP_NODE_CATEGORY = 'group nodes>workflow'
const GROUP_NODE_PREFIX = 'workflow>'
const GROUP_NODE_NAME = 'group_node' // Node name in given workflow
@@ -270,7 +268,10 @@ test.describe('Group Node', () => {
await comfyPage.setSetting('Comfy.ConfirmClear', false)
// Clear workflow
await comfyPage.executeCommand('Comfy.ClearWorkflow')
await comfyPage.menu.topbar.triggerTopbarCommand([
'Edit',
'Clear Workflow'
])
await comfyPage.ctrlV()
await verifyNodeLoaded(comfyPage, 1)
@@ -279,7 +280,7 @@ test.describe('Group Node', () => {
test('Copies and pastes group node into a newly created blank workflow', async ({
comfyPage
}) => {
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
await comfyPage.ctrlV()
await verifyNodeLoaded(comfyPage, 1)
})
@@ -295,7 +296,7 @@ test.describe('Group Node', () => {
test('Serializes group node after copy and paste across workflows', async ({
comfyPage
}) => {
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
await comfyPage.ctrlV()
const currentGraphState = await comfyPage.page.evaluate(() =>
window['app'].graph.serialize()

View File

@@ -10,7 +10,7 @@ import { type NodeReference } from '../fixtures/utils/litegraphUtils'
test.describe('Item Interaction', () => {
test('Can select/delete all items', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('groups/mixed_graph_items')
await comfyPage.loadWorkflow('mixed_graph_items')
await comfyPage.canvas.press('Control+a')
await expect(comfyPage.canvas).toHaveScreenshot('selected-all.png')
await comfyPage.canvas.press('Delete')
@@ -18,7 +18,7 @@ test.describe('Item Interaction', () => {
})
test('Can pin/unpin items with keyboard shortcut', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('groups/mixed_graph_items')
await comfyPage.loadWorkflow('mixed_graph_items')
await comfyPage.canvas.press('Control+a')
await comfyPage.canvas.press('KeyP')
await comfyPage.nextFrame()
@@ -223,7 +223,7 @@ test.describe('Node Interaction', () => {
})
test('Link snap to slot', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('links/snap_to_slot')
await comfyPage.loadWorkflow('snap_to_slot')
await expect(comfyPage.canvas).toHaveScreenshot('snap_to_slot.png')
const outputSlotPos = {
@@ -240,7 +240,7 @@ test.describe('Node Interaction', () => {
})
test('Can batch move links by drag with shift', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('links/batch_move_links')
await comfyPage.loadWorkflow('batch_move_links')
await expect(comfyPage.canvas).toHaveScreenshot('batch_move_links.png')
const outputSlot1Pos = {
@@ -320,7 +320,7 @@ test.describe('Node Interaction', () => {
x: 167,
y: 143
}
await comfyPage.loadWorkflow('nodes/single_save_image_node')
await comfyPage.loadWorkflow('single_save_image_node')
await comfyPage.canvas.click({
position: textWidgetPos
})
@@ -340,7 +340,7 @@ test.describe('Node Interaction', () => {
})
test('Can double click node title to edit', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('nodes/single_ksampler')
await comfyPage.loadWorkflow('single_ksampler')
await comfyPage.canvas.dblclick({
position: {
x: 50,
@@ -356,7 +356,7 @@ test.describe('Node Interaction', () => {
test('Double click node body does not trigger edit', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('nodes/single_ksampler')
await comfyPage.loadWorkflow('single_ksampler')
await comfyPage.canvas.dblclick({
position: {
x: 50,
@@ -381,7 +381,7 @@ test.describe('Node Interaction', () => {
})
test('Can fit group to contents', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('groups/oversized_group')
await comfyPage.loadWorkflow('oversized_group')
await comfyPage.ctrlA()
await comfyPage.nextFrame()
await comfyPage.executeCommand('Comfy.Graph.FitGroupToContents')
@@ -414,7 +414,7 @@ test.describe('Node Interaction', () => {
test.describe('Group Interaction', () => {
test('Can double click group title to edit', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('groups/single_group')
await comfyPage.loadWorkflow('single_group')
await comfyPage.canvas.dblclick({
position: {
x: 50,
@@ -632,21 +632,21 @@ test.describe('Widget Interaction', () => {
test.describe('Load workflow', () => {
test('Can load workflow with string node id', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('nodes/string_node_id')
await comfyPage.loadWorkflow('string_node_id')
await expect(comfyPage.canvas).toHaveScreenshot('string_node_id.png')
})
test('Can load workflow with ("STRING",) input node', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('inputs/string_input')
await comfyPage.loadWorkflow('string_input')
await expect(comfyPage.canvas).toHaveScreenshot('string_input.png')
})
test('Restore workflow on reload (switch workflow)', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('nodes/single_ksampler')
await comfyPage.loadWorkflow('single_ksampler')
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler.png')
await comfyPage.setup({ clearStorage: false })
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler.png')
@@ -655,7 +655,7 @@ test.describe('Load workflow', () => {
test('Restore workflow on reload (modify workflow)', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('nodes/single_ksampler')
await comfyPage.loadWorkflow('single_ksampler')
const node = (await comfyPage.getFirstNodeRef())!
await node.click('collapse')
// Wait 300ms between 2 clicks so that it is not treated as a double click
@@ -684,7 +684,7 @@ test.describe('Load workflow', () => {
workflowA = generateUniqueFilename()
await comfyPage.menu.topbar.saveWorkflow(workflowA)
workflowB = generateUniqueFilename()
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
await comfyPage.menu.topbar.saveWorkflow(workflowB)
// Wait for localStorage to persist the workflow paths before reloading
@@ -734,7 +734,7 @@ test.describe('Load workflow', () => {
test('Auto fit view after loading workflow', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.EnableWorkflowViewRestore', false)
await comfyPage.loadWorkflow('nodes/single_ksampler')
await comfyPage.loadWorkflow('single_ksampler')
await expect(comfyPage.canvas).toHaveScreenshot('single_ksampler_fit.png')
})
})
@@ -747,10 +747,10 @@ test.describe('Load duplicate workflow', () => {
test('A workflow can be loaded multiple times in a row', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('nodes/single_ksampler')
await comfyPage.loadWorkflow('single_ksampler')
await comfyPage.menu.workflowsTab.open()
await comfyPage.executeCommand('Comfy.NewBlankWorkflow')
await comfyPage.loadWorkflow('nodes/single_ksampler')
await comfyPage.loadWorkflow('single_ksampler')
expect(await comfyPage.getGraphNodesCount()).toBe(1)
})
})

View File

@@ -22,7 +22,7 @@ test.describe('Load Workflow in Media', () => {
test(`Load workflow in ${fileName} (drop from filesystem)`, async ({
comfyPage
}) => {
await comfyPage.dragAndDropFile(`workflowInMedia/${fileName}`)
await comfyPage.dragAndDropFile(fileName)
await expect(comfyPage.canvas).toHaveScreenshot(`${fileName}.png`)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 105 KiB

View File

@@ -73,80 +73,9 @@ test.describe('Menu', () => {
expect(isTextCutoff).toBe(false)
})
test('Clicking on active state items does not close menu', async ({
comfyPage
}) => {
// Open the menu
await comfyPage.menu.topbar.openTopbarMenu()
const menu = comfyPage.page.locator('.comfy-command-menu')
// Navigate to View menu
const viewMenuItem = comfyPage.page.locator(
'.p-menubar-item-label:text-is("View")'
)
await viewMenuItem.hover()
// Wait for submenu to appear
const viewSubmenu = comfyPage.page
.locator('.p-tieredmenu-submenu:visible')
.first()
await viewSubmenu.waitFor({ state: 'visible' })
// Find Bottom Panel menu item
const bottomPanelMenuItem = viewSubmenu
.locator('.p-tieredmenu-item:has-text("Bottom Panel")')
.first()
const bottomPanelItem = bottomPanelMenuItem.locator(
'.p-menubar-item-label:text-is("Bottom Panel")'
)
await bottomPanelItem.waitFor({ state: 'visible' })
// Get checkmark icon element
const checkmark = bottomPanelMenuItem.locator('.pi-check')
// Check initial state of bottom panel (it's initially hidden)
const bottomPanel = comfyPage.page.locator('.bottom-panel')
await expect(bottomPanel).not.toBeVisible()
// Checkmark should be invisible initially (panel is hidden)
await expect(checkmark).toHaveClass(/invisible/)
// Click Bottom Panel to toggle it on
await bottomPanelItem.click()
// Verify menu is still visible after clicking
await expect(menu).toBeVisible()
await expect(viewSubmenu).toBeVisible()
// Verify bottom panel is now visible
await expect(bottomPanel).toBeVisible()
// Checkmark should now be visible (panel is shown)
await expect(checkmark).not.toHaveClass(/invisible/)
// Click Bottom Panel again to toggle it off
await bottomPanelItem.click()
// Verify menu is still visible after second click
await expect(menu).toBeVisible()
await expect(viewSubmenu).toBeVisible()
// Verify bottom panel is hidden again
await expect(bottomPanel).not.toBeVisible()
// Checkmark should be invisible again (panel is hidden)
await expect(checkmark).toHaveClass(/invisible/)
// Click outside to close menu
await comfyPage.page.locator('body').click({ position: { x: 10, y: 10 } })
// Verify menu is now closed
await expect(menu).not.toBeVisible()
})
test('Displays keybinding next to item', async ({ comfyPage }) => {
await comfyPage.menu.topbar.openTopbarMenu()
const workflowMenuItem = comfyPage.menu.topbar.getMenuItem('File')
const workflowMenuItem = comfyPage.menu.topbar.getMenuItem('Workflow')
await workflowMenuItem.hover()
const exportTag = comfyPage.page.locator('.keybinding-tag', {
hasText: 'Ctrl + s'

View File

@@ -66,7 +66,7 @@ test.describe('Node source badge', () => {
Object.values(NodeBadgeMode).forEach(async (mode) => {
test(`Shows node badges (${mode})`, async ({ comfyPage }) => {
// Execution error workflow has both custom node and core node.
await comfyPage.loadWorkflow('nodes/execution_error')
await comfyPage.loadWorkflow('execution_error')
await comfyPage.setSetting('Comfy.NodeBadge.NodeSourceBadgeMode', mode)
await comfyPage.setSetting('Comfy.NodeBadge.NodeIdBadgeMode', mode)
await comfyPage.nextFrame()

View File

@@ -6,32 +6,32 @@ import { comfyPageFixture as test } from '../fixtures/ComfyPage'
// a hollow circle no matter what shape it was defined in the workflow JSON.
test.describe('Optional input', () => {
test('No shape specified', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/optional_input_no_shape')
await comfyPage.loadWorkflow('optional_input_no_shape')
await expect(comfyPage.canvas).toHaveScreenshot('optional_input.png')
})
test('Wrong shape specified', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/optional_input_wrong_shape')
await comfyPage.loadWorkflow('optional_input_wrong_shape')
await expect(comfyPage.canvas).toHaveScreenshot('optional_input.png')
})
test('Correct shape specified', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/optional_input_correct_shape')
await comfyPage.loadWorkflow('optional_input_correct_shape')
await expect(comfyPage.canvas).toHaveScreenshot('optional_input.png')
})
test('Force input', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/force_input')
await comfyPage.loadWorkflow('force_input')
await expect(comfyPage.canvas).toHaveScreenshot('force_input.png')
})
test('Default input', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/default_input')
await comfyPage.loadWorkflow('default_input')
await expect(comfyPage.canvas).toHaveScreenshot('default_input.png')
})
test('Only optional inputs', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/only_optional_inputs')
await comfyPage.loadWorkflow('only_optional_inputs')
expect(await comfyPage.getGraphNodesCount()).toBe(1)
await expect(
comfyPage.page.locator('.comfy-missing-nodes')
@@ -43,7 +43,7 @@ test.describe('Optional input', () => {
)
})
test('Old workflow with converted input', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/old_workflow_converted_input')
await comfyPage.loadWorkflow('old_workflow_converted_input')
const node = await comfyPage.getNodeRefById('1')
const inputs = await node.getProperty('inputs')
const vaeInput = inputs.find((w) => w.name === 'vae')
@@ -55,25 +55,25 @@ test.describe('Optional input', () => {
expect(convertedInput.link).not.toBeNull()
})
test('Renamed converted input', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/renamed_converted_widget')
await comfyPage.loadWorkflow('renamed_converted_widget')
const node = await comfyPage.getNodeRefById('3')
const inputs = await node.getProperty('inputs')
const renamedInput = inputs.find((w) => w.name === 'breadth')
expect(renamedInput).toBeUndefined()
})
test('slider', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/simple_slider')
await comfyPage.loadWorkflow('simple_slider')
await expect(comfyPage.canvas).toHaveScreenshot('simple_slider.png')
})
test('unknown converted widget', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Workflow.ShowMissingNodesWarning', false)
await comfyPage.loadWorkflow('missing/missing_nodes_converted_widget')
await comfyPage.loadWorkflow('missing_nodes_converted_widget')
await expect(comfyPage.canvas).toHaveScreenshot(
'missing_nodes_converted_widget.png'
)
})
test('dynamically added input', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/dynamically_added_input')
await comfyPage.loadWorkflow('dynamically_added_input')
await expect(comfyPage.canvas).toHaveScreenshot(
'dynamically_added_input.png'
)

View File

@@ -319,7 +319,7 @@ test.describe('Node Help', () => {
comfyPage
}) => {
// First load workflow with custom node
await comfyPage.loadWorkflow('groupnodes/group_node_v1.3.3')
await comfyPage.loadWorkflow('group_node_v1.3.3')
// Mock custom node documentation with fallback
await comfyPage.page.route(

View File

@@ -16,7 +16,7 @@ test.describe('Node search box', () => {
})
test(`Can trigger on group body double click`, async ({ comfyPage }) => {
await comfyPage.loadWorkflow('groups/single_group_only')
await comfyPage.loadWorkflow('single_group_only')
await comfyPage.page.mouse.dblclick(50, 50, { delay: 5 })
await comfyPage.nextFrame()
await expect(comfyPage.searchBox.input).toHaveCount(1)
@@ -59,7 +59,7 @@ test.describe('Node search box', () => {
})
test('Can auto link batch moved node', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('links/batch_move_links')
await comfyPage.loadWorkflow('batch_move_links')
const outputSlot1Pos = {
x: 304,
@@ -110,7 +110,7 @@ test.describe('Node search box', () => {
test('@mobile Can trigger on empty canvas tap', async ({ comfyPage }) => {
await comfyPage.closeMenu()
await comfyPage.loadWorkflow('nodes/single_ksampler')
await comfyPage.loadWorkflow('single_ksampler')
const screenCenter = {
x: 200,
y: 400

View File

@@ -4,7 +4,7 @@ import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Note Node', () => {
test('Can load node nodes', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('nodes/note_nodes')
await comfyPage.loadWorkflow('note_nodes')
await expect(comfyPage.canvas).toHaveScreenshot('note_nodes.png')
})
})

View File

@@ -89,7 +89,7 @@ test.describe('Remote COMBO Widget', () => {
comfyPage
}) => {
const nodeName = 'Remote Widget Node'
await comfyPage.loadWorkflow('inputs/remote_widget')
await comfyPage.loadWorkflow('remote_widget')
await comfyPage.page.waitForTimeout(512)
const node = await comfyPage.page.evaluate((name) => {

View File

@@ -15,10 +15,10 @@ test.describe('Reroute Node', () => {
test('loads from inserted workflow', async ({ comfyPage }) => {
const workflowName = 'single_connected_reroute_node.json'
await comfyPage.setupWorkflowsDirectory({
[workflowName]: 'links/single_connected_reroute_node.json'
[workflowName]: workflowName
})
await comfyPage.setup()
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
// Insert the workflow
const workflowsTab = comfyPage.menu.workflowsTab

View File

@@ -33,7 +33,7 @@ test.describe('Selection Toolbox', () => {
test('shows at correct position when node is pasted', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('nodes/single_ksampler')
await comfyPage.loadWorkflow('single_ksampler')
await comfyPage.selectNodes(['KSampler'])
await comfyPage.ctrlC()
await comfyPage.page.mouse.move(100, 100)
@@ -56,7 +56,7 @@ test.describe('Selection Toolbox', () => {
test('hide when select and drag happen at the same time', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('nodes/single_ksampler')
await comfyPage.loadWorkflow('single_ksampler')
const node = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
const nodePos = await node.getPosition()
@@ -103,7 +103,7 @@ test.describe('Selection Toolbox', () => {
comfyPage
}) => {
// A group + a KSampler node
await comfyPage.loadWorkflow('groups/single_group')
await comfyPage.loadWorkflow('single_group')
// Select group + node should show bypass button
await comfyPage.page.focus('canvas')

View File

@@ -74,7 +74,7 @@ test.describe('Workflows sidebar', () => {
test('Can open workflow after insert', async ({ comfyPage }) => {
await comfyPage.setupWorkflowsDirectory({
'workflow1.json': 'nodes/single_ksampler.json'
'workflow1.json': 'single_ksampler.json'
})
const tab = comfyPage.menu.workflowsTab
@@ -241,7 +241,7 @@ test.describe('Workflows sidebar', () => {
test('Does not report warning when switching between opened workflows', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('missing/missing_nodes')
await comfyPage.loadWorkflow('missing_nodes')
await comfyPage.closeDialog()
// Load blank workflow

View File

@@ -20,7 +20,7 @@ test.describe('Subgraph Slot Rename Dialog', () => {
test('Shows current slot label (not stale) in rename dialog', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.loadWorkflow('basic-subgraph')
const subgraphNode = await comfyPage.getNodeRefById('2')
await subgraphNode.navigateIntoSubgraph()
@@ -106,7 +106,7 @@ test.describe('Subgraph Slot Rename Dialog', () => {
test('Shows current output slot label in rename dialog', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.loadWorkflow('basic-subgraph')
const subgraphNode = await comfyPage.getNodeRefById('2')
await subgraphNode.navigateIntoSubgraph()

View File

@@ -52,7 +52,7 @@ test.describe('Subgraph Operations', () => {
test.describe('I/O Slot Management', () => {
test('Can add input slots to subgraph', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.loadWorkflow('basic-subgraph')
const subgraphNode = await comfyPage.getNodeRefById('2')
await subgraphNode.navigateIntoSubgraph()
@@ -68,7 +68,7 @@ test.describe('Subgraph Operations', () => {
})
test('Can add output slots to subgraph', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.loadWorkflow('basic-subgraph')
const subgraphNode = await comfyPage.getNodeRefById('2')
await subgraphNode.navigateIntoSubgraph()
@@ -84,7 +84,7 @@ test.describe('Subgraph Operations', () => {
})
test('Can remove input slots from subgraph', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.loadWorkflow('basic-subgraph')
const subgraphNode = await comfyPage.getNodeRefById('2')
await subgraphNode.navigateIntoSubgraph()
@@ -104,7 +104,7 @@ test.describe('Subgraph Operations', () => {
})
test('Can remove output slots from subgraph', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.loadWorkflow('basic-subgraph')
const subgraphNode = await comfyPage.getNodeRefById('2')
await subgraphNode.navigateIntoSubgraph()
@@ -124,7 +124,7 @@ test.describe('Subgraph Operations', () => {
})
test('Can rename I/O slots', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.loadWorkflow('basic-subgraph')
const subgraphNode = await comfyPage.getNodeRefById('2')
await subgraphNode.navigateIntoSubgraph()
@@ -157,7 +157,7 @@ test.describe('Subgraph Operations', () => {
})
test('Can rename input slots via double-click', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.loadWorkflow('basic-subgraph')
const subgraphNode = await comfyPage.getNodeRefById('2')
await subgraphNode.navigateIntoSubgraph()
@@ -189,7 +189,7 @@ test.describe('Subgraph Operations', () => {
})
test('Can rename output slots via double-click', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.loadWorkflow('basic-subgraph')
const subgraphNode = await comfyPage.getNodeRefById('2')
await subgraphNode.navigateIntoSubgraph()
@@ -224,7 +224,7 @@ test.describe('Subgraph Operations', () => {
test('Right-click context menu still works alongside double-click', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.loadWorkflow('basic-subgraph')
const subgraphNode = await comfyPage.getNodeRefById('2')
await subgraphNode.navigateIntoSubgraph()
@@ -261,7 +261,7 @@ test.describe('Subgraph Operations', () => {
test('Can double-click on slot label text to rename', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.loadWorkflow('basic-subgraph')
const subgraphNode = await comfyPage.getNodeRefById('2')
await subgraphNode.navigateIntoSubgraph()
@@ -355,7 +355,7 @@ test.describe('Subgraph Operations', () => {
})
test('Can delete subgraph node', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.loadWorkflow('basic-subgraph')
const subgraphNode = await comfyPage.getNodeRefById('2')
expect(await subgraphNode.exists()).toBe(true)
@@ -377,7 +377,7 @@ test.describe('Subgraph Operations', () => {
test('Can copy subgraph node by dragging + alt', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.loadWorkflow('basic-subgraph')
const subgraphNode = await comfyPage.getNodeRefById('2')
@@ -406,7 +406,7 @@ test.describe('Subgraph Operations', () => {
test('Copying subgraph node by dragging + alt creates a new subgraph node with unique type', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.loadWorkflow('basic-subgraph')
const subgraphNode = await comfyPage.getNodeRefById('2')
@@ -438,7 +438,7 @@ test.describe('Subgraph Operations', () => {
test.describe('Operations Inside Subgraphs', () => {
test('Can copy and paste nodes in subgraph', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.loadWorkflow('basic-subgraph')
const subgraphNode = await comfyPage.getNodeRefById('2')
await subgraphNode.navigateIntoSubgraph()
@@ -469,7 +469,7 @@ test.describe('Subgraph Operations', () => {
})
test('Can undo and redo operations in subgraph', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.loadWorkflow('basic-subgraph')
const subgraphNode = await comfyPage.getNodeRefById('2')
await subgraphNode.navigateIntoSubgraph()
@@ -506,7 +506,7 @@ test.describe('Subgraph Operations', () => {
test('Breadcrumb updates when subgraph node title is changed', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('subgraphs/nested-subgraph')
await comfyPage.loadWorkflow('nested-subgraph')
await comfyPage.nextFrame()
const subgraphNode = await comfyPage.getNodeRefById('10')
@@ -558,9 +558,7 @@ test.describe('Subgraph Operations', () => {
test('DOM widget visibility persists through subgraph navigation', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow(
'subgraphs/subgraph-with-promoted-text-widget'
)
await comfyPage.loadWorkflow('subgraph-with-promoted-text-widget')
await comfyPage.nextFrame()
// Verify promoted widget is visible in parent graph
@@ -591,9 +589,7 @@ test.describe('Subgraph Operations', () => {
test('DOM widget content is preserved through navigation', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow(
'subgraphs/subgraph-with-promoted-text-widget'
)
await comfyPage.loadWorkflow('subgraph-with-promoted-text-widget')
const textarea = comfyPage.page.locator(SELECTORS.domWidget)
await textarea.fill(TEST_WIDGET_CONTENT)
@@ -614,9 +610,7 @@ test.describe('Subgraph Operations', () => {
test('DOM elements are cleaned up when subgraph node is removed', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow(
'subgraphs/subgraph-with-promoted-text-widget'
)
await comfyPage.loadWorkflow('subgraph-with-promoted-text-widget')
const initialCount = await comfyPage.page
.locator(SELECTORS.domWidget)
@@ -641,7 +635,7 @@ test.describe('Subgraph Operations', () => {
// Enable new menu for breadcrumb navigation
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
const workflowName = 'subgraphs/subgraph-with-promoted-text-widget'
const workflowName = 'subgraph-with-promoted-text-widget'
await comfyPage.loadWorkflow(workflowName)
const textareaCount = await comfyPage.page
@@ -667,8 +661,8 @@ test.describe('Subgraph Operations', () => {
// Click breadcrumb to navigate back to parent graph
const homeBreadcrumb = comfyPage.page.getByRole('link', {
// In the subgraph navigation breadcrumbs, the home/top level
// breadcrumb is just the workflow name without the folder path
name: 'subgraph-with-promoted-text-widget'
// breadcrumb is just the workflow name
name: workflowName
})
await homeBreadcrumb.waitFor({ state: 'visible' })
await homeBreadcrumb.click()
@@ -686,9 +680,7 @@ test.describe('Subgraph Operations', () => {
test('Multiple promoted widgets are handled correctly', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow(
'subgraphs/subgraph-with-multiple-promoted-widgets'
)
await comfyPage.loadWorkflow('subgraph-with-multiple-promoted-widgets')
const parentCount = await comfyPage.page
.locator(SELECTORS.domWidget)
@@ -719,7 +711,7 @@ test.describe('Subgraph Operations', () => {
})
test('Navigation hotkey can be customized', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.loadWorkflow('basic-subgraph')
await comfyPage.nextFrame()
// Change the Exit Subgraph keybinding from Escape to Alt+Q
@@ -775,7 +767,7 @@ test.describe('Subgraph Operations', () => {
test('Escape prioritizes closing dialogs over exiting subgraph', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('subgraphs/basic-subgraph')
await comfyPage.loadWorkflow('basic-subgraph')
await comfyPage.nextFrame()
const subgraphNode = await comfyPage.getNodeRefById('2')

View File

@@ -38,7 +38,7 @@ test.describe('Combo text widget', () => {
.options.values
})
await comfyPage.loadWorkflow('inputs/optional_combo_input')
await comfyPage.loadWorkflow('optional_combo_input')
const initialComboValues = await getComboValues()
// Focus canvas
@@ -57,7 +57,7 @@ test.describe('Combo text widget', () => {
test('Should refresh combo values of nodes with v2 combo input spec', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('inputs/node_with_v2_combo_input')
await comfyPage.loadWorkflow('node_with_v2_combo_input')
// click canvas to focus
await comfyPage.page.mouse.click(400, 300)
// press R to trigger refresh
@@ -90,7 +90,7 @@ test.describe('Boolean widget', () => {
test.describe('Slider widget', () => {
test('Can drag adjust value', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/simple_slider')
await comfyPage.loadWorkflow('simple_slider')
await comfyPage.page.waitForTimeout(300)
const node = (await comfyPage.getFirstNodeRef())!
const widget = await node.getWidget(0)
@@ -136,7 +136,7 @@ test.describe('Dynamic widget manipulation', () => {
test('Auto expand node when widget is added dynamically', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('nodes/single_ksampler')
await comfyPage.loadWorkflow('single_ksampler')
await comfyPage.page.waitForTimeout(300)
await comfyPage.page.evaluate(() => {

View File

@@ -63,7 +63,7 @@ test.describe('Workflow Tab Thumbnails', () => {
test('Should show thumbnail when hovering over a non-active tab', async ({
comfyPage
}) => {
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
const thumbnailImg = await getTabThumbnailImage(
comfyPage,
0,
@@ -73,7 +73,7 @@ test.describe('Workflow Tab Thumbnails', () => {
})
test('Should not show thumbnail for active tab', async ({ comfyPage }) => {
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
const thumbnailImg = await getTabThumbnailImage(
comfyPage,
1,
@@ -105,7 +105,7 @@ test.describe('Workflow Tab Thumbnails', () => {
await comfyPage.nextFrame()
// Create a new workflow (tab 1) which will be empty
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
await comfyPage.menu.topbar.triggerTopbarCommand(['Workflow', 'New'])
await comfyPage.nextFrame()
// Now we have two tabs: tab 0 (default workflow with nodes) and tab 1 (empty)

View File

@@ -1,45 +0,0 @@
# ComfyUI Extensions Documentation
## Overview
Extensions are the primary way to add functionality to ComfyUI. They can be custom nodes, custom nodes that render widgets (UIs made with javascript), ComfyUI shell UI enhancements, and more. This documentation covers everything you need to know about understanding, using, and developing extensions.
## Documentation Structure
- **[Development Guide](./development.md)** - How to develop extensions, including:
- Extension architecture and terminology
- How extensions load (backend vs frontend)
- Why extensions don't work in dev server
- Development workarounds and best practices
- **[Core Extensions Reference](./core.md)** - Detailed reference for core extensions:
- Complete list of all core extensions
- Extension architecture principles
- Hook execution sequence
- Best practices for extension development
## Quick Links
### Key Concepts
- **Extension**: Umbrella term for any code that extends ComfyUI
- **Custom Nodes**: Python backend nodes (a type of extension)
- **JavaScript Extensions**: Frontend UI enhancements
- **Core Extensions**: Built-in extensions bundled with ComfyUI
### Common Tasks
- [Developing extensions in dev mode](./development.md#development-workarounds)
- [Understanding the shim system](./development.md#how-the-shim-works)
- [Extension hooks and lifecycle](./core.md#extension-hooks)
### External Resources
- [Official JavaScript Extension Docs](https://docs.comfy.org/custom-nodes/js/javascript_overview)
- [ComfyExtension TypeScript Interface](../../src/types/comfy.ts)
## Need Help?
- Check the [Development Guide](./development.md) for common issues
- Review [Core Extensions](./core.md) for examples
- Visit the [ComfyUI Discord](https://discord.com/invite/comfyorg) for community support

View File

@@ -1,181 +0,0 @@
# Core Extensions
This directory contains the core extensions that provide essential functionality to the ComfyUI frontend.
## Table of Contents
- [Overview](#overview)
- [Extension Architecture](#extension-architecture)
- [Core Extensions](#core-extensions)
- [Extension Development](#extension-development)
- [Extension Hooks](#extension-hooks)
- [Further Reading](#further-reading)
## Overview
Extensions in ComfyUI are modular JavaScript modules that extend and enhance the functionality of the frontend. The extensions in this directory are considered "core" as they provide fundamental features that are built into ComfyUI by default.
## Extension Architecture
ComfyUI's extension system follows these key principles:
1. **Registration-based:** Extensions must register themselves with the application using `app.registerExtension()`
2. **Hook-driven:** Extensions interact with the system through predefined hooks
3. **Non-intrusive:** Extensions should avoid directly modifying core objects where possible
## Core Extensions List
The following table lists ALL core extensions in the system as of 2025-01-30:
### Main Extensions
| Extension | Description | Category |
|-----------|-------------|----------|
| clipspace.ts | Implements the Clipspace feature for temporary image storage | Image |
| contextMenuFilter.ts | Provides context menu filtering capabilities | UI |
| dynamicPrompts.ts | Provides dynamic prompt generation capabilities | Prompts |
| editAttention.ts | Implements attention editing functionality | Text |
| electronAdapter.ts | Adapts functionality for Electron environment | Platform |
| groupNode.ts | Implements the group node functionality to organize workflows | Graph |
| groupNodeManage.ts | Provides group node management operations | Graph |
| groupOptions.ts | Handles group node configuration options | Graph |
| index.ts | Main extension registration and coordination | Core |
| load3d.ts | Supports 3D model loading and visualization | 3D |
| maskEditorOld.ts | Legacy mask editor implementation | Image |
| maskeditor.ts | Implements the mask editor for image masking operations | Image |
| nodeTemplates.ts | Provides node template functionality | Templates |
| noteNode.ts | Adds note nodes for documentation within workflows | Graph |
| previewAny.ts | Universal preview functionality for various data types | Preview |
| rerouteNode.ts | Implements reroute nodes for cleaner workflow connections | Graph |
| saveImageExtraOutput.ts | Handles additional image output saving | Image |
| saveMesh.ts | Implements 3D mesh saving functionality | 3D |
| simpleTouchSupport.ts | Provides basic touch interaction support | Input |
| slotDefaults.ts | Manages default values for node slots | Nodes |
| uploadAudio.ts | Handles audio file upload functionality | Audio |
| uploadImage.ts | Handles image upload functionality | Image |
| webcamCapture.ts | Provides webcam capture capabilities | Media |
| widgetInputs.ts | Implements various widget input types | Widgets |
### Conditional Lines Subdirectory
Located in `extensions/core/load3d/conditional-lines/`:
| File | Description |
|------|-------------|
| ColoredShadowMaterial.js | Material for colored shadow rendering |
| ConditionalEdgesGeometry.js | Geometry for conditional edge rendering |
| ConditionalEdgesShader.js | Shader for conditional edges |
| OutsideEdgesGeometry.js | Geometry for outside edge detection |
### Lines2 Subdirectory
Located in `extensions/core/load3d/conditional-lines/Lines2/`:
| File | Description |
|------|-------------|
| ConditionalLineMaterial.js | Material for conditional line rendering |
| ConditionalLineSegmentsGeometry.js | Geometry for conditional line segments |
### ThreeJS Override Subdirectory
Located in `extensions/core/load3d/threejsOverride/`:
| File | Description |
|------|-------------|
| OverrideMTLLoader.js | Custom MTL loader with enhanced functionality |
## Extension Development
When developing or modifying extensions, follow these best practices:
1. **Use provided hooks** rather than directly modifying core application objects
2. **Maintain compatibility** with other extensions
3. **Follow naming conventions** for both extension names and settings
4. **Properly document** extension hooks and functionality
5. **Test with other extensions** to ensure no conflicts
### Extension Registration
Extensions are registered using the `app.registerExtension()` method:
```javascript
app.registerExtension({
name: "MyExtension",
// Hook implementations
async init() {
// Implementation
},
async beforeRegisterNodeDef(nodeType, nodeData, app) {
// Implementation
}
// Other hooks as needed
});
```
## Extension Hooks
ComfyUI extensions can implement various hooks that are called at specific points in the application lifecycle:
### Hook Execution Sequence
#### Web Page Load
```
init
addCustomNodeDefs
getCustomWidgets
beforeRegisterNodeDef [repeated multiple times]
registerCustomNodes
beforeConfigureGraph
nodeCreated
loadedGraphNode
afterConfigureGraph
setup
```
#### Loading Workflow
```
beforeConfigureGraph
beforeRegisterNodeDef [zero, one, or multiple times]
nodeCreated [repeated multiple times]
loadedGraphNode [repeated multiple times]
afterConfigureGraph
```
#### Adding New Node
```
nodeCreated
```
### Key Hooks
| Hook | Description |
|------|-------------|
| `init` | Called after canvas creation but before nodes are added |
| `setup` | Called after the application is fully set up and running |
| `addCustomNodeDefs` | Called before nodes are registered with the graph |
| `getCustomWidgets` | Allows extensions to add custom widgets |
| `beforeRegisterNodeDef` | Allows extensions to modify nodes before registration |
| `registerCustomNodes` | Allows extensions to register additional nodes |
| `loadedGraphNode` | Called when a node is reloaded onto the graph |
| `nodeCreated` | Called after a node's constructor |
| `beforeConfigureGraph` | Called before a graph is configured |
| `afterConfigureGraph` | Called after a graph is configured |
| `getSelectionToolboxCommands` | Allows extensions to add commands to the selection toolbox |
For the complete list of available hooks and detailed descriptions, see the [ComfyExtension interface in comfy.ts](https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/types/comfy.ts).
## Further Reading
For more detailed information about ComfyUI's extension system, refer to the official documentation:
- [JavaScript Extension Overview](https://docs.comfy.org/custom-nodes/js/javascript_overview)
- [JavaScript Hooks](https://docs.comfy.org/custom-nodes/js/javascript_hooks)
- [JavaScript Objects and Hijacking](https://docs.comfy.org/custom-nodes/js/javascript_objects_and_hijacking)
- [JavaScript Settings](https://docs.comfy.org/custom-nodes/js/javascript_settings)
- [JavaScript Examples](https://docs.comfy.org/custom-nodes/js/javascript_examples)
Also, check the main [README.md](https://github.com/Comfy-Org/ComfyUI_frontend#developer-apis) section on Developer APIs for the latest information on extension APIs and features.

View File

@@ -1,136 +0,0 @@
# Extension Development Guide
## Understanding Extensions in ComfyUI
### Terminology Clarification
**ComfyUI Extension** - The umbrella term for any 3rd party code that extends ComfyUI functionality. This includes:
1. **Python Custom Nodes** - Backend nodes providing new operations
- Located in `/custom_nodes/` directories
- Registered via Python module system
- Identified by `custom_nodes.<name>` in `python_module` field
2. **JavaScript Extensions** - Frontend functionality that can be:
- Pure JavaScript extensions (implement `ComfyExtension` interface)
- JavaScript components of custom nodes (in `/web/` or `/js/` folders, or custom directories specified via `WEB_DIRECTORY` export in `__init__.py` [see docs](https://docs.comfy.org/custom-nodes/backend/lifecycle#web-directory))
- Core extensions (built into frontend at `/src/extensions/core/` - see [Core Extensions Documentation](./core.md))
### How Extensions Load
**Backend Flow (Python Custom Nodes):**
1. ComfyUI server starts → scans `/custom_nodes/` directories
2. Loads Python modules (e.g., `/custom_nodes/ComfyUI-Impact-Pack/__init__.py`)
3. Python code registers new node types with the server
4. Server exposes these via `/object_info` API with metadata like `python_module: "custom_nodes.ComfyUI-Impact-Pack"`
5. These nodes execute on the server when workflows run
**Frontend Flow (JavaScript):**
*Core Extensions (always available):*
1. Built directly into the frontend bundle at `/src/extensions/core/`
2. Loaded immediately when the frontend starts
3. No network requests needed - they're part of the compiled code
*Custom Node JavaScript (loaded dynamically):*
1. Frontend starts → calls `/extensions` API
2. Server responds with list of JavaScript files from:
- `/web/extensions/*.js` (legacy location)
- `/custom_nodes/*/web/*.js` (node-specific UI code)
3. Frontend fetches each JavaScript file (e.g., `/extensions/ComfyUI-Impact-Pack/impact.js`)
4. JavaScript executes immediately, calling `app.registerExtension()` to hook into the UI
5. These registered hooks enhance the UI for their associated Python nodes
**The Key Distinction:**
- **Python nodes** = Backend processing (what shows in your node menu)
- **JavaScript extensions** = Frontend enhancements (how nodes look/behave in the UI)
- A custom node package can have both, just Python, or (rarely) just JavaScript
## Why Extensions Don't Load in Dev Server
The development server cannot load custom node JavaScript due to architectural constraints from the TypeScript/Vite migration.
### The Technical Challenge
ComfyUI migrated to TypeScript and Vite, but thousands of extensions rely on the old unbundled module system. The solution was a **shim system** that maintains backward compatibility - but only in production builds.
### How the Shim Works
**Production Build:**
During production build, a custom Vite plugin:
- Binds all module exports to `window.comfyAPI`
- Generates shim files that re-export from this global object
```javascript
// Original source: /scripts/api.ts
export const api = { }
// Generated shim: /scripts/api.js
export * from window.comfyAPI.modules['/scripts/api.js']
// Extension imports work unchanged:
import { api } from '/scripts/api.js'
```
**Why Dev Server Can't Support This:**
- The dev server serves raw source files without bundling
- Vite refuses to transform node_modules in unbundled mode
- Creating real-time shims would require intercepting every module request
- This would defeat the purpose of a fast dev server
### The Trade-off
This was the least friction approach:
- ✅ Extensions work in production without changes
- ✅ Developers get modern tooling (TypeScript, hot reload)
- ❌ Extension testing requires production build or workarounds
The alternative would have been breaking all existing extensions or staying with the legacy build system.
## Development Workarounds
### Option 1: Develop as Core Extension (Recommended)
1. Copy your extension to `src/extensions/core/` (see [Core Extensions Documentation](./core.md) for existing core extensions and architecture)
2. Update imports to relative paths:
```javascript
import { app } from '../../scripts/app'
import { api } from '../../scripts/api'
```
3. Add to `src/extensions/core/index.ts`
4. Test with hot reload working
5. Move back when complete
### Option 2: Use Production Build
Build the frontend for full functionality:
```bash
npm run build
```
For faster iteration during development, use watch mode:
```bash
npx vite build --watch
```
Note: Watch mode provides faster rebuilds than full builds, but still no hot reload
### Option 3: Test Against Cloud/Staging
For cloud extensions, modify `.env`:
```
DEV_SERVER_COMFYUI_URL=http://stagingcloud.comfy.org/
```
## Key Points
- Python nodes work normally in dev mode
- JavaScript extensions require workarounds or production builds
- Core extensions provide built-in functionality - see [Core Extensions Documentation](./core.md) for the complete list
- The `ComfyExtension` interface defines all available hooks for extending the frontend
## Further Reading
- [Core Extensions Architecture](./core.md) - Complete list of core extensions and development guidelines
- [JavaScript Extension Hooks](https://docs.comfy.org/custom-nodes/js/javascript_hooks) - Official documentation on extension hooks
- [ComfyExtension Interface](../../src/types/comfy.ts) - TypeScript interface defining all extension capabilities

View File

@@ -1,8 +1,6 @@
// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
import pluginJs from '@eslint/js'
import pluginI18n from '@intlify/eslint-plugin-vue-i18n'
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
import storybook from 'eslint-plugin-storybook'
import unusedImports from 'eslint-plugin-unused-imports'
import pluginVue from 'eslint-plugin-vue'
import globals from 'globals'
@@ -96,6 +94,5 @@ export default [
}
]
}
},
...storybook.configs['flat/recommended']
}
]

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