mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-15 04:01:00 +00:00
Compare commits
10 Commits
test/e2e-c
...
tests/e2e-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24c124c20b | ||
|
|
9b91af567e | ||
|
|
34a02a29c9 | ||
|
|
4bd655f625 | ||
|
|
a09bb81b98 | ||
|
|
aeedb60628 | ||
|
|
43fb5a8b19 | ||
|
|
c484c3984f | ||
|
|
2524846f5c | ||
|
|
12f578870e |
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
24
.github/workflows/release-biweekly-comfyui.yaml
vendored
24
.github/workflows/release-biweekly-comfyui.yaml
vendored
@@ -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
|
||||
|
||||
|
||||
68
browser_tests/assets/missing/node_replacement_multi.json
Normal file
68
browser_tests/assets/missing/node_replacement_multi.json
Normal 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
|
||||
}
|
||||
59
browser_tests/assets/missing/node_replacement_simple.json
Normal file
59
browser_tests/assets/missing/node_replacement_simple.json
Normal 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
|
||||
}
|
||||
47
browser_tests/fixtures/data/nodeReplacements.ts
Normal file
47
browser_tests/fixtures/data/nodeReplacements.ts
Normal 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
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -12,7 +12,6 @@ test.describe(
|
||||
() => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.workflow.loadWorkflow('default')
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
})
|
||||
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}) => {
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
|
||||
186
browser_tests/tests/nodeReplacement.spec.ts
Normal file
186
browser_tests/tests/nodeReplacement.spec.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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(
|
||||
|
||||
@@ -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({})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -12,7 +12,6 @@ test.describe(
|
||||
await comfyPage.workflow.loadWorkflow(
|
||||
'widgets/save_image_and_animated_webp'
|
||||
)
|
||||
await comfyPage.vueNodes.waitForNodes()
|
||||
|
||||
await comfyPage.runButton.click()
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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": "المستخدم غير مصدق"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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": "کاربر احراز هویت نشده است"
|
||||
|
||||
@@ -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 l’image d’arriè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 l’option Clé API Comfy.",
|
||||
"userNotAuthenticated": "Utilisateur non authentifié"
|
||||
|
||||
@@ -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": "ユーザーが認証されていません"
|
||||
|
||||
@@ -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": "사용자가 인증되지 않았습니다"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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": "Пользователь не аутентифицирован"
|
||||
|
||||
@@ -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ı"
|
||||
|
||||
@@ -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": "使用者未驗證"
|
||||
|
||||
@@ -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": "用户未认证"
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user