diff --git a/.github/workflows/ci-shell-validation.yaml b/.github/workflows/ci-shell-validation.yaml new file mode 100644 index 0000000000..783d1b03cc --- /dev/null +++ b/.github/workflows/ci-shell-validation.yaml @@ -0,0 +1,26 @@ +# Description: Runs shellcheck on tracked shell scripts when they change +name: "CI: Shell Validation" + +on: + push: + branches: + - main + paths: + - '**/*.sh' + pull_request: + paths: + - '**/*.sh' + +jobs: + shell-lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Install shellcheck + run: | + sudo apt-get update + sudo apt-get install -y shellcheck + + - name: Run shellcheck + run: bash ./scripts/cicd/check-shell.sh diff --git a/.github/workflows/pr-backport.yaml b/.github/workflows/pr-backport.yaml index 69607d6fb1..13fab6cdb4 100644 --- a/.github/workflows/pr-backport.yaml +++ b/.github/workflows/pr-backport.yaml @@ -16,6 +16,10 @@ on: type: boolean default: false +concurrency: + group: backport-${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }} + cancel-in-progress: false + jobs: backport: if: > diff --git a/.github/workflows/pr-update-playwright-expectations.yaml b/.github/workflows/pr-update-playwright-expectations.yaml index 9f6fa56a56..d78ad96bf0 100644 --- a/.github/workflows/pr-update-playwright-expectations.yaml +++ b/.github/workflows/pr-update-playwright-expectations.yaml @@ -124,12 +124,16 @@ jobs: - name: Stage changed snapshot files id: changed-snapshots run: | + set -euo pipefail echo "==========================================" echo "STAGING CHANGED SNAPSHOTS (Shard ${{ matrix.shardIndex }})" echo "==========================================" - # Get list of changed snapshot files - changed_files=$(git diff --name-only browser_tests/ 2>/dev/null | grep -E '\-snapshots/' || echo "") + # Get list of changed snapshot files (including untracked/new files) + changed_files=$( ( + git diff --name-only browser_tests/ 2>/dev/null || true + git ls-files --others --exclude-standard browser_tests/ 2>/dev/null || true + ) | sort -u | grep -E '\-snapshots/' || true ) if [ -z "$changed_files" ]; then echo "No snapshot changes in this shard" @@ -151,6 +155,11 @@ jobs: # 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 file_without_prefix="${file#browser_tests/}" # Create parent directories @@ -261,11 +270,19 @@ jobs: echo "CHANGES SUMMARY" echo "==========================================" echo "" - echo "Changed files in browser_tests:" - git diff --name-only browser_tests/ | head -20 || echo "No changes" - echo "" - echo "Total changes:" - git diff --name-only browser_tests/ | wc -l || echo "0" + echo "Changed files in browser_tests (including untracked):" + CHANGES=$(git status --porcelain=v1 --untracked-files=all -- browser_tests/) + if [ -z "$CHANGES" ]; then + echo "No changes" + echo "" + echo "Total changes:" + echo "0" + else + echo "$CHANGES" | head -50 + echo "" + echo "Total changes:" + echo "$CHANGES" | wc -l + fi - name: Commit updated expectations id: commit @@ -273,7 +290,7 @@ jobs: git config --global user.name 'github-actions' git config --global user.email 'github-actions@github.com' - if git diff --quiet browser_tests/; then + if [ -z "$(git status --porcelain=v1 --untracked-files=all -- browser_tests/)" ]; then echo "No changes to commit" echo "has-changes=false" >> $GITHUB_OUTPUT exit 0 diff --git a/.github/workflows/release-version-bump.yaml b/.github/workflows/release-version-bump.yaml index 766f5d4067..ca08e25bdd 100644 --- a/.github/workflows/release-version-bump.yaml +++ b/.github/workflows/release-version-bump.yaml @@ -20,6 +20,13 @@ on: required: true default: 'main' type: string + schedule: + # 00:00 UTC ≈ 4:00 PM PST / 5:00 PM PDT on the previous calendar day + - cron: '0 0 * * *' + +concurrency: + group: release-version-bump + cancel-in-progress: true jobs: bump-version: @@ -29,15 +36,99 @@ jobs: pull-requests: write steps: + - name: Prepare inputs + id: prepared-inputs + shell: bash + env: + RAW_VERSION_TYPE: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version_type || '' }} + RAW_PRE_RELEASE: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.pre_release || '' }} + RAW_BRANCH: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.branch || '' }} + run: | + set -euo pipefail + VERSION_TYPE="$RAW_VERSION_TYPE" + PRE_RELEASE="$RAW_PRE_RELEASE" + TARGET_BRANCH="$RAW_BRANCH" + + if [[ -z "$VERSION_TYPE" ]]; then + VERSION_TYPE='patch' + fi + + if [[ -z "$TARGET_BRANCH" ]]; then + TARGET_BRANCH='main' + fi + + { + echo "version_type=$VERSION_TYPE" + echo "pre_release=$PRE_RELEASE" + echo "branch=$TARGET_BRANCH" + } >> "$GITHUB_OUTPUT" + + - name: Close stale nightly version bump PRs + if: github.event_name == 'schedule' + uses: actions/github-script@v7 + with: + github-token: ${{ github.token }} + script: | + const prefix = 'version-bump-' + const closed = [] + const prs = await github.paginate(github.rest.pulls.list, { + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + per_page: 100 + }) + + for (const pr of prs) { + if (!pr.head?.ref?.startsWith(prefix)) { + continue + } + + if (pr.user?.login !== 'github-actions[bot]') { + continue + } + + // Only clean up stale nightly PRs targeting main. + // Adjust here if other target branches should be cleaned. + if (pr.base?.ref !== 'main') { + continue + } + + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number, + state: 'closed' + }) + + try { + await github.rest.git.deleteRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: `heads/${pr.head.ref}` + }) + } catch (error) { + if (![404, 422].includes(error.status)) { + throw error + } + } + + closed.push(pr.number) + } + + core.info(`Closed ${closed.length} stale PR(s).`) + - name: Checkout repository uses: actions/checkout@v5 with: - ref: ${{ github.event.inputs.branch }} + ref: ${{ steps.prepared-inputs.outputs.branch }} fetch-depth: 0 + persist-credentials: false - name: Validate branch exists + env: + TARGET_BRANCH: ${{ steps.prepared-inputs.outputs.branch }} run: | - BRANCH="${{ github.event.inputs.branch }}" + BRANCH="$TARGET_BRANCH" if ! git show-ref --verify --quiet "refs/heads/$BRANCH" && ! git show-ref --verify --quiet "refs/remotes/origin/$BRANCH"; then echo "❌ Branch '$BRANCH' does not exist" echo "" @@ -51,7 +142,7 @@ jobs: echo "✅ Branch '$BRANCH' exists" - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 with: version: 10 @@ -62,16 +153,31 @@ jobs: - name: Bump version id: bump-version + env: + VERSION_TYPE: ${{ steps.prepared-inputs.outputs.version_type }} + PRE_RELEASE: ${{ steps.prepared-inputs.outputs.pre_release }} run: | - pnpm version ${{ github.event.inputs.version_type }} --preid ${{ github.event.inputs.pre_release }} --no-git-tag-version + set -euo pipefail + if [[ -n "$PRE_RELEASE" && ! "$VERSION_TYPE" =~ ^pre(major|minor|patch)$ && "$VERSION_TYPE" != "prerelease" ]]; then + echo "❌ pre_release was provided but version_type='$VERSION_TYPE' does not support --preid" + exit 1 + fi + if [[ -n "$PRE_RELEASE" ]]; then + pnpm version "$VERSION_TYPE" --preid "$PRE_RELEASE" --no-git-tag-version + else + pnpm version "$VERSION_TYPE" --no-git-tag-version + fi + NEW_VERSION=$(node -p "require('./package.json').version") - echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT + echo "NEW_VERSION=$NEW_VERSION" >> "$GITHUB_OUTPUT" - name: Format PR string id: capitalised + env: + VERSION_TYPE: ${{ steps.prepared-inputs.outputs.version_type }} run: | - CAPITALISED_TYPE=${{ github.event.inputs.version_type }} - echo "capitalised=${CAPITALISED_TYPE@u}" >> $GITHUB_OUTPUT + CAPITALISED_TYPE="$VERSION_TYPE" + echo "capitalised=${CAPITALISED_TYPE@u}" >> "$GITHUB_OUTPUT" - name: Create Pull Request uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e @@ -82,8 +188,8 @@ jobs: body: | ${{ steps.capitalised.outputs.capitalised }} version increment to ${{ steps.bump-version.outputs.NEW_VERSION }} - **Base branch:** `${{ github.event.inputs.branch }}` + **Base branch:** `${{ steps.prepared-inputs.outputs.branch }}` branch: version-bump-${{ steps.bump-version.outputs.NEW_VERSION }} - base: ${{ github.event.inputs.branch }} + base: ${{ steps.prepared-inputs.outputs.branch }} labels: | Release diff --git a/.oxlintrc.json b/.oxlintrc.json index 4160601d9e..276ef5461d 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -2,16 +2,20 @@ "$schema": "./node_modules/oxlint/configuration_schema.json", "ignorePatterns": [ ".i18nrc.cjs", - "components.d.ts", - "lint-staged.config.js", - "vitest.setup.ts", + ".nx/*", "**/vite.config.*.timestamp*", "**/vitest.config.*.timestamp*", + "components.d.ts", + "coverage/*", + "dist/*", "packages/registry-types/src/comfyRegistryTypes.ts", + "playwright-report/*", "src/extensions/core/*", "src/scripts/*", "src/types/generatedManagerTypes.ts", - "src/types/vue-shim.d.ts" + "src/types/vue-shim.d.ts", + "test-results/*", + "vitest.setup.ts" ], "plugins": [ "eslint", @@ -24,9 +28,55 @@ ], "rules": { "no-async-promise-executor": "off", + "no-console": [ + "error", + { + "allow": [ + "warn", + "error" + ] + } + ], "no-control-regex": "off", "no-eval": "off", "no-redeclare": "error", + "no-restricted-imports": [ + "error", + { + "paths": [ + { + "name": "primevue/calendar", + "message": "Calendar is deprecated in PrimeVue 4+. Use DatePicker instead: import DatePicker from 'primevue/datepicker'" + }, + { + "name": "primevue/dropdown", + "message": "Dropdown is deprecated in PrimeVue 4+. Use Select instead: import Select from 'primevue/select'" + }, + { + "name": "primevue/inputswitch", + "message": "InputSwitch is deprecated in PrimeVue 4+. Use ToggleSwitch instead: import ToggleSwitch from 'primevue/toggleswitch'" + }, + { + "name": "primevue/overlaypanel", + "message": "OverlayPanel is deprecated in PrimeVue 4+. Use Popover instead: import Popover from 'primevue/popover'" + }, + { + "name": "primevue/sidebar", + "message": "Sidebar is deprecated in PrimeVue 4+. Use Drawer instead: import Drawer from 'primevue/drawer'" + }, + { + "name": "@/i18n--to-enable", + "importNames": [ + "st", + "t", + "te", + "d" + ], + "message": "Don't import `@/i18n` directly, prefer `useI18n()`" + } + ] + } + ], "no-self-assign": "allow", "no-unused-expressions": "off", "no-unused-private-class-members": "off", @@ -64,5 +114,16 @@ "typescript/no-floating-promises": "error", "vue/no-import-compiler-macros": "error", "vue/no-dupe-keys": "error" - } + }, + "overrides": [ + { + "files": [ + "**/*.{stories,test,spec}.ts", + "**/*.stories.vue" + ], + "rules": { + "no-console": "allow" + } + } + ] } \ No newline at end of file diff --git a/apps/desktop-ui/src/components/install/InstallFooter.vue b/apps/desktop-ui/src/components/install/InstallFooter.vue index 4c93020227..5cd71356e9 100644 --- a/apps/desktop-ui/src/components/install/InstallFooter.vue +++ b/apps/desktop-ui/src/components/install/InstallFooter.vue @@ -51,8 +51,6 @@ defineProps<{ canProceed: boolean /** Whether the location step should be disabled */ disableLocationStep: boolean - /** Whether the migration step should be disabled */ - disableMigrationStep: boolean /** Whether the settings step should be disabled */ disableSettingsStep: boolean }>() diff --git a/browser_tests/assets/subgraphs/subgraph-compressed-target-slot.json b/browser_tests/assets/subgraphs/subgraph-compressed-target-slot.json new file mode 100644 index 0000000000..471011d380 --- /dev/null +++ b/browser_tests/assets/subgraphs/subgraph-compressed-target-slot.json @@ -0,0 +1,150 @@ +{ + "id": "e0cb1d7e-5437-4911-b574-c9603dfbeaee", + "revision": 0, + "last_node_id": 2, + "last_link_id": 0, + "nodes": [ + { + "id": 2, + "type": "8bfe4227-f272-49e1-a892-0a972a86867c", + "pos": [ + -317, + -336 + ], + "size": [ + 210, + 58 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [], + "properties": { + "proxyWidgets": [ + [ + "-1", + "batch_size" + ] + ] + }, + "widgets_values": [ + 1 + ] + } + ], + "links": [], + "groups": [], + "definitions": { + "subgraphs": [ + { + "id": "8bfe4227-f272-49e1-a892-0a972a86867c", + "version": 1, + "state": { + "lastGroupId": 0, + "lastNodeId": 1, + "lastLinkId": 1, + "lastRerouteId": 0 + }, + "revision": 0, + "config": {}, + "name": "New Subgraph", + "inputNode": { + "id": -10, + "bounding": [ + -562, + -358, + 120, + 60 + ] + }, + "outputNode": { + "id": -20, + "bounding": [ + -52, + -358, + 120, + 40 + ] + }, + "inputs": [ + { + "id": "b4a8bc2a-8e9f-41aa-938d-c567a11d2c00", + "name": "batch_size", + "type": "INT", + "linkIds": [ + 1 + ], + "pos": [ + -462, + -338 + ] + } + ], + "outputs": [], + "widgets": [], + "nodes": [ + { + "id": 1, + "type": "EmptyLatentImage", + "pos": [ + -382, + -376 + ], + "size": [ + 270, + 106 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [ + { + "localized_name": "batch_size", + "name": "batch_size", + "type": "INT", + "widget": { + "name": "batch_size" + }, + "link": 1 + } + ], + "outputs": [ + { + "localized_name": "LATENT", + "name": "LATENT", + "type": "LATENT", + "links": null + } + ], + "properties": { + "Node name for S&R": "EmptyLatentImage" + }, + "widgets_values": [ + 512, + 512, + 1 + ] + } + ], + "groups": [], + "links": [ + { + "id": 1, + "origin_id": -10, + "origin_slot": 0, + "target_id": 1, + "target_slot": 0, + "type": "INT" + } + ], + "extra": {} + } + ] + }, + "config": {}, + "extra": { + "frontendVersion": "1.35.1" + }, + "version": 0.4 +} \ No newline at end of file diff --git a/browser_tests/assets/workflowInMedia/workflow_prompt_parameters.png b/browser_tests/assets/workflowInMedia/workflow_prompt_parameters.png new file mode 100644 index 0000000000..71b5035067 Binary files /dev/null and b/browser_tests/assets/workflowInMedia/workflow_prompt_parameters.png differ diff --git a/browser_tests/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-hidden-links-chromium-linux.png b/browser_tests/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-hidden-links-chromium-linux.png deleted file mode 100644 index 4dc0b3f43d..0000000000 Binary files a/browser_tests/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-hidden-links-chromium-linux.png and /dev/null differ diff --git a/browser_tests/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-visible-links-chromium-linux.png b/browser_tests/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-visible-links-chromium-linux.png deleted file mode 100644 index 2676f31eb3..0000000000 Binary files a/browser_tests/browser_tests/tests/graphCanvasMenu.spec.ts-snapshots/canvas-with-visible-links-chromium-linux.png and /dev/null differ diff --git a/browser_tests/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png b/browser_tests/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png deleted file mode 100644 index cea542a4f0..0000000000 Binary files a/browser_tests/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png and /dev/null differ diff --git a/browser_tests/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png b/browser_tests/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png deleted file mode 100644 index 1b4771e319..0000000000 Binary files a/browser_tests/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png and /dev/null differ diff --git a/browser_tests/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts-snapshots/vue-nodes-paned-with-touch-mobile-chrome-linux.png b/browser_tests/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts-snapshots/vue-nodes-paned-with-touch-mobile-chrome-linux.png deleted file mode 100644 index ed2e63d047..0000000000 Binary files a/browser_tests/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts-snapshots/vue-nodes-paned-with-touch-mobile-chrome-linux.png and /dev/null differ diff --git a/browser_tests/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png b/browser_tests/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png deleted file mode 100644 index 391dd76372..0000000000 Binary files a/browser_tests/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png and /dev/null differ diff --git a/browser_tests/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png b/browser_tests/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png deleted file mode 100644 index 4ed230307b..0000000000 Binary files a/browser_tests/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png and /dev/null differ diff --git a/browser_tests/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png b/browser_tests/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png deleted file mode 100644 index 077e34405e..0000000000 Binary files a/browser_tests/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png and /dev/null differ diff --git a/browser_tests/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png b/browser_tests/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png deleted file mode 100644 index d0235f54bb..0000000000 Binary files a/browser_tests/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png and /dev/null differ diff --git a/browser_tests/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png b/browser_tests/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png deleted file mode 100644 index e4155c01ff..0000000000 Binary files a/browser_tests/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png and /dev/null differ diff --git a/browser_tests/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png b/browser_tests/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png deleted file mode 100644 index c5d0151f1c..0000000000 Binary files a/browser_tests/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png and /dev/null differ diff --git a/browser_tests/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png b/browser_tests/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png deleted file mode 100644 index 7b1d91b3d2..0000000000 Binary files a/browser_tests/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png and /dev/null differ diff --git a/browser_tests/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png b/browser_tests/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png deleted file mode 100644 index 26eb49e5ce..0000000000 Binary files a/browser_tests/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png and /dev/null differ diff --git a/browser_tests/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png b/browser_tests/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png deleted file mode 100644 index 94d12be43b..0000000000 Binary files a/browser_tests/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png and /dev/null differ diff --git a/browser_tests/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png b/browser_tests/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png deleted file mode 100644 index e91d3a5fd9..0000000000 Binary files a/browser_tests/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png and /dev/null differ diff --git a/browser_tests/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-touch-mobile-chrome-linux.png b/browser_tests/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-touch-mobile-chrome-linux.png deleted file mode 100644 index 088ec907c6..0000000000 Binary files a/browser_tests/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-touch-mobile-chrome-linux.png and /dev/null differ diff --git a/browser_tests/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png b/browser_tests/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png deleted file mode 100644 index 6dabeb42ec..0000000000 Binary files a/browser_tests/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png and /dev/null differ diff --git a/browser_tests/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-dark-all-colors-chromium-linux.png b/browser_tests/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-dark-all-colors-chromium-linux.png deleted file mode 100644 index e04cb92a72..0000000000 Binary files a/browser_tests/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-dark-all-colors-chromium-linux.png and /dev/null differ diff --git a/browser_tests/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-light-all-colors-chromium-linux.png b/browser_tests/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-light-all-colors-chromium-linux.png deleted file mode 100644 index 6f5e35a1cc..0000000000 Binary files a/browser_tests/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-light-all-colors-chromium-linux.png and /dev/null differ diff --git a/browser_tests/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts-snapshots/vue-node-muted-state-chromium-linux.png b/browser_tests/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts-snapshots/vue-node-muted-state-chromium-linux.png deleted file mode 100644 index 9adde1602a..0000000000 Binary files a/browser_tests/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts-snapshots/vue-node-muted-state-chromium-linux.png and /dev/null differ diff --git a/browser_tests/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts-snapshots/vue-nodes-upload-widgets-chromium-linux.png b/browser_tests/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts-snapshots/vue-nodes-upload-widgets-chromium-linux.png deleted file mode 100644 index 4cc3ebe2c6..0000000000 Binary files a/browser_tests/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts-snapshots/vue-nodes-upload-widgets-chromium-linux.png and /dev/null differ diff --git a/browser_tests/fixtures/ComfyPage.ts b/browser_tests/fixtures/ComfyPage.ts index e29f6ea5bc..782af9921a 100644 --- a/browser_tests/fixtures/ComfyPage.ts +++ b/browser_tests/fixtures/ComfyPage.ts @@ -585,9 +585,15 @@ export class ComfyPage { fileName?: string url?: string dropPosition?: Position + waitForUpload?: boolean } = {} ) { - const { dropPosition = { x: 100, y: 100 }, fileName, url } = options + const { + dropPosition = { x: 100, y: 100 }, + fileName, + url, + waitForUpload = false + } = options if (!fileName && !url) throw new Error('Must provide either fileName or url') @@ -624,6 +630,14 @@ export class ComfyPage { // Dropping a URL (e.g., dropping image across browser tabs in Firefox) if (url) evaluateParams.url = url + // Set up response waiter for file uploads before triggering the drop + const uploadResponsePromise = waitForUpload + ? this.page.waitForResponse( + (resp) => resp.url().includes('/upload/') && resp.status() === 200, + { timeout: 10000 } + ) + : null + // Execute the drag and drop in the browser await this.page.evaluate(async (params) => { const dataTransfer = new DataTransfer() @@ -690,12 +704,17 @@ export class ComfyPage { } }, evaluateParams) + // Wait for file upload to complete + if (uploadResponsePromise) { + await uploadResponsePromise + } + await this.nextFrame() } async dragAndDropFile( fileName: string, - options: { dropPosition?: Position } = {} + options: { dropPosition?: Position; waitForUpload?: boolean } = {} ) { return this.dragAndDropExternalResource({ fileName, ...options }) } diff --git a/browser_tests/fixtures/VueNodeHelpers.ts b/browser_tests/fixtures/VueNodeHelpers.ts index e6121b3c34..e08b39bd74 100644 --- a/browser_tests/fixtures/VueNodeHelpers.ts +++ b/browser_tests/fixtures/VueNodeHelpers.ts @@ -160,7 +160,7 @@ export class VueNodeHelpers { return { input: widget.locator('input'), incrementButton: widget.locator('button').first(), - decrementButton: widget.locator('button').last() + decrementButton: widget.locator('button').nth(1) } } } diff --git a/browser_tests/helpers/templates.ts b/browser_tests/helpers/templates.ts index c690b8702a..829837bd1c 100644 --- a/browser_tests/helpers/templates.ts +++ b/browser_tests/helpers/templates.ts @@ -1,4 +1,5 @@ import type { Locator, Page } from '@playwright/test' +import { expect } from '@playwright/test' import path from 'path' import type { @@ -8,9 +9,20 @@ import type { export class ComfyTemplates { readonly content: Locator + readonly allTemplateCards: Locator constructor(readonly page: Page) { this.content = page.getByTestId('template-workflows-content') + this.allTemplateCards = page.locator('[data-testid^="template-workflow-"]') + } + + async waitForMinimumCardCount(count: number) { + return await expect(async () => { + const cardCount = await this.allTemplateCards.count() + expect(cardCount).toBeGreaterThanOrEqual(count) + }).toPass({ + timeout: 1_000 + }) } async loadTemplate(id: string) { diff --git a/browser_tests/tests/backgroundImageUpload.spec.ts b/browser_tests/tests/backgroundImageUpload.spec.ts index 44ae2d6aea..b620f5441e 100644 --- a/browser_tests/tests/backgroundImageUpload.spec.ts +++ b/browser_tests/tests/backgroundImageUpload.spec.ts @@ -77,8 +77,7 @@ test.describe('Background Image Upload', () => { // Verify the URL input now has an API URL const urlInput = backgroundImageSetting.locator('input[type="text"]') - const inputValue = await urlInput.inputValue() - expect(inputValue).toMatch(/^\/api\/view\?.*subfolder=backgrounds/) + await expect(urlInput).toHaveValue(/^\/api\/view\?.*subfolder=backgrounds/) // Verify clear button is now enabled const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)') diff --git a/browser_tests/tests/execution.spec.ts b/browser_tests/tests/execution.spec.ts index ed77b79497..ba22b38e96 100644 --- a/browser_tests/tests/execution.spec.ts +++ b/browser_tests/tests/execution.spec.ts @@ -36,9 +36,10 @@ test.describe('Execute to selected output nodes', () => { await output1.click('title') await comfyPage.executeCommand('Comfy.QueueSelectedOutputNodes') - - expect(await (await input.getWidget(0)).getValue()).toBe('foo') - expect(await (await output1.getWidget(0)).getValue()).toBe('foo') - expect(await (await output2.getWidget(0)).getValue()).toBe('') + await expect(async () => { + expect(await (await input.getWidget(0)).getValue()).toBe('foo') + expect(await (await output1.getWidget(0)).getValue()).toBe('foo') + expect(await (await output2.getWidget(0)).getValue()).toBe('') + }).toPass({ timeout: 2_000 }) }) }) diff --git a/browser_tests/tests/interaction.spec.ts b/browser_tests/tests/interaction.spec.ts index 37442bf4e0..451cbffa3d 100644 --- a/browser_tests/tests/interaction.spec.ts +++ b/browser_tests/tests/interaction.spec.ts @@ -306,14 +306,16 @@ test.describe('Node Interaction', () => { await comfyPage.canvas.click({ position: numberWidgetPos }) - await expect(comfyPage.canvas).toHaveScreenshot('prompt-dialog-opened.png') + const legacyPrompt = comfyPage.page.locator('.graphdialog') + await expect(legacyPrompt).toBeVisible() + await comfyPage.delay(300) await comfyPage.canvas.click({ position: { x: 10, y: 10 } }) - await expect(comfyPage.canvas).toHaveScreenshot('prompt-dialog-closed.png') + await expect(legacyPrompt).toBeHidden() }) test('Can close prompt dialog with canvas click (text widget)', async ({ @@ -327,18 +329,16 @@ test.describe('Node Interaction', () => { await comfyPage.canvas.click({ position: textWidgetPos }) - await expect(comfyPage.canvas).toHaveScreenshot( - 'prompt-dialog-opened-text.png' - ) + const legacyPrompt = comfyPage.page.locator('.graphdialog') + await expect(legacyPrompt).toBeVisible() + await comfyPage.delay(300) await comfyPage.canvas.click({ position: { x: 10, y: 10 } }) - await expect(comfyPage.canvas).toHaveScreenshot( - 'prompt-dialog-closed-text.png' - ) + await expect(legacyPrompt).toBeHidden() }) test('Can double click node title to edit', async ({ comfyPage }) => { diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/prompt-dialog-closed-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/prompt-dialog-closed-chromium-linux.png index 6c0bdcc603..0c57f7db1d 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/prompt-dialog-closed-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/prompt-dialog-closed-chromium-linux.png differ diff --git a/browser_tests/tests/interaction.spec.ts-snapshots/prompt-dialog-closed-text-chromium-linux.png b/browser_tests/tests/interaction.spec.ts-snapshots/prompt-dialog-closed-text-chromium-linux.png index 5d89bf47f8..8f87009b72 100644 Binary files a/browser_tests/tests/interaction.spec.ts-snapshots/prompt-dialog-closed-text-chromium-linux.png and b/browser_tests/tests/interaction.spec.ts-snapshots/prompt-dialog-closed-text-chromium-linux.png differ diff --git a/browser_tests/tests/loadWorkflowInMedia.spec.ts b/browser_tests/tests/loadWorkflowInMedia.spec.ts index f091058d24..3a7f7a71a6 100644 --- a/browser_tests/tests/loadWorkflowInMedia.spec.ts +++ b/browser_tests/tests/loadWorkflowInMedia.spec.ts @@ -12,6 +12,7 @@ test.describe('Load Workflow in Media', () => { 'edited_workflow.webp', 'no_workflow.webp', 'large_workflow.webp', + 'workflow_prompt_parameters.png', 'workflow.webm', // Skipped due to 3d widget unstable visual result. // 3d widget shows grid after fully loaded. diff --git a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/dropped-workflow-url-hidream-dev-example-png-chromium-linux.png b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/dropped-workflow-url-hidream-dev-example-png-chromium-linux.png index 427051e172..0c57f7db1d 100644 Binary files a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/dropped-workflow-url-hidream-dev-example-png-chromium-linux.png and b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/dropped-workflow-url-hidream-dev-example-png-chromium-linux.png differ diff --git a/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-prompt-parameters-png-chromium-linux.png b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-prompt-parameters-png-chromium-linux.png new file mode 100644 index 0000000000..5688c3547f Binary files /dev/null and b/browser_tests/tests/loadWorkflowInMedia.spec.ts-snapshots/workflow-prompt-parameters-png-chromium-linux.png differ diff --git a/browser_tests/tests/mobileBaseline.spec.ts b/browser_tests/tests/mobileBaseline.spec.ts new file mode 100644 index 0000000000..21be3ca949 --- /dev/null +++ b/browser_tests/tests/mobileBaseline.spec.ts @@ -0,0 +1,29 @@ +import { comfyPageFixture as test } from '../fixtures/ComfyPage' +import { expect } from '@playwright/test' + +test.describe('Mobile Baseline Snapshots', () => { + test('@mobile empty canvas', async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.ConfirmClear', false) + await comfyPage.executeCommand('Comfy.ClearWorkflow') + await expect(async () => { + expect(await comfyPage.getGraphNodesCount()).toBe(0) + }).toPass({ timeout: 256 }) + await comfyPage.nextFrame() + await expect(comfyPage.canvas).toHaveScreenshot('mobile-empty-canvas.png') + }) + + test('@mobile default workflow', async ({ comfyPage }) => { + await comfyPage.loadWorkflow('default') + await expect(comfyPage.canvas).toHaveScreenshot( + 'mobile-default-workflow.png' + ) + }) + + test('@mobile settings dialog', async ({ comfyPage }) => { + await comfyPage.settingDialog.open() + await comfyPage.nextFrame() + await expect(comfyPage.settingDialog.root).toHaveScreenshot( + 'mobile-settings-dialog.png' + ) + }) +}) diff --git a/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-default-workflow-mobile-chrome-linux.png b/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-default-workflow-mobile-chrome-linux.png new file mode 100644 index 0000000000..afbc492aaa Binary files /dev/null and b/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-default-workflow-mobile-chrome-linux.png differ diff --git a/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-empty-canvas-mobile-chrome-linux.png b/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-empty-canvas-mobile-chrome-linux.png new file mode 100644 index 0000000000..e02471466a Binary files /dev/null and b/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-empty-canvas-mobile-chrome-linux.png differ diff --git a/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-settings-dialog-mobile-chrome-linux.png b/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-settings-dialog-mobile-chrome-linux.png new file mode 100644 index 0000000000..23c6a0c8d4 Binary files /dev/null and b/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-settings-dialog-mobile-chrome-linux.png differ diff --git a/browser_tests/tests/nodeSearchBox.spec.ts b/browser_tests/tests/nodeSearchBox.spec.ts index ce8875defc..2eea223290 100644 --- a/browser_tests/tests/nodeSearchBox.spec.ts +++ b/browser_tests/tests/nodeSearchBox.spec.ts @@ -260,6 +260,12 @@ test.describe('Release context menu', () => { test('Can trigger on link release', async ({ comfyPage }) => { await comfyPage.disconnectEdge() + const contextMenu = comfyPage.page.locator('.litecontextmenu') + // Wait for context menu with correct title (slot name | slot type) + // The title shows the output slot name and type from the disconnected link + await expect(contextMenu.locator('.litemenu-title')).toContainText( + 'CLIP | CLIP' + ) await comfyPage.page.mouse.move(10, 10) await comfyPage.nextFrame() await expect(comfyPage.canvas).toHaveScreenshot( diff --git a/browser_tests/tests/remoteWidgets.spec.ts b/browser_tests/tests/remoteWidgets.spec.ts index b717f62824..18cc77f445 100644 --- a/browser_tests/tests/remoteWidgets.spec.ts +++ b/browser_tests/tests/remoteWidgets.spec.ts @@ -212,8 +212,12 @@ test.describe('Remote COMBO Widget', () => { // Click on the canvas to trigger widget refresh await comfyPage.page.mouse.click(400, 300) - const refreshedOptions = await getWidgetOptions(comfyPage, nodeName) - expect(refreshedOptions).not.toEqual(initialOptions) + await expect(async () => { + const refreshedOptions = await getWidgetOptions(comfyPage, nodeName) + expect(refreshedOptions).not.toEqual(initialOptions) + }).toPass({ + timeout: 2_000 + }) }) test('does not refresh when TTL is not set', async ({ comfyPage }) => { @@ -321,8 +325,12 @@ test.describe('Remote COMBO Widget', () => { await clickRefreshButton(comfyPage, nodeName) // Verify the selected value of the widget is the first option in the refreshed list - const refreshedValue = await getWidgetValue(comfyPage, nodeName) - expect(refreshedValue).toEqual('new first option') + await expect(async () => { + const refreshedValue = await getWidgetValue(comfyPage, nodeName) + expect(refreshedValue).toEqual('new first option') + }).toPass({ + timeout: 2_000 + }) }) }) diff --git a/browser_tests/tests/sidebar/nodeLibrary.spec.ts b/browser_tests/tests/sidebar/nodeLibrary.spec.ts index 58f2ae4484..ac7a2efa9c 100644 --- a/browser_tests/tests/sidebar/nodeLibrary.spec.ts +++ b/browser_tests/tests/sidebar/nodeLibrary.spec.ts @@ -290,16 +290,20 @@ test.describe('Node library sidebar', () => { await comfyPage.page.keyboard.insertText('bar') await comfyPage.page.keyboard.press('Enter') await comfyPage.nextFrame() - expect( - await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') - ).toEqual(['bar/']) - expect( - await comfyPage.getSetting('Comfy.NodeLibrary.BookmarksCustomization') - ).toEqual({ - 'bar/': { - icon: 'pi-folder', - color: '#007bff' - } + await expect(async () => { + expect( + await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2') + ).toEqual(['bar/']) + expect( + await comfyPage.getSetting('Comfy.NodeLibrary.BookmarksCustomization') + ).toEqual({ + 'bar/': { + icon: 'pi-folder', + color: '#007bff' + } + }) + }).toPass({ + timeout: 2_000 }) }) diff --git a/browser_tests/tests/subgraph.spec.ts b/browser_tests/tests/subgraph.spec.ts index 06a09210c5..6359156211 100644 --- a/browser_tests/tests/subgraph.spec.ts +++ b/browser_tests/tests/subgraph.spec.ts @@ -329,6 +329,15 @@ test.describe('Subgraph Operations', () => { expect(newInputName).toBe(labelClickRenamedName) expect(newInputName).not.toBe(initialInputLabel) }) + test('Can create widget from link with compressed target_slot', async ({ + comfyPage + }) => { + await comfyPage.loadWorkflow('subgraphs/subgraph-compressed-target-slot') + const step = await comfyPage.page.evaluate(() => { + return window['app'].graph.nodes[0].widgets[0].options.step + }) + expect(step).toBe(10) + }) }) test.describe('Subgraph Creation and Deletion', () => { diff --git a/browser_tests/tests/templates.spec.ts b/browser_tests/tests/templates.spec.ts index 1e0d24dd68..0e28379064 100644 --- a/browser_tests/tests/templates.spec.ts +++ b/browser_tests/tests/templates.spec.ts @@ -188,22 +188,19 @@ test.describe('Templates', () => { .locator('header') .filter({ hasText: 'Templates' }) - const cardCount = await comfyPage.page - .locator('[data-testid^="template-workflow-"]') - .count() - expect(cardCount).toBeGreaterThan(0) + await comfyPage.templates.waitForMinimumCardCount(1) await expect(templateGrid).toBeVisible() await expect(nav).toBeVisible() // Nav should be visible at desktop size const mobileSize = { width: 640, height: 800 } await comfyPage.page.setViewportSize(mobileSize) - expect(cardCount).toBeGreaterThan(0) + await comfyPage.templates.waitForMinimumCardCount(1) await expect(templateGrid).toBeVisible() await expect(nav).not.toBeVisible() // Nav should collapse at mobile size const tabletSize = { width: 1024, height: 800 } await comfyPage.page.setViewportSize(tabletSize) - expect(cardCount).toBeGreaterThan(0) + await comfyPage.templates.waitForMinimumCardCount(1) await expect(templateGrid).toBeVisible() await expect(nav).toBeVisible() // Nav should be visible at tablet size }) diff --git a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png index ba3b6c4f41..09c358eec0 100644 Binary files a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png and b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-create-group-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png index d0d736214e..e7c4c55a59 100644 Binary files a/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png and b/browser_tests/tests/vueNodes/groups/groups.spec.ts-snapshots/vue-groups-fit-to-contents-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts-snapshots/vue-nodes-paned-with-touch-mobile-chrome-linux.png b/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts-snapshots/vue-nodes-paned-with-touch-mobile-chrome-linux.png index d58403f385..f8b6fb2efb 100644 Binary files a/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts-snapshots/vue-nodes-paned-with-touch-mobile-chrome-linux.png and b/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts-snapshots/vue-nodes-paned-with-touch-mobile-chrome-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png index 96d02e0233..1350b32bcb 100644 Binary files a/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/canvas/zoom.spec.ts-snapshots/zoomed-in-ctrl-shift-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png index 3d2b3332a1..616b7956af 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png index 40e2799b5d..0eced54c2a 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-ctrl-alt-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png index 247ce51638..b715e828ad 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-input-drag-reuses-origin-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png index bf438bb70b..395538bdf9 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-input-drag-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png index 4bd65d8b3e..912ac481a5 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-reroute-output-shift-drag-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png index c3bb492d7d..3cac8048c6 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-shift-output-multi-link-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png index 3f43b78db6..f9a5c9c9f5 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-node-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png index d343f6f933..a5a6329a95 100644 Binary files a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts-snapshots/vue-node-snap-to-slot-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts new file mode 100644 index 0000000000..6e20f9e367 --- /dev/null +++ b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts @@ -0,0 +1,144 @@ +import { + comfyExpect as expect, + comfyPageFixture as test +} from '../../../../fixtures/ComfyPage' +import type { ComfyPage } from '../../../../fixtures/ComfyPage' +import { fitToViewInstant } from '../../../../helpers/fitToView' + +test.describe('Vue Node Bring to Front', () => { + test.beforeEach(async ({ comfyPage }) => { + await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') + await comfyPage.setSetting('Comfy.VueNodes.Enabled', true) + await comfyPage.loadWorkflow('vueNodes/simple-triple') + await comfyPage.vueNodes.waitForNodes() + await fitToViewInstant(comfyPage) + }) + + /** + * Helper to get the z-index of a node by its title + */ + async function getNodeZIndex( + comfyPage: ComfyPage, + title: string + ): Promise { + const node = comfyPage.vueNodes.getNodeByTitle(title) + const style = await node.getAttribute('style') + if (!style) { + throw new Error( + `Node "${title}" has no style attribute (observed: ${style})` + ) + } + const match = style.match(/z-index:\s*(\d+)/) + if (!match) { + throw new Error( + `Node "${title}" has no z-index in style (observed: "${style}")` + ) + } + return parseInt(match[1], 10) + } + + /** + * Helper to get the bounding box center of a node + */ + async function getNodeCenter( + comfyPage: ComfyPage, + title: string + ): Promise<{ x: number; y: number }> { + const node = comfyPage.vueNodes.getNodeByTitle(title) + const box = await node.boundingBox() + if (!box) throw new Error(`Node "${title}" not found`) + return { x: box.x + box.width / 2, y: box.y + box.height / 2 } + } + + test('should bring overlapped node to front when clicking on it', async ({ + comfyPage + }) => { + // Get initial positions + const clipCenter = await getNodeCenter(comfyPage, 'CLIP Text Encode') + const ksamplerHeader = await comfyPage.page + .getByText('KSampler') + .boundingBox() + if (!ksamplerHeader) throw new Error('KSampler header not found') + + // Drag KSampler on top of CLIP Text Encode + await comfyPage.dragAndDrop( + { x: ksamplerHeader.x + 50, y: ksamplerHeader.y + 10 }, + clipCenter + ) + await comfyPage.nextFrame() + + // Screenshot showing KSampler on top of CLIP + await expect(comfyPage.canvas).toHaveScreenshot( + 'bring-to-front-overlapped-before.png' + ) + + // KSampler should be on top (higher z-index) after being dragged + const ksamplerZIndexBefore = await getNodeZIndex(comfyPage, 'KSampler') + const clipZIndexBefore = await getNodeZIndex(comfyPage, 'CLIP Text Encode') + expect(ksamplerZIndexBefore).toBeGreaterThan(clipZIndexBefore) + + // Click on CLIP Text Encode (underneath) - need to click on a visible part + // Since KSampler is on top, we click on the edge of CLIP that should still be visible + const clipNode = comfyPage.vueNodes.getNodeByTitle('CLIP Text Encode') + const clipBox = await clipNode.boundingBox() + if (!clipBox) throw new Error('CLIP node not found') + + // Click on a visible edge of CLIP + await comfyPage.page.mouse.click(clipBox.x + 30, clipBox.y + 10) + await comfyPage.nextFrame() + + // CLIP should now be on top - compare post-action z-indices + const clipZIndexAfter = await getNodeZIndex(comfyPage, 'CLIP Text Encode') + const ksamplerZIndexAfter = await getNodeZIndex(comfyPage, 'KSampler') + expect(clipZIndexAfter).toBeGreaterThan(ksamplerZIndexAfter) + + // Screenshot showing CLIP now on top + await expect(comfyPage.canvas).toHaveScreenshot( + 'bring-to-front-overlapped-after.png' + ) + }) + + test('should bring overlapped node to front when clicking on its widget', async ({ + comfyPage + }) => { + // Get CLIP Text Encode position (it has a text widget) + const clipCenter = await getNodeCenter(comfyPage, 'CLIP Text Encode') + + // Get VAE Decode position and drag it on top of CLIP + const vaeHeader = await comfyPage.page.getByText('VAE Decode').boundingBox() + if (!vaeHeader) throw new Error('VAE Decode header not found') + + await comfyPage.dragAndDrop( + { x: vaeHeader.x + 50, y: vaeHeader.y + 10 }, + { x: clipCenter.x - 50, y: clipCenter.y } + ) + await comfyPage.nextFrame() + + // VAE should be on top after drag + const vaeZIndexBefore = await getNodeZIndex(comfyPage, 'VAE Decode') + const clipZIndexBefore = await getNodeZIndex(comfyPage, 'CLIP Text Encode') + expect(vaeZIndexBefore).toBeGreaterThan(clipZIndexBefore) + + // Screenshot showing VAE on top + await expect(comfyPage.canvas).toHaveScreenshot( + 'bring-to-front-widget-overlapped-before.png' + ) + + // Click on the text widget of CLIP Text Encode + const clipNode = comfyPage.vueNodes.getNodeByTitle('CLIP Text Encode') + const clipBox = await clipNode.boundingBox() + if (!clipBox) throw new Error('CLIP node not found') + await comfyPage.page.mouse.click(clipBox.x + 170, clipBox.y + 80) + await comfyPage.nextFrame() + + // CLIP should now be on top - compare post-action z-indices + const clipZIndexAfter = await getNodeZIndex(comfyPage, 'CLIP Text Encode') + const vaeZIndexAfter = await getNodeZIndex(comfyPage, 'VAE Decode') + expect(clipZIndexAfter).toBeGreaterThan(vaeZIndexAfter) + + // Screenshot showing CLIP now on top after widget click + await expect(comfyPage.canvas).toHaveScreenshot( + 'bring-to-front-widget-overlapped-after.png' + ) + }) +}) diff --git a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-after-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-after-chromium-linux.png new file mode 100644 index 0000000000..543528904f Binary files /dev/null and b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-after-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-before-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-before-chromium-linux.png new file mode 100644 index 0000000000..3b17ade02b Binary files /dev/null and b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-overlapped-before-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-after-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-after-chromium-linux.png new file mode 100644 index 0000000000..db1cba04c2 Binary files /dev/null and b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-after-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-before-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-before-chromium-linux.png new file mode 100644 index 0000000000..6db7f4ea3b Binary files /dev/null and b/browser_tests/tests/vueNodes/interactions/node/bringToFront.spec.ts-snapshots/bring-to-front-widget-overlapped-before-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png index 0a8792fc0a..15c83ee404 100644 Binary files a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png and b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-touch-mobile-chrome-linux.png b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-touch-mobile-chrome-linux.png index d66194cf20..87dd6f2da8 100644 Binary files a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-touch-mobile-chrome-linux.png and b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-touch-mobile-chrome-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts-snapshots/vue-node-bypassed-state-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts-snapshots/vue-node-bypassed-state-chromium-linux.png index 640e59fdd8..0b7cf999ea 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts-snapshots/vue-node-bypassed-state-chromium-linux.png and b/browser_tests/tests/vueNodes/nodeStates/bypass.spec.ts-snapshots/vue-node-bypassed-state-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png index 99186b1ef4..60ddfe142f 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png and b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-color-blue-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-dark-all-colors-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-dark-all-colors-chromium-linux.png index 30a3db231f..f3de99e7fd 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-dark-all-colors-chromium-linux.png and b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-dark-all-colors-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-light-all-colors-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-light-all-colors-chromium-linux.png index 39c5ecacd5..357c3fac23 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-light-all-colors-chromium-linux.png and b/browser_tests/tests/vueNodes/nodeStates/colors.spec.ts-snapshots/vue-node-custom-colors-light-all-colors-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts-snapshots/vue-node-muted-state-chromium-linux.png b/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts-snapshots/vue-node-muted-state-chromium-linux.png index b2aeeef56e..ea0e5dcd52 100644 Binary files a/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts-snapshots/vue-node-muted-state-chromium-linux.png and b/browser_tests/tests/vueNodes/nodeStates/mute.spec.ts-snapshots/vue-node-muted-state-chromium-linux.png differ diff --git a/browser_tests/tests/vueNodes/widgets/int/integerWidget.spec.ts b/browser_tests/tests/vueNodes/widgets/int/integerWidget.spec.ts index bb956e3395..ee6b6bbfb2 100644 --- a/browser_tests/tests/vueNodes/widgets/int/integerWidget.spec.ts +++ b/browser_tests/tests/vueNodes/widgets/int/integerWidget.spec.ts @@ -15,7 +15,9 @@ test.describe('Vue Integer Widget', () => { await comfyPage.loadWorkflow('vueNodes/linked-int-widget') await comfyPage.vueNodes.waitForNodes() - const seedWidget = comfyPage.vueNodes.getWidgetByName('KSampler', 'seed') + const seedWidget = comfyPage.vueNodes + .getWidgetByName('KSampler', 'seed') + .first() const controls = comfyPage.vueNodes.getInputNumberControls(seedWidget) const initialValue = Number(await controls.input.inputValue()) diff --git a/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts-snapshots/vue-nodes-upload-widgets-chromium-linux.png b/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts-snapshots/vue-nodes-upload-widgets-chromium-linux.png index 24491eb95c..dd25a81213 100644 Binary files a/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts-snapshots/vue-nodes-upload-widgets-chromium-linux.png and b/browser_tests/tests/vueNodes/widgets/load/uploadWidgets.spec.ts-snapshots/vue-nodes-upload-widgets-chromium-linux.png differ diff --git a/browser_tests/tests/widget.spec.ts b/browser_tests/tests/widget.spec.ts index 7afa823e35..002ad29242 100644 --- a/browser_tests/tests/widget.spec.ts +++ b/browser_tests/tests/widget.spec.ts @@ -252,7 +252,8 @@ test.describe('Animated image widget', () => { // Drag and drop image file onto the load animated webp node await comfyPage.dragAndDropFile('animated_webp.webp', { - dropPosition: { x, y } + dropPosition: { x, y }, + waitForUpload: true }) // Expect the filename combo value to be updated diff --git a/eslint.config.ts b/eslint.config.ts index 394df066d3..69efe4bef6 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -62,16 +62,20 @@ export default defineConfig([ { ignores: [ '.i18nrc.cjs', - 'components.d.ts', - 'lint-staged.config.js', - 'vitest.setup.ts', + '.nx/*', '**/vite.config.*.timestamp*', '**/vitest.config.*.timestamp*', + 'components.d.ts', + 'coverage/*', + 'dist/*', 'packages/registry-types/src/comfyRegistryTypes.ts', + 'playwright-report/*', 'src/extensions/core/*', 'src/scripts/*', 'src/types/generatedManagerTypes.ts', - 'src/types/vue-shim.d.ts' + 'src/types/vue-shim.d.ts', + 'test-results/*', + 'vitest.setup.ts' ] }, { @@ -103,24 +107,17 @@ export default defineConfig([ tseslintConfigs.recommended, // Difference in typecheck on CI vs Local - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore Bad types in the plugin pluginVue.configs['flat/recommended'], eslintPluginPrettierRecommended, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore Type incompatibility between import-x plugin and ESLint config types storybook.configs['flat/recommended'], - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore Type incompatibility between import-x plugin and ESLint config types + // @ts-expect-error Type incompatibility between import-x plugin and ESLint config types importX.flatConfigs.recommended, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore Type incompatibility between import-x plugin and ESLint config types + // @ts-expect-error Type incompatibility between import-x plugin and ESLint config types importX.flatConfigs.typescript, { plugins: { 'unused-imports': unusedImports, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore Type incompatibility in i18n plugin + // @ts-expect-error Type incompatibility in i18n plugin '@intlify/vue-i18n': pluginI18n }, rules: { @@ -138,7 +135,6 @@ export default defineConfig([ 'import-x/no-useless-path-segments': 'error', 'import-x/no-relative-packages': 'error', 'unused-imports/no-unused-imports': 'error', - 'no-console': ['error', { allow: ['warn', 'error'] }], 'vue/no-v-html': 'off', // Prohibit dark-theme: and dark: prefixes 'vue/no-restricted-class': ['error', '/^dark(-theme)?:/'], @@ -153,39 +149,7 @@ export default defineConfig([ 'vue/no-use-v-else-with-v-for': 'error', 'vue/one-component-per-file': 'error', 'vue/require-default-prop': 'off', // TODO: fix -- this one is very worthwhile - // Restrict deprecated PrimeVue components - 'no-restricted-imports': [ - 'error', - { - paths: [ - { - name: 'primevue/calendar', - message: - 'Calendar is deprecated in PrimeVue 4+. Use DatePicker instead: import DatePicker from "primevue/datepicker"' - }, - { - name: 'primevue/dropdown', - message: - 'Dropdown is deprecated in PrimeVue 4+. Use Select instead: import Select from "primevue/select"' - }, - { - name: 'primevue/inputswitch', - message: - 'InputSwitch is deprecated in PrimeVue 4+. Use ToggleSwitch instead: import ToggleSwitch from "primevue/toggleswitch"' - }, - { - name: 'primevue/overlaypanel', - message: - 'OverlayPanel is deprecated in PrimeVue 4+. Use Popover instead: import Popover from "primevue/popover"' - }, - { - name: 'primevue/sidebar', - message: - 'Sidebar is deprecated in PrimeVue 4+. Use Drawer instead: import Drawer from "primevue/drawer"' - } - ] - } - ], + // i18n rules '@intlify/vue-i18n/no-raw-text': [ 'error', @@ -273,12 +237,6 @@ export default defineConfig([ ] } }, - { - files: ['**/*.{test,spec,stories}.ts', '**/*.stories.vue'], - rules: { - 'no-console': 'off' - } - }, { files: ['scripts/**/*.js'], languageOptions: { diff --git a/lint-staged.config.js b/lint-staged.config.js deleted file mode 100644 index 5283d261ed..0000000000 --- a/lint-staged.config.js +++ /dev/null @@ -1,17 +0,0 @@ -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) => f.replace(process.cwd() + '/', '')) - return [ - `pnpm exec eslint --cache --fix ${relativePaths.join(' ')}`, - `pnpm exec prettier --cache --write ${relativePaths.join(' ')}` - ] -} diff --git a/lint-staged.config.ts b/lint-staged.config.ts new file mode 100644 index 0000000000..89deaa4b0e --- /dev/null +++ b/lint-staged.config.ts @@ -0,0 +1,21 @@ +import path from 'node:path' + +export default { + './**/*.js': (stagedFiles: string[]) => formatAndEslint(stagedFiles), + + './**/*.{ts,tsx,vue,mts}': (stagedFiles: string[]) => [ + ...formatAndEslint(stagedFiles), + 'pnpm typecheck' + ] +} + +function formatAndEslint(fileNames: string[]) { + // 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}` + ] +} diff --git a/package.json b/package.json index 746ae4efa2..eb7190b288 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@comfyorg/comfyui-frontend", "private": true, - "version": "1.35.1", + "version": "1.35.6", "type": "module", "repository": "https://github.com/Comfy-Org/ComfyUI_frontend", "homepage": "https://comfy.org", diff --git a/packages/design-system/src/css/style.css b/packages/design-system/src/css/style.css index f23f9a4084..1153d4543a 100644 --- a/packages/design-system/src/css/style.css +++ b/packages/design-system/src/css/style.css @@ -98,7 +98,6 @@ --color-bypass: #6a246a; --color-error: #962a2a; - --color-comfy-menu-secondary: var(--comfy-menu-secondary-bg); --color-interface-panel-job-progress-primary: var(--color-azure-300); --color-interface-panel-job-progress-secondary: var(--color-alpha-azure-600-30); @@ -438,7 +437,11 @@ --color-interface-button-hover-surface: var( --interface-button-hover-surface ); + --color-comfy-input: var(--comfy-input-bg); + --color-comfy-input-foreground: var(--input-text); --color-comfy-menu-bg: var(--comfy-menu-bg); + --color-comfy-menu-secondary: var(--comfy-menu-secondary-bg); + --color-interface-stroke: var(--interface-stroke); --color-nav-background: var(--nav-background); --color-node-border: var(--node-border); @@ -1325,6 +1328,15 @@ audio.comfy-audio.empty-audio-widget { font-size 0.1s ease; } +/* Performance optimization during canvas interaction */ +.transform-pane--interacting .lg-node * { + transition: none !important; +} + +.transform-pane--interacting .lg-node { + will-change: transform; +} + /* ===================== Mask Editor Styles ===================== */ /* To be migrated to Tailwind later */ #maskEditor_brush { diff --git a/packages/registry-types/src/comfyRegistryTypes.ts b/packages/registry-types/src/comfyRegistryTypes.ts index f7df7081ae..f358588e04 100644 --- a/packages/registry-types/src/comfyRegistryTypes.ts +++ b/packages/registry-types/src/comfyRegistryTypes.ts @@ -1945,6 +1945,40 @@ export interface paths { patch?: never; trace?: never; }; + "/proxy/kling/v1/images/omni-image": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** KlingAI Create Omni-Image Task */ + post: operations["klingCreateOmniImage"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/proxy/kling/v1/images/omni-image/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** KlingAI Query Single Omni-Image Task */ + get: operations["klingOmniImageQuerySingleTask"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/proxy/kling/v1/images/kolors-virtual-try-on": { parameters: { query?: never; @@ -3876,7 +3910,7 @@ export interface components { * @description The subscription tier level * @enum {string} */ - SubscriptionTier: "STANDARD" | "CREATOR" | "PRO"; + SubscriptionTier: "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION"; FeaturesResponse: { /** * @description The conversion rate for partner nodes @@ -5096,6 +5130,71 @@ export interface components { }; }; }; + KlingOmniImageRequest: { + /** + * @description Model Name + * @default kling-image-o1 + * @enum {string} + */ + model_name: "kling-image-o1"; + /** @description Text prompt words, which can include positive and negative descriptions. Must not exceed 2,500 characters. The Omni model can achieve various capabilities through Prompt with elements and images. Specify an image in the format of <<<>>>, such as <<>>. */ + prompt: string; + /** @description Reference Image List. Supports inputting image Base64 encoding or image URL (ensure accessibility). Supported formats include .jpg/.jpeg/.png. File size cannot exceed 10MB. Width and height dimensions shall not be less than 300px, aspect ratio between 1:2.5 ~ 2.5:1. Maximum 10 images. */ + image_list?: { + /** @description Image Base64 encoding or image URL (ensure accessibility) */ + image?: string; + }[]; + /** + * @description Image generation resolution. 1k is 1K standard, 2k is 2K high-res, 4k is 4K high-res. + * @default 1k + * @enum {string} + */ + resolution: "1k" | "2k" | "4k"; + /** + * @description Number of generated images. Value range [1,9]. + * @default 1 + */ + n: number; + /** + * @description Aspect ratio of the generated images (width:height). auto is to intelligently generate images based on incoming content. + * @default auto + * @enum {string} + */ + aspect_ratio: "16:9" | "9:16" | "1:1" | "4:3" | "3:4" | "3:2" | "2:3" | "21:9" | "auto"; + /** + * Format: uri + * @description The callback notification address for the result of this task. If configured, the server will actively notify when the task status changes. + */ + callback_url?: string; + /** @description Customized Task ID. Must be unique within a single user account. */ + external_task_id?: string; + }; + KlingOmniImageResponse: { + /** @description Error code */ + code?: number; + /** @description Error message */ + message?: string; + /** @description Request ID */ + request_id?: string; + data?: { + /** @description Task ID */ + task_id?: string; + task_status?: components["schemas"]["KlingTaskStatus"]; + /** @description Task status information, displaying the failure reason when the task fails (such as triggering the content risk control of the platform, etc.) */ + task_status_msg?: string; + task_info?: { + /** @description Customer-defined task ID */ + external_task_id?: string; + }; + /** @description Task creation time, Unix timestamp in milliseconds */ + created_at?: number; + /** @description Task update time, Unix timestamp in milliseconds */ + updated_at?: number; + task_result?: { + images?: components["schemas"]["KlingImageResult"][]; + }; + }; + }; KlingLipSyncInputObject: { /** @description The ID of the video generated by Kling AI. Only supports 5-second and 10-second videos generated within the last 30 days. */ video_id?: string; @@ -10065,7 +10164,7 @@ export interface components { }; BytePlusImageGenerationRequest: { /** @enum {string} */ - model: "seedream-3-0-t2i-250415" | "seededit-3-0-i2i-250628" | "seedream-4-0-250828"; + model: "seedream-3-0-t2i-250415" | "seededit-3-0-i2i-250628" | "seedream-4-0-250828" | "seedream-4-5-251128"; /** @description Text description for image generation or transformation */ prompt: string; /** @@ -10170,10 +10269,10 @@ export interface components { }; BytePlusVideoGenerationRequest: { /** - * @description The ID of the model to call. Available models include seedance-1-0-pro-250528, seedance-1-0-lite-t2v-250428, seedance-1-0-lite-i2v-250428 + * @description The ID of the model to call. Available models include seedance-1-0-pro-250528, seedance-1-0-pro-fast-251015, seedance-1-0-lite-t2v-250428, seedance-1-0-lite-i2v-250428 * @enum {string} */ - model: "seedance-1-0-pro-250528" | "seedance-1-0-lite-t2v-250428" | "seedance-1-0-lite-i2v-250428"; + model: "seedance-1-0-pro-250528" | "seedance-1-0-lite-t2v-250428" | "seedance-1-0-lite-i2v-250428" | "seedance-1-0-pro-fast-251015"; /** @description The input content for the model to generate a video */ content: components["schemas"]["BytePlusVideoGenerationContent"][]; /** @@ -13947,6 +14046,15 @@ export interface operations { "application/json": components["schemas"]["Node"]; }; }; + /** @description Redirect to node with normalized name match */ + 302: { + headers: { + /** @description URL of the node with the correct ID */ + Location?: string; + [name: string]: unknown; + }; + content?: never; + }; /** @description Forbidden */ 403: { headers: { @@ -18345,6 +18453,198 @@ export interface operations { }; }; }; + klingCreateOmniImage: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Create task for generating omni-image */ + requestBody: { + content: { + "application/json": components["schemas"]["KlingOmniImageRequest"]; + }; + }; + responses: { + /** @description Successful response (Request successful) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["KlingOmniImageResponse"]; + }; + }; + /** @description Invalid request parameters */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["KlingErrorResponse"]; + }; + }; + /** @description Authentication failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["KlingErrorResponse"]; + }; + }; + /** @description Unauthorized access to requested resource */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["KlingErrorResponse"]; + }; + }; + /** @description Resource not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["KlingErrorResponse"]; + }; + }; + /** @description Account exception or Rate limit exceeded */ + 429: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["KlingErrorResponse"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["KlingErrorResponse"]; + }; + }; + /** @description Service temporarily unavailable */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["KlingErrorResponse"]; + }; + }; + /** @description Server timeout */ + 504: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["KlingErrorResponse"]; + }; + }; + }; + }; + klingOmniImageQuerySingleTask: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Task ID or External Task ID. Can query by either task_id (generated by system) or external_task_id (customized task ID) */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful response (Request successful) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["KlingOmniImageResponse"]; + }; + }; + /** @description Invalid request parameters */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["KlingErrorResponse"]; + }; + }; + /** @description Authentication failed */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["KlingErrorResponse"]; + }; + }; + /** @description Unauthorized access to requested resource */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["KlingErrorResponse"]; + }; + }; + /** @description Resource not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["KlingErrorResponse"]; + }; + }; + /** @description Account exception or Rate limit exceeded */ + 429: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["KlingErrorResponse"]; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["KlingErrorResponse"]; + }; + }; + /** @description Service temporarily unavailable */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["KlingErrorResponse"]; + }; + }; + /** @description Server timeout */ + 504: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["KlingErrorResponse"]; + }; + }; + }; + }; klingVirtualTryOnQueryTaskList: { parameters: { query?: { diff --git a/scripts/cicd/check-shell.sh b/scripts/cicd/check-shell.sh new file mode 100755 index 0000000000..a95bcde28f --- /dev/null +++ b/scripts/cicd/check-shell.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(git rev-parse --show-toplevel)" +cd "$ROOT_DIR" + +if ! command -v shellcheck >/dev/null 2>&1; then + echo "Error: shellcheck is required but not installed" >&2 + exit 127 +fi + +mapfile -t shell_files < <(git ls-files -- '*.sh') + +if [[ ${#shell_files[@]} -eq 0 ]]; then + echo 'No shell scripts found.' + exit 0 +fi + +shellcheck --format=gcc "${shell_files[@]}" diff --git a/scripts/cicd/pr-playwright-deploy-and-comment.sh b/scripts/cicd/pr-playwright-deploy-and-comment.sh index aeab37c8e3..840203f44a 100755 --- a/scripts/cicd/pr-playwright-deploy-and-comment.sh +++ b/scripts/cicd/pr-playwright-deploy-and-comment.sh @@ -74,7 +74,7 @@ deploy_report() { # Project name with dots converted to dashes for Cloudflare - sanitized_browser=$(echo "$browser" | sed 's/\./-/g') + sanitized_browser="${browser//./-}" project="comfyui-playwright-${sanitized_browser}" echo "Deploying $browser to project $project on branch $branch..." >&2 @@ -208,7 +208,7 @@ else # Wait for all deployments to complete for pid in $pids; do - wait $pid + wait "$pid" done # Collect URLs and counts in order @@ -254,9 +254,9 @@ else total_tests=0 # Parse counts and calculate totals - IFS='|' - set -- $all_counts - for counts_json; do + IFS='|' read -r -a counts_array <<< "$all_counts" + for counts_json in "${counts_array[@]}"; do + [ -z "$counts_json" ] && continue if [ "$counts_json" != "{}" ] && [ -n "$counts_json" ]; then # Parse JSON counts using simple grep/sed if jq is not available if command -v jq > /dev/null 2>&1; then @@ -324,13 +324,12 @@ $status_icon **$status_text** # Add browser results with individual counts i=0 - IFS='|' - set -- $all_counts - for counts_json; do - # Get browser name - browser=$(echo "$BROWSERS" | cut -d' ' -f$((i + 1))) - # Get URL at position i - url=$(echo "$urls" | cut -d' ' -f$((i + 1))) + IFS=' ' read -r -a browser_array <<< "$BROWSERS" + IFS=' ' read -r -a url_array <<< "$urls" + for counts_json in "${counts_array[@]}"; do + [ -z "$counts_json" ] && { i=$((i + 1)); continue; } + browser="${browser_array[$i]:-}" + url="${url_array[$i]:-}" if [ "$url" != "failed" ] && [ -n "$url" ]; then # Parse individual browser counts @@ -374,4 +373,4 @@ $status_icon **$status_text** 🎉 Click on the links above to view detailed test results for each browser configuration." post_comment "$comment" -fi \ No newline at end of file +fi diff --git a/scripts/cicd/resolve-comfyui-release.ts b/scripts/cicd/resolve-comfyui-release.ts index 0d5462bd45..56f6b06a77 100755 --- a/scripts/cicd/resolve-comfyui-release.ts +++ b/scripts/cicd/resolve-comfyui-release.ts @@ -264,7 +264,7 @@ if (!releaseInfo) { } // Output as JSON for GitHub Actions -// eslint-disable-next-line no-console + console.log(JSON.stringify(releaseInfo, null, 2)) export { resolveRelease } diff --git a/src/components/actionbar/ComfyActionbar.vue b/src/components/actionbar/ComfyActionbar.vue index 1d831db21b..fa2096aee0 100644 --- a/src/components/actionbar/ComfyActionbar.vue +++ b/src/components/actionbar/ComfyActionbar.vue @@ -15,14 +15,7 @@ :style="style" class="flex flex-col items-stretch" > - +
- + + +
- +
position.value !== 'Disabled') const tabContainer = document.querySelector('.workflow-tabs-container') const actionbarWrapperRef = ref(null) -const panelRef = ref(null) +const panelRef = ref(null) const dragHandleRef = ref(null) const docked = computed({ get: () => props.docked ?? false, @@ -244,7 +237,14 @@ const setInitialPosition = () => { } } } -onMounted(setInitialPosition) + +//The ComfyRunButton is a dynamic import. Which means it will not be loaded onMount in this component. +//So we must use suspense resolve to ensure that is has loaded and updated the DOM before calling setInitialPosition() +async function comfyRunButtonResolved() { + await nextTick() + setInitialPosition() +} + watch(visible, async (newVisible) => { if (newVisible) { await nextTick(setInitialPosition) diff --git a/src/components/actionbar/ComfyRunButton/ComfyQueueButton.vue b/src/components/actionbar/ComfyRunButton/ComfyQueueButton.vue index c9e8495549..036d575bf5 100644 --- a/src/components/actionbar/ComfyRunButton/ComfyQueueButton.vue +++ b/src/components/actionbar/ComfyRunButton/ComfyQueueButton.vue @@ -58,7 +58,7 @@ const { mode: queueMode, batchCount } = storeToRefs(useQueueSettingsStore()) const nodeDefStore = useNodeDefStore() const hasMissingNodes = computed(() => - graphHasMissingNodes(app.graph, nodeDefStore.nodeDefsByName) + graphHasMissingNodes(app.rootGraph, nodeDefStore.nodeDefsByName) ) const { t } = useI18n() diff --git a/src/components/breadcrumb/SubgraphBreadcrumbItem.vue b/src/components/breadcrumb/SubgraphBreadcrumbItem.vue index c743f0d988..8ffbd1e792 100644 --- a/src/components/breadcrumb/SubgraphBreadcrumbItem.vue +++ b/src/components/breadcrumb/SubgraphBreadcrumbItem.vue @@ -83,7 +83,7 @@ const props = withDefaults(defineProps(), { const nodeDefStore = useNodeDefStore() const hasMissingNodes = computed(() => - graphHasMissingNodes(app.graph, nodeDefStore.nodeDefsByName) + graphHasMissingNodes(app.rootGraph, nodeDefStore.nodeDefsByName) ) const { t } = useI18n() diff --git a/src/components/input/SearchBox.stories.ts b/src/components/common/SearchBox.stories.ts similarity index 75% rename from src/components/input/SearchBox.stories.ts rename to src/components/common/SearchBox.stories.ts index f339fa1e7a..cc054d4caa 100644 --- a/src/components/input/SearchBox.stories.ts +++ b/src/components/common/SearchBox.stories.ts @@ -1,13 +1,20 @@ import type { Meta, StoryObj } from '@storybook/vue3-vite' import { ref } from 'vue' -import SearchBox from './SearchBox.vue' +import SearchBox from '@/components/common/SearchBox.vue' +import type { ComponentExposed } from 'vue-component-type-helpers' +interface GenericMeta extends Omit, 'component'> { + component: Omit, 'focus'> +} -const meta: Meta = { +const meta: GenericMeta = { title: 'Components/Input/SearchBox', component: SearchBox, tags: ['autodocs'], argTypes: { + modelValue: { + control: 'text' + }, placeholder: { control: 'text' }, @@ -19,9 +26,12 @@ const meta: Meta = { control: 'select', options: ['md', 'lg'], description: 'Size variant of the search box' - } + }, + 'onUpdate:modelValue': { action: 'update:modelValue' }, + onSearch: { action: 'search' } }, args: { + modelValue: '', placeholder: 'Search...', showBorder: false, size: 'md' diff --git a/src/components/input/SearchBox.test.ts b/src/components/common/SearchBox.test.ts similarity index 85% rename from src/components/input/SearchBox.test.ts rename to src/components/common/SearchBox.test.ts index 575cbb0f55..5fe9294d86 100644 --- a/src/components/input/SearchBox.test.ts +++ b/src/components/common/SearchBox.test.ts @@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { nextTick } from 'vue' import { createI18n } from 'vue-i18n' -import SearchBox from './SearchBox.vue' +import SearchBox from '@/components/common/SearchBox.vue' const i18n = createI18n({ legacy: false, @@ -50,15 +50,15 @@ describe('SearchBox', () => { await input.setValue('test') // Model should not update immediately - expect(wrapper.emitted('update:modelValue')).toBeFalsy() + expect(wrapper.emitted('search')).toBeFalsy() // Advance timers by 299ms (just before debounce delay) - vi.advanceTimersByTime(299) + await vi.advanceTimersByTimeAsync(299) await nextTick() - expect(wrapper.emitted('update:modelValue')).toBeFalsy() + expect(wrapper.emitted('search')).toBeFalsy() // Advance timers by 1ms more (reaching 300ms) - vi.advanceTimersByTime(1) + await vi.advanceTimersByTimeAsync(1) await nextTick() // Model should now be updated @@ -82,19 +82,19 @@ describe('SearchBox', () => { // Type third character (should reset timer again) await input.setValue('tes') - vi.advanceTimersByTime(200) + await vi.advanceTimersByTimeAsync(200) await nextTick() // Should not have emitted yet (only 200ms passed since last keystroke) - expect(wrapper.emitted('update:modelValue')).toBeFalsy() + expect(wrapper.emitted('search')).toBeFalsy() // Advance final 100ms to reach 300ms - vi.advanceTimersByTime(100) + await vi.advanceTimersByTimeAsync(100) await nextTick() // Should now emit with final value - expect(wrapper.emitted('update:modelValue')).toBeTruthy() - expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['tes']) + expect(wrapper.emitted('search')).toBeTruthy() + expect(wrapper.emitted('search')?.[0]).toEqual(['tes', []]) }) it('should only emit final value after rapid typing', async () => { @@ -105,19 +105,20 @@ describe('SearchBox', () => { const searchTerms = ['s', 'se', 'sea', 'sear', 'searc', 'search'] for (const term of searchTerms) { await input.setValue(term) - vi.advanceTimersByTime(50) // Less than debounce delay + await vi.advanceTimersByTimeAsync(50) // Less than debounce delay } + await nextTick() // Should not have emitted yet - expect(wrapper.emitted('update:modelValue')).toBeFalsy() + expect(wrapper.emitted('search')).toBeFalsy() // Complete the debounce delay - vi.advanceTimersByTime(300) + await vi.advanceTimersByTimeAsync(350) await nextTick() // Should emit only once with final value - expect(wrapper.emitted('update:modelValue')).toHaveLength(1) - expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['search']) + expect(wrapper.emitted('search')).toHaveLength(1) + expect(wrapper.emitted('search')?.[0]).toEqual(['search', []]) }) describe('bidirectional model sync', () => { diff --git a/src/components/common/SearchBox.vue b/src/components/common/SearchBox.vue index 00b705ce17..d7db4d5341 100644 --- a/src/components/common/SearchBox.vue +++ b/src/components/common/SearchBox.vue @@ -1,84 +1,93 @@ diff --git a/src/renderer/core/layout/transform/useTransformSettling.ts b/src/renderer/core/layout/transform/useTransformSettling.ts new file mode 100644 index 0000000000..a336eef7ea --- /dev/null +++ b/src/renderer/core/layout/transform/useTransformSettling.ts @@ -0,0 +1,84 @@ +import { useDebounceFn, useEventListener } from '@vueuse/core' +import { ref } from 'vue' +import type { MaybeRefOrGetter } from 'vue' + +interface TransformSettlingOptions { + /** + * Delay in ms before transform is considered "settled" after last interaction + * @default 200 + */ + settleDelay?: number + /** + * Whether to use passive event listeners (better performance but can't preventDefault) + * @default true + */ + passive?: boolean +} + +/** + * Tracks when canvas zoom transforms are actively changing vs settled. + * + * This composable helps optimize rendering quality during zoom transformations. + * When the user is actively zooming, we can reduce rendering quality + * for better performance. Once the transform "settles" (stops changing), we can + * trigger high-quality re-rasterization. + * + * The settling concept prevents constant quality switching during interactions + * by waiting for a period of inactivity before considering the transform complete. + * + * Uses VueUse's useEventListener for automatic cleanup and useDebounceFn for + * efficient settle detection. + * + * @example + * ```ts + * const { isTransforming } = useTransformSettling(canvasRef, { + * settleDelay: 200 + * }) + * + * // Use in CSS classes or rendering logic + * const cssClass = computed(() => ({ + * 'low-quality': isTransforming.value, + * 'high-quality': !isTransforming.value + * })) + * ``` + */ +export function useTransformSettling( + target: MaybeRefOrGetter, + options: TransformSettlingOptions = {} +) { + const { settleDelay = 256, passive = true } = options + + const isTransforming = ref(false) + + /** + * Mark transform as active + */ + const markTransformActive = () => { + isTransforming.value = true + } + + /** + * Mark transform as settled (debounced) + */ + const markTransformSettled = useDebounceFn(() => { + isTransforming.value = false + }, settleDelay) + + /** + * Handle zoom transform event - mark active then queue settle + */ + const handleWheel = () => { + markTransformActive() + void markTransformSettled() + } + + // Register wheel event listener with auto-cleanup + useEventListener(target, 'wheel', handleWheel, { + capture: true, + passive + }) + + return { + isTransforming + } +} diff --git a/src/renderer/core/layout/utils/layoutMath.ts b/src/renderer/core/layout/utils/layoutMath.ts index adb73aa3d6..ff841b340d 100644 --- a/src/renderer/core/layout/utils/layoutMath.ts +++ b/src/renderer/core/layout/utils/layoutMath.ts @@ -41,13 +41,3 @@ export function calculateBounds(nodes: NodeLayout[]): Bounds { height: maxY - minY } } - -/** - * Calculate combined bounds for Vue nodes selection - * @param nodes Array of NodeLayout objects to calculate bounds for - * @returns Bounds of the nodes or null if no nodes provided - */ -export function selectionBounds(nodes: NodeLayout[]): Bounds | null { - if (nodes.length === 0) return null - return calculateBounds(nodes) -} diff --git a/src/renderer/extensions/vueNodes/VideoPreview.vue b/src/renderer/extensions/vueNodes/VideoPreview.vue index 6c42a51cc0..635e5564ca 100644 --- a/src/renderer/extensions/vueNodes/VideoPreview.vue +++ b/src/renderer/extensions/vueNodes/VideoPreview.vue @@ -90,7 +90,7 @@
-
+
{{ $t('g.errorLoadingVideo') }} diff --git a/src/renderer/extensions/vueNodes/components/ImagePreview.vue b/src/renderer/extensions/vueNodes/components/ImagePreview.vue index 4429c50322..538a5a5d34 100644 --- a/src/renderer/extensions/vueNodes/components/ImagePreview.vue +++ b/src/renderer/extensions/vueNodes/components/ImagePreview.vue @@ -1,21 +1,26 @@ diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberSlider.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberSlider.vue index ee9eec6fa2..a7f1463552 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberSlider.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberSlider.vue @@ -39,6 +39,7 @@ import { filterWidgetProps } from '@/utils/widgetPropFilter' +import { useNumberStepCalculation } from '../composables/useNumberStepCalculation' import { useNumberWidgetButtonPt } from '../composables/useNumberWidgetButtonPt' import { WidgetInputBaseClass } from './layout' import WidgetLayoutField from './layout/WidgetLayoutField.vue' @@ -56,7 +57,7 @@ const updateLocalValue = (newValue: number[] | undefined): void => { } const handleNumberInputUpdate = (newValue: number | undefined) => { - if (newValue) { + if (newValue !== undefined) { updateLocalValue([newValue]) return } @@ -67,33 +68,11 @@ const filteredProps = computed(() => filterWidgetProps(widget.options, STANDARD_EXCLUDED_PROPS) ) -// Get the precision value for proper number formatting -const precision = computed(() => { - const p = widget.options?.precision - // Treat negative or non-numeric precision as undefined - return typeof p === 'number' && p >= 0 ? p : undefined -}) +const p = widget.options?.precision +const precision = typeof p === 'number' && p >= 0 ? p : undefined // Calculate the step value based on precision or widget options -const stepValue = computed(() => { - // Use step2 (correct input spec value) instead of step (legacy 10x value) - if (widget.options?.step2 !== undefined) { - return widget.options.step2 - } - - // Otherwise, derive from precision - if (precision.value === undefined) { - return undefined - } - - if (precision.value === 0) { - return 1 - } - - // For precision > 0, step = 1 / (10^precision) - // precision 1 → 0.1, precision 2 → 0.01, etc. - return 1 / Math.pow(10, precision.value) -}) +const stepValue = useNumberStepCalculation(widget.options, precision, true) const sliderNumberPt = useNumberWidgetButtonPt({ roundedLeft: true, diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberWithControl.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberWithControl.vue new file mode 100644 index 0000000000..f333fe48de --- /dev/null +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberWithControl.vue @@ -0,0 +1,67 @@ + + + diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetRecordAudio.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetRecordAudio.vue index 73c37111ff..aa0fc4df32 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetRecordAudio.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetRecordAudio.vue @@ -155,8 +155,8 @@ const isWaveformActive = computed(() => isRecording.value || isPlaying.value) const modelValue = defineModel({ default: '' }) const litegraphNode = computed(() => { - if (!props.nodeId || !app.rootGraph) return null - return app.rootGraph.getNodeById(props.nodeId) as LGraphNode | null + if (!props.nodeId || !app.canvas.graph) return null + return app.canvas.graph.getNodeById(props.nodeId) as LGraphNode | null }) async function handleRecordingComplete(blob: Blob) { diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue index 78702bafa5..28a148c45d 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue @@ -146,11 +146,9 @@ const outputItems = computed(() => { }) const allItems = computed(() => { - if (props.isAssetMode && assetData) { - return assetData.dropdownItems.value - } return [...inputItems.value, ...outputItems.value] }) + const dropdownItems = computed(() => { if (props.isAssetMode) { return allItems.value @@ -163,7 +161,7 @@ const dropdownItems = computed(() => { return outputItems.value case 'all': default: - return allItems.value + return [...inputItems.value, ...outputItems.value] } }) diff --git a/src/renderer/extensions/vueNodes/widgets/components/audio/AudioPreviewPlayer.vue b/src/renderer/extensions/vueNodes/widgets/components/audio/AudioPreviewPlayer.vue index 9f5eb24598..bf69add86c 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/audio/AudioPreviewPlayer.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/audio/AudioPreviewPlayer.vue @@ -185,8 +185,8 @@ const showVolumeTwo = computed(() => !isMuted.value && volume.value > 0.5) const showVolumeOne = computed(() => isMuted.value && volume.value > 0) const litegraphNode = computed(() => { - if (!props.nodeId || !app.rootGraph) return null - return app.rootGraph.getNodeById(props.nodeId) as LGraphNode | null + if (!props.nodeId || !app.canvas.graph) return null + return app.canvas.graph.getNodeById(props.nodeId) as LGraphNode | null }) const hidden = computed(() => { diff --git a/src/renderer/extensions/vueNodes/widgets/components/layout/WidgetLayoutField.vue b/src/renderer/extensions/vueNodes/widgets/components/layout/WidgetLayoutField.vue index e2e45b739c..696e37a45a 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/layout/WidgetLayoutField.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/layout/WidgetLayoutField.vue @@ -1,5 +1,4 @@