Compare commits

..

18 Commits

Author SHA1 Message Date
Terry Jia
5b3bc77926 call test 2025-11-10 20:25:20 -05:00
BunnyAI
c94cedf8ee add 'SaveVideo' into saveNodeTypes (#6647)
## Summary

add 'SaveVideo' into saveNodeTypes to fix [ComfyUI
10285](https://github.com/comfyanonymous/ComfyUI/issues/10285) and
[ComfyUI 10479](https://github.com/comfyanonymous/ComfyUI/issues/10479)

## Changes

add 'SaveVideo' into saveNodeTypes values

## Review Focus

is this enough?

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6647-add-SaveVideo-into-saveNodeTypes-2a76d73d365081d9bb7eccc358c9836d)
by [Unito](https://www.unito.io)
2025-11-10 19:47:33 -05:00
Comfy Org PR Bot
ba355b543d 1.32.4 (#6641)
Patch version increment to 1.32.4

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6641-1-32-4-2a56d73d365081a8abaaeca5d7473390)
by [Unito](https://www.unito.io)

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2025-11-08 12:24:08 -07:00
Jin Yi
e9b641cfb7 [bugfix] Fix Storybook _sfc_main undefined error after v1.29.3 (#6635)
## Summary

Fixes Storybook rendering issue where all components fail to load with
`_sfc_main is not defined` error in **local development** since v1.29.3.

## Problem

After upgrading to v1.29.3, all Storybook components fail to render **in
local development** (`pnpm storybook`) with the following error:

```
ReferenceError: _sfc_main is not defined
The component failed to render properly, likely due to a configuration issue in Storybook.
```

**Important**: This issue only affects **local development**
environments. The deployed/built Storybook works correctly.

This affects both:
- Main Storybook (`pnpm storybook`)
- Desktop-ui Storybook instances

## Root Cause

In v1.29.3, commit `64430708e` ("perf: tree shaking and minify #6068")
enabled build optimizations in `vite.config.mts`:

```typescript
// Before (v1.29.2)
rollupOptions: {
  treeshake: false
}
esbuild: {
  minifyIdentifiers: false
}

// After (v1.29.3)
rollupOptions: {
  treeshake: true  // ⚠️ Enabled
}
esbuild: {
  minifyIdentifiers: SHOULD_MINIFY  // ⚠️ Conditionally enabled
}
```

While these optimizations are beneficial for production builds, they
cause issues in **Storybook's local dev server**:

1. **Tree-shaking in dev mode**: Rollup incorrectly identifies Vue SFC's
`_sfc_main` exports as unused code during the dev server's module
transformation
2. **Identifier minification**: esbuild minifies `_sfc_main` to shorter
names in development, breaking Storybook's HMR (Hot Module Replacement)
and dynamic module loading

Since Storybook's `main.ts` inherits settings from `vite.config.mts` via
`mergeConfig`, these optimizations were applied to Storybook's dev
server configuration, causing Vue components to fail rendering in local
development.

**Why deployed Storybook works**: Production builds have different
optimization pipelines that handle Vue SFCs correctly, but the dev
server's real-time transformation breaks with these settings.

## Solution

Added explicit build configuration overrides in both Storybook
configurations to ensure the **dev server** doesn't inherit problematic
optimizations:

**Files changed:**
- `.storybook/main.ts`
- `apps/desktop-ui/.storybook/main.ts`

**Changes:**
```typescript
esbuild: {
  // Prevent minification of identifiers to preserve _sfc_main in dev mode
  minifyIdentifiers: false,
  keepNames: true
},
build: {
  rollupOptions: {
    // Disable tree-shaking for Storybook dev server to prevent Vue SFC exports from being removed
    treeshake: false,
    // ... existing onwarn config
  }
}
```

This ensures Storybook's **local development server** prioritizes
stability and debuggability over bundle size optimization, while
production builds continue to benefit from tree-shaking and
minification.

## Testing

1. Cleared Storybook and Vite caches: `rm -rf .storybook/.cache
node_modules/.vite`
2. Started local Storybook dev server with `pnpm storybook`
3. Verified all component stories render correctly without `_sfc_main`
errors
4. Ran `pnpm typecheck` to ensure TypeScript compilation succeeds
5. Tested HMR (Hot Module Replacement) works correctly with component
changes

## Context

- This is a **local development-only** issue; deployed Storybook builds
work fine
- Storybook dev server requires special handling because it dynamically
imports and hot-reloads all stories at runtime
- Vue SFC compilation generates `_sfc_main` as an internal identifier
that must be preserved during dev transformations
- Development tools like Storybook benefit from unoptimized builds for
better debugging, HMR, and stability
- Production builds remain optimized with tree-shaking and minification
enabled

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6635-bugfix-Fix-Storybook-_sfc_main-undefined-error-after-v1-29-3-2a56d73d36508194a25eef56789e5e2b)
by [Unito](https://www.unito.io)
2025-11-08 10:40:33 -07:00
Christian Byrne
56153596d9 fix: release summary comment action (#6640)
Fixes
https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/19188216288/job/54858802407

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6640-fix-release-summary-comment-action-2a56d73d365081f89bf1ec7c4e4818a4)
by [Unito](https://www.unito.io)
2025-11-08 10:39:46 -07:00
Christian Byrne
b679bfe8f8 logging: log context on session storage write error (QuotaError) (#6636)
adds some context to the storage write errors observed in telemetry data
- in order to pinpoint the exact cause.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6636-logging-log-context-on-session-storage-write-error-QuotaError-2a56d73d365081c28bb0dbfdfef8395a)
by [Unito](https://www.unito.io)
2025-11-08 10:19:12 -07:00
Johnpaul Chiwetelu
54979701d0 Cloud feedback Extension (#6626)
This pull request introduces a new extensible system for adding custom
action bar buttons to the top menu, and demonstrates its use by adding a
cloud feedback button. The changes include defining a new
`ActionBarButton` type, updating the extension system to support action
bar buttons, creating a Pinia store to aggregate buttons from
extensions, and updating the UI to render these buttons in the top menu.
The cloud feedback button is now conditionally loaded for cloud
environments.

**Extensible Action Bar Button System**

* Defined a new `ActionBarButton` interface in `comfy.ts` for describing
buttons (icon, label, tooltip, class, click handler) and added an
`actionBarButtons` property to the `ComfyExtension` interface to allow
extensions to contribute buttons.
[[1]](diffhunk://#diff-c29886a1b0c982c6fff3545af0ca
<img width="1134" height="437" alt="Screenshot 2025-11-07 at 01 07 36"
src="https://github.com/user-attachments/assets/36b6145a-0b82-4f7d-88e8-f2ea350a359b"
/>
8ec269876c2cf3948f867d08c14032c04d66R60-R82)
[[2]](diffhunk://#diff-c29886a1b0c982c6fff3545af0ca8ec269876c2cf3948f867d08c14032c04d66R128-R131)
* Created a Pinia store (`actionBarButtonStore.ts`) that collects all
action bar buttons from registered extensions for use in the UI.

**UI Integration**

* Added a new `ActionBarButtons.vue` component that renders all action
bar buttons using the store, and integrated it into the top menu section
(`TopMenuSection.vue`).
[[1]](diffhunk://#diff-d6820f57cde865083d515ac0c23e1ad09e6fbc6792d742ae948a1d3b772be473R1-R28)
[[2]](diffhunk://#diff-45dffe390927ed2d5ba2426a139c52adae28ce15f81821c88e7991944562074cR10)
[[3]](diffhunk://#diff-45dffe390927ed2d5ba2426a139c52adae28ce15f81821c88e7991944562074cR28)

**Cloud Feedback Button Extension**

* Implemented a new extension (`cloudFeedbackTopbarButton.ts`) that
registers a "Feedback" button opening the Zendesk feedback page, and
ensured it loads only in cloud environments.
[[1]](diffhunk://#diff-b84a6a0dfaca19fd77b3fae6999a40c3ab8d04ed22636fcdecc65b385a8e9a98R1-R24)
[[2]](diffhunk://#diff-236993d9e4213efe96d267c75c3292d32b93aa4dd6c3318d26a397e0ae56bc87R32)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6626-Cloud-feedback-Extension-2a46d73d36508170bd07f582ccfabb3c)
by [Unito](https://www.unito.io)
2025-11-08 12:36:04 +01:00
Comfy Org PR Bot
64e704c2f9 1.32.3 (#6634)
Patch version increment to 1.32.3

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6634-1-32-3-2a56d73d365081c7ad4bda4aba8b4076)
by [Unito](https://www.unito.io)

---------

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2025-11-07 21:50:23 -07:00
Benjamin Lu
b1050e3195 Fix session cookie creation race: skip initial token refresh, wrap extension auth hooks (#6563)
Fixes a race causing “No auth header available for session creation”
during sign‑in, by skipping the initial token refresh event, and
wrapping extension auth hooks with async error handling.

Sentry:
https://comfy-org.sentry.io/issues/6990347926/?alert_rule_id=1614600&project=4509681221369857

Context
- Error surfaced as an unhandled rejection when session creation was
triggered without a valid auth header.
- Triggers: both onAuthUserResolved and onAuthTokenRefreshed fired
during initial login.
- Pre‑fix, onIdTokenChanged treated the very first token emission as a
“refresh” as well, so two concurrent createSession() calls ran
back‑to‑back.
- One of those calls could land before a Firebase ID token existed, so
getAuthHeader() returned null → createSession threw “No auth header
available for session creation”.

Exact pre‑fix failure path
- src/extensions/core/cloudSessionCookie.ts
  - onAuthUserResolved → useSessionCookie().createSession()
  - onAuthTokenRefreshed → useSessionCookie().createSession()
- src/stores/firebaseAuthStore.ts
- onIdTokenChanged increments tokenRefreshTrigger even for the initial
token (treated as a refresh)
- getAuthHeader() → getIdToken() may be undefined briefly during
initialization
- src/platform/auth/session/useSessionCookie.ts
- createSession(): calls authStore.getAuthHeader(); if falsy, throws
Error('No auth header available for session creation')

What this PR changes
1) Skip initial token “refresh”
- Track lastTokenUserId and ignore the first onIdTokenChanged for a
user; only subsequent token changes count as refresh events.
   - File: src/stores/firebaseAuthStore.ts
2) Wrap extension auth hooks with async error handling
- Use wrapWithErrorHandlingAsync for
onAuthUserResolved/onAuthTokenRefreshed/onAuthUserLogout callbacks to
avoid unhandled rejections.
   - File: src/services/extensionService.ts

Result
- Eliminates the timing window where createSession() runs before
getIdToken() returns a token.
- Ensures any remaining errors are caught and reported instead of
surfacing as unhandled promise rejections.

Notes
- Lint and typecheck run clean (pnpm lint:fix && pnpm typecheck).

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6563-Fix-session-cookie-creation-race-dedupe-calls-skip-initial-token-refresh-wrap-extensio-2a16d73d365081ef8c22c5ac8cb948aa)
by [Unito](https://www.unito.io)
2025-11-07 21:30:49 -07:00
Benjamin Lu
ba100c4a04 Hide browser tab star when autosave is enabled (#6568)
- Hide "*" indicator in the browser tab title when autosave is enabled
(Comfy.Workflow.AutoSave === 'after delay').
- Refactor: extract readable computed values
(`shouldShowUnsavedIndicator`, `isActiveWorkflowModified`,
`isActiveWorkflowPersisted`).
- Aligns with workflow tab behavior; also hides while Shift is held
(matches in-app tab logic).

Files touched:
- src/composables/useBrowserTabTitle.ts

Validation:
- Ran `pnpm lint:fix` and `pnpm typecheck` — both passed.

Manual test suggestions:
- With autosave set to 'after delay': modify a workflow → browser tab
should not show `*`.
- With autosave 'off': modify or open non-persisted workflow → browser
tab shows `*`.
- Hold Shift: indicator hidden while held (consistent with workflow
tab).

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6568-Hide-browser-tab-star-when-autosave-is-enabled-refactor-title-logic-2a16d73d365081549906e9d1fed07a42)
by [Unito](https://www.unito.io)
2025-11-07 21:30:34 -07:00
Christian Byrne
cafd2de961 ci: comment when publish to npm/pypi finishes successfully (#6628)
This change adds a reusable `post-release-summary` composite action that
automatically figures out the current/previous version, generates diff +
PyPI/npm links, and posts (or updates) the release summary comment
whenever the publish jobs succeed.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6628-ci-comment-when-publish-to-npm-pypi-finishes-successfully-2a46d73d36508181a8d0eb050efe7762)
by [Unito](https://www.unito.io)
2025-11-07 20:55:51 -07:00
Christian Byrne
1f3fb90b1b increase tracking heartbeat interval from 30sec to 5min (#6631)
This event is taking up too much of the quota, increase the interval for
now.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6631-increase-tracking-heartbeat-interval-from-30sec-to-5min-2a46d73d3650814291c4fae50227d4ec)
by [Unito](https://www.unito.io)
2025-11-07 00:05:38 -07:00
Christian Byrne
27afd01297 add language selector to desktop onboarding views (#6591)
Allows changing language during desktop onboarding

<img width="3816" height="2045" alt="Selection_2231"
src="https://github.com/user-attachments/assets/b8a0dda3-70e7-42a9-96f1-10d00e2fd85c"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6591-add-language-selector-to-desktop-onboarding-views-2a26d73d365081f58d00dca2b2759d82)
by [Unito](https://www.unito.io)
2025-11-06 22:17:21 -07:00
Christian Byrne
535f857330 refactor: move renderer-dependent utils into workbench scope (#6621)
This PR cleans up the base-layer utilities so they no longer pull
renderer or workbench code. The renderer-only `isPrimitiveNode` guard
now lives in `src/renderer/utils/nodeTypeGuards.ts`, and the node
help/model/ordering helpers have moved into `src/workbench/utils`. All
affected services, stores, scripts, and tests were updated to import
from the new locations.

The idea is to reduce the number of Base→Renderer/Base→Workbench edges
(higher scoped base/common utils should not import from
renderer/workbench layers).

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6621-refactor-move-renderer-dependent-utils-into-workbench-scope-2a36d73d36508167aff0fc8a22202d7f)
by [Unito](https://www.unito.io)
2025-11-06 19:32:41 -07:00
Marwan Ahmed
adb15aac40 feat: pre-fill user info in Zendesk support link (#6586)
Add user email and ID as URL parameters when opening the Contact Support
link to improve support experience. Only includes user data when logged
in.

## Summary

Enhanced the Contact Support command to automatically pre-fill user
email and ID in Zendesk support tickets, streamlining the support
request process for authenticated users.

## Changes

- **What**: 
- Added `useCurrentUser` composable to access authenticated user data in
`useCoreCommands.ts`
- Modified `Comfy.ContactSupport` command to append user email
(`tf_anonymous_requester_email` and `tf_40029135130388`) and user ID
(`tf_42515251051412`) as URL parameters when available
- Maintained backward compatibility by only adding user parameters when
user is logged in
- Preserved existing `tf_42243568391700` parameter for distribution type
(oss/ccloud)

## Review Focus

- Verify that the URL parameters are correctly appended only when user
is authenticated
- Confirm that non-authenticated users still get the base support URL
with just the distribution type parameter
- Check that both Firebase auth and API key auth users have their
information properly included

Example URLs generated when you press on help locally (it will change
automatically to ccloud on Cloud):
- **Logged out**:
`https://support.comfy.org/hc/en-us/requests/new?tf_42243568391700=oss`
- **Logged in**:
`https://support.comfy.org/hc/en-us/requests/new?tf_42243568391700=ccloud&tf_anonymous_requester_email=user@example.com&tf_40029135130388=user@example.com&tf_42515251051412=abc123xyz`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6586-feat-pre-fill-user-info-in-Zendesk-support-link-2a26d73d36508171b428c634b310f68b)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: bymyself <cbyrne@comfy.org>
2025-11-06 19:17:01 -07:00
Alexander Brown
8752f1b06d fix: re-add translations dropped in 6564 (#6613)
## Summary

Re-adding some strings that got dropped in the merge.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6613-fix-re-add-translations-dropped-in-6564-2a36d73d3650818ca617cb5bddd11bc7)
by [Unito](https://www.unito.io)
2025-11-06 01:02:09 -08:00
Christian Byrne
90c2c0fae0 style: update Vue node designs to use semantic tokens (#6304)
## Summary

Use semantic tokens instead of colors

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6304-style-update-Vue-node-designs-to-use-semantic-tokens-2986d73d365081f69acce7793a218699)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-06 01:01:39 -08:00
Jin Yi
34155bccb1 fix: Handle vite:preloadError for graceful deployment asset updates (#6609)
## Summary
- Implement graceful handling of Vite preload errors that occur when
assets are deleted after new deployments
- Auto-reload when safe (no unsaved changes), show confirmation dialog
when user has unsaved work
- Add i18n support for user-friendly error messages

## Implementation Details
- Add `vite:preloadError` event listener in App.vue 
- Smart reload logic: check `app.vueAppReady` and
`workflowStore.activeWorkflow?.isModified`
- User confirmation dialog using existing `dialogService.confirm`
- Comprehensive i18n keys for title and message

## Background
This addresses the issue described in [Vite
documentation](https://vite.dev/guide/build.html#load-error-handling)
where users encounter import errors when hosting services delete old
assets after new deployments.

[screen-capture
(1).webm](https://github.com/user-attachments/assets/beed3b8e-6f32-4288-a560-55da391a79a1)

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

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6609-fix-Handle-vite-preloadError-for-graceful-deployment-asset-updates-2a36d73d365081a0b3adeac9fcd1e1dc)
by [Unito](https://www.unito.io)
2025-11-06 00:53:56 -08:00
130 changed files with 1240 additions and 735 deletions

View File

@@ -0,0 +1,116 @@
name: Post Release Summary Comment
description: Post or update a PR comment summarizing release links with diff, derived versions, and optional extras.
author: ComfyUI Frontend Team
inputs:
issue-number:
description: Optional PR number override (defaults to the current pull request)
default: ''
version_file:
description: Path to the JSON file containing the current version (relative to repo root)
required: true
outputs:
prev_version:
description: Previous version derived from the parent commit
value: ${{ steps.build.outputs.prev_version }}
runs:
using: composite
steps:
- name: Build comment body
id: build
shell: bash
run: |
set -euo pipefail
VERSION_FILE="${{ inputs.version_file }}"
REPO="${{ github.repository }}"
if [[ -z "$VERSION_FILE" ]]; then
echo '::error::version_file input is required' >&2
exit 1
fi
PREV_JSON=$(git show HEAD^1:"$VERSION_FILE" 2>/dev/null || true)
if [[ -z "$PREV_JSON" ]]; then
echo "::error::Unable to read $VERSION_FILE from parent commit" >&2
exit 1
fi
PREV_VERSION=$(printf '%s' "$PREV_JSON" | node -pe "const data = JSON.parse(require('fs').readFileSync(0, 'utf8')); if (!data.version) { process.exit(1); } data.version")
if [[ -z "$PREV_VERSION" ]]; then
echo "::error::Unable to determine previous version from $VERSION_FILE" >&2
exit 1
fi
NEW_VERSION=$(node -pe "const fs=require('fs');const data=JSON.parse(fs.readFileSync(process.argv[1],'utf8'));if(!data.version){process.exit(1);}data.version" "$VERSION_FILE")
if [[ -z "$NEW_VERSION" ]]; then
echo "::error::Unable to determine current version from $VERSION_FILE" >&2
exit 1
fi
MARKER='release-summary'
MESSAGE='Publish jobs finished successfully:'
LINKS_VALUE=''
case "$VERSION_FILE" in
package.json)
LINKS_VALUE=$'PyPI|https://pypi.org/project/comfyui-frontend-package/{{version}}/\n''npm types|https://npm.im/@comfyorg/comfyui-frontend-types@{{version}}'
;;
apps/desktop-ui/package.json)
MARKER='desktop-release-summary'
LINKS_VALUE='npm desktop UI|https://npm.im/@comfyorg/desktop-ui@{{version}}'
;;
esac
DIFF_PREFIX='v'
DIFF_LABEL='Diff'
DIFF_URL="https://github.com/${REPO}/compare/${DIFF_PREFIX}${PREV_VERSION}...${DIFF_PREFIX}${NEW_VERSION}"
COMMENT_FILE=$(mktemp)
{
printf '<!--%s:%s%s-->\n' "$MARKER" "$DIFF_PREFIX" "$NEW_VERSION"
printf '%s\n\n' "$MESSAGE"
printf -- '- %s: [%s%s...%s%s](%s)\n' "$DIFF_LABEL" "$DIFF_PREFIX" "$PREV_VERSION" "$DIFF_PREFIX" "$NEW_VERSION" "$DIFF_URL"
while IFS= read -r RAW_LINE; do
LINE=$(printf '%s' "$RAW_LINE" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
[[ -z "$LINE" ]] && continue
if [[ "$LINE" != *"|"* ]]; then
echo "::warning::Skipping malformed link entry: $LINE" >&2
continue
fi
LABEL=${LINE%%|*}
URL_TEMPLATE=${LINE#*|}
URL=${URL_TEMPLATE//\{\{version\}\}/$NEW_VERSION}
URL=${URL//\{\{prev_version\}\}/$PREV_VERSION}
printf -- '- %s: %s\n' "$LABEL" "$URL"
done <<< "$LINKS_VALUE"
printf '\n'
} > "$COMMENT_FILE"
{
echo "body<<'EOF'"
cat "$COMMENT_FILE"
echo 'EOF'
} >> "$GITHUB_OUTPUT"
echo "prev_version=$PREV_VERSION" >> "$GITHUB_OUTPUT"
echo "marker_search=<!--$MARKER:" >> "$GITHUB_OUTPUT"
echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
- name: Find existing comment
id: find
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad
with:
issue-number: ${{ inputs.issue-number || github.event.pull_request.number }}
comment-author: github-actions[bot]
body-includes: ${{ steps.build.outputs.marker_search }}
- name: Post or update comment
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9
with:
issue-number: ${{ inputs.issue-number || github.event.pull_request.number }}
comment-id: ${{ steps.find.outputs.comment-id }}
body: ${{ steps.build.outputs.body }}
edit-mode: replace

View File

@@ -1,51 +0,0 @@
name: "CI: Tests E2E Cloud"
description: "Cloud E2E testing with Playwright against stagingcloud.comfy.org"
on:
workflow_dispatch:
push:
branches: [cloud/*, main]
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
playwright-tests-cloud:
runs-on: ubuntu-latest
timeout-minutes: 20
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Setup frontend
uses: ./.github/actions/setup-frontend
with:
include_build_step: false # Cloud tests don't need build
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
- name: Run Playwright cloud tests
id: playwright
env:
CLOUD_TEST_EMAIL: ${{ secrets.CLOUD_TEST_EMAIL }}
CLOUD_TEST_PASSWORD: ${{ secrets.CLOUD_TEST_PASSWORD }}
run: |
PLAYWRIGHT_JSON_OUTPUT_NAME=playwright-report/report.json \
pnpm exec playwright test --config=playwright.cloud.config.ts \
--reporter=list \
--reporter=html \
--reporter=json
- name: Upload Playwright report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-cloud
path: ./playwright-report/
retention-days: 30

View File

@@ -1,9 +1,10 @@
---
name: Publish Desktop UI on PR Merge
on:
pull_request:
types: [ closed ]
branches: [ main, core/* ]
types: ['closed']
branches: [main, core/*]
paths:
- 'apps/desktop-ui/package.json'
@@ -57,3 +58,26 @@ jobs:
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
comment_desktop_publish:
name: Comment Desktop Publish Summary
needs:
- resolve
- publish
if: success()
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
pull-requests: write
steps:
- name: Checkout merge commit
uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}
fetch-depth: 2
- name: Post desktop release summary comment
uses: ./.github/actions/comment-release-links
with:
issue-number: ${{ github.event.pull_request.number }}
version_file: apps/desktop-ui/package.json

View File

@@ -1,9 +1,10 @@
---
name: Release Draft Create
on:
pull_request:
types: [ closed ]
branches: [ main, core/* ]
types: ['closed']
branches: [main, core/*]
paths:
- 'package.json'
@@ -30,7 +31,9 @@ jobs:
- name: Get current version
id: current_version
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
run: |
VERSION=$(node -p "require('./package.json').version")
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Check if prerelease
id: check_prerelease
run: |
@@ -71,7 +74,8 @@ jobs:
name: dist-files
- name: Create release
id: create_release
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
uses: >-
softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
@@ -79,9 +83,14 @@ jobs:
dist.zip
tag_name: v${{ needs.build.outputs.version }}
target_commitish: ${{ github.event.pull_request.base.ref }}
make_latest: ${{ github.event.pull_request.base.ref == 'main' && needs.build.outputs.is_prerelease == 'false' }}
draft: ${{ github.event.pull_request.base.ref != 'main' || needs.build.outputs.is_prerelease == 'true' }}
prerelease: ${{ needs.build.outputs.is_prerelease == 'true' }}
make_latest: >-
${{ github.event.pull_request.base.ref == 'main' &&
needs.build.outputs.is_prerelease == 'false' }}
draft: >-
${{ github.event.pull_request.base.ref != 'main' ||
needs.build.outputs.is_prerelease == 'true' }}
prerelease: >-
${{ needs.build.outputs.is_prerelease == 'true' }}
generate_release_notes: true
publish_pypi:
@@ -110,7 +119,8 @@ jobs:
env:
COMFYUI_FRONTEND_VERSION: ${{ needs.build.outputs.version }}
- name: Publish pypi package
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc
uses: >-
pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc
with:
password: ${{ secrets.PYPI_TOKEN }}
packages-dir: comfyui_frontend_package/dist
@@ -122,3 +132,28 @@ jobs:
version: ${{ needs.build.outputs.version }}
ref: ${{ github.event.pull_request.merge_commit_sha }}
secrets: inherit
comment_release_summary:
name: Comment Release Summary
needs:
- draft_release
- publish_pypi
- publish_types
if: success()
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
pull-requests: write
steps:
- name: Checkout merge commit
uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}
fetch-depth: 2
- name: Post release summary comment
uses: ./.github/actions/comment-release-links
with:
issue-number: ${{ github.event.pull_request.number }}
version_file: package.json

1
.gitignore vendored
View File

@@ -58,7 +58,6 @@ coverage/
/playwright/.cache/
browser_tests/**/*-win32.png
browser_tests/local/
browser_tests/.auth/
.env

View File

@@ -74,8 +74,15 @@ const config: StorybookConfig = {
'@': process.cwd() + '/src'
}
},
esbuild: {
// Prevent minification of identifiers to preserve _sfc_main
minifyIdentifiers: false,
keepNames: true
},
build: {
rollupOptions: {
// Disable tree-shaking for Storybook to prevent Vue SFC exports from being removed
treeshake: false,
onwarn: (warning, warn) => {
// Suppress specific warnings
if (

View File

@@ -75,8 +75,15 @@ const config: StorybookConfig = {
'@frontend-locales': process.cwd() + '/../../src/locales'
}
},
esbuild: {
// Prevent minification of identifiers to preserve _sfc_main
minifyIdentifiers: false,
keepNames: true
},
build: {
rollupOptions: {
// Disable tree-shaking for Storybook to prevent Vue SFC exports from being removed
treeshake: false,
onwarn: (warning, warn) => {
// Suppress specific warnings
if (

View File

@@ -0,0 +1,206 @@
<template>
<Select
:id="dropdownId"
v-model="selectedLocale"
:options="localeOptions"
option-label="label"
option-value="value"
:disabled="isSwitching"
:pt="dropdownPt"
:size="props.size"
class="language-selector"
@change="onLocaleChange"
>
<template #value="{ value }">
<span :class="valueClass">
<i class="pi pi-language" :class="iconClass" />
<span>{{ displayLabel(value as SupportedLocale) }}</span>
</span>
</template>
<template #option="{ option }">
<span :class="optionClass">
<i class="pi pi-language" :class="iconClass" />
<span class="leading-none">{{ option.label }}</span>
</span>
</template>
</Select>
</template>
<script setup lang="ts">
import Select from 'primevue/select'
import type { SelectChangeEvent } from 'primevue/select'
import { computed, ref, watch } from 'vue'
import { i18n, loadLocale, st } from '@/i18n'
type VariantKey = 'dark' | 'light'
type SizeKey = 'small' | 'large'
const props = withDefaults(
defineProps<{
variant?: VariantKey
size?: SizeKey
}>(),
{
variant: 'dark',
size: 'small'
}
)
const dropdownId = `language-select-${Math.random().toString(36).slice(2)}`
const LOCALES = [
['en', 'English'],
['zh', '中文'],
['zh-TW', '繁體中文'],
['ru', 'Русский'],
['ja', '日本語'],
['ko', '한국어'],
['fr', 'Français'],
['es', 'Español'],
['ar', 'عربي'],
['tr', 'Türkçe']
] as const satisfies ReadonlyArray<[string, string]>
type SupportedLocale = (typeof LOCALES)[number][0]
const SIZE_PRESETS = {
large: {
wrapper: 'px-3 py-1 min-w-[7rem]',
gap: 'gap-2',
valueText: 'text-xs',
optionText: 'text-sm',
icon: 'text-sm'
},
small: {
wrapper: 'px-2 py-0.5 min-w-[5rem]',
gap: 'gap-1',
valueText: 'text-[0.65rem]',
optionText: 'text-xs',
icon: 'text-xs'
}
} as const satisfies Record<SizeKey, Record<string, string>>
const VARIANT_PRESETS = {
light: {
root: 'bg-white/80 border border-neutral-200 text-neutral-700 rounded-full shadow-sm backdrop-blur hover:border-neutral-400 transition-colors focus-visible:ring-offset-2 focus-visible:ring-offset-white',
trigger: 'text-neutral-500 hover:text-neutral-700',
item: 'text-neutral-700 bg-transparent hover:bg-neutral-100 focus-visible:outline-none',
valueText: 'text-neutral-600',
optionText: 'text-neutral-600',
icon: 'text-neutral-500'
},
dark: {
root: 'bg-neutral-900/70 border border-neutral-700 text-neutral-200 rounded-full shadow-sm backdrop-blur hover:border-neutral-500 transition-colors focus-visible:ring-offset-2 focus-visible:ring-offset-neutral-900',
trigger: 'text-neutral-400 hover:text-neutral-200',
item: 'text-neutral-200 bg-transparent hover:bg-neutral-800/80 focus-visible:outline-none',
valueText: 'text-neutral-100',
optionText: 'text-neutral-100',
icon: 'text-neutral-300'
}
} as const satisfies Record<VariantKey, Record<string, string>>
const selectedLocale = ref<string>(i18n.global.locale.value)
const isSwitching = ref(false)
const sizePreset = computed(() => SIZE_PRESETS[props.size as SizeKey])
const variantPreset = computed(
() => VARIANT_PRESETS[props.variant as VariantKey]
)
const dropdownPt = computed(() => ({
root: {
class: `${variantPreset.value.root} ${sizePreset.value.wrapper}`
},
trigger: {
class: variantPreset.value.trigger
},
item: {
class: `${variantPreset.value.item} ${sizePreset.value.optionText}`
}
}))
const valueClass = computed(() =>
[
'flex items-center font-medium uppercase tracking-wide leading-tight',
sizePreset.value.gap,
sizePreset.value.valueText,
variantPreset.value.valueText
].join(' ')
)
const optionClass = computed(() =>
[
'flex items-center leading-tight',
sizePreset.value.gap,
variantPreset.value.optionText,
sizePreset.value.optionText
].join(' ')
)
const iconClass = computed(() =>
[sizePreset.value.icon, variantPreset.value.icon].join(' ')
)
const localeOptions = computed(() =>
LOCALES.map(([value, fallback]) => ({
value,
label: st(`settings.Comfy_Locale.options.${value}`, fallback)
}))
)
const labelLookup = computed(() =>
localeOptions.value.reduce<Record<string, string>>((acc, option) => {
acc[option.value] = option.label
return acc
}, {})
)
function displayLabel(locale?: SupportedLocale) {
if (!locale) {
return st('settings.Comfy_Locale.name', 'Language')
}
return labelLookup.value[locale] ?? locale
}
watch(
() => i18n.global.locale.value,
(newLocale) => {
if (newLocale !== selectedLocale.value) {
selectedLocale.value = newLocale
}
}
)
async function onLocaleChange(event: SelectChangeEvent) {
const nextLocale = event.value as SupportedLocale | undefined
if (!nextLocale || nextLocale === i18n.global.locale.value) {
return
}
isSwitching.value = true
try {
await loadLocale(nextLocale)
i18n.global.locale.value = nextLocale
} catch (error) {
console.error(`Failed to change locale to "${nextLocale}"`, error)
selectedLocale.value = i18n.global.locale.value
} finally {
isSwitching.value = false
}
}
</script>
<style scoped>
@reference '../../assets/css/style.css';
:deep(.p-dropdown-panel .p-dropdown-item) {
@apply transition-colors;
}
:deep(.p-dropdown) {
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-yellow/60 focus-visible:ring-offset-2;
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<BaseViewTemplate dark>
<BaseViewTemplate dark hide-language-selector>
<div class="h-full p-8 2xl:p-16 flex flex-col items-center justify-center">
<div
class="bg-neutral-800 rounded-lg shadow-lg p-6 w-full max-w-[600px] flex flex-col gap-6"

View File

@@ -1,7 +1,7 @@
<template>
<BaseViewTemplate dark>
<div class="flex items-center justify-center min-h-screen">
<div class="grid grid-rows-2 gap-8">
<div class="grid gap-8">
<!-- Top container: Logo -->
<div class="flex items-end justify-center">
<img

View File

@@ -1,12 +1,15 @@
<template>
<div
class="font-sans w-screen h-screen flex flex-col"
class="font-sans w-screen h-screen flex flex-col relative"
:class="[
dark
? 'text-neutral-300 bg-neutral-900 dark-theme'
: 'text-neutral-900 bg-neutral-300'
]"
>
<div v-if="showLanguageSelector" class="absolute top-6 right-6 z-10">
<LanguageSelector :variant="variant" />
</div>
<!-- Virtual top menu for native window (drag handle) -->
<div
v-show="isNativeWindow()"
@@ -20,14 +23,20 @@
</template>
<script setup lang="ts">
import { nextTick, onMounted, ref } from 'vue'
import { computed, nextTick, onMounted, ref } from 'vue'
import LanguageSelector from '@/components/common/LanguageSelector.vue'
import { electronAPI, isElectron, isNativeWindow } from '../../utils/envUtil'
const { dark = false } = defineProps<{
const { dark = false, hideLanguageSelector = false } = defineProps<{
dark?: boolean
hideLanguageSelector?: boolean
}>()
const variant = computed(() => (dark ? 'dark' : 'light'))
const showLanguageSelector = computed(() => !hideLanguageSelector)
const darkTheme = {
color: 'rgba(0, 0, 0, 0)',
symbolColor: '#d4d4d4'

View File

@@ -140,7 +140,7 @@ When writing new tests, follow these patterns:
```typescript
// Import the test fixture
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Feature Name', () => {
// Set up test environment if needed

View File

@@ -1,44 +0,0 @@
import type { APIRequestContext, Page } from '@playwright/test'
import { ComfyPage } from './ComfyPage'
import type { FolderStructure } from './ComfyPage'
/**
* Cloud-specific implementation of ComfyPage.
* Uses Firebase auth persistence and cloud API for settings.
*/
export class CloudComfyPage extends ComfyPage {
constructor(
page: Page,
request: APIRequestContext,
parallelIndex: number = 0
) {
super(page, request, parallelIndex)
}
async setupUser(username: string): Promise<string | null> {
// No-op for cloud - user already authenticated via Firebase in globalSetup
// Firebase auth is persisted via storageState in the fixture
return null
}
async setupSettings(settings: Record<string, any>): Promise<void> {
// Cloud uses batch settings API (not devtools)
// Firebase auth token is automatically included from restored localStorage
const resp = await this.request.post(`${this.url}/api/settings`, {
data: settings
})
if (!resp.ok()) {
throw new Error(`Failed to setup cloud settings: ${await resp.text()}`)
}
}
async setupWorkflowsDirectory(structure: FolderStructure): Promise<void> {
// Cloud workflow API not yet implemented
// For initial smoke tests, we can skip this functionality
console.warn(
'setupWorkflowsDirectory: not yet implemented for cloud mode - skipping'
)
}
}

View File

@@ -1,5 +1,5 @@
import type { APIRequestContext, Locator, Page } from '@playwright/test'
import { expect } from '@playwright/test'
import { test as base, expect } from '@playwright/test'
import dotenv from 'dotenv'
import * as fs from 'fs'
@@ -7,8 +7,10 @@ import type { LGraphNode } 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'
import { NodeBadgeMode } from '../../src/types/nodeSource'
import { ComfyActionbar } from '../helpers/actionbar'
import { ComfyTemplates } from '../helpers/templates'
import { ComfyMouse } from './ComfyMouse'
import { VueNodeHelpers } from './VueNodeHelpers'
import { ComfyNodeSearchBox } from './components/ComfyNodeSearchBox'
import { SettingDialog } from './components/SettingDialog'
@@ -92,7 +94,7 @@ class ComfyMenu {
}
}
export type FolderStructure = {
type FolderStructure = {
[key: string]: FolderStructure | string
}
@@ -120,11 +122,7 @@ class ConfirmDialog {
}
}
/**
* Abstract base class for ComfyUI page objects.
* Subclasses must implement backend-specific methods for different environments (localhost, cloud, etc.)
*/
export abstract class ComfyPage {
export class ComfyPage {
private _history: TaskHistory | null = null
public readonly url: string
@@ -158,13 +156,12 @@ export abstract class ComfyPage {
/** Test user ID for the current context */
get id() {
return this.userIds[this.parallelIndex]
return this.userIds[comfyPageFixture.info().parallelIndex]
}
constructor(
public readonly page: Page,
public readonly request: APIRequestContext,
public readonly parallelIndex: number = 0
public readonly request: APIRequestContext
) {
this.url = process.env.PLAYWRIGHT_TEST_URL || 'http://localhost:8188'
this.canvas = page.locator('#graph-canvas')
@@ -218,24 +215,65 @@ export abstract class ComfyPage {
})
}
/**
* Setup workflows directory structure. Implementation varies by environment.
* @param structure - Folder structure to create
*/
abstract setupWorkflowsDirectory(structure: FolderStructure): Promise<void>
async setupWorkflowsDirectory(structure: FolderStructure) {
const resp = await this.request.post(
`${this.url}/api/devtools/setup_folder_structure`,
{
data: {
tree_structure: this.convertLeafToContent(structure),
base_path: `user/${this.id}/workflows`
}
}
)
/**
* Setup user for testing. Implementation varies by environment.
* @param username - Username to setup
* @returns User ID or null if not applicable
*/
abstract setupUser(username: string): Promise<string | null>
if (resp.status() !== 200) {
throw new Error(
`Failed to setup workflows directory: ${await resp.text()}`
)
}
/**
* Setup settings for testing. Implementation varies by environment.
* @param settings - Settings object to apply
*/
abstract setupSettings(settings: Record<string, any>): Promise<void>
await this.page.evaluate(async () => {
await window['app'].extensionManager.workflow.syncWorkflows()
})
}
async setupUser(username: string) {
const res = await this.request.get(`${this.url}/api/users`)
if (res.status() !== 200)
throw new Error(`Failed to retrieve users: ${await res.text()}`)
const apiRes = await res.json()
const user = Object.entries(apiRes?.users ?? {}).find(
([, name]) => name === username
)
const id = user?.[0]
return id ? id : await this.createUser(username)
}
async createUser(username: string) {
const resp = await this.request.post(`${this.url}/api/users`, {
data: { username }
})
if (resp.status() !== 200)
throw new Error(`Failed to create user: ${await resp.text()}`)
return await resp.json()
}
async setupSettings(settings: Record<string, any>) {
const resp = await this.request.post(
`${this.url}/api/devtools/set_settings`,
{
data: settings
}
)
if (resp.status() !== 200) {
throw new Error(`Failed to setup settings: ${await resp.text()}`)
}
}
setupHistory(): TaskHistory {
this._history ??= new TaskHistory(this)
@@ -1590,8 +1628,50 @@ export abstract class ComfyPage {
}
}
// Re-export shared constants and fixture
export { testComfySnapToGridGridSize } from './constants'
export const testComfySnapToGridGridSize = 50
export const comfyPageFixture = base.extend<{
comfyPage: ComfyPage
comfyMouse: ComfyMouse
}>({
comfyPage: async ({ page, request }, use, testInfo) => {
const comfyPage = new ComfyPage(page, request)
const { parallelIndex } = testInfo
const username = `playwright-test-${parallelIndex}`
const userId = await comfyPage.setupUser(username)
comfyPage.userIds[parallelIndex] = userId
try {
await comfyPage.setupSettings({
'Comfy.UseNewMenu': 'Top',
// Hide canvas menu/info/selection toolbox by default.
'Comfy.Graph.CanvasInfo': false,
'Comfy.Graph.CanvasMenu': false,
'Comfy.Canvas.SelectionToolbox': false,
// Hide all badges by default.
'Comfy.NodeBadge.NodeIdBadgeMode': NodeBadgeMode.None,
'Comfy.NodeBadge.NodeSourceBadgeMode': NodeBadgeMode.None,
// Disable tooltips by default to avoid flakiness.
'Comfy.EnableTooltips': false,
'Comfy.userId': userId,
// Set tutorial completed to true to avoid loading the tutorial workflow.
'Comfy.TutorialCompleted': true,
'Comfy.SnapToGrid.GridSize': testComfySnapToGridGridSize,
'Comfy.VueNodes.AutoScaleLayout': false
})
} catch (e) {
console.error(e)
}
await comfyPage.setup()
await use(comfyPage)
},
comfyMouse: async ({ comfyPage }, use) => {
const comfyMouse = new ComfyMouse(comfyPage)
await use(comfyMouse)
}
})
const makeMatcher = function <T>(
getValue: (node: NodeReference) => Promise<T> | T,

View File

@@ -1,48 +0,0 @@
import { test as base } from '@playwright/test'
import { CloudComfyPage } from './CloudComfyPage'
import { ComfyMouse } from './ComfyMouse'
import type { ComfyPage } from './ComfyPage'
/**
* Cloud-specific fixture for ComfyPage.
* Uses Firebase auth persisted from globalSetupCloud.ts.
*/
export const comfyPageCloudFixture = base.extend<{
comfyPage: ComfyPage
comfyMouse: ComfyMouse
}>({
// Use the storageState saved by globalSetupCloud
storageState: 'browser_tests/.auth/cloudUser.json',
comfyPage: async ({ page, request }, use) => {
const comfyPage = new CloudComfyPage(page, request)
// Note: No setupUser needed - Firebase auth persisted via storageState
// Setup cloud-specific settings (optional - can customize per test)
try {
await comfyPage.setupSettings({
'Comfy.UseNewMenu': 'Top',
// Hide canvas menu/info/selection toolbox by default.
'Comfy.Graph.CanvasInfo': false,
'Comfy.Graph.CanvasMenu': false,
'Comfy.Canvas.SelectionToolbox': false,
// Disable tooltips by default to avoid flakiness.
'Comfy.EnableTooltips': false,
// Set tutorial completed to true to avoid loading the tutorial workflow.
'Comfy.TutorialCompleted': true
})
} catch (e) {
console.error('Failed to setup cloud settings:', e)
}
// Don't mock releases for cloud - cloud handles its own releases
await comfyPage.setup({ mockReleases: false })
await use(comfyPage)
},
comfyMouse: async ({ comfyPage }, use) => {
const comfyMouse = new ComfyMouse(comfyPage)
await use(comfyMouse)
}
})

View File

@@ -1,78 +0,0 @@
import type { APIRequestContext, Page } from '@playwright/test'
import { ComfyPage } from './ComfyPage'
import type { FolderStructure } from './ComfyPage'
/**
* Localhost-specific implementation of ComfyPage.
* Uses devtools API and multi-user mode for test isolation.
*/
export class LocalhostComfyPage extends ComfyPage {
constructor(
page: Page,
request: APIRequestContext,
parallelIndex: number = 0
) {
super(page, request, parallelIndex)
}
async setupWorkflowsDirectory(structure: FolderStructure): Promise<void> {
const resp = await this.request.post(
`${this.url}/api/devtools/setup_folder_structure`,
{
data: {
tree_structure: this.convertLeafToContent(structure),
base_path: `user/${this.id}/workflows`
}
}
)
if (resp.status() !== 200) {
throw new Error(
`Failed to setup workflows directory: ${await resp.text()}`
)
}
await this.page.evaluate(async () => {
await window['app'].extensionManager.workflow.syncWorkflows()
})
}
async setupUser(username: string): Promise<string | null> {
const res = await this.request.get(`${this.url}/api/users`)
if (res.status() !== 200)
throw new Error(`Failed to retrieve users: ${await res.text()}`)
const apiRes = await res.json()
const user = Object.entries(apiRes?.users ?? {}).find(
([, name]) => name === username
)
const id = user?.[0]
return id ? id : await this.createUser(username)
}
private async createUser(username: string): Promise<string> {
const resp = await this.request.post(`${this.url}/api/users`, {
data: { username }
})
if (resp.status() !== 200)
throw new Error(`Failed to create user: ${await resp.text()}`)
return await resp.json()
}
async setupSettings(settings: Record<string, any>): Promise<void> {
const resp = await this.request.post(
`${this.url}/api/devtools/set_settings`,
{
data: settings
}
)
if (resp.status() !== 200) {
throw new Error(`Failed to setup settings: ${await resp.text()}`)
}
}
}

View File

@@ -1,56 +0,0 @@
import { test as base } from '@playwright/test'
import { NodeBadgeMode } from '../../src/types/nodeSource'
import type { ComfyPage } from './ComfyPage'
import { ComfyMouse } from './ComfyMouse'
import { testComfySnapToGridGridSize } from './constants'
import { LocalhostComfyPage } from './LocalhostComfyPage'
/**
* Localhost fixture for ComfyPage.
* Creates a test user and sets up default settings for stable testing.
*/
export const comfyPageFixture = base.extend<{
comfyPage: ComfyPage
comfyMouse: ComfyMouse
}>({
comfyPage: async ({ page, request }, use, testInfo) => {
const { parallelIndex } = testInfo
const comfyPage = new LocalhostComfyPage(page, request, parallelIndex)
const username = `playwright-test-${parallelIndex}`
const userId = await comfyPage.setupUser(username)
if (userId) {
comfyPage.userIds[parallelIndex] = userId
}
try {
await comfyPage.setupSettings({
'Comfy.UseNewMenu': 'Top',
// Hide canvas menu/info/selection toolbox by default.
'Comfy.Graph.CanvasInfo': false,
'Comfy.Graph.CanvasMenu': false,
'Comfy.Canvas.SelectionToolbox': false,
// Hide all badges by default.
'Comfy.NodeBadge.NodeIdBadgeMode': NodeBadgeMode.None,
'Comfy.NodeBadge.NodeSourceBadgeMode': NodeBadgeMode.None,
// Disable tooltips by default to avoid flakiness.
'Comfy.EnableTooltips': false,
'Comfy.userId': userId,
// Set tutorial completed to true to avoid loading the tutorial workflow.
'Comfy.TutorialCompleted': true,
'Comfy.SnapToGrid.GridSize': testComfySnapToGridGridSize,
'Comfy.VueNodes.AutoScaleLayout': false
})
} catch (e) {
console.error(e)
}
await comfyPage.setup()
await use(comfyPage)
},
comfyMouse: async ({ comfyPage }, use) => {
const comfyMouse = new ComfyMouse(comfyPage)
await use(comfyMouse)
}
})

View File

@@ -1,4 +0,0 @@
/**
* Shared constants for browser tests
*/
export const testComfySnapToGridGridSize = 50

View File

@@ -1,63 +0,0 @@
import { chromium } from '@playwright/test'
import type { FullConfig } from '@playwright/test'
import dotenv from 'dotenv'
import * as fs from 'fs'
import * as path from 'path'
dotenv.config()
/**
* Global setup for cloud tests.
* Authenticates with Firebase and saves auth state for test reuse.
*/
export default async function globalSetupCloud(config: FullConfig) {
const CLOUD_TEST_EMAIL = process.env.CLOUD_TEST_EMAIL
const CLOUD_TEST_PASSWORD = process.env.CLOUD_TEST_PASSWORD
if (!CLOUD_TEST_EMAIL || !CLOUD_TEST_PASSWORD) {
throw new Error(
'CLOUD_TEST_EMAIL and CLOUD_TEST_PASSWORD must be set in environment variables'
)
}
const browser = await chromium.launch()
const context = await browser.newContext()
const page = await context.newPage()
try {
// Navigate to cloud login page
await page.goto('https://stagingcloud.comfy.org/cloud/login', {
waitUntil: 'networkidle',
timeout: 30000
})
// Fill in email and password
await page.fill('#cloud-sign-in-email', CLOUD_TEST_EMAIL)
await page.fill('#cloud-sign-in-password', CLOUD_TEST_PASSWORD)
// Click login button
await page.click('button[type="submit"]')
// Wait for redirect to main app
await page.waitForURL('**/cloud', { timeout: 30000 })
// Wait a bit for auth tokens to be written to localStorage
await page.waitForTimeout(2000)
// Ensure .auth directory exists
const authDir = path.join(__dirname, '.auth')
if (!fs.existsSync(authDir)) {
fs.mkdirSync(authDir, { recursive: true })
}
// Save authentication state (includes localStorage with Firebase tokens)
await context.storageState({
path: 'browser_tests/.auth/cloudUser.json'
})
} catch (error) {
console.error('❌ Failed to authenticate:', error)
throw error
} finally {
await browser.close()
}
}

View File

@@ -1,36 +0,0 @@
# Cloud E2E Tests
## Setup
Cloud tests run against `https://stagingcloud.comfy.org` with Firebase authentication.
### Required GitHub Secrets
Add these to repository settings → Secrets → Actions:
- `CLOUD_TEST_EMAIL`: Firebase test account email
- `CLOUD_TEST_PASSWORD`: Firebase test account password
### Running Locally
```bash
# Set environment variables
export CLOUD_TEST_EMAIL="your-test-email@example.com"
export CLOUD_TEST_PASSWORD="your-password"
# Run cloud tests
pnpm exec playwright test --config=playwright.cloud.config.ts
```
### Running in CI
Workflow: `.github/workflows/ci-tests-e2e-cloud.yaml`
Trigger manually via Actions tab → "CI: Tests E2E Cloud" → Run workflow
### Test Structure
- Tests tagged with `@cloud` run only in cloud config
- Auth handled once in `globalSetupCloud.ts`
- Auth state saved to `browser_tests/.auth/cloudUser.json`
- Cloud fixture in `fixtures/ComfyPageCloud.ts`

View File

@@ -2,7 +2,7 @@ import type { Response } from '@playwright/test'
import { expect, mergeTests } from '@playwright/test'
import type { StatusWsMessage } from '../../src/schemas/apiSchema.ts'
import { comfyPageFixture } from '../fixtures/comfyPageFixture.ts'
import { comfyPageFixture } from '../fixtures/ComfyPage.ts'
import { webSocketFixture } from '../fixtures/ws.ts'
const test = mergeTests(comfyPageFixture, webSocketFixture)

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Bottom Panel Shortcuts', () => {
test.beforeEach(async ({ comfyPage }) => {

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Browser tab title', () => {
test.describe('Beta Menu', () => {

View File

@@ -1,6 +1,8 @@
import type { ComfyPage } from '../fixtures/ComfyPage'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyExpect as expect } from '../fixtures/ComfyPage'
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../fixtures/ComfyPage'
async function beforeChange(comfyPage: ComfyPage) {
await comfyPage.page.evaluate(() => {

View File

@@ -1,7 +1,7 @@
import type { Page } from '@playwright/test'
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,49 +0,0 @@
/**
* @cloud
* Cloud E2E tests.
* Tests run against stagingcloud.comfy.org with authenticated user.
*/
import { expect } from '@playwright/test'
import { comfyPageCloudFixture as test } from '../fixtures/ComfyPageCloud'
test.describe('Cloud E2E @cloud', () => {
test('loads app with authentication', async ({ comfyPage }) => {
// App should be loaded from setup()
await expect(comfyPage.canvas).toBeVisible()
// Verify we're authenticated (cloud-specific check)
const isAuthenticated = await comfyPage.page.evaluate(() => {
// Check for Firebase auth in localStorage
const keys = Object.keys(localStorage)
return keys.some(
(key) => key.startsWith('firebase:') || key.includes('authUser')
)
})
expect(isAuthenticated).toBe(true)
})
test('can interact with canvas', async ({ comfyPage }) => {
// Basic canvas interaction
await comfyPage.doubleClickCanvas()
await expect(comfyPage.searchBox.input).toBeVisible()
// Close search box
await comfyPage.page.keyboard.press('Escape')
await expect(comfyPage.searchBox.input).not.toBeVisible()
})
test('can access settings dialog', async ({ comfyPage }) => {
// Open settings dialog
await comfyPage.page.click('button[data-testid="settings-button"]', {
timeout: 10000
})
// Settings dialog should be visible
await expect(comfyPage.page.locator('.p-dialog')).toBeVisible()
// Close settings
await comfyPage.closeDialog()
})
})

View File

@@ -1,7 +1,7 @@
import { expect } from '@playwright/test'
import type { Palette } from '../../src/schemas/colorPaletteSchema'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,7 +1,7 @@
import { expect } from '@playwright/test'
import type { Locator } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
async function verifyCustomIconSvg(iconElement: Locator) {
const svgVariable = await iconElement.evaluate((element) => {

View File

@@ -2,7 +2,7 @@ import type { Locator } from '@playwright/test'
import { expect } from '@playwright/test'
import type { Keybinding } from '../../src/schemas/keyBindingSchema'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,7 +1,7 @@
import { expect } from '@playwright/test'
import type { SettingParams } from '../../src/platform/settings/types'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Topbar commands', () => {
test.beforeEach(async ({ comfyPage }) => {

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,7 +1,7 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import type { ComfyPage } from '../fixtures/ComfyPage'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import type { NodeReference } from '../fixtures/utils/litegraphUtils'
test.beforeEach(async ({ comfyPage }) => {

View File

@@ -2,9 +2,11 @@ import type { Locator } from '@playwright/test'
import { expect } from '@playwright/test'
import type { Position } from '@vueuse/core'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import {
comfyPageFixture as test,
testComfySnapToGridGridSize
} from '../fixtures/ComfyPage'
import type { ComfyPage } from '../fixtures/ComfyPage'
import { testComfySnapToGridGridSize } from '../fixtures/ComfyPage'
import type { NodeReference } from '../fixtures/utils/litegraphUtils'
test.beforeEach(async ({ comfyPage }) => {

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Menu', () => {
test.beforeEach(async ({ comfyPage }) => {

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Minimap', () => {
test.beforeEach(async ({ comfyPage }) => {

View File

@@ -2,7 +2,7 @@ import { expect } from '@playwright/test'
import type { ComfyApp } from '../../src/scripts/app'
import { NodeBadgeMode } from '../../src/types/nodeSource'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,5 +1,7 @@
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyExpect as expect } from '../fixtures/ComfyPage'
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../fixtures/ComfyPage'
// TODO: there might be a better solution for this
// Helper function to pan canvas and select node

View File

@@ -1,5 +1,7 @@
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyExpect as expect } from '../fixtures/ComfyPage'
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import type { NodeReference } from '../fixtures/utils/litegraphUtils'
test.beforeEach(async ({ comfyPage }) => {

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Release Notifications', () => {
test.beforeEach(async ({ comfyPage }) => {

View File

@@ -1,7 +1,7 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import type { ComfyPage } from '../fixtures/ComfyPage'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Remote COMBO Widget', () => {
const mockOptions = ['d', 'c', 'b', 'a']

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { getMiddlePoint } from '../fixtures/utils/litegraphUtils'
test.describe('Reroute Node', () => {

View File

@@ -1,7 +1,7 @@
import { expect } from '@playwright/test'
import { NodeBadgeMode } from '../../src/types/nodeSource'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture } from '../fixtures/comfyPageFixture'
import { comfyPageFixture } from '../fixtures/ComfyPage'
const test = comfyPageFixture

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
test.describe('Node library sidebar', () => {
test.beforeEach(async ({ comfyPage }) => {

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
test.describe.skip('Queue sidebar', () => {
test.beforeEach(async ({ comfyPage }) => {

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
test.describe('Workflows sidebar', () => {
test.beforeEach(async ({ comfyPage }) => {

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
// Constants
const INITIAL_NAME = 'initial_slot_name'

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
// Constants
const RENAMED_INPUT_NAME = 'renamed_input'

View File

@@ -1,7 +1,7 @@
import type { Page } from '@playwright/test'
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
async function checkTemplateFileExists(
page: Page,

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,7 +1,7 @@
import { expect } from '@playwright/test'
import type { SystemStats } from '../../src/schemas/apiSchema'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Version Mismatch Warnings', () => {
const ALWAYS_AHEAD_OF_INSTALLED_VERSION = '100.100.100'

View File

@@ -1,5 +1,7 @@
import { comfyPageFixture as test } from '../../../fixtures/comfyPageFixture'
import { comfyExpect as expect } from '../../../fixtures/ComfyPage'
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../fixtures/ComfyPage'
const CREATE_GROUP_HOTKEY = 'Control+g'

View File

@@ -1,5 +1,7 @@
import { comfyPageFixture as test } from '../../../../fixtures/comfyPageFixture'
import { comfyExpect as expect } from '../../../../fixtures/ComfyPage'
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../../fixtures/ComfyPage'
test.describe('Vue Nodes Canvas Pan', () => {
test.beforeEach(async ({ comfyPage }) => {

View File

@@ -1,5 +1,7 @@
import { comfyPageFixture as test } from '../../../../fixtures/comfyPageFixture'
import { comfyExpect as expect } from '../../../../fixtures/ComfyPage'
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../../fixtures/ComfyPage'
test.describe('Vue Nodes Zoom', () => {
test.beforeEach(async ({ comfyPage }) => {

View File

@@ -2,8 +2,10 @@ import type { Locator, Page } from '@playwright/test'
import type { NodeId } from '../../../../../src/platform/workflow/validation/schemas/workflowSchema'
import { getSlotKey } from '../../../../../src/renderer/core/layout/slots/slotIdentifier'
import { comfyPageFixture as test } from '../../../../fixtures/comfyPageFixture'
import { comfyExpect as expect } from '../../../../fixtures/ComfyPage'
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../../fixtures/ComfyPage'
import { getMiddlePoint } from '../../../../fixtures/utils/litegraphUtils'
import { fitToViewInstant } from '../../../../helpers/fitToView'

View File

@@ -1,6 +1,8 @@
import { comfyPageFixture as test } from '../../../../fixtures/comfyPageFixture'
import { comfyExpect as expect } from '../../../../fixtures/ComfyPage'
import type { ComfyPage } from '../../../../fixtures/ComfyPage'
import {
type ComfyPage,
comfyExpect as expect,
comfyPageFixture as test
} from '../../../../fixtures/ComfyPage'
import type { Position } from '../../../../fixtures/types'
test.describe('Vue Node Moving', () => {

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../../../../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../../../../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,5 +1,7 @@
import { comfyPageFixture as test } from '../../../../fixtures/comfyPageFixture'
import { comfyExpect as expect } from '../../../../fixtures/ComfyPage'
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../../fixtures/ComfyPage'
test.describe('Vue Nodes Renaming', () => {
test.beforeEach(async ({ comfyPage }) => {

View File

@@ -1,5 +1,7 @@
import { comfyPageFixture as test } from '../../../../fixtures/comfyPageFixture'
import { comfyExpect as expect } from '../../../../fixtures/ComfyPage'
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,5 +1,7 @@
import { comfyPageFixture as test } from '../../../fixtures/comfyPageFixture'
import { comfyExpect as expect } from '../../../fixtures/ComfyPage'
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../fixtures/ComfyPage'
const BYPASS_HOTKEY = 'Control+b'
const BYPASS_CLASS = /before:bg-bypass\/60/

View File

@@ -1,5 +1,7 @@
import { comfyPageFixture as test } from '../../../fixtures/comfyPageFixture'
import { comfyExpect as expect } from '../../../fixtures/ComfyPage'
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../fixtures/ComfyPage'
test.describe('Vue Node Collapse', () => {
test.beforeEach(async ({ comfyPage }) => {

View File

@@ -1,5 +1,7 @@
import { comfyPageFixture as test } from '../../../fixtures/comfyPageFixture'
import { comfyExpect as expect } from '../../../fixtures/ComfyPage'
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../fixtures/ComfyPage'
test.describe('Vue Node Custom Colors', () => {
test.beforeEach(async ({ comfyPage }) => {

View File

@@ -1,5 +1,7 @@
import { comfyPageFixture as test } from '../../../fixtures/comfyPageFixture'
import { comfyExpect as expect } from '../../../fixtures/ComfyPage'
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../fixtures/ComfyPage'
const ERROR_CLASS = /border-node-stroke-error/

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../../../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../../../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,5 +1,7 @@
import { comfyPageFixture as test } from '../../../fixtures/comfyPageFixture'
import { comfyExpect as expect } from '../../../fixtures/ComfyPage'
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../fixtures/ComfyPage'
const MUTE_HOTKEY = 'Control+m'
const MUTE_OPACITY = '0.5'

View File

@@ -1,5 +1,7 @@
import { comfyPageFixture as test } from '../../../fixtures/comfyPageFixture'
import { comfyExpect as expect } from '../../../fixtures/ComfyPage'
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../fixtures/ComfyPage'
const PIN_HOTKEY = 'p'
const PIN_INDICATOR = '[data-testid="node-pin-indicator"]'

View File

@@ -1,5 +1,7 @@
import { comfyPageFixture as test } from '../../../../fixtures/comfyPageFixture'
import { comfyExpect as expect } from '../../../../fixtures/ComfyPage'
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../../fixtures/ComfyPage'
test.describe('Vue Integer Widget', () => {
test.beforeEach(async ({ comfyPage }) => {

View File

@@ -1,5 +1,7 @@
import { comfyPageFixture as test } from '../../../../fixtures/comfyPageFixture'
import { comfyExpect as expect } from '../../../../fixtures/ComfyPage'
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../../fixtures/ComfyPage'
test.describe('Vue Upload Widgets', () => {
test.beforeEach(async ({ comfyPage }) => {

View File

@@ -1,6 +1,8 @@
import { comfyPageFixture as test } from '../../../../fixtures/comfyPageFixture'
import { comfyExpect as expect } from '../../../../fixtures/ComfyPage'
import type { ComfyPage } from '../../../../fixtures/ComfyPage'
import {
type ComfyPage,
comfyExpect as expect,
comfyPageFixture as test
} from '../../../../fixtures/ComfyPage'
test.describe('Vue Multiline String Widget', () => {
test.beforeEach(async ({ comfyPage }) => {

View File

@@ -1,5 +1,7 @@
import { comfyPageFixture as test } from '../../../fixtures/comfyPageFixture'
import { comfyExpect as expect } from '../../../fixtures/ComfyPage'
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../fixtures/ComfyPage'
test.describe('Vue Widget Reactivity', () => {
test.beforeEach(async ({ comfyPage }) => {

View File

@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')

View File

@@ -1,7 +1,6 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/comfyPageFixture'
import type { ComfyPage } from '../fixtures/ComfyPage'
import { type ComfyPage, comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Workflow Tab Thumbnails', () => {
test.beforeEach(async ({ comfyPage }) => {

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.32.2",
"version": "1.32.4",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",

View File

@@ -1,34 +0,0 @@
import { defineConfig, devices } from '@playwright/test'
/**
* Playwright configuration for cloud E2E tests.
* Tests run against stagingcloud.comfy.org with authenticated user.
*/
export default defineConfig({
testDir: './browser_tests/tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
reporter: 'html',
// Cloud tests need more time due to network latency
timeout: 75000, // 5x the default 15000ms
retries: process.env.CI ? 2 : 0,
use: {
trace: 'on-first-retry',
// Cloud URL - can override with PLAYWRIGHT_TEST_URL env var
baseURL: process.env.PLAYWRIGHT_TEST_URL || 'https://stagingcloud.comfy.org'
},
// Authenticate once before all tests
globalSetup: './browser_tests/globalSetupCloud.ts',
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
timeout: 75000,
grep: /@cloud/ // Only run tests tagged with @cloud
}
]
})

View File

@@ -31,7 +31,7 @@ export default defineConfig({
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
timeout: 15000,
grepInvert: /@mobile|@cloud/ // Run all tests except those tagged with @mobile or @cloud
grepInvert: /@mobile/ // Run all tests except those tagged with @mobile
},
{

View File

@@ -16,6 +16,10 @@ import { computed, onMounted } from 'vue'
import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
import config from '@/config'
import { t } from '@/i18n'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { app } from '@/scripts/app'
import { useDialogService } from '@/services/dialogService'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
@@ -23,6 +27,8 @@ import { electronAPI, isElectron } from './utils/envUtil'
const workspaceStore = useWorkspaceStore()
const conflictDetection = useConflictDetection()
const workflowStore = useWorkflowStore()
const dialogService = useDialogService()
const isLoading = computed<boolean>(() => workspaceStore.spinner)
const handleKey = (e: KeyboardEvent) => {
workspaceStore.shiftDown = e.shiftKey
@@ -48,6 +54,26 @@ onMounted(() => {
document.addEventListener('contextmenu', showContextMenu)
}
// Handle Vite preload errors (e.g., when assets are deleted after deployment)
window.addEventListener('vite:preloadError', async (_event) => {
// Auto-reload if app is not ready or there are no unsaved changes
if (!app.vueAppReady || !workflowStore.activeWorkflow?.isModified) {
window.location.reload()
} else {
// Show confirmation dialog if there are unsaved changes
await dialogService
.confirm({
title: t('g.vitePreloadErrorTitle'),
message: t('g.vitePreloadErrorMessage')
})
.then((confirmed) => {
if (confirmed) {
window.location.reload()
}
})
}
})
// Initialize conflict detection in background
// This runs async and doesn't block UI setup
void conflictDetection.initializeConflictDetection()

View File

@@ -7,6 +7,7 @@
<div
class="actionbar-container pointer-events-auto flex h-12 items-center rounded-lg border border-[var(--interface-stroke)] px-2 shadow-interface"
>
<ActionBarButtons />
<!-- Support for legacy topbar elements attached by custom scripts, hidden if no elements present -->
<div
ref="legacyCommandsContainerRef"
@@ -24,6 +25,7 @@ import { onMounted, ref } from 'vue'
import ComfyActionbar from '@/components/actionbar/ComfyActionbar.vue'
import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
import ActionBarButtons from '@/components/topbar/ActionBarButtons.vue'
import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'
import LoginButton from '@/components/topbar/LoginButton.vue'
import { useCurrentUser } from '@/composables/auth/useCurrentUser'

View File

@@ -103,7 +103,7 @@ if (isComponentWidget(props.widget)) {
const load3DSceneRef = ref<InstanceType<typeof Load3DScene> | null>(null)
const {
// configs
// config
sceneConfig,
modelConfig,
cameraConfig,

View File

@@ -0,0 +1,29 @@
<template>
<div class="flex h-full shrink-0 items-center gap-1">
<Button
v-for="(button, index) in actionBarButtonStore.buttons"
:key="index"
v-tooltip.bottom="button.tooltip"
:label="button.label"
:aria-label="button.tooltip || button.label"
:class="button.class"
text
rounded
severity="secondary"
class="h-7"
@click="button.onClick"
>
<template #icon>
<i :class="button.icon" />
</template>
</Button>
</div>
</template>
<script lang="ts" setup>
import Button from 'primevue/button'
import { useActionBarButtonStore } from '@/stores/actionBarButtonStore'
const actionBarButtonStore = useActionBarButtonStore()
</script>

View File

@@ -5,6 +5,7 @@ import { t } from '@/i18n'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { useExecutionStore } from '@/stores/executionStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
const DEFAULT_TITLE = 'ComfyUI'
const TITLE_SUFFIX = ' - ComfyUI'
@@ -13,6 +14,7 @@ export const useBrowserTabTitle = () => {
const executionStore = useExecutionStore()
const settingStore = useSettingStore()
const workflowStore = useWorkflowStore()
const workspaceStore = useWorkspaceStore()
const executionText = computed(() =>
executionStore.isIdle
@@ -24,11 +26,27 @@ export const useBrowserTabTitle = () => {
() => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'
)
const isAutoSaveEnabled = computed(
() => settingStore.get('Comfy.Workflow.AutoSave') === 'after delay'
)
const isActiveWorkflowModified = computed(
() => !!workflowStore.activeWorkflow?.isModified
)
const isActiveWorkflowPersisted = computed(
() => !!workflowStore.activeWorkflow?.isPersisted
)
const shouldShowUnsavedIndicator = computed(() => {
if (workspaceStore.shiftDown) return false
if (isAutoSaveEnabled.value) return false
if (!isActiveWorkflowPersisted.value) return true
if (isActiveWorkflowModified.value) return true
return false
})
const isUnsavedText = computed(() =>
workflowStore.activeWorkflow?.isModified ||
!workflowStore.activeWorkflow?.isPersisted
? ' *'
: ''
shouldShowUnsavedIndicator.value ? ' *' : ''
)
const workflowNameText = computed(() => {
const workflowName = workflowStore.activeWorkflow?.filename

View File

@@ -1,3 +1,4 @@
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
import { useModelSelectorDialog } from '@/composables/useModelSelectorDialog'
@@ -20,7 +21,7 @@ import { useAssetBrowserDialog } from '@/platform/assets/composables/useAssetBro
import { createModelNodeFromAsset } from '@/platform/assets/utils/createModelNodeFromAsset'
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
import { useSettingStore } from '@/platform/settings/settingStore'
import { SUPPORT_URL } from '@/platform/support/config'
import { buildSupportUrl } from '@/platform/support/config'
import { useTelemetry } from '@/platform/telemetry'
import type { ExecutionTriggerSource } from '@/platform/telemetry/types'
import { useToastStore } from '@/platform/updates/common/toastStore'
@@ -840,7 +841,12 @@ export function useCoreCommands(): ComfyCommand[] {
label: 'Contact Support',
versionAdded: '1.17.8',
function: () => {
window.open(SUPPORT_URL, '_blank')
const { userEmail, resolvedUserInfo } = useCurrentUser()
const supportUrl = buildSupportUrl({
userEmail: userEmail.value,
userId: resolvedUserInfo.value?.id
})
window.open(supportUrl, '_blank')
}
},
{

View File

@@ -0,0 +1,23 @@
import { t } from '@/i18n'
import { useExtensionService } from '@/services/extensionService'
import type { ActionBarButton } from '@/types/comfy'
// Zendesk feedback URL - update this with the actual URL
const ZENDESK_FEEDBACK_URL =
'https://support.comfy.org/hc/en-us/requests/new?ticket_form_id=43066738713236'
const buttons: ActionBarButton[] = [
{
icon: 'icon-[lucide--message-circle-question-mark]',
label: t('actionbar.feedback'),
tooltip: t('actionbar.feedbackTooltip'),
onClick: () => {
window.open(ZENDESK_FEEDBACK_URL, '_blank', 'noopener,noreferrer')
}
}
]
useExtensionService().registerExtension({
name: 'Comfy.Cloud.FeedbackButton',
actionBarButtons: buttons
})

View File

@@ -29,6 +29,7 @@ if (isCloud) {
await import('./cloudRemoteConfig')
await import('./cloudBadges')
await import('./cloudSessionCookie')
await import('./cloudFeedbackTopbarButton')
if (window.__CONFIG__?.subscription_required) {
await import('./cloudSubscription')

View File

@@ -4,6 +4,7 @@ import { app } from '../../scripts/app'
const saveNodeTypes = new Set([
'SaveImage',
'SaveVideo',
'SaveAnimatedWEBP',
'SaveWEBM',
'SaveAudio',

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