Compare commits
145 Commits
vue-nodes/
...
bl-test
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c004a2b8bd | ||
|
|
d0aee031e9 | ||
|
|
cec1de0147 | ||
|
|
b4976c1ddc | ||
|
|
1611c7a224 | ||
|
|
d01081dab4 | ||
|
|
e5d4d07d32 | ||
|
|
f086377307 | ||
|
|
687b9e659c | ||
|
|
da0d51311b | ||
|
|
e314d9cbd9 | ||
|
|
95baf8d2f1 | ||
|
|
f951e07cea | ||
|
|
023e466dba | ||
|
|
abd6823744 | ||
|
|
c4c0e52e64 | ||
|
|
295332dc46 | ||
|
|
5c498348b8 | ||
|
|
8133bd4b7b | ||
|
|
fd12591756 | ||
|
|
b3c939ff15 | ||
|
|
0801778f60 | ||
|
|
8ffe63f54e | ||
|
|
893409dfc8 | ||
|
|
df2fda6077 | ||
|
|
4f5bbe0605 | ||
|
|
a975e50f1b | ||
|
|
a17c74fa0c | ||
|
|
5e625a5002 | ||
|
|
002fac0232 | ||
|
|
7e115543fa | ||
|
|
80d75bb164 | ||
|
|
d59885839a | ||
|
|
cbb0f765b8 | ||
|
|
726a2fbbc9 | ||
|
|
553b5aa02b | ||
|
|
2ff0d951ed | ||
|
|
1f88925144 | ||
|
|
250433a91a | ||
|
|
eb664f47af | ||
|
|
bc85d4e87b | ||
|
|
7585444ce6 | ||
|
|
a886798a10 | ||
|
|
37975e4eac | ||
|
|
a41b8a6d4f | ||
|
|
b264685052 | ||
|
|
78d0ea6fa5 | ||
|
|
ea4e57b602 | ||
|
|
4789d86fe8 | ||
|
|
09e7d1040e | ||
|
|
dfa1cbba4f | ||
|
|
08220d50d9 | ||
|
|
045232a99b | ||
|
|
fb07941700 | ||
|
|
6866e1277a | ||
|
|
ff5d0923ca | ||
|
|
6b59f839e0 | ||
|
|
6a01b08ebf | ||
|
|
ede43c5e5c | ||
|
|
0483630f82 | ||
|
|
15cffe9d9e | ||
|
|
f5b949762d | ||
|
|
6786d8e4fb | ||
|
|
71ca28a46f | ||
|
|
4ff18fd7f0 | ||
|
|
45a46be513 | ||
|
|
631746939a | ||
|
|
4f8e820c51 | ||
|
|
b8d8193a38 | ||
|
|
2b57291756 | ||
|
|
62897c669b | ||
|
|
9918914b9d | ||
|
|
e601bcb300 | ||
|
|
853038859b | ||
|
|
186e065528 | ||
|
|
237e807fc9 | ||
|
|
27ab355f9c | ||
|
|
4c8c4a1ad4 | ||
|
|
e3bb29ceb8 | ||
|
|
ca312fd1ea | ||
|
|
bbff9c8217 | ||
|
|
6349ceee6c | ||
|
|
8c6ee026c0 | ||
|
|
668500cfa5 | ||
|
|
07fbe7267e | ||
|
|
fe09f88ea3 | ||
|
|
96a663704f | ||
|
|
11cb525545 | ||
|
|
4eb13ca17c | ||
|
|
6017fc43a6 | ||
|
|
6e2d86520b | ||
|
|
6e2a3a0d07 | ||
|
|
c7325c4da9 | ||
|
|
d146a7896a | ||
|
|
6b166a9d2f | ||
|
|
c145fd9df1 | ||
|
|
c04af09956 | ||
|
|
ac107b45ea | ||
|
|
f6a115e182 | ||
|
|
aa7a99f4e3 | ||
|
|
8804755ffa | ||
|
|
f28ebcac19 | ||
|
|
bfef8d7d4b | ||
|
|
90bf8dc74a | ||
|
|
6f878abea4 | ||
|
|
b1917c6469 | ||
|
|
bf054113f8 | ||
|
|
4604bbd669 | ||
|
|
77ee20597f | ||
|
|
8ae50d140d | ||
|
|
63f194b394 | ||
|
|
2b8abca6d8 | ||
|
|
48d01745cd | ||
|
|
90b1b47dd0 | ||
|
|
0e01ca0a98 | ||
|
|
cf093d02a7 | ||
|
|
1845708ddb | ||
|
|
c588f2f457 | ||
|
|
29ad47d20f | ||
|
|
9aeeab9a9f | ||
|
|
6645b3917f | ||
|
|
4ec6223189 | ||
|
|
dfcbbec2b9 | ||
|
|
c051c3a507 | ||
|
|
54cbf42a84 | ||
|
|
ef7575b8d6 | ||
|
|
68845ce33a | ||
|
|
46f4ce3890 | ||
|
|
5df037dfc4 | ||
|
|
3bc25b7aeb | ||
|
|
08fe2829d4 | ||
|
|
e70ddea684 | ||
|
|
038f86fe84 | ||
|
|
cb0dab6cdc | ||
|
|
568be0c44c | ||
|
|
6ea021d595 | ||
|
|
7245213ed6 | ||
|
|
b72e22f6be | ||
|
|
5f045b335d | ||
|
|
44e470488d | ||
|
|
ca220440b2 | ||
|
|
2e64c64ac7 | ||
|
|
d561f315d3 | ||
|
|
080334754c | ||
|
|
169d7404fe |
@@ -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
|
||||
|
||||
|
||||
@@ -33,4 +33,3 @@ DISABLE_VUE_PLUGINS=false
|
||||
# Algolia credentials required for developing with the new custom node manager.
|
||||
ALGOLIA_APP_ID=4E0RO38HS8
|
||||
ALGOLIA_API_KEY=684d998c36b67a9a9fce8fc2d8860579
|
||||
|
||||
|
||||
@@ -7,3 +7,21 @@ 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
|
||||
|
||||
110
.github/workflows/backport.yaml
vendored
@@ -4,10 +4,25 @@ on:
|
||||
pull_request_target:
|
||||
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:
|
||||
@@ -29,7 +73,7 @@ jobs:
|
||||
id: check-existing
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
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')
|
||||
@@ -39,6 +83,13 @@ jobs:
|
||||
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
|
||||
@@ -50,8 +101,17 @@ jobs:
|
||||
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
|
||||
@@ -75,12 +135,20 @@ jobs:
|
||||
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}"
|
||||
@@ -133,11 +201,18 @@ jobs:
|
||||
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}"
|
||||
|
||||
@@ -166,9 +241,16 @@ jobs:
|
||||
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 }}
|
||||
|
||||
9
.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:
|
||||
|
||||
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
|
||||
|
||||
25
.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
|
||||
@@ -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 \
|
||||
npx 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
|
||||
npx playwright merge-reports --reporter=html ./all-blob-reports
|
||||
# Generate JSON report separately with explicit output path
|
||||
PLAYWRIGHT_JSON_OUTPUT_NAME=playwright-report/report.json \
|
||||
npx playwright merge-reports --reporter=json ./all-blob-reports
|
||||
working-directory: ComfyUI_frontend
|
||||
|
||||
- name: Upload HTML report
|
||||
|
||||
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
|
||||
|
||||
6
.gitignore
vendored
@@ -44,6 +44,7 @@ components.d.ts
|
||||
tests-ui/data/*
|
||||
tests-ui/ComfyUI_examples
|
||||
tests-ui/workflows/examples
|
||||
coverage/
|
||||
|
||||
# Browser tests
|
||||
/test-results/
|
||||
@@ -51,6 +52,7 @@ tests-ui/workflows/examples
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
browser_tests/**/*-win32.png
|
||||
browser_tests/local/
|
||||
|
||||
.env
|
||||
|
||||
@@ -77,8 +79,8 @@ vite.config.mts.timestamp-*.mjs
|
||||
*storybook.log
|
||||
storybook-static
|
||||
|
||||
|
||||
|
||||
# MCP Servers
|
||||
.playwright-mcp/*
|
||||
|
||||
.nx/cache
|
||||
.nx/workspace-data
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -15,21 +15,32 @@ 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: {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -16,9 +16,14 @@ 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:
|
||||
|
||||
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}
|
||||
@@ -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 {
|
||||
@@ -1421,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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
110
browser_tests/fixtures/VueNodeHelpers.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* 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 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,4 +1,5 @@
|
||||
import { Locator, Page, expect } from '@playwright/test'
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
export class Topbar {
|
||||
private readonly menuLocator: Locator
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ComfyPage } from '../fixtures/ComfyPage'
|
||||
import {
|
||||
ComfyPage,
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../fixtures/ComfyPage'
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 180 KiB After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 169 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 168 KiB After Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 165 KiB After Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 100 KiB |
@@ -1,4 +1,5 @@
|
||||
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'
|
||||
@@ -36,6 +37,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')
|
||||
|
||||
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 95 KiB |
@@ -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'
|
||||
|
||||
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 100 KiB |
@@ -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'
|
||||
import type { NodeReference } from '../fixtures/utils/litegraphUtils'
|
||||
|
||||
test.describe('Group Node', () => {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
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.describe('Item Interaction', () => {
|
||||
test('Can select/delete all items', async ({ comfyPage }) => {
|
||||
@@ -1012,6 +1013,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: 106 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 102 KiB After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 105 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 9.1 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 99 KiB |