Skip to content

Commit ccc1039

Browse files
christian-byrnegithub-actions
andauthored
[feat] Add file upload support to canvas background image setting (#3958)
Co-authored-by: github-actions <[email protected]>
1 parent 49400c6 commit ccc1039

File tree

14 files changed

+414
-39
lines changed

14 files changed

+414
-39
lines changed
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
import { expect } from '@playwright/test'
2+
3+
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
4+
5+
test.describe('Background Image Upload', () => {
6+
test.beforeEach(async ({ comfyPage }) => {
7+
// Reset the background image setting before each test
8+
await comfyPage.setSetting('Comfy.Canvas.BackgroundImage', '')
9+
})
10+
11+
test.afterEach(async ({ comfyPage }) => {
12+
// Clean up background image setting after each test
13+
await comfyPage.setSetting('Comfy.Canvas.BackgroundImage', '')
14+
})
15+
16+
test('should show background image upload component in settings', async ({
17+
comfyPage
18+
}) => {
19+
// Open settings dialog
20+
await comfyPage.page.keyboard.press('Control+,')
21+
22+
// Navigate to Appearance category
23+
const appearanceOption = comfyPage.page.locator('text=Appearance')
24+
await appearanceOption.click()
25+
26+
// Find the background image setting
27+
const backgroundImageSetting = comfyPage.page.locator(
28+
'#Comfy\\.Canvas\\.BackgroundImage'
29+
)
30+
await expect(backgroundImageSetting).toBeVisible()
31+
32+
// Verify the component has the expected elements using semantic selectors
33+
const urlInput = backgroundImageSetting.locator('input[type="text"]')
34+
await expect(urlInput).toBeVisible()
35+
await expect(urlInput).toHaveAttribute('placeholder')
36+
37+
const uploadButton = backgroundImageSetting.locator(
38+
'button:has(.pi-upload)'
39+
)
40+
await expect(uploadButton).toBeVisible()
41+
42+
const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)')
43+
await expect(clearButton).toBeVisible()
44+
await expect(clearButton).toBeDisabled() // Should be disabled when no image
45+
})
46+
47+
test('should upload image file and set as background', async ({
48+
comfyPage
49+
}) => {
50+
// Open settings dialog
51+
await comfyPage.page.keyboard.press('Control+,')
52+
53+
// Navigate to Appearance category
54+
const appearanceOption = comfyPage.page.locator('text=Appearance')
55+
await appearanceOption.click()
56+
57+
// Find the background image setting
58+
const backgroundImageSetting = comfyPage.page.locator(
59+
'#Comfy\\.Canvas\\.BackgroundImage'
60+
)
61+
// Click the upload button to trigger file input
62+
const uploadButton = backgroundImageSetting.locator(
63+
'button:has(.pi-upload)'
64+
)
65+
66+
// Set up file upload handler
67+
const fileChooserPromise = comfyPage.page.waitForEvent('filechooser')
68+
await uploadButton.click()
69+
const fileChooser = await fileChooserPromise
70+
71+
// Upload the test image
72+
await fileChooser.setFiles(comfyPage.assetPath('image32x32.webp'))
73+
74+
// Wait for upload to complete and verify the setting was updated
75+
await comfyPage.page.waitForTimeout(500) // Give time for file reading
76+
77+
// Verify the URL input now has an API URL
78+
const urlInput = backgroundImageSetting.locator('input[type="text"]')
79+
const inputValue = await urlInput.inputValue()
80+
expect(inputValue).toMatch(/^\/api\/view\?.*subfolder=backgrounds/)
81+
82+
// Verify clear button is now enabled
83+
const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)')
84+
await expect(clearButton).toBeEnabled()
85+
86+
// Verify the setting value was actually set
87+
const settingValue = await comfyPage.getSetting(
88+
'Comfy.Canvas.BackgroundImage'
89+
)
90+
expect(settingValue).toMatch(/^\/api\/view\?.*subfolder=backgrounds/)
91+
})
92+
93+
test('should accept URL input for background image', async ({
94+
comfyPage
95+
}) => {
96+
const testImageUrl = 'https://example.com/test-image.png'
97+
98+
// Open settings dialog
99+
await comfyPage.page.keyboard.press('Control+,')
100+
101+
// Navigate to Appearance category
102+
const appearanceOption = comfyPage.page.locator('text=Appearance')
103+
await appearanceOption.click()
104+
105+
// Find the background image setting
106+
const backgroundImageSetting = comfyPage.page.locator(
107+
'#Comfy\\.Canvas\\.BackgroundImage'
108+
)
109+
// Enter URL in the input field
110+
const urlInput = backgroundImageSetting.locator('input[type="text"]')
111+
await urlInput.fill(testImageUrl)
112+
113+
// Trigger blur event to ensure the value is set
114+
await urlInput.blur()
115+
116+
// Verify clear button is now enabled
117+
const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)')
118+
await expect(clearButton).toBeEnabled()
119+
120+
// Verify the setting value was updated
121+
const settingValue = await comfyPage.getSetting(
122+
'Comfy.Canvas.BackgroundImage'
123+
)
124+
expect(settingValue).toBe(testImageUrl)
125+
})
126+
127+
test('should clear background image when clear button is clicked', async ({
128+
comfyPage
129+
}) => {
130+
const testImageUrl = 'https://example.com/test-image.png'
131+
132+
// First set a background image
133+
await comfyPage.setSetting('Comfy.Canvas.BackgroundImage', testImageUrl)
134+
135+
// Open settings dialog
136+
await comfyPage.page.keyboard.press('Control+,')
137+
138+
// Navigate to Appearance category
139+
const appearanceOption = comfyPage.page.locator('text=Appearance')
140+
await appearanceOption.click()
141+
142+
// Find the background image setting
143+
const backgroundImageSetting = comfyPage.page.locator(
144+
'#Comfy\\.Canvas\\.BackgroundImage'
145+
)
146+
// Verify the input has the test URL
147+
const urlInput = backgroundImageSetting.locator('input[type="text"]')
148+
await expect(urlInput).toHaveValue(testImageUrl)
149+
150+
// Verify clear button is enabled
151+
const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)')
152+
await expect(clearButton).toBeEnabled()
153+
154+
// Click the clear button
155+
await clearButton.click()
156+
157+
// Verify the input is now empty
158+
await expect(urlInput).toHaveValue('')
159+
160+
// Verify clear button is now disabled
161+
await expect(clearButton).toBeDisabled()
162+
163+
// Verify the setting value was cleared
164+
const settingValue = await comfyPage.getSetting(
165+
'Comfy.Canvas.BackgroundImage'
166+
)
167+
expect(settingValue).toBe('')
168+
})
169+
170+
test('should show tooltip on upload and clear buttons', async ({
171+
comfyPage
172+
}) => {
173+
// Open settings dialog
174+
await comfyPage.page.keyboard.press('Control+,')
175+
176+
// Navigate to Appearance category
177+
const appearanceOption = comfyPage.page.locator('text=Appearance')
178+
await appearanceOption.click()
179+
180+
// Find the background image setting
181+
const backgroundImageSetting = comfyPage.page.locator(
182+
'#Comfy\\.Canvas\\.BackgroundImage'
183+
)
184+
// Hover over upload button and verify tooltip appears
185+
const uploadButton = backgroundImageSetting.locator(
186+
'button:has(.pi-upload)'
187+
)
188+
await uploadButton.hover()
189+
190+
// Wait for tooltip to appear and verify it exists
191+
await comfyPage.page.waitForTimeout(700) // Tooltip delay
192+
const uploadTooltip = comfyPage.page.locator('.p-tooltip:visible')
193+
await expect(uploadTooltip).toBeVisible()
194+
195+
// Move away to hide tooltip
196+
await comfyPage.page.locator('body').hover()
197+
await comfyPage.page.waitForTimeout(100)
198+
199+
// Set a background to enable clear button
200+
const urlInput = backgroundImageSetting.locator('input[type="text"]')
201+
await urlInput.fill('https://example.com/test.png')
202+
await urlInput.blur()
203+
204+
// Hover over clear button and verify tooltip appears
205+
const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)')
206+
await clearButton.hover()
207+
208+
// Wait for tooltip to appear and verify it exists
209+
await comfyPage.page.waitForTimeout(700) // Tooltip delay
210+
const clearTooltip = comfyPage.page.locator('.p-tooltip:visible')
211+
await expect(clearTooltip).toBeVisible()
212+
})
213+
214+
test('should maintain reactive updates between URL input and clear button state', async ({
215+
comfyPage
216+
}) => {
217+
// Open settings dialog
218+
await comfyPage.page.keyboard.press('Control+,')
219+
220+
// Navigate to Appearance category
221+
const appearanceOption = comfyPage.page.locator('text=Appearance')
222+
await appearanceOption.click()
223+
224+
// Find the background image setting
225+
const backgroundImageSetting = comfyPage.page.locator(
226+
'#Comfy\\.Canvas\\.BackgroundImage'
227+
)
228+
const urlInput = backgroundImageSetting.locator('input[type="text"]')
229+
const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)')
230+
231+
// Initially clear button should be disabled
232+
await expect(clearButton).toBeDisabled()
233+
234+
// Type some text - clear button should become enabled
235+
await urlInput.fill('test')
236+
await expect(clearButton).toBeEnabled()
237+
238+
// Clear the text manually - clear button should become disabled again
239+
await urlInput.fill('')
240+
await expect(clearButton).toBeDisabled()
241+
242+
// Add text again - clear button should become enabled
243+
await urlInput.fill('https://example.com/image.png')
244+
await expect(clearButton).toBeEnabled()
245+
246+
// Use clear button - should clear input and disable itself
247+
await clearButton.click()
248+
await expect(urlInput).toHaveValue('')
249+
await expect(clearButton).toBeDisabled()
250+
})
251+
})
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<template>
2+
<div class="flex gap-2">
3+
<InputText
4+
v-model="modelValue"
5+
class="flex-1"
6+
:placeholder="$t('g.imageUrl')"
7+
/>
8+
<Button
9+
v-tooltip="$t('g.upload')"
10+
:icon="isUploading ? 'pi pi-spin pi-spinner' : 'pi pi-upload'"
11+
size="small"
12+
:disabled="isUploading"
13+
@click="triggerFileInput"
14+
/>
15+
<Button
16+
v-tooltip="$t('g.clear')"
17+
outlined
18+
icon="pi pi-trash"
19+
severity="danger"
20+
size="small"
21+
:disabled="!modelValue"
22+
@click="clearImage"
23+
/>
24+
<input
25+
ref="fileInput"
26+
type="file"
27+
class="hidden"
28+
accept="image/*"
29+
@change="handleFileUpload"
30+
/>
31+
</div>
32+
</template>
33+
34+
<script setup lang="ts">
35+
import Button from 'primevue/button'
36+
import InputText from 'primevue/inputtext'
37+
import { ref } from 'vue'
38+
39+
import { api } from '@/scripts/api'
40+
import { useToastStore } from '@/stores/toastStore'
41+
42+
const modelValue = defineModel<string>()
43+
44+
const fileInput = ref<HTMLInputElement | null>(null)
45+
const isUploading = ref(false)
46+
47+
const triggerFileInput = () => {
48+
fileInput.value?.click()
49+
}
50+
51+
const uploadFile = async (file: File): Promise<string | null> => {
52+
const body = new FormData()
53+
body.append('image', file)
54+
body.append('subfolder', 'backgrounds')
55+
56+
const resp = await api.fetchApi('/upload/image', {
57+
method: 'POST',
58+
body
59+
})
60+
61+
if (resp.status !== 200) {
62+
useToastStore().addAlert(
63+
`Upload failed: ${resp.status} - ${resp.statusText}`
64+
)
65+
return null
66+
}
67+
68+
const data = await resp.json()
69+
return data.subfolder ? `${data.subfolder}/${data.name}` : data.name
70+
}
71+
72+
const handleFileUpload = async (event: Event) => {
73+
const target = event.target as HTMLInputElement
74+
if (target.files && target.files[0]) {
75+
const file = target.files[0]
76+
77+
isUploading.value = true
78+
try {
79+
const uploadedPath = await uploadFile(file)
80+
if (uploadedPath) {
81+
// Set the value to the API view URL with subfolder parameter
82+
const params = new URLSearchParams({
83+
filename: uploadedPath,
84+
type: 'input',
85+
subfolder: 'backgrounds'
86+
})
87+
modelValue.value = `/api/view?${params.toString()}`
88+
}
89+
} catch (error) {
90+
useToastStore().addAlert(`Upload error: ${String(error)}`)
91+
} finally {
92+
isUploading.value = false
93+
}
94+
}
95+
}
96+
97+
const clearImage = () => {
98+
modelValue.value = ''
99+
if (fileInput.value) {
100+
fileInput.value.value = ''
101+
}
102+
}
103+
</script>

src/components/common/FormItem.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import Select from 'primevue/select'
3636
import ToggleSwitch from 'primevue/toggleswitch'
3737
import { type Component, markRaw } from 'vue'
3838
39+
import BackgroundImageUpload from '@/components/common/BackgroundImageUpload.vue'
3940
import CustomFormValue from '@/components/common/CustomFormValue.vue'
4041
import FormColorPicker from '@/components/common/FormColorPicker.vue'
4142
import FormImageUpload from '@/components/common/FormImageUpload.vue'
@@ -102,6 +103,8 @@ function getFormComponent(item: FormItem): Component {
102103
return FormColorPicker
103104
case 'url':
104105
return UrlInput
106+
case 'backgroundImage':
107+
return BackgroundImageUpload
105108
default:
106109
return InputText
107110
}

0 commit comments

Comments
 (0)