Compare commits

...

10 Commits

Author SHA1 Message Date
bymyself
24c124c20b fix: use exact match for Replace All button to avoid strict mode violation 2026-04-14 19:39:02 +00:00
bymyself
9b91af567e test: add e2e tests for node replacement flows 2026-04-14 19:26:36 +00:00
pythongosssss
34a02a29c9 test: Remove unnecessary setup, UseNewMenu and waitForNodes calls (#11237)
## Summary

More simplification

## Changes

- **What**: 
- Remove more UseNewMenu settings calls
- Remove `await comfyPage.setup()`
- Remove `waitForNodes` in vue node tagged tests

┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11237-test-Remove-unnecessary-setup-UseNewMenu-and-waitForNodes-calls-3426d73d36508198a100c218420d479c)
by [Unito](https://www.unito.io)
2026-04-14 17:40:23 +00:00
Alexander Brown
4bd655f625 feat: add PreToolUse hooks to enforce pnpm scripts (#11201)
## Summary

Add Claude Code PreToolUse hooks to block agents from bypassing
package.json scripts with raw tool invocations.

## Changes

- **What**: 15 PreToolUse hooks in `.claude/settings.json` that
intercept `npx`/`pnpx`/bare invocations of tsc, vitest, eslint,
prettier, oxlint, stylelint, and knip — redirecting agents to the
correct `pnpm` script (`pnpm typecheck`, `pnpm test:unit`, `pnpm lint`,
etc.)
- Also removes stale `permissions.allow` entries left over from a
debugging session

## Review Focus

- Pattern coverage: are there common agent invocations we're missing?
- The `if` field only supports simple `*` globs (no alternation), so
each pattern needs its own hook entry

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11201-feat-add-PreToolUse-hooks-to-enforce-pnpm-scripts-3416d73d365081a59a38c86ee4669aee)
by [Unito](https://www.unito.io)

Co-authored-by: Amp <amp@ampcode.com>
2026-04-14 17:38:15 +00:00
pythongosssss
a09bb81b98 test: Auto wait for nodes after loadWorkflow in vue-node tests (#11238)
## Summary

Updates tests to auto wait for vue-nodes when loading a workflow in a
test with the vue-nodes tag

## Changes

- **What**: 
- If tag includes vue-nodes, wait 
- Remove all load->wait calls

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11238-test-Auto-wait-for-nodes-after-loadWorkflow-in-vue-node-tests-3426d73d3650810e8760c5601186fde8)
by [Unito](https://www.unito.io)
2026-04-14 17:30:49 +00:00
Christian Byrne
aeedb60628 fix(ci): resolve pnpm version in release workflow for frontend/ checkout path (#11224)
The release workflow checks out to `frontend/` subdirectory, but
`pnpm/action-setup` looks for `package.json` at the repo root by
default. This causes `No pnpm version is specified` failures.

Adds `package_json_file: frontend/package.json` so the action can read
the `packageManager` field. Same pattern used in #10972 for the
version-bump workflow.

┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11224-fix-ci-resolve-pnpm-version-in-release-workflow-for-frontend-checkout-path-3426d73d365081c28d16cb01bf8218ef)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-04-13 21:41:22 -07:00
Christian Byrne
43fb5a8b19 ops: add patch release support to ComfyUI release workflow (#11223)
## Summary

Add `release_type` input (`minor`/`patch`) to the release workflow so
patch releases can target the current production branch instead of
always preferring the next minor.

## Problem

When a patch release is needed for `core/1.42` but `core/1.43` already
exists, the resolver always prefers `1.43`. There was no way to do a
patch release with PyPI publish + ComfyUI PR for the current production
version.

## Changes

- Rename workflow from "Release: Bi-weekly ComfyUI" → "Release: ComfyUI"
(serves both cadences)
- Add `release_type` choice input: `minor` (default, bi-weekly) vs
`patch` (hotfix for current production version)
- Update `resolve-comfyui-release.ts` to read `RELEASE_TYPE` env var for
branch targeting
- Scheduled runs continue to work as before (default to `minor`)

## Usage

```bash
# Bi-weekly minor release (or just let the schedule run)
gh workflow run release-biweekly-comfyui.yaml --ref main

# Patch release for current production version
gh workflow run release-biweekly-comfyui.yaml --ref main --field release_type=patch
```

┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11223-ops-add-patch-release-support-to-ComfyUI-release-workflow-3426d73d365081498c15ff978a7f1116)
by [Unito](https://www.unito.io)
2026-04-13 21:10:17 -07:00
Christian Byrne
c484c3984f fix: remove appendTo override from FormDropdown to restore correct positioning (#11147)
## Problem

PR #10338 added `useTransformCompatOverlayProps()` to FormDropdown and
FormDropdownMenuActions, which sets `appendTo: 'self'` in graph mode.
This breaks PrimeVue Popover positioning inside CSS-transformed
containers — the dropdown appears at incorrect Y positions.

## Root Cause

PrimeVue Popover with `appendTo: 'self'` renders the overlay inside the
component's DOM, inheriting parent CSS transforms. This causes the
popover to miscalculate its position when the parent has `transform:
scale()` or `translate()`.

## Fix

Remove the `appendTo` override from both FormDropdown and
FormDropdownMenuActions. PrimeVue defaults to `appendTo: 'body'`, which
teleports the popover to `<body>` — correctly positioning it outside any
CSS transform context.

- **Graph mode**: restores pre-#10338 behavior (`appendTo: 'body'`
default)
- **App mode**: unaffected — `'body'` is exactly what app mode needs
(prevents sidebar overflow clipping)

## Testing

- Existing unit tests pass (5/5)
- Typecheck clean
- Lint clean
- **E2E test rationale**: No E2E test added — this is a pure removal of
a prop override (reverting to PrimeVue defaults). The positioning bug
requires CSS transforms at specific viewport scales which are
impractical to assert reliably in Playwright. The existing
`subgraph-dom-widget-clipping` perf test exercises dropdown rendering in
transformed contexts and shows no regression.

Fixes #10499
Supersedes #11001 (temporary hotfix for backport)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-04-13 20:02:39 -07:00
Dante
2524846f5c fix: guard progress_text before canvas init (#11174)
## Summary
Prevent early `progress_text` websocket events from throwing before the
graph canvas is initialized.

## Changes
- **What**: Guard `handleProgressText()` until `canvasStore.canvas`
exists, and add a regression test for a startup-time `progress_text`
event arriving before `GraphCanvas` finishes initialization.

## Review Focus
Confirm this is the right guard point for the startup race between
`GraphView` websocket binding and `GraphCanvas` async setup, and that
progress text behavior is unchanged once the canvas is ready.

## Validation
- `pnpm exec eslint src/stores/executionStore.ts
src/stores/executionStore.test.ts`
- `pnpm exec vitest run src/stores/executionStore.test.ts -t "should
ignore progress_text before the canvas is initialized"`
- `pnpm test:unit -- --run src/stores/executionStore.test.ts` still
reports one unrelated isolated-file failure in
`nodeLocatorIdToExecutionId` on current `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11174-fix-guard-progress_text-before-canvas-init-3406d73d3650813dad23d511fb51add5)
by [Unito](https://www.unito.io)
2026-04-13 23:47:14 +00:00
Comfy Org PR Bot
12f578870e 1.44.4 (#11177)
Patch version increment to 1.44.4

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11177-1-44-4-3416d73d365081c0a2e0def7071c1441)
by [Unito](https://www.unito.io)

---------

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-04-13 23:19:20 +00:00
50 changed files with 704 additions and 106 deletions

View File

@@ -1,9 +1,86 @@
{
"permissions": {
"allow": [
"Bash(pnpx vitest run --testPathPattern=\"draftCacheV2.property\")",
"Bash(pnpx vitest run \"draftCacheV2.property\")",
"Bash(node -e \"const fc = require\\(''fast-check''\\); console.log\\(Object.keys\\(fc\\).filter\\(k => k.includes\\(''string''\\)\\).join\\('', ''\\)\\)\")"
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(tsc *)",
"command": "echo 'Use `pnpm typecheck` instead of running tsc directly.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(vue-tsc *)",
"command": "echo 'Use `pnpm typecheck` instead of running vue-tsc directly.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(npx tsc *)",
"command": "echo 'Use `pnpm typecheck` instead of running tsc via npx.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(pnpx tsc *)",
"command": "echo 'Use `pnpm typecheck` instead of running tsc via pnpx.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(pnpm exec tsc *)",
"command": "echo 'Use `pnpm typecheck` instead of `pnpm exec tsc`.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(npx vitest *)",
"command": "echo 'Use `pnpm test:unit` (or `pnpm test:unit -- <path>`) instead of npx vitest.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(pnpx vitest *)",
"command": "echo 'Use `pnpm test:unit` (or `pnpm test:unit -- <path>`) instead of pnpx vitest.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(npx eslint *)",
"command": "echo 'Use `pnpm lint` or `pnpm lint:fix` instead of npx eslint.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(pnpx eslint *)",
"command": "echo 'Use `pnpm lint` or `pnpm lint:fix` instead of pnpx eslint.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(npx prettier *)",
"command": "echo 'This project uses oxfmt, not prettier. Use `pnpm format` or `pnpm format:check`.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(pnpx prettier *)",
"command": "echo 'This project uses oxfmt, not prettier. Use `pnpm format` or `pnpm format:check`.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(npx oxlint *)",
"command": "echo 'Use `pnpm oxlint` instead of npx oxlint.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(npx stylelint *)",
"command": "echo 'Use `pnpm stylelint` instead of npx stylelint.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(npx knip *)",
"command": "echo 'Use `pnpm knip` instead of npx knip.' >&2 && exit 2"
},
{
"type": "command",
"if": "Bash(pnpx knip *)",
"command": "echo 'Use `pnpm knip` instead of pnpx knip.' >&2 && exit 2"
}
]
}
]
}
}

View File

@@ -1,14 +1,23 @@
# Automated bi-weekly workflow to bump ComfyUI frontend RC releases
name: 'Release: Bi-weekly ComfyUI'
# Release workflow for ComfyUI frontend: version bump → PyPI publish → ComfyUI PR.
# Runs on a bi-weekly schedule for minor releases, or manually for patch/hotfix releases.
name: 'Release: ComfyUI'
on:
# Schedule for Monday at 12:00 PM PST (20:00 UTC)
# Bi-weekly schedule: Monday at 20:00 UTC
schedule:
- cron: '0 20 * * 1'
# Allow manual triggering (bypasses bi-weekly check)
# Manual trigger for both on-demand minor and patch/hotfix releases
workflow_dispatch:
inputs:
release_type:
description: 'minor = next minor version (bi-weekly cadence), patch = hotfix for current production version'
required: true
default: 'minor'
type: choice
options:
- minor
- patch
comfyui_fork:
description: 'ComfyUI fork to use for PR (e.g., Comfy-Org/ComfyUI)'
required: false
@@ -41,10 +50,11 @@ jobs:
- name: Summary
run: |
echo "## Bi-weekly Check" >> $GITHUB_STEP_SUMMARY
echo "## Release Check" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- Is release week: ${{ steps.check.outputs.is_release_week }}" >> $GITHUB_STEP_SUMMARY
echo "- Manual trigger: ${{ github.event_name == 'workflow_dispatch' }}" >> $GITHUB_STEP_SUMMARY
echo "- Release type: ${{ inputs.release_type || 'minor (scheduled)' }}" >> $GITHUB_STEP_SUMMARY
resolve-version:
needs: check-release-week
@@ -76,6 +86,8 @@ jobs:
- name: Install pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0
with:
package_json_file: frontend/package.json
- name: Setup Node.js
uses: actions/setup-node@v6
@@ -89,6 +101,8 @@ jobs:
- name: Resolve release information
id: resolve
working-directory: frontend
env:
RELEASE_TYPE: ${{ inputs.release_type || 'minor' }}
run: |
set -euo pipefail

View File

@@ -0,0 +1,68 @@
{
"last_node_id": 4,
"last_link_id": 2,
"nodes": [
{
"id": 1,
"type": "E2E_OldSampler",
"pos": [100, 100],
"size": [300, 262],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{ "name": "model", "type": "MODEL", "link": null },
{ "name": "positive", "type": "CONDITIONING", "link": null },
{ "name": "negative", "type": "CONDITIONING", "link": null },
{ "name": "latent_image", "type": "LATENT", "link": null }
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [],
"slot_index": 0
}
],
"properties": { "Node name for S&R": "E2E_OldSampler" },
"widgets_values": [42, 20, 7, "euler", "normal"]
},
{
"id": 2,
"type": "E2E_OldUpscaler",
"pos": [500, 100],
"size": [300, 80],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [{ "name": "image", "type": "IMAGE", "link": null }],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [2],
"slot_index": 0
}
],
"properties": { "Node name for S&R": "E2E_OldUpscaler" },
"widgets_values": ["lanczos", 1.5]
},
{
"id": 3,
"type": "SaveImage",
"pos": [900, 100],
"size": [300, 80],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [{ "name": "images", "type": "IMAGE", "link": 2 }],
"properties": { "Node name for S&R": "SaveImage" },
"widgets_values": ["ComfyUI"]
}
],
"links": [[2, 2, 0, 3, 0, "IMAGE"]],
"groups": [],
"config": {},
"extra": { "ds": { "scale": 1, "offset": [0, 0] } },
"version": 0.4
}

View File

@@ -0,0 +1,59 @@
{
"last_node_id": 3,
"last_link_id": 1,
"nodes": [
{
"id": 1,
"type": "E2E_OldSampler",
"pos": [100, 100],
"size": [300, 262],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{ "name": "model", "type": "MODEL", "link": null },
{ "name": "positive", "type": "CONDITIONING", "link": null },
{ "name": "negative", "type": "CONDITIONING", "link": null },
{ "name": "latent_image", "type": "LATENT", "link": null }
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [1],
"slot_index": 0
}
],
"properties": { "Node name for S&R": "E2E_OldSampler" },
"widgets_values": [42, 20, 7, "euler", "normal"]
},
{
"id": 2,
"type": "VAEDecode",
"pos": [500, 100],
"size": [210, 46],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{ "name": "samples", "type": "LATENT", "link": 1 },
{ "name": "vae", "type": "VAE", "link": null }
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [],
"slot_index": 0
}
],
"properties": { "Node name for S&R": "VAEDecode" },
"widgets_values": []
}
],
"links": [[1, 1, 0, 2, 0, "LATENT"]],
"groups": [],
"config": {},
"extra": { "ds": { "scale": 1, "offset": [0, 0] } },
"version": 0.4
}

View File

@@ -0,0 +1,47 @@
import type { NodeReplacementResponse } from '@/platform/nodeReplacement/types'
/**
* Mock node replacement mappings for e2e tests.
*
* Maps fake "missing" node types (E2E_OldSampler, E2E_OldUpscaler) to real
* core node types that are always available in the test server.
*/
export const mockNodeReplacements: NodeReplacementResponse = {
E2E_OldSampler: [
{
new_node_id: 'KSampler',
old_node_id: 'E2E_OldSampler',
old_widget_ids: ['seed', 'steps', 'cfg', 'sampler_name', 'scheduler'],
input_mapping: [
{ new_id: 'model', old_id: 'model' },
{ new_id: 'positive', old_id: 'positive' },
{ new_id: 'negative', old_id: 'negative' },
{ new_id: 'latent_image', old_id: 'latent_image' },
{ new_id: 'seed', old_id: 'seed' },
{ new_id: 'steps', old_id: 'steps' },
{ new_id: 'cfg', old_id: 'cfg' },
{ new_id: 'sampler_name', old_id: 'sampler_name' },
{ new_id: 'scheduler', old_id: 'scheduler' }
],
output_mapping: [{ new_idx: 0, old_idx: 0 }]
}
],
E2E_OldUpscaler: [
{
new_node_id: 'ImageScaleBy',
old_node_id: 'E2E_OldUpscaler',
old_widget_ids: ['upscale_method', 'scale_by'],
input_mapping: [
{ new_id: 'image', old_id: 'image' },
{ new_id: 'upscale_method', old_id: 'upscale_method' },
{ new_id: 'scale_by', old_id: 'scale_by' }
],
output_mapping: [{ new_idx: 0, old_idx: 0 }]
}
]
}
/** Subset containing only the E2E_OldSampler replacement. */
export const mockNodeReplacementsSingle: NodeReplacementResponse = {
E2E_OldSampler: mockNodeReplacements.E2E_OldSampler
}

View File

@@ -1,5 +1,7 @@
import { readFileSync } from 'fs'
import { test } from '@playwright/test'
import type { AppMode } from '@/composables/useAppMode'
import type {
ComfyApiWorkflow,
@@ -73,6 +75,9 @@ export class WorkflowHelper {
assetPath(`${workflowName}.json`)
)
await this.comfyPage.nextFrame()
if (test.info().tags.includes('@vue-nodes')) {
await this.comfyPage.vueNodes.waitForNodes()
}
}
async deleteWorkflow(

View File

@@ -61,6 +61,7 @@ export const TestIds = {
missingModelDownload: 'missing-model-download',
missingModelImportUnsupported: 'missing-model-import-unsupported',
missingMediaGroup: 'error-group-missing-media',
swapNodesGroup: 'error-group-swap-nodes',
missingMediaRow: 'missing-media-row',
missingMediaUploadDropzone: 'missing-media-upload-dropzone',
missingMediaLibrarySelect: 'missing-media-library-select',

View File

@@ -5,10 +5,6 @@ import type { WorkspaceStore } from '@e2e/types/globals'
test.describe('Browser tab title', { tag: '@smoke' }, () => {
test.describe('Beta Menu', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
})
test('Can display workflow name', async ({ comfyPage }) => {
const workflowName = await comfyPage.page.evaluate(async () => {
return (window.app!.extensionManager as WorkspaceStore).workflow

View File

@@ -12,7 +12,6 @@ test.describe(
() => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.workflow.loadWorkflow('default')
await comfyPage.vueNodes.waitForNodes()
})
test.afterEach(async ({ comfyPage }) => {

View File

@@ -5,7 +5,6 @@ import {
test.describe('Queue Clear History Dialog', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setup()
await comfyPage.queuePanel.overlayToggle.click()
})

View File

@@ -4,10 +4,6 @@ import {
} from '@e2e/fixtures/ComfyPage'
test.describe('Focus Mode', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setup()
})
test('Focus mode hides UI chrome', async ({ comfyPage }) => {
await expect(comfyPage.menu.sideToolbar).toBeVisible()

View File

@@ -7,7 +7,6 @@ import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
test.describe('Image Compare', { tag: ['@widget', '@vue-nodes'] }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.workflow.loadWorkflow('widgets/image_compare_widget')
await comfyPage.vueNodes.waitForNodes()
})
function createTestImageDataUrl(label: string, color: string): string {

View File

@@ -7,8 +7,6 @@ import {
test.describe('Job History Actions', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setup()
// Expand the queue overlay so the JobHistoryActionsMenu is visible
await comfyPage.page.getByTestId('queue-overlay-toggle').click()
})

View File

@@ -4,10 +4,6 @@ import {
} from '@e2e/fixtures/ComfyPage'
test.describe('Linear Mode', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setup()
})
test('Displays linear controls when app mode active', async ({
comfyPage
}) => {

View File

@@ -6,7 +6,6 @@ import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
test.describe('Mask Editor', { tag: '@vue-nodes' }, () => {
async function loadImageOnNode(comfyPage: ComfyPage) {
await comfyPage.workflow.loadWorkflow('widgets/load_image_widget')
await comfyPage.vueNodes.waitForNodes()
const loadImageNode = (
await comfyPage.nodeOps.getNodeRefsByType('LoadImage')

View File

@@ -87,7 +87,6 @@ async function setLocaleAndWaitForWorkflowReload(
test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setup()
await comfyPage.settings.setSetting('Comfy.NodeLibrary.NewDesign', false)
})

View File

@@ -0,0 +1,186 @@
import type { Page } from '@playwright/test'
import {
comfyPageFixture as test,
comfyExpect as expect
} from '@e2e/fixtures/ComfyPage'
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
import {
mockNodeReplacements,
mockNodeReplacementsSingle
} from '@e2e/fixtures/data/nodeReplacements'
import type { NodeReplacementResponse } from '@/platform/nodeReplacement/types'
import { TestIds } from '@e2e/fixtures/selectors'
import { loadWorkflowAndOpenErrorsTab } from '@e2e/tests/propertiesPanel/ErrorsTabHelper'
/**
* Mock the `/api/node_replacements` endpoint and enable the feature flag +
* settings required for node replacement to function.
*/
async function setupNodeReplacement(
comfyPage: ComfyPage,
replacements: NodeReplacementResponse
) {
await comfyPage.page.route('**/api/node_replacements', (route) =>
route.fulfill({ json: replacements })
)
await comfyPage.settings.setSetting(
'Comfy.RightSidePanel.ShowErrorsTab',
true
)
await comfyPage.settings.setSetting('Comfy.NodeReplacement.Enabled', true)
// Enable the server feature flag so the store fetches replacements.
await comfyPage.page.evaluate(() => {
const flags = window.app!.api.serverFeatureFlags
flags.value = { ...flags.value, node_replacements: true }
})
}
function getSwapNodesGroup(page: Page) {
return page.getByTestId(TestIds.dialogs.swapNodesGroup)
}
test.describe('Node replacement', { tag: ['@node', '@ui'] }, () => {
test.describe('Single replacement', () => {
test.beforeEach(async ({ comfyPage }) => {
await setupNodeReplacement(comfyPage, mockNodeReplacementsSingle)
await loadWorkflowAndOpenErrorsTab(
comfyPage,
'missing/node_replacement_simple'
)
})
test('Swap Nodes group appears in errors tab for replaceable nodes', async ({
comfyPage
}) => {
const swapGroup = getSwapNodesGroup(comfyPage.page)
await expect(swapGroup).toBeVisible()
await expect(swapGroup).toContainText('E2E_OldSampler')
await expect(
swapGroup.getByRole('button', { name: 'Replace All', exact: true })
).toBeVisible()
})
test('Replace Node replaces a single group in-place', async ({
comfyPage
}) => {
const swapGroup = getSwapNodesGroup(comfyPage.page)
await swapGroup.getByRole('button', { name: /replace node/i }).click()
// Swap group should disappear after replacement
await expect(swapGroup).toBeHidden()
// Verify the replacement was applied correctly via the exported workflow
const workflow = await comfyPage.workflow.getExportedWorkflow()
// Node count stays the same (in-place replacement)
expect(
workflow.nodes,
'Node count should be unchanged after in-place replacement'
).toHaveLength(2)
// The old type should be gone and replaced by KSampler
const nodeTypes = workflow.nodes.map((n) => n.type)
expect(nodeTypes).not.toContain('E2E_OldSampler')
expect(nodeTypes).toContain('KSampler')
// The replaced node should keep the same id
const ksampler = workflow.nodes.find((n) => n.type === 'KSampler')
expect(ksampler?.id).toBe(1)
// Output connection from old node → VAEDecode should be preserved
// Link tuple format: [link_id, source_node, source_slot, target_node, target_slot, type]
const link = workflow.links?.find((l) => l[1] === 1 && l[3] === 2)
expect(
link,
'Output link from replaced node to VAEDecode should be preserved'
).toBeDefined()
})
test('Widget values are preserved after replacement', async ({
comfyPage
}) => {
await getSwapNodesGroup(comfyPage.page)
.getByRole('button', { name: /replace node/i })
.click()
const workflow = await comfyPage.workflow.getExportedWorkflow()
const ksampler = workflow.nodes.find((n) => n.type === 'KSampler')
// The original workflow had widgets_values: [42, 20, 7, "euler", "normal"]
// mapped to: seed=42, steps=20, cfg=7, sampler_name="euler", scheduler="normal"
expect(ksampler?.widgets_values).toBeDefined()
const widgetValues = ksampler!.widgets_values as unknown[]
expect(widgetValues).toContain(42)
expect(widgetValues).toContain(20)
})
test('Success toast is shown after replacement', async ({ comfyPage }) => {
await getSwapNodesGroup(comfyPage.page)
.getByRole('button', { name: /replace node/i })
.click()
await expect(comfyPage.visibleToasts.first()).toContainText(
/replaced|swapped/i
)
})
})
test.describe('Multi-type replacement', () => {
test.beforeEach(async ({ comfyPage }) => {
await setupNodeReplacement(comfyPage, mockNodeReplacements)
await loadWorkflowAndOpenErrorsTab(
comfyPage,
'missing/node_replacement_multi'
)
})
test('Replace All replaces all groups across multiple types', async ({
comfyPage
}) => {
const swapGroup = getSwapNodesGroup(comfyPage.page)
await expect(swapGroup).toBeVisible()
// Both types should appear
await expect(swapGroup).toContainText('E2E_OldSampler')
await expect(swapGroup).toContainText('E2E_OldUpscaler')
// Click "Replace All"
await swapGroup
.getByRole('button', { name: 'Replace All', exact: true })
.click()
// Swap group should disappear
await expect(swapGroup).toBeHidden()
// Verify both old types are gone
const workflow = await comfyPage.workflow.getExportedWorkflow()
const nodeTypes = workflow.nodes.map((n) => n.type)
expect(nodeTypes).not.toContain('E2E_OldSampler')
expect(nodeTypes).not.toContain('E2E_OldUpscaler')
expect(nodeTypes).toContain('KSampler')
expect(nodeTypes).toContain('ImageScaleBy')
})
test('Output connections are preserved across replacement with output mapping', async ({
comfyPage
}) => {
await getSwapNodesGroup(comfyPage.page)
.getByRole('button', { name: 'Replace All', exact: true })
.click()
const workflow = await comfyPage.workflow.getExportedWorkflow()
// E2E_OldUpscaler (id=2) had an output link to SaveImage (id=3).
// After replacement to ImageScaleBy, that link should be preserved.
// Link tuple format: [link_id, source_node, source_slot, target_node, target_slot, type]
const linkToSave = workflow.links?.find((l) => l[1] === 2 && l[3] === 3)
expect(
linkToSave,
'Output link from replaced upscaler to SaveImage should be preserved'
).toBeDefined()
})
})
})

View File

@@ -13,7 +13,6 @@ test.describe('Painter', { tag: ['@widget', '@vue-nodes'] }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => window.app?.graph?.clear())
await comfyPage.workflow.loadWorkflow('widgets/painter_widget')
await comfyPage.vueNodes.waitForNodes()
})
test(

View File

@@ -4,10 +4,6 @@ import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
import { getMiddlePoint } from '@e2e/fixtures/utils/litegraphUtils'
test.describe('Reroute Node', { tag: ['@screenshot', '@node'] }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
})
test.afterEach(async ({ comfyPage }) => {
await comfyPage.workflow.setupWorkflowsDirectory({})
})

View File

@@ -4,15 +4,10 @@ import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
test.describe('MediaLightbox', { tag: ['@slow', '@vue-nodes'] }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setup()
})
async function runAndOpenGallery(comfyPage: ComfyPage) {
await comfyPage.workflow.loadWorkflow(
'widgets/save_image_and_animated_webp'
)
await comfyPage.vueNodes.waitForNodes()
await comfyPage.runButton.click()
// Wait for SaveImage node to produce output

View File

@@ -12,7 +12,6 @@ test.describe(
await comfyPage.workflow.loadWorkflow(
'widgets/save_image_and_animated_webp'
)
await comfyPage.vueNodes.waitForNodes()
await comfyPage.runButton.click()

View File

@@ -4,10 +4,6 @@ import {
} from '@e2e/fixtures/ComfyPage'
test.describe('Toast Notifications', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setup()
})
async function triggerErrorToast(comfyPage: {
page: { evaluate: (fn: () => void) => Promise<void> }
nextFrame: () => Promise<void>

View File

@@ -128,7 +128,6 @@ test.describe('Vue Node Groups', { tag: ['@screenshot', '@vue-nodes'] }, () => {
})
test('should allow fitting group to contents', async ({ comfyPage }) => {
await comfyPage.setup()
await comfyPage.workflow.loadWorkflow('groups/oversized_group')
await comfyPage.keyboard.selectAll()
await comfyPage.command.executeCommand('Comfy.Graph.FitGroupToContents')

View File

@@ -106,7 +106,6 @@ test.describe(
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default')
await comfyPage.workflow.loadWorkflow('vueNodes/simple-triple')
await comfyPage.vueNodes.waitForNodes()
await fitToViewInstant(comfyPage)
})

View File

@@ -12,7 +12,6 @@ test.describe(
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.workflow.loadWorkflow('vueNodes/simple-triple')
await comfyPage.vueNodes.waitForNodes()
await fitToViewInstant(comfyPage)
})

View File

@@ -10,7 +10,6 @@ import {
test.describe('Vue Nodes Image Preview', { tag: '@vue-nodes' }, () => {
async function loadImageOnNode(comfyPage: ComfyPage) {
await comfyPage.workflow.loadWorkflow('widgets/load_image_widget')
await comfyPage.vueNodes.waitForNodes()
const loadImageNode = (
await comfyPage.nodeOps.getNodeRefsByType('LoadImage')
@@ -69,7 +68,6 @@ test.describe('Vue Nodes Image Preview', { tag: '@vue-nodes' }, () => {
await comfyPage.workflow.loadWorkflow(
'subgraphs/subgraph-with-multiple-promoted-previews'
)
await comfyPage.vueNodes.waitForNodes()
const firstSubgraphNode = comfyPage.vueNodes.getNodeLocator('7')
const secondSubgraphNode = comfyPage.vueNodes.getNodeLocator('8')

View File

@@ -2,24 +2,13 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe(
'Vue Nodes - Delete Key Interaction',
{ tag: '@vue-nodes' },
() => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', false)
await comfyPage.setup()
})
test('Can select all and delete Vue nodes with Delete key', async ({
comfyPage
}) => {
await comfyPage.vueNodes.waitForNodes()
// Get initial Vue node count
await expect
.poll(() => comfyPage.vueNodes.getNodeCount())
@@ -44,8 +33,6 @@ test.describe(
test('Can select specific Vue node and delete it', async ({
comfyPage
}) => {
await comfyPage.vueNodes.waitForNodes()
// Get initial Vue node count
await expect
.poll(() => comfyPage.vueNodes.getNodeCount())
@@ -71,8 +58,6 @@ test.describe(
test('Can select and delete Vue node with Backspace key', async ({
comfyPage
}) => {
await comfyPage.vueNodes.waitForNodes()
const initialNodeCount = await comfyPage.vueNodes.getNodeCount()
// Select first Vue node
@@ -114,8 +99,6 @@ test.describe(
test('Delete key does not delete node when nothing is selected', async ({
comfyPage
}) => {
await comfyPage.vueNodes.waitForNodes()
// Ensure no Vue nodes are selected
await comfyPage.vueNodes.clearSelection()
await expect(comfyPage.vueNodes.selectedNodes).toHaveCount(0)
@@ -132,7 +115,6 @@ test.describe(
test('Can multi-select with Ctrl+click and delete multiple Vue nodes', async ({
comfyPage
}) => {
await comfyPage.vueNodes.waitForNodes()
const initialNodeCount = await comfyPage.vueNodes.getNodeCount()
// Multi-select first two Vue nodes using Ctrl+click

View File

@@ -41,7 +41,6 @@ test.describe(
test('should load node colors from workflow', async ({ comfyPage }) => {
await comfyPage.workflow.loadWorkflow('nodes/every_node_color')
await comfyPage.vueNodes.waitForNodes()
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-node-custom-colors-dark-all-colors.png'
)
@@ -52,7 +51,6 @@ test.describe(
}) => {
await comfyPage.settings.setSetting('Comfy.ColorPalette', 'light')
await comfyPage.workflow.loadWorkflow('nodes/every_node_color')
await comfyPage.vueNodes.waitForNodes()
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-node-custom-colors-light-all-colors.png'
)

View File

@@ -9,7 +9,6 @@ test.describe('Vue Node Error', { tag: '@vue-nodes' }, () => {
test('should display error state when node is missing (node from workflow is not installed)', async ({
comfyPage
}) => {
await comfyPage.setup()
await comfyPage.workflow.loadWorkflow('missing/missing_nodes')
// Expect error state on missing unknown node
@@ -23,7 +22,6 @@ test.describe('Vue Node Error', { tag: '@vue-nodes' }, () => {
test('should display error state when node causes execution error', async ({
comfyPage
}) => {
await comfyPage.setup()
await comfyPage.workflow.loadWorkflow('nodes/execution_error')
await comfyPage.runButton.click()

View File

@@ -7,7 +7,6 @@ test.describe('Vue Reroute Node Size', { tag: '@vue-nodes' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.Minimap.Visible', false)
await comfyPage.workflow.loadWorkflow('links/single_connected_reroute_node')
await comfyPage.vueNodes.waitForNodes()
})
test(

View File

@@ -10,7 +10,6 @@ test.describe('Image Crop', { tag: ['@widget', '@vue-nodes'] }, () => {
test.describe('without source image', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.workflow.loadWorkflow('widgets/image_crop_widget')
await comfyPage.vueNodes.waitForNodes()
})
test(
@@ -72,7 +71,6 @@ test.describe('Image Crop', { tag: ['@widget', '@vue-nodes'] }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.workflow.loadWorkflow('widgets/image_crop_with_source')
await comfyPage.vueNodes.waitForNodes()
await comfyPage.runButton.click()
await expect(
comfyPage.vueNodes.getNodeLocator('2').locator('img')

View File

@@ -8,7 +8,6 @@ test.describe('Vue Integer Widget', { tag: '@vue-nodes' }, () => {
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow('vueNodes/linked-int-widget')
await comfyPage.vueNodes.waitForNodes()
const seedWidget = comfyPage.vueNodes
.getWidgetByName('KSampler', 'seed')

View File

@@ -6,9 +6,7 @@ import { TestIds } from '@e2e/fixtures/selectors'
test.describe('Vue Upload Widgets', { tag: '@vue-nodes' }, () => {
test('should hide canvas-only upload buttons', async ({ comfyPage }) => {
await comfyPage.setup()
await comfyPage.workflow.loadWorkflow('widgets/all_load_widgets')
await comfyPage.vueNodes.waitForNodes()
await expect(
comfyPage.page.getByText('choose file to upload', { exact: true })

View File

@@ -1,6 +1,6 @@
{
"name": "@comfyorg/comfyui-frontend",
"version": "1.44.3",
"version": "1.44.4",
"private": true,
"description": "Official front-end implementation of ComfyUI",
"homepage": "https://comfy.org",

View File

@@ -137,35 +137,71 @@ function resolveRelease(
// Fetch all branches
exec('git fetch origin', frontendRepoPath)
// Try next minor first, fall back to current minor if not available
let targetMinor = currentMinor + 1
let targetBranch = `core/1.${targetMinor}`
// Determine target branch based on release type:
// 'patch' → target current minor (hotfix for production version)
// 'minor' → try next minor, fall back to current minor (bi-weekly cadence)
const releaseTypeInput =
process.env.RELEASE_TYPE?.trim().toLowerCase() || 'minor'
if (releaseTypeInput !== 'minor' && releaseTypeInput !== 'patch') {
console.error(
`Invalid RELEASE_TYPE: "${releaseTypeInput}". Expected "minor" or "patch"`
)
return null
}
const releaseType: 'minor' | 'patch' = releaseTypeInput
let targetMinor: number
let targetBranch: string
const nextMinorExists = exec(
`git rev-parse --verify origin/${targetBranch}`,
frontendRepoPath
)
if (!nextMinorExists) {
// Fall back to current minor for patch releases
if (releaseType === 'patch') {
targetMinor = currentMinor
targetBranch = `core/1.${targetMinor}`
const currentMinorExists = exec(
const branchExists = exec(
`git rev-parse --verify origin/${targetBranch}`,
frontendRepoPath
)
if (!currentMinorExists) {
if (!branchExists) {
console.error(
`Neither core/1.${currentMinor + 1} nor core/1.${currentMinor} branches exist in frontend repo`
`Patch release requested but branch ${targetBranch} does not exist`
)
return null
}
console.error(
`Next minor branch core/1.${currentMinor + 1} not found, falling back to core/1.${currentMinor} for patch release`
`Patch release: targeting current production branch ${targetBranch}`
)
} else {
// Try next minor first, fall back to current minor if not available
targetMinor = currentMinor + 1
targetBranch = `core/1.${targetMinor}`
const nextMinorExists = exec(
`git rev-parse --verify origin/${targetBranch}`,
frontendRepoPath
)
if (!nextMinorExists) {
// Fall back to current minor for minor release
targetMinor = currentMinor
targetBranch = `core/1.${targetMinor}`
const currentMinorExists = exec(
`git rev-parse --verify origin/${targetBranch}`,
frontendRepoPath
)
if (!currentMinorExists) {
console.error(
`Neither core/1.${currentMinor + 1} nor core/1.${currentMinor} branches exist in frontend repo`
)
return null
}
console.error(
`Next minor branch core/1.${currentMinor + 1} not found, falling back to core/1.${currentMinor} for minor release`
)
}
}
// Get latest patch tag for target minor
@@ -264,7 +300,7 @@ if (!releaseInfo) {
}
// Output as JSON for GitHub Actions
// oxlint-disable-next-line no-console -- stdout is captured by the workflow
console.log(JSON.stringify(releaseInfo, null, 2))
export { resolveRelease }

View File

@@ -1642,9 +1642,18 @@
"exportRecording": "تصدير التسجيل",
"exportingModel": "جارٍ تصدير النموذج...",
"fov": "مجال الرؤية (FOV)",
"hdri": {
"changeFile": "تغيير HDRI",
"intensity": "الشدة",
"label": "بيئة HDRI",
"removeFile": "إزالة HDRI",
"showAsBackground": "عرض كخلفية",
"uploadFile": "رفع HDRI (.hdr, .exr)"
},
"light": "الإضاءة",
"lightIntensity": "شدة الإضاءة",
"loadingBackgroundImage": "جارٍ تحميل صورة الخلفية",
"loadingHDRI": "جارٍ تحميل HDRI...",
"loadingModel": "جارٍ تحميل النموذج ثلاثي الأبعاد...",
"materialMode": "وضع المادة",
"materialModes": {
@@ -3431,6 +3440,7 @@
"failedToInitiateCreditPurchase": "فشل في بدء شراء الرصيد: {error}",
"failedToInitiateSubscription": "فشل في بدء الاشتراك: {error}",
"failedToLoadBackgroundImage": "فشل في تحميل صورة الخلفية",
"failedToLoadHDRI": "فشل في تحميل ملف HDRI",
"failedToLoadModel": "فشل في تحميل النموذج ثلاثي الأبعاد",
"failedToPurchaseCredits": "فشل في شراء الرصيد: {error}",
"failedToQueue": "فشل في الإضافة إلى قائمة الانتظار",
@@ -3466,6 +3476,7 @@
"pleaseSelectOutputNodes": "يرجى اختيار عقد الإخراج",
"unableToGetModelFilePath": "غير قادر على الحصول على مسار ملف النموذج",
"unauthorizedDomain": "النطاق الخاص بك {domain} غير مخول لاستخدام هذه الخدمة. يرجى الاتصال بـ {email} لإضافة النطاق إلى القائمة البيضاء.",
"unsupportedHDRIFormat": "تنسيق الملف غير مدعوم. يرجى رفع ملف .hdr أو .exr.",
"updateRequested": "تم طلب التحديث",
"useApiKeyTip": "نصيحة: لا يمكنك الدخول عبر تسجيل الدخول العادي؟ استخدم خيار مفتاح API الخاص بـ Comfy.",
"userNotAuthenticated": "المستخدم غير مصدق"

View File

@@ -1642,9 +1642,18 @@
"exportRecording": "Exportar grabación",
"exportingModel": "Exportando modelo...",
"fov": "FOV",
"hdri": {
"changeFile": "Cambiar HDRI",
"intensity": "Intensidad",
"label": "Entorno HDRI",
"removeFile": "Eliminar HDRI",
"showAsBackground": "Mostrar como fondo",
"uploadFile": "Subir HDRI (.hdr, .exr)"
},
"light": "Luz",
"lightIntensity": "Intensidad de luz",
"loadingBackgroundImage": "Cargando imagen de fondo",
"loadingHDRI": "Cargando HDRI...",
"loadingModel": "Cargando modelo 3D...",
"materialMode": "Modo de material",
"materialModes": {
@@ -3431,6 +3440,7 @@
"failedToInitiateCreditPurchase": "No se pudo iniciar la compra de créditos: {error}",
"failedToInitiateSubscription": "Error al iniciar la suscripción: {error}",
"failedToLoadBackgroundImage": "Error al cargar la imagen de fondo",
"failedToLoadHDRI": "No se pudo cargar el archivo HDRI",
"failedToLoadModel": "Error al cargar el modelo 3D",
"failedToPurchaseCredits": "No se pudo comprar créditos: {error}",
"failedToQueue": "Error al encolar",
@@ -3466,6 +3476,7 @@
"pleaseSelectOutputNodes": "Por favor, selecciona los nodos de salida",
"unableToGetModelFilePath": "No se puede obtener la ruta del archivo del modelo",
"unauthorizedDomain": "Tu dominio {domain} no está autorizado para usar este servicio. Por favor, contacta a {email} para agregar tu dominio a la lista blanca.",
"unsupportedHDRIFormat": "Formato de archivo no compatible. Por favor, sube un archivo .hdr o .exr.",
"updateRequested": "Actualización solicitada",
"useApiKeyTip": "Consejo: ¿No puedes acceder al inicio de sesión normal? Usa la opción de clave API de Comfy.",
"userNotAuthenticated": "Usuario no autenticado"

View File

@@ -1642,9 +1642,18 @@
"exportRecording": "خروجی گرفتن ضبط",
"exportingModel": "در حال خروجی گرفتن مدل...",
"fov": "زاویه دید (FOV)",
"hdri": {
"changeFile": "تغییر HDRI",
"intensity": "شدت",
"label": "محیط HDRI",
"removeFile": "حذف HDRI",
"showAsBackground": "نمایش به عنوان پس‌زمینه",
"uploadFile": "بارگذاری HDRI (.hdr، .exr)"
},
"light": "نور",
"lightIntensity": "شدت نور",
"loadingBackgroundImage": "در حال بارگذاری تصویر پس‌زمینه",
"loadingHDRI": "در حال بارگذاری HDRI...",
"loadingModel": "در حال بارگذاری مدل سه‌بعدی...",
"materialMode": "حالت متریال",
"materialModes": {
@@ -3443,6 +3452,7 @@
"failedToInitiateCreditPurchase": "آغاز خرید اعتبار انجام نشد: {error}",
"failedToInitiateSubscription": "آغاز اشتراک انجام نشد: {error}",
"failedToLoadBackgroundImage": "بارگذاری تصویر پس‌زمینه انجام نشد",
"failedToLoadHDRI": "بارگذاری فایل HDRI ناموفق بود",
"failedToLoadModel": "بارگذاری مدل سه‌بعدی انجام نشد",
"failedToPurchaseCredits": "خرید اعتبار انجام نشد: {error}",
"failedToQueue": "صف‌بندی انجام نشد",
@@ -3478,6 +3488,7 @@
"pleaseSelectOutputNodes": "لطفاً nodeهای خروجی را انتخاب کنید",
"unableToGetModelFilePath": "امکان دریافت مسیر فایل مدل وجود ندارد",
"unauthorizedDomain": "دامنه شما ({domain}) مجاز به استفاده از این سرویس نیست. لطفاً برای افزودن دامنه خود به لیست سفید با {email} تماس بگیرید.",
"unsupportedHDRIFormat": "فرمت فایل پشتیبانی نمی‌شود. لطفاً یک فایل .hdr یا .exr بارگذاری کنید.",
"updateRequested": "درخواست به‌روزرسانی ثبت شد",
"useApiKeyTip": "نکته: به ورود عادی دسترسی ندارید؟ از گزینه Comfy API Key استفاده کنید.",
"userNotAuthenticated": "کاربر احراز هویت نشده است"

View File

@@ -1642,9 +1642,18 @@
"exportRecording": "Exporter l'enregistrement",
"exportingModel": "Exportation du modèle en cours...",
"fov": "FOV",
"hdri": {
"changeFile": "Changer l'HDRI",
"intensity": "Intensité",
"label": "Environnement HDRI",
"removeFile": "Supprimer l'HDRI",
"showAsBackground": "Afficher comme arrière-plan",
"uploadFile": "Télécharger un HDRI (.hdr, .exr)"
},
"light": "Lumière",
"lightIntensity": "Intensité de la lumière",
"loadingBackgroundImage": "Chargement de limage darrière-plan",
"loadingHDRI": "Chargement de l'HDRI...",
"loadingModel": "Chargement du modèle 3D...",
"materialMode": "Mode Matériel",
"materialModes": {
@@ -3431,6 +3440,7 @@
"failedToInitiateCreditPurchase": "Échec de l'initiation de l'achat de crédits : {error}",
"failedToInitiateSubscription": "Échec de l'initialisation de l'abonnement : {error}",
"failedToLoadBackgroundImage": "Échec du chargement de l'image d'arrière-plan",
"failedToLoadHDRI": "Échec du chargement du fichier HDRI",
"failedToLoadModel": "Échec du chargement du modèle 3D",
"failedToPurchaseCredits": "Échec de l'achat de crédits : {error}",
"failedToQueue": "Échec de la mise en file d'attente",
@@ -3466,6 +3476,7 @@
"pleaseSelectOutputNodes": "Veuillez sélectionner les nœuds de sortie",
"unableToGetModelFilePath": "Impossible d'obtenir le chemin du fichier modèle",
"unauthorizedDomain": "Votre domaine {domain} n'est pas autorisé à utiliser ce service. Veuillez contacter {email} pour ajouter votre domaine à la liste blanche.",
"unsupportedHDRIFormat": "Format de fichier non pris en charge. Veuillez télécharger un fichier .hdr ou .exr.",
"updateRequested": "Mise à jour demandée",
"useApiKeyTip": "Astuce : Vous ne pouvez pas accéder à la connexion normale ? Utilisez loption Clé API Comfy.",
"userNotAuthenticated": "Utilisateur non authentifié"

View File

@@ -1642,9 +1642,18 @@
"exportRecording": "録画をエクスポート",
"exportingModel": "モデルをエクスポート中...",
"fov": "FOV",
"hdri": {
"changeFile": "HDRIを変更",
"intensity": "強度",
"label": "HDRI環境",
"removeFile": "HDRIを削除",
"showAsBackground": "背景として表示",
"uploadFile": "HDRIをアップロード.hdr、.exr"
},
"light": "ライト",
"lightIntensity": "光の強度",
"loadingBackgroundImage": "背景画像を読み込んでいます",
"loadingHDRI": "HDRIを読み込み中...",
"loadingModel": "3Dモデルを読み込んでいます...",
"materialMode": "マテリアルモード",
"materialModes": {
@@ -3431,6 +3440,7 @@
"failedToInitiateCreditPurchase": "クレジット購入の開始に失敗しました: {error}",
"failedToInitiateSubscription": "サブスクリプションの開始に失敗しました: {error}",
"failedToLoadBackgroundImage": "背景画像の読み込みに失敗しました",
"failedToLoadHDRI": "HDRIファイルの読み込みに失敗しました",
"failedToLoadModel": "3Dモデルの読み込みに失敗しました",
"failedToPurchaseCredits": "クレジットの購入に失敗しました: {error}",
"failedToQueue": "キューに追加できませんでした",
@@ -3466,6 +3476,7 @@
"pleaseSelectOutputNodes": "出力ノードを選択してください",
"unableToGetModelFilePath": "モデルファイルのパスを取得できません",
"unauthorizedDomain": "あなたのドメイン {domain} はこのサービスを利用する権限がありません。ご利用のドメインをホワイトリストに追加するには、{email} までご連絡ください。",
"unsupportedHDRIFormat": "サポートされていないファイル形式です。.hdrまたは.exrファイルをアップロードしてください。",
"updateRequested": "更新が要求されました",
"useApiKeyTip": "ヒント通常のログインにアクセスできませんかComfy APIキーオプションを使用してください。",
"userNotAuthenticated": "ユーザーが認証されていません"

View File

@@ -1642,9 +1642,18 @@
"exportRecording": "녹화 내보내기",
"exportingModel": "모델 내보내기 중...",
"fov": "FOV",
"hdri": {
"changeFile": "HDRI 변경",
"intensity": "강도",
"label": "HDRI 환경",
"removeFile": "HDRI 제거",
"showAsBackground": "배경으로 표시",
"uploadFile": "HDRI 업로드 (.hdr, .exr)"
},
"light": "빛",
"lightIntensity": "조명 강도",
"loadingBackgroundImage": "배경 이미지 불러오는 중",
"loadingHDRI": "HDRI 불러오는 중...",
"loadingModel": "3D 모델 로딩 중...",
"materialMode": "재질 모드",
"materialModes": {
@@ -3431,6 +3440,7 @@
"failedToInitiateCreditPurchase": "크레딧 구매를 시작하지 못했습니다: {error}",
"failedToInitiateSubscription": "구독을 시작하지 못함: {error}",
"failedToLoadBackgroundImage": "배경 이미지를 로드하지 못함",
"failedToLoadHDRI": "HDRI 파일을 불러오지 못했습니다",
"failedToLoadModel": "3D 모델을 로드하지 못함",
"failedToPurchaseCredits": "크레딧 구매에 실패했습니다: {error}",
"failedToQueue": "대기열 추가 실패",
@@ -3466,6 +3476,7 @@
"pleaseSelectOutputNodes": "출력 노드를 선택해 주세요",
"unableToGetModelFilePath": "모델 파일 경로를 가져올 수 없습니다",
"unauthorizedDomain": "귀하의 도메인 {domain}은(는) 이 서비스를 사용할 수 있는 권한이 없습니다. 도메인을 허용 목록에 추가하려면 {email}로 문의해 주세요.",
"unsupportedHDRIFormat": "지원되지 않는 파일 형식입니다. .hdr 또는 .exr 파일을 업로드해 주세요.",
"updateRequested": "업데이트 요청됨",
"useApiKeyTip": "팁: 일반 로그인을 사용할 수 없나요? Comfy API Key 옵션을 사용하세요.",
"userNotAuthenticated": "사용자가 인증되지 않았습니다"

View File

@@ -1642,9 +1642,18 @@
"exportRecording": "Exportar Gravação",
"exportingModel": "Exportando modelo...",
"fov": "Campo de Visão (FOV)",
"hdri": {
"changeFile": "Alterar HDRI",
"intensity": "Intensidade",
"label": "Ambiente HDRI",
"removeFile": "Remover HDRI",
"showAsBackground": "Exibir como fundo",
"uploadFile": "Enviar HDRI (.hdr, .exr)"
},
"light": "Luz",
"lightIntensity": "Intensidade da Luz",
"loadingBackgroundImage": "Carregando Imagem de Fundo",
"loadingHDRI": "Carregando HDRI...",
"loadingModel": "Carregando Modelo 3D...",
"materialMode": "Modo de Material",
"materialModes": {
@@ -3443,6 +3452,7 @@
"failedToInitiateCreditPurchase": "Falha ao iniciar compra de créditos: {error}",
"failedToInitiateSubscription": "Falha ao iniciar assinatura: {error}",
"failedToLoadBackgroundImage": "Falha ao carregar imagem de fundo",
"failedToLoadHDRI": "Falha ao carregar o arquivo HDRI",
"failedToLoadModel": "Falha ao carregar modelo 3D",
"failedToPurchaseCredits": "Falha ao comprar créditos: {error}",
"failedToQueue": "Falha ao enfileirar",
@@ -3478,6 +3488,7 @@
"pleaseSelectOutputNodes": "Por favor, selecione os nós de saída",
"unableToGetModelFilePath": "Não foi possível obter o caminho do arquivo do modelo",
"unauthorizedDomain": "Seu domínio {domain} não está autorizado a usar este serviço. Por favor, entre em contato com {email} para adicionar seu domínio à lista de permissões.",
"unsupportedHDRIFormat": "Formato de arquivo não suportado. Por favor, envie um arquivo .hdr ou .exr.",
"updateRequested": "Atualização solicitada",
"useApiKeyTip": "Dica: Não consegue acessar o login normal? Use a opção Comfy API Key.",
"userNotAuthenticated": "Usuário não autenticado"

View File

@@ -1642,9 +1642,18 @@
"exportRecording": "Экспортировать запись",
"exportingModel": "Экспорт модели...",
"fov": "Угол обзора",
"hdri": {
"changeFile": "Сменить HDRI",
"intensity": "Интенсивность",
"label": "HDRI-окружение",
"removeFile": "Удалить HDRI",
"showAsBackground": "Показать как фон",
"uploadFile": "Загрузить HDRI (.hdr, .exr)"
},
"light": "Свет",
"lightIntensity": "Интенсивность света",
"loadingBackgroundImage": "Загрузка фонового изображения",
"loadingHDRI": "Загрузка HDRI...",
"loadingModel": "Загрузка 3D модели...",
"materialMode": "Режим Материала",
"materialModes": {
@@ -3431,6 +3440,7 @@
"failedToInitiateCreditPurchase": "Не удалось начать покупку кредитов: {error}",
"failedToInitiateSubscription": "Не удалось инициировать подписку: {error}",
"failedToLoadBackgroundImage": "Не удалось загрузить фоновое изображение",
"failedToLoadHDRI": "Не удалось загрузить файл HDRI",
"failedToLoadModel": "Не удалось загрузить 3D-модель",
"failedToPurchaseCredits": "Не удалось купить кредиты: {error}",
"failedToQueue": "Не удалось поставить в очередь",
@@ -3466,6 +3476,7 @@
"pleaseSelectOutputNodes": "Пожалуйста, выберите выходные узлы",
"unableToGetModelFilePath": "Не удалось получить путь к файлу модели",
"unauthorizedDomain": "Ваш домен {domain} не авторизован для использования этого сервиса. Пожалуйста, свяжитесь с {email}, чтобы добавить ваш домен в белый список.",
"unsupportedHDRIFormat": "Неподдерживаемый формат файла. Пожалуйста, загрузите файл .hdr или .exr.",
"updateRequested": "Запрошено обновление",
"useApiKeyTip": "Совет: Нет доступа к обычному входу? Используйте опцию Comfy API Key.",
"userNotAuthenticated": "Пользователь не аутентифицирован"

View File

@@ -1642,9 +1642,18 @@
"exportRecording": "Kaydı Dışa Aktar",
"exportingModel": "Model dışa aktarılıyor...",
"fov": "FOV",
"hdri": {
"changeFile": "HDRI Değiştir",
"intensity": "Yoğunluk",
"label": "HDRI Ortamı",
"removeFile": "HDRI Kaldır",
"showAsBackground": "Arka Plan Olarak Göster",
"uploadFile": "HDRI Yükle (.hdr, .exr)"
},
"light": "Işık",
"lightIntensity": "Işık Yoğunluğu",
"loadingBackgroundImage": "Arka Plan Resmi Yükleniyor",
"loadingHDRI": "HDRI Yükleniyor...",
"loadingModel": "3D Model Yükleniyor...",
"materialMode": "Malzeme Modu",
"materialModes": {
@@ -3431,6 +3440,7 @@
"failedToInitiateCreditPurchase": "Kredi satın alma başlatılamadı: {error}",
"failedToInitiateSubscription": "Abonelik başlatılamadı: {error}",
"failedToLoadBackgroundImage": "Arka plan görseli yüklenemedi",
"failedToLoadHDRI": "HDRI dosyası yüklenemedi",
"failedToLoadModel": "3B model yüklenemedi",
"failedToPurchaseCredits": "Kredi satın alınamadı: {error}",
"failedToQueue": "Kuyruğa alınamadı",
@@ -3466,6 +3476,7 @@
"pleaseSelectOutputNodes": "Lütfen çıktı düğümlerini seçin",
"unableToGetModelFilePath": "Model dosyası yolu alınamıyor",
"unauthorizedDomain": "{domain} alan adınız bu hizmeti kullanma yetkisine sahip değil. Alan adınızı beyaz listeye eklemek için lütfen {email} ile iletişime geçin.",
"unsupportedHDRIFormat": "Desteklenmeyen dosya formatı. Lütfen .hdr veya .exr dosyası yükleyin.",
"updateRequested": "Güncelleme istendi",
"useApiKeyTip": "İpucu: Normal girişe erişemiyor musunuz? Comfy API Anahtarı seçeneğini kullanın.",
"userNotAuthenticated": "Kullanıcı doğrulanmadı"

View File

@@ -1642,9 +1642,18 @@
"exportRecording": "匯出錄影",
"exportingModel": "正在匯出模型...",
"fov": "視野角度",
"hdri": {
"changeFile": "更換 HDRI",
"intensity": "強度",
"label": "HDRI 環境",
"removeFile": "移除 HDRI",
"showAsBackground": "作為背景顯示",
"uploadFile": "上傳 HDRI.hdr, .exr"
},
"light": "光源",
"lightIntensity": "光源強度",
"loadingBackgroundImage": "正在載入背景圖片",
"loadingHDRI": "正在載入 HDRI...",
"loadingModel": "正在載入 3D 模型...",
"materialMode": "材質模式",
"materialModes": {
@@ -3431,6 +3440,7 @@
"failedToInitiateCreditPurchase": "啟動點數購買失敗:{error}",
"failedToInitiateSubscription": "無法啟用訂閱:{error}",
"failedToLoadBackgroundImage": "無法載入背景圖片",
"failedToLoadHDRI": "載入 HDRI 檔案失敗",
"failedToLoadModel": "無法載入 3D 模型",
"failedToPurchaseCredits": "購買點數失敗:{error}",
"failedToQueue": "加入佇列失敗",
@@ -3466,6 +3476,7 @@
"pleaseSelectOutputNodes": "請選擇輸出節點",
"unableToGetModelFilePath": "無法取得模型檔案路徑",
"unauthorizedDomain": "您的網域 {domain} 未被授權使用此服務。請聯絡 {email} 以將您的網域加入白名單。",
"unsupportedHDRIFormat": "不支援的檔案格式。請上傳 .hdr 或 .exr 檔案。",
"updateRequested": "已請求更新",
"useApiKeyTip": "提示:無法正常登入?請使用 Comfy API 金鑰選項。",
"userNotAuthenticated": "使用者未驗證"

View File

@@ -1642,9 +1642,18 @@
"exportRecording": "导出录制",
"exportingModel": "正在导出模型...",
"fov": "视场",
"hdri": {
"changeFile": "更换HDRI",
"intensity": "强度",
"label": "HDRI环境",
"removeFile": "移除HDRI",
"showAsBackground": "作为背景显示",
"uploadFile": "上传HDRI.hdr, .exr"
},
"light": "灯光",
"lightIntensity": "光照强度",
"loadingBackgroundImage": "正在加载背景图像",
"loadingHDRI": "正在加载HDRI...",
"loadingModel": "正在加载3D模型...",
"materialMode": "材质模式",
"materialModes": {
@@ -3443,6 +3452,7 @@
"failedToInitiateCreditPurchase": "发起积分购买失败:{error}",
"failedToInitiateSubscription": "订阅启动失败:{error}",
"failedToLoadBackgroundImage": "无法加载背景图片",
"failedToLoadHDRI": "HDRI文件加载失败",
"failedToLoadModel": "无法加载3D模型",
"failedToPurchaseCredits": "购买积分失败:{error}",
"failedToQueue": "排队失败",
@@ -3478,6 +3488,7 @@
"pleaseSelectOutputNodes": "请选择输出节点",
"unableToGetModelFilePath": "无法获取模型文件路径",
"unauthorizedDomain": "您的域名 {domain} 未被授权使用此服务。请联系 {email} 将您的域名添加到白名单。",
"unsupportedHDRIFormat": "不支持的文件格式。请上传.hdr或.exr文件。",
"updateRequested": "已请求更新",
"useApiKeyTip": "提示:无法正常登录?请使用 Comfy API Key 选项。",
"userNotAuthenticated": "用户未认证"

View File

@@ -4,7 +4,6 @@ import Popover from 'primevue/popover'
import { computed, ref, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import { useTransformCompatOverlayProps } from '@/composables/useTransformCompatOverlayProps'
import { useToastStore } from '@/platform/updates/common/toastStore'
import type {
@@ -51,7 +50,6 @@ interface Props {
}
const { t } = useI18n()
const overlayProps = useTransformCompatOverlayProps()
const {
placeholder,
@@ -211,7 +209,6 @@ function handleSelection(item: FormDropdownItem, index: number) {
ref="popoverRef"
:dismissable="true"
:close-on-escape="true"
:append-to="overlayProps.appendTo"
unstyled
:pt="{
root: {

View File

@@ -4,7 +4,6 @@ import { ref, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import Button from '@/components/ui/button/Button.vue'
import { useTransformCompatOverlayProps } from '@/composables/useTransformCompatOverlayProps'
import type {
FilterOption,
OwnershipFilterOption,
@@ -16,7 +15,6 @@ import FormSearchInput from '../FormSearchInput.vue'
import type { LayoutMode, SortOption } from './types'
const { t } = useI18n()
const overlayProps = useTransformCompatOverlayProps()
defineProps<{
sortOptions: SortOption[]
@@ -135,7 +133,6 @@ function toggleBaseModelSelection(item: FilterOption) {
ref="sortPopoverRef"
:dismissable="true"
:close-on-escape="true"
:append-to="overlayProps.appendTo"
unstyled
:pt="{
root: {
@@ -198,7 +195,6 @@ function toggleBaseModelSelection(item: FilterOption) {
ref="ownershipPopoverRef"
:dismissable="true"
:close-on-escape="true"
:append-to="overlayProps.appendTo"
unstyled
:pt="{
root: {
@@ -261,7 +257,6 @@ function toggleBaseModelSelection(item: FilterOption) {
ref="baseModelPopoverRef"
:dismissable="true"
:close-on-escape="true"
:append-to="overlayProps.appendTo"
unstyled
:pt="{
root: {

View File

@@ -7,12 +7,21 @@ import { useMissingNodesErrorStore } from '@/platform/nodeReplacement/missingNod
import { executionIdToNodeLocatorId } from '@/utils/graphTraversalUtil'
// Create mock functions that will be shared
const mockNodeExecutionIdToNodeLocatorId = vi.fn()
const mockNodeIdToNodeLocatorId = vi.fn()
const mockNodeLocatorIdToNodeExecutionId = vi.fn()
const {
mockNodeExecutionIdToNodeLocatorId,
mockNodeIdToNodeLocatorId,
mockNodeLocatorIdToNodeExecutionId,
mockShowTextPreview
} = vi.hoisted(() => ({
mockNodeExecutionIdToNodeLocatorId: vi.fn(),
mockNodeIdToNodeLocatorId: vi.fn(),
mockNodeLocatorIdToNodeExecutionId: vi.fn(),
mockShowTextPreview: vi.fn()
}))
import type * as WorkflowStoreModule from '@/platform/workflow/management/stores/workflowStore'
import type { NodeProgressState } from '@/schemas/apiSchema'
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
import { createMockLGraphNode } from '@/utils/__tests__/litegraphTestUtils'
import { createTestingPinia } from '@pinia/testing'
@@ -38,7 +47,7 @@ declare global {
vi.mock('@/composables/node/useNodeProgressText', () => ({
useNodeProgressText: () => ({
showTextPreview: vi.fn()
showTextPreview: mockShowTextPreview
})
}))
@@ -431,6 +440,56 @@ describe('useExecutionStore - reconcileInitializingJobs', () => {
})
})
describe('useExecutionStore - progress_text startup guard', () => {
let store: ReturnType<typeof useExecutionStore>
function fireProgressText(detail: {
nodeId: string
text: string
prompt_id?: string
}) {
const handler = apiEventHandlers.get('progress_text')
if (!handler) throw new Error('progress_text handler not bound')
handler(new CustomEvent('progress_text', { detail }))
}
beforeEach(() => {
vi.clearAllMocks()
apiEventHandlers.clear()
setActivePinia(createTestingPinia({ stubActions: false }))
store = useExecutionStore()
store.bindExecutionEvents()
})
it('should ignore progress_text before the canvas is initialized', async () => {
const { useCanvasStore } =
await import('@/renderer/core/canvas/canvasStore')
useCanvasStore().canvas = null
expect(() =>
fireProgressText({
nodeId: '1',
text: 'warming up'
})
).not.toThrow()
expect(mockShowTextPreview).not.toHaveBeenCalled()
})
it('should call showTextPreview when canvas is available', async () => {
const mockNode = createMockLGraphNode({ id: 1 })
const { useCanvasStore } =
await import('@/renderer/core/canvas/canvasStore')
useCanvasStore().canvas = {
graph: { getNodeById: vi.fn(() => mockNode) }
} as unknown as LGraphCanvas
fireProgressText({ nodeId: '1', text: 'warming up' })
expect(mockShowTextPreview).toHaveBeenCalledWith(mockNode, 'warming up')
})
})
describe('useExecutionErrorStore - Node Error Lookups', () => {
let store: ReturnType<typeof useExecutionErrorStore>

View File

@@ -527,7 +527,7 @@ export const useExecutionStore = defineStore('execution', () => {
// Handle execution node IDs for subgraphs
const currentId = getNodeIdIfExecuting(nodeId)
if (!currentId) return
const node = canvasStore.getCanvas().graph?.getNodeById(currentId)
const node = canvasStore.canvas?.graph?.getNodeById(currentId)
if (!node) return
useNodeProgressText().showTextPreview(node, text)