mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-22 21:38:52 +00:00
Compare commits
10 Commits
jaewon/fix
...
jaewon/fe-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55b0329203 | ||
|
|
c579c88e90 | ||
|
|
0797b7af7a | ||
|
|
ffe8d0f2ee | ||
|
|
ec0711d62e | ||
|
|
e48dcd11f1 | ||
|
|
09943f855a | ||
|
|
b34026527a | ||
|
|
fcdc4404eb | ||
|
|
7f6d354a8e |
@@ -19,5 +19,8 @@ runs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cp -r ./tools/devtools/* /ComfyUI/custom_nodes/ComfyUI_devtools/
|
||||
cd /ComfyUI && python3 main.py --cpu --multi-user --front-end-root "${{ inputs.front_end_root }}" &
|
||||
# TODO(FE-729): remove --enable-assets once BE-786 lands in the CI ComfyUI image
|
||||
# (BE-786 removes the gate so /api/assets is always on). Until then, FE-729
|
||||
# routes modelStore through assetService, which 503s without this flag.
|
||||
cd /ComfyUI && python3 main.py --cpu --multi-user --enable-assets --front-end-root "${{ inputs.front_end_root }}" &
|
||||
wait-for-it --service 127.0.0.1:8188 -t ${{ inputs.timeout }}
|
||||
|
||||
@@ -119,9 +119,22 @@ export class BuilderSelectHelper {
|
||||
)[0]
|
||||
if (!nodeRef) throw new Error(`Node ${nodeTitle} not found`)
|
||||
await nodeRef.centerOnNode()
|
||||
const widgetLocator = this.comfyPage.vueNodes
|
||||
.getNodeLocator(String(nodeRef.id))
|
||||
.getByLabel(widgetName, { exact: true })
|
||||
const node = this.comfyPage.vueNodes.getNodeLocator(String(nodeRef.id))
|
||||
// Grid-mode widgets (WidgetSelectDefault) and number widgets expose
|
||||
// aria-label on a wrapper/input. Asset-mode widgets (WidgetSelectDropdown)
|
||||
// do not — the widget name lives in a sibling
|
||||
// [data-testid="widget-layout-field-label"] div, so fall back to clicking
|
||||
// the dropdown trigger button in the same row.
|
||||
const byAriaLabel = node.getByLabel(widgetName, { exact: true })
|
||||
const widgetLocator =
|
||||
(await byAriaLabel.count()) > 0
|
||||
? byAriaLabel
|
||||
: node
|
||||
.getByTestId('widget-layout-field-label')
|
||||
.filter({ hasText: widgetName })
|
||||
.locator('..')
|
||||
.locator('button')
|
||||
.first()
|
||||
// oxlint-disable-next-line playwright/no-force-option -- Node container has conditional pointer-events:none that blocks actionability
|
||||
await widgetLocator.click({ force: true })
|
||||
await this.comfyPage.nextFrame()
|
||||
|
||||
@@ -27,7 +27,6 @@ test.describe('Default Keybindings', { tag: '@keyboard' }, () => {
|
||||
const sidebarTabs = [
|
||||
{ key: 'KeyW', tabId: 'workflows', label: 'workflows' },
|
||||
{ key: 'KeyN', tabId: 'node-library', label: 'node library' },
|
||||
{ key: 'KeyM', tabId: 'model-library', label: 'model library' },
|
||||
{ key: 'KeyA', tabId: 'assets', label: 'assets' }
|
||||
] as const
|
||||
|
||||
|
||||
@@ -115,42 +115,5 @@ test.describe('Errors tab - Missing models', { tag: '@ui' }, () => {
|
||||
comfyPage.page.getByTestId(TestIds.dialogs.missingModelRefresh)
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
test('Should clear resolved missing model when Refresh is clicked', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await loadWorkflowAndOpenErrorsTab(comfyPage, 'missing/missing_models')
|
||||
await comfyPage.page.route(/\/object_info$/, async (route) => {
|
||||
const response = await route.fetch()
|
||||
const objectInfo = await response.json()
|
||||
const ckptName =
|
||||
objectInfo.CheckpointLoaderSimple.input.required.ckpt_name
|
||||
ckptName[0] = [...ckptName[0], 'fake_model.safetensors']
|
||||
await route.fulfill({ response, json: objectInfo })
|
||||
})
|
||||
|
||||
const objectInfoResponse = comfyPage.page.waitForResponse((response) => {
|
||||
const url = new URL(response.url())
|
||||
return url.pathname.endsWith('/object_info') && response.ok()
|
||||
})
|
||||
const modelFoldersResponse = comfyPage.page.waitForResponse(
|
||||
(response) => {
|
||||
const url = new URL(response.url())
|
||||
return url.pathname.endsWith('/experiment/models') && response.ok()
|
||||
}
|
||||
)
|
||||
const refreshButton = comfyPage.page.getByTestId(
|
||||
TestIds.dialogs.missingModelRefresh
|
||||
)
|
||||
|
||||
await Promise.all([
|
||||
objectInfoResponse,
|
||||
modelFoldersResponse,
|
||||
refreshButton.click()
|
||||
])
|
||||
await expect(
|
||||
comfyPage.page.getByTestId(TestIds.dialogs.missingModelsGroup)
|
||||
).toBeHidden()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
|
||||
|
||||
const MOCK_FOLDERS: Record<string, string[]> = {
|
||||
checkpoints: [
|
||||
'sd_xl_base_1.0.safetensors',
|
||||
'dreamshaper_8.safetensors',
|
||||
'realisticVision_v51.safetensors'
|
||||
],
|
||||
loras: ['detail_tweaker_xl.safetensors', 'add_brightness.safetensors'],
|
||||
vae: ['sdxl_vae.safetensors']
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// 1. Tab open/close
|
||||
// ==========================================================================
|
||||
|
||||
test.describe('Model library sidebar - tab', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.modelLibrary.mockFoldersWithFiles(MOCK_FOLDERS)
|
||||
await comfyPage.setup()
|
||||
})
|
||||
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
await comfyPage.modelLibrary.clearMocks()
|
||||
})
|
||||
|
||||
test('Opens model library tab and shows tree', async ({ comfyPage }) => {
|
||||
const tab = comfyPage.menu.modelLibraryTab
|
||||
await tab.open()
|
||||
|
||||
await expect(tab.modelTree).toBeVisible()
|
||||
await expect(tab.searchInput).toBeVisible()
|
||||
})
|
||||
|
||||
test('Shows refresh and load all folders buttons', async ({ comfyPage }) => {
|
||||
const tab = comfyPage.menu.modelLibraryTab
|
||||
await tab.open()
|
||||
|
||||
await expect(tab.refreshButton).toBeVisible()
|
||||
await expect(tab.loadAllFoldersButton).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================================================
|
||||
// 2. Folder display
|
||||
// ==========================================================================
|
||||
|
||||
test.describe('Model library sidebar - folders', () => {
|
||||
// Mocks are set up before setup(), so app.ts's loadModelFolders()
|
||||
// call during initialization hits the mock and populates the store.
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.modelLibrary.mockFoldersWithFiles(MOCK_FOLDERS)
|
||||
await comfyPage.setup()
|
||||
})
|
||||
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
await comfyPage.modelLibrary.clearMocks()
|
||||
})
|
||||
|
||||
test('Displays model folders after opening tab', async ({ comfyPage }) => {
|
||||
const tab = comfyPage.menu.modelLibraryTab
|
||||
await tab.open()
|
||||
|
||||
await expect(tab.getFolderByLabel('checkpoints')).toBeVisible()
|
||||
await expect(tab.getFolderByLabel('loras')).toBeVisible()
|
||||
await expect(tab.getFolderByLabel('vae')).toBeVisible()
|
||||
})
|
||||
|
||||
test('Expanding a folder loads and shows models', async ({ comfyPage }) => {
|
||||
const tab = comfyPage.menu.modelLibraryTab
|
||||
await tab.open()
|
||||
|
||||
// Click the folder to expand it
|
||||
await tab.getFolderByLabel('checkpoints').click()
|
||||
|
||||
// Models should appear as leaf nodes
|
||||
await expect(tab.getLeafByLabel('sd_xl_base_1.0')).toBeVisible()
|
||||
await expect(tab.getLeafByLabel('dreamshaper_8')).toBeVisible()
|
||||
await expect(tab.getLeafByLabel('realisticVision_v51')).toBeVisible()
|
||||
})
|
||||
|
||||
test('Expanding a different folder shows its models', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
const tab = comfyPage.menu.modelLibraryTab
|
||||
await tab.open()
|
||||
|
||||
await tab.getFolderByLabel('loras').click()
|
||||
|
||||
await expect(tab.getLeafByLabel('detail_tweaker_xl')).toBeVisible()
|
||||
await expect(tab.getLeafByLabel('add_brightness')).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================================================
|
||||
// 3. Search
|
||||
// ==========================================================================
|
||||
|
||||
test.describe('Model library sidebar - search', () => {
|
||||
test.beforeEach(async ({ comfyPage }) => {
|
||||
await comfyPage.modelLibrary.mockFoldersWithFiles(MOCK_FOLDERS)
|
||||
await comfyPage.setup()
|
||||
})
|
||||
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
await comfyPage.modelLibrary.clearMocks()
|
||||
})
|
||||
|
||||
test('Search filters models by filename', async ({ comfyPage }) => {
|
||||
const tab = comfyPage.menu.modelLibraryTab
|
||||
await tab.open()
|
||||
|
||||
await tab.searchInput.fill('dreamshaper')
|
||||
|
||||
// Wait for debounce (300ms) + load + render
|
||||
await expect(tab.getLeafByLabel('dreamshaper_8')).toBeVisible()
|
||||
|
||||
// Other models should not be visible
|
||||
await expect(tab.getLeafByLabel('sd_xl_base_1.0')).toBeHidden()
|
||||
})
|
||||
|
||||
test('Clearing search restores folder view', async ({ comfyPage }) => {
|
||||
const tab = comfyPage.menu.modelLibraryTab
|
||||
await tab.open()
|
||||
|
||||
await tab.searchInput.fill('dreamshaper')
|
||||
await expect(tab.getLeafByLabel('dreamshaper_8')).toBeVisible()
|
||||
|
||||
// Clear the search
|
||||
await tab.searchInput.fill('')
|
||||
|
||||
// Folders should be visible again (collapsed)
|
||||
await expect(tab.getFolderByLabel('checkpoints')).toBeVisible()
|
||||
await expect(tab.getFolderByLabel('loras')).toBeVisible()
|
||||
})
|
||||
|
||||
test('Search with no matches shows empty tree', async ({ comfyPage }) => {
|
||||
const tab = comfyPage.menu.modelLibraryTab
|
||||
await tab.open()
|
||||
|
||||
// Expand a folder and verify models are present before searching
|
||||
await tab.getFolderByLabel('checkpoints').click()
|
||||
await expect(tab.leafNodes).not.toHaveCount(0)
|
||||
|
||||
await tab.searchInput.fill('nonexistent_model_xyz')
|
||||
|
||||
// Wait for debounce, then verify no leaf nodes
|
||||
await expect.poll(() => tab.leafNodes.count()).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================================================
|
||||
// 4. Refresh and load all
|
||||
// ==========================================================================
|
||||
|
||||
test.describe('Model library sidebar - refresh', () => {
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
await comfyPage.modelLibrary.clearMocks()
|
||||
})
|
||||
|
||||
test('Refresh button reloads folder list', async ({ comfyPage }) => {
|
||||
await comfyPage.modelLibrary.mockFoldersWithFiles({
|
||||
checkpoints: ['model_a.safetensors']
|
||||
})
|
||||
await comfyPage.setup()
|
||||
|
||||
const tab = comfyPage.menu.modelLibraryTab
|
||||
await tab.open()
|
||||
|
||||
await expect(tab.getFolderByLabel('checkpoints')).toBeVisible()
|
||||
|
||||
// Update mock to include a new folder
|
||||
await comfyPage.modelLibrary.clearMocks()
|
||||
await comfyPage.modelLibrary.mockFoldersWithFiles({
|
||||
checkpoints: ['model_a.safetensors'],
|
||||
loras: ['lora_b.safetensors']
|
||||
})
|
||||
|
||||
// Wait for the refresh request to complete
|
||||
const refreshRequest = comfyPage.page.waitForRequest(
|
||||
(req) => req.url().endsWith('/experiment/models'),
|
||||
{ timeout: 5000 }
|
||||
)
|
||||
await tab.refreshButton.click()
|
||||
await refreshRequest
|
||||
|
||||
await expect(tab.getFolderByLabel('loras')).toBeVisible()
|
||||
})
|
||||
|
||||
test('Load all folders button triggers loading all model data', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.modelLibrary.mockFoldersWithFiles(MOCK_FOLDERS)
|
||||
await comfyPage.setup()
|
||||
|
||||
const tab = comfyPage.menu.modelLibraryTab
|
||||
await tab.open()
|
||||
|
||||
// Wait for a per-folder model files request triggered by load all
|
||||
const folderRequest = comfyPage.page.waitForRequest(
|
||||
(req) =>
|
||||
/\/api\/experiment\/models\/[^/]+$/.test(req.url()) &&
|
||||
req.method() === 'GET',
|
||||
{ timeout: 5000 }
|
||||
)
|
||||
|
||||
await tab.loadAllFoldersButton.click()
|
||||
await folderRequest
|
||||
})
|
||||
})
|
||||
|
||||
// ==========================================================================
|
||||
// 5. Empty state
|
||||
// ==========================================================================
|
||||
|
||||
test.describe('Model library sidebar - empty state', () => {
|
||||
test.afterEach(async ({ comfyPage }) => {
|
||||
await comfyPage.modelLibrary.clearMocks()
|
||||
})
|
||||
|
||||
test('Shows empty tree when no model folders exist', async ({
|
||||
comfyPage
|
||||
}) => {
|
||||
await comfyPage.modelLibrary.mockFoldersWithFiles({})
|
||||
await comfyPage.setup()
|
||||
|
||||
const tab = comfyPage.menu.modelLibraryTab
|
||||
await tab.open()
|
||||
|
||||
await expect(tab.modelTree).toBeVisible()
|
||||
await expect(tab.folderNodes).toHaveCount(0)
|
||||
await expect(tab.leafNodes).toHaveCount(0)
|
||||
})
|
||||
})
|
||||
@@ -1283,23 +1283,9 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
{
|
||||
id: 'Comfy.BrowseModelAssets',
|
||||
icon: 'pi pi-folder-open',
|
||||
label: 'Experimental: Browse Model Assets',
|
||||
label: 'Browse Model Assets',
|
||||
versionAdded: '1.28.3',
|
||||
function: async () => {
|
||||
if (!useSettingStore().get('Comfy.Assets.UseAssetAPI')) {
|
||||
const confirmed = await dialogService.confirm({
|
||||
title: 'Enable Asset API',
|
||||
message:
|
||||
'The Asset API is currently disabled. Would you like to enable it?',
|
||||
type: 'default'
|
||||
})
|
||||
|
||||
if (!confirmed) return
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
await settingStore.set('Comfy.Assets.UseAssetAPI', true)
|
||||
await workflowService.reloadCurrentWorkflow()
|
||||
}
|
||||
const assetBrowserDialog = useAssetBrowserDialog()
|
||||
await assetBrowserDialog.browse({
|
||||
assetType: 'models',
|
||||
@@ -1318,22 +1304,6 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.ToggleAssetAPI',
|
||||
icon: 'pi pi-database',
|
||||
label: () =>
|
||||
`Experimental: ${
|
||||
useSettingStore().get('Comfy.Assets.UseAssetAPI')
|
||||
? 'Disable'
|
||||
: 'Enable'
|
||||
} AssetAPI`,
|
||||
function: async () => {
|
||||
const settingStore = useSettingStore()
|
||||
const current = settingStore.get('Comfy.Assets.UseAssetAPI') ?? false
|
||||
await settingStore.set('Comfy.Assets.UseAssetAPI', !current)
|
||||
await useWorkflowService().reloadCurrentWorkflow() // ensure changes take effect immediately
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.ToggleQPOV2',
|
||||
icon: 'pi pi-list',
|
||||
|
||||
@@ -75,7 +75,6 @@ vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/assets/utils/assetPreviewUtil', () => ({
|
||||
isAssetPreviewSupported: vi.fn(() => false),
|
||||
persistThumbnail: vi.fn().mockResolvedValue(undefined)
|
||||
}))
|
||||
|
||||
@@ -1483,22 +1482,9 @@ describe('useLoad3d', () => {
|
||||
expect(composable).toBeDefined()
|
||||
})
|
||||
|
||||
it('does not call captureThumbnail when asset preview is unsupported', async () => {
|
||||
const { isAssetPreviewSupported } =
|
||||
it('captures thumbnail and persists it when a model_file widget has a value', async () => {
|
||||
const { persistThumbnail } =
|
||||
await import('@/platform/assets/utils/assetPreviewUtil')
|
||||
vi.mocked(isAssetPreviewSupported).mockReturnValue(false)
|
||||
|
||||
const { handler } = await getModelReadyHandler()
|
||||
handler()
|
||||
await Promise.resolve()
|
||||
|
||||
expect(mockLoad3d.captureThumbnail).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('captures thumbnail and persists it when asset preview is supported and a model_file widget has a value', async () => {
|
||||
const { isAssetPreviewSupported, persistThumbnail } =
|
||||
await import('@/platform/assets/utils/assetPreviewUtil')
|
||||
vi.mocked(isAssetPreviewSupported).mockReturnValue(true)
|
||||
vi.mocked(Load3dUtils.splitFilePath).mockReturnValue([
|
||||
'',
|
||||
'cube.glb'
|
||||
@@ -1523,9 +1509,8 @@ describe('useLoad3d', () => {
|
||||
})
|
||||
|
||||
it('skips persistence when the model widget has no value', async () => {
|
||||
const { isAssetPreviewSupported, persistThumbnail } =
|
||||
const { persistThumbnail } =
|
||||
await import('@/platform/assets/utils/assetPreviewUtil')
|
||||
vi.mocked(isAssetPreviewSupported).mockReturnValue(true)
|
||||
mockNode.widgets = [
|
||||
{ name: 'model_file', value: '' } as unknown as IWidget
|
||||
]
|
||||
@@ -1539,9 +1524,8 @@ describe('useLoad3d', () => {
|
||||
})
|
||||
|
||||
it('swallows captureThumbnail rejections silently', async () => {
|
||||
const { isAssetPreviewSupported, persistThumbnail } =
|
||||
const { persistThumbnail } =
|
||||
await import('@/platform/assets/utils/assetPreviewUtil')
|
||||
vi.mocked(isAssetPreviewSupported).mockReturnValue(true)
|
||||
vi.mocked(Load3dUtils.splitFilePath).mockReturnValue([
|
||||
'',
|
||||
'broken.glb'
|
||||
|
||||
@@ -8,10 +8,7 @@ import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import type Load3d from '@/extensions/core/load3d/Load3d'
|
||||
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
|
||||
import { createLoad3d } from '@/extensions/core/load3d/createLoad3d'
|
||||
import {
|
||||
isAssetPreviewSupported,
|
||||
persistThumbnail
|
||||
} from '@/platform/assets/utils/assetPreviewUtil'
|
||||
import { persistThumbnail } from '@/platform/assets/utils/assetPreviewUtil'
|
||||
import type {
|
||||
AnimationItem,
|
||||
CameraConfig,
|
||||
@@ -862,7 +859,7 @@ export const useLoad3d = (nodeOrRef: MaybeRef<LGraphNode | null>) => {
|
||||
isFirstModelLoad = false
|
||||
},
|
||||
modelReady: () => {
|
||||
if (!load3d || !isAssetPreviewSupported()) return
|
||||
if (!load3d) return
|
||||
|
||||
const node = nodeRef.value
|
||||
const modelWidget = node?.widgets?.find(
|
||||
|
||||
@@ -50,7 +50,6 @@ vi.mock('@/scripts/domWidget', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/assets/utils/assetPreviewUtil', () => ({
|
||||
isAssetPreviewSupported: vi.fn(() => false),
|
||||
persistThumbnail: vi.fn()
|
||||
}))
|
||||
|
||||
|
||||
@@ -17,10 +17,7 @@ type SaveMeshOutput = NodeOutputWith<{
|
||||
'3d'?: ResultItem[]
|
||||
}>
|
||||
import type { CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import {
|
||||
isAssetPreviewSupported,
|
||||
persistThumbnail
|
||||
} from '@/platform/assets/utils/assetPreviewUtil'
|
||||
import { persistThumbnail } from '@/platform/assets/utils/assetPreviewUtil'
|
||||
import { app } from '@/scripts/app'
|
||||
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
@@ -60,15 +57,13 @@ function applySaveGLBOutput(node: LGraphNode, fileInfo: ResultItem): void {
|
||||
silentOnNotFound: true
|
||||
})
|
||||
|
||||
if (isAssetPreviewSupported()) {
|
||||
const filename = fileInfo.filename ?? ''
|
||||
void load3d
|
||||
.whenLoadIdle()
|
||||
.then(() => load3d.captureThumbnail(256, 256))
|
||||
.then((dataUrl) => fetch(dataUrl).then((r) => r.blob()))
|
||||
.then((blob) => persistThumbnail(filename, blob))
|
||||
.catch(() => {})
|
||||
}
|
||||
const filename = fileInfo.filename ?? ''
|
||||
void load3d
|
||||
.whenLoadIdle()
|
||||
.then(() => load3d.captureThumbnail(256, 256))
|
||||
.then((dataUrl) => fetch(dataUrl).then((r) => r.blob()))
|
||||
.then((blob) => persistThumbnail(filename, blob))
|
||||
.catch(() => {})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -194,16 +189,14 @@ useExtensionService().registerExtension({
|
||||
silentOnNotFound: true
|
||||
})
|
||||
|
||||
if (isAssetPreviewSupported()) {
|
||||
const filename = fileInfo.filename ?? ''
|
||||
const filename = fileInfo.filename ?? ''
|
||||
|
||||
void load3d
|
||||
.whenLoadIdle()
|
||||
.then(() => load3d.captureThumbnail(256, 256))
|
||||
.then((dataUrl) => fetch(dataUrl).then((r) => r.blob()))
|
||||
.then((blob) => persistThumbnail(filename, blob))
|
||||
.catch(() => {})
|
||||
}
|
||||
void load3d
|
||||
.whenLoadIdle()
|
||||
.then(() => load3d.captureThumbnail(256, 256))
|
||||
.then((dataUrl) => fetch(dataUrl).then((r) => r.blob()))
|
||||
.then((blob) => persistThumbnail(filename, blob))
|
||||
.catch(() => {})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,15 +5,12 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { AssetMeta } from '../schemas/mediaAssetSchema'
|
||||
import Media3DTop from './Media3DTop.vue'
|
||||
|
||||
const {
|
||||
mockUseIntersectionObserver,
|
||||
mockFindServerPreviewUrl,
|
||||
mockIsAssetPreviewSupported
|
||||
} = vi.hoisted(() => ({
|
||||
mockUseIntersectionObserver: vi.fn(),
|
||||
mockFindServerPreviewUrl: vi.fn(),
|
||||
mockIsAssetPreviewSupported: vi.fn(() => true)
|
||||
}))
|
||||
const { mockUseIntersectionObserver, mockFindServerPreviewUrl } = vi.hoisted(
|
||||
() => ({
|
||||
mockUseIntersectionObserver: vi.fn(),
|
||||
mockFindServerPreviewUrl: vi.fn()
|
||||
})
|
||||
)
|
||||
|
||||
vi.mock('@vueuse/core', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof VueUseCore>()
|
||||
@@ -24,8 +21,7 @@ vi.mock('@vueuse/core', async (importOriginal) => {
|
||||
})
|
||||
|
||||
vi.mock('../utils/assetPreviewUtil', () => ({
|
||||
findServerPreviewUrl: mockFindServerPreviewUrl,
|
||||
isAssetPreviewSupported: mockIsAssetPreviewSupported
|
||||
findServerPreviewUrl: mockFindServerPreviewUrl
|
||||
}))
|
||||
|
||||
function makeAsset(overrides: Partial<AssetMeta> = {}): AssetMeta {
|
||||
@@ -66,7 +62,6 @@ const globalConfig = { mocks: { $t: (key: string) => key } }
|
||||
describe('Media3DTop', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockIsAssetPreviewSupported.mockReturnValue(true)
|
||||
})
|
||||
|
||||
it('renders the placeholder when no thumbnail has loaded', () => {
|
||||
@@ -117,18 +112,6 @@ describe('Media3DTop', () => {
|
||||
expect(img).toHaveAttribute('src', 'http://server/from-name.png')
|
||||
})
|
||||
|
||||
it('skips the server query when isAssetPreviewSupported is false', async () => {
|
||||
fireObserverIntersecting()
|
||||
mockIsAssetPreviewSupported.mockReturnValue(false)
|
||||
render(Media3DTop, {
|
||||
props: { asset: makeAsset() },
|
||||
global: globalConfig
|
||||
})
|
||||
await flush()
|
||||
|
||||
expect(mockFindServerPreviewUrl).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('picks up a patched preview_url after the IntersectionObserver gate has closed', async () => {
|
||||
// Initial render: observer fires, server has no preview yet — hasAttempted=true
|
||||
fireObserverIntersecting()
|
||||
|
||||
@@ -23,10 +23,7 @@ import { useIntersectionObserver } from '@vueuse/core'
|
||||
import { onBeforeUnmount, ref, watch } from 'vue'
|
||||
|
||||
import type { AssetMeta } from '../schemas/mediaAssetSchema'
|
||||
import {
|
||||
findServerPreviewUrl,
|
||||
isAssetPreviewSupported
|
||||
} from '../utils/assetPreviewUtil'
|
||||
import { findServerPreviewUrl } from '../utils/assetPreviewUtil'
|
||||
|
||||
const { asset } = defineProps<{ asset: AssetMeta }>()
|
||||
|
||||
@@ -49,7 +46,7 @@ async function loadThumbnail() {
|
||||
|
||||
if (!asset?.src) return
|
||||
|
||||
if (asset.name && isAssetPreviewSupported()) {
|
||||
if (asset.name) {
|
||||
const serverPreviewUrl = await findServerPreviewUrl(asset.name)
|
||||
if (serverPreviewUrl) {
|
||||
thumbnailSrc.value = serverPreviewUrl
|
||||
|
||||
@@ -10,21 +10,6 @@ import {
|
||||
} from '@/platform/assets/services/assetService'
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
const mockDistributionState = vi.hoisted(() => ({ isCloud: false }))
|
||||
const mockSettingStoreGet = vi.hoisted(() => vi.fn(() => false))
|
||||
|
||||
vi.mock('@/platform/distribution/types', () => ({
|
||||
get isCloud() {
|
||||
return mockDistributionState.isCloud
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: vi.fn(() => ({
|
||||
get: mockSettingStoreGet
|
||||
}))
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/modelToNodeStore', () => {
|
||||
const registeredNodeTypes: Record<string, string> = {
|
||||
CheckpointLoaderSimple: 'ckpt_name',
|
||||
@@ -85,59 +70,27 @@ function validAsset(overrides: Partial<AssetItem> = {}): AssetItem {
|
||||
describe(assetService.shouldUseAssetBrowser, () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockDistributionState.isCloud = false
|
||||
mockSettingStoreGet.mockReturnValue(false)
|
||||
})
|
||||
|
||||
it('returns false when not on cloud', () => {
|
||||
mockDistributionState.isCloud = false
|
||||
mockSettingStoreGet.mockReturnValue(true)
|
||||
|
||||
expect(
|
||||
assetService.shouldUseAssetBrowser('CheckpointLoaderSimple', 'ckpt_name')
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false when asset API setting is disabled', () => {
|
||||
mockDistributionState.isCloud = true
|
||||
mockSettingStoreGet.mockReturnValue(false)
|
||||
|
||||
expect(
|
||||
assetService.shouldUseAssetBrowser('CheckpointLoaderSimple', 'ckpt_name')
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false when node type is not eligible', () => {
|
||||
mockDistributionState.isCloud = true
|
||||
mockSettingStoreGet.mockReturnValue(true)
|
||||
|
||||
expect(
|
||||
assetService.shouldUseAssetBrowser('UnknownNode', 'some_input')
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('returns true when cloud, setting enabled, and node is eligible', () => {
|
||||
mockDistributionState.isCloud = true
|
||||
mockSettingStoreGet.mockReturnValue(true)
|
||||
|
||||
it('returns true when the node is eligible', () => {
|
||||
expect(
|
||||
assetService.shouldUseAssetBrowser('CheckpointLoaderSimple', 'ckpt_name')
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false when nodeType is undefined', () => {
|
||||
mockDistributionState.isCloud = true
|
||||
mockSettingStoreGet.mockReturnValue(true)
|
||||
|
||||
expect(assetService.shouldUseAssetBrowser(undefined, 'ckpt_name')).toBe(
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
it('returns false when widget name does not match registered input', () => {
|
||||
mockDistributionState.isCloud = true
|
||||
mockSettingStoreGet.mockReturnValue(true)
|
||||
|
||||
expect(
|
||||
assetService.shouldUseAssetBrowser(
|
||||
'CheckpointLoaderSimple',
|
||||
|
||||
@@ -20,8 +20,6 @@ import type {
|
||||
ModelFolder,
|
||||
TagsOperationResult
|
||||
} from '@/platform/assets/schemas/assetSchema'
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useModelToNodeStore } from '@/stores/modelToNodeStore'
|
||||
|
||||
@@ -175,7 +173,6 @@ function getLocalizedErrorMessage(errorCode: string): string {
|
||||
const ASSETS_ENDPOINT = '/assets'
|
||||
const ASSETS_DOWNLOAD_ENDPOINT = '/assets/download'
|
||||
const ASSETS_EXPORT_ENDPOINT = '/assets/export'
|
||||
const EXPERIMENTAL_WARNING = `EXPERIMENTAL: If you are seeing this please make sure "Comfy.Assets.UseAssetAPI" is set to "false" in your ComfyUI Settings.\n`
|
||||
const DEFAULT_LIMIT = 500
|
||||
const INPUT_ASSETS_WITH_PUBLIC_LIMIT = 500
|
||||
|
||||
@@ -229,9 +226,7 @@ function validateAssetResponse(data: unknown): AssetResponse {
|
||||
if (result.success) return result.data
|
||||
|
||||
const error = fromZodError(result.error)
|
||||
throw new Error(
|
||||
`${EXPERIMENTAL_WARNING}Invalid asset response against zod schema:\n${error}`
|
||||
)
|
||||
throw new Error(`Invalid asset response against zod schema:\n${error}`)
|
||||
}
|
||||
|
||||
function validateUploadedAssetResponse(
|
||||
@@ -309,7 +304,7 @@ function createAssetService() {
|
||||
: await api.fetchApi(url)
|
||||
if (!res.ok) {
|
||||
throw new Error(
|
||||
`${EXPERIMENTAL_WARNING}Unable to load ${context}: Server returned ${res.status}. Please try again.`
|
||||
`Unable to load ${context}: Server returned ${res.status}. Please try again.`
|
||||
)
|
||||
}
|
||||
const data = await res.json()
|
||||
@@ -378,17 +373,8 @@ function createAssetService() {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the asset API is enabled (cloud environment + user setting).
|
||||
*/
|
||||
function isAssetAPIEnabled(): boolean {
|
||||
if (!isCloud) return false
|
||||
return !!useSettingStore().get('Comfy.Assets.UseAssetAPI')
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the asset browser should be used for a given node input.
|
||||
* Combines the cloud environment check, user setting, and eligibility check.
|
||||
*
|
||||
* @param nodeType - The ComfyUI node comfyClass
|
||||
* @param widgetName - The name of the widget to check
|
||||
@@ -398,7 +384,7 @@ function createAssetService() {
|
||||
nodeType: string | undefined,
|
||||
widgetName: string
|
||||
): boolean {
|
||||
return isAssetAPIEnabled() && isAssetBrowserEligible(nodeType, widgetName)
|
||||
return isAssetBrowserEligible(nodeType, widgetName)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -448,7 +434,7 @@ function createAssetService() {
|
||||
const res = await api.fetchApi(`${ASSETS_ENDPOINT}/${id}`)
|
||||
if (!res.ok) {
|
||||
throw new Error(
|
||||
`${EXPERIMENTAL_WARNING}Unable to load asset details for ${id}: Server returned ${res.status}. Please try again.`
|
||||
`Unable to load asset details for ${id}: Server returned ${res.status}. Please try again.`
|
||||
)
|
||||
}
|
||||
const data = await res.json()
|
||||
@@ -459,9 +445,7 @@ function createAssetService() {
|
||||
const error = result.error
|
||||
? fromZodError(result.error)
|
||||
: 'Unknown validation error'
|
||||
throw new Error(
|
||||
`${EXPERIMENTAL_WARNING}Invalid asset response against zod schema:\n${error}`
|
||||
)
|
||||
throw new Error(`Invalid asset response against zod schema:\n${error}`)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -939,7 +923,6 @@ function createAssetService() {
|
||||
return {
|
||||
getAssetModelFolders,
|
||||
getAssetModels,
|
||||
isAssetAPIEnabled,
|
||||
isAssetBrowserEligible,
|
||||
shouldUseAssetBrowser,
|
||||
getAssetsForNodeType,
|
||||
|
||||
@@ -3,7 +3,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
findOutputAsset,
|
||||
findServerPreviewUrl,
|
||||
isAssetPreviewSupported,
|
||||
persistThumbnail
|
||||
} from '@/platform/assets/utils/assetPreviewUtil'
|
||||
|
||||
@@ -11,8 +10,6 @@ const mockFetchApi = vi.hoisted(() => vi.fn())
|
||||
const mockApiURL = vi.hoisted(() =>
|
||||
vi.fn((path: string) => `http://localhost:8188${path}`)
|
||||
)
|
||||
const mockGetServerFeature = vi.hoisted(() => vi.fn(() => false))
|
||||
const mockIsAssetAPIEnabled = vi.hoisted(() => vi.fn(() => false))
|
||||
const mockUploadAssetFromBase64 = vi.hoisted(() => vi.fn())
|
||||
const mockUpdateAsset = vi.hoisted(() => vi.fn())
|
||||
const mockSetAssetPreview = vi.hoisted(() => vi.fn())
|
||||
@@ -21,14 +18,12 @@ vi.mock('@/scripts/api', () => ({
|
||||
api: {
|
||||
fetchApi: mockFetchApi,
|
||||
apiURL: mockApiURL,
|
||||
api_base: '',
|
||||
getServerFeature: mockGetServerFeature
|
||||
api_base: ''
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/platform/assets/services/assetService', () => ({
|
||||
assetService: {
|
||||
isAssetAPIEnabled: mockIsAssetAPIEnabled,
|
||||
uploadAssetFromBase64: mockUploadAssetFromBase64,
|
||||
updateAsset: mockUpdateAsset
|
||||
}
|
||||
@@ -81,26 +76,6 @@ const localAssetWithPreview = {
|
||||
preview_url: '/api/view?type=output&filename=preview.png'
|
||||
}
|
||||
|
||||
describe('isAssetPreviewSupported', () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
|
||||
it('returns true when asset API is enabled (cloud)', () => {
|
||||
mockIsAssetAPIEnabled.mockReturnValue(true)
|
||||
expect(isAssetPreviewSupported()).toBe(true)
|
||||
})
|
||||
|
||||
it('returns true when server assets feature is enabled (local)', () => {
|
||||
mockGetServerFeature.mockReturnValue(true)
|
||||
expect(isAssetPreviewSupported()).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false when neither is enabled', () => {
|
||||
mockIsAssetAPIEnabled.mockReturnValue(false)
|
||||
mockGetServerFeature.mockReturnValue(false)
|
||||
expect(isAssetPreviewSupported()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('findOutputAsset', () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
|
||||
|
||||
@@ -10,12 +10,6 @@ interface AssetRecord {
|
||||
preview_id?: string | null
|
||||
}
|
||||
|
||||
export function isAssetPreviewSupported(): boolean {
|
||||
return (
|
||||
assetService.isAssetAPIEnabled() || api.getServerFeature('assets', false)
|
||||
)
|
||||
}
|
||||
|
||||
async function fetchAssets(
|
||||
params: Record<string, string>
|
||||
): Promise<AssetRecord[]> {
|
||||
|
||||
@@ -1218,14 +1218,6 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
defaultValue: true,
|
||||
versionAdded: '1.30.3'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Assets.UseAssetAPI',
|
||||
name: 'Use Asset API for model library',
|
||||
type: 'hidden',
|
||||
tooltip: 'Use new Asset API for model browsing',
|
||||
defaultValue: isCloud ? true : false,
|
||||
experimental: true
|
||||
},
|
||||
{
|
||||
id: 'Comfy.VersionCompatibility.DisableWarnings',
|
||||
name: 'Disable version compatibility warnings',
|
||||
|
||||
@@ -18,8 +18,7 @@ const flushPromises = () =>
|
||||
|
||||
vi.mock('@/platform/assets/services/assetService', () => ({
|
||||
assetService: {
|
||||
shouldUseAssetBrowser: vi.fn(() => true),
|
||||
isAssetAPIEnabled: vi.fn(() => true)
|
||||
shouldUseAssetBrowser: vi.fn(() => true)
|
||||
}
|
||||
}))
|
||||
|
||||
|
||||
@@ -17,12 +17,10 @@ const i18n = createI18n({
|
||||
|
||||
// Mock state for asset service
|
||||
const mockShouldUseAssetBrowser = vi.hoisted(() => vi.fn(() => false))
|
||||
const mockIsAssetAPIEnabled = vi.hoisted(() => vi.fn(() => false))
|
||||
|
||||
vi.mock('@/platform/assets/services/assetService', () => ({
|
||||
assetService: {
|
||||
shouldUseAssetBrowser: mockShouldUseAssetBrowser,
|
||||
isAssetAPIEnabled: mockIsAssetAPIEnabled
|
||||
shouldUseAssetBrowser: mockShouldUseAssetBrowser
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -68,7 +66,6 @@ const globalConfig = {
|
||||
describe('WidgetSelect Value Binding', () => {
|
||||
beforeEach(() => {
|
||||
mockShouldUseAssetBrowser.mockReturnValue(false)
|
||||
mockIsAssetAPIEnabled.mockReturnValue(false)
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ const specDescriptor = computed<{
|
||||
const isAssetMode = computed(
|
||||
() =>
|
||||
assetService.shouldUseAssetBrowser(props.nodeType, props.widget.name) ||
|
||||
(assetService.isAssetAPIEnabled() && props.widget.type === 'asset')
|
||||
props.widget.type === 'asset'
|
||||
)
|
||||
|
||||
const assetKind = computed(() => specDescriptor.value.kind)
|
||||
|
||||
@@ -12,14 +12,12 @@ import { AssetKindKey } from './types'
|
||||
import type { FormDropdownMenuItemProps } from './types'
|
||||
|
||||
const mockFindServerPreviewUrl = vi.hoisted(() => vi.fn())
|
||||
const mockIsAssetPreviewSupported = vi.hoisted(() => vi.fn(() => true))
|
||||
const intersectionCallbacks = vi.hoisted(
|
||||
() => [] as Array<(entries: Array<{ isIntersecting: boolean }>) => void>
|
||||
)
|
||||
|
||||
vi.mock('@/platform/assets/utils/assetPreviewUtil', () => ({
|
||||
findServerPreviewUrl: (name: string) => mockFindServerPreviewUrl(name),
|
||||
isAssetPreviewSupported: () => mockIsAssetPreviewSupported()
|
||||
findServerPreviewUrl: (name: string) => mockFindServerPreviewUrl(name)
|
||||
}))
|
||||
|
||||
vi.mock('@vueuse/core', () => ({
|
||||
@@ -83,7 +81,6 @@ describe('FormDropdownMenuItem', () => {
|
||||
beforeEach(() => {
|
||||
intersectionCallbacks.length = 0
|
||||
mockFindServerPreviewUrl.mockReset()
|
||||
mockIsAssetPreviewSupported.mockReset().mockReturnValue(true)
|
||||
})
|
||||
|
||||
describe('Label and name', () => {
|
||||
@@ -167,14 +164,6 @@ describe('FormDropdownMenuItem', () => {
|
||||
expect(img.getAttribute('src')).toBe('/api/preview/resolved.png')
|
||||
})
|
||||
|
||||
it('skips lookup when asset preview is unsupported', async () => {
|
||||
mockIsAssetPreviewSupported.mockReturnValue(false)
|
||||
renderItem({ name: '3d/model.glb' }, { assetKind: 'mesh' })
|
||||
fireIntersection(true)
|
||||
await flushPromises()
|
||||
expect(mockFindServerPreviewUrl).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('only looks up once for repeated intersection events', async () => {
|
||||
mockFindServerPreviewUrl.mockResolvedValue(null)
|
||||
renderItem({ name: '3d/model.glb' }, { assetKind: 'mesh' })
|
||||
|
||||
@@ -5,10 +5,7 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
|
||||
import {
|
||||
findServerPreviewUrl,
|
||||
isAssetPreviewSupported
|
||||
} from '@/platform/assets/utils/assetPreviewUtil'
|
||||
import { findServerPreviewUrl } from '@/platform/assets/utils/assetPreviewUtil'
|
||||
|
||||
import { AssetKindKey } from './types'
|
||||
import type { FormDropdownMenuItemProps } from './types'
|
||||
@@ -40,7 +37,6 @@ function toLookupName(name: string): string {
|
||||
}
|
||||
|
||||
async function resolveMeshPreview() {
|
||||
if (!isAssetPreviewSupported()) return
|
||||
const url = await findServerPreviewUrl(toLookupName(props.name))
|
||||
if (url) resolvedMeshPreview.value = url
|
||||
}
|
||||
|
||||
@@ -425,7 +425,6 @@ const zSettings = z.object({
|
||||
'Comfy.VueNodes.Enabled': z.boolean(),
|
||||
'Comfy.AppBuilder.VueNodeSwitchDismissed': z.boolean(),
|
||||
'Comfy.VueNodes.AutoScaleLayout': z.boolean(),
|
||||
'Comfy.Assets.UseAssetAPI': z.boolean(),
|
||||
'Comfy.Queue.QPOV2': z.boolean(),
|
||||
'Comfy.Queue.ShowRunProgressBar': z.boolean(),
|
||||
'Comfy-Desktop.AutoUpdate': z.boolean(),
|
||||
|
||||
@@ -3,15 +3,12 @@ import { setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { assetService } from '@/platform/assets/services/assetService'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useModelStore } from '@/stores/modelStore'
|
||||
|
||||
// Mock the api
|
||||
vi.mock('@/scripts/api', () => ({
|
||||
api: {
|
||||
getModels: vi.fn(),
|
||||
getModelFolders: vi.fn(),
|
||||
viewMetadata: vi.fn(),
|
||||
apiURL: vi.fn((path: string) => `http://localhost:8188${path}`),
|
||||
addEventListener: vi.fn(),
|
||||
@@ -27,37 +24,8 @@ vi.mock('@/platform/assets/services/assetService', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
// Mock the settingStore
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: vi.fn()
|
||||
}))
|
||||
|
||||
function enableMocks(useAssetAPI = false) {
|
||||
// Mock settingStore to return the useAssetAPI setting
|
||||
const mockSettingStore = {
|
||||
get: vi.fn().mockImplementation((key: string) => {
|
||||
if (key === 'Comfy.Assets.UseAssetAPI') {
|
||||
return useAssetAPI
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
vi.mocked(useSettingStore, { partial: true }).mockReturnValue(
|
||||
mockSettingStore
|
||||
)
|
||||
|
||||
// Mock experimental API - returns objects with name and folders properties
|
||||
vi.mocked(api.getModels).mockResolvedValue([
|
||||
{ name: 'sdxl.safetensors', pathIndex: 0 },
|
||||
{ name: 'sdv15.safetensors', pathIndex: 0 },
|
||||
{ name: 'noinfo.safetensors', pathIndex: 0 }
|
||||
])
|
||||
vi.mocked(api.getModelFolders).mockResolvedValue([
|
||||
{ name: 'checkpoints', folders: ['/path/to/checkpoints'] },
|
||||
{ name: 'vae', folders: ['/path/to/vae'] }
|
||||
])
|
||||
|
||||
// Mock asset API - also returns objects with name and folders properties
|
||||
function enableMocks() {
|
||||
// Mock asset API - returns objects with name and folders properties
|
||||
vi.mocked(assetService.getAssetModelFolders).mockResolvedValue([
|
||||
{ name: 'checkpoints', folders: ['/path/to/checkpoints'] },
|
||||
{ name: 'vae', folders: ['/path/to/vae'] }
|
||||
@@ -141,11 +109,11 @@ describe('useModelStore', () => {
|
||||
enableMocks()
|
||||
store = useModelStore()
|
||||
await store.loadModelFolders()
|
||||
expect(api.getModels).toHaveBeenCalledTimes(0)
|
||||
expect(assetService.getAssetModels).toHaveBeenCalledTimes(0)
|
||||
await store.getLoadedModelFolder('checkpoints')
|
||||
expect(api.getModels).toHaveBeenCalledTimes(1)
|
||||
expect(assetService.getAssetModels).toHaveBeenCalledTimes(1)
|
||||
await store.getLoadedModelFolder('checkpoints')
|
||||
expect(api.getModels).toHaveBeenCalledTimes(1)
|
||||
expect(assetService.getAssetModels).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
describe('refreshModelFolder', () => {
|
||||
@@ -154,9 +122,9 @@ describe('useModelStore', () => {
|
||||
store = useModelStore()
|
||||
await store.loadModelFolders()
|
||||
await store.getLoadedModelFolder('checkpoints')
|
||||
expect(api.getModels).toHaveBeenCalledTimes(1)
|
||||
expect(assetService.getAssetModels).toHaveBeenCalledTimes(1)
|
||||
|
||||
vi.mocked(api.getModels).mockResolvedValueOnce([
|
||||
vi.mocked(assetService.getAssetModels).mockResolvedValueOnce([
|
||||
{ name: 'sdxl.safetensors', pathIndex: 0 },
|
||||
{ name: 'sdv15.safetensors', pathIndex: 0 },
|
||||
{ name: 'noinfo.safetensors', pathIndex: 0 },
|
||||
@@ -165,7 +133,7 @@ describe('useModelStore', () => {
|
||||
|
||||
await store.refreshModelFolder('checkpoints')
|
||||
|
||||
expect(api.getModels).toHaveBeenCalledTimes(2)
|
||||
expect(assetService.getAssetModels).toHaveBeenCalledTimes(2)
|
||||
const folder = await store.getLoadedModelFolder('checkpoints')
|
||||
expect(Object.keys(folder!.models)).toHaveLength(4)
|
||||
expect(folder!.models['0/new-upload.safetensors']).toBeDefined()
|
||||
@@ -175,12 +143,12 @@ describe('useModelStore', () => {
|
||||
enableMocks()
|
||||
store = useModelStore()
|
||||
await store.loadModelFolders()
|
||||
expect(api.getModelFolders).toHaveBeenCalledTimes(1)
|
||||
expect(assetService.getAssetModelFolders).toHaveBeenCalledTimes(1)
|
||||
|
||||
await store.refreshModelFolder('loras')
|
||||
|
||||
expect(api.getModelFolders).toHaveBeenCalledTimes(2)
|
||||
expect(api.getModels).not.toHaveBeenCalled()
|
||||
expect(assetService.getAssetModelFolders).toHaveBeenCalledTimes(2)
|
||||
expect(assetService.getAssetModels).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -190,13 +158,15 @@ describe('useModelStore', () => {
|
||||
store = useModelStore()
|
||||
await store.loadModelFolders()
|
||||
await store.getLoadedModelFolder('checkpoints')
|
||||
expect(api.getModels).toHaveBeenCalledTimes(1)
|
||||
expect(assetService.getAssetModels).toHaveBeenCalledTimes(1)
|
||||
|
||||
await store.refresh()
|
||||
|
||||
expect(api.getModelFolders).toHaveBeenCalledTimes(2)
|
||||
expect(api.getModels).toHaveBeenCalledTimes(2)
|
||||
expect(api.getModels).toHaveBeenLastCalledWith('checkpoints')
|
||||
expect(assetService.getAssetModelFolders).toHaveBeenCalledTimes(2)
|
||||
expect(assetService.getAssetModels).toHaveBeenCalledTimes(2)
|
||||
expect(assetService.getAssetModels).toHaveBeenLastCalledWith(
|
||||
'checkpoints'
|
||||
)
|
||||
})
|
||||
|
||||
it('does not load folders that were never opened', async () => {
|
||||
@@ -206,38 +176,20 @@ describe('useModelStore', () => {
|
||||
|
||||
await store.refresh()
|
||||
|
||||
expect(api.getModelFolders).toHaveBeenCalledTimes(2)
|
||||
expect(api.getModels).not.toHaveBeenCalled()
|
||||
expect(assetService.getAssetModelFolders).toHaveBeenCalledTimes(2)
|
||||
expect(assetService.getAssetModels).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('API switching functionality', () => {
|
||||
it('should use experimental API for complete workflow when UseAssetAPI setting is false', async () => {
|
||||
enableMocks(false) // useAssetAPI = false
|
||||
describe('asset API usage', () => {
|
||||
it('uses the asset API for model folders and models', async () => {
|
||||
enableMocks()
|
||||
store = useModelStore()
|
||||
await store.loadModelFolders()
|
||||
const folderStore = await store.getLoadedModelFolder('checkpoints')
|
||||
|
||||
// Both APIs return objects with .name property, modelStore extracts folder.name in both cases
|
||||
expect(api.getModelFolders).toHaveBeenCalledTimes(1)
|
||||
expect(api.getModels).toHaveBeenCalledWith('checkpoints')
|
||||
expect(assetService.getAssetModelFolders).toHaveBeenCalledTimes(0)
|
||||
expect(assetService.getAssetModels).toHaveBeenCalledTimes(0)
|
||||
expect(folderStore).toBeDefined()
|
||||
expect(Object.keys(folderStore!.models)).toHaveLength(3)
|
||||
})
|
||||
|
||||
it('should use asset API for complete workflow when UseAssetAPI setting is true', async () => {
|
||||
enableMocks(true) // useAssetAPI = true
|
||||
store = useModelStore()
|
||||
await store.loadModelFolders()
|
||||
const folderStore = await store.getLoadedModelFolder('checkpoints')
|
||||
|
||||
// Both APIs return objects with .name property, modelStore extracts folder.name in both cases
|
||||
expect(assetService.getAssetModelFolders).toHaveBeenCalledTimes(1)
|
||||
expect(assetService.getAssetModels).toHaveBeenCalledWith('checkpoints')
|
||||
expect(api.getModelFolders).toHaveBeenCalledTimes(0)
|
||||
expect(api.getModels).toHaveBeenCalledTimes(0)
|
||||
expect(folderStore).toBeDefined()
|
||||
expect(Object.keys(folderStore!.models)).toHaveLength(3)
|
||||
})
|
||||
|
||||
@@ -3,7 +3,6 @@ import { computed, ref } from 'vue'
|
||||
|
||||
import type { ModelFile } from '@/platform/assets/schemas/assetSchema'
|
||||
import { assetService } from '@/platform/assets/services/assetService'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { api } from '@/scripts/api'
|
||||
|
||||
/** (Internal helper) finds a value in a metadata object from any of a list of keys. */
|
||||
@@ -193,7 +192,6 @@ export class ModelFolder {
|
||||
|
||||
/** Model store handler, wraps individual per-folder model stores */
|
||||
export const useModelStore = defineStore('models', () => {
|
||||
const settingStore = useSettingStore()
|
||||
const modelFolderNames = ref<string[]>([])
|
||||
const modelFolderByName = ref<Record<string, ModelFolder>>({})
|
||||
const modelFolders = computed<ModelFolder[]>(() =>
|
||||
@@ -206,21 +204,14 @@ export const useModelStore = defineStore('models', () => {
|
||||
)
|
||||
|
||||
function createGetModelsFunc(): (folder: string) => Promise<ModelFile[]> {
|
||||
const useAssetAPI: boolean = settingStore.get('Comfy.Assets.UseAssetAPI')
|
||||
return useAssetAPI
|
||||
? (folder) => assetService.getAssetModels(folder)
|
||||
: (folder) => api.getModels(folder)
|
||||
return (folder) => assetService.getAssetModels(folder)
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the model folders from the server
|
||||
*/
|
||||
async function loadModelFolders() {
|
||||
const useAssetAPI: boolean = settingStore.get('Comfy.Assets.UseAssetAPI')
|
||||
|
||||
const resData = useAssetAPI
|
||||
? await assetService.getAssetModelFolders()
|
||||
: await api.getModelFolders()
|
||||
const resData = await assetService.getAssetModelFolders()
|
||||
modelFolderNames.value = resData.map((folder) => folder.name)
|
||||
modelFolderByName.value = {}
|
||||
const getModelsFunc = createGetModelsFunc()
|
||||
|
||||
@@ -73,13 +73,9 @@ export const useSidebarTabStore = defineStore('sidebarTab', () => {
|
||||
versionAdded: '1.3.9',
|
||||
category: 'view-controls' as const,
|
||||
function: async () => {
|
||||
const settingStore = useSettingStore()
|
||||
const commandStore = useCommandStore()
|
||||
|
||||
if (
|
||||
tab.id === 'model-library' &&
|
||||
settingStore.get('Comfy.Assets.UseAssetAPI')
|
||||
) {
|
||||
if (tab.id === 'model-library') {
|
||||
await commandStore.commands
|
||||
.find((cmd) => cmd.id === 'Comfy.BrowseModelAssets')
|
||||
?.function?.()
|
||||
|
||||
Reference in New Issue
Block a user