Compare commits

...

5 Commits

Author SHA1 Message Date
jaeone94
fcf7060ffe test: assert promoted asset initial value 2026-06-23 17:16:19 +09:00
jaeone94
0e3cf499d6 test: cover asset widget modal callback 2026-06-23 17:16:19 +09:00
jaeone94
d1314ae13a fix: commit promoted asset modal selections 2026-06-23 17:16:19 +09:00
jaeone94
22acbffe96 test: use test ids for asset browser e2e 2026-06-23 17:16:19 +09:00
jaeone94
fe6accdacf test: cover promoted asset widget selection 2026-06-23 17:16:19 +09:00
7 changed files with 213 additions and 9 deletions

View File

@@ -112,6 +112,10 @@ export const TestIds = {
root: 'properties-panel',
errorsTab: 'panel-tab-errors'
},
assets: {
browserModal: 'asset-browser-modal',
card: 'asset-card'
},
subgraphEditor: {
hiddenSection: 'subgraph-editor-hidden-section',
iconEye: 'icon-eye',

View File

@@ -0,0 +1,99 @@
import { expect } from '@playwright/test'
import type { Page } from '@playwright/test'
import {
assetRequestIncludesTag,
createCloudAssetsFixture
} from '@e2e/fixtures/assetApiFixture'
import {
STABLE_CHECKPOINT,
STABLE_CHECKPOINT_2
} from '@e2e/fixtures/data/assetFixtures'
import { TestIds } from '@e2e/fixtures/selectors'
const WORKFLOW = 'missing/missing_model_promoted_widget'
const HOST_NODE_ID = 2
const WIDGET_NAME = 'ckpt_name'
const SELECTED_MODEL = STABLE_CHECKPOINT_2.name
const test = createCloudAssetsFixture([STABLE_CHECKPOINT, STABLE_CHECKPOINT_2])
interface WidgetSnapshot {
type: string
value: string
hasLayout: boolean
}
async function getHostWidgetSnapshot(page: Page): Promise<WidgetSnapshot> {
return await page.evaluate(
({ nodeId, widgetName }) => {
const node = window.app!.graph.getNodeById(nodeId)
const widget = node?.widgets?.find((widget) => widget.name === widgetName)
return {
type: widget?.type ?? '',
value: String(widget?.value ?? ''),
hasLayout: widget?.last_y != null
}
},
{ nodeId: HOST_NODE_ID, widgetName: WIDGET_NAME }
)
}
test.describe(
'Promoted subgraph asset widgets',
{ tag: ['@cloud', '@canvas', '@widget'] },
() => {
test.afterEach(async ({ comfyPage }) => {
await comfyPage.nodeOps.clearGraph()
})
test('legacy asset browser selection updates the promoted host widget value', async ({
cloudAssetRequests,
comfyPage
}) => {
await comfyPage.settings.setSetting('Comfy.Assets.UseAssetAPI', true)
await comfyPage.workflow.loadWorkflow(WORKFLOW)
await expect
.poll(
() =>
cloudAssetRequests.some((url) =>
assetRequestIncludesTag(url, 'checkpoints')
),
{ timeout: 10_000 }
)
.toBe(true)
await expect
.poll(() => getHostWidgetSnapshot(comfyPage.page))
.toMatchObject({
type: 'asset',
hasLayout: true
})
const initialWidget = await getHostWidgetSnapshot(comfyPage.page)
expect(initialWidget.value).not.toBe(SELECTED_MODEL)
const hostNode = await comfyPage.nodeOps.getNodeRefById(HOST_NODE_ID)
await hostNode.centerOnNode()
const promotedWidget = await hostNode.getWidgetByName(WIDGET_NAME)
await promotedWidget.click()
const modal = comfyPage.page.getByTestId(TestIds.assets.browserModal)
await expect(modal).toBeVisible()
const assetCard = modal
.getByTestId(TestIds.assets.card)
.filter({ hasText: SELECTED_MODEL })
.first()
await expect(assetCard).toBeVisible()
await assetCard.getByRole('button', { name: 'Use' }).click()
await expect(modal).toBeHidden()
await expect
.poll(() =>
getHostWidgetSnapshot(comfyPage.page).then((widget) => widget.value)
)
.toBe(SELECTED_MODEL)
})
}
)

View File

@@ -7,7 +7,6 @@ import type {
LLink
} from '@/lib/litegraph/src/litegraph'
import { NodeSlot } from '@/lib/litegraph/src/node/NodeSlot'
import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events'
import type {
IBaseWidget,
TWidgetValue
@@ -275,13 +274,6 @@ export class PrimitiveNode extends LGraphNode {
inputNameForBrowser: targetInputName,
defaultValue,
onValueChange: (widget, newValue, oldValue) => {
widget.callback?.(
widget.value,
app.canvas,
this,
app.canvas.graph_mouse,
{} as CanvasPointerEvent
)
this.onWidgetChanged?.(widget.name, newValue, oldValue, widget)
}
})

View File

@@ -1,6 +1,7 @@
<template>
<BaseModalLayout
v-model:right-panel-open="isRightPanelOpen"
data-testid="asset-browser-modal"
data-component-id="AssetBrowserModal"
class="size-full max-h-full max-w-full min-w-0"
:content-title="displayTitle"

View File

@@ -1,5 +1,6 @@
<template>
<div
data-testid="asset-card"
data-component-id="AssetCard"
:data-asset-id="asset.id"
:aria-labelledby="titleId"

View File

@@ -0,0 +1,104 @@
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import type {
IAssetWidget,
IBaseWidget
} from '@/lib/litegraph/src/types/widgets'
import { useAssetBrowserDialog } from '@/platform/assets/composables/useAssetBrowserDialog'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import { createAssetWidget } from './createAssetWidget'
vi.mock('@/i18n', () => ({
t: (key: string) => key
}))
vi.mock('@/platform/assets/composables/useAssetBrowserDialog', () => {
const show = vi.fn()
const browse = vi.fn()
const dialog = { show, browse }
return {
useAssetBrowserDialog: () => dialog
}
})
function checkpointAsset(name: string): AssetItem {
return {
id: `asset-${name}`,
name,
hash: 'checkpoint-hash',
mime_type: 'application/octet-stream',
tags: []
}
}
function expectAssetWidget(
widget: IBaseWidget
): asserts widget is IAssetWidget {
if (widget.type !== 'asset') {
throw new Error('Expected createAssetWidget to create an asset widget')
}
}
describe('createAssetWidget', () => {
beforeEach(() => {
vi.resetAllMocks()
setActivePinia(createTestingPinia({ stubActions: false }))
})
it('notifies the widget callback when the asset browser commits a selected filename', async () => {
const node: Pick<LGraphNode, 'addWidget'> = {
addWidget(type, name, value, callback, options): IBaseWidget {
return {
type,
name,
value,
callback: typeof callback === 'function' ? callback : undefined,
options:
typeof options === 'string'
? { property: options }
: (options ?? {}),
y: 0
}
}
}
const callback = vi.fn<NonNullable<IBaseWidget['callback']>>()
const onValueChange =
vi.fn<
(widget: IBaseWidget, newValue: string, oldValue: unknown) => void
>()
const widget = createAssetWidget({
node,
widgetName: 'ckpt_name',
nodeTypeForBrowser: 'CheckpointLoaderSimple',
defaultValue: 'fake_model.safetensors',
onValueChange
})
expectAssetWidget(widget)
widget.callback = callback
await widget.options.openModal(widget)
const showOptions = vi.mocked(useAssetBrowserDialog().show).mock
.calls[0]?.[0]
if (!showOptions) {
throw new Error('Expected the asset browser dialog to open')
}
showOptions.onAssetSelected?.(checkpointAsset('real_model.safetensors'))
expect(widget.value).toBe('real_model.safetensors')
expect(callback).toHaveBeenCalledTimes(1)
expect(callback).toHaveBeenCalledWith('real_model.safetensors')
expect(onValueChange).toHaveBeenCalledTimes(1)
expect(onValueChange).toHaveBeenCalledWith(
widget,
'real_model.safetensors',
'fake_model.safetensors'
)
})
})

View File

@@ -14,9 +14,11 @@ import {
import { useToastStore } from '@/platform/updates/common/toastStore'
import { getAssetFilename } from '@/platform/assets/utils/assetMetadataUtils'
type AssetWidgetNode = Pick<LGraphNode, 'addWidget'>
interface CreateAssetWidgetParams {
/** The node to add the widget to */
node: LGraphNode
node: AssetWidgetNode
/** The widget name */
widgetName: string
/** The node type to show in asset browser (may differ from node.comfyClass for PrimitiveNode) */
@@ -98,6 +100,7 @@ export function createAssetWidget(
const oldValue = widget.value
widget.value = validatedFilename.data
widget.callback?.(widget.value)
onValueChange?.(widget, validatedFilename.data, oldValue)
}
})