Compare commits

..

11 Commits

Author SHA1 Message Date
Benjamin Lu
6afc2b62a7 chore(tsconfig): include scripts and tailwind config 2025-09-18 16:58:33 -07:00
AustinMroz
eb664f47af Fix cyclic prototype errors with subgraphNodes (#5637)
#5024 added support for connecting primitive nodes to subgraph inputs.
To accomplish this, it pulls WidgetLocator information from the node
owning the widget.

This `node` property does not exist on all IBaseWidget. `toConcrete` was
used to instead have a BaseWidget which is guaranteed to have a node
property. The issue that was missed, is that a widget which lacks this
information (such as most implemented by custom nodes) sets the node
value to the argument which was passed. Here that is the reference to
the subgraph node. Sometimes, this `#setWidget` call is made multiple
times, and when this occurs, the `input.widget` has itself set as the
protoyep, throwing an error.

This is resolved by instead taking an additional input which is
unambiguous.

For reference, this is a near minimal workflow using comfy_mtb that
replicates the issue

[cyclic.json](https://github.com/user-attachments/files/22412187/cyclic.json)

Special thanks to @melMass for assistance discovering this issue.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5637-Fix-cyclic-prototype-errors-with-subgraphNodes-2726d73d365081fea356f5197e4c2b42)
by [Unito](https://www.unito.io)
2025-09-18 16:06:12 -07:00
Christian Byrne
bc85d4e87b Make Vue nodes read-only when in panning mode (#5574)
## Summary

Integrated Vue node components with canvas panning mode to prevent UI
interference during navigation.

## Changes

- **What**: Added
[canCapturePointerEvents](https://docs.comfy.org/guide/vue-nodes)
computed property to `useCanvasInteractions` composable that checks
canvas read-only state
- **What**: Modified Vue node components (LGraphNode, NodeWidgets) to
conditionally handle pointer events based on canvas navigation mode
- **What**: Updated node event handlers to respect panning mode and
forward events to canvas when appropriate

## Review Focus

Event forwarding logic in panning mode and pointer event capture state
management across Vue node hierarchy.

```mermaid
graph TD
    A[User Interaction] --> B{Canvas in Panning Mode?}
    B -->|Yes| C[Forward to Canvas]
    B -->|No| D[Handle in Vue Component]
    C --> E[Canvas Navigation]
    D --> F[Node Selection/Widget Interaction]

    G[canCapturePointerEvents] --> H{read_only === false}
    H -->|Yes| I[Allow Vue Events]
    H -->|No| J[Block Vue Events]

    style A fill:#f9f9f9,stroke:#333,color:#000
    style E fill:#f9f9f9,stroke:#333,color:#000
    style F fill:#f9f9f9,stroke:#333,color:#000
    style I fill:#e1f5fe,stroke:#01579b,color:#000
    style J fill:#ffebee,stroke:#c62828,color:#000
```

## Screenshots




https://github.com/user-attachments/assets/00dc5e4a-2b56-43be-b92e-eaf511e52542

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5574-Make-Vue-nodes-read-only-when-in-panning-mode-26f6d73d3650818c951cd82c8fe58972)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2025-09-18 15:43:35 -07:00
Comfy Org PR Bot
7585444ce6 1.28.0 (#5640)
Minor version increment to 1.28.0

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5640-1-28-0-2726d73d3650818e846fcf78cbf33b73)
by [Unito](https://www.unito.io)

Co-authored-by: benceruleanlu <162923238+benceruleanlu@users.noreply.github.com>
2025-09-18 14:13:33 -07:00
Robin Huang
a886798a10 Explicitly add email scope for social auth login. (#5638)
## Summary

Some users were authenticating successfully but their email addresses
weren't being extracted from the Firebase token. This happened because
we weren't explicitly requesting the email scope during OAuth
authentication.
 
While Firebase's default configuration includes basic profile info, it
doesn't guarantee email access for all account types - particularly
Google Workspace accounts with restrictive policies or users with
privacy-conscious settings.

[Github
Scopes](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps)

## Changes

Adding email scope for Google + Github social OAuth.

## Review Focus
N/A

## Screenshots (if applicable)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5638-Explicitly-add-email-scope-for-social-auth-login-2726d73d3650817ab356fc9c04f8641b)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
2025-09-18 14:09:16 -07:00
Christian Byrne
37975e4eac [test] Add component test for image compare widget (#5549)
## Summary

Added comprehensive component test suite for WidgetImageCompare widget
with 410 test assertions covering display, edge cases, and integration
scenarios.

## Changes

- **What**: Created [Vue Test Utils](https://vue-test-utils.vuejs.org/)
test suite for [WidgetImageCompare
component](src/renderer/extensions/vueNodes/widgets/components/WidgetImageCompare.vue)
using [Vitest](https://vitest.dev/) testing framework

## Review Focus

Test coverage completeness for string vs object value handling,
accessibility attribute propagation, and edge case robustness including
malformed URLs and empty states.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5549-test-Add-component-test-for-image-compare-widget-26e6d73d365081189fe0d010f87d1eec)
by [Unito](https://www.unito.io)

---------

Co-authored-by: DrJKL <DrJKL@users.noreply.github.com>
2025-09-18 13:44:21 -07:00
Jin Yi
a41b8a6d4f refactor: Change manager flag from --disable-manager to --enable-manager (#5635)
## Summary
- Updated frontend to align with backend changes in ComfyUI core PR
#7555
- Changed manager startup argument from `--disable-manager` (opt-out) to
`--enable-manager` (opt-in)
- Manager is now disabled by default unless explicitly enabled

## Changes
- Modified `useManagerState.ts` to check for `--enable-manager` flag
presence
- Inverted logic: manager is disabled when the flag is NOT present
- Updated all related tests to reflect the new opt-in behavior
- Fixed edge case where `systemStats` is null

## Related
- Backend PR: https://github.com/comfyanonymous/ComfyUI/pull/7555

## Test Plan
- [x] All unit tests pass
- [x] Verified manager state logic with different flag combinations
- [x] TypeScript type checking passes
- [x] Linting passes

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

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5635-refactor-Change-manager-flag-from-disable-manager-to-enable-manager-2726d73d36508153a88bd9f152132b2a)
by [Unito](https://www.unito.io)
2025-09-18 11:45:07 -07:00
Alexander Brown
b264685052 lint: add tsconfig for browser_tests, fix existing violations (#5633)
## Summary

See https://typescript-eslint.io/blog/project-service/ for context.
Creates a browser_tests specific tsconfig so that they can be linted.

Does not add a package.json script to do the linting yet, but `pnpm exec
eslint browser_tests` should work for now.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5633-lint-add-tsconfig-for-browser_tests-fix-existing-violations-2726d73d3650819d8ef2c4b0abc31e14)
by [Unito](https://www.unito.io)
2025-09-18 11:35:44 -07:00
Johnpaul Chiwetelu
78d0ea6fa5 LazyImage on Safari (#5626)
This pull request improves the lazy loading behavior and caching
strategy for images in the `LazyImage.vue` component. The most
significant changes are focused on optimizing image rendering and
resource management, as well as improving code clarity.

**Lazy loading behavior improvements:**

* Changed the `<img>` element to render only when `cachedSrc` is
available, ensuring that images are not displayed before they are ready.
* Updated watchers in `LazyImage.vue` to use clearer variable names
(`shouldLoadVal` instead of `shouldLoad`) for better readability and
maintainability.
[[1]](diffhunk://#diff-3a1bfa7eb8cb26b04bea73f7b4b4e3c01e9d20a7eba6c3738fb47f96da1a7c95L80-R81)
[[2]](diffhunk://#diff-3a1bfa7eb8cb26b04bea73f7b4b4e3c01e9d20a7eba6c3738fb47f96da1a7c95L96-R96)

**Caching strategy enhancement:**

* Modified the `fetch` call in `mediaCacheService.ts` to use `{ cache:
'force-cache' }`, which leverages the browser's cache more aggressively
when loading media, potentially improving performance and reducing
network requests.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5626-LazyImage-on-Safari-2716d73d365081eeb1d3c2a96be4d408)
by [Unito](https://www.unito.io)
2025-09-18 11:20:19 -07:00
Christian Byrne
ea4e57b602 Move VueFire persistence configuration to initialization (#5614)
Currently, we set persistence method in the auth store setup. This
creates pattern of using the default on init (indexed DB) up until the
firebase store is initialized and `setPersistence` is called. For
devices that don't support indexed DB or have the connection aggresively
terminated or cleared, like
[Safari](https://comfy-org.sentry.io/issues/6879071102/?project=4509681221369857&query=is%3Aunresolved&referrer=issue-stream),
this can create problems with maintaing auth persistence.

Fix by setting persistence method in the initialization in main.ts

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5614-Move-VueFire-persistence-configuration-to-initialization-2716d73d3650817480e0c8feb1f37b9a)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-09-18 11:18:05 -07:00
Johnpaul Chiwetelu
4789d86fe8 Line Selection toolbox up with Vue Nodes (#5601)
This pull request improves the selection toolbox behavior during node
dragging by ensuring that it correctly responds to both LiteGraph and
Vue node drag events. The main changes introduce a reactive drag state
for Vue nodes in the layout store and update the selection toolbox
composable and Vue node component to use this state.

**Selection toolbox behavior improvements:**

* Added a helper function and separate watchers in
`useSelectionToolboxPosition.ts` to hide the selection toolbox when
either LiteGraph or Vue nodes are being dragged. This ensures consistent
UI feedback regardless of node type.
[[1]](diffhunk://#diff-57a51ac5e656e64ae7fd276d71b115058631621755de33b1eb8e8a4731d48713L171-R172)
[[2]](diffhunk://#diff-57a51ac5e656e64ae7fd276d71b115058631621755de33b1eb8e8a4731d48713R212-R224)

**Vue node drag state management:**

* Added a reactive `isDraggingVueNodes` property to the
`LayoutStoreImpl` class, along with getter and setter methods to manage
Vue node drag state. This allows other components to reactively track
when Vue nodes are being dragged.
[[1]](diffhunk://#diff-80d32fe0fb72730c16cf7259adef8b20732ff214df240b1d39ae516737beaf3bR133-R135)
[[2]](diffhunk://#diff-80d32fe0fb72730c16cf7259adef8b20732ff214df240b1d39ae516737beaf3bR354-R367)
* Updated `LGraphNode.vue` to set and clear the Vue node dragging state
in the layout store during pointer down and up events, ensuring the
selection toolbox is hidden while dragging Vue nodes.
[[1]](diffhunk://#diff-a7744614cf842e54416047326db79ad81f7c7ab7bfb66ae2b46f5c73ac7d47f2R357-R360)
[[2]](diffhunk://#diff-a7744614cf842e54416047326db79ad81f7c7ab7bfb66ae2b46f5c73ac7d47f2R376-R378)

**Dependency updates:**

* Imported the `layoutStore` in `LGraphNode.vue` to access the new drag
state management methods.
* Added missing `ref` import in `layoutStore.ts` to support the new
reactive property.



https://github.com/user-attachments/assets/d6e9c15e-63b5-4de2-9688-ebbc6a3be545

---------

Co-authored-by: GitHub Action <action@github.com>
2025-09-18 11:17:14 -07:00
73 changed files with 753 additions and 186 deletions

View File

@@ -1,4 +1,5 @@
import { Page, test as base } from '@playwright/test'
import type { Page } from '@playwright/test'
import { test as base } from '@playwright/test'
export class UserSelectPage {
constructor(

View File

@@ -1,4 +1,4 @@
import { Locator, Page } from '@playwright/test'
import type { Locator, Page } from '@playwright/test'
export class ComfyNodeSearchFilterSelectionPanel {
constructor(public readonly page: Page) {}

View File

@@ -1,6 +1,6 @@
import { Page } from '@playwright/test'
import type { Page } from '@playwright/test'
import { ComfyPage } from '../ComfyPage'
import type { ComfyPage } from '../ComfyPage'
export class SettingDialog {
constructor(

View File

@@ -1,4 +1,4 @@
import { Locator, Page } from '@playwright/test'
import type { Locator, Page } from '@playwright/test'
class SidebarTab {
constructor(

View File

@@ -1,4 +1,5 @@
import { Locator, Page, expect } from '@playwright/test'
import type { Locator, Page } from '@playwright/test'
import { expect } from '@playwright/test'
export class Topbar {
private readonly menuLocator: Locator

View File

@@ -12,9 +12,10 @@ export const webSocketFixture = base.extend<{
// so we can look it up to trigger messages
const store: Record<string, WebSocket> = ((window as any).__ws__ = {})
window.WebSocket = class extends window.WebSocket {
constructor() {
// @ts-expect-error
super(...arguments)
constructor(
...rest: ConstructorParameters<typeof window.WebSocket>
) {
super(...rest)
store[this.url] = this
}
}

View File

@@ -1,4 +1,4 @@
import { FullConfig } from '@playwright/test'
import type { FullConfig } from '@playwright/test'
import dotenv from 'dotenv'
import { backupPath } from './utils/backupUtils'

View File

@@ -1,4 +1,4 @@
import { FullConfig } from '@playwright/test'
import type { FullConfig } from '@playwright/test'
import dotenv from 'dotenv'
import { restorePath } from './utils/backupUtils'

View File

@@ -1,4 +1,4 @@
import { Locator, Page } from '@playwright/test'
import type { Locator, Page } from '@playwright/test'
export class ManageGroupNode {
footer: Locator

View File

@@ -1,7 +1,7 @@
import { Locator, Page } from '@playwright/test'
import type { Locator, Page } from '@playwright/test'
import path from 'path'
import {
import type {
TemplateInfo,
WorkflowTemplates
} from '../../src/platform/workflow/templates/types/template'

View File

@@ -29,9 +29,9 @@ test.describe('Actionbar', () => {
// Intercept the prompt queue endpoint
let promptNumber = 0
comfyPage.page.route('**/api/prompt', async (route, req) => {
await comfyPage.page.route('**/api/prompt', async (route, req) => {
await new Promise((r) => setTimeout(r, 100))
route.fulfill({
await route.fulfill({
status: 200,
body: JSON.stringify({
prompt_id: promptNumber,

View File

@@ -1,5 +1,5 @@
import type { ComfyPage } from '../fixtures/ComfyPage'
import {
ComfyPage,
comfyExpect as expect,
comfyPageFixture as test
} from '../fixtures/ComfyPage'

View File

@@ -1,4 +1,5 @@
import { Page, expect } from '@playwright/test'
import type { Page } from '@playwright/test'
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'

View File

@@ -1,4 +1,5 @@
import { Locator, expect } from '@playwright/test'
import type { Locator } from '@playwright/test'
import { expect } from '@playwright/test'
import type { Keybinding } from '../../src/schemas/keyBindingSchema'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'

View File

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

View File

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

View File

@@ -1,12 +1,13 @@
import { Locator, expect } from '@playwright/test'
import { Position } from '@vueuse/core'
import type { Locator } from '@playwright/test'
import { expect } from '@playwright/test'
import type { Position } from '@vueuse/core'
import {
type ComfyPage,
comfyPageFixture as test,
testComfySnapToGridGridSize
} from '../fixtures/ComfyPage'
import { type NodeReference } from '../fixtures/utils/litegraphUtils'
import type { NodeReference } from '../fixtures/utils/litegraphUtils'
test.describe('Item Interaction', () => {
test('Can select/delete all items', async ({ comfyPage }) => {

View File

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

View File

@@ -160,7 +160,9 @@ test.describe.skip('Queue sidebar', () => {
comfyPage
}) => {
await comfyPage.nextFrame()
expect(comfyPage.menu.queueTab.getGalleryImage(firstImage)).toBeVisible()
await expect(
comfyPage.menu.queueTab.getGalleryImage(firstImage)
).toBeVisible()
})
test('maintains active gallery item when new tasks are added', async ({
@@ -174,7 +176,9 @@ test.describe.skip('Queue sidebar', () => {
const newTask = comfyPage.menu.queueTab.tasks.getByAltText(newImage)
await newTask.waitFor({ state: 'visible' })
// The active gallery item should still be the initial image
expect(comfyPage.menu.queueTab.getGalleryImage(firstImage)).toBeVisible()
await expect(
comfyPage.menu.queueTab.getGalleryImage(firstImage)
).toBeVisible()
})
test.describe('Gallery navigation', () => {
@@ -196,7 +200,9 @@ test.describe.skip('Queue sidebar', () => {
delay: 256
})
await comfyPage.nextFrame()
expect(comfyPage.menu.queueTab.getGalleryImage(end)).toBeVisible()
await expect(
comfyPage.menu.queueTab.getGalleryImage(end)
).toBeVisible()
})
})
})

View File

@@ -1,4 +1,5 @@
import { Page, expect } from '@playwright/test'
import type { Page } from '@playwright/test'
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'

View File

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

View File

@@ -1,5 +1,5 @@
{
"extends": "./tsconfig.json",
"extends": "../tsconfig.json",
"compilerOptions": {
/* Test files should not be compiled */
"noEmit": true,
@@ -9,13 +9,6 @@
"resolveJsonModule": true
},
"include": [
"*.ts",
"*.mts",
"*.config.js",
"browser_tests/**/*.ts",
"scripts/**/*.js",
"scripts/**/*.ts",
"tests-ui/**/*.ts",
".storybook/**/*.ts"
"**/*.ts",
]
}

View File

@@ -20,7 +20,6 @@ export default defineConfig([
'src/types/vue-shim.d.ts',
'src/types/comfyRegistryTypes.ts',
'src/types/generatedManagerTypes.ts',
'browser_tests/**/*',
'**/vite.config.*.timestamp*',
'**/vitest.config.*.timestamp*'
]
@@ -154,5 +153,14 @@ export default defineConfig([
}
]
}
},
{
files: ['tests-ui/**/*'],
rules: {
'@typescript-eslint/consistent-type-imports': [
'error',
{ disallowTypeAnnotations: false }
]
}
}
])

View File

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

View File

@@ -10,7 +10,7 @@
class="absolute inset-0"
/>
<img
v-show="isImageLoaded"
v-if="cachedSrc"
ref="imageRef"
:src="cachedSrc"
:alt="alt"
@@ -77,8 +77,8 @@ const shouldLoad = computed(() => isIntersecting.value)
watch(
shouldLoad,
async (shouldLoad) => {
if (shouldLoad && src && !cachedSrc.value && !hasError.value) {
async (shouldLoadVal) => {
if (shouldLoadVal && src && !cachedSrc.value && !hasError.value) {
try {
const cachedMedia = await getCachedMedia(src)
if (cachedMedia.error) {
@@ -93,7 +93,7 @@ watch(
console.warn('Failed to load cached media:', error)
cachedSrc.value = src
}
} else if (!shouldLoad) {
} else if (!shouldLoadVal) {
if (cachedSrc.value?.startsWith('blob:')) {
releaseUrl(src)
}

View File

@@ -96,7 +96,6 @@ import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vu
import SideToolbar from '@/components/sidebar/SideToolbar.vue'
import SecondRowWorkflowTabs from '@/components/topbar/SecondRowWorkflowTabs.vue'
import { useChainCallback } from '@/composables/functional/useChainCallback'
import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions'
import { useViewportCulling } from '@/composables/graph/useViewportCulling'
import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle'
import { useNodeBadge } from '@/composables/node/useNodeBadge'
@@ -118,6 +117,7 @@ import { useWorkflowAutoSave } from '@/platform/workflow/persistence/composables
import { useWorkflowPersistence } from '@/platform/workflow/persistence/composables/useWorkflowPersistence'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { SelectedNodeIdsKey } from '@/renderer/core/canvas/injectionKeys'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import TransformPane from '@/renderer/core/layout/transform/TransformPane.vue'
import MiniMap from '@/renderer/extensions/minimap/MiniMap.vue'
import VueGraphNode from '@/renderer/extensions/vueNodes/components/LGraphNode.vue'

View File

@@ -124,11 +124,11 @@ import ButtonGroup from 'primevue/buttongroup'
import { computed, onBeforeUnmount, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions'
import { useZoomControls } from '@/composables/useZoomControls'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap'
import { useCommandStore } from '@/stores/commandStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'

View File

@@ -5,12 +5,12 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createI18n } from 'vue-i18n'
import SelectionToolbox from '@/components/graph/SelectionToolbox.vue'
import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import { useExtensionService } from '@/services/extensionService'
// Mock the composables and services
vi.mock('@/composables/graph/useCanvasInteractions', () => ({
vi.mock('@/renderer/core/canvas/useCanvasInteractions', () => ({
useCanvasInteractions: vi.fn(() => ({
handleWheel: vi.fn()
}))

View File

@@ -60,9 +60,9 @@ import MaskEditorButton from '@/components/graph/selectionToolbox/MaskEditorButt
import RefreshSelectionButton from '@/components/graph/selectionToolbox/RefreshSelectionButton.vue'
import PublishSubgraphButton from '@/components/graph/selectionToolbox/SaveToSubgraphLibrary.vue'
import { useSelectionToolboxPosition } from '@/composables/canvas/useSelectionToolboxPosition'
import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions'
import { useSelectionState } from '@/composables/graph/useSelectionState'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap'
import { useExtensionService } from '@/services/extensionService'
import { type ComfyCommandImpl, useCommandStore } from '@/stores/commandStore'

View File

@@ -1,4 +1,4 @@
import { onUnmounted, ref, watch } from 'vue'
import { computed, onUnmounted, ref, watch } from 'vue'
import type { Ref } from 'vue'
import { useCanvasTransformSync } from '@/composables/canvas/useCanvasTransformSync'
@@ -170,50 +170,75 @@ export function useSelectionToolboxPosition(
}
)
// Watch for dragging state
watch(
() => canvasStore.canvas?.state?.draggingItems,
(dragging) => {
if (dragging) {
visible.value = false
if (moreOptionsOpen.value) {
const currentSig = buildSelectionSignature(canvasStore)
if (currentSig !== moreOptionsSelectionSignature) {
moreOptionsSelectionSignature = null
}
moreOptionsWasOpenBeforeDrag = true
moreOptionsOpen.value = false
moreOptionsRestorePending.value = !!moreOptionsSelectionSignature
if (moreOptionsRestorePending.value) {
forceCloseMoreOptionsSignal.value++
} else {
moreOptionsWasOpenBeforeDrag = false
}
} else {
moreOptionsRestorePending.value = false
moreOptionsWasOpenBeforeDrag = false
}
} else {
requestAnimationFrame(() => {
updateSelectionBounds()
const selectionMatches = currentSelectionMatchesSignature(canvasStore)
const shouldRestore =
moreOptionsWasOpenBeforeDrag &&
visible.value &&
moreOptionsRestorePending.value &&
selectionMatches
if (shouldRestore) {
restoreMoreOptionsSignal.value++
} else {
moreOptionsRestorePending.value = false
}
moreOptionsWasOpenBeforeDrag = false
})
}
const handleDragStateChange = (dragging: boolean) => {
if (dragging) {
handleDragStart()
return
}
)
handleDragEnd()
}
const handleDragStart = () => {
visible.value = false
// Early return if more options wasn't open
if (!moreOptionsOpen.value) {
moreOptionsRestorePending.value = false
moreOptionsWasOpenBeforeDrag = false
return
}
// Handle more options cleanup
const currentSig = buildSelectionSignature(canvasStore)
const selectionChanged = currentSig !== moreOptionsSelectionSignature
if (selectionChanged) {
moreOptionsSelectionSignature = null
}
moreOptionsOpen.value = false
moreOptionsWasOpenBeforeDrag = true
moreOptionsRestorePending.value = !!moreOptionsSelectionSignature
if (moreOptionsRestorePending.value) {
forceCloseMoreOptionsSignal.value++
return
}
moreOptionsWasOpenBeforeDrag = false
}
const handleDragEnd = () => {
requestAnimationFrame(() => {
updateSelectionBounds()
const selectionMatches = currentSelectionMatchesSignature(canvasStore)
const shouldRestore =
moreOptionsWasOpenBeforeDrag &&
visible.value &&
moreOptionsRestorePending.value &&
selectionMatches
// Single point of assignment for each ref
moreOptionsRestorePending.value =
shouldRestore && moreOptionsRestorePending.value
moreOptionsWasOpenBeforeDrag = false
if (shouldRestore) {
restoreMoreOptionsSignal.value++
}
})
}
// Unified dragging state - combines both LiteGraph and Vue node dragging
const isDragging = computed((): boolean => {
const litegraphDragging = canvasStore.canvas?.state?.draggingItems ?? false
const vueNodeDragging =
shouldRenderVueNodes.value && layoutStore.isDraggingVueNodes.value
return litegraphDragging || vueNodeDragging
})
watch(isDragging, handleDragStateChange)
onUnmounted(() => {
resetMoreOptionsState()

View File

@@ -1,5 +1,5 @@
import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
import { fitDimensionsToNodeWidth } from '@/utils/imageUtil'

View File

@@ -42,7 +42,12 @@ export function useManagerState() {
)
// Check command line args first (highest priority)
if (systemStats.value?.system?.argv?.includes('--disable-manager')) {
// --enable-manager flag enables the manager (opposite of old --disable-manager)
const hasEnableManager =
systemStats.value?.system?.argv?.includes('--enable-manager')
// If --enable-manager is NOT present, manager is disabled
if (!hasEnableManager) {
return ManagerUIState.DISABLED
}

View File

@@ -4,7 +4,10 @@ import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
import { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import { LLink, type ResolvedConnection } from '@/lib/litegraph/src/LLink'
import { RecursionError } from '@/lib/litegraph/src/infrastructure/RecursionError'
import type { ISubgraphInput } from '@/lib/litegraph/src/interfaces'
import type {
ISubgraphInput,
IWidgetLocator
} from '@/lib/litegraph/src/interfaces'
import type {
INodeInputSlot,
ISlotType,
@@ -78,9 +81,10 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
const existingInput = this.inputs.find((i) => i.name == name)
if (existingInput) {
const linkId = subgraphInput.linkIds[0]
const { inputNode } = subgraph.links[linkId].resolve(subgraph)
const { inputNode, input } = subgraph.links[linkId].resolve(subgraph)
const widget = inputNode?.widgets?.find?.((w) => w.name == name)
if (widget) this.#setWidget(subgraphInput, existingInput, widget)
if (widget)
this.#setWidget(subgraphInput, existingInput, widget, input?.widget)
return
}
const input = this.addInput(name, type)
@@ -185,13 +189,14 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
subgraphInput.events.addEventListener(
'input-connected',
() => {
(e) => {
if (input._widget) return
const widget = subgraphInput._widget
if (!widget) return
this.#setWidget(subgraphInput, input, widget)
const widgetLocator = e.detail.input.widget
this.#setWidget(subgraphInput, input, widget, widgetLocator)
},
{ signal }
)
@@ -301,7 +306,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
const widget = resolved.inputNode.getWidgetFromSlot(resolved.input)
if (!widget) continue
this.#setWidget(subgraphInput, input, widget)
this.#setWidget(subgraphInput, input, widget, resolved.input.widget)
break
}
}
@@ -310,11 +315,13 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
#setWidget(
subgraphInput: Readonly<SubgraphInput>,
input: INodeInputSlot,
widget: Readonly<IBaseWidget>
widget: Readonly<IBaseWidget>,
inputWidget: IWidgetLocator | undefined
) {
// Use the first matching widget
const targetWidget = toConcreteWidget(widget, this)
const promotedWidget = targetWidget.createCopyForNode(this)
const promotedWidget = toConcreteWidget(widget, this).createCopyForNode(
this
)
Object.assign(promotedWidget, {
get name() {
@@ -372,11 +379,9 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
// NOTE: This code creates linked chains of prototypes for passing across
// multiple levels of subgraphs. As part of this, it intentionally avoids
// creating new objects. Have care when making changes.
const backingInput =
targetWidget.node.findInputSlot(widget.name, true)?.widget ?? {}
input.widget ??= { name: subgraphInput.name }
input.widget.name = subgraphInput.name
Object.setPrototypeOf(input.widget, backingInput)
if (inputWidget) Object.setPrototypeOf(input.widget, inputWidget)
input._widget = promotedWidget
}

View File

@@ -2,6 +2,11 @@ import { definePreset } from '@primevue/themes'
import Aura from '@primevue/themes/aura'
import * as Sentry from '@sentry/vue'
import { initializeApp } from 'firebase/app'
import {
browserLocalPersistence,
browserSessionPersistence,
indexedDBLocalPersistence
} from 'firebase/auth'
import { createPinia } from 'pinia'
import 'primeicons/primeicons.css'
import PrimeVue from 'primevue/config'
@@ -9,7 +14,7 @@ import ConfirmationService from 'primevue/confirmationservice'
import ToastService from 'primevue/toastservice'
import Tooltip from 'primevue/tooltip'
import { createApp } from 'vue'
import { VueFire, VueFireAuth } from 'vuefire'
import { VueFire, VueFireAuthWithDependencies } from 'vuefire'
import { FIREBASE_CONFIG } from '@/config/firebase'
import '@/lib/litegraph/public/css/litegraph.css'
@@ -66,6 +71,18 @@ app
.use(i18n)
.use(VueFire, {
firebaseApp,
modules: [VueFireAuth()]
modules: [
// Configure Firebase Auth persistence: localStorage first, IndexedDB last.
// Localstorage is preferred to IndexedDB for mobile Safari compatibility.
VueFireAuthWithDependencies({
dependencies: {
persistence: [
browserLocalPersistence,
browserSessionPersistence,
indexedDBLocalPersistence
]
}
})
]
})
.mount('#vue-app')

View File

@@ -11,12 +11,21 @@ import { app } from '@/scripts/app'
*/
export function useCanvasInteractions() {
const settingStore = useSettingStore()
const { getCanvas } = useCanvasStore()
const canvasStore = useCanvasStore()
const { getCanvas } = canvasStore
const isStandardNavMode = computed(
() => settingStore.get('Comfy.Canvas.NavigationMode') === 'standard'
)
/**
* Whether Vue node components should handle pointer events.
* Returns false when canvas is in read-only/panning mode (e.g., space key held for panning).
*/
const shouldHandleNodePointerEvents = computed(
() => !(canvasStore.canvas?.read_only ?? false)
)
/**
* Handles wheel events from UI components that should be forwarded to canvas
* when appropriate (e.g., Ctrl+wheel for zoom in standard mode)
@@ -97,6 +106,7 @@ export function useCanvasInteractions() {
return {
handleWheel,
handlePointer,
forwardEventToCanvas
forwardEventToCanvas,
shouldHandleNodePointerEvents
}
}

View File

@@ -5,7 +5,7 @@
* CRDT ensures conflict-free operations for both single and multi-user scenarios.
*/
import log from 'loglevel'
import { type ComputedRef, type Ref, computed, customRef } from 'vue'
import { type ComputedRef, type Ref, computed, customRef, ref } from 'vue'
import * as Y from 'yjs'
import { ACTOR_CONFIG } from '@/renderer/core/layout/constants'
@@ -134,6 +134,9 @@ class LayoutStoreImpl implements LayoutStore {
private slotSpatialIndex: SpatialIndexManager // For slots
private rerouteSpatialIndex: SpatialIndexManager // For reroutes
// Vue dragging state for selection toolbox (public ref for direct mutation)
public isDraggingVueNodes = ref(false)
constructor() {
// Initialize Yjs data structures
this.ynodes = this.ydoc.getMap('nodes')

View File

@@ -10,7 +10,9 @@
'bg-white dark-theme:bg-charcoal-800',
'lg-node absolute rounded-2xl',
'border border-solid border-sand-100 dark-theme:border-charcoal-600',
'hover:ring-7 ring-gray-500/50 dark-theme:ring-gray-500/20',
// hover (only when node should handle events)
shouldHandleNodePointerEvents &&
'hover:ring-7 ring-gray-500/50 dark-theme:ring-gray-500/20',
'outline-transparent -outline-offset-2 outline-2',
borderClass,
outlineClass,
@@ -21,7 +23,9 @@
'will-change-transform': isDragging
},
lodCssClass,
'pointer-events-auto'
shouldHandleNodePointerEvents
? 'pointer-events-auto'
: 'pointer-events-none'
)
"
:style="[
@@ -34,6 +38,7 @@
@pointerdown="handlePointerDown"
@pointermove="handlePointerMove"
@pointerup="handlePointerUp"
@wheel="handleWheel"
>
<div class="flex items-center">
<template v-if="isCollapsed">
@@ -147,7 +152,9 @@ import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { SelectedNodeIdsKey } from '@/renderer/core/canvas/injectionKeys'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import { TransformStateKey } from '@/renderer/core/layout/injectionKeys'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { useNodeExecutionState } from '@/renderer/extensions/vueNodes/execution/useNodeExecutionState'
import { useNodeLayout } from '@/renderer/extensions/vueNodes/layout/useNodeLayout'
import { LODLevel, useLOD } from '@/renderer/extensions/vueNodes/lod/useLOD'
@@ -236,6 +243,14 @@ const hasAnyError = computed(
const bypassed = computed((): boolean => nodeData.mode === 4)
// Use canvas interactions for proper wheel event handling and pointer event capture control
const {
handleWheel,
handlePointer,
forwardEventToCanvas,
shouldHandleNodePointerEvents
} = useCanvasInteractions()
// LOD (Level of Detail) system based on zoom level
const zoomRef = toRef(() => zoomLevel)
const {
@@ -361,14 +376,27 @@ const handlePointerDown = (event: PointerEvent) => {
return
}
// Don't handle pointer events when canvas is in panning mode - forward to canvas instead
if (!shouldHandleNodePointerEvents.value) {
forwardEventToCanvas(event)
return
}
// Start drag using layout system
isDragging.value = true
// Set Vue node dragging state for selection toolbox
layoutStore.isDraggingVueNodes.value = true
startDrag(event)
lastY.value = event.clientY
lastX.value = event.clientX
}
const handlePointerMove = (event: PointerEvent) => {
// Check if this should be forwarded to canvas (e.g., space panning, middle mouse)
handlePointer(event)
if (isDragging.value) {
void handleLayoutDrag(event)
}
@@ -378,7 +406,17 @@ const handlePointerUp = (event: PointerEvent) => {
if (isDragging.value) {
isDragging.value = false
void endDrag(event)
// Clear Vue node dragging state for selection toolbox
layoutStore.isDraggingVueNodes.value = false
}
// Don't emit node-click when canvas is in panning mode - forward to canvas instead
if (!shouldHandleNodePointerEvents.value) {
forwardEventToCanvas(event)
return
}
// Emit node-click for selection handling in GraphCanvas
const dx = event.clientX - lastX.value
const dy = event.clientY - lastY.value
@@ -401,6 +439,12 @@ const handleSlotClick = (
console.warn('LGraphNode: nodeData is null/undefined in handleSlotClick')
return
}
// Don't handle slot clicks when canvas is in panning mode
if (!shouldHandleNodePointerEvents.value) {
return
}
emit('slot-click', event, nodeData, slotIndex, isInput)
}

View File

@@ -2,7 +2,20 @@
<div v-if="renderError" class="node-error p-2 text-red-500 text-sm">
{{ $t('Node Widgets Error') }}
</div>
<div v-else class="lg-node-widgets flex flex-col gap-2 pr-4">
<div
v-else
:class="
cn(
'lg-node-widgets flex flex-col gap-2 pr-4',
shouldHandleNodePointerEvents
? 'pointer-events-auto'
: 'pointer-events-none'
)
"
@pointerdown="handleWidgetPointerEvent"
@pointermove="handleWidgetPointerEvent"
@pointerup="handleWidgetPointerEvent"
>
<div
v-for="(widget, index) in processedWidgets"
:key="`widget-${index}-${widget.name}`"
@@ -46,6 +59,7 @@ import type {
} from '@/composables/graph/useGraphNodeManager'
import { useErrorHandling } from '@/composables/useErrorHandling'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import { LODLevel } from '@/renderer/extensions/vueNodes/lod/useLOD'
// Import widget components directly
import WidgetInputText from '@/renderer/extensions/vueNodes/widgets/components/WidgetInputText.vue'
@@ -55,6 +69,7 @@ import {
shouldRenderAsVue
} from '@/renderer/extensions/vueNodes/widgets/registry/widgetRegistry'
import type { SimplifiedWidget, WidgetValue } from '@/types/simplifiedWidget'
import { cn } from '@/utils/tailwindUtil'
import InputSlot from './InputSlot.vue'
@@ -67,6 +82,14 @@ interface NodeWidgetsProps {
const props = defineProps<NodeWidgetsProps>()
const { shouldHandleNodePointerEvents, forwardEventToCanvas } =
useCanvasInteractions()
const handleWidgetPointerEvent = (event: PointerEvent) => {
if (!shouldHandleNodePointerEvents.value) {
forwardEventToCanvas(event)
}
}
// Error boundary implementation
const renderError = ref<string | null>(null)

View File

@@ -12,6 +12,7 @@ import type { Ref } from 'vue'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import { useNodeZIndex } from '@/renderer/extensions/vueNodes/composables/useNodeZIndex'
interface NodeManager {
@@ -21,6 +22,7 @@ interface NodeManager {
export function useNodeEventHandlers(nodeManager: Ref<NodeManager | null>) {
const canvasStore = useCanvasStore()
const { bringNodeToFront } = useNodeZIndex()
const { shouldHandleNodePointerEvents } = useCanvasInteractions()
/**
* Handle node selection events
@@ -31,6 +33,8 @@ export function useNodeEventHandlers(nodeManager: Ref<NodeManager | null>) {
nodeData: VueNodeData,
wasDragging: boolean
) => {
if (!shouldHandleNodePointerEvents.value) return
if (!canvasStore.canvas || !nodeManager.value) return
const node = nodeManager.value.getNode(nodeData.id)
@@ -69,6 +73,8 @@ export function useNodeEventHandlers(nodeManager: Ref<NodeManager | null>) {
* Uses LiteGraph's native collapse method for proper state management
*/
const handleNodeCollapse = (nodeId: string, collapsed: boolean) => {
if (!shouldHandleNodePointerEvents.value) return
if (!nodeManager.value) return
const node = nodeManager.value.getNode(nodeId)
@@ -86,6 +92,8 @@ export function useNodeEventHandlers(nodeManager: Ref<NodeManager | null>) {
* Updates the title in LiteGraph for persistence across sessions
*/
const handleNodeTitleUpdate = (nodeId: string, newTitle: string) => {
if (!shouldHandleNodePointerEvents.value) return
if (!nodeManager.value) return
const node = nodeManager.value.getNode(nodeId)
@@ -103,6 +111,8 @@ export function useNodeEventHandlers(nodeManager: Ref<NodeManager | null>) {
event: PointerEvent,
nodeData: VueNodeData
) => {
if (!shouldHandleNodePointerEvents.value) return
if (!canvasStore.canvas || !nodeManager.value) return
const node = nodeManager.value.getNode(nodeData.id)
@@ -123,6 +133,8 @@ export function useNodeEventHandlers(nodeManager: Ref<NodeManager | null>) {
* Integrates with LiteGraph's context menu system
*/
const handleNodeRightClick = (event: PointerEvent, nodeData: VueNodeData) => {
if (!shouldHandleNodePointerEvents.value) return
if (!canvasStore.canvas || !nodeManager.value) return
const node = nodeManager.value.getNode(nodeData.id)
@@ -145,6 +157,8 @@ export function useNodeEventHandlers(nodeManager: Ref<NodeManager | null>) {
* Prepares node for dragging and sets appropriate visual state
*/
const handleNodeDragStart = (event: DragEvent, nodeData: VueNodeData) => {
if (!shouldHandleNodePointerEvents.value) return
if (!canvasStore.canvas || !nodeManager.value) return
const node = nodeManager.value.getNode(nodeData.id)
@@ -173,6 +187,8 @@ export function useNodeEventHandlers(nodeManager: Ref<NodeManager | null>) {
* Useful for selection toolbox or area selection
*/
const selectNodes = (nodeIds: string[], addToSelection = false) => {
if (!shouldHandleNodePointerEvents.value) return
if (!canvasStore.canvas || !nodeManager.value) return
if (!addToSelection) {
@@ -193,6 +209,8 @@ export function useNodeEventHandlers(nodeManager: Ref<NodeManager | null>) {
* Deselect specific nodes
*/
const deselectNodes = (nodeIds: string[]) => {
if (!shouldHandleNodePointerEvents.value) return
if (!canvasStore.canvas || !nodeManager.value) return
nodeIds.forEach((nodeId) => {

View File

@@ -0,0 +1,337 @@
import { mount } from '@vue/test-utils'
import PrimeVue from 'primevue/config'
import ImageCompare from 'primevue/imagecompare'
import { describe, expect, it } from 'vitest'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import WidgetImageCompare, {
type ImageCompareValue
} from './WidgetImageCompare.vue'
describe('WidgetImageCompare Display', () => {
const createMockWidget = (
value: ImageCompareValue | string,
options: SimplifiedWidget['options'] = {}
): SimplifiedWidget<ImageCompareValue | string> => ({
name: 'test_imagecompare',
type: 'object',
value,
options
})
const mountComponent = (
widget: SimplifiedWidget<ImageCompareValue | string>,
readonly = false
) => {
return mount(WidgetImageCompare, {
global: {
plugins: [PrimeVue],
components: { ImageCompare }
},
props: {
widget,
readonly
}
})
}
describe('Component Rendering', () => {
it('renders imagecompare component with proper structure and styling', () => {
const value: ImageCompareValue = {
before: 'https://example.com/before.jpg',
after: 'https://example.com/after.jpg'
}
const widget = createMockWidget(value)
const wrapper = mountComponent(widget)
// Component exists
const imageCompare = wrapper.findComponent({ name: 'ImageCompare' })
expect(imageCompare.exists()).toBe(true)
// Renders both images with correct URLs
const images = wrapper.findAll('img')
expect(images).toHaveLength(2)
expect(images[0].attributes('src')).toBe('https://example.com/before.jpg')
expect(images[1].attributes('src')).toBe('https://example.com/after.jpg')
// Images have proper styling classes
images.forEach((img) => {
expect(img.classes()).toContain('object-cover')
expect(img.classes()).toContain('w-full')
expect(img.classes()).toContain('h-full')
})
})
})
describe('Object Value Input', () => {
it('handles alt text correctly - custom, default, and empty', () => {
// Test custom alt text
const customAltValue: ImageCompareValue = {
before: 'https://example.com/before.jpg',
after: 'https://example.com/after.jpg',
beforeAlt: 'Original design',
afterAlt: 'Updated design'
}
const customWrapper = mountComponent(createMockWidget(customAltValue))
const customImages = customWrapper.findAll('img')
expect(customImages[0].attributes('alt')).toBe('Original design')
expect(customImages[1].attributes('alt')).toBe('Updated design')
// Test default alt text
const defaultAltValue: ImageCompareValue = {
before: 'https://example.com/before.jpg',
after: 'https://example.com/after.jpg'
}
const defaultWrapper = mountComponent(createMockWidget(defaultAltValue))
const defaultImages = defaultWrapper.findAll('img')
expect(defaultImages[0].attributes('alt')).toBe('Before image')
expect(defaultImages[1].attributes('alt')).toBe('After image')
// Test empty string alt text (falls back to default)
const emptyAltValue: ImageCompareValue = {
before: 'https://example.com/before.jpg',
after: 'https://example.com/after.jpg',
beforeAlt: '',
afterAlt: ''
}
const emptyWrapper = mountComponent(createMockWidget(emptyAltValue))
const emptyImages = emptyWrapper.findAll('img')
expect(emptyImages[0].attributes('alt')).toBe('Before image')
expect(emptyImages[1].attributes('alt')).toBe('After image')
})
it('handles missing and partial image URLs gracefully', () => {
// Missing URLs
const missingValue: ImageCompareValue = { before: '', after: '' }
const missingWrapper = mountComponent(createMockWidget(missingValue))
const missingImages = missingWrapper.findAll('img')
expect(missingImages[0].attributes('src')).toBe('')
expect(missingImages[1].attributes('src')).toBe('')
// Partial URLs
const partialValue: ImageCompareValue = {
before: 'https://example.com/before.jpg',
after: ''
}
const partialWrapper = mountComponent(createMockWidget(partialValue))
const partialImages = partialWrapper.findAll('img')
expect(partialImages[0].attributes('src')).toBe(
'https://example.com/before.jpg'
)
expect(partialImages[1].attributes('src')).toBe('')
})
})
describe('String Value Input', () => {
it('handles string value as before image only', () => {
const value = 'https://example.com/single.jpg'
const widget = createMockWidget(value)
const wrapper = mountComponent(widget)
const images = wrapper.findAll('img')
expect(images[0].attributes('src')).toBe('https://example.com/single.jpg')
expect(images[1].attributes('src')).toBe('')
})
it('uses default alt text for string values', () => {
const value = 'https://example.com/single.jpg'
const widget = createMockWidget(value)
const wrapper = mountComponent(widget)
const images = wrapper.findAll('img')
expect(images[0].attributes('alt')).toBe('Before image')
expect(images[1].attributes('alt')).toBe('After image')
})
})
describe('Widget Options Handling', () => {
it('passes through accessibility options', () => {
const value: ImageCompareValue = {
before: 'https://example.com/before.jpg',
after: 'https://example.com/after.jpg'
}
const widget = createMockWidget(value, {
tabindex: 1,
ariaLabel: 'Compare images',
ariaLabelledby: 'compare-label'
})
const wrapper = mountComponent(widget)
const imageCompare = wrapper.findComponent({ name: 'ImageCompare' })
expect(imageCompare.props('tabindex')).toBe(1)
expect(imageCompare.props('ariaLabel')).toBe('Compare images')
expect(imageCompare.props('ariaLabelledby')).toBe('compare-label')
})
it('uses default tabindex when not provided', () => {
const value: ImageCompareValue = {
before: 'https://example.com/before.jpg',
after: 'https://example.com/after.jpg'
}
const widget = createMockWidget(value)
const wrapper = mountComponent(widget)
const imageCompare = wrapper.findComponent({ name: 'ImageCompare' })
expect(imageCompare.props('tabindex')).toBe(0)
})
it('passes through PrimeVue specific options', () => {
const value: ImageCompareValue = {
before: 'https://example.com/before.jpg',
after: 'https://example.com/after.jpg'
}
const widget = createMockWidget(value, {
unstyled: true,
pt: { root: { class: 'custom-class' } },
ptOptions: { mergeSections: true }
})
const wrapper = mountComponent(widget)
const imageCompare = wrapper.findComponent({ name: 'ImageCompare' })
expect(imageCompare.props('unstyled')).toBe(true)
expect(imageCompare.props('pt')).toEqual({
root: { class: 'custom-class' }
})
expect(imageCompare.props('ptOptions')).toEqual({ mergeSections: true })
})
})
describe('Readonly Mode', () => {
it('renders normally in readonly mode (no interaction restrictions)', () => {
const value: ImageCompareValue = {
before: 'https://example.com/before.jpg',
after: 'https://example.com/after.jpg'
}
const widget = createMockWidget(value)
const wrapper = mountComponent(widget, true)
// ImageCompare is display-only, readonly doesn't affect rendering
const imageCompare = wrapper.findComponent({ name: 'ImageCompare' })
expect(imageCompare.exists()).toBe(true)
const images = wrapper.findAll('img')
expect(images).toHaveLength(2)
})
})
describe('Edge Cases', () => {
it('handles null or undefined widget value', () => {
const widget = createMockWidget('')
const wrapper = mountComponent(widget)
const images = wrapper.findAll('img')
expect(images[0].attributes('src')).toBe('')
expect(images[1].attributes('src')).toBe('')
expect(images[0].attributes('alt')).toBe('Before image')
expect(images[1].attributes('alt')).toBe('After image')
})
it('handles empty object value', () => {
const value: ImageCompareValue = {} as ImageCompareValue
const widget = createMockWidget(value)
const wrapper = mountComponent(widget)
const images = wrapper.findAll('img')
expect(images[0].attributes('src')).toBe('')
expect(images[1].attributes('src')).toBe('')
})
it('handles malformed object value', () => {
const value = { randomProp: 'test', before: '', after: '' }
const widget = createMockWidget(value)
const wrapper = mountComponent(widget)
const images = wrapper.findAll('img')
expect(images[0].attributes('src')).toBe('')
expect(images[1].attributes('src')).toBe('')
})
it('handles special content - long URLs, special characters, and long alt text', () => {
// Test very long URLs
const longUrl = 'https://example.com/' + 'a'.repeat(1000) + '.jpg'
const longUrlValue: ImageCompareValue = {
before: longUrl,
after: longUrl
}
const longUrlWrapper = mountComponent(createMockWidget(longUrlValue))
const longUrlImages = longUrlWrapper.findAll('img')
expect(longUrlImages[0].attributes('src')).toBe(longUrl)
expect(longUrlImages[1].attributes('src')).toBe(longUrl)
// Test special characters in URLs
const specialUrl =
'https://example.com/path with spaces & symbols!@#$.jpg'
const specialUrlValue: ImageCompareValue = {
before: specialUrl,
after: specialUrl
}
const specialUrlWrapper = mountComponent(
createMockWidget(specialUrlValue)
)
const specialUrlImages = specialUrlWrapper.findAll('img')
expect(specialUrlImages[0].attributes('src')).toBe(specialUrl)
expect(specialUrlImages[1].attributes('src')).toBe(specialUrl)
// Test very long alt text
const longAlt =
'Very long alt text that exceeds normal length: ' +
'description '.repeat(50)
const longAltValue: ImageCompareValue = {
before: 'https://example.com/before.jpg',
after: 'https://example.com/after.jpg',
beforeAlt: longAlt,
afterAlt: longAlt
}
const longAltWrapper = mountComponent(createMockWidget(longAltValue))
const longAltImages = longAltWrapper.findAll('img')
expect(longAltImages[0].attributes('alt')).toBe(longAlt)
expect(longAltImages[1].attributes('alt')).toBe(longAlt)
})
})
describe('Template Structure', () => {
it('correctly assigns images to left and right template slots', () => {
const value: ImageCompareValue = {
before: 'https://example.com/before.jpg',
after: 'https://example.com/after.jpg'
}
const widget = createMockWidget(value)
const wrapper = mountComponent(widget)
const images = wrapper.findAll('img')
// First image (before) should be in left template slot
expect(images[0].attributes('src')).toBe('https://example.com/before.jpg')
// Second image (after) should be in right template slot
expect(images[1].attributes('src')).toBe('https://example.com/after.jpg')
})
})
describe('Integration', () => {
it('works with various URL types - data URLs and blob URLs', () => {
// Test data URLs
const dataUrl =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg=='
const dataUrlValue: ImageCompareValue = {
before: dataUrl,
after: dataUrl
}
const dataUrlWrapper = mountComponent(createMockWidget(dataUrlValue))
const dataUrlImages = dataUrlWrapper.findAll('img')
expect(dataUrlImages[0].attributes('src')).toBe(dataUrl)
expect(dataUrlImages[1].attributes('src')).toBe(dataUrl)
// Test blob URLs
const blobUrl =
'blob:http://example.com/12345678-1234-1234-1234-123456789012'
const blobUrlValue: ImageCompareValue = {
before: blobUrl,
after: blobUrl
}
const blobUrlWrapper = mountComponent(createMockWidget(blobUrlValue))
const blobUrlImages = blobUrlWrapper.findAll('img')
expect(blobUrlImages[0].attributes('src')).toBe(blobUrl)
expect(blobUrlImages[1].attributes('src')).toBe(blobUrl)
})
})
})

View File

@@ -30,7 +30,7 @@ import { computed } from 'vue'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
interface ImageCompareValue {
export interface ImageCompareValue {
before: string
after: string
beforeAlt?: string

View File

@@ -113,7 +113,7 @@ class MediaCacheService {
try {
// Fetch the media
const response = await fetch(src)
const response = await fetch(src, { cache: 'force-cache' })
if (!response.ok) {
throw new Error(`Failed to fetch: ${response.status}`)
}

View File

@@ -6,12 +6,10 @@ import {
GoogleAuthProvider,
type User,
type UserCredential,
browserLocalPersistence,
createUserWithEmailAndPassword,
deleteUser,
onAuthStateChanged,
sendPasswordResetEmail,
setPersistence,
signInWithEmailAndPassword,
signInWithPopup,
signOut,
@@ -62,10 +60,12 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
// Providers
const googleProvider = new GoogleAuthProvider()
googleProvider.addScope('email')
googleProvider.setCustomParameters({
prompt: 'select_account'
})
const githubProvider = new GithubAuthProvider()
githubProvider.addScope('user:email')
githubProvider.setCustomParameters({
prompt: 'select_account'
})
@@ -80,8 +80,6 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
// Retrieves the Firebase Auth instance. Returns `null` on the server.
// When using this function on the client in TypeScript, you can force the type with `useFirebaseAuth()!`.
const auth = useFirebaseAuth()!
// Set persistence to localStorage (works in both browser and Electron)
void setPersistence(auth, browserLocalPersistence)
onAuthStateChanged(auth, (user) => {
currentUser.value = user

View File

@@ -1,5 +1,6 @@
import { createTestingPinia } from '@pinia/testing'
import { VueWrapper, mount } from '@vue/test-utils'
import type { VueWrapper } from '@vue/test-utils'
import { mount } from '@vue/test-utils'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import { createI18n } from 'vue-i18n'

View File

@@ -14,7 +14,7 @@ import {
} from '@/stores/comfyManagerStore'
import { useCommandStore } from '@/stores/commandStore'
import { useDialogStore } from '@/stores/dialogStore'
import { TaskLog } from '@/types/comfyManagerTypes'
import type { TaskLog } from '@/types/comfyManagerTypes'
// Mock modules
vi.mock('@/stores/comfyManagerStore')

View File

@@ -2,12 +2,8 @@ import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
import {
LGraphEventMode,
LGraphNode,
Positionable,
Reroute
} from '@/lib/litegraph/src/litegraph'
import type { LGraphNode, Positionable } from '@/lib/litegraph/src/litegraph'
import { LGraphEventMode, Reroute } from '@/lib/litegraph/src/litegraph'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { app } from '@/scripts/app'

View File

@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
import { useManagerQueue } from '@/composables/useManagerQueue'
import { components } from '@/types/generatedManagerTypes'
import type { components } from '@/types/generatedManagerTypes'
// Mock dialog service
vi.mock('@/services/dialogService', () => ({

View File

@@ -56,10 +56,10 @@ describe('useManagerState', () => {
})
describe('managerUIState property', () => {
it('should return DISABLED state when --disable-manager is present', () => {
it('should return DISABLED state when --enable-manager is NOT present', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({
system: { argv: ['python', 'main.py', '--disable-manager'] }
system: { argv: ['python', 'main.py'] } // No --enable-manager flag
}),
isInitialized: ref(true)
} as any)
@@ -76,7 +76,14 @@ describe('useManagerState', () => {
it('should return LEGACY_UI state when --enable-manager-legacy-ui is present', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({
system: { argv: ['python', 'main.py', '--enable-manager-legacy-ui'] }
system: {
argv: [
'python',
'main.py',
'--enable-manager',
'--enable-manager-legacy-ui'
]
} // Both flags needed
}),
isInitialized: ref(true)
} as any)
@@ -92,7 +99,9 @@ describe('useManagerState', () => {
it('should return NEW_UI state when client and server both support v4', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
systemStats: ref({
system: { argv: ['python', 'main.py', '--enable-manager'] }
}), // Need --enable-manager
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
@@ -114,7 +123,9 @@ describe('useManagerState', () => {
it('should return LEGACY_UI state when server supports v4 but client does not', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
systemStats: ref({
system: { argv: ['python', 'main.py', '--enable-manager'] }
}), // Need --enable-manager
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
@@ -136,7 +147,9 @@ describe('useManagerState', () => {
it('should return LEGACY_UI state when legacy manager extension exists', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
systemStats: ref({
system: { argv: ['python', 'main.py', '--enable-manager'] }
}), // Need --enable-manager
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({})
@@ -155,7 +168,9 @@ describe('useManagerState', () => {
it('should return NEW_UI state when server feature flags are undefined', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
systemStats: ref({
system: { argv: ['python', 'main.py', '--enable-manager'] }
}), // Need --enable-manager
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({})
@@ -175,7 +190,9 @@ describe('useManagerState', () => {
it('should return LEGACY_UI state when server does not support v4', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
systemStats: ref({
system: { argv: ['python', 'main.py', '--enable-manager'] }
}), // Need --enable-manager
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({})
@@ -212,14 +229,17 @@ describe('useManagerState', () => {
const managerState = useManagerState()
expect(managerState.managerUIState.value).toBe(ManagerUIState.NEW_UI)
// When systemStats is null, we can't check for --enable-manager flag, so manager is disabled
expect(managerState.managerUIState.value).toBe(ManagerUIState.DISABLED)
})
})
describe('helper properties', () => {
it('isManagerEnabled should return true when state is not DISABLED', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
systemStats: ref({
system: { argv: ['python', 'main.py', '--enable-manager'] }
}), // Need --enable-manager
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
@@ -237,7 +257,7 @@ describe('useManagerState', () => {
it('isManagerEnabled should return false when state is DISABLED', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({
system: { argv: ['python', 'main.py', '--disable-manager'] }
system: { argv: ['python', 'main.py'] } // No --enable-manager flag means disabled
}),
isInitialized: ref(true)
} as any)
@@ -252,7 +272,9 @@ describe('useManagerState', () => {
it('isNewManagerUI should return true when state is NEW_UI', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
systemStats: ref({
system: { argv: ['python', 'main.py', '--enable-manager'] }
}), // Need --enable-manager
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
@@ -270,7 +292,14 @@ describe('useManagerState', () => {
it('isLegacyManagerUI should return true when state is LEGACY_UI', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({
system: { argv: ['python', 'main.py', '--enable-manager-legacy-ui'] }
system: {
argv: [
'python',
'main.py',
'--enable-manager',
'--enable-manager-legacy-ui'
]
} // Both flags needed
}),
isInitialized: ref(true)
} as any)
@@ -285,7 +314,9 @@ describe('useManagerState', () => {
it('shouldShowInstallButton should return true only for NEW_UI', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
systemStats: ref({
system: { argv: ['python', 'main.py', '--enable-manager'] }
}), // Need --enable-manager
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({
@@ -302,7 +333,9 @@ describe('useManagerState', () => {
it('shouldShowManagerButtons should return true when not DISABLED', () => {
vi.mocked(useSystemStatsStore).mockReturnValue({
systemStats: ref({ system: { argv: ['python', 'main.py'] } }),
systemStats: ref({
system: { argv: ['python', 'main.py', '--enable-manager'] }
}), // Need --enable-manager
isInitialized: ref(true)
} as any)
vi.mocked(api.getClientFeatureFlags).mockReturnValue({

View File

@@ -1,7 +1,7 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useNodeChatHistory } from '@/composables/node/useNodeChatHistory'
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
vi.mock(
'@/renderer/extensions/vueNodes/widgets/composables/useChatHistoryWidget',

View File

@@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import { useServerLogs } from '@/composables/useServerLogs'
import { LogsWsMessage } from '@/schemas/apiSchema'
import type { LogsWsMessage } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
vi.mock('@/scripts/api', () => ({

View File

@@ -5,7 +5,7 @@ import { LinkConnector } from '@/lib/litegraph/src/litegraph'
import { MovingOutputLink } from '@/lib/litegraph/src/litegraph'
import { ToOutputRenderLink } from '@/lib/litegraph/src/litegraph'
import { LGraphNode, LLink } from '@/lib/litegraph/src/litegraph'
import { NodeInputSlot } from '@/lib/litegraph/src/litegraph'
import type { NodeInputSlot } from '@/lib/litegraph/src/litegraph'
import { createTestSubgraph } from '../subgraph/fixtures/subgraphHelpers'

View File

@@ -1,7 +1,7 @@
import { describe, expect, it, vi } from 'vitest'
import { LGraphButton } from '@/lib/litegraph/src/litegraph'
import { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
import type { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
describe('LGraphNode Title Buttons', () => {

View File

@@ -1,13 +1,12 @@
// TODO: Fix these tests after migration
import { afterEach, describe, expect, vi } from 'vitest'
import type { LGraph, Reroute } from '@/lib/litegraph/src/litegraph'
import {
type CanvasPointerEvent,
LGraph,
LGraphNode,
LLink,
LinkConnector,
Reroute,
type RerouteId
} from '@/lib/litegraph/src/litegraph'

View File

@@ -1,6 +1,9 @@
import { describe, expect, it } from 'vitest'
import { INodeInputSlot, INodeOutputSlot } from '@/lib/litegraph/src/litegraph'
import type {
INodeInputSlot,
INodeOutputSlot
} from '@/lib/litegraph/src/litegraph'
import {
inputAsSerialisable,
outputAsSerialisable

View File

@@ -1,9 +1,8 @@
// TODO: Fix these tests after migration
import { assert, describe, expect, it } from 'vitest'
import type { ISlotType, LGraph } from '@/lib/litegraph/src/litegraph'
import {
ISlotType,
LGraph,
LGraphGroup,
LGraphNode,
LiteGraph

View File

@@ -2,7 +2,7 @@
import { describe, expect, it, vi } from 'vitest'
import { LGraphButton } from '@/lib/litegraph/src/litegraph'
import { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
import type { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
import {
createTestSubgraph,

View File

@@ -5,8 +5,8 @@ import { LinkConnector } from '@/lib/litegraph/src/litegraph'
import { ToInputFromIoNodeLink } from '@/lib/litegraph/src/litegraph'
import { SUBGRAPH_INPUT_ID } from '@/lib/litegraph/src/litegraph'
import { LGraphNode, type LinkNetwork } from '@/lib/litegraph/src/litegraph'
import { NodeInputSlot } from '@/lib/litegraph/src/litegraph'
import { NodeOutputSlot } from '@/lib/litegraph/src/litegraph'
import type { NodeInputSlot } from '@/lib/litegraph/src/litegraph'
import type { NodeOutputSlot } from '@/lib/litegraph/src/litegraph'
import {
isSubgraphInput,
isSubgraphOutput

View File

@@ -1,8 +1,8 @@
// TODO: Fix these tests after migration
import { describe, expect, it } from 'vitest'
import type { ISlotType } from '@/lib/litegraph/src/litegraph'
import { LGraphNode, Subgraph } from '@/lib/litegraph/src/litegraph'
import type { ISlotType, Subgraph } from '@/lib/litegraph/src/litegraph'
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { TWidgetType } from '@/lib/litegraph/src/litegraph'
import { BaseWidget } from '@/lib/litegraph/src/litegraph'

View File

@@ -5,8 +5,9 @@
* in their test files. Each fixture provides a clean, pre-configured subgraph
* setup for different testing scenarios.
*/
import { LGraph, Subgraph } from '@/lib/litegraph/src/litegraph'
import { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
import type { Subgraph } from '@/lib/litegraph/src/litegraph'
import { LGraph } from '@/lib/litegraph/src/litegraph'
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
import { test } from '../../core/fixtures/testExtensions'
import {

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import { ComfyNodeDef } from '@/schemas/nodeDefSchema'
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
import { NodeSearchService } from '@/services/nodeSearchService'
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'

View File

@@ -1,9 +1,9 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions'
import type { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
// Mock stores
vi.mock('@/renderer/core/canvas/canvasStore', () => {

View File

@@ -1,10 +1,11 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ref } from 'vue'
import { computed, ref } from 'vue'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager'
import type { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager'
import type { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
import { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers'
@@ -12,10 +13,18 @@ vi.mock('@/renderer/core/canvas/canvasStore', () => ({
useCanvasStore: vi.fn()
}))
vi.mock('@/renderer/core/canvas/useCanvasInteractions', () => ({
useCanvasInteractions: vi.fn()
}))
vi.mock('@/renderer/core/layout/operations/layoutMutations', () => ({
useLayoutMutations: vi.fn()
}))
vi.mock('@/composables/graph/useGraphNodeManager', () => ({
useGraphNodeManager: vi.fn()
}))
function createMockCanvas(): Pick<
LGraphCanvas,
'select' | 'deselect' | 'deselectAll'
@@ -68,12 +77,22 @@ function createMockLayoutMutations(): Pick<
}
}
function createMockCanvasInteractions(): Pick<
ReturnType<typeof useCanvasInteractions>,
'shouldHandleNodePointerEvents'
> {
return {
shouldHandleNodePointerEvents: computed(() => true) // Default to allowing pointer events
}
}
describe('useNodeEventHandlers', () => {
let mockCanvas: ReturnType<typeof createMockCanvas>
let mockNode: ReturnType<typeof createMockNode>
let mockNodeManager: ReturnType<typeof createMockNodeManager>
let mockCanvasStore: ReturnType<typeof createMockCanvasStore>
let mockLayoutMutations: ReturnType<typeof createMockLayoutMutations>
let mockCanvasInteractions: ReturnType<typeof createMockCanvasInteractions>
const testNodeData: VueNodeData = {
id: 'node-1',
@@ -90,6 +109,7 @@ describe('useNodeEventHandlers', () => {
mockNodeManager = createMockNodeManager(mockNode)
mockCanvasStore = createMockCanvasStore(mockCanvas)
mockLayoutMutations = createMockLayoutMutations()
mockCanvasInteractions = createMockCanvasInteractions()
vi.mocked(useCanvasStore).mockReturnValue(
mockCanvasStore as ReturnType<typeof useCanvasStore>
@@ -97,6 +117,9 @@ describe('useNodeEventHandlers', () => {
vi.mocked(useLayoutMutations).mockReturnValue(
mockLayoutMutations as ReturnType<typeof useLayoutMutations>
)
vi.mocked(useCanvasInteractions).mockReturnValue(
mockCanvasInteractions as ReturnType<typeof useCanvasInteractions>
)
})
describe('handleNodeSelect', () => {

View File

@@ -2,7 +2,7 @@ import axios from 'axios'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { useRemoteWidget } from '@/renderer/extensions/vueNodes/widgets/composables/useRemoteWidget'
import { RemoteWidgetConfig } from '@/schemas/nodeDefSchema'
import type { RemoteWidgetConfig } from '@/schemas/nodeDefSchema'
vi.mock('axios', () => {
return {

View File

@@ -1,10 +1,8 @@
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import {
ComfyWorkflow,
useWorkflowStore
} from '@/platform/workflow/management/stores/workflowStore'
import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
vi.mock('@/renderer/core/thumbnail/graphThumbnailRenderer', () => ({
createGraphThumbnail: vi.fn()

View File

@@ -4,7 +4,7 @@ import { nextTick, ref } from 'vue'
import { useComfyManagerService } from '@/services/comfyManagerService'
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
import { components as ManagerComponents } from '@/types/generatedManagerTypes'
import type { components as ManagerComponents } from '@/types/generatedManagerTypes'
type InstalledPacksResponse =
ManagerComponents['schemas']['InstalledPacksResponse']

View File

@@ -58,9 +58,11 @@ vi.mock('firebase/auth', async (importOriginal) => {
onAuthStateChanged: vi.fn(),
signInWithPopup: vi.fn(),
GoogleAuthProvider: class {
addScope = vi.fn()
setCustomParameters = vi.fn()
},
GithubAuthProvider: class {
addScope = vi.fn()
setCustomParameters = vi.fn()
},
setPersistence: vi.fn().mockResolvedValue(undefined)
@@ -148,13 +150,6 @@ describe('useFirebaseAuthStore', () => {
expect(store.loading).toBe(false)
})
it('should set persistence to local storage on initialization', () => {
expect(firebaseAuth.setPersistence).toHaveBeenCalledWith(
mockAuth,
firebaseAuth.browserLocalPersistence
)
})
it('should properly clean up error state between operations', async () => {
// First, cause an error
const mockError = new Error('Invalid password')

View File

@@ -1,8 +1,8 @@
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { ExecutedWsMessage } from '@/schemas/apiSchema'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { ExecutedWsMessage } from '@/schemas/apiSchema'
import { app } from '@/scripts/app'
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
import * as litegraphUtil from '@/utils/litegraphUtil'

View File

@@ -1,7 +1,7 @@
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it } from 'vitest'
import { ServerConfig } from '@/constants/serverConfig'
import type { ServerConfig } from '@/constants/serverConfig'
import type { FormItem } from '@/platform/settings/types'
import { useServerConfigStore } from '@/stores/serverConfigStore'

View File

@@ -3,9 +3,11 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import type { Subgraph } from '@/lib/litegraph/src/litegraph'
import {
import type {
ComfyWorkflow,
LoadedComfyWorkflow,
LoadedComfyWorkflow
} from '@/platform/workflow/management/stores/workflowStore'
import {
useWorkflowBookmarkStore,
useWorkflowStore
} from '@/platform/workflow/management/stores/workflowStore'

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import { ISerialisedGraph } from '@/lib/litegraph/src/types/serialisation'
import type { ISerialisedGraph } from '@/lib/litegraph/src/types/serialisation'
import type { IWidget } from '@/lib/litegraph/src/types/widgets'
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import {

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import { TreeNode } from '@/types/treeExplorerTypes'
import type { TreeNode } from '@/types/treeExplorerTypes'
import { buildTree, sortedTree } from '@/utils/treeUtil'
describe('buildTree', () => {

14
tests-ui/tsconfig.json Normal file
View File

@@ -0,0 +1,14 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
/* Test files should not be compiled */
"noEmit": true,
// "strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"resolveJsonModule": true
},
"include": [
"**/*.ts",
]
}

View File

@@ -34,7 +34,9 @@
"tests-ui/**/*",
"global.d.ts",
"eslint.config.ts",
"scripts/**/*.ts",
"vite.config.mts",
"tailwind.config.ts",
".storybook/**/*"
]
}