Compare commits

...

3 Commits

Author SHA1 Message Date
Austin Mroz
7954e28bb7 Fix test 2026-07-09 16:39:14 -07:00
Austin Mroz
42186132b0 Add test 2026-07-09 15:44:37 -07:00
Austin Mroz
05caa42475 Support adding descriptions to app inputs 2026-07-09 15:44:37 -07:00
8 changed files with 125 additions and 6 deletions

View File

@@ -1,6 +1,7 @@
import type { Locator, Page } from '@playwright/test'
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
import { TestIds } from '@e2e/fixtures/selectors'
import { WidgetSelectDropdownFixture } from '@e2e/fixtures/components/WidgetSelectDropdown'
/**
@@ -26,6 +27,12 @@ export class AppModeWidgetHelper {
return this.container.locator(`[data-widget-key$=":${key}"]`)
}
getWidgetDescription(key: string) {
return this.page
.locator(`[data-widget-key$=":${key}"]`)
.getByTestId(TestIds.appMode.widgetDescription)
}
/** Get a FormDropdown widget by its key (e.g. "10:image"). */
getSelectDropdown(key: string): WidgetSelectDropdownFixture {
return new WidgetSelectDropdownFixture(this.getWidgetItem(key))

View File

@@ -218,6 +218,7 @@ export const TestIds = {
},
appMode: {
widgetItem: 'app-mode-widget-item',
widgetDescription: 'app-mode-widget-description',
welcome: 'linear-welcome',
emptyWorkflow: 'linear-welcome-empty-workflow',
buildApp: 'linear-welcome-build-app',

View File

@@ -46,6 +46,54 @@ test.describe('App mode builder selection', () => {
await expect(items).toHaveCount(1)
})
test(
'Can add description to widgets',
{ tag: '@vue-nodes' },
async ({ comfyPage }) => {
const descLocator =
comfyPage.appMode.widgets.getWidgetDescription('6:text')
await test.step('set up baseline app', async () => {
await comfyPage.appMode.enterAppModeWithInputs([['6', 'text']])
await expect(descLocator, 'Empty description hidden').toBeHidden()
})
const description = "Don't forget the massive fennec ears!"
await test.step('Enter builder and add description', async () => {
await comfyPage.appMode.enterBuilder()
await comfyPage.appMode.steps.goToPreview()
await expect(
descLocator,
'Display placeholder in builder'
).toBeVisible()
await descLocator.dblclick()
await descLocator.locator('input').fill(description)
await descLocator.locator('input').blur()
await expect(descLocator, 'Description updates').toHaveText(description)
})
await test.step('Exit builder and return to app mode', async () => {
await comfyPage.appMode.footer.exitBuilder()
await comfyPage.appMode.toggleAppMode()
await expect(descLocator, 'Description displays').toHaveText(
description
)
})
await test.step('Swap workflows to test persistance', async () => {
await comfyPage.appMode.toggleAppMode()
await comfyPage.menu.topbar.getTab(0).click()
await comfyPage.menu.topbar.getTab(1).click()
await comfyPage.appMode.toggleAppMode()
await expect(descLocator, 'Description persists').toHaveText(
description
)
})
}
)
test('Can not select nodes with errors or notes', async ({ comfyPage }) => {
//Manually set error state on checkpoint loader
//Shouldn't be needed on ci, but has spotty reliability

View File

@@ -1,10 +1,10 @@
<script setup lang="ts">
import { computed, provide } from 'vue'
import { useAppModeWidgetResizing } from '@/components/builder/useAppModeWidgetResizing'
import { useResolvedSelectedInputs } from '@/components/builder/useResolvedSelectedInputs'
import { useI18n } from 'vue-i18n'
import WidgetDescription from '@/components/builder/WidgetDescription.vue'
import { useAppModeWidgetResizing } from '@/components/builder/useAppModeWidgetResizing'
import { useResolvedSelectedInputs } from '@/components/builder/useResolvedSelectedInputs'
import Popover from '@/components/ui/Popover.vue'
import Button from '@/components/ui/button/Button.vue'
import { extractVueNodeData } from '@/composables/graph/useGraphNodeManager'
@@ -33,6 +33,7 @@ interface WidgetEntry {
widgets: NonNullable<ReturnType<typeof nodeToNodeData>['widgets']>
}
action: { widget: IBaseWidget; node: LGraphNode }
description?: string
}
const { mobile = false, builderMode = false } = defineProps<{
@@ -83,6 +84,7 @@ const mappedSelections = computed((): WidgetEntry[] => {
{
key: widgetId,
persistedHeight: config?.height,
description: config?.description,
nodeData: {
...fullNodeData,
widgets: [matchingWidget]
@@ -148,7 +150,13 @@ defineExpose({ handleDragDrop })
</script>
<template>
<div
v-for="{ key, persistedHeight, nodeData, action } in mappedSelections"
v-for="{
key,
persistedHeight,
nodeData,
action,
description
} in mappedSelections"
:key
:class="
cn(
@@ -211,6 +219,24 @@ defineExpose({ handleDragDrop })
</template>
</Popover>
</div>
<div
v-if="description || builderMode"
data-testid="app-mode-widget-description"
:class="
cn(
'h-5 px-5',
description ? 'text-muted-foreground' : 'text-muted-background'
)
"
>
<WidgetDescription
:description
label-class="drag-handle"
label-type="div"
:disabled="!builderMode"
:widget="action.widget"
/>
</div>
<div
:style="
persistedHeight

View File

@@ -0,0 +1,36 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import EditableText from '@/components/common/EditableText.vue'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import { useAppModeStore } from '@/stores/appModeStore'
const { t } = useI18n()
const { widget } = defineProps<{
widget: IBaseWidget
description?: string
disabled?: boolean
}>()
const placeholder = computed(() =>
t('linearMode.arrange.descriptionPlaceholder')
)
const isEditing = ref(false)
function onEditComplete(val: string) {
isEditing.value = false
const description = val && val !== placeholder.value ? val : undefined
useAppModeStore().updateInputConfig(widget, { description })
}
</script>
<template>
<EditableText
:model-value="description || placeholder"
:is-editing
label-class="truncate"
@dblclick="!disabled && (isEditing = true)"
@edit="onEditComplete"
@cancel="isEditing = false"
/>
</template>

View File

@@ -31,7 +31,6 @@ const {
modelValue,
isEditing = false,
inputAttrs = {},
labelClass = '',
labelType = 'span'
} = defineProps<{
modelValue: string

View File

@@ -3736,7 +3736,8 @@
"outputExamples": "Examples: 'Save Image' or 'Save Video'",
"switchToOutputsButton": "Switch to Outputs",
"outputs": "Outputs",
"resultsLabel": "Results generated from the selected output node(s) will be shown here after running this app"
"resultsLabel": "Results generated from the selected output node(s) will be shown here after running this app",
"descriptionPlaceholder": "Double click to add a description"
},
"builder": {
"title": "App builder mode",

View File

@@ -14,6 +14,7 @@ import type { WidgetId } from '@/types/widgetId'
export interface InputWidgetConfig {
height?: number
description?: string
}
type LinearInputId = WidgetId | NodeLocatorId | SerializedNodeId