From 26eb3eff4d8727b588fe5f4c5e1fe303ef23c3aa Mon Sep 17 00:00:00 2001 From: Terry Jia Date: Wed, 28 Jan 2026 05:11:54 -0500 Subject: [PATCH 01/64] fix: add ResizeObserver to fix Preview3D initial render stretch (#8351) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary When Preview3D node was added to canvas, the Three.js scene would stretch outside the node bounds until mouse hover. This happened because the container size was not stable during initialization. Add ResizeObserver to Load3d class to automatically refresh viewport when container size changes, ensuring correct render dimensions. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8351-fix-add-ResizeObserver-to-fix-Preview3D-initial-render-stretch-2f66d73d3650810cbd1fc64dde9ddc17) by [Unito](https://www.unito.io) --- src/extensions/core/load3d/Load3d.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/extensions/core/load3d/Load3d.ts b/src/extensions/core/load3d/Load3d.ts index 60690e4f8..00cc62280 100644 --- a/src/extensions/core/load3d/Load3d.ts +++ b/src/extensions/core/load3d/Load3d.ts @@ -55,6 +55,7 @@ class Load3d { private rightMouseMoved: boolean = false private readonly dragThreshold: number = 5 private contextMenuAbortController: AbortController | null = null + private resizeObserver: ResizeObserver | null = null constructor(container: Element | HTMLElement, options: Load3DOptions = {}) { this.clock = new THREE.Clock() @@ -145,6 +146,7 @@ class Load3d { this.STATUS_MOUSE_ON_VIEWER = false this.initContextMenu() + this.initResizeObserver(container) this.handleResize() this.startAnimation() @@ -154,6 +156,14 @@ class Load3d { }, 100) } + private initResizeObserver(container: Element | HTMLElement): void { + this.resizeObserver = new ResizeObserver(() => { + this.handleResize() + this.forceRender() + }) + this.resizeObserver.observe(container) + } + /** * Initialize context menu on the Three.js canvas * Detects right-click vs right-drag to show menu only on click @@ -809,6 +819,11 @@ class Load3d { } public remove(): void { + if (this.resizeObserver) { + this.resizeObserver.disconnect() + this.resizeObserver = null + } + if (this.contextMenuAbortController) { this.contextMenuAbortController.abort() this.contextMenuAbortController = null From 3720b3e7946924a6a688a7f81c9f48103a36be7b Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Wed, 28 Jan 2026 02:15:09 -0800 Subject: [PATCH 02/64] feat: make invalid URL error message more actionable (#8368) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Updates the error message shown when users enter an unsupported URL in the BYOM (Bring Your Own Model) upload dialog. **Before:** "Only URLs from Civitai, Hugging Face are supported" **After:** "Please check the link format. Only URLs from Civitai, Hugging Face are supported." This provides more actionable guidance by suggesting users verify their link format before listing the supported sources. ## Changes - Updated `unsupportedUrlSource` i18n key in `src/locales/en/main.json` ## Testing - `pnpm typecheck` ✅ - `pnpm lint` ✅ - Manual: Enter invalid URL (e.g., `https://example.com/model.safetensors`) in model upload dialog ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8368-feat-make-invalid-URL-error-message-more-actionable-2f66d73d3650810bbcc1e9fa3d1cd962) by [Unito](https://www.unito.io) Co-authored-by: Subagent 5 Co-authored-by: Amp --- src/locales/en/main.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/locales/en/main.json b/src/locales/en/main.json index b3465916b..57fa47db6 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -2544,7 +2544,7 @@ "tagsPlaceholder": "e.g., models, checkpoint", "tryAdjustingFilters": "Try adjusting your search or filters", "unknown": "Unknown", - "unsupportedUrlSource": "Only URLs from {sources} are supported", + "unsupportedUrlSource": "This URL is not supported. Use a direct model link from {sources}. See the how-to videos below for help.", "upgradeFeatureDescription": "This feature is only available with Creator or Pro plans.", "upgradeToUnlockFeature": "Upgrade to unlock this feature", "upload": "Import", From 3e2352423b5a18aae8d8836ef8b98cf0138f9c70 Mon Sep 17 00:00:00 2001 From: Terry Jia Date: Wed, 28 Jan 2026 14:51:40 -0500 Subject: [PATCH 03/64] fix: remove redundant forceRender call and add ResizeObserver guard (#8372) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Remove duplicate forceRender() in ResizeObserver callback since handleResize() already calls it - Add guard for environments without ResizeObserver support - Disconnect existing observer before reassigning to prevent leaks requested by @DrJKL in https://github.com/Comfy-Org/ComfyUI_frontend/pull/8351 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8372-fix-remove-redundant-forceRender-call-and-add-ResizeObserver-guard-2f66d73d3650811bb3a6de5c59b3c1fb) by [Unito](https://www.unito.io) --- src/extensions/core/load3d/Load3d.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/extensions/core/load3d/Load3d.ts b/src/extensions/core/load3d/Load3d.ts index 00cc62280..445e9d608 100644 --- a/src/extensions/core/load3d/Load3d.ts +++ b/src/extensions/core/load3d/Load3d.ts @@ -157,9 +157,11 @@ class Load3d { } private initResizeObserver(container: Element | HTMLElement): void { + if (typeof ResizeObserver === 'undefined') return + + this.resizeObserver?.disconnect() this.resizeObserver = new ResizeObserver(() => { this.handleResize() - this.forceRender() }) this.resizeObserver.observe(container) } @@ -522,7 +524,6 @@ class Load3d { this.viewHelperManager.recreateViewHelper() this.handleResize() - this.forceRender() } getCurrentCameraType(): 'perspective' | 'orthographic' { @@ -584,7 +585,6 @@ class Load3d { } this.handleResize() - this.forceRender() this.loadingPromise = null } @@ -618,7 +618,6 @@ class Load3d { this.targetHeight = height this.targetAspectRatio = width / height this.handleResize() - this.forceRender() } addEventListener(event: string, callback: EventCallback): void { @@ -631,7 +630,6 @@ class Load3d { refreshViewport(): void { this.handleResize() - this.forceRender() } handleResize(): void { From e44b411ff681d2d14e2d701b4cf57253f50a3f12 Mon Sep 17 00:00:00 2001 From: Alexander Brown Date: Wed, 28 Jan 2026 12:17:16 -0800 Subject: [PATCH 04/64] test: simplify test file mocking patterns (#8320) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simplifies test mocking patterns across multiple test files. - Removes redundant `vi.hoisted()` calls - Cleans up mock implementations - Removes unused imports and variables ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8320-test-simplify-test-file-mocking-patterns-2f46d73d36508150981bd8ecb99a6a11) by [Unito](https://www.unito.io) --------- Co-authored-by: Amp Co-authored-by: GitHub Action --- docs/testing/unit-testing.md | 77 +++ src/components/graph/GraphCanvas.vue | 6 +- src/components/graph/SelectionToolbox.test.ts | 22 +- .../selectionToolbox/ExecuteButton.test.ts | 8 +- src/composables/useTemplateFiltering.test.ts | 4 +- src/lib/litegraph/src/LGraph.test.ts | 9 +- src/lib/litegraph/src/litegraph.test.ts | 28 +- .../composables/useMediaAssetActions.test.ts | 7 +- .../surveys/useFeatureUsageTracker.test.ts | 40 +- src/platform/telemetry/topupTracker.test.ts | 32 +- src/platform/telemetry/useTelemetry.test.ts | 11 +- .../updates/common/releaseStore.test.ts | 449 +++++++++++------- .../common/versionCompatibilityStore.test.ts | 7 +- .../thumbnail/useWorkflowThumbnail.test.ts | 9 +- .../minimap/composables/useMinimap.test.ts | 5 +- .../composables/useMinimapRenderer.test.ts | 4 +- .../composables/useMinimapViewport.test.ts | 26 +- .../composables/useRemoteWidget.test.ts | 5 +- src/services/jobOutputCache.test.ts | 102 ++-- src/services/newUserService.ts | 74 --- ...vice.test.ts => useNewUserService.test.ts} | 98 ++-- src/services/useNewUserService.ts | 82 ++++ src/stores/subgraphNavigationStore.test.ts | 2 +- .../manager/packCard/PackCard.test.ts | 31 +- 24 files changed, 627 insertions(+), 511 deletions(-) delete mode 100644 src/services/newUserService.ts rename src/services/{newUserService.test.ts => useNewUserService.test.ts} (84%) create mode 100644 src/services/useNewUserService.ts diff --git a/docs/testing/unit-testing.md b/docs/testing/unit-testing.md index a17592643..9be403659 100644 --- a/docs/testing/unit-testing.md +++ b/docs/testing/unit-testing.md @@ -11,6 +11,7 @@ This guide covers patterns and examples for unit testing utilities, composables, 5. [Mocking Utility Functions](#mocking-utility-functions) 6. [Testing with Debounce and Throttle](#testing-with-debounce-and-throttle) 7. [Mocking Node Definitions](#mocking-node-definitions) +8. [Mocking Composables with Reactive State](#mocking-composables-with-reactive-state) ## Testing Vue Composables with Reactivity @@ -253,3 +254,79 @@ it('should validate node definition', () => { expect(validateComfyNodeDef(EXAMPLE_NODE_DEF)).not.toBeNull() }) ``` + +## Mocking Composables with Reactive State + +When mocking composables that return reactive refs, define the mock implementation inline in `vi.mock()`'s factory function. This ensures stable singleton instances across all test invocations. + +### Rules + +1. **Define mocks in the factory function** — Create `vi.fn()` and `ref()` instances directly inside `vi.mock()`, not in `beforeEach` +2. **Use singleton pattern** — The factory runs once; all calls to the composable return the same mock object +3. **Access mocks per-test** — Call the composable directly in each test to get the singleton instance rather than storing in a shared variable +4. **Wrap in `vi.mocked()` for type safety** — Use `vi.mocked(service.method).mockResolvedValue(...)` when configuring +5. **Rely on `vi.resetAllMocks()`** — Resets call counts without recreating instances; ref values may need manual reset if mutated + +### Pattern + +```typescript +// Example from: src/platform/updates/common/releaseStore.test.ts +import { ref } from 'vue' + +vi.mock('@/path/to/composable', () => { + const doSomething = vi.fn() + const isLoading = ref(false) + const error = ref(null) + return { + useMyComposable: () => ({ + doSomething, + isLoading, + error + }) + } +}) + +describe('MyStore', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should call the composable method', async () => { + const service = useMyComposable() + vi.mocked(service.doSomething).mockResolvedValue({ data: 'test' }) + + await store.initialize() + + expect(service.doSomething).toHaveBeenCalledWith(expectedArgs) + }) + + it('should handle errors from the composable', async () => { + const service = useMyComposable() + vi.mocked(service.doSomething).mockResolvedValue(null) + service.error.value = 'Something went wrong' + + await store.initialize() + + expect(store.error).toBe('Something went wrong') + }) +}) +``` + +### Anti-patterns + +```typescript +// ❌ Don't configure mock return values in beforeEach with shared variable +let mockService: { doSomething: Mock } +beforeEach(() => { + mockService = { doSomething: vi.fn() } + vi.mocked(useMyComposable).mockReturnValue(mockService) +}) + +// ❌ Don't auto-mock then override — reactive refs won't work correctly +vi.mock('@/path/to/composable') +vi.mocked(useMyComposable).mockReturnValue({ isLoading: ref(false) }) +``` + +``` + +``` diff --git a/src/components/graph/GraphCanvas.vue b/src/components/graph/GraphCanvas.vue index 08e7573a9..27c038076 100644 --- a/src/components/graph/GraphCanvas.vue +++ b/src/components/graph/GraphCanvas.vue @@ -149,7 +149,7 @@ import { app as comfyApp } from '@/scripts/app' import { ChangeTracker } from '@/scripts/changeTracker' import { IS_CONTROL_WIDGET, updateControlWidgetLabel } from '@/scripts/widgets' import { useColorPaletteService } from '@/services/colorPaletteService' -import { newUserService } from '@/services/newUserService' +import { useNewUserService } from '@/services/useNewUserService' import { storeToRefs } from 'pinia' import { useBootstrapStore } from '@/stores/bootstrapStore' @@ -457,11 +457,9 @@ onMounted(async () => { // Register core settings immediately after settings are ready CORE_SETTINGS.forEach(settingStore.addSetting) - // Wait for both i18n and newUserService in parallel - // (newUserService only needs settings, not i18n) await Promise.all([ until(() => isI18nReady.value || !!i18nError.value).toBe(true), - newUserService().initializeIfNewUser(settingStore) + useNewUserService().initializeIfNewUser() ]) if (i18nError.value) { console.warn( diff --git a/src/components/graph/SelectionToolbox.test.ts b/src/components/graph/SelectionToolbox.test.ts index 441ca027f..bbbe9d786 100644 --- a/src/components/graph/SelectionToolbox.test.ts +++ b/src/components/graph/SelectionToolbox.test.ts @@ -13,6 +13,8 @@ import { createMockCanvas, createMockPositionable } from '@/utils/__tests__/litegraphTestUtils' +import * as litegraphUtil from '@/utils/litegraphUtil' +import * as nodeFilterUtil from '@/utils/nodeFilterUtil' function createMockExtensionService(): ReturnType { return { @@ -289,9 +291,8 @@ describe('SelectionToolbox', () => { ) }) - it('should show mask editor only for single image nodes', async () => { - const mockUtils = await import('@/utils/litegraphUtil') - const isImageNodeSpy = vi.spyOn(mockUtils, 'isImageNode') + it('should show mask editor only for single image nodes', () => { + const isImageNodeSpy = vi.spyOn(litegraphUtil, 'isImageNode') // Single image node isImageNodeSpy.mockReturnValue(true) @@ -307,9 +308,8 @@ describe('SelectionToolbox', () => { expect(wrapper2.find('.mask-editor-button').exists()).toBe(false) }) - it('should show Color picker button only for single Load3D nodes', async () => { - const mockUtils = await import('@/utils/litegraphUtil') - const isLoad3dNodeSpy = vi.spyOn(mockUtils, 'isLoad3dNode') + it('should show Color picker button only for single Load3D nodes', () => { + const isLoad3dNodeSpy = vi.spyOn(litegraphUtil, 'isLoad3dNode') // Single Load3D node isLoad3dNodeSpy.mockReturnValue(true) @@ -325,13 +325,9 @@ describe('SelectionToolbox', () => { expect(wrapper2.find('.load-3d-viewer-button').exists()).toBe(false) }) - it('should show ExecuteButton only when output nodes are selected', async () => { - const mockNodeFilterUtil = await import('@/utils/nodeFilterUtil') - const isOutputNodeSpy = vi.spyOn(mockNodeFilterUtil, 'isOutputNode') - const filterOutputNodesSpy = vi.spyOn( - mockNodeFilterUtil, - 'filterOutputNodes' - ) + it('should show ExecuteButton only when output nodes are selected', () => { + const isOutputNodeSpy = vi.spyOn(nodeFilterUtil, 'isOutputNode') + const filterOutputNodesSpy = vi.spyOn(nodeFilterUtil, 'filterOutputNodes') // With output node selected isOutputNodeSpy.mockReturnValue(true) diff --git a/src/components/graph/selectionToolbox/ExecuteButton.test.ts b/src/components/graph/selectionToolbox/ExecuteButton.test.ts index 6fe236d5b..370b96b1d 100644 --- a/src/components/graph/selectionToolbox/ExecuteButton.test.ts +++ b/src/components/graph/selectionToolbox/ExecuteButton.test.ts @@ -7,6 +7,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { createI18n } from 'vue-i18n' import ExecuteButton from '@/components/graph/selectionToolbox/ExecuteButton.vue' +import { useSelectionState } from '@/composables/graph/useSelectionState' import type { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { useCommandStore } from '@/stores/commandStore' @@ -47,7 +48,7 @@ describe('ExecuteButton', () => { } }) - beforeEach(async () => { + beforeEach(() => { // Set up Pinia with testing utilities setActivePinia( createTestingPinia({ @@ -71,10 +72,7 @@ describe('ExecuteButton', () => { vi.spyOn(commandStore, 'execute').mockResolvedValue() // Update the useSelectionState mock - const { useSelectionState } = vi.mocked( - await import('@/composables/graph/useSelectionState') - ) - useSelectionState.mockReturnValue({ + vi.mocked(useSelectionState).mockReturnValue({ selectedNodes: { value: mockSelectedNodes } diff --git a/src/composables/useTemplateFiltering.test.ts b/src/composables/useTemplateFiltering.test.ts index a44db0e99..48e7f6fd8 100644 --- a/src/composables/useTemplateFiltering.test.ts +++ b/src/composables/useTemplateFiltering.test.ts @@ -4,6 +4,7 @@ import { nextTick, ref } from 'vue' import type { IFuseOptions } from 'fuse.js' import type { TemplateInfo } from '@/platform/workflow/templates/types/template' +import { useTemplateFiltering } from '@/composables/useTemplateFiltering' const defaultSettingStore = { get: vi.fn((key: string) => { @@ -50,9 +51,6 @@ vi.mock('@/scripts/api', () => ({ } })) -const { useTemplateFiltering } = - await import('@/composables/useTemplateFiltering') - describe('useTemplateFiltering', () => { beforeEach(() => { setActivePinia(createPinia()) diff --git a/src/lib/litegraph/src/LGraph.test.ts b/src/lib/litegraph/src/LGraph.test.ts index 1c2f38da2..ed119aec5 100644 --- a/src/lib/litegraph/src/LGraph.test.ts +++ b/src/lib/litegraph/src/LGraph.test.ts @@ -46,12 +46,9 @@ describe('LGraph', () => { expect(graph.extra).toBe('TestGraph') }) - test('is exactly the same type', async ({ expect }) => { - const directImport = await import('@/lib/litegraph/src/LGraph') - const entryPointImport = await import('@/lib/litegraph/src/litegraph') - - expect(LiteGraph.LGraph).toBe(directImport.LGraph) - expect(LiteGraph.LGraph).toBe(entryPointImport.LGraph) + test('is exactly the same type', ({ expect }) => { + // LGraph from barrel export and LiteGraph.LGraph should be the same + expect(LiteGraph.LGraph).toBe(LGraph) }) test('populates optional values', ({ expect, minimalSerialisableGraph }) => { diff --git a/src/lib/litegraph/src/litegraph.test.ts b/src/lib/litegraph/src/litegraph.test.ts index 936b82f6e..19645a682 100644 --- a/src/lib/litegraph/src/litegraph.test.ts +++ b/src/lib/litegraph/src/litegraph.test.ts @@ -1,12 +1,15 @@ import { clamp } from 'es-toolkit/compat' -import { beforeEach, describe, expect, vi } from 'vitest' +import { describe, expect } from 'vitest' import { LiteGraphGlobal, LGraphCanvas, - LiteGraph + LiteGraph, + LGraph } from '@/lib/litegraph/src/litegraph' +import { LGraph as DirectLGraph } from '@/lib/litegraph/src/LGraph' + import { test } from './__fixtures__/testExtensions' describe('Litegraph module', () => { @@ -27,22 +30,9 @@ describe('Litegraph module', () => { }) describe('Import order dependency', () => { - beforeEach(() => { - vi.resetModules() - }) - - test('Imports without error when entry point is imported first', async ({ - expect - }) => { - async function importNormally() { - const entryPointImport = await import('@/lib/litegraph/src/litegraph') - const directImport = await import('@/lib/litegraph/src/LGraph') - - // Sanity check that imports were cleared. - expect(Object.is(LiteGraph, entryPointImport.LiteGraph)).toBe(false) - expect(Object.is(LiteGraph.LGraph, directImport.LGraph)).toBe(false) - } - - await expect(importNormally()).resolves.toBeUndefined() + test('Imports reference the same types', ({ expect }) => { + // Both imports should reference the same LGraph class + expect(LiteGraph.LGraph).toBe(DirectLGraph) + expect(LiteGraph.LGraph).toBe(LGraph) }) }) diff --git a/src/platform/assets/composables/useMediaAssetActions.test.ts b/src/platform/assets/composables/useMediaAssetActions.test.ts index 6c8f680a0..2d0a7a291 100644 --- a/src/platform/assets/composables/useMediaAssetActions.test.ts +++ b/src/platform/assets/composables/useMediaAssetActions.test.ts @@ -4,6 +4,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type { LGraphNode } from '@/lib/litegraph/src/litegraph' import type { AssetItem } from '@/platform/assets/schemas/assetSchema' +import { useMediaAssetActions } from './useMediaAssetActions' + // Use vi.hoisted to create a mutable reference for isCloud const mockIsCloud = vi.hoisted(() => ({ value: false })) @@ -126,7 +128,6 @@ describe('useMediaAssetActions', () => { }) it('should use asset.name as filename', async () => { - const { useMediaAssetActions } = await import('./useMediaAssetActions') const actions = useMediaAssetActions() const asset = createMockAsset({ @@ -146,7 +147,6 @@ describe('useMediaAssetActions', () => { }) it('should use asset_hash as filename when available', async () => { - const { useMediaAssetActions } = await import('./useMediaAssetActions') const actions = useMediaAssetActions() const asset = createMockAsset({ @@ -160,7 +160,6 @@ describe('useMediaAssetActions', () => { }) it('should fall back to asset.name when asset_hash is not available', async () => { - const { useMediaAssetActions } = await import('./useMediaAssetActions') const actions = useMediaAssetActions() const asset = createMockAsset({ @@ -174,7 +173,6 @@ describe('useMediaAssetActions', () => { }) it('should fall back to asset.name when asset_hash is null', async () => { - const { useMediaAssetActions } = await import('./useMediaAssetActions') const actions = useMediaAssetActions() const asset = createMockAsset({ @@ -196,7 +194,6 @@ describe('useMediaAssetActions', () => { }) it('should use asset_hash for each asset', async () => { - const { useMediaAssetActions } = await import('./useMediaAssetActions') const actions = useMediaAssetActions() const assets = [ diff --git a/src/platform/surveys/useFeatureUsageTracker.test.ts b/src/platform/surveys/useFeatureUsageTracker.test.ts index 5313e9eaf..f37b41ee1 100644 --- a/src/platform/surveys/useFeatureUsageTracker.test.ts +++ b/src/platform/surveys/useFeatureUsageTracker.test.ts @@ -1,27 +1,27 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { useFeatureUsageTracker } from './useFeatureUsageTracker' + const STORAGE_KEY = 'Comfy.FeatureUsage' describe('useFeatureUsageTracker', () => { beforeEach(() => { localStorage.clear() - vi.resetModules() + vi.clearAllMocks() }) afterEach(() => { localStorage.clear() }) - it('initializes with zero count for new feature', async () => { - const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker') - const { useCount } = useFeatureUsageTracker('test-feature') + it('initializes with zero count for new feature', () => { + const { useCount } = useFeatureUsageTracker('test-feature-1') expect(useCount.value).toBe(0) }) - it('increments count on trackUsage', async () => { - const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker') - const { useCount, trackUsage } = useFeatureUsageTracker('test-feature') + it('increments count on trackUsage', () => { + const { useCount, trackUsage } = useFeatureUsageTracker('test-feature-2') expect(useCount.value).toBe(0) @@ -32,14 +32,12 @@ describe('useFeatureUsageTracker', () => { expect(useCount.value).toBe(2) }) - it('sets firstUsed only on first use', async () => { + it('sets firstUsed only on first use', () => { vi.useFakeTimers() const firstTs = 1000000 vi.setSystemTime(firstTs) try { - const { useFeatureUsageTracker } = - await import('./useFeatureUsageTracker') - const { usage, trackUsage } = useFeatureUsageTracker('test-feature') + const { usage, trackUsage } = useFeatureUsageTracker('test-feature-3') trackUsage() expect(usage.value?.firstUsed).toBe(firstTs) @@ -52,12 +50,10 @@ describe('useFeatureUsageTracker', () => { } }) - it('updates lastUsed on each use', async () => { + it('updates lastUsed on each use', () => { vi.useFakeTimers() try { - const { useFeatureUsageTracker } = - await import('./useFeatureUsageTracker') - const { usage, trackUsage } = useFeatureUsageTracker('test-feature') + const { usage, trackUsage } = useFeatureUsageTracker('test-feature-4') trackUsage() const firstLastUsed = usage.value?.lastUsed ?? 0 @@ -71,10 +67,9 @@ describe('useFeatureUsageTracker', () => { } }) - it('reset clears feature data', async () => { - const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker') + it('reset clears feature data', () => { const { useCount, trackUsage, reset } = - useFeatureUsageTracker('test-feature') + useFeatureUsageTracker('test-feature-5') trackUsage() trackUsage() @@ -84,8 +79,7 @@ describe('useFeatureUsageTracker', () => { expect(useCount.value).toBe(0) }) - it('tracks multiple features independently', async () => { - const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker') + it('tracks multiple features independently', () => { const featureA = useFeatureUsageTracker('feature-a') const featureB = useFeatureUsageTracker('feature-b') @@ -100,8 +94,6 @@ describe('useFeatureUsageTracker', () => { it('persists to localStorage', async () => { vi.useFakeTimers() try { - const { useFeatureUsageTracker } = - await import('./useFeatureUsageTracker') const { trackUsage } = useFeatureUsageTracker('persisted-feature') trackUsage() @@ -114,7 +106,7 @@ describe('useFeatureUsageTracker', () => { } }) - it('loads existing data from localStorage', async () => { + it('loads existing data from localStorage', () => { localStorage.setItem( STORAGE_KEY, JSON.stringify({ @@ -122,8 +114,6 @@ describe('useFeatureUsageTracker', () => { }) ) - vi.resetModules() - const { useFeatureUsageTracker } = await import('./useFeatureUsageTracker') const { useCount } = useFeatureUsageTracker('existing-feature') expect(useCount.value).toBe(5) diff --git a/src/platform/telemetry/topupTracker.test.ts b/src/platform/telemetry/topupTracker.test.ts index 1ea07ff90..9f8e38ccf 100644 --- a/src/platform/telemetry/topupTracker.test.ts +++ b/src/platform/telemetry/topupTracker.test.ts @@ -1,6 +1,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import type * as TopupTrackerModule from '@/platform/telemetry/topupTracker' +import { + startTopupTracking, + checkForCompletedTopup, + clearTopupTracking +} from '@/platform/telemetry/topupTracker' import type { AuditLog } from '@/services/customerEventsService' // Mock localStorage @@ -25,19 +29,15 @@ vi.mock('@/platform/telemetry', () => ({ })) describe('topupTracker', () => { - let topupTracker: typeof TopupTrackerModule - - beforeEach(async () => { + beforeEach(() => { vi.clearAllMocks() - // Dynamically import to ensure fresh module state - topupTracker = await import('@/platform/telemetry/topupTracker') }) describe('startTopupTracking', () => { it('should save current timestamp to localStorage', () => { const beforeTimestamp = Date.now() - topupTracker.startTopupTracking() + startTopupTracking() expect(mockLocalStorage.setItem).toHaveBeenCalledWith( 'pending_topup_timestamp', @@ -57,7 +57,7 @@ describe('topupTracker', () => { it('should return false if no pending topup exists', () => { mockLocalStorage.getItem.mockReturnValue(null) - const result = topupTracker.checkForCompletedTopup([]) + const result = checkForCompletedTopup([]) expect(result).toBe(false) expect(mockTelemetry.trackApiCreditTopupSucceeded).not.toHaveBeenCalled() @@ -66,7 +66,7 @@ describe('topupTracker', () => { it('should return false if events array is empty', () => { mockLocalStorage.getItem.mockReturnValue(Date.now().toString()) - const result = topupTracker.checkForCompletedTopup([]) + const result = checkForCompletedTopup([]) expect(result).toBe(false) expect(mockTelemetry.trackApiCreditTopupSucceeded).not.toHaveBeenCalled() @@ -75,7 +75,7 @@ describe('topupTracker', () => { it('should return false if events array is null', () => { mockLocalStorage.getItem.mockReturnValue(Date.now().toString()) - const result = topupTracker.checkForCompletedTopup(null) + const result = checkForCompletedTopup(null) expect(result).toBe(false) expect(mockTelemetry.trackApiCreditTopupSucceeded).not.toHaveBeenCalled() @@ -94,7 +94,7 @@ describe('topupTracker', () => { } ] - const result = topupTracker.checkForCompletedTopup(events) + const result = checkForCompletedTopup(events) expect(result).toBe(false) expect(mockLocalStorage.removeItem).toHaveBeenCalledWith( @@ -122,7 +122,7 @@ describe('topupTracker', () => { } ] - const result = topupTracker.checkForCompletedTopup(events) + const result = checkForCompletedTopup(events) expect(result).toBe(true) expect(mockTelemetry.trackApiCreditTopupSucceeded).toHaveBeenCalledOnce() @@ -144,7 +144,7 @@ describe('topupTracker', () => { } ] - const result = topupTracker.checkForCompletedTopup(events) + const result = checkForCompletedTopup(events) expect(result).toBe(false) expect(mockTelemetry.trackApiCreditTopupSucceeded).not.toHaveBeenCalled() @@ -164,7 +164,7 @@ describe('topupTracker', () => { } ] - const result = topupTracker.checkForCompletedTopup(events) + const result = checkForCompletedTopup(events) expect(result).toBe(false) expect(mockTelemetry.trackApiCreditTopupSucceeded).not.toHaveBeenCalled() @@ -189,7 +189,7 @@ describe('topupTracker', () => { } ] - const result = topupTracker.checkForCompletedTopup(events) + const result = checkForCompletedTopup(events) expect(result).toBe(false) expect(mockTelemetry.trackApiCreditTopupSucceeded).not.toHaveBeenCalled() @@ -198,7 +198,7 @@ describe('topupTracker', () => { describe('clearTopupTracking', () => { it('should remove pending topup from localStorage', () => { - topupTracker.clearTopupTracking() + clearTopupTracking() expect(mockLocalStorage.removeItem).toHaveBeenCalledWith( 'pending_topup_timestamp' diff --git a/src/platform/telemetry/useTelemetry.test.ts b/src/platform/telemetry/useTelemetry.test.ts index b5c887c74..d3d565c2e 100644 --- a/src/platform/telemetry/useTelemetry.test.ts +++ b/src/platform/telemetry/useTelemetry.test.ts @@ -1,5 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' +import { useTelemetry } from '@/platform/telemetry' + vi.mock('@/platform/distribution/types', () => ({ isCloud: false })) @@ -9,17 +11,14 @@ describe('useTelemetry', () => { vi.clearAllMocks() }) - it('should return null when not in cloud distribution', async () => { - const { useTelemetry } = await import('@/platform/telemetry') + it('should return null when not in cloud distribution', () => { const provider = useTelemetry() // Should return null for OSS builds expect(provider).toBeNull() - }, 10000) - - it('should return null consistently for OSS builds', async () => { - const { useTelemetry } = await import('@/platform/telemetry') + }) + it('should return null consistently for OSS builds', () => { const provider1 = useTelemetry() const provider2 = useTelemetry() diff --git a/src/platform/updates/common/releaseStore.test.ts b/src/platform/updates/common/releaseStore.test.ts index 2f22d18e0..b54c8e87d 100644 --- a/src/platform/updates/common/releaseStore.test.ts +++ b/src/platform/updates/common/releaseStore.test.ts @@ -1,19 +1,96 @@ -import { createPinia, setActivePinia } from 'pinia' -import { compare, valid } from 'semver' -import type { Mock } from 'vitest' +import { until } from '@vueuse/core' +import { setActivePinia } from 'pinia' +import { compare } from 'semver' import { beforeEach, describe, expect, it, vi } from 'vitest' import { ref } from 'vue' import type { ReleaseNote } from '@/platform/updates/common/releaseService' +import { useSettingStore } from '@/platform/settings/settingStore' import { useReleaseStore } from '@/platform/updates/common/releaseStore' +import { useReleaseService } from '@/platform/updates/common/releaseService' +import { useSystemStatsStore } from '@/stores/systemStatsStore' +import { isElectron } from '@/utils/envUtil' +import { createTestingPinia } from '@pinia/testing' +import type { SystemStats } from '@/types' // Mock the dependencies -vi.mock('semver') -vi.mock('@/utils/envUtil') +vi.mock('semver', () => ({ + compare: vi.fn(), + valid: vi.fn(() => '1.0.0') +})) + +vi.mock('@/utils/envUtil', () => ({ + isElectron: vi.fn(() => true) +})) + vi.mock('@/platform/distribution/types', () => ({ isCloud: false })) -vi.mock('@/platform/updates/common/releaseService') -vi.mock('@/platform/settings/settingStore') -vi.mock('@/stores/systemStatsStore') + +vi.mock('@/platform/updates/common/releaseService', () => { + const getReleases = vi.fn() + const isLoading = ref(false) + const error = ref(null) + return { + useReleaseService: () => ({ + getReleases, + isLoading, + error + }) + } +}) + +vi.mock('@/platform/settings/settingStore', () => { + const get = vi.fn((key: string) => { + if (key === 'Comfy.Notification.ShowVersionUpdates') return true + return null + }) + const set = vi.fn() + return { + useSettingStore: () => ({ get, set }) + } +}) + +const mockSystemStatsState = vi.hoisted(() => ({ + systemStats: { + system: { + comfyui_version: '1.0.0', + argv: [] + } + } satisfies { + system: Partial + }, + isInitialized: true, + reset() { + this.systemStats = { + system: { + comfyui_version: '1.0.0', + argv: [] + } satisfies Partial + } + this.isInitialized = true + } +})) +vi.mock('@/stores/systemStatsStore', () => { + const refetchSystemStats = vi.fn() + const getFormFactor = vi.fn(() => 'git-windows') + return { + useSystemStatsStore: () => ({ + get systemStats() { + return mockSystemStatsState.systemStats + }, + set systemStats(val) { + mockSystemStatsState.systemStats = val + }, + get isInitialized() { + return mockSystemStatsState.isInitialized + }, + set isInitialized(val) { + mockSystemStatsState.isInitialized = val + }, + refetchSystemStats, + getFormFactor + }) + } +}) vi.mock('@vueuse/core', () => ({ until: vi.fn(() => Promise.resolve()), useStorage: vi.fn(() => ({ value: {} })), @@ -21,27 +98,6 @@ vi.mock('@vueuse/core', () => ({ })) describe('useReleaseStore', () => { - let store: ReturnType - let mockReleaseService: { - getReleases: Mock - isLoading: ReturnType> - error: ReturnType> - } - let mockSettingStore: { get: Mock; set: Mock } - let mockSystemStatsStore: { - systemStats: { - system: { - comfyui_version: string - argv?: string[] - [key: string]: unknown - } - devices?: unknown[] - } | null - isInitialized: boolean - refetchSystemStats: Mock - getFormFactor: Mock - } - const mockRelease = { id: 1, project: 'comfyui' as const, @@ -51,71 +107,16 @@ describe('useReleaseStore', () => { attention: 'high' as const } - beforeEach(async () => { - setActivePinia(createPinia()) + beforeEach(() => { + setActivePinia(createTestingPinia({ stubActions: false })) - // Reset all mocks - vi.clearAllMocks() - - // Setup mock services with proper refs - mockReleaseService = { - getReleases: vi.fn(), - isLoading: ref(false), - error: ref(null) - } - - mockSettingStore = { - get: vi.fn(), - set: vi.fn() - } - - mockSystemStatsStore = { - systemStats: { - system: { - comfyui_version: '1.0.0' - } - }, - isInitialized: true, - refetchSystemStats: vi.fn(), - getFormFactor: vi.fn(() => 'git-windows') - } - - // Setup mock implementations - const { useReleaseService } = - await import('@/platform/updates/common/releaseService') - const { useSettingStore } = await import('@/platform/settings/settingStore') - const { useSystemStatsStore } = await import('@/stores/systemStatsStore') - const { isElectron } = await import('@/utils/envUtil') - - vi.mocked(useReleaseService).mockReturnValue( - mockReleaseService as Partial< - ReturnType - > as ReturnType - ) - vi.mocked(useSettingStore).mockReturnValue( - mockSettingStore as Partial< - ReturnType - > as ReturnType - ) - vi.mocked(useSystemStatsStore).mockReturnValue( - mockSystemStatsStore as Partial< - ReturnType - > as ReturnType - ) - vi.mocked(isElectron).mockReturnValue(true) - vi.mocked(valid).mockReturnValue('1.0.0') - - // Default showVersionUpdates to true - mockSettingStore.get.mockImplementation((key: string) => { - if (key === 'Comfy.Notification.ShowVersionUpdates') return true - return null - }) - - store = useReleaseStore() + vi.resetAllMocks() + mockSystemStatsState.reset() }) describe('initial state', () => { it('should initialize with default state', () => { + const store = useReleaseStore() expect(store.releases).toEqual([]) expect(store.isLoading).toBe(false) expect(store.error).toBeNull() @@ -124,6 +125,7 @@ describe('useReleaseStore', () => { describe('computed properties', () => { it('should return most recent release', () => { + const store = useReleaseStore() const olderRelease = { ...mockRelease, id: 2, @@ -136,6 +138,7 @@ describe('useReleaseStore', () => { }) it('should return 3 most recent releases', () => { + const store = useReleaseStore() const releases = [ mockRelease, { ...mockRelease, id: 2, version: '1.1.0' }, @@ -148,6 +151,7 @@ describe('useReleaseStore', () => { }) it('should show update button (shouldShowUpdateButton)', () => { + const store = useReleaseStore() vi.mocked(compare).mockReturnValue(1) // newer version available store.releases = [mockRelease] @@ -155,6 +159,7 @@ describe('useReleaseStore', () => { }) it('should not show update button when no new version', () => { + const store = useReleaseStore() vi.mocked(compare).mockReturnValue(-1) // current version is newer store.releases = [mockRelease] @@ -163,19 +168,17 @@ describe('useReleaseStore', () => { }) describe('showVersionUpdates setting', () => { - beforeEach(async () => { - store.releases = [mockRelease] - }) - describe('when notifications are enabled', () => { - beforeEach(async () => { - mockSettingStore.get.mockImplementation((key: string) => { + beforeEach(() => { + const settingStore = useSettingStore() + vi.mocked(settingStore.get).mockImplementation((key: string) => { if (key === 'Comfy.Notification.ShowVersionUpdates') return true return null }) }) it('should show toast for medium/high attention releases', () => { + const store = useReleaseStore() vi.mocked(compare).mockReturnValue(1) store.releases = [mockRelease] @@ -183,6 +186,7 @@ describe('useReleaseStore', () => { }) it('should not show toast for low attention releases', () => { + const store = useReleaseStore() vi.mocked(compare).mockReturnValue(1) const lowAttentionRelease = { @@ -196,13 +200,18 @@ describe('useReleaseStore', () => { }) it('should show red dot for new versions', () => { + const store = useReleaseStore() + store.releases = [mockRelease] vi.mocked(compare).mockReturnValue(1) expect(store.shouldShowRedDot).toBe(true) }) it('should show popup for latest version', () => { - mockSystemStatsStore.systemStats!.system.comfyui_version = '1.2.0' + const store = useReleaseStore() + store.releases = [mockRelease] + const systemStatsStore = useSystemStatsStore() + systemStatsStore.systemStats!.system.comfyui_version = '1.2.0' vi.mocked(compare).mockReturnValue(0) @@ -210,11 +219,13 @@ describe('useReleaseStore', () => { }) it('should fetch releases during initialization', async () => { - mockReleaseService.getReleases.mockResolvedValue([mockRelease]) + const store = useReleaseStore() + const releaseService = useReleaseService() + vi.mocked(releaseService.getReleases).mockResolvedValue([mockRelease]) await store.initialize() - expect(mockReleaseService.getReleases).toHaveBeenCalledWith({ + expect(releaseService.getReleases).toHaveBeenCalledWith({ project: 'comfyui', current_version: '1.0.0', form_factor: 'git-windows', @@ -224,27 +235,35 @@ describe('useReleaseStore', () => { }) describe('when notifications are disabled', () => { - beforeEach(async () => { - mockSettingStore.get.mockImplementation((key: string) => { + beforeEach(() => { + const settingStore = useSettingStore() + vi.mocked(settingStore.get).mockImplementation((key: string) => { if (key === 'Comfy.Notification.ShowVersionUpdates') return false return null }) }) it('should not show toast even with new version available', () => { + const store = useReleaseStore() + store.releases = [mockRelease] vi.mocked(compare).mockReturnValue(1) expect(store.shouldShowToast).toBe(false) }) it('should not show red dot even with new version available', () => { + const store = useReleaseStore() + store.releases = [mockRelease] vi.mocked(compare).mockReturnValue(1) expect(store.shouldShowRedDot).toBe(false) }) it('should not show popup even for latest version', () => { - mockSystemStatsStore.systemStats!.system.comfyui_version = '1.2.0' + const store = useReleaseStore() + store.releases = [mockRelease] + const systemStatsStore = useSystemStatsStore() + systemStatsStore.systemStats!.system.comfyui_version = '1.2.0' vi.mocked(compare).mockReturnValue(0) @@ -252,15 +271,19 @@ describe('useReleaseStore', () => { }) it('should skip fetching releases during initialization', async () => { + const store = useReleaseStore() + const releaseService = useReleaseService() await store.initialize() - expect(mockReleaseService.getReleases).not.toHaveBeenCalled() + expect(releaseService.getReleases).not.toHaveBeenCalled() }) it('should not fetch releases when calling fetchReleases directly', async () => { + const store = useReleaseStore() + const releaseService = useReleaseService() await store.fetchReleases() - expect(mockReleaseService.getReleases).not.toHaveBeenCalled() + expect(releaseService.getReleases).not.toHaveBeenCalled() expect(store.isLoading).toBe(false) }) }) @@ -268,11 +291,13 @@ describe('useReleaseStore', () => { describe('release initialization', () => { it('should fetch releases successfully', async () => { - mockReleaseService.getReleases.mockResolvedValue([mockRelease]) + const store = useReleaseStore() + const releaseService = useReleaseService() + vi.mocked(releaseService.getReleases).mockResolvedValue([mockRelease]) await store.initialize() - expect(mockReleaseService.getReleases).toHaveBeenCalledWith({ + expect(releaseService.getReleases).toHaveBeenCalledWith({ project: 'comfyui', current_version: '1.0.0', form_factor: 'git-windows', @@ -282,12 +307,15 @@ describe('useReleaseStore', () => { }) it('should include form_factor in API call', async () => { - mockSystemStatsStore.getFormFactor.mockReturnValue('desktop-mac') - mockReleaseService.getReleases.mockResolvedValue([mockRelease]) + const store = useReleaseStore() + const releaseService = useReleaseService() + const systemStatsStore = useSystemStatsStore() + vi.mocked(systemStatsStore.getFormFactor).mockReturnValue('desktop-mac') + vi.mocked(releaseService.getReleases).mockResolvedValue([mockRelease]) await store.initialize() - expect(mockReleaseService.getReleases).toHaveBeenCalledWith({ + expect(releaseService.getReleases).toHaveBeenCalledWith({ project: 'comfyui', current_version: '1.0.0', form_factor: 'desktop-mac', @@ -296,16 +324,22 @@ describe('useReleaseStore', () => { }) it('should skip fetching when --disable-api-nodes is present', async () => { - mockSystemStatsStore.systemStats!.system.argv = ['--disable-api-nodes'] + const store = useReleaseStore() + const releaseService = useReleaseService() + const systemStatsStore = useSystemStatsStore() + systemStatsStore.systemStats!.system.argv = ['--disable-api-nodes'] await store.initialize() - expect(mockReleaseService.getReleases).not.toHaveBeenCalled() + expect(releaseService.getReleases).not.toHaveBeenCalled() expect(store.isLoading).toBe(false) }) it('should skip fetching when --disable-api-nodes is one of multiple args', async () => { - mockSystemStatsStore.systemStats!.system.argv = [ + const store = useReleaseStore() + const releaseService = useReleaseService() + const systemStatsStore = useSystemStatsStore() + systemStatsStore.systemStats!.system.argv = [ '--port', '8080', '--disable-api-nodes', @@ -314,37 +348,46 @@ describe('useReleaseStore', () => { await store.initialize() - expect(mockReleaseService.getReleases).not.toHaveBeenCalled() + expect(releaseService.getReleases).not.toHaveBeenCalled() expect(store.isLoading).toBe(false) }) it('should fetch normally when --disable-api-nodes is not present', async () => { - mockSystemStatsStore.systemStats!.system.argv = [ + const store = useReleaseStore() + const releaseService = useReleaseService() + const systemStatsStore = useSystemStatsStore() + systemStatsStore.systemStats!.system.argv = [ '--port', '8080', '--verbose' ] - mockReleaseService.getReleases.mockResolvedValue([mockRelease]) + vi.mocked(releaseService.getReleases).mockResolvedValue([mockRelease]) await store.initialize() - expect(mockReleaseService.getReleases).toHaveBeenCalled() + expect(releaseService.getReleases).toHaveBeenCalled() expect(store.releases).toEqual([mockRelease]) }) it('should fetch normally when argv is undefined', async () => { - mockSystemStatsStore.systemStats!.system.argv = undefined - mockReleaseService.getReleases.mockResolvedValue([mockRelease]) + const store = useReleaseStore() + const releaseService = useReleaseService() + const systemStatsStore = useSystemStatsStore() + // TODO: Consider deleting this test since the types have to be violated for it to be relevant + delete (systemStatsStore.systemStats!.system as { argv?: string[] }).argv + vi.mocked(releaseService.getReleases).mockResolvedValue([mockRelease]) await store.initialize() - expect(mockReleaseService.getReleases).toHaveBeenCalled() + expect(releaseService.getReleases).toHaveBeenCalled() expect(store.releases).toEqual([mockRelease]) }) it('should handle API errors gracefully', async () => { - mockReleaseService.getReleases.mockResolvedValue(null) - mockReleaseService.error.value = 'API Error' + const store = useReleaseStore() + const releaseService = useReleaseService() + vi.mocked(releaseService.getReleases).mockResolvedValue(null) + releaseService.error.value = 'API Error' await store.initialize() @@ -353,7 +396,9 @@ describe('useReleaseStore', () => { }) it('should handle non-Error objects', async () => { - mockReleaseService.getReleases.mockRejectedValue('String error') + const store = useReleaseStore() + const releaseService = useReleaseService() + vi.mocked(releaseService.getReleases).mockRejectedValue('String error') await store.initialize() @@ -361,12 +406,14 @@ describe('useReleaseStore', () => { }) it('should set loading state correctly', async () => { + const store = useReleaseStore() + const releaseService = useReleaseService() let resolvePromise: (value: ReleaseNote[] | null) => void const promise = new Promise((resolve) => { resolvePromise = resolve }) - mockReleaseService.getReleases.mockReturnValue(promise) + vi.mocked(releaseService.getReleases).mockReturnValue(promise) const initPromise = store.initialize() expect(store.isLoading).toBe(true) @@ -378,19 +425,23 @@ describe('useReleaseStore', () => { }) it('should fetch system stats if not available', async () => { - const { until } = await import('@vueuse/core') - mockSystemStatsStore.systemStats = null - mockSystemStatsStore.isInitialized = false - mockReleaseService.getReleases.mockResolvedValue([mockRelease]) + const store = useReleaseStore() + const releaseService = useReleaseService() + const systemStatsStore = useSystemStatsStore() + systemStatsStore.systemStats = null + systemStatsStore.isInitialized = false + vi.mocked(releaseService.getReleases).mockResolvedValue([mockRelease]) await store.initialize() - expect(until).toHaveBeenCalled() - expect(mockReleaseService.getReleases).toHaveBeenCalled() + expect(vi.mocked(until)).toHaveBeenCalled() + expect(releaseService.getReleases).toHaveBeenCalled() }) it('should not set loading state when notifications disabled', async () => { - mockSettingStore.get.mockImplementation((key: string) => { + const store = useReleaseStore() + const settingStore = useSettingStore() + vi.mocked(settingStore.get).mockImplementation((key: string) => { if (key === 'Comfy.Notification.ShowVersionUpdates') return false return null }) @@ -403,16 +454,22 @@ describe('useReleaseStore', () => { describe('--disable-api-nodes argument handling', () => { it('should skip fetchReleases when --disable-api-nodes is present', async () => { - mockSystemStatsStore.systemStats!.system.argv = ['--disable-api-nodes'] + const store = useReleaseStore() + const releaseService = useReleaseService() + const systemStatsStore = useSystemStatsStore() + systemStatsStore.systemStats!.system.argv = ['--disable-api-nodes'] await store.fetchReleases() - expect(mockReleaseService.getReleases).not.toHaveBeenCalled() + expect(releaseService.getReleases).not.toHaveBeenCalled() expect(store.isLoading).toBe(false) }) it('should skip fetchReleases when --disable-api-nodes is among other args', async () => { - mockSystemStatsStore.systemStats!.system.argv = [ + const store = useReleaseStore() + const releaseService = useReleaseService() + const systemStatsStore = useSystemStatsStore() + systemStatsStore.systemStats!.system.argv = [ '--port', '8080', '--disable-api-nodes', @@ -421,96 +478,109 @@ describe('useReleaseStore', () => { await store.fetchReleases() - expect(mockReleaseService.getReleases).not.toHaveBeenCalled() + expect(releaseService.getReleases).not.toHaveBeenCalled() expect(store.isLoading).toBe(false) }) it('should proceed with fetchReleases when --disable-api-nodes is not present', async () => { - mockSystemStatsStore.systemStats!.system.argv = [ + const store = useReleaseStore() + const releaseService = useReleaseService() + const systemStatsStore = useSystemStatsStore() + systemStatsStore.systemStats!.system.argv = [ '--port', '8080', '--verbose' ] - mockReleaseService.getReleases.mockResolvedValue([mockRelease]) + vi.mocked(releaseService.getReleases).mockResolvedValue([mockRelease]) await store.fetchReleases() - expect(mockReleaseService.getReleases).toHaveBeenCalled() + expect(releaseService.getReleases).toHaveBeenCalled() }) it('should proceed with fetchReleases when argv is undefined', async () => { - mockSystemStatsStore.systemStats!.system.argv = undefined - mockReleaseService.getReleases.mockResolvedValue([mockRelease]) + const store = useReleaseStore() + const releaseService = useReleaseService() + const systemStatsStore = useSystemStatsStore() + delete (systemStatsStore.systemStats!.system as { argv?: string[] }).argv + vi.mocked(releaseService.getReleases).mockResolvedValue([mockRelease]) await store.fetchReleases() - expect(mockReleaseService.getReleases).toHaveBeenCalled() + expect(releaseService.getReleases).toHaveBeenCalled() }) it('should proceed with fetchReleases when system stats are not available', async () => { - const { until } = await import('@vueuse/core') - mockSystemStatsStore.systemStats = null - mockSystemStatsStore.isInitialized = false - mockReleaseService.getReleases.mockResolvedValue([mockRelease]) + const store = useReleaseStore() + const releaseService = useReleaseService() + const systemStatsStore = useSystemStatsStore() + systemStatsStore.systemStats = null + systemStatsStore.isInitialized = false + vi.mocked(releaseService.getReleases).mockResolvedValue([mockRelease]) await store.fetchReleases() expect(until).toHaveBeenCalled() - expect(mockReleaseService.getReleases).toHaveBeenCalled() + expect(releaseService.getReleases).toHaveBeenCalled() }) }) describe('action handlers', () => { - beforeEach(async () => { - store.releases = [mockRelease] - }) - it('should handle skip release', async () => { + const store = useReleaseStore() + store.releases = [mockRelease] + const settingStore = useSettingStore() await store.handleSkipRelease('1.2.0') - expect(mockSettingStore.set).toHaveBeenCalledWith( + expect(settingStore.set).toHaveBeenCalledWith( 'Comfy.Release.Version', '1.2.0' ) - expect(mockSettingStore.set).toHaveBeenCalledWith( + expect(settingStore.set).toHaveBeenCalledWith( 'Comfy.Release.Status', 'skipped' ) - expect(mockSettingStore.set).toHaveBeenCalledWith( + expect(settingStore.set).toHaveBeenCalledWith( 'Comfy.Release.Timestamp', expect.any(Number) ) }) it('should handle show changelog', async () => { + const store = useReleaseStore() + store.releases = [mockRelease] + const settingStore = useSettingStore() await store.handleShowChangelog('1.2.0') - expect(mockSettingStore.set).toHaveBeenCalledWith( + expect(settingStore.set).toHaveBeenCalledWith( 'Comfy.Release.Version', '1.2.0' ) - expect(mockSettingStore.set).toHaveBeenCalledWith( + expect(settingStore.set).toHaveBeenCalledWith( 'Comfy.Release.Status', 'changelog seen' ) - expect(mockSettingStore.set).toHaveBeenCalledWith( + expect(settingStore.set).toHaveBeenCalledWith( 'Comfy.Release.Timestamp', expect.any(Number) ) }) it('should handle whats new seen', async () => { + const store = useReleaseStore() + store.releases = [mockRelease] + const settingStore = useSettingStore() await store.handleWhatsNewSeen('1.2.0') - expect(mockSettingStore.set).toHaveBeenCalledWith( + expect(settingStore.set).toHaveBeenCalledWith( 'Comfy.Release.Version', '1.2.0' ) - expect(mockSettingStore.set).toHaveBeenCalledWith( + expect(settingStore.set).toHaveBeenCalledWith( 'Comfy.Release.Status', "what's new seen" ) - expect(mockSettingStore.set).toHaveBeenCalledWith( + expect(settingStore.set).toHaveBeenCalledWith( 'Comfy.Release.Timestamp', expect.any(Number) ) @@ -519,7 +589,9 @@ describe('useReleaseStore', () => { describe('popup visibility', () => { it('should show toast for medium/high attention releases', () => { - mockSettingStore.get.mockImplementation((key: string) => { + const store = useReleaseStore() + const settingStore = useSettingStore() + vi.mocked(settingStore.get).mockImplementation((key: string) => { if (key === 'Comfy.Release.Version') return null if (key === 'Comfy.Release.Status') return null if (key === 'Comfy.Notification.ShowVersionUpdates') return true @@ -534,8 +606,10 @@ describe('useReleaseStore', () => { }) it('should show red dot for new versions', () => { + const store = useReleaseStore() + const settingStore = useSettingStore() vi.mocked(compare).mockReturnValue(1) - mockSettingStore.get.mockImplementation((key: string) => { + vi.mocked(settingStore.get).mockImplementation((key: string) => { if (key === 'Comfy.Notification.ShowVersionUpdates') return true return null }) @@ -546,8 +620,11 @@ describe('useReleaseStore', () => { }) it('should show popup for latest version', () => { - mockSystemStatsStore.systemStats!.system.comfyui_version = '1.2.0' // Same as release - mockSettingStore.get.mockImplementation((key: string) => { + const store = useReleaseStore() + const systemStatsStore = useSystemStatsStore() + const settingStore = useSettingStore() + systemStatsStore.systemStats!.system.comfyui_version = '1.2.0' // Same as release + vi.mocked(settingStore.get).mockImplementation((key: string) => { if (key === 'Comfy.Notification.ShowVersionUpdates') return true return null }) @@ -562,8 +639,11 @@ describe('useReleaseStore', () => { describe('edge cases', () => { it('should handle missing system stats gracefully', async () => { - mockSystemStatsStore.systemStats = null - mockSettingStore.get.mockImplementation((key: string) => { + const store = useReleaseStore() + const systemStatsStore = useSystemStatsStore() + const settingStore = useSettingStore() + systemStatsStore.systemStats = null + vi.mocked(settingStore.get).mockImplementation((key: string) => { if (key === 'Comfy.Notification.ShowVersionUpdates') return false return null }) @@ -571,11 +651,13 @@ describe('useReleaseStore', () => { await store.initialize() // Should not fetch system stats when notifications disabled - expect(mockSystemStatsStore.refetchSystemStats).not.toHaveBeenCalled() + expect(systemStatsStore.refetchSystemStats).not.toHaveBeenCalled() }) it('should handle concurrent fetchReleases calls', async () => { - mockReleaseService.getReleases.mockImplementation( + const store = useReleaseStore() + const releaseService = useReleaseService() + vi.mocked(releaseService.getReleases).mockImplementation( () => new Promise((resolve) => setTimeout(() => resolve([mockRelease]), 100) @@ -589,41 +671,37 @@ describe('useReleaseStore', () => { await Promise.all([promise1, promise2]) // Should only call API once due to loading check - expect(mockReleaseService.getReleases).toHaveBeenCalledTimes(1) + expect(releaseService.getReleases).toHaveBeenCalledTimes(1) }) }) describe('isElectron environment checks', () => { - beforeEach(async () => { - // Set up a new version available - store.releases = [mockRelease] - mockSettingStore.get.mockImplementation((key: string) => { - if (key === 'Comfy.Notification.ShowVersionUpdates') return true - return null - }) - }) - describe('when running in Electron (desktop)', () => { - beforeEach(async () => { - const { isElectron } = await import('@/utils/envUtil') + beforeEach(() => { vi.mocked(isElectron).mockReturnValue(true) }) it('should show toast when conditions are met', () => { - vi.mocked(compare).mockReturnValue(1) + const store = useReleaseStore() store.releases = [mockRelease] + vi.mocked(compare).mockReturnValue(1) expect(store.shouldShowToast).toBe(true) }) it('should show red dot when new version available', () => { + const store = useReleaseStore() + store.releases = [mockRelease] vi.mocked(compare).mockReturnValue(1) expect(store.shouldShowRedDot).toBe(true) }) it('should show popup for latest version', () => { - mockSystemStatsStore.systemStats!.system.comfyui_version = '1.2.0' + const store = useReleaseStore() + store.releases = [mockRelease] + const systemStatsStore = useSystemStatsStore() + systemStatsStore.systemStats!.system.comfyui_version = '1.2.0' vi.mocked(compare).mockReturnValue(0) @@ -632,12 +710,12 @@ describe('useReleaseStore', () => { }) describe('when NOT running in Electron (web)', () => { - beforeEach(async () => { - const { isElectron } = await import('@/utils/envUtil') + beforeEach(() => { vi.mocked(isElectron).mockReturnValue(false) }) it('should NOT show toast even when all other conditions are met', () => { + const store = useReleaseStore() vi.mocked(compare).mockReturnValue(1) // Set up all conditions that would normally show toast @@ -647,12 +725,15 @@ describe('useReleaseStore', () => { }) it('should NOT show red dot even when new version available', () => { + const store = useReleaseStore() + store.releases = [mockRelease] vi.mocked(compare).mockReturnValue(1) expect(store.shouldShowRedDot).toBe(false) }) it('should NOT show toast regardless of attention level', () => { + const store = useReleaseStore() vi.mocked(compare).mockReturnValue(1) // Test with high attention releases @@ -672,6 +753,7 @@ describe('useReleaseStore', () => { }) it('should NOT show red dot even with high attention release', () => { + const store = useReleaseStore() vi.mocked(compare).mockReturnValue(1) store.releases = [{ ...mockRelease, attention: 'high' as const }] @@ -680,7 +762,10 @@ describe('useReleaseStore', () => { }) it('should NOT show popup even for latest version', () => { - mockSystemStatsStore.systemStats!.system.comfyui_version = '1.2.0' + const store = useReleaseStore() + store.releases = [mockRelease] + const systemStatsStore = useSystemStatsStore() + systemStatsStore.systemStats!.system.comfyui_version = '1.2.0' vi.mocked(compare).mockReturnValue(0) diff --git a/src/platform/updates/common/versionCompatibilityStore.test.ts b/src/platform/updates/common/versionCompatibilityStore.test.ts index 78aa87c74..3fdb80eaa 100644 --- a/src/platform/updates/common/versionCompatibilityStore.test.ts +++ b/src/platform/updates/common/versionCompatibilityStore.test.ts @@ -1,3 +1,4 @@ +import { until } from '@vueuse/core' import { createPinia, setActivePinia } from 'pinia' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { ref } from 'vue' @@ -357,17 +358,15 @@ describe('useVersionCompatibilityStore', () => { describe('initialization', () => { it('should fetch system stats if not available', async () => { - const { until } = await import('@vueuse/core') mockSystemStatsStore.systemStats = null mockSystemStatsStore.isInitialized = false await store.initialize() - expect(until).toHaveBeenCalled() + expect(vi.mocked(until)).toHaveBeenCalled() }) it('should not fetch system stats if already available', async () => { - const { until } = await import('@vueuse/core') mockSystemStatsStore.systemStats = { system: { comfyui_version: '1.24.0', @@ -378,7 +377,7 @@ describe('useVersionCompatibilityStore', () => { await store.initialize() - expect(until).not.toHaveBeenCalled() + expect(vi.mocked(until)).not.toHaveBeenCalled() }) }) }) diff --git a/src/renderer/core/thumbnail/useWorkflowThumbnail.test.ts b/src/renderer/core/thumbnail/useWorkflowThumbnail.test.ts index 39f8de56a..4f7ea6ef6 100644 --- a/src/renderer/core/thumbnail/useWorkflowThumbnail.test.ts +++ b/src/renderer/core/thumbnail/useWorkflowThumbnail.test.ts @@ -3,6 +3,9 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore' import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore' +import { createGraphThumbnail } from '@/renderer/core/thumbnail/graphThumbnailRenderer' +import { useWorkflowThumbnail } from '@/renderer/core/thumbnail/useWorkflowThumbnail' +import { api } from '@/scripts/api' vi.mock('@/renderer/core/thumbnail/graphThumbnailRenderer', () => ({ createGraphThumbnail: vi.fn() @@ -19,12 +22,6 @@ vi.mock('@/scripts/api', () => ({ } })) -const { useWorkflowThumbnail } = - await import('@/renderer/core/thumbnail/useWorkflowThumbnail') -const { createGraphThumbnail } = - await import('@/renderer/core/thumbnail/graphThumbnailRenderer') -const { api } = await import('@/scripts/api') - describe('useWorkflowThumbnail', () => { let workflowStore: ReturnType diff --git a/src/renderer/extensions/minimap/composables/useMinimap.test.ts b/src/renderer/extensions/minimap/composables/useMinimap.test.ts index 1ab52999c..db050ad1f 100644 --- a/src/renderer/extensions/minimap/composables/useMinimap.test.ts +++ b/src/renderer/extensions/minimap/composables/useMinimap.test.ts @@ -200,9 +200,8 @@ vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({ })) })) -const { useMinimap } = - await import('@/renderer/extensions/minimap/composables/useMinimap') -const { api } = await import('@/scripts/api') +import { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap' +import { api } from '@/scripts/api' describe('useMinimap', () => { let moduleMockCanvasElement: HTMLCanvasElement diff --git a/src/renderer/extensions/minimap/composables/useMinimapRenderer.test.ts b/src/renderer/extensions/minimap/composables/useMinimapRenderer.test.ts index a2de2a759..fbdff5142 100644 --- a/src/renderer/extensions/minimap/composables/useMinimapRenderer.test.ts +++ b/src/renderer/extensions/minimap/composables/useMinimapRenderer.test.ts @@ -103,9 +103,7 @@ describe('useMinimapRenderer', () => { expect(vi.mocked(renderMinimapToCanvas)).not.toHaveBeenCalled() }) - it('should only render when redraw is needed', async () => { - const { renderMinimapToCanvas } = - await import('@/renderer/extensions/minimap/minimapCanvasRenderer') + it('should only render when redraw is needed', () => { const canvasRef = ref(mockCanvas) const graphRef = ref(mockGraph) as Ref const boundsRef = ref({ minX: 0, minY: 0, width: 100, height: 100 }) diff --git a/src/renderer/extensions/minimap/composables/useMinimapViewport.test.ts b/src/renderer/extensions/minimap/composables/useMinimapViewport.test.ts index 76f0ca710..c2416edc3 100644 --- a/src/renderer/extensions/minimap/composables/useMinimapViewport.test.ts +++ b/src/renderer/extensions/minimap/composables/useMinimapViewport.test.ts @@ -4,6 +4,11 @@ import { ref } from 'vue' import type { Ref } from 'vue' import type { LGraph } from '@/lib/litegraph/src/litegraph' +import { + calculateMinimapScale, + calculateNodeBounds, + enforceMinimumBounds +} from '@/renderer/core/spatial/boundsCalculator' import { useMinimapViewport } from '@/renderer/extensions/minimap/composables/useMinimapViewport' import type { MinimapCanvas } from '@/renderer/extensions/minimap/types' @@ -66,10 +71,7 @@ describe('useMinimapViewport', () => { expect(viewport.scale.value).toBe(1) }) - it('should calculate graph bounds from nodes', async () => { - const { calculateNodeBounds, enforceMinimumBounds } = - await import('@/renderer/core/spatial/boundsCalculator') - + it('should calculate graph bounds from nodes', () => { vi.mocked(calculateNodeBounds).mockReturnValue({ minX: 100, minY: 100, @@ -92,10 +94,7 @@ describe('useMinimapViewport', () => { expect(enforceMinimumBounds).toHaveBeenCalled() }) - it('should handle empty graph', async () => { - const { calculateNodeBounds } = - await import('@/renderer/core/spatial/boundsCalculator') - + it('should handle empty graph', () => { vi.mocked(calculateNodeBounds).mockReturnValue(null) const canvasRef = ref(mockCanvas) as Ref @@ -131,11 +130,7 @@ describe('useMinimapViewport', () => { }) }) - it('should calculate viewport transform', async () => { - const { calculateNodeBounds, enforceMinimumBounds, calculateMinimapScale } = - await import('@/renderer/core/spatial/boundsCalculator') - - // Mock the bounds calculation + it('should calculate viewport transform', () => { vi.mocked(calculateNodeBounds).mockReturnValue({ minX: 0, minY: 0, @@ -236,10 +231,7 @@ describe('useMinimapViewport', () => { expect(() => viewport.centerViewOn(100, 100)).not.toThrow() }) - it('should calculate scale correctly', async () => { - const { calculateMinimapScale, calculateNodeBounds, enforceMinimumBounds } = - await import('@/renderer/core/spatial/boundsCalculator') - + it('should calculate scale correctly', () => { const testBounds = { minX: 0, minY: 0, diff --git a/src/renderer/extensions/vueNodes/widgets/composables/useRemoteWidget.test.ts b/src/renderer/extensions/vueNodes/widgets/composables/useRemoteWidget.test.ts index ef6d15359..599a3bfab 100644 --- a/src/renderer/extensions/vueNodes/widgets/composables/useRemoteWidget.test.ts +++ b/src/renderer/extensions/vueNodes/widgets/composables/useRemoteWidget.test.ts @@ -2,6 +2,7 @@ import axios from 'axios' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { IWidget } from '@/lib/litegraph/src/litegraph' +import { api } from '@/scripts/api' import { LGraphNode } from '@/lib/litegraph/src/litegraph' import { useRemoteWidget } from '@/renderer/extensions/vueNodes/widgets/composables/useRemoteWidget' import type { RemoteWidgetConfig } from '@/schemas/nodeDefSchema' @@ -610,7 +611,6 @@ describe('useRemoteWidget', () => { }) it('should register event listener when enabled', async () => { - const { api } = await import('@/scripts/api') const addEventListenerSpy = vi.spyOn(api, 'addEventListener') const mockNode = { @@ -636,7 +636,6 @@ describe('useRemoteWidget', () => { }) it('should refresh widget when workflow completes successfully', async () => { - const { api } = await import('@/scripts/api') let executionSuccessHandler: (() => void) | undefined vi.spyOn(api, 'addEventListener').mockImplementation((event, handler) => { @@ -674,7 +673,6 @@ describe('useRemoteWidget', () => { }) it('should not refresh when toggle is disabled', async () => { - const { api } = await import('@/scripts/api') let executionSuccessHandler: (() => void) | undefined vi.spyOn(api, 'addEventListener').mockImplementation((event, handler) => { @@ -707,7 +705,6 @@ describe('useRemoteWidget', () => { }) it('should cleanup event listener on node removal', async () => { - const { api } = await import('@/scripts/api') let executionSuccessHandler: (() => void) | undefined vi.spyOn(api, 'addEventListener').mockImplementation((event, handler) => { diff --git a/src/services/jobOutputCache.test.ts b/src/services/jobOutputCache.test.ts index 38e47e973..99fbd22f1 100644 --- a/src/services/jobOutputCache.test.ts +++ b/src/services/jobOutputCache.test.ts @@ -1,9 +1,17 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' +import { extractWorkflow } from '@/platform/remote/comfyui/jobs/fetchJobs' +import { api } from '@/scripts/api' import type { JobDetail, JobListItem } from '@/platform/remote/comfyui/jobs/jobTypes' +import { + findActiveIndex, + getJobDetail, + getJobWorkflow, + getOutputsForTask +} from '@/services/jobOutputCache' import { ResultItemImpl, TaskItemImpl } from '@/stores/queueStore' vi.mock('@/platform/remote/comfyui/jobs/fetchJobs', () => ({ @@ -11,6 +19,15 @@ vi.mock('@/platform/remote/comfyui/jobs/fetchJobs', () => ({ extractWorkflow: vi.fn() })) +vi.mock('@/scripts/api', () => ({ + api: { + getJobDetail: vi.fn(), + apiURL: vi.fn((path: string) => `/api${path}`), + addEventListener: vi.fn(), + removeEventListener: vi.fn() + } +})) + function createResultItem(url: string, supportsPreview = true): ResultItemImpl { const item = new ResultItemImpl({ filename: url, @@ -48,15 +65,19 @@ function createTask( return new TaskItemImpl(job, {}, flatOutputs) } +// Generate unique IDs per test to avoid cache collisions +let testCounter = 0 +function uniqueId(prefix: string): string { + return `${prefix}-${++testCounter}-${Date.now()}` +} + describe('jobOutputCache', () => { beforeEach(() => { - vi.resetModules() vi.clearAllMocks() }) describe('findActiveIndex', () => { - it('returns index of matching URL', async () => { - const { findActiveIndex } = await import('@/services/jobOutputCache') + it('returns index of matching URL', () => { const items = [ createResultItem('a'), createResultItem('b'), @@ -66,15 +87,13 @@ describe('jobOutputCache', () => { expect(findActiveIndex(items, 'b')).toBe(1) }) - it('returns 0 when URL not found', async () => { - const { findActiveIndex } = await import('@/services/jobOutputCache') + it('returns 0 when URL not found', () => { const items = [createResultItem('a'), createResultItem('b')] expect(findActiveIndex(items, 'missing')).toBe(0) }) - it('returns 0 when URL is undefined', async () => { - const { findActiveIndex } = await import('@/services/jobOutputCache') + it('returns 0 when URL is undefined', () => { const items = [createResultItem('a'), createResultItem('b')] expect(findActiveIndex(items, undefined)).toBe(0) @@ -83,7 +102,6 @@ describe('jobOutputCache', () => { describe('getOutputsForTask', () => { it('returns previewable outputs directly when no lazy load needed', async () => { - const { getOutputsForTask } = await import('@/services/jobOutputCache') const outputs = [createResultItem('p-1'), createResultItem('p-2')] const task = createTask(undefined, outputs, 1) @@ -93,14 +111,13 @@ describe('jobOutputCache', () => { }) it('lazy loads when outputsCount > 1', async () => { - const { getOutputsForTask } = await import('@/services/jobOutputCache') const previewOutput = createResultItem('preview') const fullOutputs = [ createResultItem('full-1'), createResultItem('full-2') ] - const job = createMockJob('task-1', 3) + const job = createMockJob(uniqueId('task'), 3) const task = new TaskItemImpl(job, {}, [previewOutput]) const loadedTask = new TaskItemImpl(job, {}, fullOutputs) task.loadFullOutputs = vi.fn().mockResolvedValue(loadedTask) @@ -112,10 +129,9 @@ describe('jobOutputCache', () => { }) it('caches loaded tasks', async () => { - const { getOutputsForTask } = await import('@/services/jobOutputCache') const fullOutputs = [createResultItem('full-1')] - const job = createMockJob('task-1', 3) + const job = createMockJob(uniqueId('task'), 3) const task = new TaskItemImpl(job, {}, [createResultItem('preview')]) const loadedTask = new TaskItemImpl(job, {}, fullOutputs) task.loadFullOutputs = vi.fn().mockResolvedValue(loadedTask) @@ -130,10 +146,9 @@ describe('jobOutputCache', () => { }) it('falls back to preview outputs on load error', async () => { - const { getOutputsForTask } = await import('@/services/jobOutputCache') const previewOutput = createResultItem('preview') - const job = createMockJob('task-1', 3) + const job = createMockJob(uniqueId('task'), 3) const task = new TaskItemImpl(job, {}, [previewOutput]) task.loadFullOutputs = vi .fn() @@ -145,9 +160,8 @@ describe('jobOutputCache', () => { }) it('returns null when request is superseded', async () => { - const { getOutputsForTask } = await import('@/services/jobOutputCache') - const job1 = createMockJob('task-1', 3) - const job2 = createMockJob('task-2', 3) + const job1 = createMockJob(uniqueId('task'), 3) + const job2 = createMockJob(uniqueId('task'), 3) const task1 = new TaskItemImpl(job1, {}, [createResultItem('preview-1')]) const task2 = new TaskItemImpl(job2, {}, [createResultItem('preview-2')]) @@ -182,57 +196,51 @@ describe('jobOutputCache', () => { describe('getJobDetail', () => { it('fetches and caches job detail', async () => { - const { getJobDetail } = await import('@/services/jobOutputCache') - const { fetchJobDetail } = - await import('@/platform/remote/comfyui/jobs/fetchJobs') + const jobId = uniqueId('job') const mockDetail: JobDetail = { - id: 'job-1', + id: jobId, status: 'completed', create_time: Date.now(), priority: 0, outputs: {} } - vi.mocked(fetchJobDetail).mockResolvedValue(mockDetail) + vi.mocked(api.getJobDetail).mockResolvedValue(mockDetail) - const result = await getJobDetail('job-1') + const result = await getJobDetail(jobId) expect(result).toEqual(mockDetail) - expect(fetchJobDetail).toHaveBeenCalledWith(expect.any(Function), 'job-1') + expect(api.getJobDetail).toHaveBeenCalledWith(jobId) }) it('returns cached job detail on subsequent calls', async () => { - const { getJobDetail } = await import('@/services/jobOutputCache') - const { fetchJobDetail } = - await import('@/platform/remote/comfyui/jobs/fetchJobs') + const jobId = uniqueId('job') const mockDetail: JobDetail = { - id: 'job-2', + id: jobId, status: 'completed', create_time: Date.now(), priority: 0, outputs: {} } - vi.mocked(fetchJobDetail).mockResolvedValue(mockDetail) + vi.mocked(api.getJobDetail).mockResolvedValue(mockDetail) // First call - await getJobDetail('job-2') - expect(fetchJobDetail).toHaveBeenCalledTimes(1) + await getJobDetail(jobId) + expect(api.getJobDetail).toHaveBeenCalledTimes(1) // Second call should use cache - const result = await getJobDetail('job-2') + const result = await getJobDetail(jobId) expect(result).toEqual(mockDetail) - expect(fetchJobDetail).toHaveBeenCalledTimes(1) + expect(api.getJobDetail).toHaveBeenCalledTimes(1) }) it('returns undefined on fetch error', async () => { - const { getJobDetail } = await import('@/services/jobOutputCache') - const { fetchJobDetail } = - await import('@/platform/remote/comfyui/jobs/fetchJobs') + const jobId = uniqueId('job-error') - vi.mocked(fetchJobDetail).mockRejectedValue(new Error('Network error')) + vi.mocked(api.getJobDetail).mockRejectedValue(new Error('Network error')) - const result = await getJobDetail('job-error') + const result = await getJobDetail(jobId) expect(result).toBeUndefined() }) @@ -240,12 +248,10 @@ describe('jobOutputCache', () => { describe('getJobWorkflow', () => { it('fetches job detail and extracts workflow', async () => { - const { getJobWorkflow } = await import('@/services/jobOutputCache') - const { fetchJobDetail, extractWorkflow } = - await import('@/platform/remote/comfyui/jobs/fetchJobs') + const jobId = uniqueId('job-wf') const mockDetail: JobDetail = { - id: 'job-wf', + id: jobId, status: 'completed', create_time: Date.now(), priority: 0, @@ -253,24 +259,22 @@ describe('jobOutputCache', () => { } const mockWorkflow = { version: 1 } - vi.mocked(fetchJobDetail).mockResolvedValue(mockDetail) + vi.mocked(api.getJobDetail).mockResolvedValue(mockDetail) vi.mocked(extractWorkflow).mockResolvedValue(mockWorkflow as any) - const result = await getJobWorkflow('job-wf') + const result = await getJobWorkflow(jobId) expect(result).toEqual(mockWorkflow) expect(extractWorkflow).toHaveBeenCalledWith(mockDetail) }) it('returns undefined when job detail not found', async () => { - const { getJobWorkflow } = await import('@/services/jobOutputCache') - const { fetchJobDetail, extractWorkflow } = - await import('@/platform/remote/comfyui/jobs/fetchJobs') + const jobId = uniqueId('missing') - vi.mocked(fetchJobDetail).mockResolvedValue(undefined) + vi.mocked(api.getJobDetail).mockResolvedValue(undefined) vi.mocked(extractWorkflow).mockResolvedValue(undefined) - const result = await getJobWorkflow('missing') + const result = await getJobWorkflow(jobId) expect(result).toBeUndefined() }) diff --git a/src/services/newUserService.ts b/src/services/newUserService.ts deleted file mode 100644 index fff9eb72c..000000000 --- a/src/services/newUserService.ts +++ /dev/null @@ -1,74 +0,0 @@ -import type { useSettingStore } from '@/platform/settings/settingStore' - -let pendingCallbacks: Array<() => Promise> = [] -let isNewUserDetermined = false -let isNewUserCached: boolean | null = null - -export const newUserService = () => { - function checkIsNewUser( - settingStore: ReturnType - ): boolean { - const isNewUserSettings = - Object.keys(settingStore.settingValues).length === 0 || - !settingStore.get('Comfy.TutorialCompleted') - const hasNoWorkflow = !localStorage.getItem('workflow') - const hasNoPreviousWorkflow = !localStorage.getItem( - 'Comfy.PreviousWorkflow' - ) - - return isNewUserSettings && hasNoWorkflow && hasNoPreviousWorkflow - } - - async function registerInitCallback(callback: () => Promise) { - if (isNewUserDetermined) { - if (isNewUserCached) { - try { - await callback() - } catch (error) { - console.error('New user initialization callback failed:', error) - } - } - } else { - pendingCallbacks.push(callback) - } - } - - async function initializeIfNewUser( - settingStore: ReturnType - ) { - if (isNewUserDetermined) return - - isNewUserCached = checkIsNewUser(settingStore) - isNewUserDetermined = true - - if (!isNewUserCached) { - pendingCallbacks = [] - return - } - - await settingStore.set( - 'Comfy.InstalledVersion', - __COMFYUI_FRONTEND_VERSION__ - ) - - for (const callback of pendingCallbacks) { - try { - await callback() - } catch (error) { - console.error('New user initialization callback failed:', error) - } - } - - pendingCallbacks = [] - } - - function isNewUser(): boolean | null { - return isNewUserDetermined ? isNewUserCached : null - } - - return { - registerInitCallback, - initializeIfNewUser, - isNewUser - } -} diff --git a/src/services/newUserService.test.ts b/src/services/useNewUserService.test.ts similarity index 84% rename from src/services/newUserService.test.ts rename to src/services/useNewUserService.test.ts index 43fcc92cd..1fd590b21 100644 --- a/src/services/newUserService.test.ts +++ b/src/services/useNewUserService.test.ts @@ -7,6 +7,12 @@ const mockLocalStorage = vi.hoisted(() => ({ clear: vi.fn() })) +const mockSettingStore = vi.hoisted(() => ({ + settingValues: {} as Record, + get: vi.fn(), + set: vi.fn() +})) + Object.defineProperty(window, 'localStorage', { value: mockLocalStorage, writable: true @@ -16,31 +22,26 @@ vi.mock('@/config/version', () => ({ __COMFYUI_FRONTEND_VERSION__: '1.24.0' })) +vi.mock('@/platform/settings/settingStore', () => ({ + useSettingStore: () => mockSettingStore +})) + //@ts-expect-error Define global for the test global.__COMFYUI_FRONTEND_VERSION__ = '1.24.0' -import type { newUserService as NewUserServiceType } from '@/services/newUserService' +import { useNewUserService } from '@/services/useNewUserService' -describe('newUserService', () => { - let service: ReturnType - let mockSettingStore: any - let newUserService: typeof NewUserServiceType +describe('useNewUserService', () => { + let service: ReturnType - beforeEach(async () => { + beforeEach(() => { vi.clearAllMocks() + mockSettingStore.settingValues = {} + mockSettingStore.get.mockReset() + mockSettingStore.set.mockReset() - vi.resetModules() - - const module = await import('@/services/newUserService') - newUserService = module.newUserService - - service = newUserService() - - mockSettingStore = { - settingValues: {}, - get: vi.fn(), - set: vi.fn() - } + service = useNewUserService() + service.reset() mockLocalStorage.getItem.mockReturnValue(null) }) @@ -54,7 +55,7 @@ describe('newUserService', () => { }) mockLocalStorage.getItem.mockReturnValue(null) - await service.initializeIfNewUser(mockSettingStore) + await service.initializeIfNewUser() expect(service.isNewUser()).toBe(true) }) @@ -69,7 +70,7 @@ describe('newUserService', () => { mockLocalStorage.getItem.mockReturnValue(null) - await service.initializeIfNewUser(mockSettingStore) + await service.initializeIfNewUser() expect(service.isNewUser()).toBe(true) }) @@ -82,7 +83,7 @@ describe('newUserService', () => { }) mockLocalStorage.getItem.mockReturnValue(null) - await service.initializeIfNewUser(mockSettingStore) + await service.initializeIfNewUser() expect(service.isNewUser()).toBe(false) }) @@ -98,7 +99,7 @@ describe('newUserService', () => { return null }) - await service.initializeIfNewUser(mockSettingStore) + await service.initializeIfNewUser() expect(service.isNewUser()).toBe(false) }) @@ -114,7 +115,7 @@ describe('newUserService', () => { return null }) - await service.initializeIfNewUser(mockSettingStore) + await service.initializeIfNewUser() expect(service.isNewUser()).toBe(false) }) @@ -127,7 +128,7 @@ describe('newUserService', () => { }) mockLocalStorage.getItem.mockReturnValue(null) - await service.initializeIfNewUser(mockSettingStore) + await service.initializeIfNewUser() expect(service.isNewUser()).toBe(true) }) @@ -143,7 +144,7 @@ describe('newUserService', () => { }) mockLocalStorage.getItem.mockReturnValue(null) - await service.initializeIfNewUser(mockSettingStore) + await service.initializeIfNewUser() expect(service.isNewUser()).toBe(false) }) @@ -160,7 +161,7 @@ describe('newUserService', () => { return null }) - await service.initializeIfNewUser(mockSettingStore) + await service.initializeIfNewUser() expect(service.isNewUser()).toBe(false) }) @@ -177,7 +178,7 @@ describe('newUserService', () => { }) mockLocalStorage.getItem.mockReturnValue(null) - await service.initializeIfNewUser(mockSettingStore) + await service.initializeIfNewUser() expect(service.isNewUser()).toBe(true) await service.registerInitCallback(mockCallback) @@ -207,7 +208,7 @@ describe('newUserService', () => { }) mockLocalStorage.getItem.mockReturnValue(null) - await service.initializeIfNewUser(mockSettingStore) + await service.initializeIfNewUser() await service.registerInitCallback(mockCallback) @@ -228,7 +229,7 @@ describe('newUserService', () => { }) mockLocalStorage.getItem.mockReturnValue(null) - await service.initializeIfNewUser(mockSettingStore) + await service.initializeIfNewUser() expect(mockSettingStore.set).toHaveBeenCalledWith( 'Comfy.InstalledVersion', @@ -244,7 +245,7 @@ describe('newUserService', () => { }) mockLocalStorage.getItem.mockReturnValue(null) - await service.initializeIfNewUser(mockSettingStore) + await service.initializeIfNewUser() expect(mockSettingStore.set).not.toHaveBeenCalled() }) @@ -263,7 +264,7 @@ describe('newUserService', () => { }) mockLocalStorage.getItem.mockReturnValue(null) - await service.initializeIfNewUser(mockSettingStore) + await service.initializeIfNewUser() expect(mockCallback1).toHaveBeenCalledTimes(1) expect(mockCallback2).toHaveBeenCalledTimes(1) @@ -281,7 +282,7 @@ describe('newUserService', () => { }) mockLocalStorage.getItem.mockReturnValue(null) - await service.initializeIfNewUser(mockSettingStore) + await service.initializeIfNewUser() expect(mockCallback).not.toHaveBeenCalled() }) @@ -299,7 +300,7 @@ describe('newUserService', () => { }) mockLocalStorage.getItem.mockReturnValue(null) - await service.initializeIfNewUser(mockSettingStore) + await service.initializeIfNewUser() expect(consoleSpy).toHaveBeenCalledWith( 'New user initialization callback failed:', @@ -316,10 +317,10 @@ describe('newUserService', () => { }) mockLocalStorage.getItem.mockReturnValue(null) - await service.initializeIfNewUser(mockSettingStore) + await service.initializeIfNewUser() expect(mockSettingStore.set).toHaveBeenCalledTimes(1) - await service.initializeIfNewUser(mockSettingStore) + await service.initializeIfNewUser() expect(mockSettingStore.set).toHaveBeenCalledTimes(1) }) @@ -331,15 +332,12 @@ describe('newUserService', () => { }) mockLocalStorage.getItem.mockReturnValue(null) - // Before initialization, isNewUser should return null expect(service.isNewUser()).toBeNull() - await service.initializeIfNewUser(mockSettingStore) + await service.initializeIfNewUser() - // After initialization, isNewUser should return true for a new user expect(service.isNewUser()).toBe(true) - // Should set the installed version for new users expect(mockSettingStore.set).toHaveBeenCalledWith( 'Comfy.InstalledVersion', expect.any(String) @@ -357,7 +355,7 @@ describe('newUserService', () => { mockSettingStore.get.mockReturnValue(undefined) mockLocalStorage.getItem.mockReturnValue(null) - await service.initializeIfNewUser(mockSettingStore) + await service.initializeIfNewUser() expect(service.isNewUser()).toBe(true) }) @@ -372,7 +370,7 @@ describe('newUserService', () => { }) mockLocalStorage.getItem.mockReturnValue(null) - await service.initializeIfNewUser(mockSettingStore) + await service.initializeIfNewUser() expect(service.isNewUser()).toBe(true) }) @@ -388,7 +386,7 @@ describe('newUserService', () => { }) mockLocalStorage.getItem.mockReturnValue(null) - await service.initializeIfNewUser(mockSettingStore) + await service.initializeIfNewUser() await service.registerInitCallback(mockCallback1) await service.registerInitCallback(mockCallback2) @@ -399,9 +397,9 @@ describe('newUserService', () => { }) describe('state sharing between instances', () => { - it('should share state between multiple service instances', async () => { - const service1 = newUserService() - const service2 = newUserService() + it('should share state between multiple service calls', async () => { + const service1 = useNewUserService() + const service2 = useNewUserService() mockSettingStore.settingValues = {} mockSettingStore.get.mockImplementation((key: string) => { @@ -410,15 +408,15 @@ describe('newUserService', () => { }) mockLocalStorage.getItem.mockReturnValue(null) - await service1.initializeIfNewUser(mockSettingStore) + await service1.initializeIfNewUser() expect(service2.isNewUser()).toBe(true) expect(service1.isNewUser()).toBe(service2.isNewUser()) }) - it('should execute callbacks registered on different instances', async () => { - const service1 = newUserService() - const service2 = newUserService() + it('should execute callbacks registered on different service calls', async () => { + const service1 = useNewUserService() + const service2 = useNewUserService() const mockCallback1 = vi.fn().mockResolvedValue(undefined) const mockCallback2 = vi.fn().mockResolvedValue(undefined) @@ -433,7 +431,7 @@ describe('newUserService', () => { }) mockLocalStorage.getItem.mockReturnValue(null) - await service1.initializeIfNewUser(mockSettingStore) + await service1.initializeIfNewUser() expect(mockCallback1).toHaveBeenCalledTimes(1) expect(mockCallback2).toHaveBeenCalledTimes(1) diff --git a/src/services/useNewUserService.ts b/src/services/useNewUserService.ts new file mode 100644 index 000000000..093c273bd --- /dev/null +++ b/src/services/useNewUserService.ts @@ -0,0 +1,82 @@ +import { ref, shallowRef } from 'vue' +import { createSharedComposable } from '@vueuse/core' +import { useSettingStore } from '@/platform/settings/settingStore' + +function _useNewUserService() { + const settingStore = useSettingStore() + const pendingCallbacks = shallowRef Promise>>([]) + const isNewUserDetermined = ref(false) + const isNewUserCached = ref(null) + + function reset() { + pendingCallbacks.value = [] + isNewUserDetermined.value = false + isNewUserCached.value = null + } + + function checkIsNewUser(): boolean { + const isNewUserSettings = + Object.keys(settingStore.settingValues).length === 0 || + !settingStore.get('Comfy.TutorialCompleted') + const hasNoWorkflow = !localStorage.getItem('workflow') + const hasNoPreviousWorkflow = !localStorage.getItem( + 'Comfy.PreviousWorkflow' + ) + + return isNewUserSettings && hasNoWorkflow && hasNoPreviousWorkflow + } + + async function registerInitCallback(callback: () => Promise) { + if (isNewUserDetermined.value) { + if (isNewUserCached.value) { + try { + await callback() + } catch (error) { + console.error('New user initialization callback failed:', error) + } + } + } else { + pendingCallbacks.value = [...pendingCallbacks.value, callback] + } + } + + async function initializeIfNewUser() { + if (isNewUserDetermined.value) return + + isNewUserCached.value = checkIsNewUser() + isNewUserDetermined.value = true + + if (!isNewUserCached.value) { + pendingCallbacks.value = [] + return + } + + await settingStore.set( + 'Comfy.InstalledVersion', + __COMFYUI_FRONTEND_VERSION__ + ) + + for (const callback of pendingCallbacks.value) { + try { + await callback() + } catch (error) { + console.error('New user initialization callback failed:', error) + } + } + + pendingCallbacks.value = [] + } + + function isNewUser(): boolean | null { + return isNewUserDetermined.value ? isNewUserCached.value : null + } + + return { + registerInitCallback, + initializeIfNewUser, + isNewUser, + reset + } +} + +export const useNewUserService = createSharedComposable(_useNewUserService) diff --git a/src/stores/subgraphNavigationStore.test.ts b/src/stores/subgraphNavigationStore.test.ts index d7753bb0c..e16700602 100644 --- a/src/stores/subgraphNavigationStore.test.ts +++ b/src/stores/subgraphNavigationStore.test.ts @@ -6,6 +6,7 @@ import { useWorkflowStore } from '@/platform/workflow/management/stores/workflow import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore' import { app } from '@/scripts/app' import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore' +import { findSubgraphPathById } from '@/utils/graphTraversalUtil' vi.mock('@/scripts/app', () => { const mockCanvas = { @@ -136,7 +137,6 @@ describe('useSubgraphNavigationStore', () => { it('should clear navigation when activeSubgraph becomes undefined', async () => { const navigationStore = useSubgraphNavigationStore() const workflowStore = useWorkflowStore() - const { findSubgraphPathById } = await import('@/utils/graphTraversalUtil') // Create mock subgraph and graph structure const mockSubgraph = { diff --git a/src/workbench/extensions/manager/components/manager/packCard/PackCard.test.ts b/src/workbench/extensions/manager/components/manager/packCard/PackCard.test.ts index 9cf0da2ca..29af937f5 100644 --- a/src/workbench/extensions/manager/components/manager/packCard/PackCard.test.ts +++ b/src/workbench/extensions/manager/components/manager/packCard/PackCard.test.ts @@ -1,5 +1,5 @@ +import { createTestingPinia } from '@pinia/testing' import { mount } from '@vue/test-utils' -import { createPinia, setActivePinia } from 'pinia' import ProgressSpinner from 'primevue/progressspinner' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -15,6 +15,7 @@ const translateMock = vi.hoisted(() => ) ) const dateMock = vi.hoisted(() => vi.fn(() => '2024. 1. 1.')) +const storageMap = vi.hoisted(() => new Map()) // Mock dependencies vi.mock('vue-i18n', () => ({ @@ -45,16 +46,17 @@ vi.mock('@/stores/workspace/colorPaletteStore', () => ({ })) })) -vi.mock('@vueuse/core', async () => { - const { ref } = await import('vue') - return { - whenever: vi.fn(), - useStorage: vi.fn((_key, defaultValue) => { - return ref(defaultValue) - }), - createSharedComposable: vi.fn((fn) => fn) - } -}) +vi.mock('@vueuse/core', () => ({ + whenever: vi.fn(), + useStorage: vi.fn((key: string, defaultValue: unknown) => { + if (!storageMap.has(key)) storageMap.set(key, defaultValue) + return storageMap.get(key) + }), + createSharedComposable: vi.fn((fn) => { + let cached: ReturnType + return (...args: Parameters) => (cached ??= fn(...args)) + }) +})) vi.mock('@/config', () => ({ default: { @@ -72,12 +74,9 @@ vi.mock('@/stores/systemStatsStore', () => ({ })) describe('PackCard', () => { - let pinia: ReturnType - beforeEach(() => { vi.clearAllMocks() - pinia = createPinia() - setActivePinia(pinia) + storageMap.clear() }) const createWrapper = (props: { @@ -87,7 +86,7 @@ describe('PackCard', () => { const wrapper = mount(PackCard, { props, global: { - plugins: [pinia], + plugins: [createTestingPinia({ stubActions: false })], components: { ProgressSpinner }, From cbd073f89dcdddd0f47511fcda11a42df9bc2c5a Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Wed, 28 Jan 2026 12:20:13 -0800 Subject: [PATCH 05/64] Add inline queue progress bar and text summary (#8271) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add inline queue progress bar and summary per the new designs. This adds an inline 3px progress bar in the actionbar container (docked or floating) and a compact summary line below the top menu that follows when floating, both gated by the QPO V2 flag and hidden while the overlay is expanded. https://github.com/user-attachments/assets/da8ec7b7-35f4-4d52-a83b-15c21b484eba ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8271-Add-inline-queue-progress-bar-and-summary-for-QPO-V2-2f16d73d36508132a6dff247f71e11e4) by [Unito](https://www.unito.io) --------- Co-authored-by: github-actions --- ...e-default-workflow-mobile-chrome-linux.png | Bin 30811 -> 30489 bytes ...obile-empty-canvas-mobile-chrome-linux.png | Bin 18398 -> 18062 bytes ...s-paned-with-touch-mobile-chrome-linux.png | Bin 25954 -> 25588 bytes ...e-moved-node-touch-mobile-chrome-linux.png | Bin 27485 -> 27159 bytes src/components/TopMenuSection.test.ts | 108 ++++++++- src/components/TopMenuSection.vue | 214 +++++++++++------- src/components/actionbar/ComfyActionbar.vue | 61 ++++- .../queue/QueueInlineProgress.test.ts | 75 ++++++ src/components/queue/QueueInlineProgress.vue | 36 +++ .../queue/QueueInlineProgressSummary.vue | 70 ++++++ .../rightSidePanel/RightSidePanel.vue | 11 +- .../rightSidePanel/parameters/WidgetItem.vue | 12 +- src/composables/queue/useJobList.ts | 14 +- src/locales/en/main.json | 1 + .../vueNodes/components/NodeHeader.vue | 14 +- src/stores/workspace/favoritedWidgetsStore.ts | 9 +- src/utils/nodeTitleUtil.ts | 28 +++ 17 files changed, 536 insertions(+), 117 deletions(-) create mode 100644 src/components/queue/QueueInlineProgress.test.ts create mode 100644 src/components/queue/QueueInlineProgress.vue create mode 100644 src/components/queue/QueueInlineProgressSummary.vue create mode 100644 src/utils/nodeTitleUtil.ts diff --git a/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-default-workflow-mobile-chrome-linux.png b/browser_tests/tests/mobileBaseline.spec.ts-snapshots/mobile-default-workflow-mobile-chrome-linux.png index eaa3597314bf985895acb20f4e9e77c0829be9ce..c7334eb16765674cf165cd3bbaa61e1b2c072da8 100644 GIT binary patch literal 30489 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 eKQMG$;G4l12>Ahz4hYrty!V6o0}W{ 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 Q-hn8|t3ND#U=j8|0No04f&c&j literal 18398 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
' - } + }, + ...stubs }, directives: { tooltip: () => {} @@ -91,6 +106,7 @@ function createTask(id: string, status: JobStatus): TaskItemImpl { describe('TopMenuSection', () => { beforeEach(() => { vi.resetAllMocks() + localStorage.clear() }) describe('authentication state', () => { @@ -151,7 +167,7 @@ describe('TopMenuSection', () => { vi.mocked(settingStore.get).mockImplementation((key) => key === 'Comfy.Queue.QPOV2' ? true : undefined ) - const wrapper = createWrapper(pinia) + const wrapper = createWrapper({ pinia }) await nextTick() @@ -169,7 +185,7 @@ describe('TopMenuSection', () => { vi.mocked(settingStore.get).mockImplementation((key) => key === 'Comfy.Queue.QPOV2' ? false : undefined ) - const wrapper = createWrapper(pinia) + const wrapper = createWrapper({ pinia }) const commandStore = useCommandStore(pinia) await wrapper.find('[data-testid="queue-overlay-toggle"]').trigger('click') @@ -185,7 +201,7 @@ describe('TopMenuSection', () => { vi.mocked(settingStore.get).mockImplementation((key) => key === 'Comfy.Queue.QPOV2' ? true : undefined ) - const wrapper = createWrapper(pinia) + const wrapper = createWrapper({ pinia }) const sidebarTabStore = useSidebarTabStore(pinia) await wrapper.find('[data-testid="queue-overlay-toggle"]').trigger('click') @@ -199,7 +215,7 @@ describe('TopMenuSection', () => { vi.mocked(settingStore.get).mockImplementation((key) => key === 'Comfy.Queue.QPOV2' ? true : undefined ) - const wrapper = createWrapper(pinia) + const wrapper = createWrapper({ pinia }) const sidebarTabStore = useSidebarTabStore(pinia) const toggleButton = wrapper.find('[data-testid="queue-overlay-toggle"]') @@ -210,6 +226,84 @@ describe('TopMenuSection', () => { expect(sidebarTabStore.activeSidebarTabId).toBe(null) }) + describe('inline progress summary', () => { + const configureSettings = ( + pinia: ReturnType, + qpoV2Enabled: boolean + ) => { + const settingStore = useSettingStore(pinia) + vi.mocked(settingStore.get).mockImplementation((key) => { + if (key === 'Comfy.Queue.QPOV2') return qpoV2Enabled + if (key === 'Comfy.UseNewMenu') return 'Top' + return undefined + }) + } + + it('renders inline progress summary when QPO V2 is enabled', async () => { + const pinia = createTestingPinia({ createSpy: vi.fn }) + configureSettings(pinia, true) + + const wrapper = createWrapper({ pinia }) + + await nextTick() + + expect( + wrapper.findComponent({ name: 'QueueInlineProgressSummary' }).exists() + ).toBe(true) + }) + + it('does not render inline progress summary when QPO V2 is disabled', async () => { + const pinia = createTestingPinia({ createSpy: vi.fn }) + configureSettings(pinia, false) + + const wrapper = createWrapper({ pinia }) + + await nextTick() + + expect( + wrapper.findComponent({ name: 'QueueInlineProgressSummary' }).exists() + ).toBe(false) + }) + + it('teleports inline progress summary when actionbar is floating', async () => { + localStorage.setItem('Comfy.MenuPosition.Docked', 'false') + const actionbarTarget = document.createElement('div') + document.body.appendChild(actionbarTarget) + const pinia = createTestingPinia({ createSpy: vi.fn }) + configureSettings(pinia, true) + const executionStore = useExecutionStore(pinia) + executionStore.activePromptId = 'prompt-1' + + const ComfyActionbarStub = defineComponent({ + name: 'ComfyActionbar', + setup(_, { emit }) { + onMounted(() => { + emit('update:progressTarget', actionbarTarget) + }) + return () => h('div') + } + }) + + const wrapper = createWrapper({ + pinia, + attachTo: document.body, + stubs: { + ComfyActionbar: ComfyActionbarStub, + QueueInlineProgressSummary: false + } + }) + + try { + await nextTick() + + expect(actionbarTarget.querySelector('[role="status"]')).not.toBeNull() + } finally { + wrapper.unmount() + actionbarTarget.remove() + } + }) + }) + it('disables the clear queue context menu item when no queued jobs exist', () => { const wrapper = createWrapper() const menu = wrapper.findComponent({ name: 'ContextMenu' }) diff --git a/src/components/TopMenuSection.vue b/src/components/TopMenuSection.vue index 05149589c..a76d94f46 100644 --- a/src/components/TopMenuSection.vue +++ b/src/components/TopMenuSection.vue @@ -1,101 +1,130 @@ diff --git a/src/components/queue/QueueInlineProgressSummary.vue b/src/components/queue/QueueInlineProgressSummary.vue new file mode 100644 index 000000000..758c5d316 --- /dev/null +++ b/src/components/queue/QueueInlineProgressSummary.vue @@ -0,0 +1,70 @@ + + + diff --git a/src/components/rightSidePanel/RightSidePanel.vue b/src/components/rightSidePanel/RightSidePanel.vue index b23346bf4..1ba514f66 100644 --- a/src/components/rightSidePanel/RightSidePanel.vue +++ b/src/components/rightSidePanel/RightSidePanel.vue @@ -8,12 +8,14 @@ import Tab from '@/components/tab/Tab.vue' import TabList from '@/components/tab/TabList.vue' import Button from '@/components/ui/button/Button.vue' import { useGraphHierarchy } from '@/composables/graph/useGraphHierarchy' +import { st } from '@/i18n' import { SubgraphNode } from '@/lib/litegraph/src/litegraph' import type { LGraphNode } from '@/lib/litegraph/src/litegraph' import { useSettingStore } from '@/platform/settings/settingStore' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore' import type { RightSidePanelTab } from '@/stores/workspace/rightSidePanelStore' +import { resolveNodeDisplayName } from '@/utils/nodeTitleUtil' import { cn } from '@/utils/tailwindUtil' import TabInfo from './info/TabInfo.vue' @@ -146,9 +148,12 @@ function resolveTitle() { return groups[0].title || t('rightSidePanel.fallbackGroupTitle') } if (nodes.length === 1) { - return ( - nodes[0].title || nodes[0].type || t('rightSidePanel.fallbackNodeTitle') - ) + const fallbackNodeTitle = t('rightSidePanel.fallbackNodeTitle') + return resolveNodeDisplayName(nodes[0], { + emptyLabel: fallbackNodeTitle, + untitledLabel: fallbackNodeTitle, + st + }) } } return t('rightSidePanel.title', { count: items.length }) diff --git a/src/components/rightSidePanel/parameters/WidgetItem.vue b/src/components/rightSidePanel/parameters/WidgetItem.vue index 6a2eca748..4cd05636a 100644 --- a/src/components/rightSidePanel/parameters/WidgetItem.vue +++ b/src/components/rightSidePanel/parameters/WidgetItem.vue @@ -1,9 +1,11 @@
@@ -237,7 +237,7 @@ defineExpose({ runButtonClick }) :node-data :class=" cn( - 'py-3 gap-y-3 **:[.col-span-2]:grid-cols-1 not-has-[textarea]:flex-0 rounded-lg', + 'py-3 gap-y-3 **:[.col-span-2]:grid-cols-1 *:has-[textarea]:h-50 rounded-lg', nodeData.hasErrors && 'ring-2 ring-inset ring-node-stroke-error' ) diff --git a/src/renderer/extensions/linearMode/MobileMenu.vue b/src/renderer/extensions/linearMode/MobileMenu.vue new file mode 100644 index 000000000..93abd3bb9 --- /dev/null +++ b/src/renderer/extensions/linearMode/MobileMenu.vue @@ -0,0 +1,58 @@ + + diff --git a/src/renderer/extensions/linearMode/OutputHistory.vue b/src/renderer/extensions/linearMode/OutputHistory.vue index 4ef2bfa84..1d29a8274 100644 --- a/src/renderer/extensions/linearMode/OutputHistory.vue +++ b/src/renderer/extensions/linearMode/OutputHistory.vue @@ -156,7 +156,11 @@ watch([selectedIndex, selectedOutput], doEmit) watch( () => outputs.media.value, (newAssets, oldAssets) => { - if (newAssets.length === oldAssets.length || oldAssets.length === 0) return + if ( + newAssets.length === oldAssets.length || + (oldAssets.length === 0 && newAssets.length !== 1) + ) + return if (selectedIndex.value[0] <= 0) { selectedIndex.value = [0, 0] return diff --git a/src/views/LinearView.vue b/src/views/LinearView.vue index 3e05c6736..69deabeca 100644 --- a/src/views/LinearView.vue +++ b/src/views/LinearView.vue @@ -9,6 +9,7 @@ import Splitter from 'primevue/splitter' import SplitterPanel from 'primevue/splitterpanel' import { ref, useTemplateRef } from 'vue' +import ModeToggle from '@/components/sidebar/ModeToggle.vue' import TopbarBadges from '@/components/topbar/TopbarBadges.vue' import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue' import TypeformPopoverButton from '@/components/ui/TypeformPopoverButton.vue' @@ -17,6 +18,7 @@ import type { AssetItem } from '@/platform/assets/schemas/assetSchema' import { useSettingStore } from '@/platform/settings/settingStore' import LinearControls from '@/renderer/extensions/linearMode/LinearControls.vue' import LinearPreview from '@/renderer/extensions/linearMode/LinearPreview.vue' +import MobileMenu from '@/renderer/extensions/linearMode/MobileMenu.vue' import OutputHistory from '@/renderer/extensions/linearMode/OutputHistory.vue' import { useNodeOutputStore } from '@/stores/imagePreviewStore' import type { ResultItemImpl } from '@/stores/queueStore' @@ -58,6 +60,7 @@ const linearWorkflowRef = useTemplateRef('linearWorkflowRef') v-if="mobileDisplay" class="justify-center border-border-subtle border-t overflow-y-scroll h-[calc(100%-38px)] bg-comfy-menu-bg" > +
-
- - +
+
+ +
+
+
Date: Wed, 28 Jan 2026 17:55:12 -0800 Subject: [PATCH 08/64] Add color picker widget using native HTML5 input element (#8384) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Adds support for the color picker widget when using Litegraph nodes, it is already supported in Nodes 2.0 ## Changes - **What**: Add custom drawing of color picker widget using HTML 5 native color input element - This enables us to add a core node using the COLOR type that works on both legacy and Nodes 2.0 ## Screenshots (if applicable) Chrome Windows: image Firefox Windows: image Nodes 2.0 (unchanged): image ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8384-Add-color-picker-widget-using-native-HTML5-input-element-2f76d73d365081c69fe2f39f01fff539) by [Unito](https://www.unito.io) --- .../litegraph/src/widgets/ColorWidget.test.ts | 261 ++++++++++++++++++ src/lib/litegraph/src/widgets/ColorWidget.ts | 90 ++++-- 2 files changed, 325 insertions(+), 26 deletions(-) create mode 100644 src/lib/litegraph/src/widgets/ColorWidget.test.ts diff --git a/src/lib/litegraph/src/widgets/ColorWidget.test.ts b/src/lib/litegraph/src/widgets/ColorWidget.test.ts new file mode 100644 index 000000000..f06d059a0 --- /dev/null +++ b/src/lib/litegraph/src/widgets/ColorWidget.test.ts @@ -0,0 +1,261 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas' +import type { LGraphNode as LGraphNodeType } from '@/lib/litegraph/src/litegraph' +import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events' +import type { IColorWidget } from '@/lib/litegraph/src/types/widgets' +import type { ColorWidget as ColorWidgetType } from '@/lib/litegraph/src/widgets/ColorWidget' + +type LGraphCanvasType = InstanceType + +function createMockWidgetConfig( + overrides: Partial = {} +): IColorWidget { + return { + type: 'color', + name: 'test_color', + value: '#ff0000', + options: {}, + y: 0, + ...overrides + } +} + +function createMockCanvas(): LGraphCanvasType { + return { + setDirty: vi.fn() + } as Partial as LGraphCanvasType +} + +function createMockEvent(clientX = 100, clientY = 200): CanvasPointerEvent { + return { clientX, clientY } as CanvasPointerEvent +} + +describe('ColorWidget', () => { + let node: LGraphNodeType + let widget: ColorWidgetType + let mockCanvas: LGraphCanvasType + let mockEvent: CanvasPointerEvent + let ColorWidget: typeof ColorWidgetType + let LGraphNode: typeof LGraphNodeType + + beforeEach(async () => { + vi.clearAllMocks() + vi.useFakeTimers() + // Reset modules to get fresh globalColorInput state + vi.resetModules() + + const litegraph = await import('@/lib/litegraph/src/litegraph') + LGraphNode = litegraph.LGraphNode + + const colorWidgetModule = + await import('@/lib/litegraph/src/widgets/ColorWidget') + ColorWidget = colorWidgetModule.ColorWidget + + node = new LGraphNode('TestNode') + mockCanvas = createMockCanvas() + mockEvent = createMockEvent() + }) + + afterEach(() => { + vi.useRealTimers() + document + .querySelectorAll('input[type="color"]') + .forEach((el) => el.remove()) + }) + + describe('onClick', () => { + it('should create a color input and append it to document body', () => { + widget = new ColorWidget(createMockWidgetConfig(), node) + + widget.onClick({ e: mockEvent, node, canvas: mockCanvas }) + + const input = document.querySelector( + 'input[type="color"]' + ) as HTMLInputElement + expect(input).toBeTruthy() + expect(input.parentElement).toBe(document.body) + }) + + it('should set input value from widget value', () => { + widget = new ColorWidget( + createMockWidgetConfig({ value: '#00ff00' }), + node + ) + + widget.onClick({ e: mockEvent, node, canvas: mockCanvas }) + + const input = document.querySelector( + 'input[type="color"]' + ) as HTMLInputElement + expect(input.value).toBe('#00ff00') + }) + + it('should default to #000000 when widget value is empty', () => { + widget = new ColorWidget(createMockWidgetConfig({ value: '' }), node) + + widget.onClick({ e: mockEvent, node, canvas: mockCanvas }) + + const input = document.querySelector( + 'input[type="color"]' + ) as HTMLInputElement + expect(input.value).toBe('#000000') + }) + + it('should position input at click coordinates', () => { + widget = new ColorWidget(createMockWidgetConfig(), node) + const event = createMockEvent(150, 250) + + widget.onClick({ e: event, node, canvas: mockCanvas }) + + const input = document.querySelector( + 'input[type="color"]' + ) as HTMLInputElement + expect(input.style.left).toBe('150px') + expect(input.style.top).toBe('250px') + }) + + it('should click the input on next animation frame', () => { + widget = new ColorWidget(createMockWidgetConfig(), node) + + widget.onClick({ e: mockEvent, node, canvas: mockCanvas }) + + const input = document.querySelector( + 'input[type="color"]' + ) as HTMLInputElement + const clickSpy = vi.spyOn(input, 'click') + + expect(clickSpy).not.toHaveBeenCalled() + vi.runAllTimers() + expect(clickSpy).toHaveBeenCalled() + }) + + it('should reuse the same input element on subsequent clicks', () => { + widget = new ColorWidget(createMockWidgetConfig(), node) + + widget.onClick({ e: mockEvent, node, canvas: mockCanvas }) + const firstInput = document.querySelector('input[type="color"]') + + widget.onClick({ e: mockEvent, node, canvas: mockCanvas }) + const secondInput = document.querySelector('input[type="color"]') + + expect(firstInput).toBe(secondInput) + expect(document.querySelectorAll('input[type="color"]').length).toBe(1) + }) + + it('should update input value when clicking with different widget values', () => { + const widget1 = new ColorWidget( + createMockWidgetConfig({ value: '#ff0000' }), + node + ) + const widget2 = new ColorWidget( + createMockWidgetConfig({ value: '#0000ff' }), + node + ) + + widget1.onClick({ e: mockEvent, node, canvas: mockCanvas }) + const input = document.querySelector( + 'input[type="color"]' + ) as HTMLInputElement + expect(input.value).toBe('#ff0000') + + widget2.onClick({ e: mockEvent, node, canvas: mockCanvas }) + expect(input.value).toBe('#0000ff') + }) + }) + + describe('onChange', () => { + it('should call setValue when color input changes', () => { + widget = new ColorWidget( + createMockWidgetConfig({ value: '#ff0000' }), + node + ) + const setValueSpy = vi.spyOn(widget, 'setValue') + + widget.onClick({ e: mockEvent, node, canvas: mockCanvas }) + + const input = document.querySelector( + 'input[type="color"]' + ) as HTMLInputElement + input.value = '#00ff00' + input.dispatchEvent(new Event('change')) + + expect(setValueSpy).toHaveBeenCalledWith('#00ff00', { + e: mockEvent, + node, + canvas: mockCanvas + }) + }) + + it('should call canvas.setDirty after value change', () => { + widget = new ColorWidget(createMockWidgetConfig(), node) + + widget.onClick({ e: mockEvent, node, canvas: mockCanvas }) + + const input = document.querySelector( + 'input[type="color"]' + ) as HTMLInputElement + input.value = '#00ff00' + input.dispatchEvent(new Event('change')) + + expect(mockCanvas.setDirty).toHaveBeenCalledWith(true) + }) + + it('should remove change listener after firing once', () => { + widget = new ColorWidget(createMockWidgetConfig(), node) + const setValueSpy = vi.spyOn(widget, 'setValue') + + widget.onClick({ e: mockEvent, node, canvas: mockCanvas }) + + const input = document.querySelector( + 'input[type="color"]' + ) as HTMLInputElement + + input.value = '#00ff00' + input.dispatchEvent(new Event('change')) + input.value = '#0000ff' + input.dispatchEvent(new Event('change')) + + // Should only be called once despite two change events + expect(setValueSpy).toHaveBeenCalledTimes(1) + expect(setValueSpy).toHaveBeenCalledWith('#00ff00', expect.any(Object)) + }) + + it('should register new change listener on subsequent onClick', () => { + widget = new ColorWidget(createMockWidgetConfig(), node) + const setValueSpy = vi.spyOn(widget, 'setValue') + + // First click and change + widget.onClick({ e: mockEvent, node, canvas: mockCanvas }) + const input = document.querySelector( + 'input[type="color"]' + ) as HTMLInputElement + input.value = '#00ff00' + input.dispatchEvent(new Event('change')) + + // Second click and change + widget.onClick({ e: mockEvent, node, canvas: mockCanvas }) + input.value = '#0000ff' + input.dispatchEvent(new Event('change')) + + expect(setValueSpy).toHaveBeenCalledTimes(2) + expect(setValueSpy).toHaveBeenNthCalledWith( + 1, + '#00ff00', + expect.any(Object) + ) + expect(setValueSpy).toHaveBeenNthCalledWith( + 2, + '#0000ff', + expect.any(Object) + ) + }) + }) + + describe('type', () => { + it('should have type "color"', () => { + widget = new ColorWidget(createMockWidgetConfig(), node) + expect(widget.type).toBe('color') + }) + }) +}) diff --git a/src/lib/litegraph/src/widgets/ColorWidget.ts b/src/lib/litegraph/src/widgets/ColorWidget.ts index f2c50083a..7752706fa 100644 --- a/src/lib/litegraph/src/widgets/ColorWidget.ts +++ b/src/lib/litegraph/src/widgets/ColorWidget.ts @@ -1,12 +1,26 @@ -import { t } from '@/i18n' - import type { IColorWidget } from '../types/widgets' -import { BaseWidget } from './BaseWidget' import type { DrawWidgetOptions, WidgetEventOptions } from './BaseWidget' +import { BaseWidget } from './BaseWidget' + +// Have one color input to prevent leaking instances +// Browsers don't seem to fire any events when the color picker is cancelled +let colorInput: HTMLInputElement | null = null + +function getColorInput(): HTMLInputElement { + if (!colorInput) { + colorInput = document.createElement('input') + colorInput.type = 'color' + colorInput.style.position = 'absolute' + colorInput.style.opacity = '0' + colorInput.style.pointerEvents = 'none' + colorInput.style.zIndex = '-999' + document.body.appendChild(colorInput) + } + return colorInput +} /** - * Widget for displaying a color picker - * This is a widget that only has a Vue widgets implementation + * Widget for displaying a color picker using native HTML color input */ export class ColorWidget extends BaseWidget @@ -15,35 +29,59 @@ export class ColorWidget override type = 'color' as const drawWidget(ctx: CanvasRenderingContext2D, options: DrawWidgetOptions): void { + const { fillStyle, strokeStyle, textAlign } = ctx + + this.drawWidgetShape(ctx, options) + const { width } = options - const { y, height } = this + const { height, y } = this + const { margin } = BaseWidget - const { fillStyle, strokeStyle, textAlign, textBaseline, font } = ctx + const swatchWidth = 40 + const swatchHeight = height - 6 + const swatchRadius = swatchHeight / 2 + const rightPadding = 10 - ctx.fillStyle = this.background_color - ctx.fillRect(15, y, width - 30, height) + // Swatch fixed on the right + const swatchX = width - margin - rightPadding - swatchWidth + const swatchY = y + 3 - ctx.strokeStyle = this.outline_color - ctx.strokeRect(15, y, width - 30, height) + // Draw color swatch as rounded pill + ctx.beginPath() + ctx.roundRect(swatchX, swatchY, swatchWidth, swatchHeight, swatchRadius) + ctx.fillStyle = this.value || '#000000' + ctx.fill() + // Draw label on the left + ctx.fillStyle = this.secondary_text_color + ctx.textAlign = 'left' + ctx.fillText(this.displayName, margin * 2 + 5, y + height * 0.7) + + // Draw hex value to the left of swatch ctx.fillStyle = this.text_color - ctx.font = '11px monospace' - ctx.textAlign = 'center' - ctx.textBaseline = 'middle' + ctx.textAlign = 'right' + ctx.fillText(this.value || '#000000', swatchX - 8, y + height * 0.7) - const text = `Color: ${t('widgets.node2only')}` - ctx.fillText(text, width / 2, y + height / 2) - - Object.assign(ctx, { - fillStyle, - strokeStyle, - textAlign, - textBaseline, - font - }) + Object.assign(ctx, { textAlign, strokeStyle, fillStyle }) } - onClick(_options: WidgetEventOptions): void { - // This is a widget that only has a Vue widgets implementation + onClick({ e, node, canvas }: WidgetEventOptions): void { + const input = getColorInput() + input.value = this.value || '#000000' + input.style.left = `${e.clientX}px` + input.style.top = `${e.clientY}px` + + input.addEventListener( + 'change', + () => { + this.setValue(input.value, { e, node, canvas }) + canvas.setDirty(true) + }, + { once: true } + ) + + // Wait for next frame else Chrome doesn't render the color picker at the mouse + // Firefox always opens it in top left of window on Windows + requestAnimationFrame(() => input.click()) } } From fe7d89d1b15985bdefb5bddd86919c13c1e9a6b4 Mon Sep 17 00:00:00 2001 From: Simula_r <18093452+simula-r@users.noreply.github.com> Date: Wed, 28 Jan 2026 18:33:44 -0800 Subject: [PATCH 09/64] =?UTF-8?q?fix:=20move=20WorkspaceAuthGate=20to=20La?= =?UTF-8?q?youtDefault=20for=20proper=20re-login=20hand=E2=80=A6=20(#8381)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Move WorkspaceAuthGate from App.vue to LayoutDefault.vue so it only wraps authenticated routes - Change initialize() to run in onMounted() for proper Vue lifecycle - Restore immediate: true in cloudRemoteConfig watcher as backup - Gate now mounts fresh after login, fixing re-login feature flag issue The root cause was a race condition: after logout + page reload, the cloudRemoteConfig watcher could be set up after the user already logged in, missing the isLoggedIn change and never calling /features endpoint. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8381-fix-move-WorkspaceAuthGate-to-LayoutDefault-for-proper-re-login-hand-2f66d73d36508182a3dec09a49214a00) by [Unito](https://www.unito.io) Co-authored-by: Claude Opus 4.5 --- src/App.vue | 17 +++++++---------- src/components/auth/WorkspaceAuthGate.vue | 11 +++++++---- src/extensions/core/cloudRemoteConfig.ts | 7 ++++--- src/views/layouts/LayoutDefault.vue | 10 +++++++--- 4 files changed, 25 insertions(+), 20 deletions(-) diff --git a/src/App.vue b/src/App.vue index b5e17500f..7c11c4c7b 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,13 +1,11 @@ diff --git a/src/extensions/core/cloudRemoteConfig.ts b/src/extensions/core/cloudRemoteConfig.ts index d3c1dc7ac..8db19df9c 100644 --- a/src/extensions/core/cloudRemoteConfig.ts +++ b/src/extensions/core/cloudRemoteConfig.ts @@ -16,15 +16,16 @@ useExtensionService().registerExtension({ const { isLoggedIn } = useCurrentUser() const { isActiveSubscription } = useSubscription() - // Refresh config when subscription status changes - // Initial auth-aware refresh happens in WorkspaceAuthGate before app renders + // Refresh config when auth or subscription status changes + // Primary auth refresh is handled by WorkspaceAuthGate on mount + // This watcher handles subscription changes and acts as a backup for auth watchDebounced( [isLoggedIn, isActiveSubscription], () => { if (!isLoggedIn.value) return void refreshRemoteConfig() }, - { debounce: 256 } + { debounce: 256, immediate: true } ) // Poll for config updates every 10 minutes (with auth) diff --git a/src/views/layouts/LayoutDefault.vue b/src/views/layouts/LayoutDefault.vue index cbe0e3408..613aa0418 100644 --- a/src/views/layouts/LayoutDefault.vue +++ b/src/views/layouts/LayoutDefault.vue @@ -1,11 +1,15 @@ From 2103dcc7880b18d955106c925fd1f38596d4aaef Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Wed, 28 Jan 2026 19:15:40 -0800 Subject: [PATCH 10/64] fix: increase Vue node resize handle size for better usability (#8391) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Increases the resize handle size on Vue nodes to improve usability, especially when nodes are selected. ## Changes - **What**: Increased resize handle from 12px to 20px and offset it slightly outside the node boundary to avoid overlap with selection outline ## Review Focus The resize handle was too small and became harder to grab when the node was selected (the 2px outline rendered outside the box, visually obscuring the corner). This fix increases the hit area and positions it to extend beyond the node edge. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8391-fix-increase-Vue-node-resize-handle-size-for-better-usability-2f76d73d36508136b2aac51bc0d53551) by [Unito](https://www.unito.io) --------- Co-authored-by: Subagent 5 Co-authored-by: Amp Co-authored-by: GitHub Action --- src/renderer/extensions/vueNodes/components/LGraphNode.vue | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/renderer/extensions/vueNodes/components/LGraphNode.vue b/src/renderer/extensions/vueNodes/components/LGraphNode.vue index d602e4062..838e20dd0 100644 --- a/src/renderer/extensions/vueNodes/components/LGraphNode.vue +++ b/src/renderer/extensions/vueNodes/components/LGraphNode.vue @@ -150,7 +150,9 @@ v-if="!isCollapsed && nodeData.resizable !== false" role="button" :aria-label="t('g.resizeFromBottomRight')" - :class="cn(baseResizeHandleClasses, 'right-0 bottom-0 cursor-se-resize')" + :class=" + cn(baseResizeHandleClasses, '-right-1 -bottom-1 cursor-se-resize') + " @pointerdown.stop="handleResizePointerDown" />
@@ -344,7 +346,7 @@ function initSizeStyles() { } const baseResizeHandleClasses = - 'absolute h-3 w-3 opacity-0 pointer-events-auto focus-visible:outline focus-visible:outline-2 focus-visible:outline-white/40' + 'absolute h-5 w-5 opacity-0 pointer-events-auto focus-visible:outline focus-visible:outline-2 focus-visible:outline-white/40' const MIN_NODE_WIDTH = 225 From 9be853f6b554ab6da4340b49eebb823a56b7ce3d Mon Sep 17 00:00:00 2001 From: guill Date: Wed, 28 Jan 2026 19:41:45 -0800 Subject: [PATCH 11/64] feat: support dev-only nodes (#8359) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Support `dev_only` property to node definitions that hides nodes from search and menus unless dev mode is enabled. Dev-only nodes display a "DEV" badge when visible. This functionality is primarily intended to support unit-testing nodes on Comfy Cloud, but also has other uses. ## Changes - **What**: Nodes flagged as dev_only in the node schema will only appear in search and menus if Dev Mode is on. ## Screenshots (if applicable) With Dev Mode off: image With Dev Mode on: image ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8359-feat-support-dev-only-nodes-2f66d73d36508102839ee7cd66a26129) by [Unito](https://www.unito.io) --- src/components/searchbox/NodeSearchItem.vue | 11 ++++---- src/lib/litegraph/src/LGraphNode.ts | 8 ++++++ src/locales/en/main.json | 1 + src/schemas/nodeDef/nodeDefSchemaV2.ts | 1 + src/schemas/nodeDefSchema.ts | 1 + src/services/litegraphService.ts | 11 ++++++-- src/stores/nodeDefStore.ts | 31 ++++++++++++++++++++- src/utils/nodeFilterUtil.test.ts | 8 +++--- 8 files changed, 60 insertions(+), 12 deletions(-) diff --git a/src/components/searchbox/NodeSearchItem.vue b/src/components/searchbox/NodeSearchItem.vue index da9a0cd83..a9859eca4 100644 --- a/src/components/searchbox/NodeSearchItem.vue +++ b/src/components/searchbox/NodeSearchItem.vue @@ -21,16 +21,17 @@
- + + { static comfyClass: string static override title: string static override category: string - static nodeData: ComfyNodeDefV1 & ComfyNodeDefV2 + static override nodeData: ComfyNodeDefV1 & ComfyNodeDefV2 _initialMinSize = { width: 1, height: 1 } @@ -394,7 +394,7 @@ export const useLitegraphService = () => { static comfyClass: string static override title: string static override category: string - static nodeData: ComfyNodeDefV1 & ComfyNodeDefV2 + static override nodeData: ComfyNodeDefV1 & ComfyNodeDefV2 _initialMinSize = { width: 1, height: 1 } @@ -496,6 +496,13 @@ export const useLitegraphService = () => { // because `registerNodeType` will overwrite the assignments. node.category = nodeDef.category node.title = nodeDef.display_name || nodeDef.name + + // Set skip_list for dev-only nodes based on current DevMode setting + // This ensures nodes registered after initial load respect the current setting + if (nodeDef.dev_only) { + const settingStore = useSettingStore() + node.skip_list = !settingStore.get('Comfy.DevMode') + } } /** diff --git a/src/stores/nodeDefStore.ts b/src/stores/nodeDefStore.ts index 217b784ca..7bfa17c2c 100644 --- a/src/stores/nodeDefStore.ts +++ b/src/stores/nodeDefStore.ts @@ -1,9 +1,10 @@ import axios from 'axios' import _ from 'es-toolkit/compat' import { defineStore } from 'pinia' -import { computed, ref } from 'vue' +import { computed, ref, watchEffect } from 'vue' import { isProxyWidget } from '@/core/graph/subgraph/proxyWidget' +import { LiteGraph } from '@/lib/litegraph/src/litegraph' import type { LGraphNode } from '@/lib/litegraph/src/litegraph' import { transformNodeDefV1ToV2 } from '@/schemas/nodeDef/migration' import type { @@ -17,6 +18,7 @@ import type { ComfyOutputTypesSpec as ComfyOutputSpecV1, PriceBadge } from '@/schemas/nodeDefSchema' +import { useSettingStore } from '@/platform/settings/settingStore' import { NodeSearchService } from '@/services/nodeSearchService' import { useSubgraphStore } from '@/stores/subgraphStore' import { NodeSourceType, getNodeSource } from '@/types/nodeSource' @@ -41,6 +43,7 @@ export class ComfyNodeDefImpl readonly help: string readonly deprecated: boolean readonly experimental: boolean + readonly dev_only: boolean readonly output_node: boolean readonly api_node: boolean /** @@ -133,6 +136,7 @@ export class ComfyNodeDefImpl this.deprecated = obj.deprecated ?? obj.category === '' this.experimental = obj.experimental ?? obj.category.startsWith('_for_testing') + this.dev_only = obj.dev_only ?? false this.output_node = obj.output_node this.api_node = !!obj.api_node this.input = obj.input ?? {} @@ -174,6 +178,7 @@ export class ComfyNodeDefImpl get nodeLifeCycleBadgeText(): string { if (this.deprecated) return '[DEPR]' if (this.experimental) return '[BETA]' + if (this.dev_only) return '[DEV]' return '' } } @@ -299,12 +304,27 @@ export interface NodeDefFilter { } export const useNodeDefStore = defineStore('nodeDef', () => { + const settingStore = useSettingStore() + const nodeDefsByName = ref>({}) const nodeDefsByDisplayName = ref>({}) const showDeprecated = ref(false) const showExperimental = ref(false) + const showDevOnly = computed(() => settingStore.get('Comfy.DevMode')) const nodeDefFilters = ref([]) + // Update skip_list on all registered node types when dev mode changes + // This ensures LiteGraph's getNodeTypesCategories/getNodeTypesInCategory + // correctly filter dev-only nodes from the right-click context menu + watchEffect(() => { + const devModeEnabled = showDevOnly.value + for (const nodeType of Object.values(LiteGraph.registered_node_types)) { + if (nodeType.nodeData?.dev_only) { + nodeType.skip_list = !devModeEnabled + } + } + }) + const nodeDefs = computed(() => { const subgraphStore = useSubgraphStore() // Blueprints first for discoverability in the node library sidebar @@ -422,6 +442,14 @@ export const useNodeDefStore = defineStore('nodeDef', () => { predicate: (nodeDef) => showExperimental.value || !nodeDef.experimental }) + // Dev-only nodes filter + registerNodeDefFilter({ + id: 'core.dev_only', + name: 'Hide Dev-Only Nodes', + description: 'Hides nodes marked as dev-only unless dev mode is enabled', + predicate: (nodeDef) => showDevOnly.value || !nodeDef.dev_only + }) + // Subgraph nodes filter // Filter out litegraph typed subgraphs, saved blueprints are added in separately registerNodeDefFilter({ @@ -446,6 +474,7 @@ export const useNodeDefStore = defineStore('nodeDef', () => { nodeDefsByDisplayName, showDeprecated, showExperimental, + showDevOnly, nodeDefFilters, nodeDefs, diff --git a/src/utils/nodeFilterUtil.test.ts b/src/utils/nodeFilterUtil.test.ts index 871f66109..0e6149c0d 100644 --- a/src/utils/nodeFilterUtil.test.ts +++ b/src/utils/nodeFilterUtil.test.ts @@ -11,7 +11,7 @@ describe('nodeFilterUtil', () => { ): LGraphNode => { // Create a custom class with the nodeData static property class MockNode extends LGraphNode { - static nodeData = isOutputNode ? { output_node: true } : {} + static override nodeData = isOutputNode ? { output_node: true } : {} } const node = new MockNode('') @@ -71,11 +71,11 @@ describe('nodeFilterUtil', () => { }) it('should handle nodes with undefined output_node', () => { - class MockNodeWithOtherData extends LGraphNode { - static nodeData = { someOtherProperty: true } + class MockNodeWithEmptyData extends LGraphNode { + static override nodeData = {} } - const node = new MockNodeWithOtherData('') + const node = new MockNodeWithEmptyData('') node.id = 1 const result = filterOutputNodes([node]) From bd916096ac30e04cfdf9df4b4935e7fb2e22ca41 Mon Sep 17 00:00:00 2001 From: Jin Yi Date: Thu, 29 Jan 2026 13:19:51 +0900 Subject: [PATCH 12/64] fix: add null check in getCanvasCenter to prevent crash on asset insert (#8399) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Adds null check in `getCanvasCenter()` to prevent crash when inserting asset as node before canvas is fully initialized. ## Changes - **What**: Added optional chaining for `app.canvas?.ds?.visible_area` with fallback to `[0, 0]` ## Review Focus - Simple defensive fix - returns origin position if canvas not ready 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8399-fix-add-null-check-in-getCanvasCenter-to-prevent-crash-on-asset-insert-2f76d73d365081e88c08ef40ea9e7b78) by [Unito](https://www.unito.io) --- src/services/litegraphService.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/services/litegraphService.ts b/src/services/litegraphService.ts index f071a2356..268d7a0a7 100644 --- a/src/services/litegraphService.ts +++ b/src/services/litegraphService.ts @@ -883,7 +883,11 @@ export const useLitegraphService = () => { function getCanvasCenter(): Point { const dpi = Math.max(window.devicePixelRatio ?? 1, 1) - const [x, y, w, h] = app.canvas.ds.visible_area + const visibleArea = app.canvas?.ds?.visible_area + if (!visibleArea) { + return [0, 0] + } + const [x, y, w, h] = visibleArea return [x + w / dpi / 2, y + h / dpi / 2] } From bd4920febcb731a67492b53181514865c213ab36 Mon Sep 17 00:00:00 2001 From: Alexander Brown Date: Wed, 28 Jan 2026 21:22:39 -0800 Subject: [PATCH 13/64] Chore: Actions updates and cleanup (#8377) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary ... ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8377-WIP-Chore-Actions-updates-and-cleanup-2f66d73d3650818483a8dffa32a6f245) by [Unito](https://www.unito.io) --------- Co-authored-by: Amp --- .gitattributes | 1 + .../actions/comment-release-links/action.yaml | 4 +- .../{action.yml => action.yaml} | 4 +- .../{action.yml => action.yaml} | 16 +------ .../{action.yml => action.yaml} | 2 +- .../{action.yml => action.yaml} | 0 .../api-update-electron-api-types.yaml | 8 ++-- .../api-update-manager-api-types.yaml | 10 ++-- .../api-update-registry-api-types.yaml | 10 ++-- .github/workflows/ci-json-validation.yaml | 2 +- .github/workflows/ci-lint-format.yaml | 21 ++------- .github/workflows/ci-python-validation.yaml | 4 +- .github/workflows/ci-size-data.yaml | 19 ++------ .github/workflows/ci-tests-e2e-forks.yaml | 6 +-- .github/workflows/ci-tests-e2e.yaml | 31 ++++++------- .../workflows/ci-tests-storybook-forks.yaml | 6 +-- .github/workflows/ci-tests-storybook.yaml | 46 +++++-------------- .github/workflows/ci-tests-unit.yaml | 17 ++----- .../workflows/ci-validate-action-pins.yaml | 21 +++++++++ .github/workflows/ci-yaml-validation.yaml | 4 +- .github/workflows/cloud-backport-tag.yaml | 4 +- .github/workflows/i18n-update-core.yaml | 2 +- .../workflows/i18n-update-custom-nodes.yaml | 6 +-- .github/workflows/i18n-update-nodes.yaml | 4 +- .github/workflows/pr-backport.yaml | 2 +- .github/workflows/pr-claude-review.yaml | 8 ++-- .github/workflows/pr-size-report.yaml | 25 +++------- .../pr-update-playwright-expectations.yaml | 22 ++++----- .../publish-desktop-ui-on-merge.yaml | 6 +-- .github/workflows/publish-desktop-ui.yaml | 6 +-- .../workflows/release-biweekly-comfyui.yaml | 10 ++-- .github/workflows/release-branch-create.yaml | 4 +- .github/workflows/release-draft-create.yaml | 26 +++++------ .github/workflows/release-npm-types.yaml | 6 +-- .github/workflows/release-pypi-dev.yaml | 16 +++---- .github/workflows/release-version-bump.yaml | 10 ++-- .../workflows/version-bump-desktop-ui.yaml | 8 ++-- .github/workflows/weekly-docs-check.yaml | 12 ++--- .pinact.yaml | 24 ++++++++++ .yamllint | 3 ++ 40 files changed, 200 insertions(+), 236 deletions(-) rename .github/actions/setup-comfyui-server/{action.yml => action.yaml} (96%) rename .github/actions/setup-frontend/{action.yml => action.yaml} (61%) rename .github/actions/setup-playwright/{action.yml => action.yaml} (96%) rename .github/actions/start-comfyui-server/{action.yml => action.yaml} (100%) create mode 100644 .github/workflows/ci-validate-action-pins.yaml create mode 100644 .pinact.yaml diff --git a/.gitattributes b/.gitattributes index 39d7f722c..17591e2d4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -11,6 +11,7 @@ *.ts text eol=lf *.vue text eol=lf *.yaml text eol=lf +*.yml text eol=lf # Generated files packages/registry-types/src/comfyRegistryTypes.ts linguist-generated=true diff --git a/.github/actions/comment-release-links/action.yaml b/.github/actions/comment-release-links/action.yaml index a198604e9..3fc704616 100644 --- a/.github/actions/comment-release-links/action.yaml +++ b/.github/actions/comment-release-links/action.yaml @@ -104,14 +104,14 @@ runs: - name: Find existing comment id: find - uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad + uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0 with: issue-number: ${{ inputs.issue-number || github.event.pull_request.number }} comment-author: github-actions[bot] body-includes: ${{ steps.build.outputs.marker_search }} - name: Post or update comment - uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 + uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0 with: issue-number: ${{ inputs.issue-number || github.event.pull_request.number }} comment-id: ${{ steps.find.outputs.comment-id }} diff --git a/.github/actions/setup-comfyui-server/action.yml b/.github/actions/setup-comfyui-server/action.yaml similarity index 96% rename from .github/actions/setup-comfyui-server/action.yml rename to .github/actions/setup-comfyui-server/action.yaml index d1aa1bd57..721b47e34 100644 --- a/.github/actions/setup-comfyui-server/action.yml +++ b/.github/actions/setup-comfyui-server/action.yaml @@ -16,7 +16,7 @@ runs: # Checkout ComfyUI repo, install the dev_tools node and start server - name: Checkout ComfyUI - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: repository: 'comfyanonymous/ComfyUI' path: 'ComfyUI' @@ -33,7 +33,7 @@ runs: fi - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: '3.10' diff --git a/.github/actions/setup-frontend/action.yml b/.github/actions/setup-frontend/action.yaml similarity index 61% rename from .github/actions/setup-frontend/action.yml rename to .github/actions/setup-frontend/action.yaml index 6787552ea..c4d5d4eed 100644 --- a/.github/actions/setup-frontend/action.yml +++ b/.github/actions/setup-frontend/action.yaml @@ -12,29 +12,17 @@ runs: # Install pnpm, Node.js, build frontend - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 with: version: 10 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: 'lts/*' cache: 'pnpm' cache-dependency-path: './pnpm-lock.yaml' - # Restore tool caches before running any build/lint operations - - name: Restore tool output cache - uses: actions/cache/restore@v4 - with: - path: | - ./.cache - ./tsconfig.tsbuildinfo - key: tool-cache-${{ runner.os }}-${{ hashFiles('./pnpm-lock.yaml') }}-${{ hashFiles('./src/**/*.{ts,vue,js,mts}', './*.config.*') }} - restore-keys: | - tool-cache-${{ runner.os }}-${{ hashFiles('./pnpm-lock.yaml') }}- - tool-cache-${{ runner.os }}- - - name: Install dependencies shell: bash run: pnpm install --frozen-lockfile diff --git a/.github/actions/setup-playwright/action.yml b/.github/actions/setup-playwright/action.yaml similarity index 96% rename from .github/actions/setup-playwright/action.yml rename to .github/actions/setup-playwright/action.yaml index 89629fb2c..63e0c0362 100644 --- a/.github/actions/setup-playwright/action.yml +++ b/.github/actions/setup-playwright/action.yaml @@ -11,7 +11,7 @@ runs: echo "playwright-version=$PLAYWRIGHT_VERSION" >> $GITHUB_OUTPUT - name: Cache Playwright Browsers - uses: actions/cache@v4 + uses: actions/cache@v5 # v5.0.2 id: cache-playwright-browsers with: path: '~/.cache/ms-playwright' diff --git a/.github/actions/start-comfyui-server/action.yml b/.github/actions/start-comfyui-server/action.yaml similarity index 100% rename from .github/actions/start-comfyui-server/action.yml rename to .github/actions/start-comfyui-server/action.yaml diff --git a/.github/workflows/api-update-electron-api-types.yaml b/.github/workflows/api-update-electron-api-types.yaml index b7c5bce71..fb9398a1b 100644 --- a/.github/workflows/api-update-electron-api-types.yaml +++ b/.github/workflows/api-update-electron-api-types.yaml @@ -13,15 +13,15 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 with: version: 10 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: lts/* cache: 'pnpm' @@ -36,7 +36,7 @@ jobs: echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT - name: Create Pull Request - uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: token: ${{ secrets.PR_GH_TOKEN }} commit-message: '[chore] Update electron-types to ${{ steps.get-version.outputs.NEW_VERSION }}' diff --git a/.github/workflows/api-update-manager-api-types.yaml b/.github/workflows/api-update-manager-api-types.yaml index a709baecf..82b39978f 100644 --- a/.github/workflows/api-update-manager-api-types.yaml +++ b/.github/workflows/api-update-manager-api-types.yaml @@ -18,15 +18,15 @@ jobs: pull-requests: write steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 with: version: 10 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: lts/* cache: 'pnpm' @@ -35,7 +35,7 @@ jobs: run: pnpm install --frozen-lockfile - name: Checkout ComfyUI-Manager repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: repository: Comfy-Org/ComfyUI-Manager path: ComfyUI-Manager @@ -86,7 +86,7 @@ jobs: - name: Create Pull Request if: steps.check-changes.outputs.changed == 'true' - uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: token: ${{ secrets.PR_GH_TOKEN }} commit-message: '[chore] Update ComfyUI-Manager API types from ComfyUI-Manager@${{ steps.manager-info.outputs.commit }}' diff --git a/.github/workflows/api-update-registry-api-types.yaml b/.github/workflows/api-update-registry-api-types.yaml index 5ae701dc2..41521cf94 100644 --- a/.github/workflows/api-update-registry-api-types.yaml +++ b/.github/workflows/api-update-registry-api-types.yaml @@ -17,15 +17,15 @@ jobs: pull-requests: write steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 with: version: 10 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: lts/* cache: 'pnpm' @@ -34,7 +34,7 @@ jobs: run: pnpm install --frozen-lockfile - name: Checkout comfy-api repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: repository: Comfy-Org/comfy-api path: comfy-api @@ -87,7 +87,7 @@ jobs: - name: Create Pull Request if: steps.check-changes.outputs.changed == 'true' - uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: token: ${{ secrets.PR_GH_TOKEN }} commit-message: '[chore] Update Comfy Registry API types from comfy-api@${{ steps.api-info.outputs.commit }}' diff --git a/.github/workflows/ci-json-validation.yaml b/.github/workflows/ci-json-validation.yaml index 9fd6f915b..20a2743d1 100644 --- a/.github/workflows/ci-json-validation.yaml +++ b/.github/workflows/ci-json-validation.yaml @@ -13,6 +13,6 @@ jobs: json-lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Validate JSON syntax run: ./scripts/cicd/check-json.sh diff --git a/.github/workflows/ci-lint-format.yaml b/.github/workflows/ci-lint-format.yaml index e001dc234..f463af16c 100644 --- a/.github/workflows/ci-lint-format.yaml +++ b/.github/workflows/ci-lint-format.yaml @@ -18,23 +18,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout PR - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: ${{ !github.event.pull_request.head.repo.fork && github.head_ref || github.ref }} - - name: Install pnpm - uses: pnpm/action-setup@v4 - with: - version: 10 - - - name: Use Node.js - uses: actions/setup-node@v4 - with: - node-version: 'lts/*' - cache: 'pnpm' - - - name: Install dependencies - run: pnpm install --frozen-lockfile + - name: Setup frontend + uses: ./.github/actions/setup-frontend - name: Run ESLint with auto-fix run: pnpm lint:fix @@ -73,7 +62,7 @@ jobs: - name: Comment on PR about auto-fix if: steps.verify-changed-files.outputs.changed == 'true' && github.event.pull_request.head.repo.full_name == github.repository continue-on-error: true - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | github.rest.issues.createComment({ @@ -86,7 +75,7 @@ jobs: - name: Comment on PR about manual fix needed if: steps.verify-changed-files.outputs.changed == 'true' && github.event.pull_request.head.repo.full_name != github.repository continue-on-error: true - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | github.rest.issues.createComment({ diff --git a/.github/workflows/ci-python-validation.yaml b/.github/workflows/ci-python-validation.yaml index b06296391..cf392f1bf 100644 --- a/.github/workflows/ci-python-validation.yaml +++ b/.github/workflows/ci-python-validation.yaml @@ -16,10 +16,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.11' diff --git a/.github/workflows/ci-size-data.yaml b/.github/workflows/ci-size-data.yaml index c88be8ad5..f56c0d17d 100644 --- a/.github/workflows/ci-size-data.yaml +++ b/.github/workflows/ci-size-data.yaml @@ -17,21 +17,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - - name: Install pnpm - uses: pnpm/action-setup@v4.1.0 - with: - version: 10 - - - name: Install Node.js - uses: actions/setup-node@v5 - with: - node-version: '24.x' - cache: pnpm - - - name: Install dependencies - run: pnpm install + - name: Setup frontend + uses: ./.github/actions/setup-frontend - name: Build project run: pnpm build @@ -46,7 +35,7 @@ jobs: echo ${{ github.base_ref }} > ./temp/size/base.txt - name: Upload size data - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: size-data path: temp/size diff --git a/.github/workflows/ci-tests-e2e-forks.yaml b/.github/workflows/ci-tests-e2e-forks.yaml index 8f039f1c4..3aaeccb30 100644 --- a/.github/workflows/ci-tests-e2e-forks.yaml +++ b/.github/workflows/ci-tests-e2e-forks.yaml @@ -31,11 +31,11 @@ jobs: echo "Is forked: ${{ github.event.workflow_run.head_repository.full_name != github.event.workflow_run.repository.full_name }}" - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Get PR Number id: pr - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | const { data: prs } = await github.rest.pulls.list({ @@ -68,7 +68,7 @@ jobs: - name: Download and Deploy Reports if: steps.pr.outputs.result != 'null' && github.event.action == 'completed' - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} run-id: ${{ github.event.workflow_run.id }} diff --git a/.github/workflows/ci-tests-e2e.yaml b/.github/workflows/ci-tests-e2e.yaml index c1e0af411..c3e2e6dd0 100644 --- a/.github/workflows/ci-tests-e2e.yaml +++ b/.github/workflows/ci-tests-e2e.yaml @@ -17,7 +17,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup frontend uses: ./.github/actions/setup-frontend with: @@ -25,7 +25,7 @@ jobs: # Upload only built dist/ (containerized test jobs will pnpm install without cache) - name: Upload built frontend - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: frontend-dist path: dist/ @@ -51,9 +51,9 @@ jobs: shardTotal: [8] steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Download built frontend - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: name: frontend-dist path: dist/ @@ -72,7 +72,7 @@ jobs: PLAYWRIGHT_BLOB_OUTPUT_DIR: ./blob-report - name: Upload blob report - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 if: ${{ !cancelled() }} with: name: blob-report-chromium-${{ matrix.shardIndex }} @@ -98,9 +98,9 @@ jobs: browser: [chromium-2x, chromium-0.5x, mobile-chrome] steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Download built frontend - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: name: frontend-dist path: dist/ @@ -128,7 +128,7 @@ jobs: pnpm exec playwright merge-reports --reporter=json ./blob-report - name: Upload Playwright report - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 if: always() with: name: playwright-report-${{ matrix.browser }} @@ -141,16 +141,13 @@ jobs: runs-on: ubuntu-latest if: ${{ !cancelled() }} steps: - - name: Checkout repository - uses: actions/checkout@v5 - - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 with: version: 10 - name: Download blob reports - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: path: ./all-blob-reports pattern: blob-report-chromium-* @@ -165,7 +162,7 @@ jobs: pnpm dlx @playwright/test merge-reports --reporter=json ./all-blob-reports - name: Upload HTML report - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: playwright-report-chromium path: ./playwright-report/ @@ -183,7 +180,7 @@ jobs: pull-requests: write steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Get start time id: start-time @@ -210,10 +207,10 @@ jobs: contents: read steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Download all playwright reports - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: pattern: playwright-report-* path: reports diff --git a/.github/workflows/ci-tests-storybook-forks.yaml b/.github/workflows/ci-tests-storybook-forks.yaml index 3012f61f2..d4f18d37b 100644 --- a/.github/workflows/ci-tests-storybook-forks.yaml +++ b/.github/workflows/ci-tests-storybook-forks.yaml @@ -31,11 +31,11 @@ jobs: echo "Is forked: ${{ github.event.workflow_run.head_repository.full_name != github.event.workflow_run.repository.full_name }}" - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Get PR Number id: pr - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | const { data: prs } = await github.rest.pulls.list({ @@ -68,7 +68,7 @@ jobs: - name: Download and Deploy Storybook if: steps.pr.outputs.result != 'null' && github.event.action == 'completed' && github.event.workflow_run.conclusion == 'success' - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} run-id: ${{ github.event.workflow_run.id }} diff --git a/.github/workflows/ci-tests-storybook.yaml b/.github/workflows/ci-tests-storybook.yaml index e900a6f0f..7a91e7a01 100644 --- a/.github/workflows/ci-tests-storybook.yaml +++ b/.github/workflows/ci-tests-storybook.yaml @@ -14,7 +14,7 @@ jobs: pull-requests: write steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Post starting comment env: @@ -36,21 +36,10 @@ jobs: workflow-url: ${{ steps.workflow-url.outputs.url }} steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - - name: Install pnpm - uses: pnpm/action-setup@v4 - with: - version: 10 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'pnpm' - - - name: Install dependencies - run: pnpm install --frozen-lockfile + - name: Setup frontend + uses: ./.github/actions/setup-frontend - name: Build Storybook run: pnpm build-storybook @@ -69,7 +58,7 @@ jobs: - name: Upload Storybook build if: success() && github.event.pull_request.head.repo.fork == false - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: storybook-static path: storybook-static/ @@ -86,27 +75,16 @@ jobs: chromatic-storybook-url: ${{ steps.chromatic.outputs.storybookUrl }} steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 # Required for Chromatic baseline - - name: Install pnpm - uses: pnpm/action-setup@v4 - with: - version: 10 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'pnpm' - - - name: Install dependencies - run: pnpm install --frozen-lockfile + - name: Setup frontend + uses: ./.github/actions/setup-frontend - name: Build Storybook and run Chromatic id: chromatic - uses: chromaui/action@latest + uses: chromaui/action@07791f8243f4cb2698bf4d00426baf4b2d1cb7e0 # v13.3.5 with: projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} buildScriptName: build-storybook @@ -136,11 +114,11 @@ jobs: contents: read steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Download Storybook build if: needs.storybook-build.outputs.conclusion == 'success' - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: name: storybook-static path: storybook-static @@ -170,7 +148,7 @@ jobs: pull-requests: write steps: - name: Update comment with Chromatic URLs - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: script: | const buildUrl = '${{ needs.chromatic-deployment.outputs.chromatic-build-url }}'; diff --git a/.github/workflows/ci-tests-unit.yaml b/.github/workflows/ci-tests-unit.yaml index c2a8c1f15..e1ba5b5d9 100644 --- a/.github/workflows/ci-tests-unit.yaml +++ b/.github/workflows/ci-tests-unit.yaml @@ -16,21 +16,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - - name: Install pnpm - uses: pnpm/action-setup@v4 - with: - version: 10 - - - name: Use Node.js - uses: actions/setup-node@v4 - with: - node-version: 'lts/*' - cache: 'pnpm' - - - name: Install dependencies - run: pnpm install --frozen-lockfile + - name: Setup frontend + uses: ./.github/actions/setup-frontend - name: Run Vitest tests run: pnpm test:unit diff --git a/.github/workflows/ci-validate-action-pins.yaml b/.github/workflows/ci-validate-action-pins.yaml new file mode 100644 index 000000000..3cd66cd7e --- /dev/null +++ b/.github/workflows/ci-validate-action-pins.yaml @@ -0,0 +1,21 @@ +name: Validate Action SHA Pins + +on: + pull_request: + paths: + - '.github/workflows/**' + - '.github/actions/**' + - '.pinact.yaml' + +permissions: + contents: read + +jobs: + validate-pins: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: suzuki-shunsuke/pinact-action@3d49c6412901042473ffa78becddab1aea46bbea # v1.3.1 + with: + skip_push: 'true' diff --git a/.github/workflows/ci-yaml-validation.yaml b/.github/workflows/ci-yaml-validation.yaml index 788c6b188..876fcfc4c 100644 --- a/.github/workflows/ci-yaml-validation.yaml +++ b/.github/workflows/ci-yaml-validation.yaml @@ -17,10 +17,10 @@ jobs: yaml-lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: '3.x' diff --git a/.github/workflows/cloud-backport-tag.yaml b/.github/workflows/cloud-backport-tag.yaml index c0edec170..73b01c682 100644 --- a/.github/workflows/cloud-backport-tag.yaml +++ b/.github/workflows/cloud-backport-tag.yaml @@ -18,12 +18,12 @@ jobs: steps: - name: Checkout merge commit - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.merge_commit_sha }} - name: Setup Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version-file: '.nvmrc' diff --git a/.github/workflows/i18n-update-core.yaml b/.github/workflows/i18n-update-core.yaml index bdf58b52f..7b0299ab1 100644 --- a/.github/workflows/i18n-update-core.yaml +++ b/.github/workflows/i18n-update-core.yaml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 # Setup playwright environment - name: Setup ComfyUI Frontend diff --git a/.github/workflows/i18n-update-custom-nodes.yaml b/.github/workflows/i18n-update-custom-nodes.yaml index 5844065ad..225c1b3e3 100644 --- a/.github/workflows/i18n-update-custom-nodes.yaml +++ b/.github/workflows/i18n-update-custom-nodes.yaml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 # Setup playwright environment with custom node repository - name: Setup ComfyUI Server (without launching) @@ -36,7 +36,7 @@ jobs: # Install the custom node repository - name: Checkout custom node repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: repository: ${{ inputs.owner }}/${{ inputs.repository }} path: 'ComfyUI/custom_nodes/${{ inputs.repository }}' @@ -113,7 +113,7 @@ jobs: git commit -m "Update locales" - name: Install SSH key For PUSH - uses: shimataro/ssh-key-action@d4fffb50872869abe2d9a9098a6d9c5aa7d16be4 + uses: shimataro/ssh-key-action@d4fffb50872869abe2d9a9098a6d9c5aa7d16be4 # v2.7.0 with: # PR private key from action server key: ${{ secrets.PR_SSH_PRIVATE_KEY }} diff --git a/.github/workflows/i18n-update-nodes.yaml b/.github/workflows/i18n-update-nodes.yaml index 9afc1f195..5a72e5b10 100644 --- a/.github/workflows/i18n-update-nodes.yaml +++ b/.github/workflows/i18n-update-nodes.yaml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 # Setup playwright environment - name: Setup ComfyUI Server (and start) uses: ./.github/actions/setup-comfyui-server @@ -40,7 +40,7 @@ jobs: env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - name: Create Pull Request - uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: token: ${{ secrets.PR_GH_TOKEN }} commit-message: 'Update locales for node definitions' diff --git a/.github/workflows/pr-backport.yaml b/.github/workflows/pr-backport.yaml index 968fcfd81..a6b15db5f 100644 --- a/.github/workflows/pr-backport.yaml +++ b/.github/workflows/pr-backport.yaml @@ -64,7 +64,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 diff --git a/.github/workflows/pr-claude-review.yaml b/.github/workflows/pr-claude-review.yaml index 56fcc8c9b..b1f3d1a7f 100644 --- a/.github/workflows/pr-claude-review.yaml +++ b/.github/workflows/pr-claude-review.yaml @@ -23,18 +23,18 @@ jobs: timeout-minutes: 30 steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 ref: refs/pull/${{ github.event.pull_request.number }}/head - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 with: version: 10 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: '20' cache: 'pnpm' @@ -44,7 +44,7 @@ jobs: pnpm install -g typescript @vue/compiler-sfc - name: Run Claude PR Review - uses: anthropics/claude-code-action@v1.0.6 + uses: anthropics/claude-code-action@ff34ce0ff04a470bd3fa56c1ef391c8f1c19f8e9 # v1.0.38 with: label_trigger: 'claude-review' prompt: | diff --git a/.github/workflows/pr-size-report.yaml b/.github/workflows/pr-size-report.yaml index 38b742054..769ce0e1a 100644 --- a/.github/workflows/pr-size-report.yaml +++ b/.github/workflows/pr-size-report.yaml @@ -33,24 +33,13 @@ jobs: github.event_name == 'workflow_dispatch' ) steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - - name: Install pnpm - uses: pnpm/action-setup@v4.1.0 - with: - version: 10 - - - name: Install Node.js - uses: actions/setup-node@v5 - with: - node-version: '24.x' - cache: pnpm - - - name: Install dependencies - run: pnpm install + - name: Setup frontend + uses: ./.github/actions/setup-frontend - name: Download size data - uses: dawidd6/action-download-artifact@v11 + uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12 with: name: size-data run_id: ${{ github.event_name == 'workflow_dispatch' && inputs.run_id || github.event.workflow_run.id }} @@ -75,7 +64,7 @@ jobs: fi - name: Download previous size data - uses: dawidd6/action-download-artifact@v11 + uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12 with: branch: ${{ steps.pr-base.outputs.content }} workflow: ci-size-data.yaml @@ -89,12 +78,12 @@ jobs: - name: Read size report id: size-report - uses: juliangruber/read-file-action@v1 + uses: juliangruber/read-file-action@b549046febe0fe86f8cb4f93c24e284433f9ab58 # v1.1.7 with: path: ./size-report.md - name: Create or update PR comment - uses: actions-cool/maintain-one-comment@v3 + uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3.2.0 with: token: ${{ secrets.GITHUB_TOKEN }} number: ${{ steps.pr-number.outputs.content }} diff --git a/.github/workflows/pr-update-playwright-expectations.yaml b/.github/workflows/pr-update-playwright-expectations.yaml index 628bc3039..dbb0a4c04 100644 --- a/.github/workflows/pr-update-playwright-expectations.yaml +++ b/.github/workflows/pr-update-playwright-expectations.yaml @@ -38,7 +38,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Find Update Comment - uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad + uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0 id: 'find-update-comment' with: issue-number: ${{ steps.pr-info.outputs.pr-number }} @@ -46,7 +46,7 @@ jobs: body-includes: 'Updating Playwright Expectations' - name: Add Starting Reaction - uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 + uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0 with: comment-id: ${{ steps.find-update-comment.outputs.comment-id }} issue-number: ${{ steps.pr-info.outputs.pr-number }} @@ -56,7 +56,7 @@ jobs: reactions: eyes - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: ${{ steps.pr-info.outputs.branch }} - name: Setup frontend @@ -66,7 +66,7 @@ jobs: # Upload built dist/ (containerized test jobs will pnpm install without cache) - name: Upload built frontend - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: frontend-dist path: dist/ @@ -91,11 +91,11 @@ jobs: shardTotal: [4] steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: ${{ needs.setup.outputs.branch }} - name: Download built frontend - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: name: frontend-dist path: dist/ @@ -149,7 +149,7 @@ jobs: # Upload ONLY the changed files from this shard - name: Upload changed snapshots - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 if: steps.changed-snapshots.outputs.has-changes == 'true' with: name: snapshots-shard-${{ matrix.shardIndex }} @@ -157,7 +157,7 @@ jobs: retention-days: 1 - name: Upload test report - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 if: always() with: name: playwright-report-shard-${{ matrix.shardIndex }} @@ -170,13 +170,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: ${{ needs.setup.outputs.branch }} # Download all changed snapshot files from shards - name: Download snapshot artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: pattern: snapshots-shard-* path: ./downloaded-snapshots @@ -301,7 +301,7 @@ jobs: echo "✓ Commit and push successful" - name: Add Done Reaction - uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 + uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0 if: github.event_name == 'issue_comment' && steps.commit.outputs.has-changes == 'true' with: comment-id: ${{ needs.setup.outputs.comment-id }} diff --git a/.github/workflows/publish-desktop-ui-on-merge.yaml b/.github/workflows/publish-desktop-ui-on-merge.yaml index 253f73cab..5036bc97b 100644 --- a/.github/workflows/publish-desktop-ui-on-merge.yaml +++ b/.github/workflows/publish-desktop-ui-on-merge.yaml @@ -20,13 +20,13 @@ jobs: dist_tag: ${{ steps.dist.outputs.dist_tag }} steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.merge_commit_sha }} persist-credentials: false - name: Setup Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: '24.x' @@ -71,7 +71,7 @@ jobs: pull-requests: write steps: - name: Checkout merge commit - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.merge_commit_sha }} fetch-depth: 2 diff --git a/.github/workflows/publish-desktop-ui.yaml b/.github/workflows/publish-desktop-ui.yaml index d2741d792..2a40445a5 100644 --- a/.github/workflows/publish-desktop-ui.yaml +++ b/.github/workflows/publish-desktop-ui.yaml @@ -77,19 +77,19 @@ jobs: fi - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: ${{ steps.resolve_ref.outputs.ref }} fetch-depth: 1 persist-credentials: false - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 with: version: 10 - name: Setup Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: '24.x' cache: 'pnpm' diff --git a/.github/workflows/release-biweekly-comfyui.yaml b/.github/workflows/release-biweekly-comfyui.yaml index 8c75548ce..be25e5ed7 100644 --- a/.github/workflows/release-biweekly-comfyui.yaml +++ b/.github/workflows/release-biweekly-comfyui.yaml @@ -61,13 +61,13 @@ jobs: steps: - name: Checkout ComfyUI_frontend - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 path: frontend - name: Checkout ComfyUI (sparse) - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: repository: Comfy-Org/ComfyUI sparse-checkout: | @@ -75,12 +75,12 @@ jobs: path: comfyui - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 with: version: 10 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: lts/* @@ -169,7 +169,7 @@ jobs: steps: - name: Checkout ComfyUI fork - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: repository: ${{ inputs.comfyui_fork || 'Comfy-Org/ComfyUI' }} token: ${{ secrets.PR_GH_TOKEN }} diff --git a/.github/workflows/release-branch-create.yaml b/.github/workflows/release-branch-create.yaml index 3e7290fdf..3ea488fdf 100644 --- a/.github/workflows/release-branch-create.yaml +++ b/.github/workflows/release-branch-create.yaml @@ -18,13 +18,13 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 token: ${{ secrets.PR_GH_TOKEN || secrets.GITHUB_TOKEN }} - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: 'lts/*' diff --git a/.github/workflows/release-draft-create.yaml b/.github/workflows/release-draft-create.yaml index 0bcb29159..1e32e450b 100644 --- a/.github/workflows/release-draft-create.yaml +++ b/.github/workflows/release-draft-create.yaml @@ -19,12 +19,12 @@ jobs: is_prerelease: ${{ steps.check_prerelease.outputs.is_prerelease }} steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 with: version: 10 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: 'lts/*' cache: 'pnpm' @@ -55,7 +55,7 @@ jobs: pnpm build pnpm zipdist - name: Upload dist artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: dist-files path: | @@ -66,16 +66,13 @@ jobs: needs: build runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v5 - name: Download dist artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: name: dist-files - name: Create release id: create_release - uses: >- - softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 + uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -98,13 +95,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Download dist artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: name: dist-files - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: '3.x' - name: Install build dependencies @@ -119,8 +116,7 @@ jobs: env: COMFYUI_FRONTEND_VERSION: ${{ needs.build.outputs.version }} - name: Publish pypi package - uses: >- - pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 with: password: ${{ secrets.PYPI_TOKEN }} packages-dir: comfyui_frontend_package/dist @@ -147,7 +143,7 @@ jobs: pull-requests: write steps: - name: Checkout merge commit - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.merge_commit_sha }} fetch-depth: 2 diff --git a/.github/workflows/release-npm-types.yaml b/.github/workflows/release-npm-types.yaml index 23f0cc016..21614c8a4 100644 --- a/.github/workflows/release-npm-types.yaml +++ b/.github/workflows/release-npm-types.yaml @@ -69,18 +69,18 @@ jobs: fi - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: ${{ steps.resolve_ref.outputs.ref }} fetch-depth: 1 - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 with: version: 10 - name: Setup Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: 'lts/*' cache: 'pnpm' diff --git a/.github/workflows/release-pypi-dev.yaml b/.github/workflows/release-pypi-dev.yaml index 868321759..5b102dbe8 100644 --- a/.github/workflows/release-pypi-dev.yaml +++ b/.github/workflows/release-pypi-dev.yaml @@ -15,12 +15,12 @@ jobs: version: ${{ steps.current_version.outputs.version }} steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 with: version: 10 - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v6 with: node-version: 'lts/*' cache: 'pnpm' @@ -40,7 +40,7 @@ jobs: pnpm build pnpm zipdist - name: Upload dist artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: dist-files path: | @@ -52,13 +52,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Download dist artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v7 with: name: dist-files - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: '3.x' - name: Install build dependencies @@ -73,7 +73,7 @@ jobs: env: COMFYUI_FRONTEND_VERSION: ${{ format('{0}.dev{1}', needs.build.outputs.version, inputs.devVersion) }} - name: Publish pypi package - uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 with: password: ${{ secrets.PYPI_TOKEN }} packages-dir: comfyui_frontend_package/dist diff --git a/.github/workflows/release-version-bump.yaml b/.github/workflows/release-version-bump.yaml index 4f0d033d9..d7ba7358e 100644 --- a/.github/workflows/release-version-bump.yaml +++ b/.github/workflows/release-version-bump.yaml @@ -65,7 +65,7 @@ jobs: - name: Close stale nightly version bump PRs if: github.event_name == 'schedule' - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: github-token: ${{ github.token }} script: | @@ -118,7 +118,7 @@ jobs: core.info(`Closed ${closed.length} stale PR(s).`) - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: ${{ steps.prepared-inputs.outputs.branch }} fetch-depth: 0 @@ -142,12 +142,12 @@ jobs: echo "✅ Branch '$BRANCH' exists" - name: Install pnpm - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 with: version: 10 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: lts/* @@ -180,7 +180,7 @@ jobs: echo "capitalised=${CAPITALISED_TYPE@u}" >> "$GITHUB_OUTPUT" - name: Create Pull Request - uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: token: ${{ secrets.PR_GH_TOKEN }} commit-message: '[release] Increment version to ${{ steps.bump-version.outputs.NEW_VERSION }}' diff --git a/.github/workflows/version-bump-desktop-ui.yaml b/.github/workflows/version-bump-desktop-ui.yaml index 0a8aca1b7..0ae940639 100644 --- a/.github/workflows/version-bump-desktop-ui.yaml +++ b/.github/workflows/version-bump-desktop-ui.yaml @@ -29,7 +29,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: ref: ${{ github.event.inputs.branch }} fetch-depth: 0 @@ -51,12 +51,12 @@ jobs: echo "✅ Branch '$BRANCH' exists" - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 with: version: 10 - name: Setup Node.js - uses: actions/setup-node@v5 + uses: actions/setup-node@v6 with: node-version: '24.x' cache: 'pnpm' @@ -79,7 +79,7 @@ jobs: echo "capitalised=${VERSION_TYPE@u}" >> $GITHUB_OUTPUT - name: Create Pull Request - uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: token: ${{ secrets.PR_GH_TOKEN }} commit-message: '[release] Increment desktop-ui to ${{ steps.bump-version.outputs.NEW_VERSION }}' diff --git a/.github/workflows/weekly-docs-check.yaml b/.github/workflows/weekly-docs-check.yaml index d5c8dc51e..81519e49e 100644 --- a/.github/workflows/weekly-docs-check.yaml +++ b/.github/workflows/weekly-docs-check.yaml @@ -22,18 +22,18 @@ jobs: timeout-minutes: 45 steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: - fetch-depth: 0 + fetch-depth: 50 ref: main - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0 with: version: 10 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: '20' cache: 'pnpm' @@ -49,7 +49,7 @@ jobs: fi - name: Run Claude Documentation Review - uses: anthropics/claude-code-action@v1.0.6 + uses: anthropics/claude-code-action@ff34ce0ff04a470bd3fa56c1ef391c8f1c19f8e9 # v1.0.38 with: prompt: | Is all documentation still 100% accurate? @@ -130,7 +130,7 @@ jobs: - name: Create or Update Pull Request if: steps.check_changes.outputs.has_changes == 'true' - uses: peter-evans/create-pull-request@v7 + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 with: token: ${{ secrets.PR_GH_TOKEN }} commit-message: 'docs: weekly documentation accuracy update' diff --git a/.pinact.yaml b/.pinact.yaml new file mode 100644 index 000000000..03fade044 --- /dev/null +++ b/.pinact.yaml @@ -0,0 +1,24 @@ +# pinact configuration +# https://github.com/suzuki-shunsuke/pinact +version: 3 + +files: + - pattern: .github/workflows/*.yaml + - pattern: .github/actions/**/*.yaml + +# Actions that don't need SHA pinning (official GitHub actions are trusted) +ignore_actions: + - name: actions/cache + ref: v5 + - name: actions/checkout + ref: v6 + - name: actions/setup-node + ref: v6 + - name: actions/setup-python + ref: v6 + - name: actions/upload-artifact + ref: v6 + - name: actions/download-artifact + ref: v7 + - name: actions/github-script + ref: v8 diff --git a/.yamllint b/.yamllint index 9108997d4..3a7b25244 100644 --- a/.yamllint +++ b/.yamllint @@ -8,3 +8,6 @@ rules: line-length: disable document-start: disable truthy: disable + comments: + min-spaces-from-content: 1 + \ No newline at end of file From 3b5d124029001710091495f62e601dac78e5c4f5 Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Wed, 28 Jan 2026 22:12:28 -0800 Subject: [PATCH 14/64] fix: use getAuthHeader in createCustomer to support API key auth (#8408) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes authentication failure when using API key authentication on staging server after frontend update to 1.33.10. image ## Changes - **What**: Changed `createCustomer()` to use `getAuthHeader()` instead of `getFirebaseAuthHeader()`, allowing API key users to authenticate successfully ## Review Focus - Verify `getAuthHeader()` correctly falls back to API key when no Firebase token exists - Backend `/customers` endpoint supports `X-API-KEY` header (per cloud PR #1766) Fixes COM-12398 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8408-fix-use-getAuthHeader-in-createCustomer-to-support-API-key-auth-2f76d73d3650819994e3e6d3ed9f3dfa) by [Unito](https://www.unito.io) Co-authored-by: Subagent 5 Co-authored-by: Amp --- src/stores/firebaseAuthStore.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/firebaseAuthStore.ts b/src/stores/firebaseAuthStore.ts index 41bae52a2..32ae22a9a 100644 --- a/src/stores/firebaseAuthStore.ts +++ b/src/stores/firebaseAuthStore.ts @@ -278,7 +278,7 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { } const createCustomer = async (): Promise => { - const authHeader = await getFirebaseAuthHeader() + const authHeader = await getAuthHeader() if (!authHeader) { throw new FirebaseAuthStoreError(t('toastMessages.userNotAuthenticated')) } From 6ce60a11a4855ad2d0b0386b0a38538b281cb19f Mon Sep 17 00:00:00 2001 From: Alexander Brown Date: Wed, 28 Jan 2026 22:21:38 -0800 Subject: [PATCH 15/64] test: use createTestingPinia instead of createPinia (#8376) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace \createPinia\ with \createTestingPinia({ stubActions: false })\ from \@pinia/testing\ across 45 test files for proper test isolation. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8376-test-use-createTestingPinia-instead-of-createPinia-2f66d73d36508137a9f0daffcddc86f7) by [Unito](https://www.unito.io) Co-authored-by: Amp --- docs/testing/store-testing.md | 7 ++++--- .../composables/useSubscriptionCredits.test.ts | 5 +++-- .../updates/common/versionCompatibilityStore.test.ts | 5 +++-- .../workflow/management/stores/workflowStore.test.ts | 5 +++-- .../composables/useWorkflowPersistence.test.ts | 5 +++-- src/platform/workspace/stores/teamWorkspaceStore.test.ts | 5 +++-- .../minimap/composables/useMinimapSettings.test.ts | 5 +++-- .../extensions/vueNodes/components/NodeHeader.test.ts | 5 +++-- .../extensions/vueNodes/components/NodeSlots.test.ts | 4 ++-- src/services/keybindingService.escape.test.ts | 5 +++-- src/services/keybindingService.forwarding.test.ts | 5 +++-- src/stores/comfyRegistryStore.test.ts | 5 +++-- src/stores/dialogStore.test.ts | 5 +++-- src/stores/domWidgetStore.test.ts | 5 +++-- src/stores/executionStore.test.ts | 7 ++++--- src/stores/firebaseAuthStore.test.ts | 7 ++++--- src/stores/imagePreviewStore.test.ts | 5 +++-- src/stores/keybindingStore.test.ts | 5 +++-- src/stores/modelStore.test.ts | 5 +++-- src/stores/modelToNodeStore.test.ts | 9 +++++---- src/stores/nodeDefStore.test.ts | 5 +++-- src/stores/queueStore.loadWorkflow.test.ts | 5 +++-- src/stores/queueStore.test.ts | 5 +++-- src/stores/serverConfigStore.test.ts | 5 +++-- src/stores/subgraphNavigationStore.test.ts | 5 +++-- src/stores/subgraphNavigationStore.viewport.test.ts | 5 +++-- src/stores/subgraphStore.test.ts | 5 +++-- src/stores/systemStatsStore.test.ts | 5 +++-- src/stores/templateRankingStore.test.ts | 5 +++-- src/stores/userFileStore.test.ts | 5 +++-- src/stores/workspace/bottomPanelStore.test.ts | 5 +++-- src/stores/workspace/nodeHelpStore.test.ts | 5 +++-- src/stores/workspace/searchBoxStore.test.ts | 5 +++-- .../components/manager/NodeConflictDialogContent.test.ts | 7 ++++--- .../manager/components/manager/PackVersionBadge.test.ts | 4 ++-- .../manager/PackVersionSelectorPopover.test.ts | 4 ++-- .../components/manager/button/PackEnableToggle.test.ts | 4 ++-- .../components/manager/packCard/PackCardFooter.test.ts | 4 ++-- .../manager/skeleton/PackCardGridSkeleton.test.ts | 4 ++-- .../composables/nodePack/usePacksSelection.test.ts | 5 +++-- .../manager/composables/nodePack/usePacksStatus.test.ts | 5 +++-- .../composables/useConflictAcknowledgment.test.ts | 5 +++-- .../manager/composables/useConflictDetection.test.ts | 7 ++++--- .../manager/composables/useImportFailedDetection.test.ts | 5 +++-- .../extensions/manager/stores/comfyManagerStore.test.ts | 5 +++-- .../manager/stores/conflictDetectionStore.test.ts | 5 +++-- 46 files changed, 139 insertions(+), 99 deletions(-) diff --git a/docs/testing/store-testing.md b/docs/testing/store-testing.md index aec716c07..9b736fa36 100644 --- a/docs/testing/store-testing.md +++ b/docs/testing/store-testing.md @@ -18,7 +18,8 @@ Basic setup for testing Pinia stores: ```typescript // Example from: tests-ui/tests/store/workflowStore.test.ts -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' import { useWorkflowStore } from '@/domains/workflow/ui/stores/workflowStore' @@ -27,8 +28,8 @@ describe('useWorkflowStore', () => { let store: ReturnType beforeEach(() => { - // Create a fresh pinia and activate it for each test - setActivePinia(createPinia()) + // Create a fresh testing pinia and activate it for each test + setActivePinia(createTestingPinia({ stubActions: false })) // Initialize the store store = useWorkflowStore() diff --git a/src/platform/cloud/subscription/composables/useSubscriptionCredits.test.ts b/src/platform/cloud/subscription/composables/useSubscriptionCredits.test.ts index dde7313e4..8e3ef1dc1 100644 --- a/src/platform/cloud/subscription/composables/useSubscriptionCredits.test.ts +++ b/src/platform/cloud/subscription/composables/useSubscriptionCredits.test.ts @@ -1,4 +1,5 @@ -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' import type * as VueI18nModule from 'vue-i18n' @@ -79,7 +80,7 @@ describe('useSubscriptionCredits', () => { let authStore: ReturnType beforeEach(() => { - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) authStore = useFirebaseAuthStore() vi.clearAllMocks() }) diff --git a/src/platform/updates/common/versionCompatibilityStore.test.ts b/src/platform/updates/common/versionCompatibilityStore.test.ts index 3fdb80eaa..06ba4aea8 100644 --- a/src/platform/updates/common/versionCompatibilityStore.test.ts +++ b/src/platform/updates/common/versionCompatibilityStore.test.ts @@ -1,5 +1,6 @@ import { until } from '@vueuse/core' -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { ref } from 'vue' @@ -40,7 +41,7 @@ describe('useVersionCompatibilityStore', () => { let mockSettingStore: { get: ReturnType } beforeEach(() => { - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) // Clear the mock dismissal storage mockDismissalStorage.value = {} diff --git a/src/platform/workflow/management/stores/workflowStore.test.ts b/src/platform/workflow/management/stores/workflowStore.test.ts index fad21ccfc..f13daf21c 100644 --- a/src/platform/workflow/management/stores/workflowStore.test.ts +++ b/src/platform/workflow/management/stores/workflowStore.test.ts @@ -1,4 +1,5 @@ -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' import { nextTick } from 'vue' @@ -66,7 +67,7 @@ describe('useWorkflowStore', () => { } beforeEach(() => { - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) store = useWorkflowStore() bookmarkStore = useWorkflowBookmarkStore() vi.clearAllMocks() diff --git a/src/platform/workflow/persistence/composables/useWorkflowPersistence.test.ts b/src/platform/workflow/persistence/composables/useWorkflowPersistence.test.ts index 76dac06c6..0fb69f65d 100644 --- a/src/platform/workflow/persistence/composables/useWorkflowPersistence.test.ts +++ b/src/platform/workflow/persistence/composables/useWorkflowPersistence.test.ts @@ -1,4 +1,5 @@ -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type * as I18n from 'vue-i18n' @@ -108,7 +109,7 @@ describe('useWorkflowPersistence', () => { beforeEach(() => { vi.useFakeTimers() vi.setSystemTime(new Date('2025-01-01T00:00:00Z')) - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) localStorage.clear() sessionStorage.clear() vi.clearAllMocks() diff --git a/src/platform/workspace/stores/teamWorkspaceStore.test.ts b/src/platform/workspace/stores/teamWorkspaceStore.test.ts index a66be714a..92b712787 100644 --- a/src/platform/workspace/stores/teamWorkspaceStore.test.ts +++ b/src/platform/workspace/stores/teamWorkspaceStore.test.ts @@ -1,4 +1,5 @@ -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { useTeamWorkspaceStore } from './teamWorkspaceStore' @@ -111,7 +112,7 @@ const mockMemberWorkspace = { describe('useTeamWorkspaceStore', () => { beforeEach(() => { - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) vi.clearAllMocks() vi.stubGlobal('localStorage', mockLocalStorage) sessionStorage.clear() diff --git a/src/renderer/extensions/minimap/composables/useMinimapSettings.test.ts b/src/renderer/extensions/minimap/composables/useMinimapSettings.test.ts index 448f64090..44d2d0f57 100644 --- a/src/renderer/extensions/minimap/composables/useMinimapSettings.test.ts +++ b/src/renderer/extensions/minimap/composables/useMinimapSettings.test.ts @@ -1,4 +1,5 @@ -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' import { useSettingStore } from '@/platform/settings/settingStore' @@ -15,7 +16,7 @@ vi.mock('@/stores/workspace/colorPaletteStore', () => ({ describe('useMinimapSettings', () => { beforeEach(() => { - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) vi.clearAllMocks() }) diff --git a/src/renderer/extensions/vueNodes/components/NodeHeader.test.ts b/src/renderer/extensions/vueNodes/components/NodeHeader.test.ts index a813e88af..e5bb31224 100644 --- a/src/renderer/extensions/vueNodes/components/NodeHeader.test.ts +++ b/src/renderer/extensions/vueNodes/components/NodeHeader.test.ts @@ -1,5 +1,6 @@ +import { createTestingPinia } from '@pinia/testing' import { mount } from '@vue/test-utils' -import { createPinia, setActivePinia } from 'pinia' +import { setActivePinia } from 'pinia' import PrimeVue from 'primevue/config' import InputText from 'primevue/inputtext' import { describe, expect, it, vi } from 'vitest' @@ -29,7 +30,7 @@ const makeNodeData = (overrides: Partial = {}): VueNodeData => ({ }) const setupMockStores = () => { - const pinia = createPinia() + const pinia = createTestingPinia({ stubActions: false }) setActivePinia(pinia) const settingStore = useSettingStore() diff --git a/src/renderer/extensions/vueNodes/components/NodeSlots.test.ts b/src/renderer/extensions/vueNodes/components/NodeSlots.test.ts index 6f3c75adf..a8991f9f8 100644 --- a/src/renderer/extensions/vueNodes/components/NodeSlots.test.ts +++ b/src/renderer/extensions/vueNodes/components/NodeSlots.test.ts @@ -1,6 +1,6 @@ /* eslint-disable vue/one-component-per-file */ +import { createTestingPinia } from '@pinia/testing' import { mount } from '@vue/test-utils' -import { createPinia } from 'pinia' import { describe, expect, it } from 'vitest' import { defineComponent } from 'vue' import type { PropType } from 'vue' @@ -84,7 +84,7 @@ const mountSlots = (nodeData: VueNodeData, readonly = false) => { }) return mount(NodeSlots, { global: { - plugins: [i18n, createPinia()], + plugins: [i18n, createTestingPinia({ stubActions: false })], stubs: { InputSlot: InputSlotStub, OutputSlot: OutputSlotStub diff --git a/src/services/keybindingService.escape.test.ts b/src/services/keybindingService.escape.test.ts index 053b48bf4..caaf10e46 100644 --- a/src/services/keybindingService.escape.test.ts +++ b/src/services/keybindingService.escape.test.ts @@ -1,4 +1,5 @@ -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' import { CORE_KEYBINDINGS } from '@/constants/coreKeybindings' @@ -30,7 +31,7 @@ describe('keybindingService - Escape key handling', () => { beforeEach(() => { vi.clearAllMocks() - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) // Mock command store execute const commandStore = useCommandStore() diff --git a/src/services/keybindingService.forwarding.test.ts b/src/services/keybindingService.forwarding.test.ts index 1c62222dd..bd3951f56 100644 --- a/src/services/keybindingService.forwarding.test.ts +++ b/src/services/keybindingService.forwarding.test.ts @@ -1,4 +1,5 @@ -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' import { app } from '@/scripts/app' @@ -68,7 +69,7 @@ describe('keybindingService - Event Forwarding', () => { beforeEach(() => { vi.clearAllMocks() - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) // Mock command store execute const commandStore = useCommandStore() diff --git a/src/stores/comfyRegistryStore.test.ts b/src/stores/comfyRegistryStore.test.ts index fa2c86ab7..f1c1d76cf 100644 --- a/src/stores/comfyRegistryStore.test.ts +++ b/src/stores/comfyRegistryStore.test.ts @@ -1,4 +1,5 @@ -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { ref } from 'vue' @@ -83,7 +84,7 @@ describe('useComfyRegistryStore', () => { } beforeEach(() => { - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) vi.clearAllMocks() mockRegistryService = { isLoading: ref(false), diff --git a/src/stores/dialogStore.test.ts b/src/stores/dialogStore.test.ts index 3d21ff695..67cfa3788 100644 --- a/src/stores/dialogStore.test.ts +++ b/src/stores/dialogStore.test.ts @@ -1,4 +1,5 @@ -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { beforeEach, describe, expect, it } from 'vitest' import { defineComponent } from 'vue' @@ -11,7 +12,7 @@ const MockComponent = defineComponent({ describe('dialogStore', () => { beforeEach(() => { - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) }) describe('priority system', () => { diff --git a/src/stores/domWidgetStore.test.ts b/src/stores/domWidgetStore.test.ts index 21a83f7c6..24c2d47e6 100644 --- a/src/stores/domWidgetStore.test.ts +++ b/src/stores/domWidgetStore.test.ts @@ -1,4 +1,5 @@ -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { beforeEach, describe, expect, it } from 'vitest' import { useDomWidgetStore } from '@/stores/domWidgetStore' @@ -30,7 +31,7 @@ describe('domWidgetStore', () => { let store: ReturnType beforeEach(() => { - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) store = useDomWidgetStore() }) diff --git a/src/stores/executionStore.test.ts b/src/stores/executionStore.test.ts index 75b6f4a43..f15c6a8dc 100644 --- a/src/stores/executionStore.test.ts +++ b/src/stores/executionStore.test.ts @@ -1,4 +1,5 @@ -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' import { app } from '@/scripts/app' @@ -59,7 +60,7 @@ describe('useExecutionStore - NodeLocatorId conversions', () => { mockNodeIdToNodeLocatorId.mockReset() mockNodeLocatorIdToNodeExecutionId.mockReset() - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) store = useExecutionStore() }) @@ -137,7 +138,7 @@ describe('useExecutionStore - Node Error Lookups', () => { beforeEach(() => { vi.clearAllMocks() - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) store = useExecutionStore() }) diff --git a/src/stores/firebaseAuthStore.test.ts b/src/stores/firebaseAuthStore.test.ts index 54beac934..47ed75c25 100644 --- a/src/stores/firebaseAuthStore.test.ts +++ b/src/stores/firebaseAuthStore.test.ts @@ -1,6 +1,7 @@ import { FirebaseError } from 'firebase/app' import * as firebaseAuth from 'firebase/auth' -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' import * as vuefire from 'vuefire' @@ -153,7 +154,7 @@ describe('useFirebaseAuthStore', () => { }) // Initialize Pinia - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) store = useFirebaseAuthStore() // Reset and set up getIdToken mock @@ -175,7 +176,7 @@ describe('useFirebaseAuthStore', () => { vi.mocked(vuefire.useFirebaseAuth).mockReturnValue(mockAuth as any) - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) store = useFirebaseAuthStore() }) diff --git a/src/stores/imagePreviewStore.test.ts b/src/stores/imagePreviewStore.test.ts index d2bb8d639..4b796e479 100644 --- a/src/stores/imagePreviewStore.test.ts +++ b/src/stores/imagePreviewStore.test.ts @@ -1,4 +1,5 @@ -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { LGraphNode } from '@/lib/litegraph/src/litegraph' @@ -30,7 +31,7 @@ const createMockOutputs = ( describe('imagePreviewStore getPreviewParam', () => { beforeEach(() => { - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) vi.clearAllMocks() vi.mocked(litegraphUtil.isVideoNode).mockReturnValue(false) }) diff --git a/src/stores/keybindingStore.test.ts b/src/stores/keybindingStore.test.ts index 120f8c174..1f7e1c6e2 100644 --- a/src/stores/keybindingStore.test.ts +++ b/src/stores/keybindingStore.test.ts @@ -1,11 +1,12 @@ -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { beforeEach, describe, expect, it } from 'vitest' import { KeybindingImpl, useKeybindingStore } from '@/stores/keybindingStore' describe('useKeybindingStore', () => { beforeEach(() => { - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) }) it('should add and retrieve default keybindings', () => { diff --git a/src/stores/modelStore.test.ts b/src/stores/modelStore.test.ts index c9ed82e55..2bc6f6ab3 100644 --- a/src/stores/modelStore.test.ts +++ b/src/stores/modelStore.test.ts @@ -1,4 +1,5 @@ -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' import { assetService } from '@/platform/assets/services/assetService' @@ -89,7 +90,7 @@ describe('useModelStore', () => { let store: ReturnType beforeEach(async () => { - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) vi.resetAllMocks() }) diff --git a/src/stores/modelToNodeStore.test.ts b/src/stores/modelToNodeStore.test.ts index c79af19b5..6e804151b 100644 --- a/src/stores/modelToNodeStore.test.ts +++ b/src/stores/modelToNodeStore.test.ts @@ -1,4 +1,5 @@ -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { ComfyNodeDef as ComfyNodeDefV1 } from '@/schemas/nodeDefSchema' @@ -82,7 +83,7 @@ vi.mock('@/stores/nodeDefStore', async (importOriginal) => { describe('useModelToNodeStore', () => { beforeEach(() => { - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) vi.clearAllMocks() }) @@ -330,7 +331,7 @@ describe('useModelToNodeStore', () => { it('should not register when nodeDefStore is empty', () => { // Create fresh Pinia for this test to avoid state persistence - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) vi.mocked(useNodeDefStore, { partial: true }).mockReturnValue({ nodeDefsByName: {} @@ -355,7 +356,7 @@ describe('useModelToNodeStore', () => { it('should return empty Record when nodeDefStore is empty', () => { // Create fresh Pinia for this test to avoid state persistence - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) vi.mocked(useNodeDefStore, { partial: true }).mockReturnValue({ nodeDefsByName: {} diff --git a/src/stores/nodeDefStore.test.ts b/src/stores/nodeDefStore.test.ts index d4d9828d4..242b457b6 100644 --- a/src/stores/nodeDefStore.test.ts +++ b/src/stores/nodeDefStore.test.ts @@ -1,4 +1,5 @@ -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { beforeEach, describe, expect, it } from 'vitest' import type { ComfyNodeDef } from '@/schemas/nodeDefSchema' @@ -9,7 +10,7 @@ describe('useNodeDefStore', () => { let store: ReturnType beforeEach(() => { - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) store = useNodeDefStore() }) diff --git a/src/stores/queueStore.loadWorkflow.test.ts b/src/stores/queueStore.loadWorkflow.test.ts index 8b9ae30a5..c0c13033c 100644 --- a/src/stores/queueStore.loadWorkflow.test.ts +++ b/src/stores/queueStore.loadWorkflow.test.ts @@ -1,4 +1,5 @@ -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { @@ -71,7 +72,7 @@ describe('TaskItemImpl.loadWorkflow - workflow fetching', () => { let mockFetchApi: ReturnType beforeEach(() => { - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) vi.clearAllMocks() mockFetchApi = vi.fn() diff --git a/src/stores/queueStore.test.ts b/src/stores/queueStore.test.ts index 063658b2f..0f3e8c0f1 100644 --- a/src/stores/queueStore.test.ts +++ b/src/stores/queueStore.test.ts @@ -1,4 +1,5 @@ -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { JobListItem } from '@/platform/remote/comfyui/jobs/jobTypes' @@ -240,7 +241,7 @@ describe('useQueueStore', () => { let store: ReturnType beforeEach(() => { - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) store = useQueueStore() vi.clearAllMocks() }) diff --git a/src/stores/serverConfigStore.test.ts b/src/stores/serverConfigStore.test.ts index dd25c5b64..ee4c9cc54 100644 --- a/src/stores/serverConfigStore.test.ts +++ b/src/stores/serverConfigStore.test.ts @@ -1,4 +1,5 @@ -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { beforeEach, describe, expect, it } from 'vitest' import type { ServerConfig } from '@/constants/serverConfig' @@ -14,7 +15,7 @@ describe('useServerConfigStore', () => { let store: ReturnType beforeEach(() => { - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) store = useServerConfigStore() }) diff --git a/src/stores/subgraphNavigationStore.test.ts b/src/stores/subgraphNavigationStore.test.ts index e16700602..bf38a0324 100644 --- a/src/stores/subgraphNavigationStore.test.ts +++ b/src/stores/subgraphNavigationStore.test.ts @@ -1,4 +1,5 @@ -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' import { nextTick } from 'vue' @@ -47,7 +48,7 @@ vi.mock('@/utils/graphTraversalUtil', () => ({ describe('useSubgraphNavigationStore', () => { beforeEach(() => { - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) }) it('should not clear navigation stack when workflow internal state changes', async () => { diff --git a/src/stores/subgraphNavigationStore.viewport.test.ts b/src/stores/subgraphNavigationStore.viewport.test.ts index 6dbf85a6b..fe52138d3 100644 --- a/src/stores/subgraphNavigationStore.viewport.test.ts +++ b/src/stores/subgraphNavigationStore.viewport.test.ts @@ -1,4 +1,5 @@ -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' import { nextTick } from 'vue' @@ -46,7 +47,7 @@ const mockCanvas = app.canvas as any describe('useSubgraphNavigationStore - Viewport Persistence', () => { beforeEach(() => { - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) // Reset canvas state mockCanvas.ds.scale = 1 mockCanvas.ds.offset = [0, 0] diff --git a/src/stores/subgraphStore.test.ts b/src/stores/subgraphStore.test.ts index 68f528ae4..e8b49f947 100644 --- a/src/stores/subgraphStore.test.ts +++ b/src/stores/subgraphStore.test.ts @@ -1,4 +1,5 @@ -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { ComfyNodeDef as ComfyNodeDefV1 } from '@/schemas/nodeDefSchema' @@ -78,7 +79,7 @@ describe('useSubgraphStore', () => { } beforeEach(() => { - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) store = useSubgraphStore() vi.clearAllMocks() }) diff --git a/src/stores/systemStatsStore.test.ts b/src/stores/systemStatsStore.test.ts index 49653c780..a6f070481 100644 --- a/src/stores/systemStatsStore.test.ts +++ b/src/stores/systemStatsStore.test.ts @@ -1,4 +1,5 @@ -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' import { api } from '@/scripts/api' @@ -25,7 +26,7 @@ describe('useSystemStatsStore', () => { beforeEach(() => { // Mock API to prevent automatic fetch on store creation vi.mocked(api.getSystemStats).mockResolvedValue(null as any) - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) store = useSystemStatsStore() vi.clearAllMocks() }) diff --git a/src/stores/templateRankingStore.test.ts b/src/stores/templateRankingStore.test.ts index cb3a9539d..12e7950fa 100644 --- a/src/stores/templateRankingStore.test.ts +++ b/src/stores/templateRankingStore.test.ts @@ -1,4 +1,5 @@ -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' import { useTemplateRankingStore } from '@/stores/templateRankingStore' @@ -12,7 +13,7 @@ vi.mock('axios', () => ({ describe('templateRankingStore', () => { beforeEach(() => { - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) vi.clearAllMocks() }) diff --git a/src/stores/userFileStore.test.ts b/src/stores/userFileStore.test.ts index dad0d3fa1..b94bd983b 100644 --- a/src/stores/userFileStore.test.ts +++ b/src/stores/userFileStore.test.ts @@ -1,4 +1,5 @@ -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' import { api } from '@/scripts/api' @@ -19,7 +20,7 @@ describe('useUserFileStore', () => { let store: ReturnType beforeEach(() => { - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) store = useUserFileStore() vi.resetAllMocks() }) diff --git a/src/stores/workspace/bottomPanelStore.test.ts b/src/stores/workspace/bottomPanelStore.test.ts index 7d9a2406e..edfa6960b 100644 --- a/src/stores/workspace/bottomPanelStore.test.ts +++ b/src/stores/workspace/bottomPanelStore.test.ts @@ -1,4 +1,5 @@ -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore' @@ -53,7 +54,7 @@ vi.mock('@/utils/envUtil', () => ({ describe('useBottomPanelStore', () => { beforeEach(() => { - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) }) it('should initialize with empty panels', () => { diff --git a/src/stores/workspace/nodeHelpStore.test.ts b/src/stores/workspace/nodeHelpStore.test.ts index 0514e9029..365df0b74 100644 --- a/src/stores/workspace/nodeHelpStore.test.ts +++ b/src/stores/workspace/nodeHelpStore.test.ts @@ -1,4 +1,5 @@ -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { beforeEach, describe, expect, it } from 'vitest' import { useNodeHelpStore } from '@/stores/workspace/nodeHelpStore' @@ -14,7 +15,7 @@ describe('nodeHelpStore', () => { } beforeEach(() => { - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) }) it('should initialize with empty state', () => { diff --git a/src/stores/workspace/searchBoxStore.test.ts b/src/stores/workspace/searchBoxStore.test.ts index 10a3ab7d4..e74111f2a 100644 --- a/src/stores/workspace/searchBoxStore.test.ts +++ b/src/stores/workspace/searchBoxStore.test.ts @@ -1,4 +1,5 @@ -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' import type NodeSearchBoxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue' @@ -34,7 +35,7 @@ function createMockSettingStore(): ReturnType { describe('useSearchBoxStore', () => { beforeEach(() => { - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) vi.restoreAllMocks() }) diff --git a/src/workbench/extensions/manager/components/manager/NodeConflictDialogContent.test.ts b/src/workbench/extensions/manager/components/manager/NodeConflictDialogContent.test.ts index e169ec9f0..c55b94dc2 100644 --- a/src/workbench/extensions/manager/components/manager/NodeConflictDialogContent.test.ts +++ b/src/workbench/extensions/manager/components/manager/NodeConflictDialogContent.test.ts @@ -1,5 +1,6 @@ import { mount } from '@vue/test-utils' -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import Button from '@/components/ui/button/Button.vue' import { beforeEach, describe, expect, it, vi } from 'vitest' import { computed, ref } from 'vue' @@ -44,11 +45,11 @@ vi.mock( ) describe('NodeConflictDialogContent', () => { - let pinia: ReturnType + let pinia: ReturnType beforeEach(() => { vi.clearAllMocks() - pinia = createPinia() + pinia = createTestingPinia({ stubActions: false }) setActivePinia(pinia) // Reset mock data mockConflictData.value = [] diff --git a/src/workbench/extensions/manager/components/manager/PackVersionBadge.test.ts b/src/workbench/extensions/manager/components/manager/PackVersionBadge.test.ts index d57b25f1f..ff846b9f1 100644 --- a/src/workbench/extensions/manager/components/manager/PackVersionBadge.test.ts +++ b/src/workbench/extensions/manager/components/manager/PackVersionBadge.test.ts @@ -1,6 +1,6 @@ import type { VueWrapper } from '@vue/test-utils' import { mount } from '@vue/test-utils' -import { createPinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' import PrimeVue from 'primevue/config' import Tooltip from 'primevue/tooltip' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -89,7 +89,7 @@ describe('PackVersionBadge', () => { ...props }, global: { - plugins: [PrimeVue, createPinia(), i18n], + plugins: [PrimeVue, createTestingPinia({ stubActions: false }), i18n], directives: { tooltip: Tooltip }, diff --git a/src/workbench/extensions/manager/components/manager/PackVersionSelectorPopover.test.ts b/src/workbench/extensions/manager/components/manager/PackVersionSelectorPopover.test.ts index 792983c22..a9337c7f1 100644 --- a/src/workbench/extensions/manager/components/manager/PackVersionSelectorPopover.test.ts +++ b/src/workbench/extensions/manager/components/manager/PackVersionSelectorPopover.test.ts @@ -1,6 +1,6 @@ import type { VueWrapper } from '@vue/test-utils' import { mount } from '@vue/test-utils' -import { createPinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' import Button from '@/components/ui/button/Button.vue' import PrimeVue from 'primevue/config' import Listbox from 'primevue/listbox' @@ -115,7 +115,7 @@ describe('PackVersionSelectorPopover', () => { ...props }, global: { - plugins: [PrimeVue, createPinia(), i18n], + plugins: [PrimeVue, createTestingPinia({ stubActions: false }), i18n], components: { Listbox, VerifiedIcon, diff --git a/src/workbench/extensions/manager/components/manager/button/PackEnableToggle.test.ts b/src/workbench/extensions/manager/components/manager/button/PackEnableToggle.test.ts index 54bbe5bb9..bfd40a9ce 100644 --- a/src/workbench/extensions/manager/components/manager/button/PackEnableToggle.test.ts +++ b/src/workbench/extensions/manager/components/manager/button/PackEnableToggle.test.ts @@ -1,6 +1,6 @@ import type { VueWrapper } from '@vue/test-utils' import { mount } from '@vue/test-utils' -import { createPinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' import PrimeVue from 'primevue/config' import ToggleSwitch from 'primevue/toggleswitch' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -81,7 +81,7 @@ describe('PackEnableToggle', () => { ...props }, global: { - plugins: [PrimeVue, createPinia(), i18n] + plugins: [PrimeVue, createTestingPinia({ stubActions: false }), i18n] } }) } diff --git a/src/workbench/extensions/manager/components/manager/packCard/PackCardFooter.test.ts b/src/workbench/extensions/manager/components/manager/packCard/PackCardFooter.test.ts index 950f8f647..9f6e1d5a6 100644 --- a/src/workbench/extensions/manager/components/manager/packCard/PackCardFooter.test.ts +++ b/src/workbench/extensions/manager/components/manager/packCard/PackCardFooter.test.ts @@ -1,6 +1,6 @@ import type { VueWrapper } from '@vue/test-utils' import { mount } from '@vue/test-utils' -import { createPinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' import PrimeVue from 'primevue/config' import { beforeEach, describe, expect, it, vi } from 'vitest' import { ref } from 'vue' @@ -73,7 +73,7 @@ describe('PackCardFooter', () => { ...props }, global: { - plugins: [PrimeVue, createPinia(), i18n], + plugins: [PrimeVue, createTestingPinia({ stubActions: false }), i18n], provide: { [IsInstallingKey]: ref(false) } diff --git a/src/workbench/extensions/manager/components/manager/skeleton/PackCardGridSkeleton.test.ts b/src/workbench/extensions/manager/components/manager/skeleton/PackCardGridSkeleton.test.ts index 1df7162d9..3a48d7eab 100644 --- a/src/workbench/extensions/manager/components/manager/skeleton/PackCardGridSkeleton.test.ts +++ b/src/workbench/extensions/manager/components/manager/skeleton/PackCardGridSkeleton.test.ts @@ -1,6 +1,6 @@ import type { VueWrapper } from '@vue/test-utils' import { mount } from '@vue/test-utils' -import { createPinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' import PrimeVue from 'primevue/config' import { describe, expect, it } from 'vitest' import { nextTick } from 'vue' @@ -32,7 +32,7 @@ describe('GridSkeleton', () => { ...props }, global: { - plugins: [PrimeVue, createPinia(), i18n], + plugins: [PrimeVue, createTestingPinia({ stubActions: false }), i18n], stubs: { PackCardSkeleton: true } diff --git a/src/workbench/extensions/manager/composables/nodePack/usePacksSelection.test.ts b/src/workbench/extensions/manager/composables/nodePack/usePacksSelection.test.ts index c38fc9c33..3a17a1ba1 100644 --- a/src/workbench/extensions/manager/composables/nodePack/usePacksSelection.test.ts +++ b/src/workbench/extensions/manager/composables/nodePack/usePacksSelection.test.ts @@ -1,4 +1,5 @@ -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { ref } from 'vue' @@ -36,7 +37,7 @@ describe('usePacksSelection', () => { beforeEach(() => { vi.clearAllMocks() - const pinia = createPinia() + const pinia = createTestingPinia({ stubActions: false }) setActivePinia(pinia) managerStore = useComfyManagerStore() diff --git a/src/workbench/extensions/manager/composables/nodePack/usePacksStatus.test.ts b/src/workbench/extensions/manager/composables/nodePack/usePacksStatus.test.ts index 2fbabb3fb..5977196ce 100644 --- a/src/workbench/extensions/manager/composables/nodePack/usePacksStatus.test.ts +++ b/src/workbench/extensions/manager/composables/nodePack/usePacksStatus.test.ts @@ -1,4 +1,5 @@ -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { ref } from 'vue' @@ -48,7 +49,7 @@ describe('usePacksStatus', () => { beforeEach(() => { vi.clearAllMocks() - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) conflictDetectionStore = useConflictDetectionStore() }) diff --git a/src/workbench/extensions/manager/composables/useConflictAcknowledgment.test.ts b/src/workbench/extensions/manager/composables/useConflictAcknowledgment.test.ts index 43f25da9e..dbd7a5f25 100644 --- a/src/workbench/extensions/manager/composables/useConflictAcknowledgment.test.ts +++ b/src/workbench/extensions/manager/composables/useConflictAcknowledgment.test.ts @@ -1,10 +1,11 @@ -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' describe('useConflictAcknowledgment', () => { beforeEach(() => { // Set up Pinia for each test - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) // Clear localStorage before each test localStorage.clear() // Reset modules to ensure fresh state diff --git a/src/workbench/extensions/manager/composables/useConflictDetection.test.ts b/src/workbench/extensions/manager/composables/useConflictDetection.test.ts index 28fa8302c..46aec9d2c 100644 --- a/src/workbench/extensions/manager/composables/useConflictDetection.test.ts +++ b/src/workbench/extensions/manager/composables/useConflictDetection.test.ts @@ -1,4 +1,5 @@ -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { computed, ref } from 'vue' @@ -114,7 +115,7 @@ vi.mock('@/workbench/extensions/manager/composables/useManagerState', () => ({ })) describe('useConflictDetection', () => { - let pinia: ReturnType + let pinia: ReturnType const mockComfyManagerService = { getImportFailInfoBulk: vi.fn(), @@ -221,7 +222,7 @@ describe('useConflictDetection', () => { beforeEach(() => { vi.clearAllMocks() - pinia = createPinia() + pinia = createTestingPinia({ stubActions: false }) setActivePinia(pinia) // Setup mocks diff --git a/src/workbench/extensions/manager/composables/useImportFailedDetection.test.ts b/src/workbench/extensions/manager/composables/useImportFailedDetection.test.ts index d548af8da..83944f4f8 100644 --- a/src/workbench/extensions/manager/composables/useImportFailedDetection.test.ts +++ b/src/workbench/extensions/manager/composables/useImportFailedDetection.test.ts @@ -1,4 +1,5 @@ -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' import { computed, ref } from 'vue' @@ -31,7 +32,7 @@ describe('useImportFailedDetection', () => { let mockDialogService: ReturnType beforeEach(() => { - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) mockComfyManagerStore = { isPackInstalled: vi.fn() diff --git a/src/workbench/extensions/manager/stores/comfyManagerStore.test.ts b/src/workbench/extensions/manager/stores/comfyManagerStore.test.ts index d1754d59a..8509d8c3a 100644 --- a/src/workbench/extensions/manager/stores/comfyManagerStore.test.ts +++ b/src/workbench/extensions/manager/stores/comfyManagerStore.test.ts @@ -1,4 +1,5 @@ -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' import { nextTick, ref } from 'vue' @@ -72,7 +73,7 @@ describe('useComfyManagerStore', () => { } beforeEach(() => { - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) vi.clearAllMocks() mockManagerService = { isLoading: ref(false), diff --git a/src/workbench/extensions/manager/stores/conflictDetectionStore.test.ts b/src/workbench/extensions/manager/stores/conflictDetectionStore.test.ts index 7c74ea6c1..0b5c1e0e1 100644 --- a/src/workbench/extensions/manager/stores/conflictDetectionStore.test.ts +++ b/src/workbench/extensions/manager/stores/conflictDetectionStore.test.ts @@ -1,4 +1,5 @@ -import { createPinia, setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' +import { setActivePinia } from 'pinia' import { beforeEach, describe, expect, it } from 'vitest' import { useConflictDetectionStore } from '@/workbench/extensions/manager/stores/conflictDetectionStore' @@ -6,7 +7,7 @@ import type { ConflictDetectionResult } from '@/workbench/extensions/manager/typ describe('useConflictDetectionStore', () => { beforeEach(() => { - setActivePinia(createPinia()) + setActivePinia(createTestingPinia({ stubActions: false })) }) const mockConflictedPackages: ConflictDetectionResult[] = [ From 65ff23c5afb78594c6ebf7bdceed3a38e3ac887f Mon Sep 17 00:00:00 2001 From: Jin Yi Date: Thu, 29 Jan 2026 15:23:47 +0900 Subject: [PATCH 16/64] [bugfix] Fix manager missing node tab with shared composable (#8409) --- .../manager/composables/nodePack/useWorkflowPacks.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/workbench/extensions/manager/composables/nodePack/useWorkflowPacks.ts b/src/workbench/extensions/manager/composables/nodePack/useWorkflowPacks.ts index e2580ee8d..a56c908ef 100644 --- a/src/workbench/extensions/manager/composables/nodePack/useWorkflowPacks.ts +++ b/src/workbench/extensions/manager/composables/nodePack/useWorkflowPacks.ts @@ -1,3 +1,4 @@ +import { createSharedComposable } from '@vueuse/core' import { computed, onUnmounted, ref } from 'vue' import type { LGraphNode } from '@/lib/litegraph/src/litegraph' @@ -9,7 +10,6 @@ import { useSystemStatsStore } from '@/stores/systemStatsStore' import type { components } from '@/types/comfyRegistryTypes' import { mapAllNodes } from '@/utils/graphTraversalUtil' import { useNodePacks } from '@/workbench/extensions/manager/composables/nodePack/useNodePacks' -import type { UseNodePacksOptions } from '@/workbench/extensions/manager/types/comfyManagerTypes' type WorkflowPack = { id: @@ -22,9 +22,10 @@ const CORE_NODES_PACK_NAME = 'comfy-core' /** * Handles parsing node pack metadata from nodes on the graph and fetching the - * associated node packs from the registry + * associated node packs from the registry. + * This is a shared singleton composable - all components use the same instance. */ -export const useWorkflowPacks = (options: UseNodePacksOptions = {}) => { +const _useWorkflowPacks = () => { const nodeDefStore = useNodeDefStore() const systemStatsStore = useSystemStatsStore() const { inferPackFromNodeName } = useComfyRegistryStore() @@ -129,7 +130,7 @@ export const useWorkflowPacks = (options: UseNodePacksOptions = {}) => { ) const { startFetch, cleanup, error, isLoading, nodePacks, isReady } = - useNodePacks(workflowPacksIds, options) + useNodePacks(workflowPacksIds) const isIdInWorkflow = (packId: string) => workflowPacksIds.value.includes(packId) @@ -153,3 +154,5 @@ export const useWorkflowPacks = (options: UseNodePacksOptions = {}) => { filterWorkflowPack } } + +export const useWorkflowPacks = createSharedComposable(_useWorkflowPacks) From 0faf2220b8680ae344059c30884e8c05bd47b8ef Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Wed, 28 Jan 2026 23:18:23 -0800 Subject: [PATCH 17/64] fix: dragging (e.g., when selecting text) in Markdown note causes node to drag (#8413) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary When attempting to select text inside Vue node Markdown widget's textarea (edit mode), the node would drag instead of text being selected. Root cause: WidgetMarkdown.vue's Textarea only had @click.stop and @keydown.stop, but was missing pointer event modifiers. The pointerdown event bubbled up to LGraphNode.vue which initiated node drag. *Fix*: Add @pointerdown.capture.stop, @pointermove.capture.stop, and @pointerup.capture.stop to match the pattern used in WidgetTextarea.vue. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8413-fix-dragging-e-g-when-selecting-text-in-Markdown-note-causes-node-to-drag-2f76d73d3650816dbf9bdf893775c3d4) by [Unito](https://www.unito.io) Co-authored-by: Subagent 5 Co-authored-by: Amp --- .../widgets/components/WidgetMarkdown.test.ts | 54 +++++++++++++++++++ .../widgets/components/WidgetMarkdown.vue | 3 ++ 2 files changed, 57 insertions(+) diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetMarkdown.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetMarkdown.test.ts index ee788cc4d..8daebc516 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetMarkdown.test.ts +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetMarkdown.test.ts @@ -206,6 +206,60 @@ describe('WidgetMarkdown Dual Mode Display', () => { expect(clickSpy).not.toHaveBeenCalled() expect(keydownSpy).not.toHaveBeenCalled() }) + + describe('Pointer Event Propagation', () => { + it('stops pointerdown propagation to prevent node drag during text selection', async () => { + const widget = createMockWidget('# Test') + const wrapper = mountComponent(widget, '# Test') + + await clickToEdit(wrapper) + + const textarea = wrapper.find('textarea') + expect(textarea.exists()).toBe(true) + + const parentPointerdownHandler = vi.fn() + const wrapperEl = wrapper.element as HTMLElement + wrapperEl.addEventListener('pointerdown', parentPointerdownHandler) + + await textarea.trigger('pointerdown') + + expect(parentPointerdownHandler).not.toHaveBeenCalled() + }) + + it('stops pointermove propagation during text selection', async () => { + const widget = createMockWidget('# Test') + const wrapper = mountComponent(widget, '# Test') + + await clickToEdit(wrapper) + + const textarea = wrapper.find('textarea') + + const parentPointermoveHandler = vi.fn() + const wrapperEl = wrapper.element as HTMLElement + wrapperEl.addEventListener('pointermove', parentPointermoveHandler) + + await textarea.trigger('pointermove') + + expect(parentPointermoveHandler).not.toHaveBeenCalled() + }) + + it('stops pointerup propagation after text selection', async () => { + const widget = createMockWidget('# Test') + const wrapper = mountComponent(widget, '# Test') + + await clickToEdit(wrapper) + + const textarea = wrapper.find('textarea') + + const parentPointerupHandler = vi.fn() + const wrapperEl = wrapper.element as HTMLElement + wrapperEl.addEventListener('pointerup', parentPointerupHandler) + + await textarea.trigger('pointerup') + + expect(parentPointerupHandler).not.toHaveBeenCalled() + }) + }) }) describe('Value Updates', () => { diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetMarkdown.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetMarkdown.vue index 28247859e..845e905b1 100644 --- a/src/renderer/extensions/vueNodes/widgets/components/WidgetMarkdown.vue +++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetMarkdown.vue @@ -21,6 +21,9 @@ } }" data-capture-wheel="true" + @pointerdown.capture.stop + @pointermove.capture.stop + @pointerup.capture.stop @click.stop @keydown.stop /> From 23a5baef43528fd777feffed8974516cbf12f98a Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Wed, 28 Jan 2026 23:30:48 -0800 Subject: [PATCH 18/64] feat: add category support for blueprints and protect global blueprints (#8378) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This PR adds two related features for subgraph blueprints: ### 1. Protect Global Blueprints from Deletion - Added `isGlobalBlueprint()` helper that distinguishes blueprints by `python_module` field - User blueprints have `python_module: 'blueprint'` - Global (installed) blueprints have `python_module` set to the node pack name - Guarded `deleteBlueprint()` to show a warning toast for global blueprints - Hidden delete menu in node library for global blueprints ### 2. Category Support for Blueprints - User blueprints now use category `Subgraph Blueprints/User` - Global blueprints use `Subgraph Blueprints/{category}` if category is provided, otherwise `Subgraph Blueprints` - Extended `GlobalSubgraphData.info` type with optional `category` field - Added `category` to `zSubgraphDefinition` schema ## Files Changed - `src/stores/subgraphStore.ts` - Core logic for both features - `src/stores/subgraphStore.test.ts` - Tests for `isGlobalBlueprint` - `src/components/sidebar/tabs/nodeLibrary/NodeTreeLeaf.vue` - Hide delete menu for global blueprints - `src/scripts/api.ts` - Extended `GlobalSubgraphData` type - `src/platform/workflow/validation/schemas/workflowSchema.ts` - Added category to schema - `src/locales/en/main.json` - Added translation key ## Testing - ✅ `pnpm typecheck` passed - ✅ `pnpm lint` passed ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8378-feat-add-category-support-for-blueprints-and-protect-global-blueprints-2f66d73d36508137aa67c2d88c358b69) by [Unito](https://www.unito.io) --------- Co-authored-by: Subagent 5 Co-authored-by: Amp Co-authored-by: GitHub Action Co-authored-by: AustinMroz --- .../sidebar/tabs/nodeLibrary/NodeTreeLeaf.vue | 22 ++++++---- src/locales/en/main.json | 3 +- .../validation/schemas/workflowSchema.ts | 2 + src/scripts/api.ts | 5 ++- src/stores/subgraphStore.test.ts | 42 ++++++++++++++---- src/stores/subgraphStore.ts | 43 +++++++++++++------ 6 files changed, 86 insertions(+), 31 deletions(-) diff --git a/src/components/sidebar/tabs/nodeLibrary/NodeTreeLeaf.vue b/src/components/sidebar/tabs/nodeLibrary/NodeTreeLeaf.vue index 325cf44f5..4f1cb6d27 100644 --- a/src/components/sidebar/tabs/nodeLibrary/NodeTreeLeaf.vue +++ b/src/components/sidebar/tabs/nodeLibrary/NodeTreeLeaf.vue @@ -13,10 +13,7 @@ severity="danger" /> -

+}=9KX}}`)~W93mIz*}UUY*trwm+NzM(te~U>jG4jB zSuT3VNe7b6a7i{98JVf+th~HDK*m~-&mE~&!&RgQwDwCwsbWntT$*V#z)+yPR z?=9+Z>#PTyfLK#?v4M1m{d7~~j~^>wf|CyS&AO7jj);{GBVbIm@iJ*O)9iCVe*Rpy z3fHjB&dzoSSIIzw9144CWf-$uQ&UsxO&;#m_a`9KZUQtU4Ewo;h6cGHI#H9B(Ad~t zZ{NO^39JpZIrj5s+Y`6Kdg8)IfV|2D`8lyj$oT&K`&BM27wH0hcXt9v)qr4$^TBF) zzm)nL4t{&kSp>MDM8Zx^Vc8PM7x-U5OrAZtK^b7~uVI;s z-gcaKpG^9y6}p-9@Q-cp6vgD?JN`Dx-wN}=R0TQIiX6TtfTOR6Zb9-o&5I1 zTS4k8%c3IfI#x6PQFSbfqYwU&pDm^stuIlwp z6);tfe;iz>nG*B8|Jq$(O-}#f6aV2W6!MHxIdr zLsu?eW+p2!@67;9Fpy7L_s7deS)KUYB|Zdl1ag&Qm5wpi&LAuz7n}wPU|`+KV&JZr z7&@+WtaONuxE z%m8j;2t7hZZ#ayOf?m?yX?I2R^=s_@wH{0Lr04JosOBGS*-Z%Zvz;!Hm8~pdWa+iO zGGD7DR%&CS{_$DJ=;XU{8b5!3#Xj93qsMjLF)E(~I0OVV$$Z4C?UHx|cb);g3dBv+ zMwURDB``4XDx{*}+)!k6^e%w#tnBPS?;pIAp2-B_2!SUc!U!|3-f@ewkm|SpP)eXk zgzeyFV9mkcxduj4YAXQ_W=6VEdY)Cz$Hjk_HTxrt_5xmUL#Hk$q^w6yf9UuEHh^2w z{_F2&szmW03ikK%S^hrujR61a$AgQkB5!Tbo)^JejBpx@{fD?)imcJvEDpzX`En}c z8$(bn&ml-Ct^6vSmSghrT%UM2Te$c32beB{Iv2g`2i}oaJfm|}qG4jwf^D+g4k%~AyC_FhKgWw zzHCa@k*lS~%*@QVDx4u@22hwy+oHK3Ss`-7442hQCczw%l4EGDVwG??xnO#re(Hi~ z5=a^~1=nPNZBxWAAz?t!>S+MOpgz?q{72YpeUReFEvA&0A2WR{rmR((mGufJ4{)@T zyoyR^OUunz$imj0cF&2wbftD8v>RCBYYWE%lV61_jJ#m zJBM|fRxbDvVbc41Pu}_d_|cNDOO?0ie<#G$87<@bv^lk_smabY+Ecz&l&eTwyHbW}OLPp`f`G zzrmOv>7IfN(R&jheECq+i? z@KJM@`%+LVsHixs91)hUoPw;qauc!!S|Z4n*xTEq2upXs?H`D%0D6|yP!X$;kQOYN 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 Oh`fx-ll(`|-uypaZ2idq diff --git a/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts-snapshots/vue-nodes-paned-with-touch-mobile-chrome-linux.png b/browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts-snapshots/vue-nodes-paned-with-touch-mobile-chrome-linux.png index f1d41cf137c1fb2d3cb4f6c33b88d1e28b619394..915fb36d923d795811ef776c71c2effc5b6ba2aa 100644 GIT binary patch literal 25588 zcmeFZXH=B!wk2AMfQo=h1_1@hk_(VfAd+*=Ny#}QIVd2KMKVQBMMiQ4B~#=eNX|Lu z4E^|>v+w>+kA3>wzPG!_xMN&@IfN>!-giA~%{kYcZ=k%a80JIbhY$z^Q$k!=5dygr z2!Y%kyMGt_%jqz~9t83jA|WiO?2@=Ojjm22HiNb&cz4i!x0kcchs3)2bmw=V#k13+ z*4(kkDJo|@``9|ECPy0CDfeAcu1D>tENGMO+;(-8J-y4-BSf^m`VqKz%cYxelxb$N zy2Zzn7CNr>C6SPk{Y>Q|;`boD85EpzB!c+tJ%>ZyeEBK^c`n>Z4S7sqhy%I*r1SyA z@Ba8b$Xh?lyO0-eIq e(~qmgdh2EIXF0=4Ny&VW$!O`prfNR#8y{VOQ-Npx3_%Q_)d~Gc4ETq{9suq04+8qCW+7WpfwODIy$;O1Dd&3=uDu<*d0fJbJ; zU&9DlDPFwzxM0@$WxnpicKWxuxfe%AOnQydlMX9A@ldmGzP_F=F83PJx3(;Ier-=y z^|!S64w+2Xid`7po^=SIJ-Y9vZ9P{r&yDo!x%<^;lEm z>2j=|rlzKmp&|ca4?79h4f5Et@oXWuU*IA;USDWc|uG6O^I$@X6-$v7?7l6dW^ zY-Uw6e|>!+)ZEgtxw$#(c{)w*ef9CAAScIVvydyCTtGD9XG1O5jEg}}TuE(hc!>U* zMxjpK%9tLPBB6+g2qHUM@x8J#E~)nNgL0Rp%*>YwOT=8(eA_G^w$$CW!Tno3N#U@_ zj*h0feU|{fd6pDGisuJL(p6^f^KO5Pv=Qn@sbccZ(rQXdFmRU$bY147)pR*HIkU%U zwY0RHolhCHDjRPuHoIeBhnKUr$?m6$0c)!%Tzvbj_2B)OG|OFXZ%|&mpww8L+w?Ft zrY7~{ecm8&cz788jQg#P!G?#`@9!TK1KB>+zN8cj%d}Ec8*2+DNP@Y47|9uAP%Cn~ zy*`zZk5ERltSh}7 zwH}o=q8&nR-nBw18sv_F5xlUVpUoL0_0VqlDUY03m9DO?sAxNK*~!U?%X%tR5QZ|4 z_@Ok=YkO_C#y(K|WlZdtgEI*+aW$qSPVMps-3GT_xJ{#k4oU)EI_1@QzxQ~#84hae z`Y^)uHZya{&+ihOjBjs$f1a$)paW|oya#?NEFnSHAOMPA*5BCJ*inj|j){qB${OmE zzK<1(X;xQXFTEdyL?X334xn!7x(~7-PP9WqL%Y*;V*bh$az-(Z-QtH!$Db8@%N!pa9UUHC&iOXEySffn zho{FI&-NwpcAO24ahd9huMV0%>xF*2Iq&K1O^9{xq>GhjP#BB5hqbKAD8tr`;KG2B&u;zgV8LHyH-M=^N(01PyZ7 zde-~4fdX#2@OkAkwRD`gO&=jQ{nE@83FpdOeb#?7E3f!m0*yGj=>XXcO=xsI#77w@{jdTzbult9kfDi7_BfE>VLkyJ zPCALl|E5w2kx>yKPnPuB?kdUo=B=5cf`Wv1dyO-Ovy?9u5_LmvFc-Egwms7jAe$Ka zj9Y(q##8$N=F?*B2N`sR8`=XQk9v=F8$HaBQC&yI*?}f*k+0OhKWAat%P(#;UtGdH z?L!dhxAFCXPGPFNu(j2blolKuoRM;H(9O)KUHvur{B+h^Z1+vVs`b99qi(ntG*rT@ zKb+s(#267usf4~UT1cC~oAH2o!!eQ171`Sx>gF(#qF+Clg6X!6q#D5j^X8V-hb%40 zGJOzVW_;lO8YtqMztWo zvIj`uXpYbl&$GQJ%VG+Q$CWt_Ka=KG2MQcCH7CZ)Ow!Y>6;^{keS?Rk(Y%p3ZMyYz z+8B}GIThO-8clw59f9C#N=tju3$vpF2nazgJS-P=KPcrbN&{$8Y*1)aU+?uY^RBR{ zXyyK0v(FZrqgJ@ZL5CKFW{aJUvZ2W+Rrm8#Q|CqtwMJ0V*&zj#{5NN_45|g0K6Q4B zD_xN^2)Pu&D$v;n=Fdsxp;}98YvL`e315x*25_-4$E~NTWpSWQ*DR!@2^-m5^ZpnF zXV|)?vPrV#;Ao&ZBA1oL>2S!%`k)`%3!x=W+Y=no+|)0~YleAFN#zNL1=SYZlbK|~ z_G2gzilC%r3tFA9lO6}ylL-~Wi@{xiUrY4O{8Fr=-H=!jaS%DFVmdz2uRVf_&?U(_AQ{gANGP)2u` z{c$nog~4*hI1G1aX+;@xr%i1UrSrAkjwXYfo10_Rxrlgz9+Be54@JertBuuJBCulB zK~^A6aFkX>v?T5{)&?&8*Z;Q7((fh{@036O)ogND9%;s(1|r z@#Ct|CT`Z=c{8~-ExA830$21x!GOjrRliv-Pm-iBev8O@LJqnARa!Qdq}R?y>jmPe z*vkv6#~lgB$%I_^o`gc>@2%gPrP}1sM3i%G-h- zF)hO1-$+72Lc~bK$}U2B^amsrZo0(<)1){`#(@d!bmJ=5$~y9gO@72s6B05&LGj&s zrJY5Rs%6e6*HMf2kiyzA`!yH90WC|b%y>Hdo+v_|D838n6ljp?m@iK3+bM;X&yX+T zAhk6+JVH~Nd_+X>s2KBQ{YzP)WXFfeHuIeF{R4@D9a%TfvF7H42hvUz<>~3KczN@4 zXv05#U8}67&Y4u;bAahQknv&yr%bW{y9i@J@xWpUb=z5d+vmcrofbs6~R zNhv8}x32D&E-pthvW?*`ProErZUh)3IqI~3a2UpyG*n@I%0);?*ONSE`GG`M=Fm26 z!*pIg6@$T~baYS@R=pWBF)>A)oHQyb)(;}N`2vOY0~`IU+w`AIms;nfee5ge8nBXa z!kn%wF)=9(lns^0QgN~;B}L;*CXQlfVTlrdDZQ8IOAvJYi16qxbaQF>SJiix;)wnzC{nRyjz zBPEvUFX@??i0N@xv2PEMhXUiqGe8*`?7ajzZK$2dD4ySaTM!I4u}2*;h&E4&F4{3OI?u& z*zpL(DBT4GV{(Dj;vCEgiYRDv2=maZTxcnukXJ*@r%^xxHd&MSJwMe-jhMM@S5zUS z6m(Hm!^bP&Pee0a8+$@_y21 zN^V-(oVIrqV?{a!&ClJ%&zY3uM0AG=^>hp9`y?Cc_nQT1GX1ThkH7rz)D{hU=6Z3| zH85~!Y@BraXt67@Bg$9e{pE?wYJbXmSVTm`VCNIckkHV0{DF6-$9d777f0`P-nNWG zZFIX4X6w7Z0H%y*>4{^VMj}fa8M>7{@w94zx`L9D!_gXR$usBEt#Jow%piuS zP53yL`boT2C%f}WBgxG%1>)zShw*Lv!}Dh^-6kia&n1HD@X36yS8k`LvIN|pSzEfK z)k1C7g5i$GS~ry1dP;a@i`d4pUb>OG#tG~ueOuuIH)8-*X#m9k6MeUAM<^3vLh$r$ zmH*&i(A)gf=kPQk2PQpz%}T?r1u%^Z)oV7|L*X|!UYp)m^W!FCUYFa>(lB-t>G0rS z0jCY9)#}R1#f8(fO$J)&I^Yl*?b&N}ME@i_@@LEp<)y5SSbqSIK3%L=;MM!{ zYGhEFN8zpoz1v*_qG)v~Z^i)GGx$(WgqgUco)BsA-F$v9ew z+tCsH@~I@n#L7Y-M)sA(nB>AP8zY?81dHlEDvs4`4UY5PT$%>E31>ew@s2{!V0C|M z>k{B6BwSXC(x%ecmhjgfdBg~qboo3lq>YVNxMr{V$HvBbd#!%>q^jw9hL*0!VhUKD zEvo@uIN$QQ(0$u!=i`Xro!oH)UIkR{bP*FLo;B)0%MI_1C6-4H>tg0%z|%az@)fmN;`s?R}r7sTtwDal#y zO81k*lZ3Msq9h9@7bpUQX{>!eNz{;J!T9;FPqrr!)$WI*h0zRZ1K;1AmpwZ=bveuq z&CaF<*Dd?2X4MX!fkB4N()sa1FmwIU0tV1i!h*xS-PVVwf(d#PU|c633Q~$~1mDkx z!mG^9^USJlt}llJ&vJ6sPG>wj`uoFq5Bbf8gdY~~sZp9ak zc;ELZ$L6KL*g^*8@JGu5%9IY>{#R0}c zT&j_dG%i%9Po-sLc>YH;R~^Tj85ssc!^7)E@wsN=;`fbkoIF$uwf9qQy-ER3p-0FL zqmy6tj*p*j+B9N5Ad)bs^}adOLkciaQ&aQvd+j8SYjeuLIgf|3x}j)@#eu64_ucF5 zA3uGSz2=+Sl6b4+5?`k|;81TE4+F9ty-UXL5oe|moT3q-R~fjw&Y5tPJ!7n^%YPj_ z^rX5@L}Vh_J%(fN45}|9@}$>)SX9OCLvx^<6J~F(4tn|Z;|(aLyB)ige8xU~fX)MH#O2$9oUY%_|@*_i|@xarz zXE;`;T6L8cwX#rK273C^%2LIs-{a$hHcq-Tlmvo^`QIO}-RaT*wboPB_1au1@ZO&k}gFEac^7M<@iYVVCy7Q$gLm>Dm#z--reZd;1W-ar~% zBuyw0yIcQE_TW0kB~~b2bGA9%a$CAj zO7k?35f)&mJxRq{!sdV_&U5L4Z+O;w$>2LW=%-I6qdn7Ds4c!fa`xcPw)ZluSJeA# zUcmdbu4>lpFmz*M0|Vlq&Vvv$MEyIHO68@z^*OHdHL+ssm;xSe)B%)5j_`uF8Q<) zwB(Sx{@>hBwO*-7Fl>+4T20=dprhSG9W*VfuU`(nYCJyK{jf7-WN7Jlet;LmBI5e~ z{k#$ez5-27LHSmDFFdGeRzX35Qq+(30|}3%pFU9kRcnHj&qda!<7*!Jy67oE6 zG^sngT)(}=S9N%aI_Vl_0~)yDQ`tn{jIWoXuWBi*@D}=4hsR+TieSIjviN}G&Bf_$n5nE z&g60;HbYueC3AD+@#eI#{1ki9^N~t?J<3@xoCbF-i;StMsV9`8gv7-31d4JmU%e{$ zJbQZg11<4HweD*a7hI#S(xY>$1T03#M+u5R*R-*>ABtums2jAA^7Kp+AHi(wQXpm2 z)%j+_sX$Tw1PYDq85syI^1Us^`)E&VqtX74ng7K-2xTo*)_p*qJiaOSIC@bojAS!O8BsOES zvQLv5KMGcl46Nfr%<^rRs}LYFczzOpe(eew+5Qp@WF%Df? z+~#oShouHd>-@Y&AE60gS%w_vMaCL4kFV~Qsu42_#q zP_6IB4|$5gtF@%$r%6!}W;=^gHLM-oH1+T1`K!_8MDMPT^@n>|1qB^UPO~>;Q|nl) z`U7mZwKCf~t>k1JLLFi7Nm)%xsJ}2&!&g@P_4%)_90Ew%#MatQ1li}6I8)lyxjAW@ ztEed9>u-%Pi~_Kk3g?DQHK=ZvKpq*OE`k&67Mt;OYp)@M3!+TZ@X5 z)98G$(gn`eNmjJf)!9F$2^bg|WsQ%=x3$T@s0}0EI5t_DZ$?BJX$>Oq~0Md>Oi*l2>v z4hqH~x{o$t&{rrL4I=w`OT-nptdjrgkEo`J9MkMYpk?%Auq3e?DvAUrH+mcN&K%vbCo`l`%4;W zg|+U5F%x+KN7T6cnfIxeozSfK@~VE7j;?wRsM1$FvTCb~F-t3Sn-CY{j>FYUO{X=p zyR8f#jWnOUA=Pgv;2@lk6spiP&nM%ZZH=O+X(N~weMc8I++sfYnTy2Rl;MVl+!gjt zr&@NC;Hur`>Ek17f@1jZe0Ev?CRzGuHSD!gijlhwLC z$u&rHy(ZbVZ7DMZ=_ssUMZ1(ua%36Hd zX zTle?<{3%pvIiBamS5e2L%?AnsPDMs7l;6yGy zp)dc37BJruu(`fIWO&}v)4VDI@gb-v&8@5y_0rnh*dQh(wB4C6ygK zr@e8kzMlZ%vz~4A1nXEp(=xT>u1UshIhS=r(KB25m7k%fhn$xhU1 zW*_eK%JMQR1VT2Fgi%~jz~{|Bf>3Azo3P)%)p>dAOp-N5J>(u{gWWnBZLc->(I7`d3JC>sRAF;~|3 z58H)PF~LlmZG)}T9P#g~w8Ar-h2v&&i~@`f%~htHe=Eb|<~xw`d;h|f@!x6rqyDE9 z>YF{};CvQf*nD!DSKpjMtpKOELY)8$(s2JjGk5&+Z3-}UMZS;53zXw|s}s!J+iWfw zt-&xKRK2n&VR{B}d-6Z@i2U_7;AO$@3fcek4!=gGso%d{22urOWo7A?*)&?NFHdcz zgY=32{?Xp*5%4>#JmTW2!q3mC&(1ypqy`isd9Pt)Ru&cN!13l-e+ssh?&Z~CzXBup z3TMO8oUU$e1KZn^C5)ppH8`=~zV$#LE}I^I2u*x^yu&2W+6oH_diwj-#yfug{D>Aw zM@I+6SlZZ-qyq0rMV0jkk}8wbpPeNYJ9JgGU=4PGNGxcWR<-R%$@i|VVC!}J0|J>C zIk%jsv?3X^DXpp^=d!l6w+}*M<>uyURas{Yn4Ec79e?R5{%!GRExq?+2=fkKbD z>>yEBYwM!GKx{(D8!^*&HBn&AZe_(-)h^hUDu}Y$3paTOJTefTwEcMLe<^fogD(1DTm8|N+lyBD`z$CF$L!vUVA9x*gP{mJv}%WQE9}cVQ+8$ z;R7c?<28dBVuRexdwcgM?qgv!I&G@z=#Zj7Ov7u(p{@s+sj1J~ra^m>P3CjDz40VQ zDj)(s(Ls-zlXy$YVb)XXd0C0IZpZZC6_J|bhNhH%W9H0 zweV~g+`=ZI)vy9pPHtY_VJnX-ogF6sPbDRG8XB(V<{Q26s=FvC#=Qya^YdQoL(9wE zDXeU4^mKG-R@r3#q4U(*5)haq7$7@4=XQn)BE?@yN=T%wx}9u|fB*j7Y&iR5cQ)lA zU0jZh2<+9LeyBMv94Iu658Pprw6(x$BSbP{wqTy0mj^ig()RZD>S}CswEw%iked-? zHk`|5NTL>OMQJ6A^qPKRmcB+sMFp$10vhemJ`qnypn~PDLP{2qx?8L3i;N8pvGn;kYpSEUxtBDY>|`v|;G#mNq`K z?hbM9Ap-cw}H+i0k8tJ^ImDgfDuZm~;8)|&7^LmuWS&(re$h+`y zF_=uLRR6UB@jFvJ!B0(-n*r6U_c|^(f2-2AT3g7P$bZ*U{>7*RFJt~x(f4P@w$$!X zs}qD(^y$b4gKih>G!GdF&WV;85sK$?{g#qK9_xXd;=I!eaE>gn66pjAvsq0j+t}EY zs7pIDFyt5Mg@=5pZU-YGLzNR16dfumxQQyt%0oKfr-Hs{*}x)3O+7RnK`C~$KD^o< zO2WZW;W#!vZUz=UKn(Hs_y043%#9+8i*0muXV!;Js_TGBr?HVA$f-dSetv!f0~k{} zK&}S}R7pt*G|KXPP@$9@G&CJ?ag#w$Yo)57$c)?SZy_PlB86pT?0*4$tnEWiPcM{cz&uy2|D{B_vd0EYd81KqY>ub^z zSCo@iT#St!{toOkEu0Om-I+4FCp+W{`Ue`ZGX*EbY)^iaIODik6WWKf$RVAeib(dUq zr*N`KM|2IwM4;KV2 z7+icXiuifE4NHLr2aJ|6(b23BA5JHJ_@hUUjQbKLNNYe{@LvWC^Mr+k0juXstrH12 zY)Y$TUDMMtdV1ccJ1~Go`N-LvHk7@+1wb_=_nAbNm0|RL%t9P)K<&W90X-YEV0RvY zik*dp#E1wDJ-vPh?}UA@t&x(E0jKc~=mcwXryLwGS%ggn6qSYn+5|7iuJaan@%;G@ zz-?D=R#uF`L|x;$10e@s7qFgiLe{CSuJ~2W4xj-}*W$G?4&Q~ClKc&Tfs@F^#YGOx zp-D-eo}M~y-|7S3tJ)5j-MhOcK)k3g^Vl{LRsXHRQ78Mi77764hR2A4bYF@9GcE0x zN=!^l8o6X#Lc-1^hnd3_m>^V4f~4ECe4U7DzeHDXzxx z#l>>c(mlXHjO_0AzHvoyic%(H1N${(DY(?2w{iX%r7J6v>ygPb)zm(p6+jbcYirX6 zbU^g*9YvH8Fk*wg3QOvz276A>1t%tWCO+8O0=1rqC_aiGa6f4Ah=+HH`qtJ~VtjlX z=-|WWmZc>nUA?`_mAS>jRem&si}~a!*g#ILWmo?)2ME8>LYtQyXnjP0+wfJ9~8HFr$YK>{UfJ2yej%2f@nhW!Gz?vfmy!w zZ9HC)P7VOyeHkel3Pp;8ll)o(^@Yl0!Ig?Y|0w>YsP(?_KZ#TP^Db#K16;%L{f{yg zunDf}-xyRIAW*3U|NpK1f1*A7FCPH^t5D{D9lB|*-P67IZ(3!5)4a@gre_vO|27Ko zH~;Sy#K`rizkql>0-yqqd>;UAVQ+v~``4^LK0a2a#Sg~gr7e}9jw~6?kfcU@sUef^ zUP>V|&8T$Mq9cz_D~w7ljDp2fZk(iO%uo0~#N%D#yNJhIrpIg8E<^2|lw2+`WkCIN z@EI%cR{qNr(cfi)YF!r`ZRTfVKmNUQfWQCG%1Zy6ZtX}fH9LD5SW?&3)Y!pl3knK= zDGdP5;~cn5nQU=WV&D#p44NqC+yYy=%*V_{+AC>s#xtZ-_`1bLb=YuVW!o&C%?sn1W_L-)*c zabpAS4urLLm#B0{N8#p1%do{40F{N7R{rJF!5^WL4<8;ZF7B3+wT`#7jE=tG=dZ0V z2;oS+X9yWU1snmG3%n1OBqFJ0mWm?2rOalO(ml5ocXb`_g{y6D?!^oGFlAYDMn^>* zjYQw^MGNih>O%JQ{m6X!W^B`ItYIs`Y2JEg#%P&pT!V;>=+lEa)*r{sS~(!cR$k>? zYj;f#{>ctBZ>_mauID?l&>|_+J;E1o<(gVoyroqYZui|_d&O(oo8Jy))86-ZWrrWPo6xfvYwtQF>trs+6D^zJycXXm06F&$YlR|qiLwk7C7@m!!T!5ki zyA-zf&B9`0h(H_~iI?Ai(LU&bBGAlWUE%x}Iia2_eMwTDa%j`fK#Njjh*;y=yYUMu zzNN)c+T45+z(B8D9B*Q&&uX}qSXz1okLZY70gTyTqTCFSHsC+78ZV8mQ%wmB>GBAzp)s?u zaR!-v}rK-6b^F7g1thLQ4u_0!nNW>$a) zh+Tn{K-!;JR7!Di@qxqT@tEGl>F)0QJgdvz9LRRb$FQy?*XlJ%sL9HOaaO-=P%T&Z zrEevce7}x_K(tEea~~&Xr81MvqwG2Eay#Z7`r$;7uSyaX1+hYqSd@x+#clhpN&OWk z3(M~+n_Xly1MU5_?F*11+X>~GnL9!BwRzuOrT{by1h<>n+iUU$9XR=LXQwRyqV6YK z3QVtPX@Ti)=~H0+UUTry&JHT7zr$+Zc=f(o(boh21<(qwfT6)+tSE~n`|2f~A?MYq>Gk5uKn%0)%`XeP7>~QM6#IsK&6I z%ZCjGvBswhQ_^yQcqn&J90-lYbLif@-v!DcK9lx$1N39w#zwnEV{>!*XgC}mO3J%Z z*3VxC)=d)mAiv^$eUXxL1?-kIvd?V1)B(M5Yng3I8E6mZ2YUj6n|4AvWJy>IU% z80%JA#sgIWRairVZkxq+?#+hs&O`-#@)lSft?oT`_Jog4HO)dhYe5)ydSZMWOqRI# zc)$UNytP$CL{N|Qfi4-H@-lQK*~^WnN~Qq;G~6M=?LPrOSLfwLa)Frhf}%l|W*>J~pDnQCg-~*?8Gh znUIup`bvVHgvZhJ?b~KhYAVQ@pQHhwRP?+aoQ&!gBMnrN;aE)9W4@`gnRlXu$0)-d z5s3bi&7}y+fLYhoBy52z`F+}*2Mm>Fs&%1b-Kk@5nk3k(YY-M{ z*iZqJu?Jw4FdRJ~PsPZ%z0{>d?y+}A=H(z(KYv|JpB%^&Gcq%W%bo+Uq4=VgYFLAD z=RT09VwrSqIs|THDJk6|hd-yKDFh1y=YhcPM5z%wEXM0GX5DD9{)sBE169G&Ppv!vR>NTf-f2cV$n{KYKSl_wD0YJukDHE7zb5 zM)mJLqPFF%@0{driy#fwsSy+7Bag)AJmYW$^S{gJNEVhUC^2$8p zF%j^-iHwqWJV|0mQ`W4qCbbv_c|1YH9wkL{yhrk%I9OI6uGMhlRHG;F=}RT0VUQh0 zUFNt`VE1AD421n|yJ%7rxt+ptmJs0J1D_aen1#8y*OlFMnovMBOv|GoBw)Ak()w^k zW_(-&uaO42Y!jpYgZ^0eY3nB9i97L(yfCPo@fyu1Ro=|bxR{Su~z{2CmG zM6k{Q!uW9fD>v+XzSgm^fSbT|s7D>Tv#o9Fxn+V>_6+9Y$K)Qr2a8Tl$I8i%PG3q% zN*3$XWviS0N&sG|bw6TQ15nq%h+^o6m5k~!AE9+-??lPKuw!L-KWx2&gace(X)ZXq zwxug5$d6)*HiohLThn%+0JJeypO|-Gzxpqe_y44de<@%xs@}@~&;o$MrSsb7u^Aiu z5hf-I3Q9vgA3%%5#Kgc^K*s0tn3PmWMWqcYHo4i{%lHZa4C{n0>IiYDe(T;GfMZir zptEaLrK>Nh1&#v=1&f@Yk77YnSoq%UF7qA8z#C2=hA`>Wj{N=&%$$@+HOY|haKt3& znjoE+Wi!hURK&$1@B5GeCQR*9`)0qur-6nb93S+qw6Ze&wt5?=*m~a6rtjVX%zF>4 zhr(;GsimZ(yuF)1{i`V}e{WYVO$Mg+X}0sRL{c8doYd5>dBAL2ZrG(yJXP@_%^dSO`6@f`_HN{)-c2N5a?89p8C+2Ui{r)^D;Lp~+m)hAGdzqQP z?EClon3#pJ7}(*qw%^s1f&N*3_Nx>K$ho}P>|9(7jEpK%*Eg5sYok3qAuI5;M>^)_ z3F$!}*f}`<$lU<-1B7{giU%V8-(+WiysuMjD`+MMzD8v2xU#CMv6WS1Qc{Mtx(*}I z!A*fQWS6DqcpinR{yZdC7##4B)tU8H_OC5%?RPY!_ z>2dLwqM~pWT|w&WJB!@1MlM`taFDe-Rsfh#((hA(4(NKAoRgOalJvW{dUB$?F0QWD z6Xo%De11qL)&A%jUtF7KR8V-2TxK_hZO;$x_QZQWQR}y*^78WSkV}+`7n#JwJl2TM zMaQ;&#e`Ji4_lZgT9-h`Zg(N2txIAO1>B#%+QJ2%*Oy8B6=HvV0C06ciW3B1s@zY> zb{rgV6`@<{^Kgndkbg<>u=mo|=G*Rpym3JyCGVn#DECo2I52Q=?FI$SuDsvc)>-M( za=N+tdFh-XDH#NWIkC^uIu}Rzv)-4^NIL5jizlnJMvhKYJ7hqSM2D4Kg%-jE`a8u{ ziEhOS*(1n3I@bBh{QTIg z=%n)F2HAILTKxi&viH@1-l?jJ{E5;e`4a8U#}?%KgT9b4;Mt(SBzz1tXn#~vy?-oE2z=e7uWebtqaW`0_zb#p?(~JurQ!4?kIOlX zlacXfXX88XXp?iMuV;`N8>^NBo)&{dG2@>cZ_B7?Sy|@Vk;B=*!wb!S?s<*I%~Q)@ ze!8R4A6+FLIc6jI)c`V3*r~oa!g6fa9s-}P(i^yTR}e%IrG7b6l$U3?G5DS*NJ~*w zGchJ6Cr5?57jbuN33Y9omUm4cxBOscI_D#bSaufG(KbP~_ijJ(GAagtNc>B73kHUL zba!FFwp0*?7H_GqFUB-J9v957uOmso*=IlF<2f~CjVRq(XaC>?;}M}MW1*z9hvA^U zkf&$!_2goEZ<>Q`&|69aCGy!j`}G~> z0u~5h_0s;OJy`*1kW^IUbNVD28c#+cNPP?gr z){@G#<~8oO>5p=RMmYSBXS@7k!pN&FBD}4(930rq^Rj}PM;l#aYp$w_%Og1qO|m4i znsm%?fCnjQOyIf8R`vK4*C^|`lY;EC*$Cz)X%`DUJ}#pTB_g6Olci;(o+eFXS1Fqk z7U@K%j^xkhVh zPw40vUop}xxhD36EWx>$xvU3WYYUXy*zWdl-FagI9T^jFu{GKIT7vW+RXvdtI82Dn zV!T}h$ASv>1*G=qbX`4hz>b<{y(hOpwr>Xmzj1DT-Qu08G-4J1mD>wOMpo`~rYe-v zHiw{FQ}P@1jyNm`Pj2<=+p&~eUZ-}79g@w?X?KMHY)j>>fdzv8eZJR?DOj^|=&4LX zLOAZ|s|(Fn3*YW$zX01GpW3`UWMt$HTj*cU^8o|nsl?(#S?h==5T9%?=j8agXNQK& z0psE3<}Q!|tARH*sXN4<5)-v4DOAP(s7njzbz5#}8;{_V*fE5F?%}92<(>yQ%0#)goWEOai z3CMy1)7D5vNq>Q%u`!u|*C) z#LPDQ6J1@o=gD1N5>K9-5i-8PHZ)kwLC|y|B^8)(gPo?QE;)6Xit;`&>>nL4Ffgt( zzK|5$$8&n9Ltv0T3Z!(O?^>Sth2u<@RF6aGCO-6 z?sbWdB_;jkc<;!`9{5wl6}`?{igVok079WeXe%;xfW;MvGd8#nUSDFX7)A=~&&|)f zzq>`-VLB$f@<1o!FOOgU{^Q4c`tcD8?MMO$bnb(+U!9nqkPzjL^_w39wY3TE&KcEp z*B)qVUs^{(Ye}5D$85f0g%_LrJO;`yifKJ{v??kVA(7qjqv_*af!9ns)TdUm1$J+^ zpxksIta3BckYmOkbF@z4c&i(=P!d%9abat^tT8lWRCfqb z_RLrkB8(-5>{-S-c4J?XkUd$m??d)|Hz;K6#uBpc+hiT5r>L@7rB?ioBd{NE$~IsrX52(LD5bHoVow%}N=W zqc$10-`_tzKK{d{s3in~jh>-y2#bNAqIT59H>s#BYU%@3>!27>(6b4>}li&h)0 zMM)X=^bbzF>lU*90PR^YD|O~7zwBIp zt>BZ76G4p>d_HJx@-(P9IpwZOOUo*f_}?m7n12-fzJ}PR5=2#Q%=)i91#*cFu*R(@ z?Cn3effBF&M{~La^1l2%UfeJ)KRf(|b=cg&<*`4Qo?(Q5rN*c<{+#(unNQLN1_k*# zdU|d9sdg~^83nee)!_YWifLkd6EYa%pYZlbSTjA0XFw(VoH-3K+@3%FJKK!^S@i1a znql~*iZJhvJtx+%WT%VG5ptmzlYUyKg}7j8p>qaUdio3f%%EEYk(Q+x`$I3i-ZZLt z)W5#=0Eoki3LYA#eS5B4U0iBkR{>Grw6>>U3`#*qMmls>&fQ>>Yhr6hpis95E!oul zx_drx!kRk z9Y=ZXkdy9;YLCt?wOSLC6;?L6VmA+&xM=UVq}KOqsYK$Lqr|qNzW91C2xU)C$pe;5 z(ECHk%Y6Jq(T6i}(L<{QXX}lnH*f4p)3&IqJ&?~1IK91z{CtB73Oe{B1~7b1h+kYV zt@gTB8WePCU0kuJTR|Zt<^%@jF%^=@9BtxQVh#xXg+?w56{AqQ*v?LqQ&+c>uv;RC z{8`3pm4*>lVS=(4`v6rQ;Gp&h2W5elZ&BD!d=6a%1TO*14K_2#X%XinzXnFcKN`HgxQMt&%_QO8eZ_?u=wSwTc5LrZ%Co}UA3p@@jS3wp0{tRsH^F)Tm49l@e;z!yktjb zVX=v<+1%o%9PAy6c>)C`A1aq|WF%ysQ*0yAL0D5*>g$H3g_d4p#+QmJf`#U?Os`Uh zY!%kV3Eug}idnk9!!Ux^`2tazgmA~0Qwy%JrET91BsxMTE6rM*sL;V zQ$q3&63j@Td!VrR`94GS&3|P9S{$z|;1(9D+S*3o7z1J>W~jwllm?wp4DI3>F_1_1 z^oEtLWPUxejj=_lV}Pc{<+17MVQsWDW>eyb%T|^S5aEcakBos=9C)9os1rNU#ePlRiWawB zMt5f8^|CT)Ol(Xu&|NW@+08{Gb2)xK+y+wO(%v#bYjJHS9b@a}CFQER&0o~(v%lF9 zQ@p$5IbCgF14AiVtfrpqqudS|bcV(yR|1>xn_60$KIno|W#X~7@RQu)pzYd*^FC@g z%&{O|%5bmeZF5_%QrzXskC$j0=2lCGhW$M@ZkVAXPEO#%!nz!fvVI&JzWLbu)wt^L zx%MgjZ9~M%9$KAoTK;sPq>7b$5JE3zZEBgLYhz?oXV>ydO6sd0#X*(Fwa`_F-x*$& zw9=g?2K0F8^+VI2c^&h>7{^VE<3Kg7`P`r2eAS5BUA}OHLFtBsTz39-oFZhD(aMqd zB0v%9;Ogl5!{YH{9|jyaA*F?-9^U1}8?6|Q+ts2FN5R4{7EC$Cs?la#+PkgC2CTRjh4CQh~}(UAIQ-x2tnS zdAAAR8rqm$jY@KgL(%OA#*h)2jw(Vq-So-IgXQG2E3 zk?U8b-!#!uu&c!f0jrb9EPvnH~$p#c}5j4Qa1@sFSA z4D@3SaEBeCMgrfk4m$1^nWFS((3hxd*q@cl)dyTqE~^wz*6g37>@3W)I5PM)LU52Y zw9sS1HJzR|hLvD^_j z+vMdPtgST#!m3mc0^21^OJT`_0AS&9ia*7bfIZUTG1YkE?#af6?2=dB@{XOF^DljM zOp|YD^=U&+q^YyUJZ~6Gks}d59pUV}ednoNbV^E7@pSW-FPv>>7mi>g0@yXM?i8cg z5IX}&>AsPXQGvd`k(rrLVh4QS?!Z6`EE##fqs%d`<|z*9TSo>afKwq>`{vkP`JeGt zJz)=JE)nz42N(>9m3R}-x1ul-wKtBXt|b^eX&=`v?eoLRw@W!7)HLz>ZX#A!D8(4P zg7S)ZP#r50MPJIA!cDi=85x{pGSq6_bPX+I)Q@CyMa9;ND_yVgB)(!kBOf@FNYtb=h25-Vvp$ z360tjeQD*{M6&A${GnIwfwUi#CxyX;k0(C&2^1RAcO6v1K^7_kah~DoDlaRmvYBkJ zlOQMm8W3u7C@1fBB<56b7=w_ltdK7ljqJWTOl6G{t9jZ2$jqd$Ih_NRTic=v*T zK}=tbbhSuX+JR4kI)jPni?JcmHo2I=nK-t@J!sP#y>_Q);ast>wz&Y=Viu=|T*7@qPB zp;x&2wbXK#KEqS74f7#CT@vN>m6aLi#tIlgv zD46{w7*uzu8k@~XK#Y2kf&#b86-VJ&{i3|`p18A zTkMZZgfl~T2mLdHWyi)LZKaICn`JjDy;YA1lt67l`jvlR`GpZJ)B%g{l1SWfU`+K{ zsN4;9vkyz#K2KasH8-0_zOfS(j@v%lO7@kj!1katO*MQ^ZN<)TOs6ut-su9PKvJDU-VTR}yd<^M z*Oj@P+g`(H_9$>&iMPMjx*gQETckg)e@K~v*Dv5r{KT0bol0ge%on^+eWbl9e1=ZHluHS{w$l_*~mSQVlYLiXk3la>0$6>4qK3j>kS-ya2&ZoShjk)_n5D<-95T;xTPQ`u)yBMYu3=Jzz*d$ zjPsk&Uz<4$l7|cpnsaZDV7D@g^dX z1!}Ke>FVkRP_xjn6pu&;@c9-{qAidR;RC`ilEOq=+mVMe@mVz%`atYMy~E;A61xWS zaCN@5wY~G*hkP{sBhTKSKNEU8{QZ-a6wMj7^{AkYPGfZvm|aZjD2a!dbTK!X--Efshgpp9(G}uW^{m{)c`^xEa=-C_fUvyP!a~u*l_@XCx{I^o zKM>Q@FFEF|3nskjd?GLh`){A&Mf#suN?B^xdD22-aP~?O%r7UPgZ!B~S{DZhlH;ht zngwLnoTV@Tr(t?HcDgf`5FC9*B57hWbIS=0AdZ^;{tb|$9%9gSfgbVOJZX* zHI(ExpDu5Q|0%`I-M#uVTT~z*K5>9Uq8$=@*HeY9Bpg>-{j2$XBp(IE3GgU0NqFBE z5{m5uKAMA6aj$XUr6@0lZqdX20SN7`T?T-l@%~Yg^=cSoy;bT3EI7EX{cv!5YlS^V z<4d<&^-5T^xVSx1)?r5s3Pp#8KCm&Abc|NvWMz-|RpU)GvI;~w6Z)f8a3*S2>(t{} z^Y|&v2XxDe2s`$Z@w-u-E25M*J}xmaW4%d%?3ctPrT16R<))({A&sG7tD&waF7$b5 zpf~EGR(!}Jdu@%D7j2b1s?fXS@;1rF38dwN=H?6c&&@; z7GbYD`L-uL{*t7d+voO?Yby8qw+gee>~A3v2>9zCt~TER;agFm&bEGRZ!`LlmEEO4 z;x!ze^2wn%jH!u)|Ha8lyJ+2b8_Yj+pbi$guvhrFV0>%qINI-rw#Lx_av~bV@@P(^i*$ z@#v|Z@t6q!Q=`NM3g+<)UCNmy?bL=Z%`KB84`kJrY8vi1ojZmZCt%sEtkwa;WlE9u;1h<5x=a+DmK~h=JV{TkZ>#*VdkT0Sv5=-(S8=$A}>(H!vd`mnMhc zA2~0V=VfHr*S@br&1bY6A0Gp$YIP$f%oFvJ`t{8ztAV4D@ij{?Z>~${AV7wk04<+B zLIOaz3yWpu?AW~ry6ETo*evNGjjuMV`zAEbY#RK zpMLn5b)wqV;LV%2_4Ts#20AY=BODx^yu8()$1Cc18ZsKX`}=bXuK4xS*-`1Js-_p) zKAU%8x`Kb^$Uxdr9Zl1gU)D*Onn~8DKYMNNp;uEc%MvM7G z-MRBD;yvX6i$a#RQ#BCtt$!rAW{BS; ze_J$6EDO>ZEK+$)YQ0lBsA=%9EN@frDWAw9b(27GSRL)`+S1}WV|-H5Gjg$uWO~^r zh$z6N?U-rQAP3=fB_FY78csTxrE96m6(G*1rCmBYq6VRMXIGa^|4Pj6xirDo9%D^w zmaBz{vphv9;^My1uvZocIPHPKg`(GFX_7-xPe3XzAdsPBLSwLM@p3N}?5l$EJ_iNe zhDFua)Zjct$jPVx9$S?AusHW&=Jw^XJmXwy84@VKIqzMjc%Hk+b{11ScK5xRf7Kcu zXa~6d4b!paJ-Pr>l2>Wcf0{gm)>-`J^ex+8`zv>E>w&?JZ`GW#g7xs`sQ$>%P=Dc$ zItBu!=wp>XkD^DMpXqRb67^rTYekCxA+Z0~kmK*3`~SVr&RrR^M0%v+3F6~9>Z@eA zQeTNVX=!Od*{6E#)twX(M>ZkCRRG03r8=;A=NG8LIoaSAV&mr*2q1;uGeSTi1A=NG z0N8Z1vk|}z4~hx;HFhoS?I3~;#$mf)bLt$CQkW z-0=7N2qX;*5kWBjDN5}!C1qY!R+b5{-B(Be9RTpNgAn`AN421giL-*e~|X!Im5)=6xW<_~%an?aw7ZSDQCk9BvuLJvLaKo=&Y(H8eC-RUH6U zs*L9n5(bD*&<9?t^O6KviVqbyRq3 zYti5)P$Zcr{QUMUT9AN7{#Fzy=Un<5ox;_Rnr&%mx%slTHjF7_XXi53-6wz_(J zc-SwL+>p7`(4Y%}3%hRJ{asVgCb9vm4d~=9EH68HWvr~MG&jE;D>rkjjERBs@(xs7 zXJ($=+!XUZb!8y8G<25&*OawRhQ20ZY0pw32*>>^MtUe zz^@Njv)LsuV;RrJyVcdZ`~uv&8afCZp@SXeVs4zR7xoan1JQOPwl zfJ5^%pJCv*2@&XoF@pXFAXTLTuH-1uBo}60zoXeZd7f+wJ!_TqJ|F-zmOp-stgEX_ zPp1d;5!!sweRgDI1YDGglz$T%tSgODj=p+g|F;(MUu@Xh|MmtG|L*<-^StL-z$!8B zhnih${wMs2LZfE2{r|?j{y)?PlhDMQ`AKAHfveu@t!kfWKELAZG~pwxB&cV>t^P}Q zAYICn)M+dS5IXrMhQuO1K;IbrCmssBx$1Xz{TVfZL`FtNBH>NQ-+~V={=V7B$p*eJ WhNnnOEPz+YUdkv*7d?OT?mqw~Hj~Q$ literal 25954 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 diff --git a/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-touch-mobile-chrome-linux.png b/browser_tests/tests/vueNodes/interactions/node/move.spec.ts-snapshots/vue-node-moved-node-touch-mobile-chrome-linux.png index 390def0a2cc9e84d10f277e419d4c23e1ee60b89..f4bac78b70c9466c304372e6b6177c7b9585f72b 100644 GIT binary patch literal 27159 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 IZ}H~e0HoOePyhe` literal 27485 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 ({ })) })) -function createWrapper(pinia = createTestingPinia({ createSpy: vi.fn })) { +type WrapperOptions = { + pinia?: ReturnType + stubs?: Record + attachTo?: HTMLElement +} + +function createWrapper({ + pinia = createTestingPinia({ createSpy: vi.fn }), + stubs = {}, + attachTo +}: WrapperOptions = {}) { const i18n = createI18n({ legacy: false, locale: 'en', @@ -55,18 +67,21 @@ function createWrapper(pinia = createTestingPinia({ createSpy: vi.fn })) { }) return mount(TopMenuSection, { + attachTo, global: { plugins: [pinia, i18n], stubs: { SubgraphBreadcrumb: true, QueueProgressOverlay: true, + QueueInlineProgressSummary: true, CurrentUserButton: true, LoginButton: true, ContextMenu: { name: 'ContextMenu', props: ['model'], template: '