mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-23 07:50:15 +00:00
Compare commits
8 Commits
v1.39.1
...
revert-812
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7e9ea9070 | ||
|
|
fc38f16543 | ||
|
|
faad2c03de | ||
|
|
d4cec49db5 | ||
|
|
af8433fb3d | ||
|
|
cabd08f0ec | ||
|
|
868180eb28 | ||
|
|
c51916d103 |
4
.github/workflows/ci-tests-e2e.yaml
vendored
4
.github/workflows/ci-tests-e2e.yaml
vendored
@@ -5,8 +5,8 @@ on:
|
||||
push:
|
||||
branches: [main, master, core/*, desktop/*]
|
||||
pull_request:
|
||||
branches-ignore:
|
||||
[wip/*, draft/*, temp/*, vue-nodes-migration, sno-playwright-*]
|
||||
branches-ignore: [wip/*, draft/*, temp/*]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
@@ -180,7 +180,7 @@ jobs:
|
||||
with:
|
||||
pattern: snapshots-shard-*
|
||||
path: ./downloaded-snapshots
|
||||
merge-multiple: false
|
||||
merge-multiple: true
|
||||
|
||||
- name: List downloaded files
|
||||
run: |
|
||||
@@ -206,13 +206,13 @@ jobs:
|
||||
echo "MERGING CHANGED SNAPSHOTS"
|
||||
echo "=========================================="
|
||||
|
||||
# Check if any artifacts were downloaded
|
||||
# Check if any artifacts were downloaded (merge-multiple puts files directly in path)
|
||||
if [ ! -d "./downloaded-snapshots" ]; then
|
||||
echo "No snapshot artifacts to merge"
|
||||
echo "=========================================="
|
||||
echo "MERGE COMPLETE"
|
||||
echo "=========================================="
|
||||
echo "Shards merged: 0"
|
||||
echo "Files merged: 0"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -222,37 +222,29 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
merged_count=0
|
||||
# Count files to merge
|
||||
file_count=$(find ./downloaded-snapshots -type f | wc -l)
|
||||
|
||||
# For each shard's changed files, copy them directly
|
||||
for shard_dir in ./downloaded-snapshots/snapshots-shard-*/; do
|
||||
if [ ! -d "$shard_dir" ]; then
|
||||
continue
|
||||
fi
|
||||
if [ "$file_count" -eq 0 ]; then
|
||||
echo "No snapshot files found in downloaded artifacts"
|
||||
echo "=========================================="
|
||||
echo "MERGE COMPLETE"
|
||||
echo "=========================================="
|
||||
echo "Files merged: 0"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
shard_name=$(basename "$shard_dir")
|
||||
file_count=$(find "$shard_dir" -type f | wc -l)
|
||||
echo "Merging $file_count snapshot file(s)..."
|
||||
|
||||
if [ "$file_count" -eq 0 ]; then
|
||||
echo " $shard_name: no files"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "Processing $shard_name ($file_count file(s))..."
|
||||
|
||||
# Copy files directly, preserving directory structure
|
||||
# Since files are already in correct structure (no browser_tests/ prefix), just copy them all
|
||||
cp -v -r "$shard_dir"* browser_tests/ 2>&1 | sed 's/^/ /'
|
||||
|
||||
merged_count=$((merged_count + 1))
|
||||
echo " ✓ Merged"
|
||||
echo ""
|
||||
done
|
||||
# Copy all files directly, preserving directory structure
|
||||
# With merge-multiple: true, files are directly in ./downloaded-snapshots/ without shard subdirs
|
||||
cp -v -r ./downloaded-snapshots/* browser_tests/ 2>&1 | sed 's/^/ /'
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "MERGE COMPLETE"
|
||||
echo "=========================================="
|
||||
echo "Shards merged: $merged_count"
|
||||
echo "Files merged: $file_count"
|
||||
|
||||
- name: Show changes
|
||||
run: |
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 80 KiB |
@@ -41,7 +41,7 @@
|
||||
:is-small="isSmall"
|
||||
/>
|
||||
<SidebarHelpCenterIcon v-if="!isIntegratedTabBar" :is-small="isSmall" />
|
||||
<SidebarBottomPanelToggleButton :is-small="isSmall" />
|
||||
<SidebarBottomPanelToggleButton v-if="!isCloud" :is-small="isSmall" />
|
||||
<SidebarShortcutsToggleButton :is-small="isSmall" />
|
||||
<SidebarSettingsButton :is-small="isSmall" />
|
||||
<ModeToggle
|
||||
@@ -65,6 +65,7 @@ import SidebarBottomPanelToggleButton from '@/components/sidebar/SidebarBottomPa
|
||||
import SidebarSettingsButton from '@/components/sidebar/SidebarSettingsButton.vue'
|
||||
import SidebarShortcutsToggleButton from '@/components/sidebar/SidebarShortcutsToggleButton.vue'
|
||||
import { useFeatureFlags } from '@/composables/useFeatureFlags'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useTelemetry } from '@/platform/telemetry'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
import type { LGraphGroup } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphGroup, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { getExtraOptionsForWidget } from '@/services/litegraphService'
|
||||
import { isLGraphGroup } from '@/utils/litegraphUtil'
|
||||
|
||||
import {
|
||||
@@ -45,6 +46,8 @@ export enum BadgeVariant {
|
||||
// Global singleton for NodeOptions component reference
|
||||
let nodeOptionsInstance: null | NodeOptionsInstance = null
|
||||
|
||||
const hoveredWidgetName = ref<string>()
|
||||
|
||||
/**
|
||||
* Toggle the node options popover
|
||||
* @param event - The trigger event
|
||||
@@ -61,6 +64,13 @@ export function toggleNodeOptions(event: Event) {
|
||||
* @param event - The trigger event (must be MouseEvent for position)
|
||||
*/
|
||||
export function showNodeOptions(event: MouseEvent) {
|
||||
hoveredWidgetName.value = undefined
|
||||
const target = event.target
|
||||
if (target instanceof HTMLElement) {
|
||||
const widgetEl = target.closest('.lg-node-widget')
|
||||
if (widgetEl instanceof HTMLElement)
|
||||
hoveredWidgetName.value = widgetEl.dataset.widgetName
|
||||
}
|
||||
if (nodeOptionsInstance?.show) {
|
||||
nodeOptionsInstance.show(event)
|
||||
}
|
||||
@@ -133,8 +143,8 @@ export function useMoreOptionsMenu() {
|
||||
} = useGroupMenuOptions()
|
||||
const {
|
||||
getBasicSelectionOptions,
|
||||
getSubgraphOptions,
|
||||
getMultipleNodesOptions
|
||||
getMultipleNodesOptions,
|
||||
getSubgraphOptions
|
||||
} = useSelectionMenuOptions()
|
||||
|
||||
const hasSubgraphs = hasSubgraphsComputed
|
||||
@@ -164,13 +174,13 @@ export function useMoreOptionsMenu() {
|
||||
|
||||
// For single node selection, also get LiteGraph menu items to merge
|
||||
const litegraphOptions: MenuOption[] = []
|
||||
const node: LGraphNode | undefined = selectedNodes.value[0]
|
||||
if (
|
||||
selectedNodes.value.length === 1 &&
|
||||
!groupContext &&
|
||||
canvasStore.canvas
|
||||
) {
|
||||
try {
|
||||
const node = selectedNodes.value[0]
|
||||
const rawItems = canvasStore.canvas.getNodeMenuOptions(node)
|
||||
// Don't apply structuring yet - we'll do it after merging with Vue options
|
||||
litegraphOptions.push(
|
||||
@@ -249,6 +259,18 @@ export function useMoreOptionsMenu() {
|
||||
options.push(...getImageMenuOptions(selectedNodes.value[0]))
|
||||
options.push({ type: 'divider' })
|
||||
}
|
||||
const rawName = hoveredWidgetName.value
|
||||
const widget = node?.widgets?.find((w) => w.name === rawName)
|
||||
if (widget) {
|
||||
const widgetOptions = convertContextMenuToOptions(
|
||||
getExtraOptionsForWidget(node, widget)
|
||||
)
|
||||
if (widgetOptions) {
|
||||
options.push(...widgetOptions)
|
||||
options.push({ type: 'divider' })
|
||||
}
|
||||
}
|
||||
|
||||
// Section 6 & 7: Extensions and Delete are handled by buildStructuredMenu
|
||||
|
||||
// Mark all Vue options with source
|
||||
|
||||
@@ -105,6 +105,7 @@ export function addWidgetPromotionOptions(
|
||||
content: `Promote Widget: ${widget.label ?? widget.name}`,
|
||||
callback: () => {
|
||||
promoteWidget(node, widget, promotableParents)
|
||||
widget.callback?.(widget.value)
|
||||
}
|
||||
})
|
||||
else {
|
||||
@@ -112,6 +113,7 @@ export function addWidgetPromotionOptions(
|
||||
content: `Un-Promote Widget: ${widget.label ?? widget.name}`,
|
||||
callback: () => {
|
||||
demoteWidget(node, widget, parents)
|
||||
widget.callback?.(widget.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -187,7 +187,7 @@ export class ClipspaceDialog extends ComfyDialog {
|
||||
|
||||
app.registerExtension({
|
||||
name: 'Comfy.Clipspace',
|
||||
init(app) {
|
||||
init() {
|
||||
app.openClipspace = function () {
|
||||
if (!ClipspaceDialog.instance) {
|
||||
ClipspaceDialog.instance = new ClipspaceDialog()
|
||||
|
||||
@@ -13,7 +13,7 @@ import { getWidgetConfig, mergeIfValid, setWidgetConfig } from './widgetInputs'
|
||||
|
||||
app.registerExtension({
|
||||
name: 'Comfy.RerouteNode',
|
||||
registerCustomNodes(app) {
|
||||
registerCustomNodes() {
|
||||
interface RerouteNode extends LGraphNode {
|
||||
__outputType?: string | number
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ const saveNodeTypes = new Set([
|
||||
|
||||
app.registerExtension({
|
||||
name: 'Comfy.SaveImageExtraOutput',
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
if (saveNodeTypes.has(nodeData.name)) {
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated
|
||||
// When the SaveImage node is created we want to override the serialization of the output name widget to run our S&R
|
||||
|
||||
@@ -511,7 +511,7 @@ export function mergeIfValid(
|
||||
|
||||
app.registerExtension({
|
||||
name: 'Comfy.WidgetInputs',
|
||||
async beforeRegisterNodeDef(nodeType, _nodeData, app) {
|
||||
async beforeRegisterNodeDef(nodeType, _nodeData) {
|
||||
// @ts-expect-error adding extra property
|
||||
nodeType.prototype.convertWidgetToInput = function (this: LGraphNode) {
|
||||
console.warn(
|
||||
|
||||
@@ -603,7 +603,7 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
tooltip:
|
||||
'The maximum number of tasks added to the queue at one button click',
|
||||
type: 'number',
|
||||
defaultValue: isCloud ? 4 : 100,
|
||||
defaultValue: isCloud ? 32 : 100,
|
||||
versionAdded: '1.3.5'
|
||||
},
|
||||
{
|
||||
@@ -1173,12 +1173,10 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Queue.QPOV2',
|
||||
category: ['Comfy', 'Queue', 'Layout'],
|
||||
name: 'Use the unified job queue in the Assets side panel',
|
||||
type: 'boolean',
|
||||
tooltip:
|
||||
'Replaces the floating job queue panel with an equivalent job queue embedded in the Assets side panel. You can disable this to return to the floating panel layout.',
|
||||
defaultValue: true,
|
||||
name: 'Queue Panel V2',
|
||||
type: 'hidden',
|
||||
tooltip: 'Enable the new Assets Panel design with list/grid view toggle',
|
||||
defaultValue: false,
|
||||
experimental: true
|
||||
},
|
||||
{
|
||||
|
||||
@@ -76,7 +76,7 @@ vi.mock(
|
||||
executing: computed(() => mockData.mockExecuting),
|
||||
progress: computed(() => undefined),
|
||||
progressPercentage: computed(() => undefined),
|
||||
progressState: computed(() => undefined as any),
|
||||
progressState: computed(() => undefined),
|
||||
executionState: computed(() => 'idle' as const)
|
||||
}))
|
||||
})
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
(!widget.simplified.options?.advanced || showAdvanced)
|
||||
"
|
||||
class="lg-node-widget group col-span-full grid grid-cols-subgrid items-stretch"
|
||||
:data-widget-name="widget.name"
|
||||
>
|
||||
<!-- Widget Input Slot Dot -->
|
||||
<div
|
||||
|
||||
@@ -290,7 +290,7 @@ describe('WidgetGalleria Image Display', () => {
|
||||
await galleria.vm.$emit('update:activeIndex', 2)
|
||||
|
||||
// Check that the internal activeIndex ref was updated
|
||||
const vm = wrapper.vm as any
|
||||
const vm = wrapper.vm as typeof wrapper.vm & { activeIndex: number }
|
||||
expect(vm.activeIndex).toBe(2)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -197,13 +197,12 @@ describe('WidgetInputNumberInput Large Integer Precision Handling', () => {
|
||||
describe('WidgetInputNumberInput Edge Cases for Precision Handling', () => {
|
||||
it('handles null/undefined model values gracefully', () => {
|
||||
const widget = createMockWidget(0, 'int')
|
||||
// Mount with undefined as modelValue
|
||||
const wrapper = mount(WidgetInputNumberInput, {
|
||||
global: { plugins: [i18n] },
|
||||
props: {
|
||||
widget,
|
||||
modelValue: undefined as any
|
||||
}
|
||||
modelValue: undefined
|
||||
} as { widget: SimplifiedWidget<number>; modelValue: number | undefined }
|
||||
})
|
||||
|
||||
expect(wrapper.findAll('button').length).toBe(2)
|
||||
|
||||
@@ -138,6 +138,7 @@ function updateValueBy(delta: number) {
|
||||
const dragValue = ref<number>()
|
||||
const dragDelta = ref(0)
|
||||
function handleMouseDown(e: PointerEvent) {
|
||||
if (e.button > 0) return
|
||||
if (props.widget.options?.disabled) return
|
||||
const { target } = e
|
||||
if (!(target instanceof HTMLElement)) return
|
||||
|
||||
@@ -152,6 +152,50 @@ describe('WidgetSelectDropdown custom label mapping', () => {
|
||||
expect(consoleErrorSpy).toHaveBeenCalled()
|
||||
consoleErrorSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('falls back to original value when label mapping returns empty string', () => {
|
||||
const getOptionLabel = vi.fn((value: string | null) => {
|
||||
if (value === 'photo_abc.jpg') {
|
||||
return ''
|
||||
}
|
||||
return `Labeled: ${value}`
|
||||
})
|
||||
|
||||
const widget = createMockWidget('img_001.png', {
|
||||
getOptionLabel
|
||||
})
|
||||
const wrapper = mountComponent(widget, 'img_001.png')
|
||||
|
||||
const inputItems = wrapper.vm.inputItems
|
||||
expect(inputItems[0].name).toBe('img_001.png')
|
||||
expect(inputItems[0].label).toBe('Labeled: img_001.png')
|
||||
expect(inputItems[1].name).toBe('photo_abc.jpg')
|
||||
expect(inputItems[1].label).toBe('photo_abc.jpg')
|
||||
expect(inputItems[2].name).toBe('hash789.png')
|
||||
expect(inputItems[2].label).toBe('Labeled: hash789.png')
|
||||
})
|
||||
|
||||
it('falls back to original value when label mapping returns undefined', () => {
|
||||
const getOptionLabel = vi.fn((value: string | null) => {
|
||||
if (value === 'hash789.png') {
|
||||
return undefined as unknown as string
|
||||
}
|
||||
return `Labeled: ${value}`
|
||||
})
|
||||
|
||||
const widget = createMockWidget('img_001.png', {
|
||||
getOptionLabel
|
||||
})
|
||||
const wrapper = mountComponent(widget, 'img_001.png')
|
||||
|
||||
const inputItems = wrapper.vm.inputItems
|
||||
expect(inputItems[0].name).toBe('img_001.png')
|
||||
expect(inputItems[0].label).toBe('Labeled: img_001.png')
|
||||
expect(inputItems[1].name).toBe('photo_abc.jpg')
|
||||
expect(inputItems[1].label).toBe('Labeled: photo_abc.jpg')
|
||||
expect(inputItems[2].name).toBe('hash789.png')
|
||||
expect(inputItems[2].label).toBe('hash789.png')
|
||||
})
|
||||
})
|
||||
|
||||
describe('output items with custom label mapping', () => {
|
||||
@@ -171,4 +215,102 @@ describe('WidgetSelectDropdown custom label mapping', () => {
|
||||
expect(Array.isArray(outputItems)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('missing value handling for template-loaded nodes', () => {
|
||||
it('creates a fallback item in "all" filter when modelValue is not in available items', () => {
|
||||
const widget = createMockWidget('template_image.png', {
|
||||
values: ['img_001.png', 'photo_abc.jpg']
|
||||
})
|
||||
const wrapper = mountComponent(widget, 'template_image.png')
|
||||
|
||||
const inputItems = wrapper.vm.inputItems
|
||||
expect(inputItems).toHaveLength(2)
|
||||
expect(
|
||||
inputItems.some((item) => item.name === 'template_image.png')
|
||||
).toBe(false)
|
||||
|
||||
// The missing value should be accessible via dropdownItems when filter is 'all' (default)
|
||||
const dropdownItems = (
|
||||
wrapper.vm as unknown as { dropdownItems: DropdownItem[] }
|
||||
).dropdownItems
|
||||
expect(
|
||||
dropdownItems.some((item) => item.name === 'template_image.png')
|
||||
).toBe(true)
|
||||
expect(dropdownItems[0].name).toBe('template_image.png')
|
||||
expect(dropdownItems[0].id).toBe('missing-template_image.png')
|
||||
})
|
||||
|
||||
it('does not include fallback item when filter is "inputs"', async () => {
|
||||
const widget = createMockWidget('template_image.png', {
|
||||
values: ['img_001.png', 'photo_abc.jpg']
|
||||
})
|
||||
const wrapper = mountComponent(widget, 'template_image.png')
|
||||
|
||||
const vmWithFilter = wrapper.vm as unknown as {
|
||||
filterSelected: string
|
||||
dropdownItems: DropdownItem[]
|
||||
}
|
||||
|
||||
vmWithFilter.filterSelected = 'inputs'
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const dropdownItems = vmWithFilter.dropdownItems
|
||||
expect(dropdownItems).toHaveLength(2)
|
||||
expect(
|
||||
dropdownItems.every((item) => !String(item.id).startsWith('missing-'))
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('does not include fallback item when filter is "outputs"', async () => {
|
||||
const widget = createMockWidget('template_image.png', {
|
||||
values: ['img_001.png', 'photo_abc.jpg']
|
||||
})
|
||||
const wrapper = mountComponent(widget, 'template_image.png')
|
||||
|
||||
const vmWithFilter = wrapper.vm as unknown as {
|
||||
filterSelected: string
|
||||
dropdownItems: DropdownItem[]
|
||||
outputItems: DropdownItem[]
|
||||
}
|
||||
|
||||
vmWithFilter.filterSelected = 'outputs'
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const dropdownItems = vmWithFilter.dropdownItems
|
||||
expect(dropdownItems).toHaveLength(wrapper.vm.outputItems.length)
|
||||
expect(
|
||||
dropdownItems.every((item) => !String(item.id).startsWith('missing-'))
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('does not create a fallback item when modelValue exists in available items', () => {
|
||||
const widget = createMockWidget('img_001.png', {
|
||||
values: ['img_001.png', 'photo_abc.jpg']
|
||||
})
|
||||
const wrapper = mountComponent(widget, 'img_001.png')
|
||||
|
||||
const dropdownItems = (
|
||||
wrapper.vm as unknown as { dropdownItems: DropdownItem[] }
|
||||
).dropdownItems
|
||||
expect(dropdownItems).toHaveLength(2)
|
||||
expect(
|
||||
dropdownItems.every((item) => !String(item.id).startsWith('missing-'))
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('does not create a fallback item when modelValue is undefined', () => {
|
||||
const widget = createMockWidget(undefined as unknown as string, {
|
||||
values: ['img_001.png', 'photo_abc.jpg']
|
||||
})
|
||||
const wrapper = mountComponent(widget, undefined)
|
||||
|
||||
const dropdownItems = (
|
||||
wrapper.vm as unknown as { dropdownItems: DropdownItem[] }
|
||||
).dropdownItems
|
||||
expect(dropdownItems).toHaveLength(2)
|
||||
expect(
|
||||
dropdownItems.every((item) => !String(item.id).startsWith('missing-'))
|
||||
).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -85,14 +85,15 @@ const selectedSet = ref<Set<SelectedKey>>(new Set())
|
||||
|
||||
/**
|
||||
* Transforms a value using getOptionLabel if available.
|
||||
* Falls back to the original value if getOptionLabel is not provided or throws an error.
|
||||
* Falls back to the original value if getOptionLabel is not provided,
|
||||
* returns undefined/null, or throws an error.
|
||||
*/
|
||||
function getDisplayLabel(value: string): string {
|
||||
const getOptionLabel = props.widget.options?.getOptionLabel
|
||||
if (!getOptionLabel) return value
|
||||
|
||||
try {
|
||||
return getOptionLabel(value)
|
||||
return getOptionLabel(value) || value
|
||||
} catch (e) {
|
||||
console.error('Failed to map value:', e)
|
||||
return value
|
||||
@@ -146,11 +147,69 @@ const outputItems = computed<DropdownItem[]>(() => {
|
||||
}))
|
||||
})
|
||||
|
||||
/**
|
||||
* Creates a fallback item for the current modelValue when it doesn't exist
|
||||
* in the available items list. This handles cases like template-loaded nodes
|
||||
* where the saved value may not exist in the current server environment.
|
||||
* Works for both local mode (inputItems/outputItems) and cloud mode (assetData).
|
||||
*/
|
||||
const missingValueItem = computed<DropdownItem | undefined>(() => {
|
||||
const currentValue = modelValue.value
|
||||
if (!currentValue) return undefined
|
||||
|
||||
// Check in cloud mode assets
|
||||
if (props.isAssetMode && assetData) {
|
||||
const existsInAssets = assetData.dropdownItems.value.some(
|
||||
(item) => item.name === currentValue
|
||||
)
|
||||
if (existsInAssets) return undefined
|
||||
|
||||
return {
|
||||
id: `missing-${currentValue}`,
|
||||
mediaSrc: '',
|
||||
name: currentValue,
|
||||
label: getDisplayLabel(currentValue),
|
||||
metadata: ''
|
||||
}
|
||||
}
|
||||
|
||||
// Check in local mode inputs/outputs
|
||||
const existsInInputs = inputItems.value.some(
|
||||
(item) => item.name === currentValue
|
||||
)
|
||||
const existsInOutputs = outputItems.value.some(
|
||||
(item) => item.name === currentValue
|
||||
)
|
||||
|
||||
if (existsInInputs || existsInOutputs) return undefined
|
||||
|
||||
const isOutput = currentValue.endsWith(' [output]')
|
||||
const strippedValue = isOutput
|
||||
? currentValue.replace(' [output]', '')
|
||||
: currentValue
|
||||
|
||||
return {
|
||||
id: `missing-${currentValue}`,
|
||||
mediaSrc: getMediaUrl(strippedValue, isOutput ? 'output' : 'input'),
|
||||
name: currentValue,
|
||||
label: getDisplayLabel(currentValue),
|
||||
metadata: ''
|
||||
}
|
||||
})
|
||||
|
||||
const allItems = computed<DropdownItem[]>(() => {
|
||||
if (props.isAssetMode && assetData) {
|
||||
return assetData.dropdownItems.value
|
||||
const items = assetData.dropdownItems.value
|
||||
if (missingValueItem.value) {
|
||||
return [missingValueItem.value, ...items]
|
||||
}
|
||||
return items
|
||||
}
|
||||
return [...inputItems.value, ...outputItems.value]
|
||||
return [
|
||||
...(missingValueItem.value ? [missingValueItem.value] : []),
|
||||
...inputItems.value,
|
||||
...outputItems.value
|
||||
]
|
||||
})
|
||||
|
||||
const dropdownItems = computed<DropdownItem[]>(() => {
|
||||
@@ -165,7 +224,7 @@ const dropdownItems = computed<DropdownItem[]>(() => {
|
||||
return outputItems.value
|
||||
case 'all':
|
||||
default:
|
||||
return [...inputItems.value, ...outputItems.value]
|
||||
return allItems.value
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ describe('FormSelectButton Core Component', () => {
|
||||
// Type-safe helper for mounting component
|
||||
const mountComponent = (
|
||||
modelValue: string | null | undefined = null,
|
||||
options: (string | number | Record<string, any>)[] = [],
|
||||
options: unknown[] = [],
|
||||
props: Record<string, unknown> = {}
|
||||
) => {
|
||||
return mount(FormSelectButton, {
|
||||
@@ -17,7 +17,11 @@ describe('FormSelectButton Core Component', () => {
|
||||
},
|
||||
props: {
|
||||
modelValue,
|
||||
options: options as any,
|
||||
options: options as (
|
||||
| string
|
||||
| number
|
||||
| { label: string; value: string | number }
|
||||
)[],
|
||||
...props
|
||||
}
|
||||
})
|
||||
@@ -474,7 +478,7 @@ describe('FormSelectButton Core Component', () => {
|
||||
})
|
||||
|
||||
it('handles mixed type options safely', () => {
|
||||
const mixedOptions: any[] = [
|
||||
const mixedOptions: unknown[] = [
|
||||
'string',
|
||||
123,
|
||||
{ label: 'Object', value: 'obj' }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { INumericWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { _for_testing } from '@/renderer/extensions/vueNodes/widgets/composables/useFloatWidget'
|
||||
|
||||
vi.mock('@/scripts/widgets', () => ({
|
||||
@@ -16,14 +17,17 @@ const { onFloatValueChange } = _for_testing
|
||||
|
||||
describe('useFloatWidget', () => {
|
||||
describe('onFloatValueChange', () => {
|
||||
let widget: any
|
||||
let widget: INumericWidget
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset the widget before each test
|
||||
widget = {
|
||||
type: 'number',
|
||||
name: 'test_widget',
|
||||
y: 0,
|
||||
options: {},
|
||||
value: 0
|
||||
}
|
||||
} as Partial<INumericWidget> as INumericWidget
|
||||
})
|
||||
|
||||
it('should not round values when round option is not set', () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { INumericWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { _for_testing } from '@/renderer/extensions/vueNodes/widgets/composables/useIntWidget'
|
||||
|
||||
vi.mock('@/scripts/widgets', () => ({
|
||||
@@ -16,14 +17,17 @@ const { onValueChange } = _for_testing
|
||||
|
||||
describe('useIntWidget', () => {
|
||||
describe('onValueChange', () => {
|
||||
let widget: any
|
||||
let widget: INumericWidget
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset the widget before each test
|
||||
widget = {
|
||||
type: 'number',
|
||||
name: 'test_widget',
|
||||
y: 0,
|
||||
options: {},
|
||||
value: 0
|
||||
}
|
||||
} as Partial<INumericWidget> as INumericWidget
|
||||
})
|
||||
|
||||
it('should round values based on step size', () => {
|
||||
|
||||
@@ -3,19 +3,20 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import type { IWidget } from '@/lib/litegraph/src/litegraph'
|
||||
import { api } from '@/scripts/api'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useRemoteWidget } from '@/renderer/extensions/vueNodes/widgets/composables/useRemoteWidget'
|
||||
import type { RemoteWidgetConfig } from '@/schemas/nodeDefSchema'
|
||||
import { createMockLGraphNode } from '@/utils/__tests__/litegraphTestUtils'
|
||||
|
||||
const createMockNode = (overrides: Partial<LGraphNode> = {}): LGraphNode => {
|
||||
const node = new LGraphNode('TestNode')
|
||||
Object.assign(node, overrides)
|
||||
return node
|
||||
function createMockWidget(overrides: Partial<IWidget> = {}): IWidget {
|
||||
return {
|
||||
name: 'test_widget',
|
||||
type: 'text',
|
||||
value: '',
|
||||
options: {},
|
||||
...overrides
|
||||
} as Partial<IWidget> as IWidget
|
||||
}
|
||||
|
||||
const createMockWidget = (overrides = {}): IWidget =>
|
||||
({ ...overrides }) as unknown as IWidget
|
||||
|
||||
const mockCloudAuth = vi.hoisted(() => ({
|
||||
isCloud: false,
|
||||
authHeader: null as { Authorization: string } | null
|
||||
@@ -67,7 +68,10 @@ function createMockConfig(overrides = {}): RemoteWidgetConfig {
|
||||
const createMockOptions = (inputOverrides = {}) => ({
|
||||
remoteConfig: createMockConfig(inputOverrides),
|
||||
defaultValue: DEFAULT_VALUE,
|
||||
node: createMockNode(),
|
||||
node: createMockLGraphNode({
|
||||
addWidget: vi.fn(() => createMockWidget()),
|
||||
onRemoved: undefined
|
||||
}),
|
||||
widget: createMockWidget()
|
||||
})
|
||||
|
||||
@@ -499,12 +503,14 @@ describe('useRemoteWidget', () => {
|
||||
})
|
||||
|
||||
it('should handle rapid cache clearing during fetch', async () => {
|
||||
let resolvePromise: (value: any) => void
|
||||
const delayedPromise = new Promise((resolve) => {
|
||||
resolvePromise = resolve
|
||||
})
|
||||
let resolvePromise: (value: { data: unknown; status?: number }) => void
|
||||
const delayedPromise = new Promise<{ data: unknown; status?: number }>(
|
||||
(resolve) => {
|
||||
resolvePromise = resolve
|
||||
}
|
||||
)
|
||||
|
||||
vi.mocked(axios.get).mockImplementationOnce(() => delayedPromise as any)
|
||||
vi.mocked(axios.get).mockImplementationOnce(() => delayedPromise)
|
||||
|
||||
const hook = useRemoteWidget(createMockOptions())
|
||||
hook.getValue()
|
||||
@@ -520,17 +526,20 @@ describe('useRemoteWidget', () => {
|
||||
})
|
||||
|
||||
it('should handle widget destroyed during fetch', async () => {
|
||||
let resolvePromise: (value: any) => void
|
||||
const delayedPromise = new Promise((resolve) => {
|
||||
resolvePromise = resolve
|
||||
})
|
||||
let resolvePromise: (value: { data: unknown; status?: number }) => void
|
||||
const delayedPromise = new Promise<{ data: unknown; status?: number }>(
|
||||
(resolve) => {
|
||||
resolvePromise = resolve
|
||||
}
|
||||
)
|
||||
|
||||
vi.mocked(axios.get).mockImplementationOnce(() => delayedPromise as any)
|
||||
vi.mocked(axios.get).mockImplementationOnce(() => delayedPromise)
|
||||
|
||||
let hook = useRemoteWidget(createMockOptions())
|
||||
let hook: ReturnType<typeof useRemoteWidget> | null =
|
||||
useRemoteWidget(createMockOptions())
|
||||
const fetchPromise = hook.getValue()
|
||||
|
||||
hook = null as any
|
||||
hook = null
|
||||
|
||||
resolvePromise!({ data: ['delayed data'] })
|
||||
await fetchPromise
|
||||
@@ -583,19 +592,19 @@ describe('useRemoteWidget', () => {
|
||||
|
||||
describe('auto-refresh on task completion', () => {
|
||||
it('should add auto-refresh toggle widget', () => {
|
||||
const mockNode = {
|
||||
const mockNode = createMockLGraphNode({
|
||||
addWidget: vi.fn(),
|
||||
widgets: []
|
||||
}
|
||||
const mockWidget = {
|
||||
})
|
||||
const mockWidget = createMockWidget({
|
||||
refresh: vi.fn()
|
||||
}
|
||||
})
|
||||
|
||||
useRemoteWidget({
|
||||
remoteConfig: createMockConfig(),
|
||||
defaultValue: DEFAULT_VALUE,
|
||||
node: mockNode as any,
|
||||
widget: mockWidget as any
|
||||
node: mockNode,
|
||||
widget: mockWidget
|
||||
})
|
||||
|
||||
// Should add auto-refresh toggle widget
|
||||
@@ -613,19 +622,19 @@ describe('useRemoteWidget', () => {
|
||||
it('should register event listener when enabled', async () => {
|
||||
const addEventListenerSpy = vi.spyOn(api, 'addEventListener')
|
||||
|
||||
const mockNode = {
|
||||
const mockNode = createMockLGraphNode({
|
||||
addWidget: vi.fn(),
|
||||
widgets: []
|
||||
}
|
||||
const mockWidget = {
|
||||
})
|
||||
const mockWidget = createMockWidget({
|
||||
refresh: vi.fn()
|
||||
}
|
||||
})
|
||||
|
||||
useRemoteWidget({
|
||||
remoteConfig: createMockConfig(),
|
||||
defaultValue: DEFAULT_VALUE,
|
||||
node: mockNode as any,
|
||||
widget: mockWidget as any
|
||||
node: mockNode,
|
||||
widget: mockWidget
|
||||
})
|
||||
|
||||
// Event listener should be registered immediately
|
||||
@@ -644,16 +653,16 @@ describe('useRemoteWidget', () => {
|
||||
}
|
||||
})
|
||||
|
||||
const mockNode = {
|
||||
const mockNode = createMockLGraphNode({
|
||||
addWidget: vi.fn(),
|
||||
widgets: []
|
||||
}
|
||||
const mockWidget = {} as any
|
||||
})
|
||||
const mockWidget = createMockWidget({})
|
||||
|
||||
useRemoteWidget({
|
||||
remoteConfig: createMockConfig(),
|
||||
defaultValue: DEFAULT_VALUE,
|
||||
node: mockNode as any,
|
||||
node: mockNode,
|
||||
widget: mockWidget
|
||||
})
|
||||
|
||||
@@ -661,8 +670,9 @@ describe('useRemoteWidget', () => {
|
||||
const refreshSpy = vi.spyOn(mockWidget, 'refresh')
|
||||
|
||||
// Get the toggle callback and enable auto-refresh
|
||||
const toggleCallback = mockNode.addWidget.mock.calls.find(
|
||||
(call) => call[0] === 'toggle'
|
||||
const addWidgetMock = mockNode.addWidget as ReturnType<typeof vi.fn>
|
||||
const toggleCallback = addWidgetMock.mock.calls.find(
|
||||
(call: unknown[]) => call[0] === 'toggle'
|
||||
)?.[3]
|
||||
toggleCallback?.(true)
|
||||
|
||||
@@ -681,16 +691,16 @@ describe('useRemoteWidget', () => {
|
||||
}
|
||||
})
|
||||
|
||||
const mockNode = {
|
||||
const mockNode = createMockLGraphNode({
|
||||
addWidget: vi.fn(),
|
||||
widgets: []
|
||||
}
|
||||
const mockWidget = {} as any
|
||||
})
|
||||
const mockWidget = createMockWidget({})
|
||||
|
||||
useRemoteWidget({
|
||||
remoteConfig: createMockConfig(),
|
||||
defaultValue: DEFAULT_VALUE,
|
||||
node: mockNode as any,
|
||||
node: mockNode,
|
||||
widget: mockWidget
|
||||
})
|
||||
|
||||
@@ -715,20 +725,20 @@ describe('useRemoteWidget', () => {
|
||||
|
||||
const removeEventListenerSpy = vi.spyOn(api, 'removeEventListener')
|
||||
|
||||
const mockNode = {
|
||||
const mockNode = createMockLGraphNode({
|
||||
addWidget: vi.fn(),
|
||||
widgets: [],
|
||||
onRemoved: undefined as any
|
||||
}
|
||||
const mockWidget = {
|
||||
onRemoved: undefined
|
||||
})
|
||||
const mockWidget = createMockWidget({
|
||||
refresh: vi.fn()
|
||||
}
|
||||
})
|
||||
|
||||
useRemoteWidget({
|
||||
remoteConfig: createMockConfig(),
|
||||
defaultValue: DEFAULT_VALUE,
|
||||
node: mockNode as any,
|
||||
widget: mockWidget as any
|
||||
node: mockNode,
|
||||
widget: mockWidget
|
||||
})
|
||||
|
||||
// Simulate node removal
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import type { Mock } from 'vitest'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
interface MockWebSocket {
|
||||
readyState: number
|
||||
send: Mock
|
||||
close: Mock
|
||||
addEventListener: Mock
|
||||
removeEventListener: Mock
|
||||
}
|
||||
|
||||
describe('API Feature Flags', () => {
|
||||
let mockWebSocket: any
|
||||
const wsEventHandlers: { [key: string]: (event: any) => void } = {}
|
||||
let mockWebSocket: MockWebSocket
|
||||
const wsEventHandlers: { [key: string]: (event: unknown) => void } = {}
|
||||
|
||||
beforeEach(() => {
|
||||
// Use fake timers
|
||||
@@ -16,7 +25,7 @@ describe('API Feature Flags', () => {
|
||||
send: vi.fn(),
|
||||
close: vi.fn(),
|
||||
addEventListener: vi.fn(
|
||||
(event: string, handler: (event: any) => void) => {
|
||||
(event: string, handler: (event: unknown) => void) => {
|
||||
wsEventHandlers[event] = handler
|
||||
}
|
||||
),
|
||||
|
||||
@@ -919,8 +919,7 @@ export class ComfyApp {
|
||||
const nodeDefArray: ComfyNodeDefV1[] = Object.values(allNodeDefs)
|
||||
useExtensionService().invokeExtensions(
|
||||
'beforeRegisterVueAppNodeDefs',
|
||||
nodeDefArray,
|
||||
this
|
||||
nodeDefArray
|
||||
)
|
||||
nodeDefStore.updateNodeDefs(nodeDefArray)
|
||||
}
|
||||
|
||||
@@ -72,7 +72,11 @@ const findIsobmffBoxByType = (
|
||||
return null
|
||||
}
|
||||
|
||||
const extractJson = (data: Uint8Array, start: number, end: number): any => {
|
||||
const extractJson = (
|
||||
data: Uint8Array,
|
||||
start: number,
|
||||
end: number
|
||||
): ComfyWorkflowJSON | ComfyApiWorkflow | null => {
|
||||
let jsonStart = start
|
||||
while (jsonStart < end && data[jsonStart] !== ASCII.OPEN_BRACE) {
|
||||
jsonStart++
|
||||
@@ -133,7 +137,7 @@ const extractMetadataValueFromDataBox = (
|
||||
lowerKeyName === ComfyMetadataTags.PROMPT.toLowerCase() ||
|
||||
lowerKeyName === ComfyMetadataTags.WORKFLOW.toLowerCase()
|
||||
) {
|
||||
return extractJson(data, valueStart, dataBoxEnd) || null
|
||||
return extractJson(data, valueStart, dataBoxEnd)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ type Props = {
|
||||
style?: Partial<CSSStyleDeclaration>
|
||||
for?: string
|
||||
textContent?: string
|
||||
[key: string]: any
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
type Children = Element[] | Element | string | string[]
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import { legacyMenuCompat } from '@/lib/litegraph/src/contextMenuCompat'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { api } from '@/scripts/api'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useExtensionStore } from '@/stores/extensionStore'
|
||||
import { KeybindingImpl, useKeybindingStore } from '@/stores/keybindingStore'
|
||||
@@ -12,6 +11,8 @@ import { useWidgetStore } from '@/stores/widgetStore'
|
||||
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
||||
import type { ComfyExtension } from '@/types/comfy'
|
||||
import type { AuthUserInfo } from '@/types/authTypes'
|
||||
import { app } from '@/scripts/app'
|
||||
import type { ComfyApp } from '@/scripts/app'
|
||||
|
||||
export const useExtensionService = () => {
|
||||
const extensionStore = useExtensionStore()
|
||||
@@ -135,14 +136,28 @@ export const useExtensionService = () => {
|
||||
}
|
||||
}
|
||||
|
||||
type FunctionPropertyNames<T> = {
|
||||
[K in keyof T]: T[K] extends (...args: unknown[]) => unknown ? K : never
|
||||
}[keyof T]
|
||||
type RemoveLastAppParam<T> = T extends (
|
||||
...args: [...infer Rest, ComfyApp]
|
||||
) => infer R
|
||||
? (...args: Rest) => R
|
||||
: T
|
||||
|
||||
type ComfyExtensionParamsWithoutApp<T extends keyof ComfyExtension> =
|
||||
RemoveLastAppParam<ComfyExtension[T]>
|
||||
/**
|
||||
* Invoke an extension callback
|
||||
* @param {keyof ComfyExtension} method The extension callback to execute
|
||||
* @param {any[]} args Any arguments to pass to the callback
|
||||
* @param {unknown[]} args Any arguments to pass to the callback
|
||||
* @returns
|
||||
*/
|
||||
const invokeExtensions = (method: keyof ComfyExtension, ...args: any[]) => {
|
||||
const results: any[] = []
|
||||
const invokeExtensions = <T extends FunctionPropertyNames<ComfyExtension>>(
|
||||
method: T,
|
||||
...args: Parameters<ComfyExtensionParamsWithoutApp<T>>
|
||||
) => {
|
||||
const results: ReturnType<ComfyExtension[T]>[] = []
|
||||
for (const ext of extensionStore.enabledExtensions) {
|
||||
if (method in ext) {
|
||||
try {
|
||||
@@ -164,12 +179,14 @@ export const useExtensionService = () => {
|
||||
* Invoke an async extension callback
|
||||
* Each callback will be invoked concurrently
|
||||
* @param {string} method The extension callback to execute
|
||||
* @param {...any} args Any arguments to pass to the callback
|
||||
* @param {...unknown} args Any arguments to pass to the callback
|
||||
* @returns
|
||||
*/
|
||||
const invokeExtensionsAsync = async (
|
||||
method: keyof ComfyExtension,
|
||||
...args: any[]
|
||||
const invokeExtensionsAsync = async <
|
||||
T extends FunctionPropertyNames<ComfyExtension>
|
||||
>(
|
||||
method: T,
|
||||
...args: Parameters<ComfyExtensionParamsWithoutApp<T>>
|
||||
) => {
|
||||
return await Promise.all(
|
||||
extensionStore.enabledExtensions.map(async (ext) => {
|
||||
|
||||
@@ -9,8 +9,8 @@ vi.mock('@/services/providers/algoliaSearchProvider')
|
||||
vi.mock('@/services/providers/registrySearchProvider')
|
||||
|
||||
describe('useRegistrySearchGateway', () => {
|
||||
let consoleWarnSpy: any
|
||||
let consoleInfoSpy: any
|
||||
let consoleWarnSpy: ReturnType<typeof vi.spyOn>
|
||||
let consoleInfoSpy: ReturnType<typeof vi.spyOn>
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
JobDetail,
|
||||
JobListItem
|
||||
} from '@/platform/remote/comfyui/jobs/jobTypes'
|
||||
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import {
|
||||
findActiveIndex,
|
||||
getJobDetail,
|
||||
@@ -257,10 +258,12 @@ describe('jobOutputCache', () => {
|
||||
priority: 0,
|
||||
outputs: {}
|
||||
}
|
||||
const mockWorkflow = { version: 1 }
|
||||
const mockWorkflow = { version: 1 } as Partial<ComfyWorkflowJSON>
|
||||
|
||||
vi.mocked(api.getJobDetail).mockResolvedValue(mockDetail)
|
||||
vi.mocked(extractWorkflow).mockResolvedValue(mockWorkflow as any)
|
||||
vi.mocked(extractWorkflow).mockResolvedValue(
|
||||
mockWorkflow as ComfyWorkflowJSON
|
||||
)
|
||||
|
||||
const result = await getJobWorkflow(jobId)
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { CORE_KEYBINDINGS } from '@/constants/coreKeybindings'
|
||||
import { useKeybindingService } from '@/services/keybindingService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import type { DialogInstance } from '@/stores/dialogStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
import {
|
||||
KeyComboImpl,
|
||||
@@ -38,10 +39,13 @@ describe('keybindingService - Escape key handling', () => {
|
||||
mockCommandExecute = vi.fn()
|
||||
commandStore.execute = mockCommandExecute
|
||||
|
||||
// Reset dialog store mock to empty
|
||||
// Reset dialog store mock to empty - only mock the properties we need for testing
|
||||
vi.mocked(useDialogStore).mockReturnValue({
|
||||
dialogStack: []
|
||||
} as any)
|
||||
dialogStack: [],
|
||||
// Add other required properties as undefined/default values to satisfy the type
|
||||
// but they won't be used in these tests
|
||||
...({} as Omit<ReturnType<typeof useDialogStore>, 'dialogStack'>)
|
||||
})
|
||||
|
||||
keybindingService = useKeybindingService()
|
||||
keybindingService.registerCoreKeybindings()
|
||||
@@ -179,8 +183,10 @@ describe('keybindingService - Escape key handling', () => {
|
||||
it('should not execute Escape keybinding when dialogs are open', async () => {
|
||||
// Mock dialog store to have open dialogs
|
||||
vi.mocked(useDialogStore).mockReturnValue({
|
||||
dialogStack: [{ key: 'test-dialog' }]
|
||||
} as any)
|
||||
dialogStack: [{ key: 'test-dialog' } as DialogInstance],
|
||||
// Add other required properties as undefined/default values to satisfy the type
|
||||
...({} as Omit<ReturnType<typeof useDialogStore>, 'dialogStack'>)
|
||||
})
|
||||
|
||||
// Re-create keybinding service to pick up new mock
|
||||
keybindingService = useKeybindingService()
|
||||
|
||||
@@ -78,7 +78,9 @@ describe('keybindingService - Event Forwarding', () => {
|
||||
// Reset dialog store mock to empty
|
||||
vi.mocked(useDialogStore).mockReturnValue({
|
||||
dialogStack: []
|
||||
} as any)
|
||||
} as Partial<ReturnType<typeof useDialogStore>> as ReturnType<
|
||||
typeof useDialogStore
|
||||
>)
|
||||
|
||||
keybindingService = useKeybindingService()
|
||||
keybindingService.registerCoreKeybindings()
|
||||
@@ -126,33 +128,35 @@ describe('keybindingService - Event Forwarding', () => {
|
||||
})
|
||||
|
||||
it('should not forward Delete key when canvas processKey is not available', async () => {
|
||||
// Temporarily replace processKey with undefined
|
||||
// Temporarily replace processKey with undefined - testing edge case
|
||||
const originalProcessKey = vi.mocked(app.canvas).processKey
|
||||
vi.mocked(app.canvas).processKey = undefined as any
|
||||
vi.mocked(app.canvas).processKey = undefined!
|
||||
|
||||
const event = createTestKeyboardEvent('Delete')
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
|
||||
|
||||
// Restore processKey for other tests
|
||||
vi.mocked(app.canvas).processKey = originalProcessKey
|
||||
try {
|
||||
await keybindingService.keybindHandler(event)
|
||||
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
// Restore processKey for other tests
|
||||
vi.mocked(app.canvas).processKey = originalProcessKey
|
||||
}
|
||||
})
|
||||
|
||||
it('should not forward Delete key when canvas is not available', async () => {
|
||||
// Temporarily set canvas to null
|
||||
const originalCanvas = vi.mocked(app).canvas
|
||||
vi.mocked(app).canvas = null as any
|
||||
vi.mocked(app).canvas = null!
|
||||
|
||||
const event = createTestKeyboardEvent('Delete')
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
|
||||
|
||||
// Restore canvas for other tests
|
||||
vi.mocked(app).canvas = originalCanvas
|
||||
try {
|
||||
await keybindingService.keybindHandler(event)
|
||||
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
// Restore canvas for other tests
|
||||
vi.mocked(app).canvas = originalCanvas
|
||||
}
|
||||
})
|
||||
|
||||
it('should not forward non-canvas keys', async () => {
|
||||
|
||||
@@ -29,6 +29,7 @@ import type {
|
||||
ISerialisableNodeOutput,
|
||||
ISerialisedNode
|
||||
} from '@/lib/litegraph/src/types/serialisation'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
@@ -71,6 +72,49 @@ export interface HasInitialMinSize {
|
||||
export const CONFIG = Symbol()
|
||||
export const GET_CONFIG = Symbol()
|
||||
|
||||
export function getExtraOptionsForWidget(
|
||||
node: LGraphNode,
|
||||
widget: IBaseWidget
|
||||
) {
|
||||
const options: IContextMenuValue[] = []
|
||||
const input = node.inputs.find((inp) => inp.widget?.name === widget.name)
|
||||
|
||||
if (input) {
|
||||
options.unshift({
|
||||
content: `${t('contextMenu.RenameWidget')}: ${widget.label ?? widget.name}`,
|
||||
callback: async () => {
|
||||
const newLabel = await useDialogService().prompt({
|
||||
title: t('g.rename'),
|
||||
message: t('g.enterNewName') + ':',
|
||||
defaultValue: widget.label,
|
||||
placeholder: widget.name
|
||||
})
|
||||
if (newLabel === null) return
|
||||
widget.label = newLabel || undefined
|
||||
input.label = newLabel || undefined
|
||||
widget.callback?.(widget.value)
|
||||
useCanvasStore().canvas?.setDirty(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const favoritedWidgetsStore = useFavoritedWidgetsStore()
|
||||
const isFavorited = favoritedWidgetsStore.isFavorited(node, widget.name)
|
||||
options.unshift({
|
||||
content: isFavorited
|
||||
? `${t('contextMenu.UnfavoriteWidget')}: ${widget.label ?? widget.name}`
|
||||
: `${t('contextMenu.FavoriteWidget')}: ${widget.label ?? widget.name}`,
|
||||
callback: () => {
|
||||
favoritedWidgetsStore.toggleFavorite(node, widget.name)
|
||||
}
|
||||
})
|
||||
|
||||
if (node.graph && !node.graph.isRootGraph) {
|
||||
addWidgetPromotionOptions(options, widget, node)
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
/**
|
||||
* Service that augments litegraph with ComfyUI specific functionality.
|
||||
*/
|
||||
@@ -678,47 +722,8 @@ export const useLitegraphService = () => {
|
||||
}
|
||||
const [x, y] = canvas.graph_mouse
|
||||
const overWidget = this.getWidgetOnPos(x, y, true)
|
||||
if (overWidget) {
|
||||
const input = this.inputs.find(
|
||||
(inp) => inp.widget?.name === overWidget.name
|
||||
)
|
||||
|
||||
if (input) {
|
||||
options.unshift({
|
||||
content: `${t('contextMenu.RenameWidget')}: ${overWidget.label ?? overWidget.name}`,
|
||||
callback: async () => {
|
||||
const newLabel = await useDialogService().prompt({
|
||||
title: t('g.rename'),
|
||||
message: t('g.enterNewName') + ':',
|
||||
defaultValue: overWidget.label,
|
||||
placeholder: overWidget.name
|
||||
})
|
||||
if (newLabel === null) return
|
||||
overWidget.label = newLabel || undefined
|
||||
input.label = newLabel || undefined
|
||||
useCanvasStore().canvas?.setDirty(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const favoritedWidgetsStore = useFavoritedWidgetsStore()
|
||||
const isFavorited = favoritedWidgetsStore.isFavorited(
|
||||
this,
|
||||
overWidget.name
|
||||
)
|
||||
options.unshift({
|
||||
content: isFavorited
|
||||
? `${t('contextMenu.UnfavoriteWidget')}: ${overWidget.label ?? overWidget.name}`
|
||||
: `${t('contextMenu.FavoriteWidget')}: ${overWidget.label ?? overWidget.name}`,
|
||||
callback: () => {
|
||||
favoritedWidgetsStore.toggleFavorite(this, overWidget.name)
|
||||
}
|
||||
})
|
||||
|
||||
if (this.graph && !this.graph.isRootGraph) {
|
||||
addWidgetPromotionOptions(options, overWidget, this)
|
||||
}
|
||||
}
|
||||
if (overWidget)
|
||||
options.unshift(...getExtraOptionsForWidget(this, overWidget))
|
||||
return []
|
||||
}
|
||||
}
|
||||
@@ -934,6 +939,7 @@ export const useLitegraphService = () => {
|
||||
addNodeOnGraph,
|
||||
addNodeInput,
|
||||
getCanvasCenter,
|
||||
getExtraOptionsForWidget,
|
||||
goToNode,
|
||||
resetView,
|
||||
fitView,
|
||||
|
||||
@@ -7,7 +7,7 @@ global.fetch = vi.fn()
|
||||
global.URL = {
|
||||
createObjectURL: vi.fn(() => 'blob:mock-url'),
|
||||
revokeObjectURL: vi.fn()
|
||||
} as any
|
||||
} as Partial<typeof URL> as typeof URL
|
||||
|
||||
describe('mediaCacheService', () => {
|
||||
describe('URL reference counting', () => {
|
||||
|
||||
@@ -37,7 +37,7 @@ interface CustomDialogComponentProps {
|
||||
export type DialogComponentProps = ComponentAttrs<typeof GlobalDialog> &
|
||||
CustomDialogComponentProps
|
||||
|
||||
interface DialogInstance<
|
||||
export interface DialogInstance<
|
||||
H extends Component = Component,
|
||||
B extends Component = Component,
|
||||
F extends Component = Component
|
||||
|
||||
@@ -134,18 +134,15 @@ export interface ComfyExtension {
|
||||
actionBarButtons?: ActionBarButton[]
|
||||
/**
|
||||
* Allows any initialisation, e.g. loading resources. Called after the canvas is created but before nodes are added
|
||||
* @param app The ComfyUI app instance
|
||||
*/
|
||||
init?(app: ComfyApp): Promise<void> | void
|
||||
/**
|
||||
* Allows any additional setup, called after the application is fully set up and running
|
||||
* @param app The ComfyUI app instance
|
||||
*/
|
||||
setup?(app: ComfyApp): Promise<void> | void
|
||||
/**
|
||||
* Called before nodes are registered with the graph
|
||||
* @param defs The collection of node definitions, add custom ones or edit existing ones
|
||||
* @param app The ComfyUI app instance
|
||||
*/
|
||||
addCustomNodeDefs?(
|
||||
defs: Record<string, ComfyNodeDef>,
|
||||
@@ -155,7 +152,6 @@ export interface ComfyExtension {
|
||||
// getCustomWidgets.
|
||||
/**
|
||||
* Allows the extension to add custom widgets
|
||||
* @param app The ComfyUI app instance
|
||||
* @returns An array of {[widget name]: widget data}
|
||||
*/
|
||||
getCustomWidgets?(app: ComfyApp): Promise<Widgets> | Widgets
|
||||
@@ -185,7 +181,7 @@ export interface ComfyExtension {
|
||||
* Allows the extension to add additional handling to the node before it is registered with **LGraph**
|
||||
* @param nodeType The node class (not an instance)
|
||||
* @param nodeData The original node object info config object
|
||||
* @param app The ComfyUI app instance
|
||||
* @param app The app instance
|
||||
*/
|
||||
beforeRegisterNodeDef?(
|
||||
nodeType: typeof LGraphNode,
|
||||
@@ -198,15 +194,13 @@ export interface ComfyExtension {
|
||||
* Modifications is expected to be made in place.
|
||||
*
|
||||
* @param defs The node definitions
|
||||
* @param app The ComfyUI app instance
|
||||
* @param app The app instance
|
||||
*/
|
||||
beforeRegisterVueAppNodeDefs?(defs: ComfyNodeDef[], app: ComfyApp): void
|
||||
|
||||
/**
|
||||
* Allows the extension to register additional nodes with LGraph after standard nodes are added.
|
||||
* Custom node classes should extend **LGraphNode**.
|
||||
*
|
||||
* @param app The ComfyUI app instance
|
||||
*/
|
||||
registerCustomNodes?(app: ComfyApp): Promise<void> | void
|
||||
/**
|
||||
@@ -214,13 +208,13 @@ export interface ComfyExtension {
|
||||
* If you break something in the backend and want to patch workflows in the frontend
|
||||
* This is the place to do this
|
||||
* @param node The node that has been loaded
|
||||
* @param app The ComfyUI app instance
|
||||
* @param app The app instance
|
||||
*/
|
||||
loadedGraphNode?(node: LGraphNode, app: ComfyApp): void
|
||||
/**
|
||||
* Allows the extension to run code after the constructor of the node
|
||||
* @param node The node that has been created
|
||||
* @param app The ComfyUI app instance
|
||||
* @param app The app instance
|
||||
*/
|
||||
nodeCreated?(node: LGraphNode, app: ComfyApp): void
|
||||
|
||||
@@ -228,18 +222,22 @@ export interface ComfyExtension {
|
||||
* Allows the extension to modify the graph data before it is configured.
|
||||
* @param graphData The graph data
|
||||
* @param missingNodeTypes The missing node types
|
||||
* @param app The app instance
|
||||
*/
|
||||
beforeConfigureGraph?(
|
||||
graphData: ComfyWorkflowJSON,
|
||||
missingNodeTypes: MissingNodeType[]
|
||||
missingNodeTypes: MissingNodeType[],
|
||||
app: ComfyApp
|
||||
): Promise<void> | void
|
||||
|
||||
/**
|
||||
* Allows the extension to run code after the graph is configured.
|
||||
* @param missingNodeTypes The missing node types
|
||||
* @param app The app instance
|
||||
*/
|
||||
afterConfigureGraph?(
|
||||
missingNodeTypes: MissingNodeType[]
|
||||
missingNodeTypes: MissingNodeType[],
|
||||
app: ComfyApp
|
||||
): Promise<void> | void
|
||||
|
||||
/**
|
||||
|
||||
@@ -211,7 +211,7 @@ export function createMockFileList(files: File[]): FileList {
|
||||
* The ChangeTracker requires a proper ComfyWorkflowJSON structure
|
||||
*/
|
||||
export function createMockChangeTracker(
|
||||
overrides: Record<string, unknown> = {}
|
||||
overrides: Partial<ChangeTracker> = {}
|
||||
): ChangeTracker {
|
||||
const partial = {
|
||||
activeState: {
|
||||
|
||||
Reference in New Issue
Block a user