Compare commits
169 Commits
graphMutat
...
bl-drop-ca
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f338480e8b | ||
|
|
a71b99d6fc | ||
|
|
5e3c91faac | ||
|
|
066a755a5b | ||
|
|
21cc208e4b | ||
|
|
a2c7db9dc2 | ||
|
|
840f7f04fa | ||
|
|
042c2caa88 | ||
|
|
cd8b5b5d50 | ||
|
|
a25d89881b | ||
|
|
5b866b74b8 | ||
|
|
f13a45c781 | ||
|
|
1c11dcc5af | ||
|
|
e8b9f8f4bc | ||
|
|
b0d2b44dad | ||
|
|
976b1a6bbd | ||
|
|
856eb446a5 | ||
|
|
992ba9d585 | ||
|
|
4b9d448b6c | ||
|
|
0ac1074a25 | ||
|
|
45f5ec15c2 | ||
|
|
a519e681dd | ||
|
|
fdb15e1296 | ||
|
|
d444b2c285 | ||
|
|
46ad1318e5 | ||
|
|
da332ed75d | ||
|
|
75d31f9d57 | ||
|
|
edcbcdfa84 | ||
|
|
247e3950eb | ||
|
|
8da5ae3af6 | ||
|
|
4b95ef94df | ||
|
|
1f4a52ca3e | ||
|
|
f2e925de62 | ||
|
|
5f7c7ca949 | ||
|
|
62441fa0f9 | ||
|
|
c96f719f91 | ||
|
|
9f19d8fb4b | ||
|
|
d954336973 | ||
|
|
49f373c46f | ||
|
|
9678a87846 | ||
|
|
3a9365af13 | ||
|
|
3ee0d394ca | ||
|
|
18b4f56158 | ||
|
|
6a70152220 | ||
|
|
9de27adfca | ||
|
|
0e33672443 | ||
|
|
5c1e00ff8e | ||
|
|
78d585eca0 | ||
|
|
a6600aa109 | ||
|
|
ecc5bed87d | ||
|
|
1ca3d75aaf | ||
|
|
c033e9e4d7 | ||
|
|
0627a71fb6 | ||
|
|
ac93a6ba3f | ||
|
|
23f3e17d52 | ||
|
|
0f46452b70 | ||
|
|
76c718e2ee | ||
|
|
961af8731e | ||
|
|
703de3e669 | ||
|
|
415ebfd67b | ||
|
|
4f6eaea257 | ||
|
|
97542efc9b | ||
|
|
13ce23399c | ||
|
|
a0c06bd723 | ||
|
|
839d8a5f47 | ||
|
|
3fc17ebdac | ||
|
|
0db2a2c03e | ||
|
|
b96bf3871c | ||
|
|
bc4549244e | ||
|
|
f3e68804e8 | ||
|
|
1749cfa678 | ||
|
|
0fea54c542 | ||
|
|
cd7666e3bc | ||
|
|
8d1261133a | ||
|
|
0919856a05 | ||
|
|
b0f81b2245 | ||
|
|
c05011594d | ||
|
|
6449d26cee | ||
|
|
9b39835cd1 | ||
|
|
57810b9350 | ||
|
|
80cabc61ee | ||
|
|
76dd935b35 | ||
|
|
99aaa4e4cb | ||
|
|
f7f3240100 | ||
|
|
e9ffce468d | ||
|
|
381d97a982 | ||
|
|
88cd60f0c5 | ||
|
|
a2be36a0bc | ||
|
|
e4022c455a | ||
|
|
c6e50d8f1b | ||
|
|
65ec322100 | ||
|
|
f99d8c1a92 | ||
|
|
8eec7fb80e | ||
|
|
d78029697c | ||
|
|
f34890296b | ||
|
|
e879bd5290 | ||
|
|
9d32b4cf06 | ||
|
|
0aa971bed9 | ||
|
|
6685e004c0 | ||
|
|
c004a2b8bd | ||
|
|
e136b89fae | ||
|
|
d0aee031e9 | ||
|
|
3f4a8060de | ||
|
|
cec1de0147 | ||
|
|
b4976c1ddc | ||
|
|
1611c7a224 | ||
|
|
d01081dab4 | ||
|
|
20d136dff3 | ||
|
|
e7f0ee40e4 | ||
|
|
bef712ed4f | ||
|
|
e5d4d07d32 | ||
|
|
263b28097d | ||
|
|
19c538c36c | ||
|
|
f086377307 | ||
|
|
687b9e659c | ||
|
|
da0d51311b | ||
|
|
e314d9cbd9 | ||
|
|
95baf8d2f1 | ||
|
|
f951e07cea | ||
|
|
023e466dba | ||
|
|
abd6823744 | ||
|
|
c4c0e52e64 | ||
|
|
295332dc46 | ||
|
|
5c498348b8 | ||
|
|
b99d70d0f0 | ||
|
|
9d668a154e | ||
|
|
c227d60626 | ||
|
|
70651dcde0 | ||
|
|
8133bd4b7b | ||
|
|
fd12591756 | ||
|
|
b3c939ff15 | ||
|
|
0801778f60 | ||
|
|
8ffe63f54e | ||
|
|
893409dfc8 | ||
|
|
22a1c61208 | ||
|
|
369da53743 | ||
|
|
48f5087116 | ||
|
|
df2fda6077 | ||
|
|
f624940e16 | ||
|
|
5c6c21cdf2 | ||
|
|
e3e1d2e8e6 | ||
|
|
939cbe0899 | ||
|
|
9a18d37019 | ||
|
|
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 |
@@ -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
|
||||
|
||||
|
||||
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
|
||||
|
||||
109
.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,10 +201,18 @@ jobs:
|
||||
if: steps.check-existing.outputs.skip != 'true' && steps.backport.outputs.success
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
|
||||
PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
|
||||
run: |
|
||||
# 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}"
|
||||
|
||||
@@ -165,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}"
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
18
.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
|
||||
@@ -232,7 +230,7 @@ jobs:
|
||||
run: |
|
||||
# Run tests with both HTML and JSON reporters
|
||||
PLAYWRIGHT_JSON_OUTPUT_NAME=playwright-report/report.json \
|
||||
npx playwright test --project=${{ matrix.browser }} \
|
||||
pnpm exec playwright test --project=${{ matrix.browser }} \
|
||||
--reporter=list \
|
||||
--reporter=html \
|
||||
--reporter=json
|
||||
@@ -283,10 +281,10 @@ jobs:
|
||||
- name: Merge into HTML Report
|
||||
run: |
|
||||
# Generate HTML report
|
||||
npx playwright merge-reports --reporter=html ./all-blob-reports
|
||||
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 \
|
||||
npx playwright merge-reports --reporter=json ./all-blob-reports
|
||||
pnpm exec playwright merge-reports --reporter=json ./all-blob-reports
|
||||
working-directory: ComfyUI_frontend
|
||||
|
||||
- name: Upload HTML report
|
||||
|
||||
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: |
|
||||
|
||||
7
.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/
|
||||
@@ -78,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
|
||||
|
||||
pnpm exec lint-staged
|
||||
pnpm exec tsx scripts/check-unused-i18n-keys.ts
|
||||
pnpm exec tsx scripts/check-unused-i18n-keys.ts
|
||||
@@ -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,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'
|
||||
)
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -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}
|
||||
@@ -1643,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(
|
||||
|
||||
@@ -13,6 +13,13 @@ export class VueNodeHelpers {
|
||||
return this.page.locator('[data-node-id]')
|
||||
}
|
||||
|
||||
/**
|
||||
* Get locator for a Vue node by its NodeId
|
||||
*/
|
||||
getNodeLocator(nodeId: string): Locator {
|
||||
return this.page.locator(`[data-node-id="${nodeId}"]`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get locator for selected Vue node components (using visual selection indicators)
|
||||
*/
|
||||
@@ -22,6 +29,13 @@ export class VueNodeHelpers {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,7 +1,7 @@
|
||||
import { Locator, Page } from '@playwright/test'
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
import path from 'path'
|
||||
|
||||
import {
|
||||
import type {
|
||||
TemplateInfo,
|
||||
WorkflowTemplates
|
||||
} from '../../src/platform/workflow/templates/types/template'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/platform/settings/types'
|
||||
import type { SettingParams } from '../../src/platform/settings/types'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Topbar commands', () => {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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: 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()
|
||||
|
||||
@@ -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)'
|
||||
|
||||
|
||||
@@ -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('Selection Toolbox - More Options Submenus', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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: 81 KiB After Width: | Height: | Size: 93 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 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('Settings Search functionality', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
// Register test settings to verify hidden/deprecated filtering
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { SystemStats } from '../../src/schemas/apiSchema'
|
||||
import type { SystemStats } from '../../src/schemas/apiSchema'
|
||||
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Version Mismatch Warnings', () => {
|
||||
@@ -85,6 +85,7 @@ test.describe('Version Mismatch Warnings', () => {
|
||||
test('should persist dismissed state across sessions', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
test.setTimeout(30_000)
|
||||
// Mock system_stats route to indicate that the installed version is always ahead of the required version
|
||||
await comfyPage.page.route('**/system_stats**', async (route) => {
|
||||
await route.fulfill({
|
||||
@@ -106,6 +107,11 @@ test.describe('Version Mismatch Warnings', () => {
|
||||
const dismissButton = warningToast.getByRole('button', { name: 'Close' })
|
||||
await dismissButton.click()
|
||||
|
||||
// Wait for the dismissed state to be persisted
|
||||
await comfyPage.page.waitForFunction(
|
||||
() => !!localStorage.getItem('comfy.versionMismatch.dismissals')
|
||||
)
|
||||
|
||||
// Reload the page, keeping local storage
|
||||
await comfyPage.setup({ clearStorage: false })
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Vue Nodes Zoom', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
})
|
||||
|
||||
test('should not capture drag while zooming with ctrl+shift+drag', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
|
||||
const nodeBoundingBox = await checkpointNode.boundingBox()
|
||||
if (!nodeBoundingBox) throw new Error('Node bounding box not available')
|
||||
|
||||
const nodeMidpointX = nodeBoundingBox.x + nodeBoundingBox.width / 2
|
||||
const nodeMidpointY = nodeBoundingBox.y + nodeBoundingBox.height / 2
|
||||
|
||||
// Start the Ctrl+Shift drag-to-zoom on the canvas and continue dragging over
|
||||
// the node. The node should not capture the drag while drag-zooming.
|
||||
await comfyPage.page.keyboard.down('Control')
|
||||
await comfyPage.page.keyboard.down('Shift')
|
||||
await comfyPage.dragAndDrop(
|
||||
{ x: 200, y: 300 },
|
||||
{ x: nodeMidpointX, y: nodeMidpointY }
|
||||
)
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('zoomed-in-ctrl-shift.png')
|
||||
})
|
||||
})
|
||||
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,791 @@
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
import type { NodeId } from '../../../../../src/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { getSlotKey } from '../../../../../src/renderer/core/layout/slots/slotIdentifier'
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../../fixtures/ComfyPage'
|
||||
import { getMiddlePoint } from '../../../../fixtures/utils/litegraphUtils'
|
||||
import { fitToViewInstant } from '../../../../helpers/fitToView'
|
||||
|
||||
async function getCenter(locator: Locator): Promise<{ x: number; y: number }> {
|
||||
const box = await locator.boundingBox()
|
||||
if (!box) throw new Error('Slot bounding box not available')
|
||||
return {
|
||||
x: box.x + box.width / 2,
|
||||
y: box.y + box.height / 2
|
||||
}
|
||||
}
|
||||
|
||||
async function getInputLinkDetails(
|
||||
page: Page,
|
||||
nodeId: NodeId,
|
||||
slotIndex: number
|
||||
) {
|
||||
return await page.evaluate(
|
||||
([targetNodeId, targetSlot]) => {
|
||||
const app = window['app']
|
||||
const graph = app?.canvas?.graph ?? app?.graph
|
||||
if (!graph) return null
|
||||
|
||||
const node = graph.getNodeById(targetNodeId)
|
||||
if (!node) return null
|
||||
|
||||
const input = node.inputs?.[targetSlot]
|
||||
if (!input) return null
|
||||
|
||||
const linkId = input.link
|
||||
if (linkId == null) return null
|
||||
|
||||
const link = graph.getLink?.(linkId)
|
||||
if (!link) return null
|
||||
|
||||
return {
|
||||
id: link.id,
|
||||
originId: link.origin_id,
|
||||
originSlot:
|
||||
typeof link.origin_slot === 'string'
|
||||
? Number.parseInt(link.origin_slot, 10)
|
||||
: link.origin_slot,
|
||||
targetId: link.target_id,
|
||||
targetSlot:
|
||||
typeof link.target_slot === 'string'
|
||||
? Number.parseInt(link.target_slot, 10)
|
||||
: link.target_slot,
|
||||
parentId: link.parentId ?? null
|
||||
}
|
||||
},
|
||||
[nodeId, slotIndex] as const
|
||||
)
|
||||
}
|
||||
|
||||
// Test helpers to reduce repetition across cases
|
||||
function slotLocator(
|
||||
page: Page,
|
||||
nodeId: NodeId,
|
||||
slotIndex: number,
|
||||
isInput: boolean
|
||||
) {
|
||||
const key = getSlotKey(String(nodeId), slotIndex, isInput)
|
||||
return page.locator(`[data-slot-key="${key}"]`)
|
||||
}
|
||||
|
||||
async function expectVisibleAll(...locators: Locator[]) {
|
||||
await Promise.all(locators.map((l) => expect(l).toBeVisible()))
|
||||
}
|
||||
|
||||
async function getSlotCenter(
|
||||
page: Page,
|
||||
nodeId: NodeId,
|
||||
slotIndex: number,
|
||||
isInput: boolean
|
||||
) {
|
||||
const locator = slotLocator(page, nodeId, slotIndex, isInput)
|
||||
await expect(locator).toBeVisible()
|
||||
return await getCenter(locator)
|
||||
}
|
||||
|
||||
async function connectSlots(
|
||||
page: Page,
|
||||
from: { nodeId: NodeId; index: number },
|
||||
to: { nodeId: NodeId; index: number },
|
||||
nextFrame: () => Promise<void>
|
||||
) {
|
||||
const fromLoc = slotLocator(page, from.nodeId, from.index, false)
|
||||
const toLoc = slotLocator(page, to.nodeId, to.index, true)
|
||||
await expectVisibleAll(fromLoc, toLoc)
|
||||
await fromLoc.dragTo(toLoc)
|
||||
await nextFrame()
|
||||
}
|
||||
|
||||
test.describe('Vue Node Link Interaction', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.setup()
|
||||
await comfyPage.loadWorkflow('vueNodes/simple-triple')
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
await fitToViewInstant(comfyPage)
|
||||
})
|
||||
|
||||
test('should show a link dragging out from a slot when dragging on a slot', async ({
|
||||
comfyPage,
|
||||
comfyMouse
|
||||
}) => {
|
||||
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
||||
expect(samplerNode).toBeTruthy()
|
||||
|
||||
const slot = slotLocator(comfyPage.page, samplerNode.id, 0, false)
|
||||
await expect(slot).toBeVisible()
|
||||
|
||||
const start = await getCenter(slot)
|
||||
|
||||
// Arbitrary value
|
||||
const dragTarget = {
|
||||
x: start.x + 180,
|
||||
y: start.y - 140
|
||||
}
|
||||
|
||||
await comfyMouse.move(start)
|
||||
await comfyMouse.drag(dragTarget)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
try {
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-node-dragging-link.png'
|
||||
)
|
||||
} finally {
|
||||
await comfyMouse.drop()
|
||||
}
|
||||
})
|
||||
|
||||
test('should create a link when dropping on a compatible slot', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
||||
const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0]
|
||||
expect(samplerNode && vaeNode).toBeTruthy()
|
||||
|
||||
const samplerOutput = await samplerNode.getOutput(0)
|
||||
const vaeInput = await vaeNode.getInput(0)
|
||||
|
||||
await connectSlots(
|
||||
comfyPage.page,
|
||||
{ nodeId: samplerNode.id, index: 0 },
|
||||
{ nodeId: vaeNode.id, index: 0 },
|
||||
() => comfyPage.nextFrame()
|
||||
)
|
||||
|
||||
expect(await samplerOutput.getLinkCount()).toBe(1)
|
||||
expect(await vaeInput.getLinkCount()).toBe(1)
|
||||
|
||||
const linkDetails = await getInputLinkDetails(comfyPage.page, vaeNode.id, 0)
|
||||
expect(linkDetails).not.toBeNull()
|
||||
expect(linkDetails).toMatchObject({
|
||||
originId: samplerNode.id,
|
||||
originSlot: 0,
|
||||
targetId: vaeNode.id,
|
||||
targetSlot: 0
|
||||
})
|
||||
})
|
||||
|
||||
test('should not create a link when slot types are incompatible', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
||||
const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0]
|
||||
expect(samplerNode && clipNode).toBeTruthy()
|
||||
|
||||
const samplerOutput = await samplerNode.getOutput(0)
|
||||
const clipInput = await clipNode.getInput(0)
|
||||
|
||||
const outputSlot = slotLocator(comfyPage.page, samplerNode.id, 0, false)
|
||||
const inputSlot = slotLocator(comfyPage.page, clipNode.id, 0, true)
|
||||
await expectVisibleAll(outputSlot, inputSlot)
|
||||
|
||||
await outputSlot.dragTo(inputSlot)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
expect(await samplerOutput.getLinkCount()).toBe(0)
|
||||
expect(await clipInput.getLinkCount()).toBe(0)
|
||||
|
||||
const graphLinkDetails = await getInputLinkDetails(
|
||||
comfyPage.page,
|
||||
clipNode.id,
|
||||
0
|
||||
)
|
||||
expect(graphLinkDetails).toBeNull()
|
||||
})
|
||||
|
||||
test('should not create a link when dropping onto a slot on the same node', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
||||
expect(samplerNode).toBeTruthy()
|
||||
|
||||
const samplerOutput = await samplerNode.getOutput(0)
|
||||
const samplerInput = await samplerNode.getInput(3)
|
||||
|
||||
const outputSlot = slotLocator(comfyPage.page, samplerNode.id, 0, false)
|
||||
const inputSlot = slotLocator(comfyPage.page, samplerNode.id, 3, true)
|
||||
await expectVisibleAll(outputSlot, inputSlot)
|
||||
|
||||
await outputSlot.dragTo(inputSlot)
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
expect(await samplerOutput.getLinkCount()).toBe(0)
|
||||
expect(await samplerInput.getLinkCount()).toBe(0)
|
||||
})
|
||||
|
||||
test('should reuse the existing origin when dragging an input link', async ({
|
||||
comfyPage,
|
||||
comfyMouse
|
||||
}) => {
|
||||
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
||||
const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0]
|
||||
expect(samplerNode && vaeNode).toBeTruthy()
|
||||
const samplerOutputCenter = await getSlotCenter(
|
||||
comfyPage.page,
|
||||
samplerNode.id,
|
||||
0,
|
||||
false
|
||||
)
|
||||
const vaeInputCenter = await getSlotCenter(
|
||||
comfyPage.page,
|
||||
vaeNode.id,
|
||||
0,
|
||||
true
|
||||
)
|
||||
|
||||
await comfyMouse.move(samplerOutputCenter)
|
||||
await comfyMouse.drag(vaeInputCenter)
|
||||
await comfyMouse.drop()
|
||||
|
||||
const dragTarget = {
|
||||
x: vaeInputCenter.x + 160,
|
||||
y: vaeInputCenter.y - 100
|
||||
}
|
||||
|
||||
await comfyMouse.move(vaeInputCenter)
|
||||
await comfyMouse.drag(dragTarget)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-node-input-drag-reuses-origin.png'
|
||||
)
|
||||
await comfyMouse.drop()
|
||||
})
|
||||
|
||||
test('ctrl+alt drag from an input starts a fresh link', async ({
|
||||
comfyPage,
|
||||
comfyMouse
|
||||
}) => {
|
||||
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
||||
const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0]
|
||||
expect(samplerNode && vaeNode).toBeTruthy()
|
||||
|
||||
const samplerOutput = await samplerNode.getOutput(0)
|
||||
const vaeInput = await vaeNode.getInput(0)
|
||||
|
||||
const samplerOutputCenter = await getSlotCenter(
|
||||
comfyPage.page,
|
||||
samplerNode.id,
|
||||
0,
|
||||
false
|
||||
)
|
||||
const vaeInputCenter = await getSlotCenter(
|
||||
comfyPage.page,
|
||||
vaeNode.id,
|
||||
0,
|
||||
true
|
||||
)
|
||||
|
||||
await comfyMouse.move(samplerOutputCenter)
|
||||
await comfyMouse.drag(vaeInputCenter)
|
||||
await comfyMouse.drop()
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const dragTarget = {
|
||||
x: vaeInputCenter.x + 140,
|
||||
y: vaeInputCenter.y - 110
|
||||
}
|
||||
|
||||
await comfyMouse.move(vaeInputCenter)
|
||||
await comfyPage.page.keyboard.down('Control')
|
||||
await comfyPage.page.keyboard.down('Alt')
|
||||
|
||||
try {
|
||||
await comfyMouse.drag(dragTarget)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-node-input-drag-ctrl-alt.png'
|
||||
)
|
||||
} finally {
|
||||
await comfyMouse.drop().catch(() => {})
|
||||
await comfyPage.page.keyboard.up('Alt').catch(() => {})
|
||||
await comfyPage.page.keyboard.up('Control').catch(() => {})
|
||||
}
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Tcehnically intended to disconnect existing as well
|
||||
expect(await vaeInput.getLinkCount()).toBe(0)
|
||||
expect(await samplerOutput.getLinkCount()).toBe(0)
|
||||
})
|
||||
|
||||
test('dropping an input link back on its slot restores the original connection', async ({
|
||||
comfyPage,
|
||||
comfyMouse
|
||||
}) => {
|
||||
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
||||
const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0]
|
||||
expect(samplerNode && vaeNode).toBeTruthy()
|
||||
|
||||
const samplerOutput = await samplerNode.getOutput(0)
|
||||
const vaeInput = await vaeNode.getInput(0)
|
||||
|
||||
const samplerOutputCenter = await getSlotCenter(
|
||||
comfyPage.page,
|
||||
samplerNode.id,
|
||||
0,
|
||||
false
|
||||
)
|
||||
const vaeInputCenter = await getSlotCenter(
|
||||
comfyPage.page,
|
||||
vaeNode.id,
|
||||
0,
|
||||
true
|
||||
)
|
||||
|
||||
await comfyMouse.move(samplerOutputCenter)
|
||||
try {
|
||||
await comfyMouse.drag(vaeInputCenter)
|
||||
} finally {
|
||||
await comfyMouse.drop()
|
||||
}
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const originalLink = await getInputLinkDetails(
|
||||
comfyPage.page,
|
||||
vaeNode.id,
|
||||
0
|
||||
)
|
||||
expect(originalLink).not.toBeNull()
|
||||
|
||||
const dragTarget = {
|
||||
x: vaeInputCenter.x + 150,
|
||||
y: vaeInputCenter.y - 100
|
||||
}
|
||||
|
||||
// To prevent needing a screenshot expectation for whether the link's off
|
||||
const vaeInputLocator = slotLocator(comfyPage.page, vaeNode.id, 0, true)
|
||||
const inputBox = await vaeInputLocator.boundingBox()
|
||||
if (!inputBox) throw new Error('Input slot bounding box not available')
|
||||
const isOutsideX =
|
||||
dragTarget.x < inputBox.x || dragTarget.x > inputBox.x + inputBox.width
|
||||
const isOutsideY =
|
||||
dragTarget.y < inputBox.y || dragTarget.y > inputBox.y + inputBox.height
|
||||
expect(isOutsideX || isOutsideY).toBe(true)
|
||||
|
||||
await comfyMouse.move(vaeInputCenter)
|
||||
await comfyMouse.drag(dragTarget)
|
||||
await comfyMouse.move(vaeInputCenter)
|
||||
await comfyMouse.drop()
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const restoredLink = await getInputLinkDetails(
|
||||
comfyPage.page,
|
||||
vaeNode.id,
|
||||
0
|
||||
)
|
||||
|
||||
expect(restoredLink).not.toBeNull()
|
||||
if (!restoredLink || !originalLink) {
|
||||
throw new Error('Expected both original and restored links to exist')
|
||||
}
|
||||
expect(restoredLink).toMatchObject({
|
||||
originId: originalLink.originId,
|
||||
originSlot: originalLink.originSlot,
|
||||
targetId: originalLink.targetId,
|
||||
targetSlot: originalLink.targetSlot,
|
||||
parentId: originalLink.parentId
|
||||
})
|
||||
expect(await samplerOutput.getLinkCount()).toBe(1)
|
||||
expect(await vaeInput.getLinkCount()).toBe(1)
|
||||
})
|
||||
|
||||
test('rerouted input drag preview remains anchored to reroute', async ({
|
||||
comfyPage,
|
||||
comfyMouse
|
||||
}) => {
|
||||
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
||||
const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0]
|
||||
|
||||
const samplerOutput = await samplerNode.getOutput(0)
|
||||
const vaeInput = await vaeNode.getInput(0)
|
||||
|
||||
await connectSlots(
|
||||
comfyPage.page,
|
||||
{ nodeId: samplerNode.id, index: 0 },
|
||||
{ nodeId: vaeNode.id, index: 0 },
|
||||
() => comfyPage.nextFrame()
|
||||
)
|
||||
|
||||
const outputPosition = await samplerOutput.getPosition()
|
||||
const inputPosition = await vaeInput.getPosition()
|
||||
const reroutePoint = getMiddlePoint(outputPosition, inputPosition)
|
||||
|
||||
// Insert a reroute programmatically on the existing link between sampler output[0] and VAE input[0].
|
||||
// This avoids relying on an exact path hit-test position.
|
||||
await comfyPage.page.evaluate(
|
||||
([targetNodeId, targetSlot, clientPoint]) => {
|
||||
const app = (window as any)['app']
|
||||
const graph = app?.canvas?.graph ?? app?.graph
|
||||
if (!graph) throw new Error('Graph not available')
|
||||
const node = graph.getNodeById(targetNodeId)
|
||||
if (!node) throw new Error('Target node not found')
|
||||
const input = node.inputs?.[targetSlot]
|
||||
if (!input) throw new Error('Target input slot not found')
|
||||
|
||||
const linkId = input.link
|
||||
if (linkId == null) throw new Error('Expected existing link on input')
|
||||
const link = graph.getLink(linkId)
|
||||
if (!link) throw new Error('Link not found')
|
||||
|
||||
// Convert the client/canvas pixel coordinates to graph space
|
||||
const pos = app.canvas.ds.convertCanvasToOffset([
|
||||
clientPoint.x,
|
||||
clientPoint.y
|
||||
])
|
||||
graph.createReroute(pos, link)
|
||||
},
|
||||
[vaeNode.id, 0, reroutePoint] as const
|
||||
)
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const vaeInputCenter = await getSlotCenter(
|
||||
comfyPage.page,
|
||||
vaeNode.id,
|
||||
0,
|
||||
true
|
||||
)
|
||||
const dragTarget = {
|
||||
x: vaeInputCenter.x + 160,
|
||||
y: vaeInputCenter.y - 120
|
||||
}
|
||||
|
||||
let dropped = false
|
||||
try {
|
||||
await comfyMouse.move(vaeInputCenter)
|
||||
await comfyMouse.drag(dragTarget)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-node-reroute-input-drag.png'
|
||||
)
|
||||
await comfyMouse.move(vaeInputCenter)
|
||||
await comfyMouse.drop()
|
||||
dropped = true
|
||||
} finally {
|
||||
if (!dropped) {
|
||||
await comfyMouse.drop().catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const linkDetails = await getInputLinkDetails(comfyPage.page, vaeNode.id, 0)
|
||||
expect(linkDetails).not.toBeNull()
|
||||
expect(linkDetails?.originId).toBe(samplerNode.id)
|
||||
expect(linkDetails?.parentId).not.toBeNull()
|
||||
})
|
||||
|
||||
test('rerouted output shift-drag preview remains anchored to reroute', async ({
|
||||
comfyPage,
|
||||
comfyMouse
|
||||
}) => {
|
||||
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
||||
const vaeNode = (await comfyPage.getNodeRefsByType('VAEDecode'))[0]
|
||||
expect(samplerNode && vaeNode).toBeTruthy()
|
||||
|
||||
const samplerOutput = await samplerNode.getOutput(0)
|
||||
const vaeInput = await vaeNode.getInput(0)
|
||||
|
||||
await connectSlots(
|
||||
comfyPage.page,
|
||||
{ nodeId: samplerNode.id, index: 0 },
|
||||
{ nodeId: vaeNode.id, index: 0 },
|
||||
() => comfyPage.nextFrame()
|
||||
)
|
||||
|
||||
const outputPosition = await samplerOutput.getPosition()
|
||||
const inputPosition = await vaeInput.getPosition()
|
||||
const reroutePoint = getMiddlePoint(outputPosition, inputPosition)
|
||||
|
||||
// Insert a reroute programmatically on the existing link between sampler output[0] and VAE input[0].
|
||||
// This avoids relying on an exact path hit-test position.
|
||||
await comfyPage.page.evaluate(
|
||||
([targetNodeId, targetSlot, clientPoint]) => {
|
||||
const app = (window as any)['app']
|
||||
const graph = app?.canvas?.graph ?? app?.graph
|
||||
if (!graph) throw new Error('Graph not available')
|
||||
const node = graph.getNodeById(targetNodeId)
|
||||
if (!node) throw new Error('Target node not found')
|
||||
const input = node.inputs?.[targetSlot]
|
||||
if (!input) throw new Error('Target input slot not found')
|
||||
|
||||
const linkId = input.link
|
||||
if (linkId == null) throw new Error('Expected existing link on input')
|
||||
const link = graph.getLink(linkId)
|
||||
if (!link) throw new Error('Link not found')
|
||||
|
||||
// Convert the client/canvas pixel coordinates to graph space
|
||||
const pos = app.canvas.ds.convertCanvasToOffset([
|
||||
clientPoint.x,
|
||||
clientPoint.y
|
||||
])
|
||||
graph.createReroute(pos, link)
|
||||
},
|
||||
[vaeNode.id, 0, reroutePoint] as const
|
||||
)
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const outputCenter = await getSlotCenter(
|
||||
comfyPage.page,
|
||||
samplerNode.id,
|
||||
0,
|
||||
false
|
||||
)
|
||||
const dragTarget = {
|
||||
x: outputCenter.x + 150,
|
||||
y: outputCenter.y - 140
|
||||
}
|
||||
|
||||
let dropPending = false
|
||||
let shiftHeld = false
|
||||
try {
|
||||
await comfyMouse.move(outputCenter)
|
||||
await comfyPage.page.keyboard.down('Shift')
|
||||
shiftHeld = true
|
||||
dropPending = true
|
||||
await comfyMouse.drag(dragTarget)
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-node-reroute-output-shift-drag.png'
|
||||
)
|
||||
await comfyMouse.move(outputCenter)
|
||||
await comfyMouse.drop()
|
||||
dropPending = false
|
||||
} finally {
|
||||
if (dropPending) await comfyMouse.drop().catch(() => {})
|
||||
if (shiftHeld) await comfyPage.page.keyboard.up('Shift').catch(() => {})
|
||||
}
|
||||
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const linkDetails = await getInputLinkDetails(comfyPage.page, vaeNode.id, 0)
|
||||
expect(linkDetails).not.toBeNull()
|
||||
expect(linkDetails?.originId).toBe(samplerNode.id)
|
||||
expect(linkDetails?.parentId).not.toBeNull()
|
||||
})
|
||||
|
||||
test('dragging input to input drags existing link', async ({
|
||||
comfyPage,
|
||||
comfyMouse
|
||||
}) => {
|
||||
const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0]
|
||||
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
||||
expect(clipNode && samplerNode).toBeTruthy()
|
||||
|
||||
// Step 1: Connect CLIP's only output (index 0) to KSampler's second input (index 1)
|
||||
await connectSlots(
|
||||
comfyPage.page,
|
||||
{ nodeId: clipNode.id, index: 0 },
|
||||
{ nodeId: samplerNode.id, index: 1 },
|
||||
() => comfyPage.nextFrame()
|
||||
)
|
||||
|
||||
// Verify initial link exists between CLIP -> KSampler input[1]
|
||||
const initialLink = await getInputLinkDetails(
|
||||
comfyPage.page,
|
||||
samplerNode.id,
|
||||
1
|
||||
)
|
||||
expect(initialLink).not.toBeNull()
|
||||
expect(initialLink).toMatchObject({
|
||||
originId: clipNode.id,
|
||||
targetId: samplerNode.id,
|
||||
targetSlot: 1
|
||||
})
|
||||
|
||||
// Step 2: Drag from KSampler's second input to its third input (index 2)
|
||||
const input2Center = await getSlotCenter(
|
||||
comfyPage.page,
|
||||
samplerNode.id,
|
||||
1,
|
||||
true
|
||||
)
|
||||
const input3Center = await getSlotCenter(
|
||||
comfyPage.page,
|
||||
samplerNode.id,
|
||||
2,
|
||||
true
|
||||
)
|
||||
|
||||
await comfyMouse.move(input2Center)
|
||||
await comfyMouse.drag(input3Center)
|
||||
await comfyMouse.drop()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Expect old link removed from input[1]
|
||||
const afterSecondInput = await getInputLinkDetails(
|
||||
comfyPage.page,
|
||||
samplerNode.id,
|
||||
1
|
||||
)
|
||||
expect(afterSecondInput).toBeNull()
|
||||
|
||||
// Expect new link exists at input[2] from CLIP
|
||||
const afterThirdInput = await getInputLinkDetails(
|
||||
comfyPage.page,
|
||||
samplerNode.id,
|
||||
2
|
||||
)
|
||||
expect(afterThirdInput).not.toBeNull()
|
||||
expect(afterThirdInput).toMatchObject({
|
||||
originId: clipNode.id,
|
||||
targetId: samplerNode.id,
|
||||
targetSlot: 2
|
||||
})
|
||||
})
|
||||
|
||||
test('shift-dragging an output with multiple links should drag all links', async ({
|
||||
comfyPage,
|
||||
comfyMouse
|
||||
}) => {
|
||||
const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0]
|
||||
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
||||
expect(clipNode && samplerNode).toBeTruthy()
|
||||
|
||||
const clipOutput = await clipNode.getOutput(0)
|
||||
|
||||
// Connect output[0] -> inputs[1] and [2]
|
||||
await connectSlots(
|
||||
comfyPage.page,
|
||||
{ nodeId: clipNode.id, index: 0 },
|
||||
{ nodeId: samplerNode.id, index: 1 },
|
||||
() => comfyPage.nextFrame()
|
||||
)
|
||||
await connectSlots(
|
||||
comfyPage.page,
|
||||
{ nodeId: clipNode.id, index: 0 },
|
||||
{ nodeId: samplerNode.id, index: 2 },
|
||||
() => comfyPage.nextFrame()
|
||||
)
|
||||
|
||||
expect(await clipOutput.getLinkCount()).toBe(2)
|
||||
|
||||
const outputCenter = await getSlotCenter(
|
||||
comfyPage.page,
|
||||
clipNode.id,
|
||||
0,
|
||||
false
|
||||
)
|
||||
const dragTarget = {
|
||||
x: outputCenter.x + 40,
|
||||
y: outputCenter.y - 140
|
||||
}
|
||||
|
||||
let dropPending = false
|
||||
let shiftHeld = false
|
||||
try {
|
||||
await comfyMouse.move(outputCenter)
|
||||
await comfyPage.page.keyboard.down('Shift')
|
||||
shiftHeld = true
|
||||
await comfyMouse.drag(dragTarget)
|
||||
dropPending = true
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-node-shift-output-multi-link.png'
|
||||
)
|
||||
} finally {
|
||||
if (dropPending) await comfyMouse.drop().catch(() => {})
|
||||
if (shiftHeld) await comfyPage.page.keyboard.up('Shift').catch(() => {})
|
||||
}
|
||||
})
|
||||
|
||||
test('should snap to node center while dragging and link on drop', async ({
|
||||
comfyPage,
|
||||
comfyMouse
|
||||
}) => {
|
||||
const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0]
|
||||
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
||||
expect(clipNode && samplerNode).toBeTruthy()
|
||||
|
||||
// Start drag from CLIP output[0]
|
||||
const clipOutputCenter = await getSlotCenter(
|
||||
comfyPage.page,
|
||||
clipNode.id,
|
||||
0,
|
||||
false
|
||||
)
|
||||
|
||||
// Drag to the visual center of the KSampler Vue node (not a slot)
|
||||
const samplerVue = comfyPage.vueNodes.getNodeLocator(String(samplerNode.id))
|
||||
await expect(samplerVue).toBeVisible()
|
||||
const samplerCenter = await getCenter(samplerVue)
|
||||
|
||||
await comfyMouse.move(clipOutputCenter)
|
||||
await comfyMouse.drag(samplerCenter)
|
||||
|
||||
// During drag, the preview should snap/highlight a compatible input on KSampler
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('vue-node-snap-to-node.png')
|
||||
|
||||
// Drop to create the link
|
||||
await comfyMouse.drop()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Validate a link was created to one of KSampler's compatible inputs (1 or 2)
|
||||
const linkOnInput1 = await getInputLinkDetails(
|
||||
comfyPage.page,
|
||||
samplerNode.id,
|
||||
1
|
||||
)
|
||||
const linkOnInput2 = await getInputLinkDetails(
|
||||
comfyPage.page,
|
||||
samplerNode.id,
|
||||
2
|
||||
)
|
||||
|
||||
const linked = linkOnInput1 ?? linkOnInput2
|
||||
expect(linked).not.toBeNull()
|
||||
expect(linked?.originId).toBe(clipNode.id)
|
||||
expect(linked?.targetId).toBe(samplerNode.id)
|
||||
})
|
||||
|
||||
test('should snap to a specific compatible slot when targeting it', async ({
|
||||
comfyPage,
|
||||
comfyMouse
|
||||
}) => {
|
||||
const clipNode = (await comfyPage.getNodeRefsByType('CLIPTextEncode'))[0]
|
||||
const samplerNode = (await comfyPage.getNodeRefsByType('KSampler'))[0]
|
||||
expect(clipNode && samplerNode).toBeTruthy()
|
||||
|
||||
// Drag from CLIP output[0] to KSampler input[2] (third slot) which is the
|
||||
// second compatible input for CLIP
|
||||
const clipOutputCenter = await getSlotCenter(
|
||||
comfyPage.page,
|
||||
clipNode.id,
|
||||
0,
|
||||
false
|
||||
)
|
||||
const samplerInput3Center = await getSlotCenter(
|
||||
comfyPage.page,
|
||||
samplerNode.id,
|
||||
2,
|
||||
true
|
||||
)
|
||||
|
||||
await comfyMouse.move(clipOutputCenter)
|
||||
await comfyMouse.drag(samplerInput3Center)
|
||||
|
||||
// Expect the preview to show snapping to the targeted slot
|
||||
await expect(comfyPage.canvas).toHaveScreenshot('vue-node-snap-to-slot.png')
|
||||
|
||||
// Finish the connection
|
||||
await comfyMouse.drop()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const linkDetails = await getInputLinkDetails(
|
||||
comfyPage.page,
|
||||
samplerNode.id,
|
||||
2
|
||||
)
|
||||
expect(linkDetails).not.toBeNull()
|
||||
expect(linkDetails).toMatchObject({
|
||||
originId: clipNode.id,
|
||||
targetId: samplerNode.id,
|
||||
targetSlot: 2
|
||||
})
|
||||
})
|
||||
})
|
||||
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 51 KiB |
@@ -1,6 +1,10 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
|
||||
import { comfyPageFixture as test } from '../../../../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Vue Nodes - Delete Key Interaction', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
@@ -0,0 +1,76 @@
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../../fixtures/ComfyPage'
|
||||
import { VueNodeFixture } from '../../../../fixtures/utils/vueNodeFixtures'
|
||||
|
||||
test.describe('Vue Nodes Renaming', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false)
|
||||
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.setup()
|
||||
})
|
||||
|
||||
test('should display node title', async ({ comfyPage }) => {
|
||||
// Get the KSampler node from the default workflow
|
||||
const nodes = await comfyPage.getNodeRefsByType('KSampler')
|
||||
expect(nodes.length).toBeGreaterThanOrEqual(1)
|
||||
|
||||
const node = nodes[0]
|
||||
const vueNode = new VueNodeFixture(node, comfyPage.page)
|
||||
|
||||
const title = await vueNode.getTitle()
|
||||
expect(title).toBe('KSampler')
|
||||
|
||||
// Verify title is visible in the header
|
||||
const header = await vueNode.getHeader()
|
||||
await expect(header).toContainText('KSampler')
|
||||
})
|
||||
|
||||
test('should allow title renaming by double clicking on the node header', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const nodes = await comfyPage.getNodeRefsByType('KSampler')
|
||||
const node = nodes[0]
|
||||
const vueNode = new VueNodeFixture(node, comfyPage.page)
|
||||
|
||||
// Test renaming with Enter
|
||||
await vueNode.setTitle('My Custom Sampler')
|
||||
const newTitle = await vueNode.getTitle()
|
||||
expect(newTitle).toBe('My Custom Sampler')
|
||||
|
||||
// Verify the title is displayed
|
||||
const header = await vueNode.getHeader()
|
||||
await expect(header).toContainText('My Custom Sampler')
|
||||
|
||||
// Test cancel with Escape
|
||||
const titleElement = await vueNode.getTitleElement()
|
||||
await titleElement.dblclick()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Type a different value but cancel
|
||||
const input = (await vueNode.getHeader()).locator(
|
||||
'[data-testid="node-title-input"]'
|
||||
)
|
||||
await input.fill('This Should Be Cancelled')
|
||||
await input.press('Escape')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Title should remain as the previously saved value
|
||||
const titleAfterCancel = await vueNode.getTitle()
|
||||
expect(titleAfterCancel).toBe('My Custom Sampler')
|
||||
})
|
||||
|
||||
test('Double click node body does not trigger edit', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const loadCheckpointNode =
|
||||
comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
|
||||
const nodeBbox = await loadCheckpointNode.boundingBox()
|
||||
if (!nodeBbox) throw new Error('Node not found')
|
||||
await loadCheckpointNode.dblclick()
|
||||
|
||||
const editingTitleInput = comfyPage.page.getByTestId('node-title-input')
|
||||
await expect(editingTitleInput).not.toBeVisible()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../../fixtures/ComfyPage'
|
||||
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
|
||||
})
|
||||
|
||||
test.describe('Vue Node Selection', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
})
|
||||
|
||||
const modifiers = [
|
||||
{ key: 'Control', name: 'ctrl' },
|
||||
{ key: 'Shift', name: 'shift' },
|
||||
{ key: 'Meta', name: 'meta' }
|
||||
] as const
|
||||
|
||||
for (const { key: modifier, name } of modifiers) {
|
||||
test(`should allow selecting multiple nodes with ${name}+click`, async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.page.getByText('Load Checkpoint').click()
|
||||
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(1)
|
||||
|
||||
await comfyPage.page.getByText('Empty Latent Image').click({
|
||||
modifiers: [modifier]
|
||||
})
|
||||
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(2)
|
||||
|
||||
await comfyPage.page.getByText('KSampler').click({
|
||||
modifiers: [modifier]
|
||||
})
|
||||
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(3)
|
||||
})
|
||||
|
||||
test(`should allow de-selecting nodes with ${name}+click`, async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.page.getByText('Load Checkpoint').click()
|
||||
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(1)
|
||||
|
||||
await comfyPage.page.getByText('Load Checkpoint').click({
|
||||
modifiers: [modifier]
|
||||
})
|
||||
expect(await comfyPage.vueNodes.getSelectedNodeCount()).toBe(0)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 51 KiB |
45
browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../fixtures/ComfyPage'
|
||||
|
||||
const BYPASS_HOTKEY = 'Control+b'
|
||||
const BYPASS_CLASS = /before:bg-bypass\/60/
|
||||
|
||||
test.describe('Vue Node Bypass', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
})
|
||||
|
||||
test('should allow toggling bypass on a selected node with hotkey', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.page.getByText('Load Checkpoint').click()
|
||||
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
|
||||
|
||||
const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
|
||||
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
|
||||
|
||||
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
|
||||
await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS)
|
||||
})
|
||||
|
||||
test('should allow toggling bypass on multiple selected nodes with hotkey', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.page.getByText('Load Checkpoint').click()
|
||||
await comfyPage.page.getByText('KSampler').click({ modifiers: ['Control'] })
|
||||
|
||||
const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
|
||||
const ksamplerNode = comfyPage.vueNodes.getNodeByTitle('KSampler')
|
||||
|
||||
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
|
||||
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
|
||||
await expect(ksamplerNode).toHaveClass(BYPASS_CLASS)
|
||||
|
||||
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
|
||||
await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS)
|
||||
await expect(ksamplerNode).not.toHaveClass(BYPASS_CLASS)
|
||||
})
|
||||
})
|
||||
@@ -1,67 +1,20 @@
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../fixtures/ComfyPage'
|
||||
import { VueNodeFixture } from '../../fixtures/utils/vueNodeFixtures'
|
||||
} from '../../../fixtures/ComfyPage'
|
||||
import { VueNodeFixture } from '../../../fixtures/utils/vueNodeFixtures'
|
||||
|
||||
test.describe('NodeHeader', () => {
|
||||
test.describe('Vue Node Collapse', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Enabled')
|
||||
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', false)
|
||||
await comfyPage.setSetting('Comfy.EnableTooltips', true)
|
||||
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.setup()
|
||||
})
|
||||
|
||||
test('displays node title', async ({ comfyPage }) => {
|
||||
// Get the KSampler node from the default workflow
|
||||
const nodes = await comfyPage.getNodeRefsByType('KSampler')
|
||||
expect(nodes.length).toBeGreaterThanOrEqual(1)
|
||||
|
||||
const node = nodes[0]
|
||||
const vueNode = new VueNodeFixture(node, comfyPage.page)
|
||||
|
||||
const title = await vueNode.getTitle()
|
||||
expect(title).toBe('KSampler')
|
||||
|
||||
// Verify title is visible in the header
|
||||
const header = await vueNode.getHeader()
|
||||
await expect(header).toContainText('KSampler')
|
||||
})
|
||||
|
||||
test('allows title renaming', async ({ comfyPage }) => {
|
||||
const nodes = await comfyPage.getNodeRefsByType('KSampler')
|
||||
const node = nodes[0]
|
||||
const vueNode = new VueNodeFixture(node, comfyPage.page)
|
||||
|
||||
// Test renaming with Enter
|
||||
await vueNode.setTitle('My Custom Sampler')
|
||||
const newTitle = await vueNode.getTitle()
|
||||
expect(newTitle).toBe('My Custom Sampler')
|
||||
|
||||
// Verify the title is displayed
|
||||
const header = await vueNode.getHeader()
|
||||
await expect(header).toContainText('My Custom Sampler')
|
||||
|
||||
// Test cancel with Escape
|
||||
const titleElement = await vueNode.getTitleElement()
|
||||
await titleElement.dblclick()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Type a different value but cancel
|
||||
const input = (await vueNode.getHeader()).locator(
|
||||
'[data-testid="node-title-input"]'
|
||||
)
|
||||
await input.fill('This Should Be Cancelled')
|
||||
await input.press('Escape')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Title should remain as the previously saved value
|
||||
const titleAfterCancel = await vueNode.getTitle()
|
||||
expect(titleAfterCancel).toBe('My Custom Sampler')
|
||||
})
|
||||
|
||||
test('handles node collapsing', async ({ comfyPage }) => {
|
||||
test('should allow collapsing node with collapse icon', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
// Get the KSampler node from the default workflow
|
||||
const nodes = await comfyPage.getNodeRefsByType('KSampler')
|
||||
const node = nodes[0]
|
||||
@@ -90,7 +43,7 @@ test.describe('NodeHeader', () => {
|
||||
expect(expandedSize.height).toBeGreaterThanOrEqual(collapsedSize.height)
|
||||
})
|
||||
|
||||
test('shows collapse/expand icon state', async ({ comfyPage }) => {
|
||||
test('should show collapse/expand icon state', async ({ comfyPage }) => {
|
||||
const nodes = await comfyPage.getNodeRefsByType('KSampler')
|
||||
const node = nodes[0]
|
||||
const vueNode = new VueNodeFixture(node, comfyPage.page)
|
||||
@@ -110,7 +63,9 @@ test.describe('NodeHeader', () => {
|
||||
expect(iconClass).toContain('pi-chevron-down')
|
||||
})
|
||||
|
||||
test('preserves title when collapsing/expanding', async ({ comfyPage }) => {
|
||||
test('should preserve title when collapsing/expanding', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const nodes = await comfyPage.getNodeRefsByType('KSampler')
|
||||
const node = nodes[0]
|
||||
const vueNode = new VueNodeFixture(node, comfyPage.page)
|
||||
49
browser_tests/tests/vueNodes/nodeStates/colors.spec.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../../../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Vue Node Custom Colors', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
|
||||
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
})
|
||||
|
||||
test('displays color picker button and allows color selection', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const loadCheckpointNode = comfyPage.page.locator('[data-node-id]').filter({
|
||||
hasText: 'Load Checkpoint'
|
||||
})
|
||||
await loadCheckpointNode.getByText('Load Checkpoint').click()
|
||||
|
||||
await comfyPage.page.locator('.selection-toolbox .pi-circle-fill').click()
|
||||
await comfyPage.page
|
||||
.locator('.color-picker-container')
|
||||
.locator('i[data-testid="blue"]')
|
||||
.click()
|
||||
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-node-custom-color-blue.png'
|
||||
)
|
||||
})
|
||||
|
||||
test('should load node colors from workflow', async ({ comfyPage }) => {
|
||||
await comfyPage.loadWorkflow('nodes/every_node_color')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-node-custom-colors-dark-all-colors.png'
|
||||
)
|
||||
})
|
||||
|
||||
test('should show brightened node colors on light theme', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
|
||||
await comfyPage.loadWorkflow('nodes/every_node_color')
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
'vue-node-custom-colors-light-all-colors.png'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 90 KiB |