Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions browser_tests/globalSetup.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import type { FullConfig } from '@playwright/test'
import dotenv from 'dotenv'
import { config as loadEnv } from 'dotenv'

import { backupPath } from './utils/backupUtils'
import { syncDevtools } from './utils/devtoolsSync'

dotenv.config()
loadEnv()

export default function globalSetup(config: FullConfig) {
export default function globalSetup(_: FullConfig) {
if (!process.env.CI) {
if (process.env.TEST_COMFYUI_DIR) {
backupPath([process.env.TEST_COMFYUI_DIR, 'user'])
backupPath([process.env.TEST_COMFYUI_DIR, 'models'], {
renameAndReplaceWithScaffolding: true
})

syncDevtools(process.env.TEST_COMFYUI_DIR)
} else {
console.warn(
'Set TEST_COMFYUI_DIR in .env to prevent user data (settings, workflows, etc.) from being overwritten'
Expand Down
52 changes: 52 additions & 0 deletions browser_tests/utils/devtoolsSync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import fs from 'fs-extra'
import path from 'path'
import { fileURLToPath } from 'url'

export function syncDevtools(targetComfyDir: string): boolean {
if (!targetComfyDir) {
console.warn('syncDevtools skipped: TEST_COMFYUI_DIR not set')
return false
}

// Validate and sanitize the target directory path
const resolvedTargetDir = path.resolve(targetComfyDir)

// Basic path validation to prevent directory traversal
if (resolvedTargetDir.includes('..') || !path.isAbsolute(resolvedTargetDir)) {
console.error('syncDevtools failed: Invalid target directory path')
return false
}

const moduleDir =
typeof __dirname !== 'undefined'
? __dirname
: path.dirname(fileURLToPath(import.meta.url))

const devtoolsSrc = path.resolve(moduleDir, '..', '..', 'tools', 'devtools')

if (!fs.pathExistsSync(devtoolsSrc)) {
console.warn(
`syncDevtools skipped: source directory not found at ${devtoolsSrc}`
)
return false
}

const devtoolsDest = path.resolve(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[security] high Priority

Issue: Directory traversal vulnerability - fs.copySync without path validation
Context: The devtoolsDest path is constructed from user input without validation, potentially allowing directory traversal attacks
Suggestion: Validate and sanitize the targetComfyDir path before using it in file operations. Use path.resolve() and check that the resolved path stays within expected boundaries

resolvedTargetDir,
'custom_nodes',
'ComfyUI_devtools'
)

console.warn(`syncDevtools: copying ${devtoolsSrc} -> ${devtoolsDest}`)

try {
fs.removeSync(devtoolsDest)
fs.ensureDirSync(devtoolsDest)
fs.copySync(devtoolsSrc, devtoolsDest, { overwrite: true })
console.warn('syncDevtools: copy complete')
return true
} catch (error) {
console.error(`Failed to sync DevTools to ${devtoolsDest}:`, error)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[quality] medium Priority

Issue: Error logging uses console.error but function continues execution after failure
Context: The function catches errors during file copying but doesn't throw or return error status to caller
Suggestion: Either throw the error to propagate failure or return a boolean indicating success/failure for better error handling

return false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import PrimeVue from 'primevue/config'
import { describe, expect, it } from 'vitest'

import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import { createMockWidget } from '../testUtils'

import WidgetColorPicker from './WidgetColorPicker.vue'
import WidgetLayoutField from './layout/WidgetLayoutField.vue'

describe('WidgetColorPicker Value Binding', () => {
const createMockWidget = (
const createLocalMockWidget = (
value: string = '#000000',
options: Partial<ColorPickerProps> = {},
callback?: (value: string) => void
Expand Down Expand Up @@ -54,7 +55,7 @@ describe('WidgetColorPicker Value Binding', () => {

describe('Vue Event Emission', () => {
it('emits Vue event when color changes', async () => {
const widget = createMockWidget('#ff0000')
const widget = createLocalMockWidget('#ff0000')
const wrapper = mountComponent(widget, '#ff0000')

const emitted = await setColorPickerValue(wrapper, '#00ff00')
Expand All @@ -64,7 +65,7 @@ describe('WidgetColorPicker Value Binding', () => {
})

it('handles different color formats', async () => {
const widget = createMockWidget('#ffffff')
const widget = createLocalMockWidget('#ffffff')
const wrapper = mountComponent(widget, '#ffffff')

const emitted = await setColorPickerValue(wrapper, '#123abc')
Expand All @@ -74,7 +75,7 @@ describe('WidgetColorPicker Value Binding', () => {
})

it('handles missing callback gracefully', async () => {
const widget = createMockWidget('#000000', {}, undefined)
const widget = createLocalMockWidget('#000000', {}, undefined)
const wrapper = mountComponent(widget, '#000000')

const emitted = await setColorPickerValue(wrapper, '#ff00ff')
Expand All @@ -85,7 +86,7 @@ describe('WidgetColorPicker Value Binding', () => {
})

it('normalizes bare hex without # to #hex on emit', async () => {
const widget = createMockWidget('ff0000')
const widget = createLocalMockWidget('ff0000')
const wrapper = mountComponent(widget, 'ff0000')

const emitted = await setColorPickerValue(wrapper, '00ff00')
Expand All @@ -95,7 +96,7 @@ describe('WidgetColorPicker Value Binding', () => {

it('normalizes rgb() strings to #hex on emit', async (context) => {
context.skip('needs diagnosis')
const widget = createMockWidget('#000000')
const widget = createLocalMockWidget('#000000')
const wrapper = mountComponent(widget, '#000000')

const emitted = await setColorPickerValue(wrapper, 'rgb(255, 0, 0)')
Expand All @@ -104,7 +105,20 @@ describe('WidgetColorPicker Value Binding', () => {
})

it('normalizes hsb() strings to #hex on emit', async () => {
const widget = createMockWidget('#000000', { format: 'hsb' })
const widget = createMockWidget<string>(
'#000000',
{},
undefined,
{
name: 'test_color',
type: 'color'
},
{
type: 'COLOR',
name: 'test_color',
options: { format: 'hsb' }
}
)
const wrapper = mountComponent(widget, '#000000')

const emitted = await setColorPickerValue(wrapper, 'hsb(120, 100, 100)')
Expand All @@ -113,7 +127,20 @@ describe('WidgetColorPicker Value Binding', () => {
})

it('normalizes HSB object values to #hex on emit', async () => {
const widget = createMockWidget('#000000', { format: 'hsb' })
const widget = createMockWidget<string>(
'#000000',
{},
undefined,
{
name: 'test_color',
type: 'color'
},
{
type: 'COLOR',
name: 'test_color',
options: { format: 'hsb' }
}
)
const wrapper = mountComponent(widget, '#000000')

const emitted = await setColorPickerValue(wrapper, {
Expand All @@ -128,7 +155,7 @@ describe('WidgetColorPicker Value Binding', () => {

describe('Component Rendering', () => {
it('renders color picker component', () => {
const widget = createMockWidget('#ff0000')
const widget = createLocalMockWidget('#ff0000')
const wrapper = mountComponent(widget, '#ff0000')

const colorPicker = wrapper.findComponent({ name: 'ColorPicker' })
Expand All @@ -137,36 +164,36 @@ describe('WidgetColorPicker Value Binding', () => {

it('normalizes display to a single leading #', () => {
// Case 1: model value already includes '#'
let widget = createMockWidget('#ff0000')
let widget = createLocalMockWidget('#ff0000')
let wrapper = mountComponent(widget, '#ff0000')
let colorText = wrapper.find('[data-testid="widget-color-text"]')
expect.soft(colorText.text()).toBe('#ff0000')

// Case 2: model value missing '#'
widget = createMockWidget('ff0000')
widget = createLocalMockWidget('ff0000')
wrapper = mountComponent(widget, 'ff0000')
colorText = wrapper.find('[data-testid="widget-color-text"]')
expect.soft(colorText.text()).toBe('#ff0000')
})

it('renders layout field wrapper', () => {
const widget = createMockWidget('#ff0000')
const widget = createLocalMockWidget('#ff0000')
const wrapper = mountComponent(widget, '#ff0000')

const layoutField = wrapper.findComponent({ name: 'WidgetLayoutField' })
expect(layoutField.exists()).toBe(true)
})

it('displays current color value as text', () => {
const widget = createMockWidget('#ff0000')
const widget = createLocalMockWidget('#ff0000')
const wrapper = mountComponent(widget, '#ff0000')

const colorText = wrapper.find('[data-testid="widget-color-text"]')
expect(colorText.text()).toBe('#ff0000')
})

it('updates color text when value changes', async () => {
const widget = createMockWidget('#ff0000')
const widget = createLocalMockWidget('#ff0000')
const wrapper = mountComponent(widget, '#ff0000')

await setColorPickerValue(wrapper, '#00ff00')
Expand All @@ -178,7 +205,7 @@ describe('WidgetColorPicker Value Binding', () => {
})

it('uses default color when no value provided', () => {
const widget = createMockWidget('')
const widget = createLocalMockWidget('')
const wrapper = mountComponent(widget, '')

const colorPicker = wrapper.findComponent({ name: 'ColorPicker' })
Expand All @@ -199,7 +226,7 @@ describe('WidgetColorPicker Value Binding', () => {
]

for (const color of validHexColors) {
const widget = createMockWidget(color)
const widget = createLocalMockWidget(color)
const wrapper = mountComponent(widget, color)

const colorText = wrapper.find('[data-testid="widget-color-text"]')
Expand All @@ -208,7 +235,7 @@ describe('WidgetColorPicker Value Binding', () => {
})

it('handles short hex colors', () => {
const widget = createMockWidget('#fff')
const widget = createLocalMockWidget('#fff')
const wrapper = mountComponent(widget, '#fff')

const colorText = wrapper.find('[data-testid="widget-color-text"]')
Expand All @@ -220,7 +247,7 @@ describe('WidgetColorPicker Value Binding', () => {
format: 'hex' as const,
inline: true
}
const widget = createMockWidget('#ff0000', colorOptions)
const widget = createLocalMockWidget('#ff0000', colorOptions)
const wrapper = mountComponent(widget, '#ff0000')

const colorPicker = wrapper.findComponent({ name: 'ColorPicker' })
Expand All @@ -231,15 +258,15 @@ describe('WidgetColorPicker Value Binding', () => {

describe('Widget Layout Integration', () => {
it('passes widget to layout field', () => {
const widget = createMockWidget('#ff0000')
const widget = createLocalMockWidget('#ff0000')
const wrapper = mountComponent(widget, '#ff0000')

const layoutField = wrapper.findComponent({ name: 'WidgetLayoutField' })
expect(layoutField.props('widget')).toEqual(widget)
})

it('maintains proper component structure', () => {
const widget = createMockWidget('#ff0000')
const widget = createLocalMockWidget('#ff0000')
const wrapper = mountComponent(widget, '#ff0000')

// Should have layout field containing label with color picker and text
Expand All @@ -257,15 +284,15 @@ describe('WidgetColorPicker Value Binding', () => {

describe('Edge Cases', () => {
it('handles empty color value', () => {
const widget = createMockWidget('')
const widget = createLocalMockWidget('')
const wrapper = mountComponent(widget, '')

const colorPicker = wrapper.findComponent({ name: 'ColorPicker' })
expect(colorPicker.exists()).toBe(true)
})

it('handles invalid color formats gracefully', async () => {
const widget = createMockWidget('invalid-color')
const widget = createLocalMockWidget('invalid-color')
const wrapper = mountComponent(widget, 'invalid-color')

const colorText = wrapper.find('[data-testid="widget-color-text"]')
Expand All @@ -277,7 +304,7 @@ describe('WidgetColorPicker Value Binding', () => {
})

it('handles widget with no options', () => {
const widget = createMockWidget('#ff0000')
const widget = createLocalMockWidget('#ff0000')
const wrapper = mountComponent(widget, '#ff0000')

const colorPicker = wrapper.findComponent({ name: 'ColorPicker' })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import ColorPicker from 'primevue/colorpicker'
import { computed, ref, watch } from 'vue'

import { isColorInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
import { isColorFormat, toHexFromFormat } from '@/utils/colorUtil'
import type { ColorFormat, HSB } from '@/utils/colorUtil'
Expand All @@ -51,18 +52,18 @@ const emit = defineEmits<{
}>()

const format = computed<ColorFormat>(() => {
const optionFormat = props.widget.options?.format
const spec = props.widget.spec
if (!spec || !isColorInputSpec(spec)) {
return 'hex'
}

const optionFormat = spec.options?.format
return isColorFormat(optionFormat) ? optionFormat : 'hex'
})

type PickerValue = string | HSB
const localValue = ref<PickerValue>(
toHexFromFormat(
props.modelValue || '#000000',
isColorFormat(props.widget.options?.format)
? props.widget.options.format
: 'hex'
)
toHexFromFormat(props.modelValue || '#000000', format.value)
)

watch(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,18 @@ describe('WidgetFileUpload File Handling', () => {
it('renders file input with correct attributes', () => {
const widget = createMockWidget<File[] | null>(
null,
{ accept: 'image/*' },
{},
undefined,
{
name: 'test_file_upload',
type: 'file'
},
{
type: 'FILEUPLOAD',
name: 'test_file_upload',
options: {
accept: 'image/*'
}
}
)
const wrapper = mountComponent(widget, null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,11 @@
ref="fileInputRef"
type="file"
class="hidden"
:accept="widget.options?.accept"
:accept="
widget.spec && isFileUploadInputSpec(widget.spec)
? widget.spec.options?.accept
: undefined
"
:aria-label="`${$t('g.upload')} ${widget.name || $t('g.file')}`"
:multiple="false"
@change="handleFileChange"
Expand All @@ -188,6 +192,7 @@ import { computed, onUnmounted, ref, watch } from 'vue'

import { useWidgetValue } from '@/composables/graph/useWidgetValue'
import { useTransformCompatOverlayProps } from '@/composables/useTransformCompatOverlayProps'
import { isFileUploadInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { SimplifiedWidget } from '@/types/simplifiedWidget'

const { widget, modelValue } = defineProps<{
Expand Down
Loading
Loading