mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-07-02 13:17:48 +00:00
Compare commits
11 Commits
let_the_wi
...
chore/incl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6afc2b62a7 | ||
|
|
eb664f47af | ||
|
|
bc85d4e87b | ||
|
|
7585444ce6 | ||
|
|
a886798a10 | ||
|
|
37975e4eac | ||
|
|
a41b8a6d4f | ||
|
|
b264685052 | ||
|
|
78d0ea6fa5 | ||
|
|
ea4e57b602 | ||
|
|
4789d86fe8 |
@@ -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(
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Locator, Page } from '@playwright/test'
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
class SidebarTab {
|
||||
constructor(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FullConfig } from '@playwright/test'
|
||||
import type { FullConfig } from '@playwright/test'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
import { backupPath } from './utils/backupUtils'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FullConfig } from '@playwright/test'
|
||||
import type { FullConfig } from '@playwright/test'
|
||||
import dotenv from 'dotenv'
|
||||
|
||||
import { restorePath } from './utils/backupUtils'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Locator, Page } from '@playwright/test'
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
export class ManageGroupNode {
|
||||
footer: Locator
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ComfyPage } from '../fixtures/ComfyPage'
|
||||
import {
|
||||
ComfyPage,
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
} from '../fixtures/ComfyPage'
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
}
|
||||
@@ -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 }
|
||||
]
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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()
|
||||
}))
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
21
src/main.ts
21
src/main.ts
@@ -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')
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
|
||||
@@ -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}`)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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', () => ({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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', () => ({
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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', () => {
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
14
tests-ui/tsconfig.json
Normal 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",
|
||||
]
|
||||
}
|
||||
@@ -34,7 +34,9 @@
|
||||
"tests-ui/**/*",
|
||||
"global.d.ts",
|
||||
"eslint.config.ts",
|
||||
"scripts/**/*.ts",
|
||||
"vite.config.mts",
|
||||
"tailwind.config.ts",
|
||||
".storybook/**/*"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user