Compare commits
152 Commits
v1.36.9
...
feat/whats
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f9afa4049 | ||
|
|
5409bf86a9 | ||
|
|
c56e8425d4 | ||
|
|
0d5ca96a2b | ||
|
|
aff7f2a296 | ||
|
|
23694f37bf | ||
|
|
2466949192 | ||
|
|
f0702793bc | ||
|
|
efc242b968 | ||
|
|
ed3c855eb6 | ||
|
|
29f727a946 | ||
|
|
538f007f1d | ||
|
|
3069c24f81 | ||
|
|
97b1a48a25 | ||
|
|
8497836811 | ||
|
|
946429f2ff | ||
|
|
3d332ff0d7 | ||
|
|
45d95728f3 | ||
|
|
81c66822f5 | ||
|
|
3bb8e94247 | ||
|
|
6382b1e099 | ||
|
|
25afd39d2b | ||
|
|
773f5f5cd9 | ||
|
|
ebca0cb1e0 | ||
|
|
b1b2fd8a4f | ||
|
|
069e94b325 | ||
|
|
2901e1e403 | ||
|
|
3412a0908d | ||
|
|
81df0102f8 | ||
|
|
84662cb94c | ||
|
|
eb213d0ad3 | ||
|
|
971316205f | ||
|
|
20d06f92ca | ||
|
|
97a78f4a35 | ||
|
|
4b2e4c59af | ||
|
|
6cbb83a1e2 | ||
|
|
14866ac11b | ||
|
|
6f3855f5f1 | ||
|
|
0f1e54530c | ||
|
|
3377844408 | ||
|
|
a166ec91a6 | ||
|
|
c6f2ae3130 | ||
|
|
dfb78b2e87 | ||
|
|
965ab674d5 | ||
|
|
05f1dfe921 | ||
|
|
b86eee8494 | ||
|
|
f6d39dbfc8 | ||
|
|
97ca9f489e | ||
|
|
7b274b74f1 | ||
|
|
44c317fd05 | ||
|
|
23e9b39593 | ||
|
|
dcfa53fd7d | ||
|
|
2d5d18c020 | ||
|
|
6883241e50 | ||
|
|
e3906a0656 | ||
|
|
3ce588ad42 | ||
|
|
818c5c32e5 | ||
|
|
dbb0bd961f | ||
|
|
11bd9022c8 | ||
|
|
df1eb32907 | ||
|
|
52b94e06a1 | ||
|
|
7bc6334065 | ||
|
|
8086f977c9 | ||
|
|
f843d779c2 | ||
|
|
bce4f876f4 | ||
|
|
4b095f3701 | ||
|
|
c8e181c841 | ||
|
|
41ffb7c627 | ||
|
|
5029a0b32c | ||
|
|
f240ecaaff | ||
|
|
6a1da7a7af | ||
|
|
a6ca2bcd42 | ||
|
|
886fe07de9 | ||
|
|
43c162a862 | ||
|
|
92f21c14d4 | ||
|
|
1bf5b5397d | ||
|
|
51a7654a39 | ||
|
|
644a8bc60c | ||
|
|
9e434a1002 | ||
|
|
a2e0c3d596 | ||
|
|
e26e1f0c9e | ||
|
|
af094ebefc | ||
|
|
eea24166e0 | ||
|
|
3bd74dcf39 | ||
|
|
405e756d4c | ||
|
|
0ca27f3d9b | ||
|
|
b54ed97557 | ||
|
|
15a05afc27 | ||
|
|
1bde87838d | ||
|
|
99cb7a2da1 | ||
|
|
b3d87673ec | ||
|
|
6a733918a7 | ||
|
|
a87d2cf1bd | ||
|
|
a1d689d3b3 | ||
|
|
dc64e16f7c | ||
|
|
c19a004f0d | ||
|
|
626d8dac70 | ||
|
|
b6a12ddae1 | ||
|
|
11f8cdb9bd | ||
|
|
dcf0886d89 | ||
|
|
ab6678534f | ||
|
|
ea3b3ceb00 | ||
|
|
2356b0bc9e | ||
|
|
dad1eafecc | ||
|
|
6e5dfc0109 | ||
|
|
43f0ac2e8f | ||
|
|
76a0b0b4b4 | ||
|
|
e6e93f2ebf | ||
|
|
372890811d | ||
|
|
14d0ec73f6 | ||
|
|
fbdaf5d7f3 | ||
|
|
a7d0825a14 | ||
|
|
10feb1fd5b | ||
|
|
832588c7a9 | ||
|
|
005633716e | ||
|
|
a326cb36a1 | ||
|
|
a13aa90875 | ||
|
|
05028894e5 | ||
|
|
87f560c713 | ||
|
|
7fcfa4c201 | ||
|
|
8d1f8edc5a | ||
|
|
825ec722c3 | ||
|
|
664aafb705 | ||
|
|
675a67cfda | ||
|
|
4c955f6725 | ||
|
|
5e932bb1e8 | ||
|
|
91f7a64513 | ||
|
|
14528aad6e | ||
|
|
b13aca47cd | ||
|
|
68d1d21865 | ||
|
|
53b1dd282c | ||
|
|
f5e51d0339 | ||
|
|
27caaa38f9 | ||
|
|
3372f455ca | ||
|
|
3ae2b52649 | ||
|
|
91ed58acc9 | ||
|
|
7b68b19f11 | ||
|
|
ea96c71818 | ||
|
|
a87bd0eb37 | ||
|
|
33d8cb7069 | ||
|
|
52bb58d307 | ||
|
|
59af15961f | ||
|
|
533295ab76 | ||
|
|
39815b5a66 | ||
|
|
27a479f9c4 | ||
|
|
f855deb4b1 | ||
|
|
723bbb98eb | ||
|
|
9fc6a5a27d | ||
|
|
ab16c153c7 | ||
|
|
08895767a9 | ||
|
|
f9b58904d9 | ||
|
|
25b9c51237 |
23
.github/actions/start-comfyui-server/action.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: Start ComfyUI Server
|
||||
description: 'Start ComfyUI server in a container environment (assumes ComfyUI is pre-installed)'
|
||||
|
||||
inputs:
|
||||
front_end_root:
|
||||
description: 'Path to frontend dist directory'
|
||||
required: false
|
||||
default: '$GITHUB_WORKSPACE/dist'
|
||||
timeout:
|
||||
description: 'Timeout in seconds for server startup'
|
||||
required: false
|
||||
default: '600'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Copy devtools and start server
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cp -r ./tools/devtools/* /ComfyUI/custom_nodes/ComfyUI_devtools/
|
||||
cd /ComfyUI && python3 main.py --cpu --multi-user --front-end-root "${{ inputs.front_end_root }}" &
|
||||
wait-for-it --service 127.0.0.1:8188 -t ${{ inputs.timeout }}
|
||||
5
.github/workflows/ci-tests-e2e-forks.yaml
vendored
@@ -1,9 +1,9 @@
|
||||
# Description: Deploys test results from forked PRs (forks can't access deployment secrets)
|
||||
name: "CI: Tests E2E (Deploy for Forks)"
|
||||
name: 'CI: Tests E2E (Deploy for Forks)'
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["CI: Tests E2E"]
|
||||
workflows: ['CI: Tests E2E']
|
||||
types: [requested, completed]
|
||||
|
||||
env:
|
||||
@@ -81,6 +81,7 @@ jobs:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
GITHUB_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||
run: |
|
||||
# Rename merged report if exists
|
||||
[ -d "reports/playwright-report-chromium-merged" ] && \
|
||||
|
||||
116
.github/workflows/ci-tests-e2e.yaml
vendored
@@ -1,5 +1,5 @@
|
||||
# Description: End-to-end testing with Playwright across multiple browsers, deploys test reports to Cloudflare Pages
|
||||
name: "CI: Tests E2E"
|
||||
name: 'CI: Tests E2E'
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -15,66 +15,56 @@ concurrency:
|
||||
jobs:
|
||||
setup:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
cache-key: ${{ steps.cache-key.outputs.key }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
# Setup Test Environment, build frontend but do not start server yet
|
||||
- name: Setup ComfyUI server
|
||||
uses: ./.github/actions/setup-comfyui-server
|
||||
- name: Setup frontend
|
||||
uses: ./.github/actions/setup-frontend
|
||||
with:
|
||||
include_build_step: true
|
||||
- name: Setup Playwright
|
||||
uses: ./.github/actions/setup-playwright # Setup Playwright and cache browsers
|
||||
|
||||
# Save the entire workspace as cache for later test jobs to restore
|
||||
- name: Generate cache key
|
||||
id: cache-key
|
||||
run: echo "key=$(date +%s)" >> $GITHUB_OUTPUT
|
||||
- name: Save cache
|
||||
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684
|
||||
# Upload only built dist/ (containerized test jobs will pnpm install without cache)
|
||||
- name: Upload built frontend
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: .
|
||||
key: comfyui-setup-${{ steps.cache-key.outputs.key }}
|
||||
name: frontend-dist
|
||||
path: dist/
|
||||
retention-days: 1
|
||||
|
||||
# Sharded chromium tests
|
||||
playwright-tests-chromium-sharded:
|
||||
needs: setup
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
container:
|
||||
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.10
|
||||
credentials:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8]
|
||||
shardTotal: [8]
|
||||
steps:
|
||||
# download built frontend repo from setup job
|
||||
- name: Wait for cache propagation
|
||||
run: sleep 10
|
||||
- name: Restore cached setup
|
||||
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
- name: Download built frontend
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
fail-on-cache-miss: true
|
||||
path: .
|
||||
key: comfyui-setup-${{ needs.setup.outputs.cache-key }}
|
||||
name: frontend-dist
|
||||
path: dist/
|
||||
|
||||
# Setup Test Environment for this runner, start server, use cached built frontend ./dist from 'setup' job
|
||||
- name: Setup ComfyUI server
|
||||
uses: ./.github/actions/setup-comfyui-server
|
||||
with:
|
||||
launch_server: true
|
||||
- name: Setup nodejs, pnpm, reuse built frontend
|
||||
uses: ./.github/actions/setup-frontend
|
||||
- name: Setup Playwright
|
||||
uses: ./.github/actions/setup-playwright
|
||||
- name: Start ComfyUI server
|
||||
uses: ./.github/actions/start-comfyui-server
|
||||
|
||||
# Run sharded tests and upload sharded reports
|
||||
- name: Install frontend deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
# Run sharded tests (browsers pre-installed in container)
|
||||
- name: Run Playwright tests (Shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
|
||||
id: playwright
|
||||
run: pnpm exec playwright test --project=chromium --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --reporter=blob
|
||||
@@ -94,39 +84,37 @@ jobs:
|
||||
timeout-minutes: 15
|
||||
needs: setup
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.10
|
||||
credentials:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
browser: [chromium-2x, chromium-0.5x, mobile-chrome]
|
||||
steps:
|
||||
# download built frontend repo from setup job
|
||||
- name: Wait for cache propagation
|
||||
run: sleep 10
|
||||
- name: Restore cached setup
|
||||
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
- name: Download built frontend
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
fail-on-cache-miss: true
|
||||
path: .
|
||||
key: comfyui-setup-${{ needs.setup.outputs.cache-key }}
|
||||
name: frontend-dist
|
||||
path: dist/
|
||||
|
||||
# Setup Test Environment for this runner, start server, use cached built frontend ./dist from 'setup' job
|
||||
- name: Setup ComfyUI server
|
||||
uses: ./.github/actions/setup-comfyui-server
|
||||
with:
|
||||
launch_server: true
|
||||
- name: Setup nodejs, pnpm, reuse built frontend
|
||||
uses: ./.github/actions/setup-frontend
|
||||
- name: Setup Playwright
|
||||
uses: ./.github/actions/setup-playwright
|
||||
- name: Start ComfyUI server
|
||||
uses: ./.github/actions/start-comfyui-server
|
||||
|
||||
# Run tests and upload reports
|
||||
- name: Install frontend deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
# Run tests (browsers pre-installed in container)
|
||||
- name: Run Playwright tests (${{ matrix.browser }})
|
||||
id: playwright
|
||||
run: |
|
||||
# Run tests with blob reporter first
|
||||
pnpm exec playwright test --project=${{ matrix.browser }} --reporter=blob
|
||||
run: pnpm exec playwright test --project=${{ matrix.browser }} --reporter=blob
|
||||
env:
|
||||
PLAYWRIGHT_BLOB_OUTPUT_DIR: ./blob-report
|
||||
|
||||
@@ -147,7 +135,7 @@ jobs:
|
||||
path: ./playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
# Merge sharded test reports
|
||||
# Merge sharded test reports (no container needed - only runs CLI)
|
||||
merge-reports:
|
||||
needs: [playwright-tests-chromium-sharded]
|
||||
runs-on: ubuntu-latest
|
||||
@@ -156,11 +144,10 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
# Setup Test Environment, we only need playwright to merge reports
|
||||
- name: Setup frontend
|
||||
uses: ./.github/actions/setup-frontend
|
||||
- name: Setup Playwright
|
||||
uses: ./.github/actions/setup-playwright
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Download blob reports
|
||||
uses: actions/download-artifact@v4
|
||||
@@ -172,10 +159,10 @@ jobs:
|
||||
- name: Merge into HTML Report
|
||||
run: |
|
||||
# Generate HTML report
|
||||
pnpm exec playwright merge-reports --reporter=html ./all-blob-reports
|
||||
pnpm dlx @playwright/test merge-reports --reporter=html ./all-blob-reports
|
||||
# Generate JSON report separately with explicit output path
|
||||
PLAYWRIGHT_JSON_OUTPUT_NAME=playwright-report/report.json \
|
||||
pnpm exec playwright merge-reports --reporter=json ./all-blob-reports
|
||||
pnpm dlx @playwright/test merge-reports --reporter=json ./all-blob-reports
|
||||
|
||||
- name: Upload HTML report
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -236,6 +223,7 @@ jobs:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
GITHUB_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
bash ./scripts/cicd/pr-playwright-deploy-and-comment.sh \
|
||||
"${{ github.event.pull_request.number }}" \
|
||||
|
||||
@@ -25,7 +25,6 @@ jobs:
|
||||
) &&
|
||||
startsWith(github.event.comment.body, '/update-playwright') )
|
||||
outputs:
|
||||
cache-key: ${{ steps.cache-key.outputs.key }}
|
||||
pr-number: ${{ steps.pr-info.outputs.pr-number }}
|
||||
branch: ${{ steps.pr-info.outputs.branch }}
|
||||
comment-id: ${{ steps.find-update-comment.outputs.comment-id }}
|
||||
@@ -64,70 +63,63 @@ jobs:
|
||||
uses: ./.github/actions/setup-frontend
|
||||
with:
|
||||
include_build_step: true
|
||||
# Save expensive build artifacts (Python env, built frontend, node_modules)
|
||||
# Source code will be checked out fresh in sharded jobs
|
||||
- name: Generate cache key
|
||||
id: cache-key
|
||||
run: echo "key=$(date +%s)" >> $GITHUB_OUTPUT
|
||||
- name: Save cache
|
||||
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684
|
||||
|
||||
# Upload built dist/ (containerized test jobs will pnpm install without cache)
|
||||
- name: Upload built frontend
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: |
|
||||
ComfyUI
|
||||
dist
|
||||
key: comfyui-setup-${{ steps.cache-key.outputs.key }}
|
||||
name: frontend-dist
|
||||
path: dist/
|
||||
retention-days: 1
|
||||
|
||||
# Sharded snapshot updates
|
||||
update-snapshots-sharded:
|
||||
needs: setup
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.10
|
||||
credentials:
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: read
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shardIndex: [1, 2, 3, 4]
|
||||
shardTotal: [4]
|
||||
steps:
|
||||
# Checkout source code fresh (not cached)
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ needs.setup.outputs.branch }}
|
||||
|
||||
# Restore expensive build artifacts from setup job
|
||||
- name: Restore cached artifacts
|
||||
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684
|
||||
- name: Download built frontend
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
fail-on-cache-miss: true
|
||||
path: |
|
||||
ComfyUI
|
||||
dist
|
||||
key: comfyui-setup-${{ needs.setup.outputs.cache-key }}
|
||||
name: frontend-dist
|
||||
path: dist/
|
||||
|
||||
- name: Setup ComfyUI server (from cache)
|
||||
uses: ./.github/actions/setup-comfyui-server
|
||||
with:
|
||||
launch_server: true
|
||||
- name: Start ComfyUI server
|
||||
uses: ./.github/actions/start-comfyui-server
|
||||
|
||||
- name: Setup nodejs, pnpm, reuse built frontend
|
||||
uses: ./.github/actions/setup-frontend
|
||||
- name: Install frontend deps
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Setup Playwright
|
||||
uses: ./.github/actions/setup-playwright
|
||||
|
||||
# Run sharded tests with snapshot updates
|
||||
# Run sharded tests with snapshot updates (browsers pre-installed in container)
|
||||
- name: Update snapshots (Shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
|
||||
id: playwright-tests
|
||||
run: pnpm exec playwright test --update-snapshots --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
|
||||
continue-on-error: true
|
||||
|
||||
# Identify and stage only changed snapshot files
|
||||
- name: Stage changed snapshot files
|
||||
id: changed-snapshots
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "=========================================="
|
||||
echo "STAGING CHANGED SNAPSHOTS (Shard ${{ matrix.shardIndex }})"
|
||||
echo "=========================================="
|
||||
|
||||
# Configure git safe.directory for container environment
|
||||
git config --global --add safe.directory "$(pwd)"
|
||||
|
||||
# Get list of changed snapshot files (including untracked/new files)
|
||||
changed_files=$( (
|
||||
@@ -136,43 +128,25 @@ jobs:
|
||||
) | sort -u | grep -E '\-snapshots/' || true )
|
||||
|
||||
if [ -z "$changed_files" ]; then
|
||||
echo "No snapshot changes in this shard"
|
||||
echo "No snapshot changes in shard ${{ matrix.shardIndex }}"
|
||||
echo "has-changes=false" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "✓ Found changed files:"
|
||||
echo "$changed_files"
|
||||
file_count=$(echo "$changed_files" | wc -l)
|
||||
echo "Count: $file_count"
|
||||
echo "Shard ${{ matrix.shardIndex }}: $file_count changed snapshot(s):"
|
||||
echo "$changed_files"
|
||||
echo "has-changes=true" >> $GITHUB_OUTPUT
|
||||
echo ""
|
||||
|
||||
# Create staging directory
|
||||
# Copy changed files to staging directory
|
||||
mkdir -p /tmp/changed_snapshots_shard
|
||||
|
||||
# Copy only changed files, preserving directory structure
|
||||
# Strip 'browser_tests/' prefix to avoid double nesting
|
||||
echo "Copying changed files to staging directory..."
|
||||
while IFS= read -r file; do
|
||||
# Skip paths that no longer exist (e.g. deletions)
|
||||
if [ ! -f "$file" ]; then
|
||||
echo " → (skipped; not a file) $file"
|
||||
continue
|
||||
fi
|
||||
# Remove 'browser_tests/' prefix
|
||||
[ -f "$file" ] || continue
|
||||
file_without_prefix="${file#browser_tests/}"
|
||||
# Create parent directories
|
||||
mkdir -p "/tmp/changed_snapshots_shard/$(dirname "$file_without_prefix")"
|
||||
# Copy file
|
||||
cp "$file" "/tmp/changed_snapshots_shard/$file_without_prefix"
|
||||
echo " → $file_without_prefix"
|
||||
done <<< "$changed_files"
|
||||
|
||||
echo ""
|
||||
echo "Staged files for upload:"
|
||||
find /tmp/changed_snapshots_shard -type f
|
||||
|
||||
# Upload ONLY the changed files from this shard
|
||||
- name: Upload changed snapshots
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -213,9 +187,15 @@ jobs:
|
||||
echo "=========================================="
|
||||
echo "DOWNLOADED SNAPSHOT FILES"
|
||||
echo "=========================================="
|
||||
find ./downloaded-snapshots -type f
|
||||
echo ""
|
||||
echo "Total files: $(find ./downloaded-snapshots -type f | wc -l)"
|
||||
if [ -d "./downloaded-snapshots" ]; then
|
||||
find ./downloaded-snapshots -type f
|
||||
echo ""
|
||||
echo "Total files: $(find ./downloaded-snapshots -type f | wc -l)"
|
||||
else
|
||||
echo "No snapshot artifacts downloaded (no changes in any shard)"
|
||||
echo ""
|
||||
echo "Total files: 0"
|
||||
fi
|
||||
|
||||
# Merge only the changed files into browser_tests
|
||||
- name: Merge changed snapshots
|
||||
@@ -226,6 +206,16 @@ jobs:
|
||||
echo "MERGING CHANGED SNAPSHOTS"
|
||||
echo "=========================================="
|
||||
|
||||
# Check if any artifacts were downloaded
|
||||
if [ ! -d "./downloaded-snapshots" ]; then
|
||||
echo "No snapshot artifacts to merge"
|
||||
echo "=========================================="
|
||||
echo "MERGE COMPLETE"
|
||||
echo "=========================================="
|
||||
echo "Shards merged: 0"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Verify target directory exists
|
||||
if [ ! -d "browser_tests" ]; then
|
||||
echo "::error::Target directory 'browser_tests' does not exist"
|
||||
|
||||
12
.github/workflows/release-biweekly-comfyui.yaml
vendored
@@ -69,7 +69,7 @@ jobs:
|
||||
- name: Checkout ComfyUI (sparse)
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
repository: comfyanonymous/ComfyUI
|
||||
repository: Comfy-Org/ComfyUI
|
||||
sparse-checkout: |
|
||||
requirements.txt
|
||||
path: comfyui
|
||||
@@ -184,7 +184,7 @@ jobs:
|
||||
# Note: This only affects the local checkout, NOT the fork's master branch
|
||||
# We only push the automation branch, leaving the fork's master untouched
|
||||
echo "Fetching upstream master..."
|
||||
if ! git fetch https://github.com/comfyanonymous/ComfyUI.git master; then
|
||||
if ! git fetch https://github.com/Comfy-Org/ComfyUI.git master; then
|
||||
echo "Failed to fetch upstream master"
|
||||
exit 1
|
||||
fi
|
||||
@@ -257,7 +257,7 @@ jobs:
|
||||
# Extract fork owner from repository name
|
||||
FORK_OWNER=$(echo "$COMFYUI_FORK" | cut -d'/' -f1)
|
||||
|
||||
echo "Creating PR from ${COMFYUI_FORK} to comfyanonymous/ComfyUI"
|
||||
echo "Creating PR from ${COMFYUI_FORK} to Comfy-Org/ComfyUI"
|
||||
|
||||
# Configure git
|
||||
git config user.name "github-actions[bot]"
|
||||
@@ -288,7 +288,7 @@ jobs:
|
||||
|
||||
# Try to create PR, ignore error if it already exists
|
||||
if ! gh pr create \
|
||||
--repo comfyanonymous/ComfyUI \
|
||||
--repo Comfy-Org/ComfyUI \
|
||||
--head "${FORK_OWNER}:${BRANCH}" \
|
||||
--base master \
|
||||
--title "Bump comfyui-frontend-package to ${{ needs.resolve-version.outputs.target_version }}" \
|
||||
@@ -297,7 +297,7 @@ jobs:
|
||||
|
||||
# Check if PR already exists
|
||||
set +e
|
||||
EXISTING_PR=$(gh pr list --repo comfyanonymous/ComfyUI --head "${FORK_OWNER}:${BRANCH}" --json number --jq '.[0].number' 2>&1)
|
||||
EXISTING_PR=$(gh pr list --repo Comfy-Org/ComfyUI --head "${FORK_OWNER}:${BRANCH}" --json number --jq '.[0].number' 2>&1)
|
||||
PR_LIST_EXIT=$?
|
||||
set -e
|
||||
|
||||
@@ -318,7 +318,7 @@ jobs:
|
||||
run: |
|
||||
echo "## ComfyUI PR Created" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Draft PR created in comfyanonymous/ComfyUI" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Draft PR created in Comfy-Org/ComfyUI" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### PR Body:" >> $GITHUB_STEP_SUMMARY
|
||||
cat pr-body.txt >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
@@ -6,10 +6,11 @@ const { defineConfig } = require('@lobehub/i18n-cli');
|
||||
module.exports = defineConfig({
|
||||
modelName: 'gpt-4.1',
|
||||
splitToken: 1024,
|
||||
saveImmediately: true,
|
||||
entry: 'src/locales/en',
|
||||
entryLocale: 'en',
|
||||
output: 'src/locales',
|
||||
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar', 'tr', 'pt-BR'],
|
||||
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar', 'tr', 'pt-BR', 'fa'],
|
||||
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream, Civitai, Hugging Face.
|
||||
'latent' is the short form of 'latent space'.
|
||||
'mask' is in the context of image processing.
|
||||
@@ -18,5 +19,11 @@ module.exports = defineConfig({
|
||||
- For 'zh' locale: Use ONLY Simplified Chinese characters (简体中文). Common examples: 节点 (not 節點), 画布 (not 畫布), 图像 (not 圖像), 选择 (not 選擇), 减小 (not 減小).
|
||||
- For 'zh-TW' locale: Use ONLY Traditional Chinese characters (繁體中文) with Taiwan-specific terminology.
|
||||
- NEVER mix Simplified and Traditional Chinese characters within the same locale.
|
||||
|
||||
IMPORTANT Persian Translation Guidelines:
|
||||
- For 'fa' locale: Use formal Persian (فارسی رسمی) for professional tone throughout the UI.
|
||||
- Keep commonly used technical terms in English when they are standard in Persian software (e.g., node, workflow).
|
||||
- Use Arabic-Indic numerals (۰-۹) for numbers where appropriate.
|
||||
- Maintain consistency with terminology used in Persian software and design applications.
|
||||
`
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { InlineConfig } from 'vite'
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
||||
addons: ['@storybook/addon-docs'],
|
||||
addons: ['@storybook/addon-docs', '@storybook/addon-mcp'],
|
||||
framework: {
|
||||
name: '@storybook/vue3-vite',
|
||||
options: {}
|
||||
@@ -69,9 +69,32 @@ const config: StorybookConfig = {
|
||||
allowedHosts: true
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': process.cwd() + '/src'
|
||||
}
|
||||
alias: [
|
||||
{
|
||||
find: '@/composables/queue/useJobList',
|
||||
replacement: process.cwd() + '/src/storybook/mocks/useJobList.ts'
|
||||
},
|
||||
{
|
||||
find: '@/composables/queue/useJobActions',
|
||||
replacement: process.cwd() + '/src/storybook/mocks/useJobActions.ts'
|
||||
},
|
||||
{
|
||||
find: '@/utils/formatUtil',
|
||||
replacement:
|
||||
process.cwd() +
|
||||
'/packages/shared-frontend-utils/src/formatUtil.ts'
|
||||
},
|
||||
{
|
||||
find: '@/utils/networkUtil',
|
||||
replacement:
|
||||
process.cwd() +
|
||||
'/packages/shared-frontend-utils/src/networkUtil.ts'
|
||||
},
|
||||
{
|
||||
find: '@',
|
||||
replacement: process.cwd() + '/src'
|
||||
}
|
||||
]
|
||||
},
|
||||
esbuild: {
|
||||
// Prevent minification of identifiers to preserve _sfc_main
|
||||
|
||||
14
AGENTS.md
@@ -63,6 +63,9 @@ The project uses **Nx** for build orchestration and task management
|
||||
- Imports:
|
||||
- sorted/grouped by plugin
|
||||
- run `pnpm format` before committing
|
||||
- use separate `import type` statements, not inline `type` in mixed imports
|
||||
- ✅ `import type { Foo } from './foo'` + `import { bar } from './foo'`
|
||||
- ❌ `import { bar, type Foo } from './foo'`
|
||||
- ESLint:
|
||||
- Vue + TS rules
|
||||
- no floating promises
|
||||
@@ -119,7 +122,10 @@ The project uses **Nx** for build orchestration and task management
|
||||
- Prefer reactive props destructuring to `const props = defineProps<...>`
|
||||
- Do not use `withDefaults` or runtime props declaration
|
||||
- Do not import Vue macros unnecessarily
|
||||
- Prefer `useModel` to separately defining a prop and emit
|
||||
- Prefer `defineModel` to separately defining a prop and emit for v-model bindings
|
||||
- Define slots via template usage, not `defineSlots`
|
||||
- Use same-name shorthand for slot prop bindings: `:isExpanded` instead of `:is-expanded="isExpanded"`
|
||||
- Derive component types using `vue-component-type-helpers` (`ComponentProps`, `ComponentSlots`) instead of separate type files
|
||||
- Be judicious with addition of new refs or other state
|
||||
- If it's possible to accomplish the design goals with just a prop, don't add a `ref`
|
||||
- If it's possible to use the `ref` or prop directly, don't add a `computed`
|
||||
@@ -137,7 +143,7 @@ The project uses **Nx** for build orchestration and task management
|
||||
8. Implement proper error handling
|
||||
9. Follow Vue 3 style guide and naming conventions
|
||||
10. Use Vite for fast development and building
|
||||
11. Use vue-i18n in composition API for any string literals. Place new translation entries in src/locales/en/main.json
|
||||
11. Use vue-i18n in composition API for any string literals. Place new translation entries in src/locales/en/main.json. Use the plurals system in i18n instead of hardcoding pluralization in templates.
|
||||
12. Avoid new usage of PrimeVue components
|
||||
13. Write tests for all changes, especially bug fixes to catch future regressions
|
||||
14. Write code that is expressive and self-documenting to the furthest degree possible. This reduces the need for code comments which can get out of sync with the code itself. Try to avoid comments unless absolutely necessary
|
||||
@@ -155,6 +161,8 @@ The project uses **Nx** for build orchestration and task management
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
See @docs/testing/*.md for detailed patterns.
|
||||
|
||||
- Frameworks:
|
||||
- Vitest (unit/component, happy-dom)
|
||||
- Playwright (E2E)
|
||||
@@ -268,6 +276,8 @@ When referencing Comfy-Org repos:
|
||||
- Use `cn()` inline in the template when feasible instead of creating a `computed` to hold the value
|
||||
- NEVER use `!important` or the `!` important prefix for tailwind classes
|
||||
- Find existing `!important` classes that are interfering with the styling and propose corrections of those instead.
|
||||
- NEVER use arbitrary percentage values like `w-[80%]` when a Tailwind fraction utility exists
|
||||
- Use `w-4/5` instead of `w-[80%]`, `w-1/2` instead of `w-[50%]`, etc.
|
||||
|
||||
## Agent-only rules
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
/src/components/graph/selectionToolbox/ @Myestery
|
||||
|
||||
# Minimap
|
||||
/src/renderer/extensions/minimap/ @jtydhr88 @Myestery
|
||||
/src/renderer/extensions/minimap/ @jtydhr88 @Myestery
|
||||
|
||||
# Workflow Templates
|
||||
/src/platform/workflow/templates/ @Myestery @christian-byrne @comfyui-wiki
|
||||
@@ -55,8 +55,7 @@
|
||||
/src/workbench/extensions/manager/ @viva-jinyi @christian-byrne @ltdrdata
|
||||
|
||||
# Translations
|
||||
/src/locales/ @Yorha4D @KarryCharon @shinshin86 @Comfy-Org/comfy_maintainer @Comfy-org/comfy_frontend_devs
|
||||
/src/locales/pt-BR/ @JonatanAtila @Yorha4D @KarryCharon @shinshin86
|
||||
/src/locales/ @Comfy-Org/comfy_maintainer @Comfy-org/comfy_frontend_devs
|
||||
|
||||
# LLM Instructions (blank on purpose)
|
||||
.claude/
|
||||
|
||||
1
THIRD_PARTY_NOTICES.md
Normal file
@@ -0,0 +1 @@
|
||||
AMD and the AMD Arrow logo are trademarks of Advanced Micro Devices, Inc.
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@comfyorg/desktop-ui",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.6",
|
||||
"type": "module",
|
||||
"nx": {
|
||||
"tags": [
|
||||
|
||||
BIN
apps/desktop-ui/public/assets/images/amd-rocm-logo.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
84
apps/desktop-ui/src/components/install/GpuPicker.stories.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
// eslint-disable-next-line storybook/no-renderer-packages
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import type {
|
||||
ElectronAPI,
|
||||
TorchDeviceType
|
||||
} from '@comfyorg/comfyui-electron-types'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import GpuPicker from './GpuPicker.vue'
|
||||
|
||||
type Platform = ReturnType<ElectronAPI['getPlatform']>
|
||||
type ElectronAPIStub = Pick<ElectronAPI, 'getPlatform'>
|
||||
type WindowWithElectron = Window & { electronAPI?: ElectronAPIStub }
|
||||
|
||||
const meta: Meta<typeof GpuPicker> = {
|
||||
title: 'Desktop/Components/GpuPicker',
|
||||
component: GpuPicker,
|
||||
parameters: {
|
||||
layout: 'padded',
|
||||
backgrounds: {
|
||||
default: 'dark',
|
||||
values: [
|
||||
{ name: 'dark', value: '#0a0a0a' },
|
||||
{ name: 'neutral-900', value: '#171717' },
|
||||
{ name: 'neutral-950', value: '#0a0a0a' }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
function createElectronDecorator(platform: Platform) {
|
||||
function getPlatform() {
|
||||
return platform
|
||||
}
|
||||
|
||||
return function ElectronDecorator() {
|
||||
const windowWithElectron = window as WindowWithElectron
|
||||
windowWithElectron.electronAPI = { getPlatform }
|
||||
return { template: '<story />' }
|
||||
}
|
||||
}
|
||||
|
||||
function renderWithDevice(device: TorchDeviceType | null) {
|
||||
return function Render() {
|
||||
return {
|
||||
components: { GpuPicker },
|
||||
setup() {
|
||||
const selected = ref<TorchDeviceType | null>(device)
|
||||
return { selected }
|
||||
},
|
||||
template: `
|
||||
<div class="min-h-screen bg-neutral-950 p-8">
|
||||
<GpuPicker v-model:device="selected" />
|
||||
</div>
|
||||
`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const windowsDecorator = createElectronDecorator('win32')
|
||||
const macDecorator = createElectronDecorator('darwin')
|
||||
|
||||
export const WindowsNvidiaSelected: Story = {
|
||||
decorators: [windowsDecorator],
|
||||
render: renderWithDevice('nvidia')
|
||||
}
|
||||
|
||||
export const WindowsAmdSelected: Story = {
|
||||
decorators: [windowsDecorator],
|
||||
render: renderWithDevice('amd')
|
||||
}
|
||||
|
||||
export const WindowsCpuSelected: Story = {
|
||||
decorators: [windowsDecorator],
|
||||
render: renderWithDevice('cpu')
|
||||
}
|
||||
|
||||
export const MacMpsSelected: Story = {
|
||||
decorators: [macDecorator],
|
||||
render: renderWithDevice('mps')
|
||||
}
|
||||
@@ -11,29 +11,32 @@
|
||||
<!-- Apple Metal / NVIDIA -->
|
||||
<HardwareOption
|
||||
v-if="platform === 'darwin'"
|
||||
:image-path="'./assets/images/apple-mps-logo.png'"
|
||||
image-path="./assets/images/apple-mps-logo.png"
|
||||
placeholder-text="Apple Metal"
|
||||
subtitle="Apple Metal"
|
||||
:value="'mps'"
|
||||
:selected="selected === 'mps'"
|
||||
:recommended="true"
|
||||
@click="pickGpu('mps')"
|
||||
/>
|
||||
<HardwareOption
|
||||
v-else
|
||||
:image-path="'./assets/images/nvidia-logo-square.jpg'"
|
||||
placeholder-text="NVIDIA"
|
||||
:subtitle="$t('install.gpuPicker.nvidiaSubtitle')"
|
||||
:value="'nvidia'"
|
||||
:selected="selected === 'nvidia'"
|
||||
:recommended="true"
|
||||
@click="pickGpu('nvidia')"
|
||||
/>
|
||||
<template v-else>
|
||||
<HardwareOption
|
||||
image-path="./assets/images/nvidia-logo-square.jpg"
|
||||
placeholder-text="NVIDIA"
|
||||
:subtitle="$t('install.gpuPicker.nvidiaSubtitle')"
|
||||
:selected="selected === 'nvidia'"
|
||||
@click="pickGpu('nvidia')"
|
||||
/>
|
||||
<HardwareOption
|
||||
image-path="./assets/images/amd-rocm-logo.png"
|
||||
placeholder-text="AMD"
|
||||
:subtitle="$t('install.gpuPicker.amdSubtitle')"
|
||||
:selected="selected === 'amd'"
|
||||
@click="pickGpu('amd')"
|
||||
/>
|
||||
</template>
|
||||
<!-- CPU -->
|
||||
<HardwareOption
|
||||
placeholder-text="CPU"
|
||||
:subtitle="$t('install.gpuPicker.cpuSubtitle')"
|
||||
:value="'cpu'"
|
||||
:selected="selected === 'cpu'"
|
||||
@click="pickGpu('cpu')"
|
||||
/>
|
||||
@@ -41,7 +44,6 @@
|
||||
<HardwareOption
|
||||
placeholder-text="Manual Install"
|
||||
:subtitle="$t('install.gpuPicker.manualSubtitle')"
|
||||
:value="'unsupported'"
|
||||
:selected="selected === 'unsupported'"
|
||||
@click="pickGpu('unsupported')"
|
||||
/>
|
||||
@@ -81,13 +83,15 @@ const selected = defineModel<TorchDeviceType | null>('device', {
|
||||
const electron = electronAPI()
|
||||
const platform = electron.getPlatform()
|
||||
|
||||
const showRecommendedBadge = computed(
|
||||
() => selected.value === 'mps' || selected.value === 'nvidia'
|
||||
const recommendedDevices: TorchDeviceType[] = ['mps', 'nvidia', 'amd']
|
||||
const showRecommendedBadge = computed(() =>
|
||||
selected.value ? recommendedDevices.includes(selected.value) : false
|
||||
)
|
||||
|
||||
const descriptionKeys = {
|
||||
mps: 'appleMetal',
|
||||
nvidia: 'nvidia',
|
||||
amd: 'amd',
|
||||
cpu: 'cpu',
|
||||
unsupported: 'manual'
|
||||
} as const
|
||||
@@ -97,7 +101,7 @@ const descriptionText = computed(() => {
|
||||
return st(`install.gpuPicker.${key}Description`, '')
|
||||
})
|
||||
|
||||
const pickGpu = (value: TorchDeviceType) => {
|
||||
function pickGpu(value: TorchDeviceType) {
|
||||
selected.value = value
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -29,7 +29,6 @@ export const AppleMetalSelected: Story = {
|
||||
imagePath: '/assets/images/apple-mps-logo.png',
|
||||
placeholderText: 'Apple Metal',
|
||||
subtitle: 'Apple Metal',
|
||||
value: 'mps',
|
||||
selected: true
|
||||
}
|
||||
}
|
||||
@@ -39,7 +38,6 @@ export const AppleMetalUnselected: Story = {
|
||||
imagePath: '/assets/images/apple-mps-logo.png',
|
||||
placeholderText: 'Apple Metal',
|
||||
subtitle: 'Apple Metal',
|
||||
value: 'mps',
|
||||
selected: false
|
||||
}
|
||||
}
|
||||
@@ -48,7 +46,6 @@ export const CPUOption: Story = {
|
||||
args: {
|
||||
placeholderText: 'CPU',
|
||||
subtitle: 'Subtitle',
|
||||
value: 'cpu',
|
||||
selected: false
|
||||
}
|
||||
}
|
||||
@@ -57,7 +54,6 @@ export const ManualInstall: Story = {
|
||||
args: {
|
||||
placeholderText: 'Manual Install',
|
||||
subtitle: 'Subtitle',
|
||||
value: 'unsupported',
|
||||
selected: false
|
||||
}
|
||||
}
|
||||
@@ -67,7 +63,6 @@ export const NvidiaSelected: Story = {
|
||||
imagePath: '/assets/images/nvidia-logo-square.jpg',
|
||||
placeholderText: 'NVIDIA',
|
||||
subtitle: 'NVIDIA',
|
||||
value: 'nvidia',
|
||||
selected: true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,17 +36,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { TorchDeviceType } from '@comfyorg/comfyui-electron-types'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
interface Props {
|
||||
imagePath?: string
|
||||
placeholderText: string
|
||||
subtitle?: string
|
||||
value: TorchDeviceType
|
||||
selected?: boolean
|
||||
recommended?: boolean
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
|
||||
@@ -104,8 +104,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { TorchDeviceType } from '@comfyorg/comfyui-electron-types'
|
||||
import { TorchMirrorUrl } from '@comfyorg/comfyui-electron-types'
|
||||
import type { TorchDeviceType } from '@comfyorg/comfyui-electron-types'
|
||||
import { isInChina } from '@comfyorg/shared-frontend-utils/networkUtil'
|
||||
import Accordion from 'primevue/accordion'
|
||||
import AccordionContent from 'primevue/accordioncontent'
|
||||
@@ -155,7 +155,7 @@ const activeAccordionIndex = ref<string[] | undefined>(undefined)
|
||||
const electron = electronAPI()
|
||||
|
||||
// Mirror configuration logic
|
||||
const getTorchMirrorItem = (device: TorchDeviceType): UVMirror => {
|
||||
function getTorchMirrorItem(device: TorchDeviceType): UVMirror {
|
||||
const settingId = 'Comfy-Desktop.UV.TorchInstallMirror'
|
||||
switch (device) {
|
||||
case 'mps':
|
||||
@@ -170,6 +170,7 @@ const getTorchMirrorItem = (device: TorchDeviceType): UVMirror => {
|
||||
mirror: TorchMirrorUrl.Cuda,
|
||||
fallbackMirror: TorchMirrorUrl.Cuda
|
||||
}
|
||||
case 'amd':
|
||||
case 'cpu':
|
||||
default:
|
||||
return {
|
||||
|
||||
@@ -63,7 +63,6 @@ const taskStore = useMaintenanceTaskStore()
|
||||
defineProps<{
|
||||
displayAsList: string
|
||||
filter: MaintenanceFilter
|
||||
isRefreshing: boolean
|
||||
}>()
|
||||
|
||||
const executeTask = async (task: MaintenanceTask) => {
|
||||
|
||||
@@ -143,6 +143,8 @@ const goToPreviousStep = () => {
|
||||
const electron = electronAPI()
|
||||
const router = useRouter()
|
||||
const install = async () => {
|
||||
if (!device.value) return
|
||||
|
||||
const options: InstallOptions = {
|
||||
installPath: installPath.value,
|
||||
autoUpdate: autoUpdate.value,
|
||||
@@ -152,7 +154,6 @@ const install = async () => {
|
||||
pythonMirror: pythonMirror.value,
|
||||
pypiMirror: pypiMirror.value,
|
||||
torchMirror: torchMirror.value,
|
||||
// @ts-expect-error fixme ts strict error
|
||||
device: device.value
|
||||
}
|
||||
electron.installComfyUI(options)
|
||||
@@ -166,7 +167,11 @@ onMounted(async () => {
|
||||
if (!electron) return
|
||||
|
||||
const detectedGpu = await electron.Config.getDetectedGpu()
|
||||
if (detectedGpu === 'mps' || detectedGpu === 'nvidia') {
|
||||
if (
|
||||
detectedGpu === 'mps' ||
|
||||
detectedGpu === 'nvidia' ||
|
||||
detectedGpu === 'amd'
|
||||
) {
|
||||
device.value = detectedGpu
|
||||
}
|
||||
|
||||
|
||||
@@ -74,7 +74,6 @@
|
||||
class="border-neutral-700 border-solid border-x-0 border-y"
|
||||
:filter
|
||||
:display-as-list
|
||||
:is-refreshing
|
||||
/>
|
||||
|
||||
<!-- Actions -->
|
||||
|
||||
@@ -3,7 +3,7 @@ import { test as base, expect } from '@playwright/test'
|
||||
import dotenv from 'dotenv'
|
||||
import * as fs from 'fs'
|
||||
|
||||
import type { LGraphNode } from '../../src/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode, LGraph } from '../../src/lib/litegraph/src/litegraph'
|
||||
import type { NodeId } from '../../src/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { KeyCombo } from '../../src/schemas/keyBindingSchema'
|
||||
import type { useWorkspaceStore } from '../../src/stores/workspaceStore'
|
||||
@@ -110,16 +110,18 @@ type KeysOfType<T, Match> = {
|
||||
}[keyof T]
|
||||
|
||||
class ConfirmDialog {
|
||||
private readonly root: Locator
|
||||
public readonly delete: Locator
|
||||
public readonly overwrite: Locator
|
||||
public readonly reject: Locator
|
||||
public readonly confirm: Locator
|
||||
|
||||
constructor(public readonly page: Page) {
|
||||
this.delete = page.locator('button.p-button[aria-label="Delete"]')
|
||||
this.overwrite = page.locator('button.p-button[aria-label="Overwrite"]')
|
||||
this.reject = page.locator('button.p-button[aria-label="Cancel"]')
|
||||
this.confirm = page.locator('button.p-button[aria-label="Confirm"]')
|
||||
this.root = page.getByRole('dialog')
|
||||
this.delete = this.root.getByRole('button', { name: 'Delete' })
|
||||
this.overwrite = this.root.getByRole('button', { name: 'Overwrite' })
|
||||
this.reject = this.root.getByRole('button', { name: 'Cancel' })
|
||||
this.confirm = this.root.getByRole('button', { name: 'Confirm' })
|
||||
}
|
||||
|
||||
async click(locator: KeysOfType<ConfirmDialog, Locator>) {
|
||||
@@ -1589,14 +1591,29 @@ export class ComfyPage {
|
||||
return window['app'].graph.nodes
|
||||
})
|
||||
}
|
||||
async getNodeRefsByType(type: string): Promise<NodeReference[]> {
|
||||
async waitForGraphNodes(count: number) {
|
||||
await this.page.waitForFunction((count) => {
|
||||
return window['app']?.canvas.graph?.nodes?.length === count
|
||||
}, count)
|
||||
}
|
||||
async getNodeRefsByType(
|
||||
type: string,
|
||||
includeSubgraph: boolean = false
|
||||
): Promise<NodeReference[]> {
|
||||
return Promise.all(
|
||||
(
|
||||
await this.page.evaluate((type) => {
|
||||
return window['app'].graph.nodes
|
||||
.filter((n: LGraphNode) => n.type === type)
|
||||
.map((n: LGraphNode) => n.id)
|
||||
}, type)
|
||||
await this.page.evaluate(
|
||||
({ type, includeSubgraph }) => {
|
||||
const graph = (
|
||||
includeSubgraph ? window['app'].canvas.graph : window['app'].graph
|
||||
) as LGraph
|
||||
const nodes = graph.nodes
|
||||
return nodes
|
||||
.filter((n: LGraphNode) => n.type === type)
|
||||
.map((n: LGraphNode) => n.id)
|
||||
},
|
||||
{ type, includeSubgraph }
|
||||
)
|
||||
).map((id: NodeId) => this.getNodeRefById(id))
|
||||
)
|
||||
}
|
||||
|
||||
@@ -159,8 +159,18 @@ export class VueNodeHelpers {
|
||||
getInputNumberControls(widget: Locator) {
|
||||
return {
|
||||
input: widget.locator('input'),
|
||||
incrementButton: widget.locator('button').first(),
|
||||
decrementButton: widget.locator('button').nth(1)
|
||||
decrementButton: widget.getByTestId('decrement'),
|
||||
incrementButton: widget.getByTestId('increment')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enter the subgraph of a node.
|
||||
* @param nodeId - The ID of the node to enter the subgraph of. If not provided, the first matched subgraph will be entered.
|
||||
*/
|
||||
async enterSubgraph(nodeId?: string): Promise<void> {
|
||||
const locator = nodeId ? this.getNodeLocator(nodeId) : this.page
|
||||
const editButton = locator.getByTestId('subgraph-enter-button')
|
||||
await editButton.click()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export class ComfyNodeSearchFilterSelectionPanel {
|
||||
async addFilter(filterValue: string, filterType: string) {
|
||||
await this.selectFilterType(filterType)
|
||||
await this.selectFilterValue(filterValue)
|
||||
await this.page.locator('.p-button-label:has-text("Add")').click()
|
||||
await this.page.locator('button:has-text("Add")').click()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -85,11 +85,11 @@ test.describe('Missing models warning', () => {
|
||||
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
|
||||
await expect(missingModelsWarning).toBeVisible()
|
||||
|
||||
const downloadButton = missingModelsWarning.getByLabel('Download')
|
||||
const downloadButton = missingModelsWarning.getByText('Download')
|
||||
await expect(downloadButton).toBeVisible()
|
||||
|
||||
// Check that the copy URL button is also visible for Desktop environment
|
||||
const copyUrlButton = missingModelsWarning.getByLabel('Copy URL')
|
||||
const copyUrlButton = missingModelsWarning.getByText('Copy URL')
|
||||
await expect(copyUrlButton).toBeVisible()
|
||||
})
|
||||
|
||||
@@ -102,11 +102,11 @@ test.describe('Missing models warning', () => {
|
||||
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
|
||||
await expect(missingModelsWarning).toBeVisible()
|
||||
|
||||
const downloadButton = missingModelsWarning.getByLabel('Download')
|
||||
const downloadButton = missingModelsWarning.getByText('Download')
|
||||
await expect(downloadButton).toBeVisible()
|
||||
|
||||
// Check that the copy URL button is also visible for Desktop environment
|
||||
const copyUrlButton = missingModelsWarning.getByLabel('Copy URL')
|
||||
const copyUrlButton = missingModelsWarning.getByText('Copy URL')
|
||||
await expect(copyUrlButton).toBeVisible()
|
||||
})
|
||||
|
||||
@@ -176,7 +176,7 @@ test.describe('Missing models warning', () => {
|
||||
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
|
||||
await expect(missingModelsWarning).toBeVisible()
|
||||
|
||||
const downloadButton = comfyPage.page.getByLabel('Download')
|
||||
const downloadButton = comfyPage.page.getByText('Download')
|
||||
await expect(downloadButton).toBeVisible()
|
||||
const downloadPromise = comfyPage.page.waitForEvent('download')
|
||||
await downloadButton.click()
|
||||
@@ -290,7 +290,7 @@ test.describe('Settings', () => {
|
||||
// Save keybinding
|
||||
const saveButton = comfyPage.page
|
||||
.getByLabel('New Blank Workflow')
|
||||
.getByLabel('Save')
|
||||
.getByText('Save')
|
||||
await saveButton.click()
|
||||
|
||||
const request = await requestPromise
|
||||
|
||||
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
@@ -19,6 +19,7 @@ test.describe('Graph', () => {
|
||||
})
|
||||
|
||||
test('Validate workflow links', async ({ comfyPage }) => {
|
||||
await comfyPage.setSetting('Comfy.Validation.Workflows', true)
|
||||
await comfyPage.loadWorkflow('links/bad_link')
|
||||
await expect(comfyPage.getVisibleToastCount()).resolves.toBe(2)
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 8.7 KiB |
@@ -133,8 +133,11 @@ test.describe('Menu', () => {
|
||||
// Checkmark should be invisible again (panel is hidden)
|
||||
await expect(checkmark).toHaveClass(/invisible/)
|
||||
|
||||
// Click outside to close menu
|
||||
await comfyPage.page.locator('body').click({ position: { x: 10, y: 10 } })
|
||||
// Click in top-right corner to close menu (avoid hamburger menu at top-left)
|
||||
const viewport = comfyPage.page.viewportSize()!
|
||||
await comfyPage.page
|
||||
.locator('body')
|
||||
.click({ position: { x: viewport.width - 10, y: 10 } })
|
||||
|
||||
// Verify menu is now closed
|
||||
await expect(menu).not.toBeVisible()
|
||||
|
||||
@@ -22,8 +22,14 @@ test.describe('Mobile Baseline Snapshots', () => {
|
||||
test('@mobile settings dialog', async ({ comfyPage }) => {
|
||||
await comfyPage.settingDialog.open()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect(comfyPage.settingDialog.root).toHaveScreenshot(
|
||||
'mobile-settings-dialog.png'
|
||||
'mobile-settings-dialog.png',
|
||||
{
|
||||
mask: [
|
||||
comfyPage.settingDialog.root.getByTestId('current-user-indicator')
|
||||
]
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 18 KiB |
@@ -123,8 +123,7 @@ test.describe('Node Help', () => {
|
||||
await expect(helpPage).toContainText('KSampler')
|
||||
|
||||
// Click the back button - use a more specific selector
|
||||
const backButton = comfyPage.page.locator('button:has(.pi-arrow-left)')
|
||||
await expect(backButton).toBeVisible()
|
||||
const backButton = helpPage.getByRole('button', { name: /back/i })
|
||||
await backButton.click()
|
||||
|
||||
// Verify that we're back to the node library view
|
||||
|
||||
@@ -8,13 +8,11 @@ test.describe('Properties panel', () => {
|
||||
|
||||
const { propertiesPanel } = comfyPage.menu
|
||||
|
||||
await expect(propertiesPanel.panelTitle).toContainText(
|
||||
'No node(s) selected'
|
||||
)
|
||||
await expect(propertiesPanel.panelTitle).toContainText('Workflow Overview')
|
||||
|
||||
await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)'])
|
||||
|
||||
await expect(propertiesPanel.panelTitle).toContainText('3 nodes selected')
|
||||
await expect(propertiesPanel.panelTitle).toContainText('3 items selected')
|
||||
await expect(propertiesPanel.root.getByText('KSampler')).toHaveCount(1)
|
||||
await expect(
|
||||
propertiesPanel.root.getByText('CLIP Text Encode (Prompt)')
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
|
||||
|
||||
test.describe('Properties panel position', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
// Open a sidebar tab to ensure sidebar is visible
|
||||
await comfyPage.menu.nodeLibraryTab.open()
|
||||
await comfyPage.actionbar.propertiesButton.click()
|
||||
})
|
||||
|
||||
test('positions on the right when sidebar is on the left', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.setSetting('Comfy.Sidebar.Location', 'left')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const propertiesPanel = comfyPage.page.getByTestId('properties-panel')
|
||||
const sidebar = comfyPage.page.locator('.side-bar-panel').first()
|
||||
|
||||
await expect(propertiesPanel).toBeVisible()
|
||||
await expect(sidebar).toBeVisible()
|
||||
|
||||
const propsBoundingBox = await propertiesPanel.boundingBox()
|
||||
const sidebarBoundingBox = await sidebar.boundingBox()
|
||||
|
||||
expect(propsBoundingBox).not.toBeNull()
|
||||
expect(sidebarBoundingBox).not.toBeNull()
|
||||
|
||||
// Properties panel should be to the right of the sidebar
|
||||
expect(propsBoundingBox!.x).toBeGreaterThan(
|
||||
sidebarBoundingBox!.x + sidebarBoundingBox!.width
|
||||
)
|
||||
})
|
||||
|
||||
test('positions on the left when sidebar is on the right', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.setSetting('Comfy.Sidebar.Location', 'right')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const propertiesPanel = comfyPage.page.getByTestId('properties-panel')
|
||||
const sidebar = comfyPage.page.locator('.side-bar-panel').first()
|
||||
|
||||
await expect(propertiesPanel).toBeVisible()
|
||||
await expect(sidebar).toBeVisible()
|
||||
|
||||
const propsBoundingBox = await propertiesPanel.boundingBox()
|
||||
const sidebarBoundingBox = await sidebar.boundingBox()
|
||||
|
||||
expect(propsBoundingBox).not.toBeNull()
|
||||
expect(sidebarBoundingBox).not.toBeNull()
|
||||
|
||||
// Properties panel should be to the left of the sidebar
|
||||
expect(propsBoundingBox!.x + propsBoundingBox!.width).toBeLessThan(
|
||||
sidebarBoundingBox!.x
|
||||
)
|
||||
})
|
||||
|
||||
test('close button icon updates based on sidebar location', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const propertiesPanel = comfyPage.page.getByTestId('properties-panel')
|
||||
|
||||
// When sidebar is on the left, panel is on the right
|
||||
await comfyPage.setSetting('Comfy.Sidebar.Location', 'left')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
await expect(propertiesPanel).toBeVisible()
|
||||
const closeButtonLeft = propertiesPanel
|
||||
.locator('button[aria-pressed]')
|
||||
.locator('i')
|
||||
await expect(closeButtonLeft).toBeVisible()
|
||||
await expect(closeButtonLeft).toHaveClass(/lucide--panel-right/)
|
||||
|
||||
// When sidebar is on the right, panel is on the left
|
||||
await comfyPage.setSetting('Comfy.Sidebar.Location', 'right')
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
const closeButtonRight = propertiesPanel
|
||||
.locator('button[aria-pressed]')
|
||||
.locator('i')
|
||||
await expect(closeButtonRight).toBeVisible()
|
||||
await expect(closeButtonRight).toHaveClass(/lucide--panel-left/)
|
||||
})
|
||||
})
|
||||
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 93 KiB |
@@ -100,7 +100,7 @@ test.describe('Node library sidebar', () => {
|
||||
const tab = comfyPage.menu.nodeLibraryTab
|
||||
|
||||
await tab.getFolder('foo').click({ button: 'right' })
|
||||
await comfyPage.page.getByLabel('New Folder').click()
|
||||
await comfyPage.page.getByRole('menuitem', { name: 'New Folder' }).click()
|
||||
const textInput = comfyPage.page.locator('.editable-text input')
|
||||
await textInput.waitFor({ state: 'visible' })
|
||||
await textInput.fill('bar')
|
||||
@@ -203,7 +203,7 @@ test.describe('Node library sidebar', () => {
|
||||
await comfyPage.page
|
||||
.locator('.color-field .p-selectbutton > *:nth-child(2)')
|
||||
.click()
|
||||
await comfyPage.page.getByLabel('Confirm').click()
|
||||
await comfyPage.page.getByRole('button', { name: 'Confirm' }).click()
|
||||
await comfyPage.nextFrame()
|
||||
expect(
|
||||
await comfyPage.getSetting('Comfy.NodeLibrary.BookmarksCustomization')
|
||||
@@ -223,7 +223,7 @@ test.describe('Node library sidebar', () => {
|
||||
await comfyPage.page
|
||||
.locator('.icon-field .p-selectbutton > *:nth-child(2)')
|
||||
.click()
|
||||
await comfyPage.page.getByLabel('Confirm').click()
|
||||
await comfyPage.page.getByRole('button', { name: 'Confirm' }).click()
|
||||
await comfyPage.nextFrame()
|
||||
expect(
|
||||
await comfyPage.getSetting('Comfy.NodeLibrary.BookmarksCustomization')
|
||||
@@ -261,7 +261,7 @@ test.describe('Node library sidebar', () => {
|
||||
await comfyPage.page
|
||||
.locator('.icon-field .p-selectbutton > *:nth-child(2)')
|
||||
.click()
|
||||
await comfyPage.page.getByLabel('Confirm').click()
|
||||
await comfyPage.page.getByRole('button', { name: 'Confirm' }).click()
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Verify the color selection is saved
|
||||
|
||||
@@ -83,7 +83,7 @@ test.describe('Templates', () => {
|
||||
|
||||
await comfyPage.page
|
||||
.locator(
|
||||
'nav > div:nth-child(2) > div > span:has-text("Getting Started")'
|
||||
'nav > div:nth-child(3) > div > span:has-text("Getting Started")'
|
||||
)
|
||||
.click()
|
||||
await comfyPage.templates.loadTemplate('default')
|
||||
@@ -109,22 +109,27 @@ test.describe('Templates', () => {
|
||||
})
|
||||
|
||||
test('Uses proper locale files for templates', async ({ comfyPage }) => {
|
||||
// Load the templates dialog and wait for the French index file request
|
||||
const requestPromise = comfyPage.page.waitForRequest(
|
||||
'**/templates/index.fr.json'
|
||||
)
|
||||
|
||||
// Set locale to French before opening templates
|
||||
await comfyPage.setSetting('Comfy.Locale', 'fr')
|
||||
|
||||
await comfyPage.executeCommand('Comfy.BrowseTemplates')
|
||||
|
||||
const request = await requestPromise
|
||||
const dialog = comfyPage.page.getByRole('dialog').filter({
|
||||
has: comfyPage.page.getByRole('heading', { name: 'Modèles', exact: true })
|
||||
})
|
||||
await expect(dialog).toBeVisible()
|
||||
|
||||
// Verify French index was requested
|
||||
expect(request.url()).toContain('templates/index.fr.json')
|
||||
// Validate that French-localized strings from the templates index are rendered
|
||||
await expect(
|
||||
dialog.getByRole('heading', { name: 'Modèles', exact: true })
|
||||
).toBeVisible()
|
||||
await expect(
|
||||
dialog.getByRole('button', { name: 'Tous les modèles', exact: true })
|
||||
).toBeVisible()
|
||||
|
||||
await expect(comfyPage.templates.content).toBeVisible()
|
||||
// Ensure the English fallback copy is not shown anywhere
|
||||
await expect(
|
||||
comfyPage.page.getByText('All Templates', { exact: true })
|
||||
).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('Falls back to English templates when locale file not found', async ({
|
||||
|
||||
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 26 KiB |
@@ -102,7 +102,7 @@ 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.setup()
|
||||
await comfyPage.loadWorkflow('vueNodes/simple-triple')
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
await fitToViewInstant(comfyPage)
|
||||
@@ -993,4 +993,51 @@ test.describe('Vue Node Link Interaction', () => {
|
||||
expect(linked).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
test('Dragging from subgraph input connects to correct slot', async ({
|
||||
comfyPage,
|
||||
comfyMouse
|
||||
}) => {
|
||||
// Setup workflow with a KSampler node
|
||||
await comfyPage.executeCommand('Comfy.NewBlankWorkflow')
|
||||
await comfyPage.waitForGraphNodes(0)
|
||||
await comfyPage.executeCommand('Workspace.SearchBox.Toggle')
|
||||
await comfyPage.nextFrame()
|
||||
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler')
|
||||
await comfyPage.waitForGraphNodes(1)
|
||||
|
||||
// Convert the KSampler node to a subgraph
|
||||
let ksamplerNode = (await comfyPage.getNodeRefsByType('KSampler'))?.[0]
|
||||
await comfyPage.vueNodes.selectNode(String(ksamplerNode.id))
|
||||
await comfyPage.executeCommand('Comfy.Graph.ConvertToSubgraph')
|
||||
|
||||
// Enter the subgraph
|
||||
await comfyPage.vueNodes.enterSubgraph()
|
||||
await fitToViewInstant(comfyPage)
|
||||
|
||||
// Get the KSampler node inside the subgraph
|
||||
ksamplerNode = (await comfyPage.getNodeRefsByType('KSampler', true))?.[0]
|
||||
const positiveInput = await ksamplerNode.getInput(1)
|
||||
const negativeInput = await ksamplerNode.getInput(2)
|
||||
|
||||
const positiveInputPos = await getSlotCenter(
|
||||
comfyPage.page,
|
||||
ksamplerNode.id,
|
||||
1,
|
||||
true
|
||||
)
|
||||
|
||||
const sourceSlot = await comfyPage.getSubgraphInputSlot()
|
||||
const calculatedSourcePos = await sourceSlot.getOpenSlotPosition()
|
||||
|
||||
await comfyMouse.move(calculatedSourcePos)
|
||||
await comfyMouse.drag(positiveInputPos)
|
||||
await comfyMouse.drop()
|
||||
|
||||
// Verify connection went to the correct slot
|
||||
const positiveLinks = await positiveInput.getLinkCount()
|
||||
const negativeLinks = await negativeInput.getLinkCount()
|
||||
expect(positiveLinks).toBe(1)
|
||||
expect(negativeLinks).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 137 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
@@ -194,7 +194,10 @@ test.describe('Image widget', () => {
|
||||
const comboEntry = comfyPage.page.getByRole('menuitem', {
|
||||
name: 'image32x32.webp'
|
||||
})
|
||||
await comboEntry.click({ noWaitAfter: true })
|
||||
await comboEntry.click()
|
||||
|
||||
// Stabilization for the image swap
|
||||
await comfyPage.nextFrame()
|
||||
|
||||
// Expect the image preview to change automatically
|
||||
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||
|
||||
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 91 KiB |
66
docs/TEMPLATE_RANKING.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Template Ranking System
|
||||
|
||||
Usage-based ordering for workflow templates with position bias normalization.
|
||||
|
||||
Scores are pre-computed and normalized offline and shipped as static JSON (mirrors `sorted-custom-node-map.json` pattern for node search).
|
||||
|
||||
## Sort Modes
|
||||
|
||||
| Mode | Formula | Description |
|
||||
| -------------- | ------------------------------------------------ | ---------------------- |
|
||||
| `recommended` | `usage × 0.5 + internal × 0.3 + freshness × 0.2` | Curated recommendation |
|
||||
| `popular` | `usage × 0.9 + freshness × 0.1` | Pure user-driven |
|
||||
| `newest` | Date sort | Existing |
|
||||
| `alphabetical` | Name sort | Existing |
|
||||
|
||||
Freshness computed at runtime from `template.date`: `1.0 / (1 + daysSinceAdded / 90)`, min 0.1.
|
||||
|
||||
## Data Files
|
||||
|
||||
**Usage scores** (generated from Mixpanel):
|
||||
|
||||
```json
|
||||
// In templates/index.json, add to any template:
|
||||
{
|
||||
"name": "some_template",
|
||||
"usage": 1000,
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**Search rank** (set per-template in workflow_templates repo):
|
||||
|
||||
```json
|
||||
// In templates/index.json, add to any template:
|
||||
{
|
||||
"name": "some_template",
|
||||
"searchRank": 8, // Scale 1-10, default 5
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
| searchRank | Effect |
|
||||
| ---------- | ---------------------------- |
|
||||
| 1-4 | Demote (bury in results) |
|
||||
| 5 | Neutral (default if not set) |
|
||||
| 6-10 | Promote (boost in results) |
|
||||
|
||||
## Position Bias Correction
|
||||
|
||||
Raw usage reflects true preference AND UI position bias. We use linear interpolation:
|
||||
|
||||
```
|
||||
correction = 1 + (position - 1) / (maxPosition - 1)
|
||||
normalizedUsage = rawUsage × correction
|
||||
```
|
||||
|
||||
| Position | Boost |
|
||||
| -------- | ----- |
|
||||
| 1 | 1.0× |
|
||||
| 50 | 1.28× |
|
||||
| 100 | 1.57× |
|
||||
| 175 | 2.0× |
|
||||
|
||||
Templates buried at the bottom get up to 2× boost to compensate for reduced visibility.
|
||||
|
||||
---
|
||||
@@ -12,12 +12,17 @@ Documentation for unit tests is organized into three guides:
|
||||
|
||||
## Testing Structure
|
||||
|
||||
The ComfyUI Frontend project uses a mixed approach to unit test organization:
|
||||
The ComfyUI Frontend project uses **colocated tests** - test files are placed alongside their source files:
|
||||
|
||||
- **Component Tests**: Located directly alongside their components with a `.spec.ts` extension
|
||||
- **Unit Tests**: Located in the `tests-ui/tests/` directory
|
||||
- **Store Tests**: Located in the `tests-ui/tests/store/` directory
|
||||
- **Browser Tests**: These are located in the `browser_tests/` directory. There is a dedicated README in the `browser_tests/` directory, so it will not be covered here.
|
||||
- **Component Tests**: Located directly alongside their components (e.g., `MyComponent.test.ts` next to `MyComponent.vue`)
|
||||
- **Unit Tests**: Located alongside their source files (e.g., `myUtil.test.ts` next to `myUtil.ts`)
|
||||
- **Store Tests**: Located in `src/stores/` alongside their store files
|
||||
- **Browser Tests**: Located in the `browser_tests/` directory (see dedicated README there)
|
||||
|
||||
### Test File Naming
|
||||
|
||||
- Use `.test.ts` extension for test files
|
||||
- Name tests after their source file: `sourceFile.test.ts`
|
||||
|
||||
## Test Frameworks and Libraries
|
||||
|
||||
@@ -35,8 +40,11 @@ To run the tests locally:
|
||||
# Run unit tests
|
||||
pnpm test:unit
|
||||
|
||||
# Run a specific test file
|
||||
pnpm test:unit -- src/path/to/file.test.ts
|
||||
|
||||
# Run unit tests in watch mode
|
||||
pnpm test:unit -- --watch
|
||||
```
|
||||
|
||||
Refer to the specific guides for more detailed information on each testing type.
|
||||
Refer to the specific guides for more detailed information on each testing type.
|
||||
138
docs/testing/vitest-patterns.md
Normal file
@@ -0,0 +1,138 @@
|
||||
---
|
||||
globs:
|
||||
- '**/*.test.ts'
|
||||
- '**/*.spec.ts'
|
||||
---
|
||||
|
||||
# Vitest Patterns
|
||||
|
||||
## Setup
|
||||
|
||||
Use `createTestingPinia` from `@pinia/testing`, not `createPinia`:
|
||||
|
||||
```typescript
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
describe('MyStore', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
vi.useFakeTimers()
|
||||
vi.resetAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
**Why `stubActions: false`?** By default, testing pinia stubs all actions. Set to `false` when testing actual store behavior.
|
||||
|
||||
## Mock Patterns
|
||||
|
||||
### Reset all mocks at once
|
||||
|
||||
```typescript
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks() // Not individual mock.mockReset() calls
|
||||
})
|
||||
```
|
||||
|
||||
### Module mocks with vi.mock()
|
||||
|
||||
```typescript
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
api: {
|
||||
addEventListener: vi.fn(),
|
||||
fetchData: vi.fn()
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/services/myService', () => ({
|
||||
myService: {
|
||||
doThing: vi.fn()
|
||||
}
|
||||
}))
|
||||
```
|
||||
|
||||
### Configure mocks in tests
|
||||
|
||||
```typescript
|
||||
import { api } from '@/scripts/api'
|
||||
import { myService } from '@/services/myService'
|
||||
|
||||
it('handles success', () => {
|
||||
vi.mocked(myService.doThing).mockResolvedValue({ data: 'test' })
|
||||
// ... test code
|
||||
})
|
||||
```
|
||||
|
||||
## Testing Event Listeners
|
||||
|
||||
When a store registers event listeners at module load time:
|
||||
|
||||
```typescript
|
||||
function getEventHandler() {
|
||||
const call = vi.mocked(api.addEventListener).mock.calls.find(
|
||||
([event]) => event === 'my_event'
|
||||
)
|
||||
return call?.[1] as (e: CustomEvent<MyEventType>) => void
|
||||
}
|
||||
|
||||
function dispatch(data: MyEventType) {
|
||||
const handler = getEventHandler()
|
||||
handler(new CustomEvent('my_event', { detail: data }))
|
||||
}
|
||||
|
||||
it('handles events', () => {
|
||||
const store = useMyStore()
|
||||
dispatch({ field: 'value' })
|
||||
expect(store.items).toHaveLength(1)
|
||||
})
|
||||
```
|
||||
|
||||
## Testing with Fake Timers
|
||||
|
||||
For stores with intervals, timeouts, or polling:
|
||||
|
||||
```typescript
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('polls after delay', async () => {
|
||||
const store = useMyStore()
|
||||
store.startPolling()
|
||||
|
||||
await vi.advanceTimersByTimeAsync(30000)
|
||||
|
||||
expect(mockService.fetch).toHaveBeenCalled()
|
||||
})
|
||||
```
|
||||
|
||||
## Assertion Style
|
||||
|
||||
Prefer `.toHaveLength()` over `.length.toBe()`:
|
||||
|
||||
```typescript
|
||||
// Good
|
||||
expect(store.items).toHaveLength(1)
|
||||
|
||||
// Avoid
|
||||
expect(store.items.length).toBe(1)
|
||||
```
|
||||
|
||||
Use `.toMatchObject()` for partial matching:
|
||||
|
||||
```typescript
|
||||
expect(store.completedItems[0]).toMatchObject({
|
||||
id: 'task-123',
|
||||
status: 'done'
|
||||
})
|
||||
```
|
||||
@@ -8,7 +8,8 @@ const config: KnipConfig = {
|
||||
'src/assets/css/style.css',
|
||||
'src/main.ts',
|
||||
'src/scripts/ui/menu/index.ts',
|
||||
'src/types/index.ts'
|
||||
'src/types/index.ts',
|
||||
'src/storybook/mocks/**/*.ts'
|
||||
],
|
||||
project: ['**/*.{js,ts,vue}', '*.{js,ts,mts}']
|
||||
},
|
||||
|
||||
23
lint-staged.config.mjs
Normal file
@@ -0,0 +1,23 @@
|
||||
import path from 'node:path'
|
||||
|
||||
export default {
|
||||
'./**/*.js': (stagedFiles) => formatAndEslint(stagedFiles),
|
||||
|
||||
'./**/*.{ts,tsx,vue,mts}': (stagedFiles) => [
|
||||
...formatAndEslint(stagedFiles),
|
||||
'pnpm typecheck'
|
||||
]
|
||||
}
|
||||
|
||||
function formatAndEslint(fileNames) {
|
||||
// Convert absolute paths to relative paths for better ESLint resolution
|
||||
const relativePaths = fileNames.map((f) => path.relative(process.cwd(), f))
|
||||
const joinedPaths = relativePaths.map((p) => `"${p}"`).join(' ')
|
||||
return [
|
||||
`pnpm exec prettier --cache --write ${joinedPaths}`,
|
||||
`pnpm exec oxlint --fix ${joinedPaths}`,
|
||||
`pnpm exec eslint --cache --fix --no-warn-ignored ${joinedPaths}`
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,13 @@
|
||||
"short_name": "ComfyUI",
|
||||
"description": "ComfyUI: AI image generation platform",
|
||||
"start_url": "/",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/assets/images/comfy-logo-single.svg",
|
||||
"sizes": "any",
|
||||
"type": "image/svg+xml"
|
||||
}
|
||||
],
|
||||
"display": "standalone",
|
||||
"background_color": "#ffffff",
|
||||
"theme_color": "#000000"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.36.9",
|
||||
"version": "1.38.2",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -42,6 +42,7 @@
|
||||
"prepare": "husky || true && git config blame.ignoreRevsFile .git-blame-ignore-revs || true",
|
||||
"preview": "nx preview",
|
||||
"storybook": "nx storybook",
|
||||
"storybook:desktop": "nx run @comfyorg/desktop-ui:storybook",
|
||||
"stylelint:fix": "stylelint --cache --fix '{apps,packages,src}/**/*.{css,vue}'",
|
||||
"stylelint": "stylelint --cache '{apps,packages,src}/**/*.{css,vue}'",
|
||||
"test:browser": "pnpm exec nx e2e",
|
||||
@@ -65,6 +66,7 @@
|
||||
"@prettier/plugin-oxc": "catalog:",
|
||||
"@sentry/vite-plugin": "catalog:",
|
||||
"@storybook/addon-docs": "catalog:",
|
||||
"@storybook/addon-mcp": "catalog:",
|
||||
"@storybook/vue3": "catalog:",
|
||||
"@storybook/vue3-vite": "catalog:",
|
||||
"@tailwindcss/vite": "catalog:",
|
||||
@@ -186,6 +188,7 @@
|
||||
"vue-i18n": "catalog:",
|
||||
"vue-router": "catalog:",
|
||||
"vuefire": "catalog:",
|
||||
"wwobjloader2": "catalog:",
|
||||
"yjs": "catalog:",
|
||||
"zod": "catalog:",
|
||||
"zod-validation-error": "catalog:"
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
|
||||
@config '../../tailwind.config.ts';
|
||||
|
||||
@custom-variant touch (@media (hover: none));
|
||||
|
||||
@theme {
|
||||
--text-xxs: 0.625rem;
|
||||
--text-xxs--line-height: calc(1 / 0.625);
|
||||
@@ -245,6 +247,7 @@
|
||||
--inverted-background-hover: var(--color-charcoal-600);
|
||||
--warning-background: var(--color-gold-400);
|
||||
--warning-background-hover: var(--color-gold-500);
|
||||
--success-background: var(--color-jade-600);
|
||||
--border-default: var(--color-smoke-600);
|
||||
--border-subtle: var(--color-smoke-400);
|
||||
--muted-background: var(--color-smoke-700);
|
||||
@@ -279,7 +282,7 @@
|
||||
--modal-card-border-highlighted: var(--secondary-background-selected);
|
||||
--modal-card-button-surface: var(--color-smoke-300);
|
||||
--modal-card-placeholder-background: var(--color-smoke-600);
|
||||
--modal-card-tag-background: var(--color-smoke-400);
|
||||
--modal-card-tag-background: var(--color-smoke-200);
|
||||
--modal-card-tag-foreground: var(--base-foreground);
|
||||
--modal-panel-background: var(--color-white);
|
||||
}
|
||||
@@ -370,6 +373,7 @@
|
||||
--inverted-background-hover: var(--color-smoke-200);
|
||||
--warning-background: var(--color-gold-600);
|
||||
--warning-background-hover: var(--color-gold-500);
|
||||
--success-background: var(--color-jade-600);
|
||||
--border-default: var(--color-charcoal-200);
|
||||
--border-subtle: var(--color-charcoal-300);
|
||||
--muted-background: var(--color-charcoal-100);
|
||||
@@ -514,6 +518,7 @@
|
||||
--color-inverted-background-hover: var(--inverted-background-hover);
|
||||
--color-warning-background: var(--warning-background);
|
||||
--color-warning-background-hover: var(--warning-background-hover);
|
||||
--color-success-background: var(--success-background);
|
||||
--color-border-default: var(--border-default);
|
||||
--color-border-subtle: var(--border-subtle);
|
||||
--color-muted-background: var(--muted-background);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { getMediaTypeFromFilename, truncateFilename } from '@/utils/formatUtil'
|
||||
import { getMediaTypeFromFilename, truncateFilename } from './formatUtil'
|
||||
|
||||
describe('formatUtil', () => {
|
||||
describe('truncateFilename', () => {
|
||||
1029
pnpm-lock.yaml
generated
@@ -4,7 +4,7 @@ packages:
|
||||
|
||||
catalog:
|
||||
'@alloc/quick-lru': ^5.2.0
|
||||
'@comfyorg/comfyui-electron-types': 0.5.5
|
||||
'@comfyorg/comfyui-electron-types': 0.6.2
|
||||
'@eslint/js': ^9.39.1
|
||||
'@iconify-json/lucide': ^1.1.178
|
||||
'@iconify/json': ^2.2.380
|
||||
@@ -15,7 +15,7 @@ catalog:
|
||||
'@nx/playwright': 22.2.6
|
||||
'@nx/storybook': 22.2.4
|
||||
'@nx/vite': 22.2.6
|
||||
'@pinia/testing': ^0.1.5
|
||||
'@pinia/testing': ^1.0.3
|
||||
'@playwright/test': ^1.57.0
|
||||
'@prettier/plugin-oxc': ^0.1.3
|
||||
'@primeuix/forms': 0.0.2
|
||||
@@ -26,9 +26,10 @@ catalog:
|
||||
'@primevue/icons': 4.2.5
|
||||
'@primevue/themes': ^4.2.5
|
||||
'@sentry/vite-plugin': ^4.6.0
|
||||
'@sentry/vue': ^8.48.0
|
||||
'@sentry/vue': ^10.32.1
|
||||
'@sparkjsdev/spark': ^0.1.10
|
||||
'@storybook/addon-docs': ^10.1.9
|
||||
'@storybook/addon-mcp': 0.1.6
|
||||
'@storybook/vue3': ^10.1.9
|
||||
'@storybook/vue3-vite': ^10.1.9
|
||||
'@tailwindcss/vite': ^4.1.12
|
||||
@@ -39,8 +40,8 @@ catalog:
|
||||
'@types/semver': ^7.7.0
|
||||
'@types/three': ^0.169.0
|
||||
'@vitejs/plugin-vue': ^6.0.0
|
||||
'@vitest/coverage-v8': ^3.2.4
|
||||
'@vitest/ui': ^3.2.0
|
||||
'@vitest/coverage-v8': ^4.0.16
|
||||
'@vitest/ui': ^4.0.16
|
||||
'@vue/test-utils': ^2.4.6
|
||||
'@vueuse/core': ^11.0.0
|
||||
'@vueuse/integrations': ^13.9.0
|
||||
@@ -59,11 +60,11 @@ catalog:
|
||||
eslint-plugin-unused-imports: ^4.3.0
|
||||
eslint-plugin-vue: ^10.6.2
|
||||
firebase: ^11.6.0
|
||||
globals: ^15.9.0
|
||||
happy-dom: ^15.11.0
|
||||
globals: ^16.5.0
|
||||
happy-dom: ^20.0.11
|
||||
husky: ^9.1.7
|
||||
jiti: 2.6.1
|
||||
jsdom: ^26.1.0
|
||||
jsdom: ^27.4.0
|
||||
knip: ^5.75.1
|
||||
lint-staged: ^16.2.7
|
||||
markdown-table: ^3.0.4
|
||||
@@ -72,7 +73,7 @@ catalog:
|
||||
oxlint: ^1.33.0
|
||||
oxlint-tsgolint: ^0.9.1
|
||||
picocolors: ^1.1.1
|
||||
pinia: ^2.1.7
|
||||
pinia: ^3.0.4
|
||||
postcss-html: ^1.8.0
|
||||
prettier: ^3.7.4
|
||||
pretty-bytes: ^7.1.0
|
||||
@@ -96,14 +97,15 @@ catalog:
|
||||
vite-plugin-dts: ^4.5.4
|
||||
vite-plugin-html: ^3.2.2
|
||||
vite-plugin-vue-devtools: ^8.0.0
|
||||
vitest: ^3.2.4
|
||||
vitest: ^4.0.16
|
||||
vue: ^3.5.13
|
||||
vue-component-type-helpers: ^3.0.7
|
||||
vue-component-type-helpers: ^3.2.1
|
||||
vue-eslint-parser: ^10.2.0
|
||||
vue-i18n: ^9.14.3
|
||||
vue-router: ^4.4.3
|
||||
vue-tsc: ^3.1.8
|
||||
vue-tsc: ^3.2.1
|
||||
vuefire: ^3.2.1
|
||||
wwobjloader2: ^6.2.1
|
||||
yjs: ^13.6.27
|
||||
zod: ^3.23.8
|
||||
zod-to-json-schema: ^3.24.1
|
||||
|
||||
@@ -10,37 +10,158 @@ interface TestStats {
|
||||
finished?: number
|
||||
}
|
||||
|
||||
interface TestResult {
|
||||
status: string
|
||||
duration?: number
|
||||
error?: {
|
||||
message?: string
|
||||
stack?: string
|
||||
}
|
||||
attachments?: Array<{
|
||||
name: string
|
||||
path?: string
|
||||
contentType: string
|
||||
}>
|
||||
}
|
||||
|
||||
interface TestCase {
|
||||
title: string
|
||||
ok: boolean
|
||||
outcome: string
|
||||
results: TestResult[]
|
||||
}
|
||||
|
||||
interface Suite {
|
||||
title: string
|
||||
file: string
|
||||
suites?: Suite[]
|
||||
tests?: TestCase[]
|
||||
}
|
||||
|
||||
interface FullReportData {
|
||||
stats?: TestStats
|
||||
suites?: Suite[]
|
||||
}
|
||||
|
||||
interface ReportData {
|
||||
stats?: TestStats
|
||||
}
|
||||
|
||||
interface FailedTest {
|
||||
name: string
|
||||
file: string
|
||||
traceUrl?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
interface TestCounts {
|
||||
passed: number
|
||||
failed: number
|
||||
flaky: number
|
||||
skipped: number
|
||||
total: number
|
||||
failures?: FailedTest[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract failed test details from Playwright report
|
||||
*/
|
||||
function extractFailedTests(
|
||||
reportData: FullReportData,
|
||||
baseUrl?: string
|
||||
): FailedTest[] {
|
||||
const failures: FailedTest[] = []
|
||||
|
||||
function processTest(test: TestCase, file: string, suitePath: string[]) {
|
||||
// Check if test failed or is flaky
|
||||
const hasFailed = test.results.some(
|
||||
(r) => r.status === 'failed' || r.status === 'timedOut'
|
||||
)
|
||||
const isFlaky = test.outcome === 'flaky'
|
||||
|
||||
if (hasFailed || isFlaky) {
|
||||
const fullTestName = [...suitePath, test.title]
|
||||
.filter(Boolean)
|
||||
.join(' › ')
|
||||
const failedResult = test.results.find(
|
||||
(r) => r.status === 'failed' || r.status === 'timedOut'
|
||||
)
|
||||
|
||||
// Find trace attachment
|
||||
let traceUrl: string | undefined
|
||||
if (failedResult?.attachments) {
|
||||
const traceAttachment = failedResult.attachments.find(
|
||||
(a) => a.name === 'trace' && a.contentType === 'application/zip'
|
||||
)
|
||||
if (traceAttachment?.path) {
|
||||
// Convert local path to URL path
|
||||
const tracePath = traceAttachment.path.replace(/\\/g, '/')
|
||||
const traceFile = path.basename(tracePath)
|
||||
if (baseUrl) {
|
||||
// Construct trace viewer URL
|
||||
const traceDataUrl = `${baseUrl}/data/${traceFile}`
|
||||
traceUrl = `${baseUrl}/trace/?trace=${encodeURIComponent(traceDataUrl)}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
failures.push({
|
||||
name: fullTestName,
|
||||
file: file,
|
||||
traceUrl,
|
||||
error: failedResult?.error?.message
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function processSuite(suite: Suite, parentPath: string[] = []) {
|
||||
const suitePath = suite.title ? [...parentPath, suite.title] : parentPath
|
||||
|
||||
// Process tests in this suite
|
||||
if (suite.tests) {
|
||||
for (const test of suite.tests) {
|
||||
processTest(test, suite.file, suitePath)
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively process nested suites
|
||||
if (suite.suites) {
|
||||
for (const childSuite of suite.suites) {
|
||||
processSuite(childSuite, suitePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (reportData.suites) {
|
||||
for (const suite of reportData.suites) {
|
||||
processSuite(suite)
|
||||
}
|
||||
}
|
||||
|
||||
return failures
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract test counts from Playwright HTML report
|
||||
* @param reportDir - Path to the playwright-report directory
|
||||
* @returns Test counts { passed, failed, flaky, skipped, total }
|
||||
* @param baseUrl - Base URL of the deployed report (for trace links)
|
||||
* @returns Test counts { passed, failed, flaky, skipped, total, failures }
|
||||
*/
|
||||
function extractTestCounts(reportDir: string): TestCounts {
|
||||
function extractTestCounts(reportDir: string, baseUrl?: string): TestCounts {
|
||||
const counts: TestCounts = {
|
||||
passed: 0,
|
||||
failed: 0,
|
||||
flaky: 0,
|
||||
skipped: 0,
|
||||
total: 0
|
||||
total: 0,
|
||||
failures: []
|
||||
}
|
||||
|
||||
try {
|
||||
// First, try to find report.json which Playwright generates with JSON reporter
|
||||
const jsonReportFile = path.join(reportDir, 'report.json')
|
||||
if (fs.existsSync(jsonReportFile)) {
|
||||
const reportJson: ReportData = JSON.parse(
|
||||
const reportJson: FullReportData = JSON.parse(
|
||||
fs.readFileSync(jsonReportFile, 'utf-8')
|
||||
)
|
||||
if (reportJson.stats) {
|
||||
@@ -54,6 +175,12 @@ function extractTestCounts(reportDir: string): TestCounts {
|
||||
counts.failed = stats.unexpected || 0
|
||||
counts.flaky = stats.flaky || 0
|
||||
counts.skipped = stats.skipped || 0
|
||||
|
||||
// Extract detailed failure information
|
||||
if (counts.failed > 0 || counts.flaky > 0) {
|
||||
counts.failures = extractFailedTests(reportJson, baseUrl)
|
||||
}
|
||||
|
||||
return counts
|
||||
}
|
||||
}
|
||||
@@ -169,15 +296,18 @@ function extractTestCounts(reportDir: string): TestCounts {
|
||||
|
||||
// Main execution
|
||||
const reportDir = process.argv[2]
|
||||
const baseUrl = process.argv[3] // Optional: base URL for trace links
|
||||
|
||||
if (!reportDir) {
|
||||
console.error('Usage: extract-playwright-counts.ts <report-directory>')
|
||||
console.error(
|
||||
'Usage: extract-playwright-counts.ts <report-directory> [base-url]'
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const counts = extractTestCounts(reportDir)
|
||||
const counts = extractTestCounts(reportDir, baseUrl)
|
||||
|
||||
// Output as JSON for easy parsing in shell script
|
||||
console.log(JSON.stringify(counts))
|
||||
process.stdout.write(JSON.stringify(counts) + '\n')
|
||||
|
||||
export { extractTestCounts }
|
||||
export { extractTestCounts, extractFailedTests }
|
||||
|
||||
@@ -134,23 +134,22 @@ post_comment() {
|
||||
|
||||
# Main execution
|
||||
if [ "$STATUS" = "starting" ]; then
|
||||
# Post starting comment
|
||||
# Post concise starting comment
|
||||
comment=$(cat <<EOF
|
||||
$COMMENT_MARKER
|
||||
## 🎭 Playwright Test Results
|
||||
## 🎭 Playwright Tests: ⏳ Running...
|
||||
|
||||
<img alt='loading' src='https://github.com/user-attachments/assets/755c86ee-e445-4ea8-bc2c-cca85df48686' width='14px' height='14px'/> **Tests are starting...**
|
||||
Tests started at $START_TIME UTC
|
||||
|
||||
⏰ Started at: $START_TIME UTC
|
||||
<details>
|
||||
<summary>📊 Browser Tests</summary>
|
||||
|
||||
### 🚀 Running Tests
|
||||
- 🧪 **chromium**: Running tests...
|
||||
- 🧪 **chromium-0.5x**: Running tests...
|
||||
- 🧪 **chromium-2x**: Running tests...
|
||||
- 🧪 **mobile-chrome**: Running tests...
|
||||
- **chromium**: Running...
|
||||
- **chromium-0.5x**: Running...
|
||||
- **chromium-2x**: Running...
|
||||
- **mobile-chrome**: Running...
|
||||
|
||||
---
|
||||
⏱️ Please wait while tests are running...
|
||||
</details>
|
||||
EOF
|
||||
)
|
||||
post_comment "$comment"
|
||||
@@ -189,7 +188,8 @@ else
|
||||
|
||||
if command -v tsx > /dev/null 2>&1 && [ -f "$EXTRACT_SCRIPT" ]; then
|
||||
echo "Extracting counts from $REPORT_DIR using $EXTRACT_SCRIPT" >&2
|
||||
counts=$(tsx "$EXTRACT_SCRIPT" "$REPORT_DIR" 2>&1 || echo '{}')
|
||||
# Pass the base URL so we can generate trace links
|
||||
counts=$(tsx "$EXTRACT_SCRIPT" "$REPORT_DIR" "$url" 2>&1 || echo '{}')
|
||||
echo "Extracted counts for $browser: $counts" >&2
|
||||
echo "$counts" > "$temp_dir/$i.counts"
|
||||
else
|
||||
@@ -286,43 +286,74 @@ else
|
||||
# Determine overall status
|
||||
if [ $total_failed -gt 0 ]; then
|
||||
status_icon="❌"
|
||||
status_text="Some tests failed"
|
||||
status_text="Failed"
|
||||
elif [ $total_flaky -gt 0 ]; then
|
||||
status_icon="⚠️"
|
||||
status_text="Tests passed with flaky tests"
|
||||
status_text="Passed with flaky tests"
|
||||
elif [ $total_tests -gt 0 ]; then
|
||||
status_icon="✅"
|
||||
status_text="All tests passed!"
|
||||
status_text="Passed"
|
||||
else
|
||||
status_icon="🕵🏻"
|
||||
status_text="No test results found"
|
||||
status_text="No test results"
|
||||
fi
|
||||
|
||||
# Generate completion comment
|
||||
# Generate concise completion comment
|
||||
comment="$COMMENT_MARKER
|
||||
## 🎭 Playwright Test Results
|
||||
|
||||
$status_icon **$status_text**
|
||||
|
||||
⏰ Completed at: $(date -u '+%m/%d/%Y, %I:%M:%S %p') UTC"
|
||||
## 🎭 Playwright Tests: $status_icon **$status_text**"
|
||||
|
||||
# Add summary counts if we have test data
|
||||
if [ $total_tests -gt 0 ]; then
|
||||
comment="$comment
|
||||
|
||||
### 📈 Summary
|
||||
- **Total Tests:** $total_tests
|
||||
- **Passed:** $total_passed ✅
|
||||
- **Failed:** $total_failed $([ $total_failed -gt 0 ] && echo '❌' || echo '')
|
||||
- **Flaky:** $total_flaky $([ $total_flaky -gt 0 ] && echo '⚠️' || echo '')
|
||||
- **Skipped:** $total_skipped $([ $total_skipped -gt 0 ] && echo '⏭️' || echo '')"
|
||||
**Results:** $total_passed passed, $total_failed failed, $total_flaky flaky, $total_skipped skipped (Total: $total_tests)"
|
||||
fi
|
||||
|
||||
# Extract and display failed tests from all browsers
|
||||
if [ $total_failed -gt 0 ] || [ $total_flaky -gt 0 ]; then
|
||||
comment="$comment
|
||||
|
||||
### ❌ Failed Tests"
|
||||
|
||||
# Process each browser's failures
|
||||
for counts_json in "${counts_array[@]}"; do
|
||||
[ -z "$counts_json" ] || [ "$counts_json" = "{}" ] && continue
|
||||
|
||||
if command -v jq > /dev/null 2>&1; then
|
||||
# Extract failures array from JSON
|
||||
failures=$(echo "$counts_json" | jq -r '.failures // [] | .[]? | "\(.name)|\(.file)|\(.traceUrl // "")"')
|
||||
|
||||
if [ -n "$failures" ]; then
|
||||
while IFS='|' read -r test_name test_file trace_url; do
|
||||
[ -z "$test_name" ] && continue
|
||||
|
||||
# Convert file path to GitHub URL (relative to repo root)
|
||||
github_file_url="https://github.com/$GITHUB_REPOSITORY/blob/$GITHUB_SHA/$test_file"
|
||||
|
||||
# Build the failed test line
|
||||
test_line="- [$test_name]($github_file_url)"
|
||||
|
||||
if [ -n "$trace_url" ] && [ "$trace_url" != "null" ]; then
|
||||
test_line="$test_line: [View trace]($trace_url)"
|
||||
fi
|
||||
|
||||
comment="$comment
|
||||
$test_line"
|
||||
done <<< "$failures"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Add browser reports in collapsible section
|
||||
comment="$comment
|
||||
|
||||
### 📊 Test Reports by Browser"
|
||||
<details>
|
||||
<summary>📊 Browser Reports</summary>
|
||||
|
||||
"
|
||||
|
||||
# Add browser results with individual counts
|
||||
# Add browser results
|
||||
i=0
|
||||
IFS=' ' read -r -a browser_array <<< "$BROWSERS"
|
||||
IFS=' ' read -r -a url_array <<< "$urls"
|
||||
@@ -349,7 +380,7 @@ $status_icon **$status_text**
|
||||
fi
|
||||
|
||||
if [ -n "$b_total" ] && [ "$b_total" != "0" ]; then
|
||||
counts_str=" • ✅ $b_passed / ❌ $b_failed / ⚠️ $b_flaky / ⏭️ $b_skipped"
|
||||
counts_str=" (✅ $b_passed / ❌ $b_failed / ⚠️ $b_flaky / ⏭️ $b_skipped)"
|
||||
else
|
||||
counts_str=""
|
||||
fi
|
||||
@@ -358,10 +389,10 @@ $status_icon **$status_text**
|
||||
fi
|
||||
|
||||
comment="$comment
|
||||
- ✅ **${browser}**: [View Report](${url})${counts_str}"
|
||||
- **${browser}**: [View Report](${url})${counts_str}"
|
||||
else
|
||||
comment="$comment
|
||||
- ❌ **${browser}**: Deployment failed"
|
||||
- **${browser}**: ❌ Deployment failed"
|
||||
fi
|
||||
i=$((i + 1))
|
||||
done
|
||||
@@ -369,8 +400,7 @@ $status_icon **$status_text**
|
||||
|
||||
comment="$comment
|
||||
|
||||
---
|
||||
🎉 Click on the links above to view detailed test results for each browser configuration."
|
||||
</details>"
|
||||
|
||||
post_comment "$comment"
|
||||
fi
|
||||
|
||||
@@ -1 +1,21 @@
|
||||
@import '@comfyorg/design-system/css/style.css';
|
||||
@import '@comfyorg/design-system/css/style.css';
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
/* List transition animations */
|
||||
.list-scale-move,
|
||||
.list-scale-enter-active,
|
||||
.list-scale-leave-active {
|
||||
transition: opacity 150ms ease, transform 150ms ease;
|
||||
}
|
||||
|
||||
.list-scale-enter-from,
|
||||
.list-scale-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(70%);
|
||||
}
|
||||
|
||||
.list-scale-leave-active {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,29 +22,38 @@
|
||||
state-storage="local"
|
||||
@resizestart="onResizestart"
|
||||
>
|
||||
<!-- First panel: sidebar when left, properties when right -->
|
||||
<SplitterPanel
|
||||
v-if="sidebarLocation === 'left' && !focusMode"
|
||||
:class="
|
||||
cn(
|
||||
'side-bar-panel bg-comfy-menu-bg pointer-events-auto',
|
||||
sidebarPanelVisible && 'min-w-78'
|
||||
)
|
||||
v-if="
|
||||
!focusMode && (sidebarLocation === 'left' || rightSidePanelVisible)
|
||||
"
|
||||
:min-size="10"
|
||||
:class="
|
||||
sidebarLocation === 'left'
|
||||
? cn(
|
||||
'side-bar-panel bg-comfy-menu-bg pointer-events-auto',
|
||||
sidebarPanelVisible && 'min-w-78'
|
||||
)
|
||||
: 'bg-comfy-menu-bg pointer-events-auto'
|
||||
"
|
||||
:min-size="sidebarLocation === 'left' ? 10 : 15"
|
||||
:size="20"
|
||||
:style="{
|
||||
display:
|
||||
sidebarPanelVisible && sidebarLocation === 'left'
|
||||
? 'flex'
|
||||
: 'none'
|
||||
}"
|
||||
:style="firstPanelStyle"
|
||||
:role="sidebarLocation === 'left' ? 'complementary' : undefined"
|
||||
:aria-label="
|
||||
sidebarLocation === 'left' ? t('sideToolbar.sidebar') : undefined
|
||||
"
|
||||
>
|
||||
<slot
|
||||
v-if="sidebarPanelVisible && sidebarLocation === 'left'"
|
||||
v-if="sidebarLocation === 'left' && sidebarPanelVisible"
|
||||
name="side-bar-panel"
|
||||
/>
|
||||
<slot
|
||||
v-else-if="sidebarLocation === 'right'"
|
||||
name="right-side-panel"
|
||||
/>
|
||||
</SplitterPanel>
|
||||
|
||||
<!-- Main panel (always present) -->
|
||||
<SplitterPanel :size="80" class="flex flex-col">
|
||||
<slot name="topmenu" :sidebar-panel-visible />
|
||||
|
||||
@@ -73,38 +82,33 @@
|
||||
</Splitter>
|
||||
</SplitterPanel>
|
||||
|
||||
<!-- Last panel: properties when left, sidebar when right -->
|
||||
<SplitterPanel
|
||||
v-if="sidebarLocation === 'right' && !focusMode"
|
||||
:class="
|
||||
cn(
|
||||
'side-bar-panel pointer-events-auto',
|
||||
sidebarPanelVisible && 'min-w-78'
|
||||
)
|
||||
v-if="
|
||||
!focusMode && (sidebarLocation === 'right' || rightSidePanelVisible)
|
||||
"
|
||||
:min-size="10"
|
||||
:class="
|
||||
sidebarLocation === 'right'
|
||||
? cn(
|
||||
'side-bar-panel bg-comfy-menu-bg pointer-events-auto',
|
||||
sidebarPanelVisible && 'min-w-78'
|
||||
)
|
||||
: 'bg-comfy-menu-bg pointer-events-auto'
|
||||
"
|
||||
:min-size="sidebarLocation === 'right' ? 10 : 15"
|
||||
:size="20"
|
||||
:style="{
|
||||
display:
|
||||
sidebarPanelVisible && sidebarLocation === 'right'
|
||||
? 'flex'
|
||||
: 'none'
|
||||
}"
|
||||
:style="lastPanelStyle"
|
||||
:role="sidebarLocation === 'right' ? 'complementary' : undefined"
|
||||
:aria-label="
|
||||
sidebarLocation === 'right' ? t('sideToolbar.sidebar') : undefined
|
||||
"
|
||||
>
|
||||
<slot v-if="sidebarLocation === 'left'" name="right-side-panel" />
|
||||
<slot
|
||||
v-if="sidebarPanelVisible && sidebarLocation === 'right'"
|
||||
v-else-if="sidebarLocation === 'right' && sidebarPanelVisible"
|
||||
name="side-bar-panel"
|
||||
/>
|
||||
</SplitterPanel>
|
||||
|
||||
<!-- Right Side Panel - independent of sidebar -->
|
||||
<SplitterPanel
|
||||
v-if="rightSidePanelVisible && !focusMode"
|
||||
class="bg-comfy-menu-bg pointer-events-auto"
|
||||
:min-size="15"
|
||||
:size="20"
|
||||
>
|
||||
<slot name="right-side-panel" />
|
||||
</SplitterPanel>
|
||||
</Splitter>
|
||||
</div>
|
||||
</div>
|
||||
@@ -117,6 +121,7 @@ import Splitter from 'primevue/splitter'
|
||||
import type { SplitterResizeStartEvent } from 'primevue/splitter'
|
||||
import SplitterPanel from 'primevue/splitterpanel'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||
@@ -128,6 +133,7 @@ const workspaceStore = useWorkspaceStore()
|
||||
const settingStore = useSettingStore()
|
||||
const rightSidePanelStore = useRightSidePanelStore()
|
||||
const sidebarTabStore = useSidebarTabStore()
|
||||
const { t } = useI18n()
|
||||
const sidebarLocation = computed<'left' | 'right'>(() =>
|
||||
settingStore.get('Comfy.Sidebar.Location')
|
||||
)
|
||||
@@ -159,12 +165,25 @@ function onResizestart({ originalEvent: event }: SplitterResizeStartEvent) {
|
||||
}
|
||||
|
||||
/*
|
||||
* Force refresh the splitter when right panel visibility changes to recalculate the width
|
||||
* Force refresh the splitter when right panel visibility or sidebar location changes
|
||||
* to recalculate the width and panel order
|
||||
*/
|
||||
const splitterRefreshKey = computed(() => {
|
||||
return rightSidePanelVisible.value
|
||||
? 'main-splitter-with-right-panel'
|
||||
: 'main-splitter'
|
||||
return `main-splitter${rightSidePanelVisible.value ? '-with-right-panel' : ''}-${sidebarLocation.value}`
|
||||
})
|
||||
|
||||
const firstPanelStyle = computed(() => {
|
||||
if (sidebarLocation.value === 'left') {
|
||||
return { display: sidebarPanelVisible.value ? 'flex' : 'none' }
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
|
||||
const lastPanelStyle = computed(() => {
|
||||
if (sidebarLocation.value === 'right') {
|
||||
return { display: sidebarPanelVisible.value ? 'flex' : 'none' }
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -5,23 +5,23 @@
|
||||
>
|
||||
<Button
|
||||
v-tooltip="{ value: $t('menu.showMenu'), showDelay: 300 }"
|
||||
icon="pi pi-bars"
|
||||
severity="secondary"
|
||||
text
|
||||
size="large"
|
||||
variant="muted-textonly"
|
||||
size="lg"
|
||||
:aria-label="$t('menu.showMenu')"
|
||||
aria-live="assertive"
|
||||
@click="exitFocusMode"
|
||||
@contextmenu="showNativeSystemMenu"
|
||||
/>
|
||||
>
|
||||
<i class="pi pi-bars" />
|
||||
</Button>
|
||||
<div class="window-actions-spacer" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { watchEffect } from 'vue'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
|
||||
@@ -20,9 +20,14 @@
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
:aria-label="t('menu.customNodesManager')"
|
||||
class="relative"
|
||||
@click="openCustomNodeManager"
|
||||
>
|
||||
<i class="icon-[lucide--puzzle] size-4" />
|
||||
<span
|
||||
v-if="shouldShowRedDot"
|
||||
class="absolute top-0.5 right-1 size-2 rounded-full bg-red-500"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -49,13 +54,16 @@
|
||||
<i class="icon-[lucide--history] size-4" />
|
||||
<span
|
||||
v-if="queuedCount > 0"
|
||||
class="absolute -top-1 -right-1 min-w-[16px] rounded-full bg-primary-background py-0.25 text-[10px] font-medium leading-[14px] text-white"
|
||||
class="absolute -top-1 -right-1 min-w-[16px] rounded-full bg-primary-background py-0.25 text-[10px] font-medium leading-[14px] text-base-foreground"
|
||||
>
|
||||
{{ queuedCount }}
|
||||
</span>
|
||||
</Button>
|
||||
<CurrentUserButton v-if="isLoggedIn" class="shrink-0" />
|
||||
<LoginButton v-else-if="isDesktop" />
|
||||
<CurrentUserButton
|
||||
v-if="isLoggedIn && !isIntegratedTabBar"
|
||||
class="shrink-0"
|
||||
/>
|
||||
<LoginButton v-else-if="isDesktop && !isIntegratedTabBar" />
|
||||
<Button
|
||||
v-if="!isRightSidePanelOpen"
|
||||
v-tooltip.bottom="rightSidePanelTooltipConfig"
|
||||
@@ -91,14 +99,19 @@ import Button from '@/components/ui/button/Button.vue'
|
||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useReleaseStore } from '@/platform/updates/common/releaseStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useQueueStore } from '@/stores/queueStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useQueueStore, useQueueUIStore } from '@/stores/queueStore'
|
||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||
import { useWorkspaceStore } from '@/stores/workspaceStore'
|
||||
import { isElectron } from '@/utils/envUtil'
|
||||
import { useConflictAcknowledgment } from '@/workbench/extensions/manager/composables/useConflictAcknowledgment'
|
||||
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
|
||||
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const rightSidePanelStore = useRightSidePanelStore()
|
||||
const managerState = useManagerState()
|
||||
@@ -106,10 +119,19 @@ const { isLoggedIn } = useCurrentUser()
|
||||
const isDesktop = isElectron()
|
||||
const { t } = useI18n()
|
||||
const { toastErrorHandler } = useErrorHandling()
|
||||
const isQueueOverlayExpanded = ref(false)
|
||||
const commandStore = useCommandStore()
|
||||
const queueStore = useQueueStore()
|
||||
const queueUIStore = useQueueUIStore()
|
||||
const { isOverlayExpanded: isQueueOverlayExpanded } = storeToRefs(queueUIStore)
|
||||
const releaseStore = useReleaseStore()
|
||||
const { shouldShowRedDot: showReleaseRedDot } = storeToRefs(releaseStore)
|
||||
const { shouldShowRedDot: shouldShowConflictRedDot } =
|
||||
useConflictAcknowledgment()
|
||||
const isTopMenuHovered = ref(false)
|
||||
const queuedCount = computed(() => queueStore.pendingTasks.length)
|
||||
const isIntegratedTabBar = computed(
|
||||
() => settingStore.get('Comfy.UI.TabBarLayout') === 'Integrated'
|
||||
)
|
||||
const queueHistoryTooltipConfig = computed(() =>
|
||||
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.viewJobHistory'))
|
||||
)
|
||||
@@ -117,6 +139,12 @@ const customNodesManagerTooltipConfig = computed(() =>
|
||||
buildTooltipConfig(t('menu.customNodesManager'))
|
||||
)
|
||||
|
||||
// Use either release red dot or conflict red dot
|
||||
const shouldShowRedDot = computed((): boolean => {
|
||||
const releaseRedDot = showReleaseRedDot.value
|
||||
return releaseRedDot || shouldShowConflictRedDot.value
|
||||
})
|
||||
|
||||
// Right side panel toggle
|
||||
const { isOpen: isRightSidePanelOpen } = storeToRefs(rightSidePanelStore)
|
||||
const rightSidePanelTooltipConfig = computed(() =>
|
||||
@@ -133,7 +161,7 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
const toggleQueueOverlay = () => {
|
||||
isQueueOverlayExpanded.value = !isQueueOverlayExpanded.value
|
||||
commandStore.execute('Comfy.Queue.ToggleOverlay')
|
||||
}
|
||||
|
||||
const openCustomNodeManager = async () => {
|
||||
|
||||
@@ -22,12 +22,13 @@
|
||||
value: item.tooltip,
|
||||
showDelay: 600
|
||||
}"
|
||||
:label="String(item.label ?? '')"
|
||||
:icon="item.icon"
|
||||
:severity="item.key === queueMode ? 'primary' : 'secondary'"
|
||||
size="small"
|
||||
text
|
||||
/>
|
||||
:variant="item.key === queueMode ? 'primary' : 'secondary'"
|
||||
size="sm"
|
||||
class="w-full justify-start"
|
||||
>
|
||||
<i v-if="item.icon" :class="item.icon" />
|
||||
{{ String(item.label ?? '') }}
|
||||
</Button>
|
||||
</template>
|
||||
</SplitButton>
|
||||
<BatchCountEdit />
|
||||
@@ -36,12 +37,12 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import Button from 'primevue/button'
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import SplitButton from 'primevue/splitbutton'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||