Compare commits
59 Commits
manager/fi
...
ry-temp3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13056a00b1 | ||
|
|
fdd6b3bcfc | ||
|
|
529a4de583 | ||
|
|
5c0eef8d3f | ||
|
|
43db891c1a | ||
|
|
1b1cb956e6 | ||
|
|
ff0c15b119 | ||
|
|
1c0f151d02 | ||
|
|
2702ac64fe | ||
|
|
8ca541e850 | ||
|
|
d3a5d9e995 | ||
|
|
168e885d50 | ||
|
|
504aabd097 | ||
|
|
c7bbab53a6 | ||
|
|
33b6df55a8 | ||
|
|
16ebe33488 | ||
|
|
108ad22d82 | ||
|
|
4a10017bd2 | ||
|
|
646d7a68be | ||
|
|
c13371ef47 | ||
|
|
775c856bf7 | ||
|
|
e035f895a3 | ||
|
|
98e543ec31 | ||
|
|
992efc4486 | ||
|
|
88130a9cae | ||
|
|
ffd2b0efab | ||
|
|
7c9b8bb7a6 | ||
|
|
18b3b11b9a | ||
|
|
80b1c2aaf7 | ||
|
|
a13eeaea7e | ||
|
|
59a1380f39 | ||
|
|
f1fbab6e1f | ||
|
|
9bd3d5cbe6 | ||
|
|
bd48649604 | ||
|
|
c6c9487c0d | ||
|
|
799795cf56 | ||
|
|
4899c9d25b | ||
|
|
0bd3c1271d | ||
|
|
6eb91e4aed | ||
|
|
3b3071c975 | ||
|
|
68f0275a83 | ||
|
|
a0d66bb0d7 | ||
|
|
1292ae0f14 | ||
|
|
8da2b304ef | ||
|
|
0950da0b43 | ||
|
|
86e2b1fc61 | ||
|
|
4a612b09ed | ||
|
|
4a3c3d9c97 | ||
|
|
c3c59988f4 | ||
|
|
e6d3e94a34 | ||
|
|
1c0c501105 | ||
|
|
980b727ff8 | ||
|
|
40c47a8e67 | ||
|
|
f0f4313afa | ||
|
|
cb5894a100 | ||
|
|
7649feb47f | ||
|
|
c27edb7e94 | ||
|
|
23e881e220 | ||
|
|
c5c06b6ba8 |
@@ -67,9 +67,9 @@ This is critical for better file inspection:
|
||||
|
||||
Use git locally for much faster analysis:
|
||||
|
||||
1. Get list of changed files: `git diff --name-only "origin/$BASE_BRANCH" > changed_files.txt`
|
||||
2. Get the full diff: `git diff "origin/$BASE_BRANCH" > pr_diff.txt`
|
||||
3. Get detailed file changes with status: `git diff --name-status "origin/$BASE_BRANCH" > file_changes.txt`
|
||||
1. Get list of changed files: `git diff --name-only "$BASE_SHA" > changed_files.txt`
|
||||
2. Get the full diff: `git diff "$BASE_SHA" > pr_diff.txt`
|
||||
3. Get detailed file changes with status: `git diff --name-status "$BASE_SHA" > file_changes.txt`
|
||||
|
||||
### Step 1.5: Create Analysis Cache
|
||||
|
||||
|
||||
@@ -1,30 +1,85 @@
|
||||
# Create Hotfix Release
|
||||
|
||||
This command guides you through creating a patch/hotfix release for ComfyUI Frontend with comprehensive safety checks and human confirmations at each step.
|
||||
This command creates patch/hotfix releases for ComfyUI Frontend by backporting fixes to stable core branches. It handles both automated backports (preferred) and manual cherry-picking (fallback).
|
||||
|
||||
**Process Overview:**
|
||||
1. **Check automated backports first** (via labels)
|
||||
2. **Skip to version bump** if backports already merged
|
||||
3. **Manual cherry-picking** if automation failed
|
||||
4. **Create patch release** with version bump
|
||||
5. **Publish GitHub release** (manually uncheck "latest")
|
||||
6. **Update ComfyUI requirements.txt** via PR
|
||||
|
||||
<task>
|
||||
Create a hotfix release by cherry-picking commits or PR commits from main to a core branch: $ARGUMENTS
|
||||
Create a hotfix release by backporting commits/PRs from main to a core branch: $ARGUMENTS
|
||||
|
||||
Expected format: Comma-separated list of commits or PR numbers
|
||||
Examples:
|
||||
- `abc123,def456,ghi789` (commits)
|
||||
- `#1234,#5678` (PRs)
|
||||
- `abc123,#1234,def456` (mixed)
|
||||
- `#1234,#5678` (PRs - preferred)
|
||||
- `abc123,def456` (commit hashes)
|
||||
- `#1234,abc123` (mixed)
|
||||
|
||||
If no arguments provided, the command will help identify the correct core branch and guide you through selecting commits/PRs.
|
||||
If no arguments provided, the command will guide you through identifying commits/PRs to backport.
|
||||
</task>
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before starting, ensure:
|
||||
- You have push access to the repository
|
||||
- GitHub CLI (`gh`) is authenticated
|
||||
- You're on a clean working tree
|
||||
- You understand the commits/PRs you're cherry-picking
|
||||
- Push access to repository
|
||||
- GitHub CLI (`gh`) authenticated
|
||||
- Clean working tree
|
||||
- Understanding of what fixes need backporting
|
||||
|
||||
## Hotfix Release Process
|
||||
|
||||
### Step 1: Identify Target Core Branch
|
||||
### Step 1: Try Automated Backports First
|
||||
|
||||
**Check if automated backports were attempted:**
|
||||
|
||||
1. **For each PR, check existing backport labels:**
|
||||
```bash
|
||||
gh pr view #1234 --json labels | jq -r '.labels[].name'
|
||||
```
|
||||
|
||||
2. **If no backport labels exist, add them now:**
|
||||
```bash
|
||||
# Add backport labels (this triggers automated backports)
|
||||
gh pr edit #1234 --add-label "needs-backport"
|
||||
gh pr edit #1234 --add-label "1.24" # Replace with target version
|
||||
```
|
||||
|
||||
3. **Check for existing backport PRs:**
|
||||
```bash
|
||||
# Check for backport PRs created by automation
|
||||
PR_NUMBER=${ARGUMENTS%%,*} # Extract first PR number from arguments
|
||||
PR_NUMBER=${PR_NUMBER#\#} # Remove # prefix
|
||||
gh pr list --search "backport-${PR_NUMBER}-to" --json number,title,state,baseRefName
|
||||
```
|
||||
|
||||
4. **Handle existing backport scenarios:**
|
||||
|
||||
**Scenario A: Automated backports already merged**
|
||||
```bash
|
||||
# Check if backport PRs were merged to core branches
|
||||
gh pr list --search "backport-${PR_NUMBER}-to" --state merged
|
||||
```
|
||||
- If backport PRs are merged → Skip to Step 10 (Version Bump)
|
||||
- **CONFIRMATION**: Automated backports completed, proceeding to version bump?
|
||||
|
||||
**Scenario B: Automated backport PRs exist but not merged**
|
||||
```bash
|
||||
# Show open backport PRs that need merging
|
||||
gh pr list --search "backport-${PR_NUMBER}-to" --state open
|
||||
```
|
||||
- **ACTION REQUIRED**: Merge the existing backport PRs first
|
||||
- Use: `gh pr merge [PR_NUMBER] --merge` for each backport PR
|
||||
- After merging, return to this command and skip to Step 10 (Version Bump)
|
||||
- **CONFIRMATION**: Have you merged all backport PRs? Ready to proceed to version bump?
|
||||
|
||||
**Scenario C: No automated backports or they failed**
|
||||
- Continue to Step 2 for manual cherry-picking
|
||||
- **CONFIRMATION**: Proceeding with manual cherry-picking because automation failed?
|
||||
|
||||
### Step 2: Identify Target Core Branch
|
||||
|
||||
1. Fetch the current ComfyUI requirements.txt from master branch:
|
||||
```bash
|
||||
@@ -36,7 +91,7 @@ Before starting, ensure:
|
||||
5. Verify the core branch exists: `git ls-remote origin refs/heads/core/*`
|
||||
6. **CONFIRMATION REQUIRED**: Is `core/X.Y` the correct target branch?
|
||||
|
||||
### Step 2: Parse and Validate Arguments
|
||||
### Step 3: Parse and Validate Arguments
|
||||
|
||||
1. Parse the comma-separated list of commits/PRs
|
||||
2. For each item:
|
||||
@@ -49,7 +104,7 @@ Before starting, ensure:
|
||||
- **CONFIRMATION REQUIRED**: Use merge commit or cherry-pick individual commits?
|
||||
4. Validate all commit hashes exist in the repository
|
||||
|
||||
### Step 3: Analyze Target Changes
|
||||
### Step 4: Analyze Target Changes
|
||||
|
||||
1. For each commit/PR to cherry-pick:
|
||||
- Display commit hash, author, date
|
||||
@@ -60,7 +115,7 @@ Before starting, ensure:
|
||||
2. Identify potential conflicts by checking changed files
|
||||
3. **CONFIRMATION REQUIRED**: Proceed with these commits?
|
||||
|
||||
### Step 4: Create Hotfix Branch
|
||||
### Step 5: Create Hotfix Branch
|
||||
|
||||
1. Checkout the core branch (e.g., `core/1.23`)
|
||||
2. Pull latest changes: `git pull origin core/X.Y`
|
||||
@@ -69,7 +124,7 @@ Before starting, ensure:
|
||||
- Example: `hotfix/1.23.4-20241120`
|
||||
5. **CONFIRMATION REQUIRED**: Created branch correctly?
|
||||
|
||||
### Step 5: Cherry-pick Changes
|
||||
### Step 6: Cherry-pick Changes
|
||||
|
||||
For each commit:
|
||||
1. Attempt cherry-pick: `git cherry-pick <commit>`
|
||||
@@ -83,7 +138,7 @@ For each commit:
|
||||
- Run validation: `pnpm typecheck && pnpm lint`
|
||||
4. **CONFIRMATION REQUIRED**: Cherry-pick successful and valid?
|
||||
|
||||
### Step 6: Create PR to Core Branch
|
||||
### Step 7: Create PR to Core Branch
|
||||
|
||||
1. Push the hotfix branch: `git push origin hotfix/<version>-<timestamp>`
|
||||
2. Create PR using gh CLI:
|
||||
@@ -100,7 +155,7 @@ For each commit:
|
||||
- Impact assessment
|
||||
5. **CONFIRMATION REQUIRED**: PR created correctly?
|
||||
|
||||
### Step 7: Wait for Tests
|
||||
### Step 8: Wait for Tests
|
||||
|
||||
1. Monitor PR checks: `gh pr checks`
|
||||
2. Display test results as they complete
|
||||
@@ -111,7 +166,7 @@ For each commit:
|
||||
4. Wait for all required checks to pass
|
||||
5. **CONFIRMATION REQUIRED**: All tests passing?
|
||||
|
||||
### Step 8: Merge Hotfix PR
|
||||
### Step 9: Merge Hotfix PR
|
||||
|
||||
1. Verify all checks have passed
|
||||
2. Check for required approvals
|
||||
@@ -119,7 +174,7 @@ For each commit:
|
||||
4. Delete the hotfix branch
|
||||
5. **CONFIRMATION REQUIRED**: PR merged successfully?
|
||||
|
||||
### Step 9: Create Version Bump
|
||||
### Step 10: Create Version Bump
|
||||
|
||||
1. Checkout the core branch: `git checkout core/X.Y`
|
||||
2. Pull latest changes: `git pull origin core/X.Y`
|
||||
@@ -131,7 +186,7 @@ For each commit:
|
||||
7. Commit: `git commit -m "[release] Bump version to 1.23.5"`
|
||||
8. **CONFIRMATION REQUIRED**: Version bump correct?
|
||||
|
||||
### Step 10: Create Release PR
|
||||
### Step 11: Create Release PR
|
||||
|
||||
1. Push release branch: `git push origin release/1.23.5`
|
||||
2. Create PR with Release label:
|
||||
@@ -184,7 +239,7 @@ For each commit:
|
||||
```
|
||||
5. **CONFIRMATION REQUIRED**: Release PR has "Release" label?
|
||||
|
||||
### Step 11: Monitor Release Process
|
||||
### Step 12: Monitor Release Process
|
||||
|
||||
1. Wait for PR checks to pass
|
||||
2. **FINAL CONFIRMATION**: Ready to trigger release by merging?
|
||||
@@ -199,7 +254,102 @@ For each commit:
|
||||
- PyPI upload
|
||||
- pnpm types publication
|
||||
|
||||
### Step 12: Post-Release Verification
|
||||
### Step 13: Manually Publish Draft Release
|
||||
|
||||
**CRITICAL**: The release workflow creates a DRAFT release. You must manually publish it:
|
||||
|
||||
1. **Go to GitHub Releases:** https://github.com/Comfy-Org/ComfyUI_frontend/releases
|
||||
2. **Find the DRAFT release** (e.g., "v1.23.5 Draft")
|
||||
3. **Click "Edit release"**
|
||||
4. **UNCHECK "Set as the latest release"** ⚠️ **CRITICAL**
|
||||
- This prevents the hotfix from showing as "latest"
|
||||
- Main branch should always be "latest release"
|
||||
5. **Click "Publish release"**
|
||||
6. **CONFIRMATION REQUIRED**: Draft release published with "latest" unchecked?
|
||||
|
||||
### Step 14: Create ComfyUI Requirements.txt Update PR
|
||||
|
||||
**IMPORTANT**: Create PR to update ComfyUI's requirements.txt via fork:
|
||||
|
||||
1. **Setup fork (if needed):**
|
||||
```bash
|
||||
# Check if fork already exists
|
||||
if gh repo view ComfyUI --json owner | jq -r '.owner.login' | grep -q "$(gh api user --jq .login)"; then
|
||||
echo "Fork already exists"
|
||||
else
|
||||
# Fork the ComfyUI repository
|
||||
gh repo fork comfyanonymous/ComfyUI --clone=false
|
||||
echo "Created fork of ComfyUI"
|
||||
fi
|
||||
```
|
||||
|
||||
2. **Clone fork and create branch:**
|
||||
```bash
|
||||
# Clone your fork (or use existing clone)
|
||||
GITHUB_USER=$(gh api user --jq .login)
|
||||
if [ ! -d "ComfyUI-fork" ]; then
|
||||
gh repo clone ${GITHUB_USER}/ComfyUI ComfyUI-fork
|
||||
fi
|
||||
|
||||
cd ComfyUI-fork
|
||||
git checkout master
|
||||
git pull origin master
|
||||
|
||||
# Create update branch
|
||||
BRANCH_NAME="update-frontend-${NEW_VERSION}"
|
||||
git checkout -b ${BRANCH_NAME}
|
||||
```
|
||||
|
||||
3. **Update requirements.txt:**
|
||||
```bash
|
||||
# Update the version in requirements.txt
|
||||
sed -i "s/comfyui-frontend-package==[0-9].*$/comfyui-frontend-package==${NEW_VERSION}/" requirements.txt
|
||||
|
||||
# Verify the change
|
||||
grep "comfyui-frontend-package" requirements.txt
|
||||
|
||||
# Commit the change
|
||||
git add requirements.txt
|
||||
git commit -m "Bump frontend to ${NEW_VERSION}"
|
||||
git push origin ${BRANCH_NAME}
|
||||
```
|
||||
|
||||
4. **Create PR from fork:**
|
||||
```bash
|
||||
# Create PR using gh CLI from fork
|
||||
gh pr create \
|
||||
--repo comfyanonymous/ComfyUI \
|
||||
--title "Bump frontend to ${NEW_VERSION}" \
|
||||
--body "$(cat <<EOF
|
||||
Bump frontend to ${NEW_VERSION}
|
||||
|
||||
\`\`\`
|
||||
python main.py --front-end-version Comfy-Org/ComfyUI_frontend@${NEW_VERSION}
|
||||
\`\`\`
|
||||
|
||||
- Diff: [Comfy-Org/ComfyUI_frontend: v${OLD_VERSION}...v${NEW_VERSION}](https://github.com/Comfy-Org/ComfyUI_frontend/compare/v${OLD_VERSION}...v${NEW_VERSION})
|
||||
- PyPI Package: https://pypi.org/project/comfyui-frontend-package/${NEW_VERSION}/
|
||||
- npm Types: https://www.npmjs.com/package/@comfyorg/comfyui-frontend-types/v/${NEW_VERSION}
|
||||
|
||||
## Changes
|
||||
|
||||
- Fix: [Brief description of hotfixes included]
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
5. **Clean up:**
|
||||
```bash
|
||||
# Return to original directory
|
||||
cd ..
|
||||
|
||||
# Keep fork directory for future updates
|
||||
echo "Fork directory 'ComfyUI-fork' kept for future use"
|
||||
```
|
||||
|
||||
6. **CONFIRMATION REQUIRED**: ComfyUI requirements.txt PR created from fork?
|
||||
|
||||
### Step 15: Post-Release Verification
|
||||
|
||||
1. Verify GitHub release:
|
||||
```bash
|
||||
@@ -213,12 +363,14 @@ For each commit:
|
||||
```bash
|
||||
pnpm view @comfyorg/comfyui-frontend-types@1.23.5
|
||||
```
|
||||
4. Generate release summary with:
|
||||
4. Monitor ComfyUI requirements.txt PR for approval/merge
|
||||
5. Generate release summary with:
|
||||
- Version released
|
||||
- Commits included
|
||||
- Issues fixed
|
||||
- Distribution status
|
||||
5. **CONFIRMATION REQUIRED**: Release completed successfully?
|
||||
- ComfyUI integration status
|
||||
6. **CONFIRMATION REQUIRED**: Hotfix release fully completed?
|
||||
|
||||
## Safety Checks
|
||||
|
||||
@@ -240,19 +392,28 @@ If something goes wrong:
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **Always try automated backports first** - This command is for when automation fails
|
||||
- Core branch version will be behind main - this is expected
|
||||
- The "Release" label triggers the PyPI/npm publication
|
||||
- **CRITICAL**: Always uncheck "Set as latest release" for hotfix releases
|
||||
- **Must create ComfyUI requirements.txt PR** - Hotfix isn't complete without it
|
||||
- PR numbers must include the `#` prefix
|
||||
- Mixed commits/PRs are supported but review carefully
|
||||
- Always wait for full test suite before proceeding
|
||||
|
||||
## Expected Timeline
|
||||
## Modern Workflow Context
|
||||
|
||||
- Step 1-3: ~10 minutes (analysis)
|
||||
- Steps 4-6: ~15-30 minutes (cherry-picking)
|
||||
- Step 7: ~10-20 minutes (tests)
|
||||
- Steps 8-10: ~10 minutes (version bump)
|
||||
- Step 11-12: ~15-20 minutes (release)
|
||||
- Total: ~60-90 minutes
|
||||
**Primary Backport Method:** Automated via `needs-backport` + `X.YY` labels
|
||||
**This Command Usage:**
|
||||
- Smart path detection - skip to version bump if backports already merged
|
||||
- Fallback to manual cherry-picking only when automation fails/has conflicts
|
||||
**Complete Hotfix:** Includes GitHub release publishing + ComfyUI requirements.txt integration
|
||||
|
||||
This process ensures a safe, verified hotfix release with multiple confirmation points and clear tracking of what changes are being released.
|
||||
## Workflow Paths
|
||||
|
||||
- **Path A:** Backports already merged → Skip to Step 10 (Version Bump)
|
||||
- **Path B:** Backport PRs need merging → Merge them → Skip to Step 10 (Version Bump)
|
||||
- **Path C:** No/failed backports → Manual cherry-picking (Steps 2-9) → Version Bump (Step 10)
|
||||
|
||||
|
||||
This process ensures a complete hotfix release with proper GitHub publishing, ComfyUI integration, and multiple safety checkpoints.
|
||||
@@ -4,3 +4,24 @@
|
||||
|
||||
# npm run format on litegraph merge (10,672 insertions, 7,327 deletions across 129 files)
|
||||
c53f197de2a3e0fa66b16dedc65c131235c1c4b6
|
||||
|
||||
# Reorganize renderer components into domain-driven folder structure
|
||||
c8a83a9caede7bdb5f8598c5492b07d08c339d49
|
||||
|
||||
# Domain-driven design (DDD) refactors - September 2025
|
||||
# These commits reorganized the codebase into domain-driven architecture
|
||||
|
||||
# [refactor] Improve renderer domain organization (#5552)
|
||||
6349ceee6c0a57fc7992e85635def9b6e22eaeb2
|
||||
|
||||
# [refactor] Improve settings domain organization (#5550)
|
||||
4c8c4a1ad4f53354f700a33ea1b95262aeda2719
|
||||
|
||||
# [refactor] Improve workflow domain organization (#5584)
|
||||
ca312fd1eab540cc4ddc0e3d244d38b3858574f0
|
||||
|
||||
# [refactor] Move thumbnail functionality to renderer/core domain (#5586)
|
||||
e3bb29ceb8174b8bbca9e48ec7d42cd540f40efa
|
||||
|
||||
# [refactor] Improve updates/notifications domain organization (#5590)
|
||||
27ab355f9c73415dc39f4d3f512b02308f847801
|
||||
|
||||
2
.gitattributes
vendored
@@ -13,4 +13,4 @@
|
||||
|
||||
# Generated files
|
||||
src/types/comfyRegistryTypes.ts linguist-generated=true
|
||||
src/types/generatedManagerTypes.ts linguist-generated=true
|
||||
src/workbench/extensions/manager/types/generatedManagerTypes.ts linguist-generated=true
|
||||
|
||||
135
.github/workflows/backport.yaml
vendored
@@ -2,12 +2,27 @@ name: Auto Backport
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [closed]
|
||||
types: [closed, labeled]
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
pr_number:
|
||||
description: 'PR number to backport'
|
||||
required: true
|
||||
type: string
|
||||
force_rerun:
|
||||
description: 'Force rerun even if backports exist'
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
backport:
|
||||
if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'needs-backport')
|
||||
if: >
|
||||
(github.event_name == 'pull_request_target' &&
|
||||
github.event.pull_request.merged == true &&
|
||||
contains(github.event.pull_request.labels.*.name, 'needs-backport')) ||
|
||||
github.event_name == 'workflow_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -15,6 +30,35 @@ jobs:
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- name: Validate inputs for manual triggers
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
# Validate PR number format
|
||||
if ! [[ "${{ inputs.pr_number }}" =~ ^[0-9]+$ ]]; then
|
||||
echo "::error::Invalid PR number format. Must be a positive integer."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate PR exists and is merged
|
||||
if ! gh pr view "${{ inputs.pr_number }}" --json merged >/dev/null 2>&1; then
|
||||
echo "::error::PR #${{ inputs.pr_number }} not found or inaccessible."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MERGED=$(gh pr view "${{ inputs.pr_number }}" --json merged --jq '.merged')
|
||||
if [ "$MERGED" != "true" ]; then
|
||||
echo "::error::PR #${{ inputs.pr_number }} is not merged. Only merged PRs can be backported."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate PR has needs-backport label
|
||||
if ! gh pr view "${{ inputs.pr_number }}" --json labels --jq '.labels[].name' | grep -q "needs-backport"; then
|
||||
echo "::error::PR #${{ inputs.pr_number }} does not have 'needs-backport' label."
|
||||
exit 1
|
||||
fi
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -25,13 +69,49 @@ jobs:
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Check if backports already exist
|
||||
id: check-existing
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
|
||||
run: |
|
||||
# Check for existing backport PRs for this PR number
|
||||
EXISTING_BACKPORTS=$(gh pr list --state all --search "backport-${PR_NUMBER}-to" --json title,headRefName,baseRefName | jq -r '.[].headRefName')
|
||||
|
||||
if [ -z "$EXISTING_BACKPORTS" ]; then
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# For manual triggers with force_rerun, proceed anyway
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.force_rerun }}" = "true" ]; then
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
echo "::warning::Force rerun requested - existing backports will be updated"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Found existing backport PRs:"
|
||||
echo "$EXISTING_BACKPORTS"
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
echo "::warning::Backport PRs already exist for PR #${PR_NUMBER}, skipping to avoid duplicates"
|
||||
|
||||
- name: Extract version labels
|
||||
if: steps.check-existing.outputs.skip != 'true'
|
||||
id: versions
|
||||
run: |
|
||||
# Extract version labels (e.g., "1.24", "1.22")
|
||||
VERSIONS=""
|
||||
LABELS='${{ toJSON(github.event.pull_request.labels) }}'
|
||||
for label in $(echo "$LABELS" | jq -r '.[].name'); do
|
||||
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
# For manual triggers, get labels from the PR
|
||||
LABELS=$(gh pr view ${{ inputs.pr_number }} --json labels | jq -r '.labels[].name')
|
||||
else
|
||||
# For automatic triggers, extract from PR event
|
||||
LABELS='${{ toJSON(github.event.pull_request.labels) }}'
|
||||
LABELS=$(echo "$LABELS" | jq -r '.[].name')
|
||||
fi
|
||||
|
||||
for label in $LABELS; do
|
||||
# Match version labels like "1.24" (major.minor only)
|
||||
if [[ "$label" =~ ^[0-9]+\.[0-9]+$ ]]; then
|
||||
# Validate the branch exists before adding to list
|
||||
@@ -52,14 +132,23 @@ jobs:
|
||||
echo "Found version labels: ${VERSIONS}"
|
||||
|
||||
- name: Backport commits
|
||||
if: steps.check-existing.outputs.skip != 'true'
|
||||
id: backport
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
MERGE_COMMIT: ${{ github.event.pull_request.merge_commit_sha }}
|
||||
PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
|
||||
run: |
|
||||
FAILED=""
|
||||
SUCCESS=""
|
||||
|
||||
# Get PR data for manual triggers
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json title,mergeCommit)
|
||||
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
|
||||
MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid')
|
||||
else
|
||||
PR_TITLE="${{ github.event.pull_request.title }}"
|
||||
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
|
||||
fi
|
||||
|
||||
for version in ${{ steps.versions.outputs.versions }}; do
|
||||
echo "::group::Backporting to core/${version}"
|
||||
@@ -109,14 +198,21 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Create PR for each successful backport
|
||||
if: steps.backport.outputs.success
|
||||
if: steps.check-existing.outputs.skip != 'true' && steps.backport.outputs.success
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
|
||||
run: |
|
||||
PR_TITLE="${{ github.event.pull_request.title }}"
|
||||
PR_NUMBER="${{ github.event.pull_request.number }}"
|
||||
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
|
||||
|
||||
# Get PR data for manual triggers
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json title,author)
|
||||
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
|
||||
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
|
||||
else
|
||||
PR_TITLE="${{ github.event.pull_request.title }}"
|
||||
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
|
||||
fi
|
||||
|
||||
for backport in ${{ steps.backport.outputs.success }}; do
|
||||
IFS=':' read -r version branch <<< "${backport}"
|
||||
|
||||
@@ -141,13 +237,20 @@ jobs:
|
||||
done
|
||||
|
||||
- name: Comment on failures
|
||||
if: failure() && steps.backport.outputs.failed
|
||||
if: steps.check-existing.outputs.skip != 'true' && failure() && steps.backport.outputs.failed
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
PR_NUMBER="${{ github.event.pull_request.number }}"
|
||||
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
|
||||
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json author,mergeCommit)
|
||||
PR_NUMBER="${{ inputs.pr_number }}"
|
||||
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
|
||||
MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid')
|
||||
else
|
||||
PR_NUMBER="${{ github.event.pull_request.number }}"
|
||||
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
|
||||
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
|
||||
fi
|
||||
|
||||
for failure in ${{ steps.backport.outputs.failed }}; do
|
||||
IFS=':' read -r version reason conflicts <<< "${failure}"
|
||||
|
||||
9
.github/workflows/claude-pr-review.yml
vendored
@@ -47,6 +47,7 @@ jobs:
|
||||
needs: wait-for-ci
|
||||
if: needs.wait-for-ci.outputs.should-proceed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
@@ -69,19 +70,17 @@ jobs:
|
||||
pnpm install -g typescript @vue/compiler-sfc
|
||||
|
||||
- name: Run Claude PR Review
|
||||
uses: anthropics/claude-code-action@main
|
||||
uses: anthropics/claude-code-action@v1.0.6
|
||||
with:
|
||||
label_trigger: "claude-review"
|
||||
direct_prompt: |
|
||||
prompt: |
|
||||
Read the file .claude/commands/comprehensive-pr-review.md and follow ALL the instructions exactly.
|
||||
|
||||
CRITICAL: You must post individual inline comments using the gh api commands shown in the file.
|
||||
DO NOT create a summary comment.
|
||||
Each issue must be posted as a separate inline comment on the specific line of code.
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
max_turns: 256
|
||||
timeout_minutes: 30
|
||||
allowed_tools: "Bash(git:*),Bash(gh api:*),Bash(gh pr:*),Bash(gh repo:*),Bash(jq:*),Bash(echo:*),Read,Write,Edit,Glob,Grep,WebFetch"
|
||||
claude_args: "--max-turns 256 --allowedTools 'Bash(git:*),Bash(gh api:*),Bash(gh pr:*),Bash(gh repo:*),Bash(jq:*),Bash(echo:*),Read,Write,Edit,Glob,Grep,WebFetch'"
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -128,45 +128,6 @@ jobs:
|
||||
echo "- Critical security patches"
|
||||
echo "- Documentation updates"
|
||||
|
||||
- name: Create branch protection rules
|
||||
if: steps.check_version.outputs.is_minor_bump == 'true' && env.branch_exists != 'true'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.PR_GH_TOKEN || secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
BRANCH_NAME="${{ steps.check_version.outputs.branch_name }}"
|
||||
|
||||
# Create branch protection using GitHub API
|
||||
echo "Setting up branch protection for $BRANCH_NAME..."
|
||||
|
||||
RESPONSE=$(curl -s -w "\n%{http_code}" -X PUT \
|
||||
-H "Authorization: token $GITHUB_TOKEN" \
|
||||
-H "Accept: application/vnd.github.v3+json" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/branches/$BRANCH_NAME/protection" \
|
||||
-d '{
|
||||
"required_status_checks": {
|
||||
"strict": true,
|
||||
"contexts": ["lint-and-format", "test", "playwright-tests"]
|
||||
},
|
||||
"enforce_admins": false,
|
||||
"required_pull_request_reviews": {
|
||||
"required_approving_review_count": 1,
|
||||
"dismiss_stale_reviews": true
|
||||
},
|
||||
"restrictions": null,
|
||||
"allow_force_pushes": false,
|
||||
"allow_deletions": false
|
||||
}')
|
||||
|
||||
HTTP_CODE=$(echo "$RESPONSE" | tail -n 1)
|
||||
BODY=$(echo "$RESPONSE" | sed '$d')
|
||||
|
||||
if [[ "$HTTP_CODE" -eq 200 ]] || [[ "$HTTP_CODE" -eq 201 ]]; then
|
||||
echo "✅ Branch protection successfully applied"
|
||||
else
|
||||
echo "⚠️ Failed to apply branch protection (HTTP $HTTP_CODE)"
|
||||
echo "Response: $BODY"
|
||||
# Don't fail the workflow, just warn
|
||||
fi
|
||||
|
||||
- name: Post summary
|
||||
if: steps.check_version.outputs.is_minor_bump == 'true'
|
||||
|
||||
15
.github/workflows/i18n-custom-nodes.yaml
vendored
@@ -32,11 +32,10 @@ jobs:
|
||||
with:
|
||||
repository: Comfy-Org/ComfyUI_frontend
|
||||
path: ComfyUI_frontend
|
||||
- name: Checkout ComfyUI_devtools
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: Comfy-Org/ComfyUI_devtools
|
||||
path: ComfyUI/custom_nodes/ComfyUI_devtools
|
||||
- name: Copy ComfyUI_devtools from frontend repo
|
||||
run: |
|
||||
mkdir -p ComfyUI/custom_nodes/ComfyUI_devtools
|
||||
cp -r ComfyUI_frontend/tools/devtools/* ComfyUI/custom_nodes/ComfyUI_devtools/
|
||||
- name: Checkout custom node repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -79,7 +78,7 @@ jobs:
|
||||
wait-for-it --service 127.0.0.1:8188 -t 600
|
||||
working-directory: ComfyUI
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install chromium --with-deps
|
||||
run: pnpm exec 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
|
||||
@@ -87,7 +86,7 @@ jobs:
|
||||
run: pnpm dev:electron &
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Capture base i18n
|
||||
run: npx tsx scripts/diff-i18n capture
|
||||
run: pnpm exec tsx scripts/diff-i18n capture
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Update en.json
|
||||
run: pnpm collect-i18n
|
||||
@@ -100,7 +99,7 @@ jobs:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Diff base vs updated i18n
|
||||
run: npx tsx scripts/diff-i18n diff
|
||||
run: pnpm exec tsx scripts/diff-i18n diff
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Update i18n in custom node repository
|
||||
run: |
|
||||
|
||||
2
.github/workflows/i18n-node-defs.yaml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
steps:
|
||||
- uses: Comfy-Org/ComfyUI_frontend_setup_action@v3
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install chromium --with-deps
|
||||
run: pnpm exec 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
|
||||
|
||||
2
.github/workflows/i18n.yaml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
restore-keys: |
|
||||
playwright-browsers-${{ runner.os }}-
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install chromium --with-deps
|
||||
run: pnpm exec 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
|
||||
|
||||
300
.github/workflows/pr-playwright-deploy.yaml
vendored
@@ -1,4 +1,4 @@
|
||||
name: PR Playwright Deploy and Comment
|
||||
name: PR Playwright Deploy (Forks)
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
@@ -9,272 +9,84 @@ env:
|
||||
DATE_FORMAT: '+%m/%d/%Y, %I:%M:%S %p'
|
||||
|
||||
jobs:
|
||||
deploy-reports:
|
||||
deploy-and-comment-forked-pr:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'Comfy-Org/ComfyUI_frontend' && github.event.workflow_run.event == 'pull_request' && github.event.action == 'completed'
|
||||
if: |
|
||||
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
|
||||
github.event.workflow_run.event == 'pull_request' &&
|
||||
github.event.workflow_run.head_repository != null &&
|
||||
github.event.workflow_run.repository != null &&
|
||||
github.event.workflow_run.head_repository.full_name != github.event.workflow_run.repository.full_name
|
||||
permissions:
|
||||
pull-requests: write
|
||||
actions: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
browser: [chromium, chromium-2x, chromium-0.5x, mobile-chrome]
|
||||
steps:
|
||||
- name: Get PR info
|
||||
id: pr-info
|
||||
- name: Log workflow trigger info
|
||||
run: |
|
||||
echo "Repository: ${{ github.repository }}"
|
||||
echo "Event: ${{ github.event.workflow_run.event }}"
|
||||
echo "Head repo: ${{ github.event.workflow_run.head_repository.full_name || 'null' }}"
|
||||
echo "Base repo: ${{ github.event.workflow_run.repository.full_name || 'null' }}"
|
||||
echo "Is forked: ${{ github.event.workflow_run.head_repository.full_name != github.event.workflow_run.repository.full_name }}"
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get PR Number
|
||||
id: pr
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { data: pullRequests } = await github.rest.pulls.list({
|
||||
const { data: prs } = await github.rest.pulls.list({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
head: `${context.repo.owner}:${context.payload.workflow_run.head_branch}`,
|
||||
});
|
||||
|
||||
if (pullRequests.length === 0) {
|
||||
console.log('No open PR found for this branch');
|
||||
return { number: null, sanitized_branch: null };
|
||||
const pr = prs.find(p => p.head.sha === context.payload.workflow_run.head_sha);
|
||||
|
||||
if (!pr) {
|
||||
console.log('No PR found for SHA:', context.payload.workflow_run.head_sha);
|
||||
return null;
|
||||
}
|
||||
|
||||
const pr = pullRequests[0];
|
||||
const branchName = context.payload.workflow_run.head_branch;
|
||||
const sanitizedBranch = branchName.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/--+/g, '-').replace(/^-|-$/g, '');
|
||||
|
||||
return {
|
||||
number: pr.number,
|
||||
sanitized_branch: sanitizedBranch
|
||||
};
|
||||
console.log(`Found PR #${pr.number} from fork: ${context.payload.workflow_run.head_repository.full_name}`);
|
||||
return pr.number;
|
||||
|
||||
- name: Set project name
|
||||
if: fromJSON(steps.pr-info.outputs.result).number != null
|
||||
id: project-name
|
||||
- name: Handle Test Start
|
||||
if: steps.pr.outputs.result != 'null' && github.event.action == 'requested'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
if [ "${{ matrix.browser }}" = "chromium-0.5x" ]; then
|
||||
echo "name=comfyui-playwright-chromium-0-5x" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "name=comfyui-playwright-${{ matrix.browser }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
echo "branch=${{ fromJSON(steps.pr-info.outputs.result).sanitized_branch }}" >> $GITHUB_OUTPUT
|
||||
chmod +x scripts/cicd/pr-playwright-deploy-and-comment.sh
|
||||
./scripts/cicd/pr-playwright-deploy-and-comment.sh \
|
||||
"${{ steps.pr.outputs.result }}" \
|
||||
"${{ github.event.workflow_run.head_branch }}" \
|
||||
"starting" \
|
||||
"$(date -u '${{ env.DATE_FORMAT }}')"
|
||||
|
||||
- name: Download playwright report
|
||||
if: fromJSON(steps.pr-info.outputs.result).number != null
|
||||
- name: Download and Deploy Reports
|
||||
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
name: playwright-report-${{ matrix.browser }}
|
||||
path: playwright-report
|
||||
|
||||
- name: Install Wrangler
|
||||
if: fromJSON(steps.pr-info.outputs.result).number != null
|
||||
run: npm install -g wrangler
|
||||
|
||||
- name: Deploy to Cloudflare Pages (${{ matrix.browser }})
|
||||
if: fromJSON(steps.pr-info.outputs.result).number != null
|
||||
id: cloudflare-deploy
|
||||
continue-on-error: true
|
||||
run: |
|
||||
# Retry logic for wrangler deploy (3 attempts)
|
||||
RETRY_COUNT=0
|
||||
MAX_RETRIES=3
|
||||
SUCCESS=false
|
||||
pattern: playwright-report-*
|
||||
path: reports
|
||||
|
||||
while [ $RETRY_COUNT -lt $MAX_RETRIES ] && [ $SUCCESS = false ]; do
|
||||
RETRY_COUNT=$((RETRY_COUNT + 1))
|
||||
echo "Deployment attempt $RETRY_COUNT of $MAX_RETRIES..."
|
||||
|
||||
if npx wrangler pages deploy playwright-report --project-name=${{ steps.project-name.outputs.name }} --branch=${{ steps.project-name.outputs.branch }}; then
|
||||
SUCCESS=true
|
||||
echo "Deployment successful on attempt $RETRY_COUNT"
|
||||
else
|
||||
echo "Deployment failed on attempt $RETRY_COUNT"
|
||||
if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then
|
||||
echo "Retrying in 10 seconds..."
|
||||
sleep 10
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ $SUCCESS = false ]; then
|
||||
echo "All deployment attempts failed"
|
||||
exit 1
|
||||
fi
|
||||
- name: Handle Test Completion
|
||||
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
|
||||
comment-tests-starting:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'Comfy-Org/ComfyUI_frontend' && github.event.workflow_run.event == 'pull_request' && github.event.action == 'requested'
|
||||
permissions:
|
||||
pull-requests: write
|
||||
actions: read
|
||||
steps:
|
||||
- name: Get PR number
|
||||
id: pr
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { data: pullRequests } = await github.rest.pulls.list({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
head: `${context.repo.owner}:${context.payload.workflow_run.head_branch}`,
|
||||
});
|
||||
|
||||
if (pullRequests.length === 0) {
|
||||
console.log('No open PR found for this branch');
|
||||
return null;
|
||||
}
|
||||
|
||||
return pullRequests[0].number;
|
||||
|
||||
- name: Get completion time
|
||||
id: completion-time
|
||||
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Generate comment body for start
|
||||
if: steps.pr.outputs.result != 'null'
|
||||
id: comment-body-start
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
echo "<!-- PLAYWRIGHT_TEST_STATUS -->" > comment.md
|
||||
echo "## 🎭 Playwright Test Results" >> comment.md
|
||||
echo "" >> comment.md
|
||||
echo "<img alt='comfy-loading-gif' src='https://github.com/user-attachments/assets/755c86ee-e445-4ea8-bc2c-cca85df48686' width='14px' height='14px' style='vertical-align: middle; margin-right: 4px;' /> **Tests are starting...** " >> comment.md
|
||||
echo "" >> comment.md
|
||||
echo "⏰ Started at: ${{ steps.completion-time.outputs.time }} UTC" >> comment.md
|
||||
echo "" >> comment.md
|
||||
echo "### 🚀 Running Tests" >> comment.md
|
||||
echo "- 🧪 **chromium**: Running tests..." >> comment.md
|
||||
echo "- 🧪 **chromium-0.5x**: Running tests..." >> comment.md
|
||||
echo "- 🧪 **chromium-2x**: Running tests..." >> comment.md
|
||||
echo "- 🧪 **mobile-chrome**: Running tests..." >> comment.md
|
||||
echo "" >> comment.md
|
||||
echo "---" >> comment.md
|
||||
echo "⏱️ Please wait while tests are running across all browsers..." >> comment.md
|
||||
|
||||
- name: Comment PR - Tests Started
|
||||
if: steps.pr.outputs.result != 'null'
|
||||
uses: edumserrano/find-create-or-update-comment@82880b65c8a3a6e4c70aa05a204995b6c9696f53 # v3.0.0
|
||||
with:
|
||||
issue-number: ${{ steps.pr.outputs.result }}
|
||||
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
|
||||
comment-author: 'github-actions[bot]'
|
||||
edit-mode: replace
|
||||
body-path: comment.md
|
||||
|
||||
comment-tests-completed:
|
||||
runs-on: ubuntu-latest
|
||||
needs: deploy-reports
|
||||
if: github.repository == 'Comfy-Org/ComfyUI_frontend' && github.event.workflow_run.event == 'pull_request' && github.event.action == 'completed' && always()
|
||||
permissions:
|
||||
pull-requests: write
|
||||
actions: read
|
||||
steps:
|
||||
- name: Get PR number
|
||||
id: pr
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const { data: pullRequests } = await github.rest.pulls.list({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
head: `${context.repo.owner}:${context.payload.workflow_run.head_branch}`,
|
||||
});
|
||||
|
||||
if (pullRequests.length === 0) {
|
||||
console.log('No open PR found for this branch');
|
||||
return null;
|
||||
}
|
||||
|
||||
return pullRequests[0].number;
|
||||
|
||||
- name: Download all deployment info
|
||||
if: steps.pr.outputs.result != 'null'
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
pattern: deployment-info-*
|
||||
merge-multiple: true
|
||||
path: deployment-info
|
||||
|
||||
- name: Get completion time
|
||||
id: completion-time
|
||||
run: echo "time=$(date -u '${{ env.DATE_FORMAT }}')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Generate comment body for completion
|
||||
if: steps.pr.outputs.result != 'null'
|
||||
id: comment-body-completed
|
||||
run: |
|
||||
echo "<!-- PLAYWRIGHT_TEST_STATUS -->" > comment.md
|
||||
echo "## 🎭 Playwright Test Results" >> comment.md
|
||||
echo "" >> comment.md
|
||||
|
||||
# Check if all tests passed
|
||||
ALL_PASSED=true
|
||||
for file in deployment-info/*.txt; do
|
||||
if [ -f "$file" ]; then
|
||||
browser=$(basename "$file" .txt)
|
||||
info=$(cat "$file")
|
||||
exit_code=$(echo "$info" | cut -d'|' -f2)
|
||||
if [ "$exit_code" != "0" ]; then
|
||||
ALL_PASSED=false
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$ALL_PASSED" = "true" ]; then
|
||||
echo "✅ **All tests passed across all browsers!**" >> comment.md
|
||||
else
|
||||
echo "❌ **Some tests failed!**" >> comment.md
|
||||
fi
|
||||
|
||||
echo "" >> comment.md
|
||||
echo "⏰ Completed at: ${{ steps.completion-time.outputs.time }} UTC" >> comment.md
|
||||
echo "" >> comment.md
|
||||
echo "### 📊 Test Reports by Browser" >> comment.md
|
||||
|
||||
for file in deployment-info/*.txt; do
|
||||
if [ -f "$file" ]; then
|
||||
browser=$(basename "$file" .txt)
|
||||
info=$(cat "$file")
|
||||
exit_code=$(echo "$info" | cut -d'|' -f2)
|
||||
url=$(echo "$info" | cut -d'|' -f3)
|
||||
|
||||
# Validate URLs before using them in comments
|
||||
sanitized_url=$(echo "$url" | grep -E '^https://[a-z0-9.-]+\.pages\.dev(/.*)?$' || echo "INVALID_URL")
|
||||
if [ "$sanitized_url" = "INVALID_URL" ]; then
|
||||
echo "Invalid deployment URL detected: $url"
|
||||
url="#" # Use safe fallback
|
||||
fi
|
||||
|
||||
if [ "$exit_code" = "0" ]; then
|
||||
status="✅"
|
||||
else
|
||||
status="❌"
|
||||
fi
|
||||
|
||||
echo "- $status **$browser**: [View Report]($url)" >> comment.md
|
||||
fi
|
||||
done
|
||||
|
||||
echo "" >> comment.md
|
||||
echo "---" >> comment.md
|
||||
if [ "$ALL_PASSED" = "true" ]; then
|
||||
echo "🎉 Your tests are passing across all browsers!" >> comment.md
|
||||
else
|
||||
echo "⚠️ Please check the test reports for details on failures." >> comment.md
|
||||
fi
|
||||
|
||||
- name: Comment PR - Tests Complete
|
||||
if: steps.pr.outputs.result != 'null'
|
||||
uses: edumserrano/find-create-or-update-comment@82880b65c8a3a6e4c70aa05a204995b6c9696f53 # v3.0.0
|
||||
with:
|
||||
issue-number: ${{ steps.pr.outputs.result }}
|
||||
body-includes: '<!-- PLAYWRIGHT_TEST_STATUS -->'
|
||||
comment-author: 'github-actions[bot]'
|
||||
edit-mode: replace
|
||||
body-path: comment.md
|
||||
# Rename merged report if exists
|
||||
[ -d "reports/playwright-report-chromium-merged" ] && \
|
||||
mv reports/playwright-report-chromium-merged reports/playwright-report-chromium
|
||||
|
||||
chmod +x scripts/cicd/pr-playwright-deploy-and-comment.sh
|
||||
./scripts/cicd/pr-playwright-deploy-and-comment.sh \
|
||||
"${{ steps.pr.outputs.result }}" \
|
||||
"${{ github.event.workflow_run.head_branch }}" \
|
||||
"completed"
|
||||
139
.github/workflows/publish-frontend-types.yaml
vendored
Normal file
@@ -0,0 +1,139 @@
|
||||
name: Publish Frontend Types
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Version to publish (e.g., 1.26.7)'
|
||||
required: true
|
||||
type: string
|
||||
dist_tag:
|
||||
description: 'npm dist-tag to use'
|
||||
required: true
|
||||
default: latest
|
||||
type: string
|
||||
ref:
|
||||
description: 'Git ref to checkout (commit SHA, tag, or branch)'
|
||||
required: false
|
||||
type: string
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
required: true
|
||||
type: string
|
||||
dist_tag:
|
||||
required: false
|
||||
type: string
|
||||
default: latest
|
||||
ref:
|
||||
required: false
|
||||
type: string
|
||||
|
||||
concurrency:
|
||||
group: publish-frontend-types-${{ github.workflow }}-${{ inputs.version }}-${{ inputs.dist_tag }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
publish_types_manual:
|
||||
name: Publish @comfyorg/comfyui-frontend-types
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Validate inputs
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VERSION="${{ inputs.version }}"
|
||||
SEMVER_REGEX='^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?$'
|
||||
if [[ ! "$VERSION" =~ $SEMVER_REGEX ]]; then
|
||||
echo "::error title=Invalid version::Version '$VERSION' must follow semantic versioning (x.y.z[-suffix][+build])" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Determine ref to checkout
|
||||
id: resolve_ref
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
REF="${{ inputs.ref }}"
|
||||
VERSION="${{ inputs.version }}"
|
||||
if [ -n "$REF" ]; then
|
||||
if ! git check-ref-format --allow-onelevel "$REF"; then
|
||||
echo "::error title=Invalid ref::Ref '$REF' fails git check-ref-format validation." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "ref=$REF" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "ref=refs/tags/v$VERSION" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ steps.resolve_ref.outputs.ref }}
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
cache: 'pnpm'
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
env:
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1'
|
||||
|
||||
- name: Build types
|
||||
run: pnpm build:types
|
||||
|
||||
- name: Verify version matches input
|
||||
id: verify
|
||||
shell: bash
|
||||
run: |
|
||||
PKG_VERSION=$(node -p "require('./package.json').version")
|
||||
TYPES_PKG_VERSION=$(node -p "require('./dist/package.json').version")
|
||||
if [ "$PKG_VERSION" != "${{ inputs.version }}" ]; then
|
||||
echo "Error: package.json version $PKG_VERSION does not match input ${{ inputs.version }}" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$TYPES_PKG_VERSION" != "${{ inputs.version }}" ]; then
|
||||
echo "Error: dist/package.json version $TYPES_PKG_VERSION does not match input ${{ inputs.version }}" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Check if version already on npm
|
||||
id: check_npm
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
NAME=$(node -p "require('./dist/package.json').name")
|
||||
VER="${{ steps.verify.outputs.version }}"
|
||||
STATUS=0
|
||||
OUTPUT=$(npm view "${NAME}@${VER}" --json 2>&1) || STATUS=$?
|
||||
if [ "$STATUS" -eq 0 ]; then
|
||||
echo "exists=true" >> "$GITHUB_OUTPUT"
|
||||
echo "::warning title=Already published::${NAME}@${VER} already exists on npm. Skipping publish."
|
||||
else
|
||||
if echo "$OUTPUT" | grep -q "E404"; then
|
||||
echo "exists=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "::error title=Registry lookup failed::$OUTPUT" >&2
|
||||
exit "$STATUS"
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Publish package
|
||||
if: steps.check_npm.outputs.exists == 'false'
|
||||
run: pnpm publish --access public --tag "${{ inputs.dist_tag }}" --no-git-checks
|
||||
working-directory: dist
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
42
.github/workflows/release.yaml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
is_prerelease: ${{ steps.check_prerelease.outputs.is_prerelease }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
- name: Download dist artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
@@ -98,7 +98,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
- name: Download dist artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
@@ -126,34 +126,8 @@ jobs:
|
||||
|
||||
publish_types:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
cache: 'pnpm'
|
||||
registry-url: https://registry.npmjs.org
|
||||
|
||||
- name: Cache tool outputs
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
.cache
|
||||
tsconfig.tsbuildinfo
|
||||
dist
|
||||
key: types-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
types-tools-cache-${{ runner.os }}-
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- run: pnpm build:types
|
||||
- name: Publish package
|
||||
run: pnpm publish --access public
|
||||
working-directory: dist
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
uses: ./.github/workflows/publish-frontend-types.yaml
|
||||
with:
|
||||
version: ${{ needs.build.outputs.version }}
|
||||
ref: ${{ github.event.pull_request.merge_commit_sha }}
|
||||
secrets: inherit
|
||||
|
||||
4
.github/workflows/test-browser-exp.yaml
vendored
@@ -19,11 +19,11 @@ jobs:
|
||||
restore-keys: |
|
||||
playwright-browsers-${{ runner.os }}-
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install chromium --with-deps
|
||||
run: pnpm exec playwright install chromium --with-deps
|
||||
working-directory: ComfyUI_frontend
|
||||
- name: Run Playwright tests and update snapshots
|
||||
id: playwright-tests
|
||||
run: npx playwright test --update-snapshots
|
||||
run: pnpm exec playwright test --update-snapshots
|
||||
continue-on-error: true
|
||||
working-directory: ComfyUI_frontend
|
||||
- uses: actions/upload-artifact@v4
|
||||
|
||||
89
.github/workflows/test-ui.yaml
vendored
@@ -27,12 +27,10 @@ jobs:
|
||||
repository: 'Comfy-Org/ComfyUI_frontend'
|
||||
path: 'ComfyUI_frontend'
|
||||
|
||||
- name: Checkout ComfyUI_devtools
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: 'Comfy-Org/ComfyUI_devtools'
|
||||
path: 'ComfyUI/custom_nodes/ComfyUI_devtools'
|
||||
ref: 'd05fd48dd787a4192e16802d4244cfcc0e2f9684'
|
||||
- name: Copy ComfyUI_devtools from frontend repo
|
||||
run: |
|
||||
mkdir -p ComfyUI/custom_nodes/ComfyUI_devtools
|
||||
cp -r ComfyUI_frontend/tools/devtools/* ComfyUI/custom_nodes/ComfyUI_devtools/
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
@@ -150,7 +148,7 @@ jobs:
|
||||
|
||||
- name: Run Playwright tests (Shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
|
||||
id: playwright
|
||||
run: npx playwright test --project=chromium --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --reporter=blob
|
||||
run: pnpm exec playwright test --project=chromium --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --reporter=blob
|
||||
working-directory: ComfyUI_frontend
|
||||
env:
|
||||
PLAYWRIGHT_BLOB_OUTPUT_DIR: ../blob-report
|
||||
@@ -229,7 +227,13 @@ jobs:
|
||||
|
||||
- name: Run Playwright tests (${{ matrix.browser }})
|
||||
id: playwright
|
||||
run: npx playwright test --project=${{ matrix.browser }} --reporter=html
|
||||
run: |
|
||||
# Run tests with both HTML and JSON reporters
|
||||
PLAYWRIGHT_JSON_OUTPUT_NAME=playwright-report/report.json \
|
||||
pnpm exec playwright test --project=${{ matrix.browser }} \
|
||||
--reporter=list \
|
||||
--reporter=html \
|
||||
--reporter=json
|
||||
working-directory: ComfyUI_frontend
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
@@ -275,7 +279,12 @@ jobs:
|
||||
merge-multiple: true
|
||||
|
||||
- name: Merge into HTML Report
|
||||
run: npx playwright merge-reports --reporter html ./all-blob-reports
|
||||
run: |
|
||||
# Generate HTML report
|
||||
pnpm exec playwright merge-reports --reporter=html ./all-blob-reports
|
||||
# Generate JSON report separately with explicit output path
|
||||
PLAYWRIGHT_JSON_OUTPUT_NAME=playwright-report/report.json \
|
||||
pnpm exec playwright merge-reports --reporter=json ./all-blob-reports
|
||||
working-directory: ComfyUI_frontend
|
||||
|
||||
- name: Upload HTML report
|
||||
@@ -284,3 +293,65 @@ jobs:
|
||||
name: playwright-report-chromium
|
||||
path: ComfyUI_frontend/playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
#### BEGIN Deployment and commenting (non-forked PRs only)
|
||||
# when using pull_request event, we have permission to comment directly
|
||||
# if its a forked repo, we need to use workflow_run event in a separate workflow (pr-playwright-deploy.yaml)
|
||||
|
||||
# Post starting comment for non-forked PRs
|
||||
comment-on-pr-start:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Get start time
|
||||
id: start-time
|
||||
run: echo "time=$(date -u '+%m/%d/%Y, %I:%M:%S %p')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Post starting comment
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
chmod +x scripts/cicd/pr-playwright-deploy-and-comment.sh
|
||||
./scripts/cicd/pr-playwright-deploy-and-comment.sh \
|
||||
"${{ github.event.pull_request.number }}" \
|
||||
"${{ github.head_ref }}" \
|
||||
"starting" \
|
||||
"${{ steps.start-time.outputs.time }}"
|
||||
|
||||
# Deploy and comment for non-forked PRs only
|
||||
deploy-and-comment:
|
||||
needs: [playwright-tests, merge-reports]
|
||||
runs-on: ubuntu-latest
|
||||
if: always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download all playwright reports
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: playwright-report-*
|
||||
path: reports
|
||||
|
||||
- name: Make deployment script executable
|
||||
run: chmod +x scripts/cicd/pr-playwright-deploy-and-comment.sh
|
||||
|
||||
- name: Deploy reports and comment on PR
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
./scripts/cicd/pr-playwright-deploy-and-comment.sh \
|
||||
"${{ github.event.pull_request.number }}" \
|
||||
"${{ github.head_ref }}" \
|
||||
"completed"
|
||||
#### END Deployment and commenting (non-forked PRs only)
|
||||
2
.github/workflows/update-electron-types.yaml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
- name: Get new version
|
||||
id: get-version
|
||||
run: |
|
||||
NEW_VERSION=$(pnpm list @comfyorg/comfyui-electron-types --json --depth=0 | jq -r '.[0].version')
|
||||
NEW_VERSION=$(pnpm list @comfyorg/comfyui-electron-types --json --depth=0 | jq -r '.[0].dependencies."@comfyorg/comfyui-electron-types".version')
|
||||
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create Pull Request
|
||||
|
||||
2
.github/workflows/update-manager-types.yaml
vendored
@@ -68,7 +68,7 @@ jobs:
|
||||
- name: Generate Manager API types
|
||||
run: |
|
||||
echo "Generating TypeScript types from ComfyUI-Manager@${{ steps.manager-info.outputs.commit }}..."
|
||||
npx openapi-typescript ./ComfyUI-Manager/openapi.yaml --output ./src/types/generatedManagerTypes.ts
|
||||
pnpm dlx openapi-typescript ./ComfyUI-Manager/openapi.yaml --output ./src/types/generatedManagerTypes.ts
|
||||
|
||||
- name: Validate generated types
|
||||
run: |
|
||||
|
||||
2
.github/workflows/update-registry-types.yaml
vendored
@@ -68,7 +68,7 @@ jobs:
|
||||
- name: Generate API types
|
||||
run: |
|
||||
echo "Generating TypeScript types from comfy-api@${{ steps.api-info.outputs.commit }}..."
|
||||
npx openapi-typescript ./comfy-api/openapi.yml --output ./src/types/comfyRegistryTypes.ts
|
||||
pnpm dlx openapi-typescript ./comfy-api/openapi.yml --output ./src/types/comfyRegistryTypes.ts
|
||||
|
||||
- name: Validate generated types
|
||||
run: |
|
||||
|
||||
8
.gitignore
vendored
@@ -22,6 +22,8 @@ dist-ssr
|
||||
*.local
|
||||
# Claude configuration
|
||||
.claude/*.local.json
|
||||
.claude/*.local.md
|
||||
.claude/*.local.txt
|
||||
CLAUDE.local.md
|
||||
|
||||
# Editor directories and files
|
||||
@@ -44,6 +46,7 @@ components.d.ts
|
||||
tests-ui/data/*
|
||||
tests-ui/ComfyUI_examples
|
||||
tests-ui/workflows/examples
|
||||
coverage/
|
||||
|
||||
# Browser tests
|
||||
/test-results/
|
||||
@@ -51,6 +54,7 @@ tests-ui/workflows/examples
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
browser_tests/**/*-win32.png
|
||||
browser_tests/local/
|
||||
|
||||
.env
|
||||
|
||||
@@ -77,8 +81,8 @@ vite.config.mts.timestamp-*.mjs
|
||||
*storybook.log
|
||||
storybook-static
|
||||
|
||||
|
||||
|
||||
# MCP Servers
|
||||
.playwright-mcp/*
|
||||
|
||||
.nx/cache
|
||||
.nx/workspace-data
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
npx lint-staged
|
||||
npx tsx scripts/check-unused-i18n-keys.ts
|
||||
pnpm exec lint-staged
|
||||
pnpm exec tsx scripts/check-unused-i18n-keys.ts
|
||||
5
.husky/pre-push
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Run Knip with cache via package script
|
||||
pnpm knip
|
||||
|
||||
@@ -9,7 +9,7 @@ module.exports = defineConfig({
|
||||
entry: 'src/locales/en',
|
||||
entryLocale: 'en',
|
||||
output: 'src/locales',
|
||||
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar'],
|
||||
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar', 'tr'],
|
||||
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.
|
||||
|
||||
12
.mcp.json
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"playwright": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@executeautomation/playwright-mcp-server"]
|
||||
},
|
||||
"context7": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@upstash/context7-mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,26 +15,37 @@ const config: StorybookConfig = {
|
||||
async viteFinal(config) {
|
||||
// Use dynamic import to avoid CJS deprecation warning
|
||||
const { mergeConfig } = await import('vite')
|
||||
const { default: tailwindcss } = await import('@tailwindcss/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
|
||||
})
|
||||
config.plugins = config.plugins
|
||||
// Type guard: ensure we have valid plugin objects with names
|
||||
.filter(
|
||||
(plugin): plugin is NonNullable<typeof plugin> & { name: string } => {
|
||||
return (
|
||||
plugin !== null &&
|
||||
plugin !== undefined &&
|
||||
typeof plugin === 'object' &&
|
||||
'name' in plugin &&
|
||||
typeof plugin.name === 'string'
|
||||
)
|
||||
}
|
||||
)
|
||||
// Business logic: filter out import-map plugins
|
||||
.filter((plugin) => !plugin.name.includes('import-map'))
|
||||
}
|
||||
|
||||
return mergeConfig(config, {
|
||||
// Replace plugins entirely to avoid inheritance issues
|
||||
plugins: [
|
||||
// Only include plugins we explicitly need for Storybook
|
||||
tailwindcss(),
|
||||
Icons({
|
||||
compiler: 'vue3',
|
||||
customCollections: {
|
||||
comfy: FileSystemIconLoader(
|
||||
process.cwd() + '/src/assets/icons/custom'
|
||||
process.cwd() + '/packages/design-system/src/icons'
|
||||
)
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -57,9 +57,8 @@
|
||||
|
||||
/* Override Storybook's problematic & selector styles */
|
||||
/* Reset only the specific properties that Storybook injects */
|
||||
#storybook-root li+li,
|
||||
#storybook-docs li+li {
|
||||
margin: inherit;
|
||||
padding: inherit;
|
||||
li+li {
|
||||
margin: 0;
|
||||
padding: revert-layer;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,7 @@
|
||||
import { definePreset } from '@primevue/themes'
|
||||
import Aura from '@primevue/themes/aura'
|
||||
import { setup } from '@storybook/vue3'
|
||||
import type { Preview } from '@storybook/vue3-vite'
|
||||
import type { Preview, StoryContext, StoryFn } from '@storybook/vue3-vite'
|
||||
import { createPinia } from 'pinia'
|
||||
import 'primeicons/primeicons.css'
|
||||
import PrimeVue from 'primevue/config'
|
||||
@@ -9,11 +9,9 @@ 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'
|
||||
import '@/assets/css/style.css'
|
||||
import { i18n } from '@/i18n'
|
||||
import '@/lib/litegraph/public/css/litegraph.css'
|
||||
|
||||
const ComfyUIPreset = definePreset(Aura, {
|
||||
semantic: {
|
||||
@@ -25,13 +23,11 @@ const ComfyUIPreset = definePreset(Aura, {
|
||||
// Setup Vue app for Storybook
|
||||
setup((app) => {
|
||||
app.directive('tooltip', Tooltip)
|
||||
|
||||
// Create Pinia instance
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(pinia)
|
||||
|
||||
// Initialize stores
|
||||
useColorPaletteStore(pinia)
|
||||
useWidgetStore(pinia)
|
||||
|
||||
app.use(i18n)
|
||||
app.use(PrimeVue, {
|
||||
theme: {
|
||||
@@ -50,8 +46,8 @@ setup((app) => {
|
||||
app.use(ToastService)
|
||||
})
|
||||
|
||||
// Dark theme decorator
|
||||
export const withTheme = (Story: any, context: any) => {
|
||||
// Theme and dialog decorator
|
||||
export const withTheme = (Story: StoryFn, context: StoryContext) => {
|
||||
const theme = context.globals.theme || 'light'
|
||||
|
||||
// Apply theme class to document root
|
||||
@@ -63,7 +59,7 @@ export const withTheme = (Story: any, context: any) => {
|
||||
document.body.classList.remove('dark-theme')
|
||||
}
|
||||
|
||||
return Story()
|
||||
return Story(context.args, context)
|
||||
}
|
||||
|
||||
const preview: Preview = {
|
||||
|
||||
70
CODEOWNERS
@@ -1,17 +1,61 @@
|
||||
# Admins
|
||||
* @Comfy-Org/comfy_frontend_devs
|
||||
# Desktop/Electron
|
||||
/src/types/desktop/ @webfiltered
|
||||
/src/constants/desktopDialogs.ts @webfiltered
|
||||
/src/constants/desktopMaintenanceTasks.ts @webfiltered
|
||||
/src/stores/electronDownloadStore.ts @webfiltered
|
||||
/src/extensions/core/electronAdapter.ts @webfiltered
|
||||
/src/views/DesktopDialogView.vue @webfiltered
|
||||
/src/components/install/ @webfiltered
|
||||
/src/components/maintenance/ @webfiltered
|
||||
/vite.electron.config.mts @webfiltered
|
||||
|
||||
# Maintainers
|
||||
*.md @Comfy-Org/comfy_maintainer
|
||||
/tests-ui/ @Comfy-Org/comfy_maintainer
|
||||
/browser_tests/ @Comfy-Org/comfy_maintainer
|
||||
/.env_example @Comfy-Org/comfy_maintainer
|
||||
# Common UI Components
|
||||
/src/components/chip/ @viva-jinyi
|
||||
/src/components/card/ @viva-jinyi
|
||||
/src/components/button/ @viva-jinyi
|
||||
/src/components/input/ @viva-jinyi
|
||||
|
||||
# Translations (AIGODLIKE team + shinshin86)
|
||||
/src/locales/ @Yorha4D @KarryCharon @DorotaLuna @shinshin86 @Comfy-Org/comfy_maintainer
|
||||
# Topbar
|
||||
/src/components/topbar/ @pythongosssss
|
||||
|
||||
# Load 3D extension
|
||||
/src/extensions/core/load3d.ts @jtydhr88 @Comfy-Org/comfy_frontend_devs
|
||||
# Thumbnail
|
||||
/src/renderer/core/thumbnail/ @pythongosssss
|
||||
|
||||
# Mask Editor extension
|
||||
/src/extensions/core/maskeditor.ts @brucew4yn3rp @trsommer @Comfy-Org/comfy_frontend_devs
|
||||
# Legacy UI
|
||||
/scripts/ui/ @pythongosssss
|
||||
|
||||
# Link rendering
|
||||
/src/renderer/core/canvas/links/ @benceruleanlu
|
||||
|
||||
# Node help system
|
||||
/src/utils/nodeHelpUtil.ts @benceruleanlu
|
||||
/src/stores/workspace/nodeHelpStore.ts @benceruleanlu
|
||||
/src/services/nodeHelpService.ts @benceruleanlu
|
||||
|
||||
# Selection toolbox
|
||||
/src/components/graph/selectionToolbox/ @Myestery
|
||||
|
||||
# Minimap
|
||||
/src/renderer/extensions/minimap/ @jtydhr88
|
||||
|
||||
# Assets
|
||||
/src/platform/assets/ @arjansingh
|
||||
|
||||
# Workflow Templates
|
||||
/src/platform/workflow/templates/ @Myestery @christian-byrne @comfyui-wiki
|
||||
/src/components/templates/ @Myestery @christian-byrne @comfyui-wiki
|
||||
|
||||
# Mask Editor
|
||||
/src/extensions/core/maskeditor.ts @trsommer @brucew4yn3rp
|
||||
/src/extensions/core/maskEditorLayerFilenames.ts @trsommer @brucew4yn3rp
|
||||
/src/extensions/core/maskEditorOld.ts @trsommer @brucew4yn3rp
|
||||
|
||||
# 3D
|
||||
/src/extensions/core/load3d.ts @jtydhr88
|
||||
/src/components/load3d/ @jtydhr88
|
||||
|
||||
# Manager
|
||||
/src/workbench/extensions/manager/ @viva-jinyi @christian-byrne @ltdrdata
|
||||
|
||||
# Translations
|
||||
/src/locales/ @Yorha4D @KarryCharon @shinshin86 @Comfy-Org/comfy_maintainer
|
||||
|
||||
@@ -265,9 +265,9 @@ The project supports three types of icons, all with automatic imports (no manual
|
||||
2. **Iconify Icons** - 200,000+ icons from various libraries: `<i-lucide:settings />`, `<i-mdi:folder />`
|
||||
3. **Custom Icons** - Your own SVG icons: `<i-comfy:workflow />`
|
||||
|
||||
Icons are powered by the unplugin-icons system, which automatically discovers and imports icons as Vue components. Custom icons are stored in `src/assets/icons/custom/` and processed by `build/customIconCollection.ts` with automatic validation.
|
||||
Icons are powered by the unplugin-icons system, which automatically discovers and imports icons as Vue components. Custom icons are stored in `packages/design-system/src/icons/` and processed by `packages/design-system/src/iconCollection.ts` with automatic validation.
|
||||
|
||||
For detailed instructions and code examples, see [src/assets/icons/README.md](src/assets/icons/README.md).
|
||||
For detailed instructions and code examples, see [packages/design-system/src/icons/README.md](packages/design-system/src/icons/README.md).
|
||||
|
||||
## Working with litegraph.js
|
||||
|
||||
|
||||
@@ -16,15 +16,20 @@ Without this flag, parallel tests will conflict and fail randomly.
|
||||
|
||||
### ComfyUI devtools
|
||||
|
||||
Clone <https://github.com/Comfy-Org/ComfyUI_devtools> to your `custom_nodes` directory.
|
||||
ComfyUI_devtools is now included in this repository under `tools/devtools/`. During CI/CD, these files are automatically copied to the `custom_nodes` directory.
|
||||
_ComfyUI_devtools adds additional API endpoints and nodes to ComfyUI for browser testing._
|
||||
|
||||
For local development, copy the devtools files to your ComfyUI installation:
|
||||
```bash
|
||||
cp -r tools/devtools/* /path/to/your/ComfyUI/custom_nodes/ComfyUI_devtools/
|
||||
```
|
||||
|
||||
### Node.js & Playwright Prerequisites
|
||||
|
||||
Ensure you have Node.js v20 or v22 installed. Then, set up the Chromium test driver:
|
||||
|
||||
```bash
|
||||
npx playwright install chromium --with-deps
|
||||
pnpm exec playwright install chromium --with-deps
|
||||
```
|
||||
|
||||
### Environment Configuration
|
||||
@@ -51,14 +56,6 @@ TEST_COMFYUI_DIR=/path/to/your/ComfyUI
|
||||
|
||||
### Common Setup Issues
|
||||
|
||||
**Most tests require the new menu system** - Add to your test:
|
||||
|
||||
```typescript
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
})
|
||||
```
|
||||
|
||||
### Release API Mocking
|
||||
|
||||
By default, all tests mock the release API (`api.comfy.org/releases`) to prevent release notification popups from interfering with test execution. This is necessary because the release notifications can appear over UI elements and block test interactions.
|
||||
@@ -76,7 +73,7 @@ For tests that specifically need to test release functionality, see the example
|
||||
**Always use UI mode for development:**
|
||||
|
||||
```bash
|
||||
npx playwright test --ui
|
||||
pnpm exec playwright test --ui
|
||||
```
|
||||
|
||||
UI mode features:
|
||||
@@ -92,8 +89,8 @@ UI mode features:
|
||||
For CI or headless testing:
|
||||
|
||||
```bash
|
||||
npx playwright test # Run all tests
|
||||
npx playwright test widget.spec.ts # Run specific test file
|
||||
pnpm exec playwright test # Run all tests
|
||||
pnpm exec playwright test widget.spec.ts # Run specific test file
|
||||
```
|
||||
|
||||
### Local Development Config
|
||||
@@ -389,7 +386,7 @@ export default defineConfig({
|
||||
Option 2 - Generate local baselines for comparison:
|
||||
|
||||
```bash
|
||||
npx playwright test --update-snapshots
|
||||
pnpm exec playwright test --update-snapshots
|
||||
```
|
||||
|
||||
### Creating New Screenshot Baselines
|
||||
|
||||
1
browser_tests/assets/vueNodes/simple-triple.json
Normal file
@@ -0,0 +1 @@
|
||||
{"id":"4412323e-2509-4258-8abc-68ddeea8f9e1","revision":0,"last_node_id":39,"last_link_id":29,"nodes":[{"id":37,"type":"KSampler","pos":[3635.923095703125,870.237548828125],"size":[428,437],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"model","name":"model","type":"MODEL","link":null},{"localized_name":"positive","name":"positive","type":"CONDITIONING","link":null},{"localized_name":"negative","name":"negative","type":"CONDITIONING","link":null},{"localized_name":"latent_image","name":"latent_image","type":"LATENT","link":null},{"localized_name":"seed","name":"seed","type":"INT","widget":{"name":"seed"},"link":null},{"localized_name":"steps","name":"steps","type":"INT","widget":{"name":"steps"},"link":null},{"localized_name":"cfg","name":"cfg","type":"FLOAT","widget":{"name":"cfg"},"link":null},{"localized_name":"sampler_name","name":"sampler_name","type":"COMBO","widget":{"name":"sampler_name"},"link":null},{"localized_name":"scheduler","name":"scheduler","type":"COMBO","widget":{"name":"scheduler"},"link":null},{"localized_name":"denoise","name":"denoise","type":"FLOAT","widget":{"name":"denoise"},"link":null}],"outputs":[{"localized_name":"LATENT","name":"LATENT","type":"LATENT","links":null}],"properties":{"Node name for S&R":"KSampler"},"widgets_values":[0,"randomize",20,8,"euler","simple",1]},{"id":38,"type":"VAEDecode","pos":[4164.01611328125,925.5230712890625],"size":[193.25,107],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"samples","name":"samples","type":"LATENT","link":null},{"localized_name":"vae","name":"vae","type":"VAE","link":null}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":null}],"properties":{"Node name for S&R":"VAEDecode"}},{"id":39,"type":"CLIPTextEncode","pos":[3259.289794921875,927.2508544921875],"size":[239.9375,155],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"clip","name":"clip","type":"CLIP","link":null},{"localized_name":"text","name":"text","type":"STRING","widget":{"name":"text"},"link":null}],"outputs":[{"localized_name":"CONDITIONING","name":"CONDITIONING","type":"CONDITIONING","links":null}],"properties":{"Node name for S&R":"CLIPTextEncode"},"widgets_values":[""]}],"links":[],"groups":[],"config":{},"extra":{"ds":{"scale":1.1576250000000001,"offset":[-2808.366467322067,-478.34316506594797]}},"version":0.4}
|
||||
@@ -10,7 +10,7 @@ import type { Position } from './types'
|
||||
* - {@link Mouse.move}
|
||||
* - {@link Mouse.up}
|
||||
*/
|
||||
export interface DragOptions {
|
||||
interface DragOptions {
|
||||
button?: 'left' | 'right' | 'middle'
|
||||
clickCount?: number
|
||||
steps?: number
|
||||
|
||||
@@ -5,13 +5,14 @@ import dotenv from 'dotenv'
|
||||
import * as fs from 'fs'
|
||||
|
||||
import type { LGraphNode } from '../../src/lib/litegraph/src/litegraph'
|
||||
import type { NodeId } from '../../src/schemas/comfyWorkflowSchema'
|
||||
import type { NodeId } from '../../src/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { KeyCombo } from '../../src/schemas/keyBindingSchema'
|
||||
import type { useWorkspaceStore } from '../../src/stores/workspaceStore'
|
||||
import { NodeBadgeMode } from '../../src/types/nodeSource'
|
||||
import { ComfyActionbar } from '../helpers/actionbar'
|
||||
import { ComfyTemplates } from '../helpers/templates'
|
||||
import { ComfyMouse } from './ComfyMouse'
|
||||
import { VueNodeHelpers } from './VueNodeHelpers'
|
||||
import { ComfyNodeSearchBox } from './components/ComfyNodeSearchBox'
|
||||
import { SettingDialog } from './components/SettingDialog'
|
||||
import {
|
||||
@@ -144,6 +145,7 @@ export class ComfyPage {
|
||||
public readonly templates: ComfyTemplates
|
||||
public readonly settingDialog: SettingDialog
|
||||
public readonly confirmDialog: ConfirmDialog
|
||||
public readonly vueNodes: VueNodeHelpers
|
||||
|
||||
/** Worker index to test user ID */
|
||||
public readonly userIds: string[] = []
|
||||
@@ -172,6 +174,7 @@ export class ComfyPage {
|
||||
this.templates = new ComfyTemplates(page)
|
||||
this.settingDialog = new SettingDialog(page, this)
|
||||
this.confirmDialog = new ConfirmDialog(page)
|
||||
this.vueNodes = new VueNodeHelpers(page)
|
||||
}
|
||||
|
||||
convertLeafToContent(structure: FolderStructure): FolderStructure {
|
||||
@@ -453,6 +456,32 @@ export class ComfyPage {
|
||||
await workflowsTab.close()
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach a screenshot to the test report.
|
||||
* By default, screenshots are only taken in non-CI environments.
|
||||
* @param name - Name for the screenshot attachment
|
||||
* @param options - Optional configuration
|
||||
* @param options.runInCI - Whether to take screenshot in CI (default: false)
|
||||
* @param options.fullPage - Whether to capture full page (default: false)
|
||||
*/
|
||||
async attachScreenshot(
|
||||
name: string,
|
||||
options: { runInCI?: boolean; fullPage?: boolean } = {}
|
||||
) {
|
||||
const { runInCI = false, fullPage = false } = options
|
||||
|
||||
// Skip in CI unless explicitly requested
|
||||
if (process.env.CI && !runInCI) {
|
||||
return
|
||||
}
|
||||
|
||||
const testInfo = comfyPageFixture.info()
|
||||
await testInfo.attach(name, {
|
||||
body: await this.page.screenshot({ fullPage }),
|
||||
contentType: 'image/png'
|
||||
})
|
||||
}
|
||||
|
||||
async resetView() {
|
||||
if (await this.resetViewButton.isVisible()) {
|
||||
await this.resetViewButton.click()
|
||||
@@ -1395,7 +1424,7 @@ export class ComfyPage {
|
||||
}
|
||||
|
||||
async closeDialog() {
|
||||
await this.page.locator('.p-dialog-close-button').click()
|
||||
await this.page.locator('.p-dialog-close-button').click({ force: true })
|
||||
await expect(this.page.locator('.p-dialog')).toBeHidden()
|
||||
}
|
||||
|
||||
@@ -1614,7 +1643,7 @@ export const comfyPageFixture = base.extend<{
|
||||
|
||||
try {
|
||||
await comfyPage.setupSettings({
|
||||
'Comfy.UseNewMenu': 'Disabled',
|
||||
'Comfy.UseNewMenu': 'Top',
|
||||
// Hide canvas menu/info/selection toolbox by default.
|
||||
'Comfy.Graph.CanvasInfo': false,
|
||||
'Comfy.Graph.CanvasMenu': false,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Page, test as base } from '@playwright/test'
|
||||
import type { Page } from '@playwright/test'
|
||||
import { test as base } from '@playwright/test'
|
||||
|
||||
export class UserSelectPage {
|
||||
constructor(
|
||||
|
||||
117
browser_tests/fixtures/VueNodeHelpers.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Vue Node Test Helpers
|
||||
*/
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
export class VueNodeHelpers {
|
||||
constructor(private page: Page) {}
|
||||
|
||||
/**
|
||||
* Get locator for all Vue node components in the DOM
|
||||
*/
|
||||
get nodes(): Locator {
|
||||
return this.page.locator('[data-node-id]')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get locator for selected Vue node components (using visual selection indicators)
|
||||
*/
|
||||
get selectedNodes(): Locator {
|
||||
return this.page.locator(
|
||||
'[data-node-id].outline-black, [data-node-id].outline-white'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get locator for a Vue node by the node's title (displayed name in the header)
|
||||
*/
|
||||
getNodeByTitle(title: string): Locator {
|
||||
return this.page.locator(`[data-node-id]`).filter({ hasText: title })
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total count of Vue nodes in the DOM
|
||||
*/
|
||||
async getNodeCount(): Promise<number> {
|
||||
return await this.nodes.count()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of selected Vue nodes
|
||||
*/
|
||||
async getSelectedNodeCount(): Promise<number> {
|
||||
return await this.selectedNodes.count()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all Vue node IDs currently in the DOM
|
||||
*/
|
||||
async getNodeIds(): Promise<string[]> {
|
||||
return await this.nodes.evaluateAll((nodes) =>
|
||||
nodes
|
||||
.map((n) => n.getAttribute('data-node-id'))
|
||||
.filter((id): id is string => id !== null)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a specific Vue node by ID
|
||||
*/
|
||||
async selectNode(nodeId: string): Promise<void> {
|
||||
await this.page.locator(`[data-node-id="${nodeId}"]`).click()
|
||||
}
|
||||
|
||||
/**
|
||||
* Select multiple Vue nodes by IDs using Ctrl+click
|
||||
*/
|
||||
async selectNodes(nodeIds: string[]): Promise<void> {
|
||||
if (nodeIds.length === 0) return
|
||||
|
||||
// Select first node normally
|
||||
await this.selectNode(nodeIds[0])
|
||||
|
||||
// Add additional nodes with Ctrl+click
|
||||
for (let i = 1; i < nodeIds.length; i++) {
|
||||
await this.page.locator(`[data-node-id="${nodeIds[i]}"]`).click({
|
||||
modifiers: ['Control']
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all selections by clicking empty space
|
||||
*/
|
||||
async clearSelection(): Promise<void> {
|
||||
await this.page.mouse.click(50, 50)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete selected Vue nodes using Delete key
|
||||
*/
|
||||
async deleteSelected(): Promise<void> {
|
||||
await this.page.locator('#graph-canvas').focus()
|
||||
await this.page.keyboard.press('Delete')
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete selected Vue nodes using Backspace key
|
||||
*/
|
||||
async deleteSelectedWithBackspace(): Promise<void> {
|
||||
await this.page.locator('#graph-canvas').focus()
|
||||
await this.page.keyboard.press('Backspace')
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for Vue nodes to be rendered
|
||||
*/
|
||||
async waitForNodes(expectedCount?: number): Promise<void> {
|
||||
if (expectedCount !== undefined) {
|
||||
await this.page.waitForFunction(
|
||||
(count) => document.querySelectorAll('[data-node-id]').length >= count,
|
||||
expectedCount
|
||||
)
|
||||
} else {
|
||||
await this.page.waitForSelector('[data-node-id]')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Locator, Page } from '@playwright/test'
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
export class ComfyNodeSearchFilterSelectionPanel {
|
||||
constructor(public readonly page: Page) {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Page } from '@playwright/test'
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
import { ComfyPage } from '../ComfyPage'
|
||||
import type { ComfyPage } from '../ComfyPage'
|
||||
|
||||
export class SettingDialog {
|
||||
constructor(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Locator, Page } from '@playwright/test'
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
class SidebarTab {
|
||||
constructor(
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { Locator, Page } from '@playwright/test'
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
export class Topbar {
|
||||
constructor(public readonly page: Page) {}
|
||||
private readonly menuLocator: Locator
|
||||
private readonly menuTrigger: Locator
|
||||
|
||||
constructor(public readonly page: Page) {
|
||||
this.menuLocator = page.locator('.comfy-command-menu')
|
||||
this.menuTrigger = page.locator('.comfyui-logo-wrapper')
|
||||
}
|
||||
|
||||
async getTabNames(): Promise<string[]> {
|
||||
return await this.page
|
||||
@@ -15,10 +22,33 @@ export class Topbar {
|
||||
.innerText()
|
||||
}
|
||||
|
||||
getMenuItem(itemLabel: string): Locator {
|
||||
/**
|
||||
* Get a menu item by its label, optionally within a specific parent container
|
||||
*/
|
||||
getMenuItem(itemLabel: string, parent?: Locator): Locator {
|
||||
if (parent) {
|
||||
return parent.locator(`.p-tieredmenu-item:has-text("${itemLabel}")`)
|
||||
}
|
||||
|
||||
return this.page.locator(`.p-menubar-item-label:text-is("${itemLabel}")`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the visible submenu (last visible submenu in case of nested menus)
|
||||
*/
|
||||
getVisibleSubmenu(): Locator {
|
||||
return this.page.locator('.p-tieredmenu-submenu:visible').last()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a menu item has an active checkmark
|
||||
*/
|
||||
async isMenuItemActive(menuItem: Locator): Promise<boolean> {
|
||||
const checkmark = menuItem.locator('.pi-check')
|
||||
const classes = await checkmark.getAttribute('class')
|
||||
return classes ? !classes.includes('invisible') : false
|
||||
}
|
||||
|
||||
getWorkflowTab(tabName: string): Locator {
|
||||
return this.page
|
||||
.locator(`.workflow-tabs .workflow-label:has-text("${tabName}")`)
|
||||
@@ -66,10 +96,50 @@ export class Topbar {
|
||||
|
||||
async openTopbarMenu() {
|
||||
await this.page.waitForTimeout(1000)
|
||||
await this.page.locator('.comfyui-logo-wrapper').click()
|
||||
const menu = this.page.locator('.comfy-command-menu')
|
||||
await menu.waitFor({ state: 'visible' })
|
||||
return menu
|
||||
await this.menuTrigger.click()
|
||||
await this.menuLocator.waitFor({ state: 'visible' })
|
||||
return this.menuLocator
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the topbar menu by clicking outside
|
||||
*/
|
||||
async closeTopbarMenu() {
|
||||
await this.page.locator('body').click({ position: { x: 10, y: 10 } })
|
||||
await expect(this.menuLocator).not.toBeVisible()
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to a submenu by hovering over a menu item
|
||||
*/
|
||||
async openSubmenu(menuItemLabel: string): Promise<Locator> {
|
||||
const menuItem = this.getMenuItem(menuItemLabel)
|
||||
await menuItem.hover()
|
||||
const submenu = this.getVisibleSubmenu()
|
||||
await submenu.waitFor({ state: 'visible' })
|
||||
return submenu
|
||||
}
|
||||
|
||||
/**
|
||||
* Get theme menu items and interact with theme switching
|
||||
*/
|
||||
async getThemeMenuItems() {
|
||||
const themeSubmenu = await this.openSubmenu('Theme')
|
||||
return {
|
||||
submenu: themeSubmenu,
|
||||
darkTheme: this.getMenuItem('Dark (Default)', themeSubmenu),
|
||||
lightTheme: this.getMenuItem('Light', themeSubmenu)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to a specific theme
|
||||
*/
|
||||
async switchTheme(theme: 'dark' | 'light') {
|
||||
const { darkTheme, lightTheme } = await this.getThemeMenuItems()
|
||||
const themeItem = theme === 'dark' ? darkTheme : lightTheme
|
||||
const themeLabel = themeItem.locator('.p-menubar-item-label')
|
||||
await themeLabel.click()
|
||||
}
|
||||
|
||||
async triggerTopbarCommand(path: string[]) {
|
||||
@@ -79,9 +149,7 @@ export class Topbar {
|
||||
|
||||
const menu = await this.openTopbarMenu()
|
||||
const tabName = path[0]
|
||||
const topLevelMenuItem = this.page.locator(
|
||||
`.p-menubar-item-label:text-is("${tabName}")`
|
||||
)
|
||||
const topLevelMenuItem = this.getMenuItem(tabName)
|
||||
const topLevelMenu = menu
|
||||
.locator('.p-tieredmenu-item')
|
||||
.filter({ has: topLevelMenuItem })
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Page } from '@playwright/test'
|
||||
|
||||
import type { NodeId } from '../../../src/schemas/comfyWorkflowSchema'
|
||||
import type { NodeId } from '../../../src/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { ManageGroupNode } from '../../helpers/manageGroupNode'
|
||||
import type { ComfyPage } from '../ComfyPage'
|
||||
import type { Position, Size } from '../types'
|
||||
@@ -134,7 +134,7 @@ export class SubgraphSlotReference {
|
||||
}
|
||||
}
|
||||
|
||||
export class NodeSlotReference {
|
||||
class NodeSlotReference {
|
||||
constructor(
|
||||
readonly type: 'input' | 'output',
|
||||
readonly index: number,
|
||||
@@ -201,7 +201,7 @@ export class NodeSlotReference {
|
||||
}
|
||||
}
|
||||
|
||||
export class NodeWidgetReference {
|
||||
class NodeWidgetReference {
|
||||
constructor(
|
||||
readonly index: number,
|
||||
readonly node: NodeReference
|
||||
|
||||
@@ -34,17 +34,23 @@ const getContentType = (filename: string, fileType: OutputFileType) => {
|
||||
}
|
||||
|
||||
const setQueueIndex = (task: TaskItem) => {
|
||||
task.prompt[0] = TaskHistory.queueIndex++
|
||||
task.prompt.priority = TaskHistory.queueIndex++
|
||||
}
|
||||
|
||||
const setPromptId = (task: TaskItem) => {
|
||||
task.prompt[1] = uuidv4()
|
||||
if (!task.prompt.prompt_id || task.prompt.prompt_id === 'prompt-id') {
|
||||
task.prompt.prompt_id = uuidv4()
|
||||
}
|
||||
}
|
||||
|
||||
export default class TaskHistory {
|
||||
static queueIndex = 0
|
||||
static readonly defaultTask: Readonly<HistoryTaskItem> = {
|
||||
prompt: [0, 'prompt-id', {}, { client_id: uuidv4() }, []],
|
||||
prompt: {
|
||||
priority: 0,
|
||||
prompt_id: 'prompt-id',
|
||||
extra_data: { client_id: uuidv4() }
|
||||
},
|
||||
outputs: {},
|
||||
status: {
|
||||
status_str: 'success',
|
||||
@@ -66,10 +72,37 @@ export default class TaskHistory {
|
||||
)
|
||||
|
||||
private async handleGetHistory(route: Route) {
|
||||
const url = route.request().url()
|
||||
|
||||
// Handle history_v2/:prompt_id endpoint
|
||||
const promptIdMatch = url.match(/history_v2\/([^?]+)/)
|
||||
if (promptIdMatch) {
|
||||
const promptId = promptIdMatch[1]
|
||||
const task = this.tasks.find((t) => t.prompt.prompt_id === promptId)
|
||||
const response: Record<string, any> = {}
|
||||
if (task) {
|
||||
response[promptId] = task
|
||||
}
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(response)
|
||||
})
|
||||
}
|
||||
|
||||
// Handle history_v2 list endpoint
|
||||
// Convert HistoryTaskItem to RawHistoryItem format expected by API
|
||||
const rawHistoryItems = this.tasks.map((task) => ({
|
||||
prompt_id: task.prompt.prompt_id,
|
||||
prompt: task.prompt,
|
||||
status: task.status,
|
||||
outputs: task.outputs,
|
||||
...(task.meta && { meta: task.meta })
|
||||
}))
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(this.tasks)
|
||||
body: JSON.stringify({ history: rawHistoryItems })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -93,7 +126,7 @@ export default class TaskHistory {
|
||||
|
||||
async setupRoutes() {
|
||||
return this.comfyPage.page.route(
|
||||
/.*\/api\/(view|history)(\?.*)?$/,
|
||||
/.*\/api\/(view|history_v2)(\/[^?]*)?(\?.*)?$/,
|
||||
async (route) => {
|
||||
const request = route.request()
|
||||
const method = request.method()
|
||||
|
||||
@@ -12,9 +12,10 @@ export const webSocketFixture = base.extend<{
|
||||
// so we can look it up to trigger messages
|
||||
const store: Record<string, WebSocket> = ((window as any).__ws__ = {})
|
||||
window.WebSocket = class extends window.WebSocket {
|
||||
constructor() {
|
||||
// @ts-expect-error
|
||||
super(...arguments)
|
||||
constructor(
|
||||
...rest: ConstructorParameters<typeof window.WebSocket>
|
||||
) {
|
||||
super(...rest)
|
||||
store[this.url] = this
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FullConfig } from '@playwright/test'
|
||||
import type { FullConfig } from '@playwright/test'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
import { backupPath } from './utils/backupUtils'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FullConfig } from '@playwright/test'
|
||||
import type { FullConfig } from '@playwright/test'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
import { restorePath } from './utils/backupUtils'
|
||||
|
||||
104
browser_tests/helpers/fitToView.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import type { ReadOnlyRect } from '../../src/lib/litegraph/src/interfaces'
|
||||
import type { ComfyPage } from '../fixtures/ComfyPage'
|
||||
|
||||
interface FitToViewOptions {
|
||||
selectionOnly?: boolean
|
||||
zoom?: number
|
||||
padding?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantly fits the canvas view to graph content without waiting for UI animation.
|
||||
*
|
||||
* Lives outside the shared fixture to keep the default ComfyPage interactions user-oriented.
|
||||
*/
|
||||
export async function fitToViewInstant(
|
||||
comfyPage: ComfyPage,
|
||||
options: FitToViewOptions = {}
|
||||
) {
|
||||
const { selectionOnly = false, zoom = 0.75, padding = 10 } = options
|
||||
|
||||
const rectangles = await comfyPage.page.evaluate<
|
||||
ReadOnlyRect[] | null,
|
||||
{ selectionOnly: boolean }
|
||||
>(
|
||||
({ selectionOnly }) => {
|
||||
const app = window['app']
|
||||
if (!app?.canvas) return null
|
||||
|
||||
const canvas = app.canvas
|
||||
const items = (() => {
|
||||
if (selectionOnly && canvas.selectedItems?.size) {
|
||||
return Array.from(canvas.selectedItems)
|
||||
}
|
||||
try {
|
||||
return Array.from(canvas.positionableItems ?? [])
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
})()
|
||||
|
||||
if (!items.length) return null
|
||||
|
||||
const rects: ReadOnlyRect[] = []
|
||||
|
||||
for (const item of items) {
|
||||
const rect = item?.boundingRect
|
||||
if (!rect) continue
|
||||
|
||||
const x = Number(rect[0])
|
||||
const y = Number(rect[1])
|
||||
const width = Number(rect[2])
|
||||
const height = Number(rect[3])
|
||||
|
||||
rects.push([x, y, width, height] as const)
|
||||
}
|
||||
|
||||
return rects.length ? rects : null
|
||||
},
|
||||
{ selectionOnly }
|
||||
)
|
||||
|
||||
if (!rectangles || rectangles.length === 0) return
|
||||
|
||||
let minX = Infinity
|
||||
let minY = Infinity
|
||||
let maxX = -Infinity
|
||||
let maxY = -Infinity
|
||||
|
||||
for (const [x, y, width, height] of rectangles) {
|
||||
minX = Math.min(minX, Number(x))
|
||||
minY = Math.min(minY, Number(y))
|
||||
maxX = Math.max(maxX, Number(x) + Number(width))
|
||||
maxY = Math.max(maxY, Number(y) + Number(height))
|
||||
}
|
||||
|
||||
const hasFiniteBounds =
|
||||
Number.isFinite(minX) &&
|
||||
Number.isFinite(minY) &&
|
||||
Number.isFinite(maxX) &&
|
||||
Number.isFinite(maxY)
|
||||
|
||||
if (!hasFiniteBounds) return
|
||||
|
||||
const bounds: ReadOnlyRect = [
|
||||
minX - padding,
|
||||
minY - padding,
|
||||
maxX - minX + 2 * padding,
|
||||
maxY - minY + 2 * padding
|
||||
]
|
||||
|
||||
await comfyPage.page.evaluate(
|
||||
({ bounds, zoom }) => {
|
||||
const app = window['app']
|
||||
if (!app?.canvas) return
|
||||
|
||||
const canvas = app.canvas
|
||||
canvas.ds.fitToBounds(bounds, { zoom })
|
||||
canvas.setDirty(true, true)
|
||||
},
|
||||
{ bounds, zoom }
|
||||
)
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Locator, Page } from '@playwright/test'
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
export class ManageGroupNode {
|
||||
footer: Locator
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Locator, Page } from '@playwright/test'
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
import path from 'path'
|
||||
|
||||
import {
|
||||
import type {
|
||||
TemplateInfo,
|
||||
WorkflowTemplates
|
||||
} from '../../src/types/workflowTemplateTypes'
|
||||
} from '../../src/platform/workflow/templates/types/template'
|
||||
|
||||
export class ComfyTemplates {
|
||||
readonly content: Locator
|
||||
|
||||
@@ -29,9 +29,9 @@ test.describe('Actionbar', () => {
|
||||
|
||||
// Intercept the prompt queue endpoint
|
||||
let promptNumber = 0
|
||||
comfyPage.page.route('**/api/prompt', async (route, req) => {
|
||||
await comfyPage.page.route('**/api/prompt', async (route, req) => {
|
||||
await new Promise((r) => setTimeout(r, 100))
|
||||
route.fulfill({
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify({
|
||||
prompt_id: promptNumber,
|
||||
|
||||
@@ -2,6 +2,10 @@ import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Background Image Upload', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
// Reset the background image setting before each test
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ComfyPage } from '../fixtures/ComfyPage'
|
||||
import {
|
||||
ComfyPage,
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../fixtures/ComfyPage'
|
||||
@@ -15,6 +15,10 @@ async function afterChange(comfyPage: ComfyPage) {
|
||||
})
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Change Tracker', () => {
|
||||
test.describe('Undo/Redo', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { Page, expect } from '@playwright/test'
|
||||
import type { Page } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
interface ChatHistoryEntry {
|
||||
prompt: string
|
||||
response: string
|
||||
|
||||
@@ -3,6 +3,10 @@ import { expect } from '@playwright/test'
|
||||
import type { Palette } from '../../src/schemas/colorPaletteSchema'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
const customColorPalettes: Record<string, Palette> = {
|
||||
obsidian: {
|
||||
version: 102,
|
||||
|
||||
@@ -2,6 +2,10 @@ import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Keybindings', () => {
|
||||
test('Should execute command', async ({ comfyPage }) => {
|
||||
await comfyPage.registerCommand('TestCommand', () => {
|
||||
|
||||
@@ -2,6 +2,10 @@ import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Copy Paste', () => {
|
||||
test('Can copy and paste node', async ({ comfyPage }) => {
|
||||
await comfyPage.clickEmptyLatentNode()
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { Locator, expect } from '@playwright/test'
|
||||
import type { Locator } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import type { Keybinding } from '../../src/schemas/keyBindingSchema'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Load workflow warning', () => {
|
||||
test('Should display a warning when loading a workflow with missing nodes', async ({
|
||||
comfyPage
|
||||
@@ -36,6 +41,10 @@ test('Does not report warning on undo/redo', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('missing/missing_nodes')
|
||||
await comfyPage.closeDialog()
|
||||
|
||||
// Wait for any async operations to complete after dialog closes
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.page.waitForTimeout(100)
|
||||
|
||||
// Make a change to the graph
|
||||
await comfyPage.doubleClickCanvas()
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler')
|
||||
|
||||
@@ -2,6 +2,10 @@ import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('DOM Widget', () => {
|
||||
test('Collapsed multiline textarea is not visible', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('widgets/collapsed_multiline')
|
||||
|
||||
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
@@ -2,6 +2,10 @@ import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Execution', () => {
|
||||
test('Report error on unconnected slot', async ({ comfyPage }) => {
|
||||
await comfyPage.disconnectEdge()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { SettingParams } from '../../src/types/settingTypes'
|
||||
import type { SettingParams } from '../../src/platform/settings/types'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Topbar commands', () => {
|
||||
@@ -247,7 +247,7 @@ test.describe('Topbar commands', () => {
|
||||
test.describe('Dialog', () => {
|
||||
test('Should allow showing a prompt dialog', async ({ comfyPage }) => {
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window['app'].extensionManager.dialog
|
||||
void window['app'].extensionManager.dialog
|
||||
.prompt({
|
||||
title: 'Test Prompt',
|
||||
message: 'Test Prompt Message'
|
||||
@@ -267,7 +267,7 @@ test.describe('Topbar commands', () => {
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window['app'].extensionManager.dialog
|
||||
void window['app'].extensionManager.dialog
|
||||
.confirm({
|
||||
title: 'Test Confirm',
|
||||
message: 'Test Confirm Message'
|
||||
@@ -284,7 +284,7 @@ test.describe('Topbar commands', () => {
|
||||
test('Should allow dismissing a dialog', async ({ comfyPage }) => {
|
||||
await comfyPage.page.evaluate(() => {
|
||||
window['value'] = 'foo'
|
||||
window['app'].extensionManager.dialog
|
||||
void window['app'].extensionManager.dialog
|
||||
.confirm({
|
||||
title: 'Test Confirm',
|
||||
message: 'Test Confirm Message'
|
||||
|
||||
@@ -2,6 +2,10 @@ import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Feature Flags', () => {
|
||||
test('Client and server exchange feature flags on connection', async ({
|
||||
comfyPage
|
||||
|
||||
@@ -2,6 +2,10 @@ import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
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
|
||||
|
||||
@@ -2,6 +2,10 @@ import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Graph Canvas Menu', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
// Set link render mode to spline to make sure it's not affected by other tests'
|
||||
|
||||
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 100 KiB |
@@ -1,8 +1,13 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { ComfyPage, comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
import type { ComfyPage } from '../fixtures/ComfyPage'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
import type { NodeReference } from '../fixtures/utils/litegraphUtils'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Group Node', () => {
|
||||
test.describe('Node library sidebar', () => {
|
||||
const groupNodeName = 'DefautWorkflowGroupNode'
|
||||
|
||||
131
browser_tests/tests/historyApi.spec.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('History API v2', () => {
|
||||
const TEST_PROMPT_ID = 'test-prompt-id'
|
||||
const TEST_CLIENT_ID = 'test-client'
|
||||
|
||||
test('Can fetch history with new v2 format', async ({ comfyPage }) => {
|
||||
// Set up mocked history with tasks
|
||||
await comfyPage.setupHistory().withTask(['example.webp']).setupRoutes()
|
||||
|
||||
// Verify history_v2 API response format
|
||||
const result = await comfyPage.page.evaluate(async () => {
|
||||
try {
|
||||
const response = await window['app'].api.getHistory()
|
||||
return { success: true, data: response }
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch history:', error)
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
})
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.data).toHaveProperty('History')
|
||||
expect(Array.isArray(result.data.History)).toBe(true)
|
||||
expect(result.data.History.length).toBeGreaterThan(0)
|
||||
|
||||
const historyItem = result.data.History[0]
|
||||
|
||||
// Verify the new prompt structure (object instead of array)
|
||||
expect(historyItem.prompt).toHaveProperty('priority')
|
||||
expect(historyItem.prompt).toHaveProperty('prompt_id')
|
||||
expect(historyItem.prompt).toHaveProperty('extra_data')
|
||||
expect(typeof historyItem.prompt.priority).toBe('number')
|
||||
expect(typeof historyItem.prompt.prompt_id).toBe('string')
|
||||
expect(historyItem.prompt.extra_data).toHaveProperty('client_id')
|
||||
})
|
||||
|
||||
test('Can load workflow from history using history_v2 endpoint', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Simple mock workflow for testing
|
||||
const mockWorkflow = {
|
||||
version: 0.4,
|
||||
nodes: [{ id: 1, type: 'TestNode', pos: [100, 100], size: [200, 100] }],
|
||||
links: [],
|
||||
groups: [],
|
||||
config: {},
|
||||
extra: {}
|
||||
}
|
||||
|
||||
// Set up history with workflow data
|
||||
await comfyPage
|
||||
.setupHistory()
|
||||
.withTask(['example.webp'], 'images', {
|
||||
prompt: {
|
||||
priority: 0,
|
||||
prompt_id: TEST_PROMPT_ID,
|
||||
extra_data: {
|
||||
client_id: TEST_CLIENT_ID,
|
||||
extra_pnginfo: { workflow: mockWorkflow }
|
||||
}
|
||||
}
|
||||
})
|
||||
.setupRoutes()
|
||||
|
||||
// Load initial workflow to clear canvas
|
||||
await comfyPage.loadWorkflow('simple_slider')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Load workflow from history
|
||||
const loadResult = await comfyPage.page.evaluate(async (promptId) => {
|
||||
try {
|
||||
const workflow =
|
||||
await window['app'].api.getWorkflowFromHistory(promptId)
|
||||
if (workflow) {
|
||||
await window['app'].loadGraphData(workflow)
|
||||
return { success: true }
|
||||
}
|
||||
return { success: false, error: 'No workflow found' }
|
||||
} catch (error) {
|
||||
console.error('Failed to load workflow from history:', error)
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
}, TEST_PROMPT_ID)
|
||||
|
||||
expect(loadResult.success).toBe(true)
|
||||
|
||||
// Verify workflow loaded correctly
|
||||
await comfyPage.nextFrame()
|
||||
const nodeInfo = await comfyPage.page.evaluate(() => {
|
||||
try {
|
||||
const graph = window['app'].graph
|
||||
return {
|
||||
success: true,
|
||||
nodeCount: graph.nodes?.length || 0,
|
||||
firstNodeType: graph.nodes?.[0]?.type || null
|
||||
}
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
})
|
||||
|
||||
expect(nodeInfo.success).toBe(true)
|
||||
expect(nodeInfo.nodeCount).toBe(1)
|
||||
expect(nodeInfo.firstNodeType).toBe('TestNode')
|
||||
})
|
||||
|
||||
test('Handles missing workflow data gracefully', async ({ comfyPage }) => {
|
||||
// Set up empty history routes
|
||||
await comfyPage.setupHistory().setupRoutes()
|
||||
|
||||
// Test loading from history with invalid prompt_id
|
||||
const result = await comfyPage.page.evaluate(async () => {
|
||||
try {
|
||||
const workflow =
|
||||
await window['app'].api.getWorkflowFromHistory('invalid-id')
|
||||
return { success: true, workflow }
|
||||
} catch (error) {
|
||||
console.error('Expected error for invalid prompt_id:', error)
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
})
|
||||
|
||||
// Should handle gracefully without throwing
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.workflow).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,12 +1,17 @@
|
||||
import { Locator, expect } from '@playwright/test'
|
||||
import { Position } from '@vueuse/core'
|
||||
import type { Locator } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
import type { Position } from '@vueuse/core'
|
||||
|
||||
import {
|
||||
type ComfyPage,
|
||||
comfyPageFixture as test,
|
||||
testComfySnapToGridGridSize
|
||||
} from '../fixtures/ComfyPage'
|
||||
import { type NodeReference } from '../fixtures/utils/litegraphUtils'
|
||||
import type { NodeReference } from '../fixtures/utils/litegraphUtils'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Item Interaction', () => {
|
||||
test('Can select/delete all items', async ({ comfyPage }) => {
|
||||
@@ -1012,6 +1017,8 @@ test.describe('Canvas Navigation', () => {
|
||||
test('Shift + mouse wheel should pan canvas horizontally', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.setSetting('Comfy.Canvas.MouseWheelScroll', 'panning')
|
||||
|
||||
await comfyPage.page.click('canvas')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 102 KiB |
@@ -2,6 +2,10 @@ import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Keybindings', () => {
|
||||
test('Should not trigger non-modifier keybinding when typing in input fields', async ({
|
||||
comfyPage
|
||||
|
||||
@@ -2,6 +2,10 @@ import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
function listenForEvent(): Promise<Event> {
|
||||
return new Promise<Event>((resolve) => {
|
||||
document.addEventListener('litegraph:canvas', (e) => resolve(e), {
|
||||
|
||||
@@ -2,6 +2,10 @@ import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Load Workflow in Media', () => {
|
||||
const fileNames = [
|
||||
'workflow.webp',
|
||||
|
||||
@@ -2,6 +2,10 @@ import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('LOD Threshold', () => {
|
||||
test('Should switch to low quality mode at correct zoom threshold', async ({
|
||||
comfyPage
|
||||
|
||||
@@ -178,6 +178,72 @@ test.describe('Menu', () => {
|
||||
await comfyPage.menu.topbar.triggerTopbarCommand(['ext', 'foo-command'])
|
||||
expect(await comfyPage.getVisibleToastCount()).toBe(1)
|
||||
})
|
||||
|
||||
test('Can navigate Theme menu and switch between Dark and Light themes', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const { topbar } = comfyPage.menu
|
||||
|
||||
// Take initial screenshot with default theme
|
||||
await comfyPage.attachScreenshot('theme-initial')
|
||||
|
||||
// Open the topbar menu
|
||||
const menu = await topbar.openTopbarMenu()
|
||||
await expect(menu).toBeVisible()
|
||||
|
||||
// Get theme menu items
|
||||
const {
|
||||
submenu: themeSubmenu,
|
||||
darkTheme: darkThemeItem,
|
||||
lightTheme: lightThemeItem
|
||||
} = await topbar.getThemeMenuItems()
|
||||
|
||||
await expect(darkThemeItem).toBeVisible()
|
||||
await expect(lightThemeItem).toBeVisible()
|
||||
|
||||
// Switch to Light theme
|
||||
await topbar.switchTheme('light')
|
||||
|
||||
// Verify menu stays open and Light theme shows as active
|
||||
await expect(menu).toBeVisible()
|
||||
await expect(themeSubmenu).toBeVisible()
|
||||
|
||||
// Check that Light theme is active
|
||||
expect(await topbar.isMenuItemActive(lightThemeItem)).toBe(true)
|
||||
|
||||
// Screenshot with light theme active
|
||||
await comfyPage.attachScreenshot('theme-menu-light-active')
|
||||
|
||||
// Verify ColorPalette setting is set to "light"
|
||||
expect(await comfyPage.getSetting('Comfy.ColorPalette')).toBe('light')
|
||||
|
||||
// Close menu to see theme change
|
||||
await topbar.closeTopbarMenu()
|
||||
|
||||
// Re-open menu and get theme items again
|
||||
await topbar.openTopbarMenu()
|
||||
const themeItems2 = await topbar.getThemeMenuItems()
|
||||
|
||||
// Switch back to Dark theme
|
||||
await topbar.switchTheme('dark')
|
||||
|
||||
// Verify menu stays open and Dark theme shows as active
|
||||
await expect(menu).toBeVisible()
|
||||
await expect(themeItems2.submenu).toBeVisible()
|
||||
|
||||
// Check that Dark theme is active and Light theme is not
|
||||
expect(await topbar.isMenuItemActive(themeItems2.darkTheme)).toBe(true)
|
||||
expect(await topbar.isMenuItemActive(themeItems2.lightTheme)).toBe(false)
|
||||
|
||||
// Screenshot with dark theme active
|
||||
await comfyPage.attachScreenshot('theme-menu-dark-active')
|
||||
|
||||
// Verify ColorPalette setting is set to "dark"
|
||||
expect(await comfyPage.getSetting('Comfy.ColorPalette')).toBe('dark')
|
||||
|
||||
// Close menu
|
||||
await topbar.closeTopbarMenu()
|
||||
})
|
||||
})
|
||||
|
||||
// Only test 'Top' to reduce test time.
|
||||
|
||||
@@ -4,6 +4,10 @@ import type { ComfyApp } from '../../src/scripts/app'
|
||||
import { NodeBadgeMode } from '../../src/types/nodeSource'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Node Badge', () => {
|
||||
test('Can add badge', async ({ comfyPage }) => {
|
||||
await comfyPage.page.evaluate(() => {
|
||||
|
||||
@@ -2,6 +2,10 @@ import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
// If an input is optional by node definition, it should be shown as
|
||||
// a hollow circle no matter what shape it was defined in the workflow JSON.
|
||||
test.describe('Optional input', () => {
|
||||
|
||||
@@ -46,7 +46,7 @@ test.describe('Node Help', () => {
|
||||
|
||||
// Click the help button in the selection toolbox
|
||||
const helpButton = comfyPage.selectionToolbox.locator(
|
||||
'button:has(.pi-question-circle)'
|
||||
'button[data-testid="info-button"]'
|
||||
)
|
||||
await expect(helpButton).toBeVisible()
|
||||
await helpButton.click()
|
||||
@@ -164,7 +164,7 @@ test.describe('Node Help', () => {
|
||||
|
||||
// Click help button
|
||||
const helpButton = comfyPage.page.locator(
|
||||
'.selection-toolbox button:has(.pi-question-circle)'
|
||||
'.selection-toolbox button[data-testid="info-button"]'
|
||||
)
|
||||
await helpButton.click()
|
||||
|
||||
@@ -194,7 +194,7 @@ test.describe('Node Help', () => {
|
||||
|
||||
// Click help button
|
||||
const helpButton = comfyPage.page.locator(
|
||||
'.selection-toolbox button:has(.pi-question-circle)'
|
||||
'.selection-toolbox button[data-testid="info-button"]'
|
||||
)
|
||||
await helpButton.click()
|
||||
|
||||
@@ -228,7 +228,7 @@ test.describe('Node Help', () => {
|
||||
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
|
||||
|
||||
const helpButton = comfyPage.page.locator(
|
||||
'.selection-toolbox button:has(.pi-question-circle)'
|
||||
'.selection-toolbox button[data-testid="info-button"]'
|
||||
)
|
||||
await helpButton.click()
|
||||
|
||||
@@ -276,7 +276,7 @@ test.describe('Node Help', () => {
|
||||
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
|
||||
|
||||
const helpButton = comfyPage.page.locator(
|
||||
'.selection-toolbox button:has(.pi-question-circle)'
|
||||
'.selection-toolbox button[data-testid="info-button"]'
|
||||
)
|
||||
await helpButton.click()
|
||||
|
||||
@@ -348,7 +348,7 @@ This is documentation for a custom node.
|
||||
}
|
||||
|
||||
const helpButton = comfyPage.page.locator(
|
||||
'.selection-toolbox button:has(.pi-question-circle)'
|
||||
'.selection-toolbox button[data-testid="info-button"]'
|
||||
)
|
||||
if (await helpButton.isVisible()) {
|
||||
await helpButton.click()
|
||||
@@ -389,7 +389,7 @@ This is documentation for a custom node.
|
||||
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
|
||||
|
||||
const helpButton = comfyPage.page.locator(
|
||||
'.selection-toolbox button:has(.pi-question-circle)'
|
||||
'.selection-toolbox button[data-testid="info-button"]'
|
||||
)
|
||||
await helpButton.click()
|
||||
|
||||
@@ -456,7 +456,7 @@ This is English documentation.
|
||||
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
|
||||
|
||||
const helpButton = comfyPage.page.locator(
|
||||
'.selection-toolbox button:has(.pi-question-circle)'
|
||||
'.selection-toolbox button[data-testid="info-button"]'
|
||||
)
|
||||
await helpButton.click()
|
||||
|
||||
@@ -479,7 +479,7 @@ This is English documentation.
|
||||
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
|
||||
|
||||
const helpButton = comfyPage.page.locator(
|
||||
'.selection-toolbox button:has(.pi-question-circle)'
|
||||
'.selection-toolbox button[data-testid="info-button"]'
|
||||
)
|
||||
await helpButton.click()
|
||||
|
||||
@@ -522,7 +522,7 @@ This is English documentation.
|
||||
await selectNodeWithPan(comfyPage, ksamplerNodes[0])
|
||||
|
||||
const helpButton = comfyPage.page.locator(
|
||||
'.selection-toolbox button:has(.pi-question-circle)'
|
||||
'.selection-toolbox button[data-testid="info-button"]'
|
||||
)
|
||||
await helpButton.click()
|
||||
|
||||
@@ -538,7 +538,7 @@ This is English documentation.
|
||||
|
||||
// Click help button again
|
||||
const helpButton2 = comfyPage.page.locator(
|
||||
'.selection-toolbox button:has(.pi-question-circle)'
|
||||
'.selection-toolbox button[data-testid="info-button"]'
|
||||
)
|
||||
await helpButton2.click()
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@ import {
|
||||
comfyPageFixture as test
|
||||
} from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Node search box', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.LinkRelease.Action', 'search box')
|
||||
|
||||
@@ -2,6 +2,10 @@ import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Note Node', () => {
|
||||
test('Can load node nodes', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('nodes/note_nodes')
|
||||
|
||||
@@ -3,6 +3,10 @@ import { expect } from '@playwright/test'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
import type { NodeReference } from '../fixtures/utils/litegraphUtils'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Primitive Node', () => {
|
||||
test('Can load with correct size', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('primitive/primitive_node')
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { ComfyPage, comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
import type { ComfyPage } from '../fixtures/ComfyPage'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Remote COMBO Widget', () => {
|
||||
const mockOptions = ['d', 'c', 'b', 'a']
|
||||
@@ -190,7 +191,9 @@ test.describe('Remote COMBO Widget', () => {
|
||||
await comfyPage.page.keyboard.press('Control+A')
|
||||
|
||||
await expect(
|
||||
comfyPage.page.locator('.selection-toolbox .pi-refresh')
|
||||
comfyPage.page.locator(
|
||||
'.selection-toolbox button[data-testid="refresh-button"]'
|
||||
)
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ test.describe('Reroute Node', () => {
|
||||
|
||||
test.describe('LiteGraph Native Reroute Node', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
await comfyPage.setSetting('LiteGraph.Reroute.SplineOffset', 80)
|
||||
})
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
@@ -3,6 +3,10 @@ import { expect } from '@playwright/test'
|
||||
import { NodeBadgeMode } from '../../src/types/nodeSource'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Canvas Right Click Menu', () => {
|
||||
test('Can add node', async ({ comfyPage }) => {
|
||||
await comfyPage.rightClickCanvas()
|
||||
|
||||
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
@@ -4,6 +4,9 @@ import { comfyPageFixture } from '../fixtures/ComfyPage'
|
||||
|
||||
const test = comfyPageFixture
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
const BLUE_COLOR = 'rgb(51, 51, 85)'
|
||||
const RED_COLOR = 'rgb(85, 51, 51)'
|
||||
|
||||
@@ -149,7 +152,7 @@ test.describe('Selection Toolbox', () => {
|
||||
// Node should have the selected color class/style
|
||||
// Note: Exact verification method depends on how color is applied to nodes
|
||||
const selectedNode = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
|
||||
expect(selectedNode.getProperty('color')).not.toBeNull()
|
||||
expect(await selectedNode.getProperty('color')).not.toBeNull()
|
||||
})
|
||||
|
||||
test('color picker shows current color of selected nodes', async ({
|
||||
|
||||
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 96 KiB |
181
browser_tests/tests/selectionToolboxSubmenus.spec.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Selection Toolbox - More Options Submenus', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
|
||||
await comfyPage.loadWorkflow('nodes/single_ksampler')
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.selectNodes(['KSampler'])
|
||||
await comfyPage.nextFrame()
|
||||
})
|
||||
|
||||
const openMoreOptions = async (comfyPage: any) => {
|
||||
const ksamplerNodes = await comfyPage.getNodeRefsByTitle('KSampler')
|
||||
if (ksamplerNodes.length === 0) {
|
||||
throw new Error('No KSampler nodes found')
|
||||
}
|
||||
|
||||
// Drag the KSampler to the center of the screen
|
||||
const nodePos = await ksamplerNodes[0].getPosition()
|
||||
const viewportSize = comfyPage.page.viewportSize()
|
||||
const centerX = viewportSize.width / 3
|
||||
const centerY = viewportSize.height / 2
|
||||
await comfyPage.dragAndDrop(
|
||||
{ x: nodePos.x, y: nodePos.y },
|
||||
{ x: centerX, y: centerY }
|
||||
)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await ksamplerNodes[0].click('title')
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.page.waitForTimeout(500)
|
||||
|
||||
await expect(comfyPage.page.locator('.selection-toolbox')).toBeVisible({
|
||||
timeout: 5000
|
||||
})
|
||||
|
||||
const moreOptionsBtn = comfyPage.page.locator(
|
||||
'[data-testid="more-options-button"]'
|
||||
)
|
||||
await expect(moreOptionsBtn).toBeVisible({ timeout: 3000 })
|
||||
|
||||
await comfyPage.page.click('[data-testid="more-options-button"]')
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const menuOptionsVisible = await comfyPage.page
|
||||
.getByText('Rename')
|
||||
.isVisible({ timeout: 2000 })
|
||||
.catch(() => false)
|
||||
if (menuOptionsVisible) {
|
||||
return
|
||||
}
|
||||
|
||||
await moreOptionsBtn.click({ force: true })
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.page.waitForTimeout(2000)
|
||||
|
||||
const menuOptionsVisibleAfterClick = await comfyPage.page
|
||||
.getByText('Rename')
|
||||
.isVisible({ timeout: 2000 })
|
||||
.catch(() => false)
|
||||
if (menuOptionsVisibleAfterClick) {
|
||||
return
|
||||
}
|
||||
|
||||
throw new Error('Could not open More Options menu - popover not showing')
|
||||
}
|
||||
|
||||
test('opens Node Info from More Options menu', async ({ comfyPage }) => {
|
||||
await openMoreOptions(comfyPage)
|
||||
const nodeInfoButton = comfyPage.page.getByText('Node Info', {
|
||||
exact: true
|
||||
})
|
||||
await expect(nodeInfoButton).toBeVisible()
|
||||
await nodeInfoButton.click()
|
||||
await comfyPage.nextFrame()
|
||||
})
|
||||
|
||||
test('changes node shape via Shape submenu', async ({ comfyPage }) => {
|
||||
const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
|
||||
const initialShape = await nodeRef.getProperty<number>('shape')
|
||||
|
||||
await openMoreOptions(comfyPage)
|
||||
await comfyPage.page.getByText('Shape', { exact: true }).click()
|
||||
await expect(comfyPage.page.getByText('Box', { exact: true })).toBeVisible({
|
||||
timeout: 5000
|
||||
})
|
||||
await comfyPage.page.getByText('Box', { exact: true }).click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const newShape = await nodeRef.getProperty<number>('shape')
|
||||
expect(newShape).not.toBe(initialShape)
|
||||
expect(newShape).toBe(1)
|
||||
})
|
||||
|
||||
test('changes node color via Color submenu swatch', async ({ comfyPage }) => {
|
||||
const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
|
||||
const initialColor = await nodeRef.getProperty<string | undefined>('color')
|
||||
|
||||
await openMoreOptions(comfyPage)
|
||||
await comfyPage.page.getByText('Color', { exact: true }).click()
|
||||
const blueSwatch = comfyPage.page.locator('[title="Blue"]')
|
||||
await expect(blueSwatch.first()).toBeVisible({ timeout: 5000 })
|
||||
await blueSwatch.first().click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const newColor = await nodeRef.getProperty<string | undefined>('color')
|
||||
expect(newColor).toBe('#223')
|
||||
if (initialColor) {
|
||||
expect(newColor).not.toBe(initialColor)
|
||||
}
|
||||
})
|
||||
|
||||
test('renames a node using Rename action', async ({ comfyPage }) => {
|
||||
const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
|
||||
await openMoreOptions(comfyPage)
|
||||
await comfyPage.page
|
||||
.getByText('Rename', { exact: true })
|
||||
.click({ force: true })
|
||||
const input = comfyPage.page.locator(
|
||||
'.group-title-editor.node-title-editor .editable-text input'
|
||||
)
|
||||
await expect(input).toBeVisible()
|
||||
await input.fill('RenamedNode')
|
||||
await input.press('Enter')
|
||||
await comfyPage.nextFrame()
|
||||
const newTitle = await nodeRef.getProperty<string>('title')
|
||||
expect(newTitle).toBe('RenamedNode')
|
||||
})
|
||||
|
||||
test('closes More Options menu when clicking outside', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await openMoreOptions(comfyPage)
|
||||
await expect(
|
||||
comfyPage.page.getByText('Rename', { exact: true })
|
||||
).toBeVisible({ timeout: 5000 })
|
||||
|
||||
await comfyPage.page
|
||||
.locator('#graph-canvas')
|
||||
.click({ position: { x: 0, y: 50 }, force: true })
|
||||
await comfyPage.nextFrame()
|
||||
await expect(
|
||||
comfyPage.page.getByText('Rename', { exact: true })
|
||||
).not.toBeVisible()
|
||||
})
|
||||
|
||||
test('closes More Options menu when clicking the button again (toggle)', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await openMoreOptions(comfyPage)
|
||||
await expect(
|
||||
comfyPage.page.getByText('Rename', { exact: true })
|
||||
).toBeVisible({ timeout: 5000 })
|
||||
|
||||
await comfyPage.page.evaluate(() => {
|
||||
const btn = document.querySelector('[data-testid="more-options-button"]')
|
||||
if (btn) {
|
||||
const event = new MouseEvent('click', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
view: window,
|
||||
detail: 1
|
||||
})
|
||||
btn.dispatchEvent(event)
|
||||
}
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.page.waitForTimeout(500)
|
||||
|
||||
await expect(
|
||||
comfyPage.page.getByText('Rename', { exact: true })
|
||||
).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -160,7 +160,9 @@ test.describe.skip('Queue sidebar', () => {
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.nextFrame()
|
||||
expect(comfyPage.menu.queueTab.getGalleryImage(firstImage)).toBeVisible()
|
||||
await expect(
|
||||
comfyPage.menu.queueTab.getGalleryImage(firstImage)
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('maintains active gallery item when new tasks are added', async ({
|
||||
@@ -174,7 +176,9 @@ test.describe.skip('Queue sidebar', () => {
|
||||
const newTask = comfyPage.menu.queueTab.tasks.getByAltText(newImage)
|
||||
await newTask.waitFor({ state: 'visible' })
|
||||
// The active gallery item should still be the initial image
|
||||
expect(comfyPage.menu.queueTab.getGalleryImage(firstImage)).toBeVisible()
|
||||
await expect(
|
||||
comfyPage.menu.queueTab.getGalleryImage(firstImage)
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test.describe('Gallery navigation', () => {
|
||||
@@ -196,7 +200,9 @@ test.describe.skip('Queue sidebar', () => {
|
||||
delay: 256
|
||||
})
|
||||
await comfyPage.nextFrame()
|
||||
expect(comfyPage.menu.queueTab.getGalleryImage(end)).toBeVisible()
|
||||
await expect(
|
||||
comfyPage.menu.queueTab.getGalleryImage(end)
|
||||
).toBeVisible()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -187,6 +187,7 @@ test.describe('Workflows sidebar', () => {
|
||||
|
||||
test('Can save workflow as with same name', async ({ comfyPage }) => {
|
||||
await comfyPage.menu.topbar.saveWorkflow('workflow5.json')
|
||||
await comfyPage.nextFrame()
|
||||
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
|
||||
'workflow5.json'
|
||||
])
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Page, expect } from '@playwright/test'
|
||||
import type { Page } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
@@ -79,6 +80,12 @@ test.describe('Templates', () => {
|
||||
// Load a template
|
||||
await comfyPage.executeCommand('Comfy.BrowseTemplates')
|
||||
await expect(comfyPage.templates.content).toBeVisible()
|
||||
|
||||
await comfyPage.page
|
||||
.locator(
|
||||
'nav > div:nth-child(2) > div > span:has-text("Getting Started")'
|
||||
)
|
||||
.click()
|
||||
await comfyPage.templates.loadTemplate('default')
|
||||
await expect(comfyPage.templates.content).toBeHidden()
|
||||
|
||||
@@ -101,48 +108,72 @@ test.describe('Templates', () => {
|
||||
expect(await comfyPage.templates.content.isVisible()).toBe(true)
|
||||
})
|
||||
|
||||
test('Uses title field as fallback when the key is not found in locales', async ({
|
||||
test('Uses proper locale files for templates', async ({ comfyPage }) => {
|
||||
// Set locale to French before opening templates
|
||||
await comfyPage.setSetting('Comfy.Locale', 'fr')
|
||||
|
||||
// Load the templates dialog and wait for the French index file request
|
||||
const requestPromise = comfyPage.page.waitForRequest(
|
||||
'**/templates/index.fr.json'
|
||||
)
|
||||
|
||||
await comfyPage.executeCommand('Comfy.BrowseTemplates')
|
||||
|
||||
const request = await requestPromise
|
||||
|
||||
// Verify French index was requested
|
||||
expect(request.url()).toContain('templates/index.fr.json')
|
||||
|
||||
await expect(comfyPage.templates.content).toBeVisible()
|
||||
})
|
||||
|
||||
test('Falls back to English templates when locale file not found', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Capture request for the index.json
|
||||
await comfyPage.page.route('**/templates/index.json', async (route, _) => {
|
||||
// Add a new template that won't have a translation pre-generated
|
||||
const response = [
|
||||
{
|
||||
moduleName: 'default',
|
||||
title: 'FALLBACK CATEGORY',
|
||||
type: 'image',
|
||||
templates: [
|
||||
{
|
||||
name: 'unknown_key_has_no_translation_available',
|
||||
title: 'FALLBACK TEMPLATE NAME',
|
||||
mediaType: 'image',
|
||||
mediaSubtype: 'webp',
|
||||
description: 'No translations found'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
// Set locale to a language that doesn't have a template file
|
||||
await comfyPage.setSetting('Comfy.Locale', 'de') // German - no index.de.json exists
|
||||
|
||||
// Wait for the German request (expected to 404)
|
||||
const germanRequestPromise = comfyPage.page.waitForRequest(
|
||||
'**/templates/index.de.json'
|
||||
)
|
||||
|
||||
// Wait for the fallback English request
|
||||
const englishRequestPromise = comfyPage.page.waitForRequest(
|
||||
'**/templates/index.json'
|
||||
)
|
||||
|
||||
// Intercept the German file to simulate a 404
|
||||
await comfyPage.page.route('**/templates/index.de.json', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify(response),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store'
|
||||
}
|
||||
status: 404,
|
||||
headers: { 'Content-Type': 'text/plain' },
|
||||
body: 'Not Found'
|
||||
})
|
||||
})
|
||||
|
||||
// Allow the English index to load normally
|
||||
await comfyPage.page.route('**/templates/index.json', (route) =>
|
||||
route.continue()
|
||||
)
|
||||
|
||||
// Load the templates dialog
|
||||
await comfyPage.executeCommand('Comfy.BrowseTemplates')
|
||||
await expect(comfyPage.templates.content).toBeVisible()
|
||||
|
||||
// Expect the title to be used as fallback for template cards
|
||||
// Verify German was requested first, then English as fallback
|
||||
const germanRequest = await germanRequestPromise
|
||||
const englishRequest = await englishRequestPromise
|
||||
|
||||
expect(germanRequest.url()).toContain('templates/index.de.json')
|
||||
expect(englishRequest.url()).toContain('templates/index.json')
|
||||
|
||||
// Verify English titles are shown as fallback
|
||||
await expect(
|
||||
comfyPage.templates.content.getByText('FALLBACK TEMPLATE NAME')
|
||||
comfyPage.templates.content.getByRole('heading', {
|
||||
name: 'Image Generation'
|
||||
})
|
||||
).toBeVisible()
|
||||
|
||||
// Expect the title to be used as fallback for the template categories
|
||||
await expect(comfyPage.page.getByLabel('FALLBACK CATEGORY')).toBeVisible()
|
||||
})
|
||||
|
||||
test('template cards are dynamically sized and responsive', async ({
|
||||
@@ -150,46 +181,33 @@ test.describe('Templates', () => {
|
||||
}) => {
|
||||
// Open templates dialog
|
||||
await comfyPage.executeCommand('Comfy.BrowseTemplates')
|
||||
await expect(comfyPage.templates.content).toBeVisible()
|
||||
await comfyPage.templates.content.waitFor({ state: 'visible' })
|
||||
|
||||
// Wait for at least one template card to appear
|
||||
await expect(comfyPage.page.locator('.template-card').first()).toBeVisible({
|
||||
timeout: 5000
|
||||
})
|
||||
const templateGrid = comfyPage.page.locator(
|
||||
'[data-testid="template-workflows-content"]'
|
||||
)
|
||||
const nav = comfyPage.page
|
||||
.locator('header')
|
||||
.filter({ hasText: 'Templates' })
|
||||
|
||||
// Take snapshot of the template grid
|
||||
const templateGrid = comfyPage.templates.content.locator('.grid').first()
|
||||
const cardCount = await comfyPage.page
|
||||
.locator('[data-testid^="template-workflow-"]')
|
||||
.count()
|
||||
expect(cardCount).toBeGreaterThan(0)
|
||||
await expect(templateGrid).toBeVisible()
|
||||
await expect(templateGrid).toHaveScreenshot('template-grid-desktop.png')
|
||||
await expect(nav).toBeVisible() // Nav should be visible at desktop size
|
||||
|
||||
// Check cards at mobile viewport size
|
||||
await comfyPage.page.setViewportSize({ width: 640, height: 800 })
|
||||
const mobileSize = { width: 640, height: 800 }
|
||||
await comfyPage.page.setViewportSize(mobileSize)
|
||||
expect(cardCount).toBeGreaterThan(0)
|
||||
await expect(templateGrid).toBeVisible()
|
||||
await expect(templateGrid).toHaveScreenshot('template-grid-mobile.png')
|
||||
await expect(nav).not.toBeVisible() // Nav should collapse at mobile size
|
||||
|
||||
// Check cards at tablet size
|
||||
await comfyPage.page.setViewportSize({ width: 1024, height: 800 })
|
||||
const tabletSize = { width: 1024, height: 800 }
|
||||
await comfyPage.page.setViewportSize(tabletSize)
|
||||
expect(cardCount).toBeGreaterThan(0)
|
||||
await expect(templateGrid).toBeVisible()
|
||||
await expect(templateGrid).toHaveScreenshot('template-grid-tablet.png')
|
||||
})
|
||||
|
||||
test('hover effects work on template cards', async ({ comfyPage }) => {
|
||||
// Open templates dialog
|
||||
await comfyPage.executeCommand('Comfy.BrowseTemplates')
|
||||
await expect(comfyPage.templates.content).toBeVisible()
|
||||
|
||||
// Get a template card
|
||||
const firstCard = comfyPage.page.locator('.template-card').first()
|
||||
await expect(firstCard).toBeVisible({ timeout: 5000 })
|
||||
|
||||
// Take snapshot before hover
|
||||
await expect(firstCard).toHaveScreenshot('template-card-before-hover.png')
|
||||
|
||||
// Hover over the card
|
||||
await firstCard.hover()
|
||||
|
||||
// Take snapshot after hover to verify hover effect
|
||||
await expect(firstCard).toHaveScreenshot('template-card-after-hover.png')
|
||||
await expect(nav).toBeVisible() // Nav should be visible at tablet size
|
||||
})
|
||||
|
||||
test('template cards descriptions adjust height dynamically', async ({
|
||||
@@ -256,21 +274,42 @@ test.describe('Templates', () => {
|
||||
await comfyPage.executeCommand('Comfy.BrowseTemplates')
|
||||
await expect(comfyPage.templates.content).toBeVisible()
|
||||
|
||||
// Verify cards are visible with varying content lengths
|
||||
// Wait for cards to load
|
||||
await expect(
|
||||
comfyPage.page.getByText('This is a short description.')
|
||||
).toBeVisible({ timeout: 5000 })
|
||||
await expect(
|
||||
comfyPage.page.getByText('This is a medium length description')
|
||||
).toBeVisible({ timeout: 5000 })
|
||||
await expect(
|
||||
comfyPage.page.getByText('This is a much longer description')
|
||||
comfyPage.page.locator(
|
||||
'[data-testid="template-workflow-short-description"]'
|
||||
)
|
||||
).toBeVisible({ timeout: 5000 })
|
||||
|
||||
// Take snapshot of a grid with specific cards
|
||||
const templateGrid = comfyPage.templates.content
|
||||
.locator('.grid:has-text("Short Description")')
|
||||
.first()
|
||||
// Verify all three cards with different descriptions are visible
|
||||
const shortDescCard = comfyPage.page.locator(
|
||||
'[data-testid="template-workflow-short-description"]'
|
||||
)
|
||||
const mediumDescCard = comfyPage.page.locator(
|
||||
'[data-testid="template-workflow-medium-description"]'
|
||||
)
|
||||
const longDescCard = comfyPage.page.locator(
|
||||
'[data-testid="template-workflow-long-description"]'
|
||||
)
|
||||
|
||||
await expect(shortDescCard).toBeVisible()
|
||||
await expect(mediumDescCard).toBeVisible()
|
||||
await expect(longDescCard).toBeVisible()
|
||||
|
||||
// Verify descriptions are visible and have line-clamp class
|
||||
// The description is in a p tag with text-muted class
|
||||
const shortDesc = shortDescCard.locator('p.text-muted.line-clamp-2')
|
||||
const mediumDesc = mediumDescCard.locator('p.text-muted.line-clamp-2')
|
||||
const longDesc = longDescCard.locator('p.text-muted.line-clamp-2')
|
||||
|
||||
await expect(shortDesc).toContainText('short description')
|
||||
await expect(mediumDesc).toContainText('medium length description')
|
||||
await expect(longDesc).toContainText('much longer description')
|
||||
|
||||
// Verify grid layout maintains consistency
|
||||
const templateGrid = comfyPage.page.locator(
|
||||
'[data-testid="template-workflows-content"]'
|
||||
)
|
||||
await expect(templateGrid).toBeVisible()
|
||||
await expect(templateGrid).toHaveScreenshot(
|
||||
'template-grid-varying-content.png'
|
||||
|
||||
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 175 KiB After Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |