mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-03 06:47:33 +00:00
## Summary
This PR removes unsafe type assertions ("as unknown as Type") from test
files and improves type safety across the codebase.
### Key Changes
#### Type Safety Improvements
- Removed improper `as unknown as Type` patterns from 17 test files in
Group 8 part 7
- Replaced with proper TypeScript patterns using factory functions and
Mock types
- Fixed createTestingPinia usage in test files (was incorrectly using
createPinia)
- Fixed vi.hoisted pattern for mockSetDirty in viewport tests
- Fixed vi.doMock lint issues with vi.mock and vi.hoisted pattern
- Retained necessary `as unknown as` casts only for complex mock objects
where direct type assertions would fail
### Files Changed
Test files (Group 8 part 7 - services, stores, utils):
- src/services/nodeOrganizationService.test.ts
- src/services/providers/algoliaSearchProvider.test.ts
- src/services/providers/registrySearchProvider.test.ts
- src/stores/comfyRegistryStore.test.ts
- src/stores/domWidgetStore.test.ts
- src/stores/executionStore.test.ts
- src/stores/firebaseAuthStore.test.ts
- src/stores/modelToNodeStore.test.ts
- src/stores/queueStore.test.ts
- src/stores/subgraphNavigationStore.test.ts
- src/stores/subgraphNavigationStore.viewport.test.ts
- src/stores/subgraphStore.test.ts
- src/stores/systemStatsStore.test.ts
- src/stores/workspace/nodeHelpStore.test.ts
- src/utils/colorUtil.test.ts
- src/utils/executableGroupNodeChildDTO.test.ts
Source files:
- src/stores/modelStore.ts - Improved type handling
### Testing
- All TypeScript type checking passes (`pnpm typecheck`)
- All affected test files pass (`pnpm test:unit`)
- Linting passes without errors (`pnpm lint`)
- Code formatting applied (`pnpm format`)
Part of the "Road to No Explicit Any" initiative, cleaning up type
casting issues from branch `fix/remove-any-types-part8`.
### Previous PRs in this series:
- Part 2: #7401
- Part 3: #7935
- Part 4: #7970
- Part 5: #8064
- Part 6: #8083
- Part 7: #8092
- Part 8 Group 1: #8253
- Part 8 Group 2: #8258
- Part 8 Group 3: #8304
- Part 8 Group 4: #8314
- Part 8 Group 5: #8329
- Part 8 Group 6: #8344
- Part 8 Group 7: #8459 (this PR)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8459-Road-to-No-explicit-any-Group-8-part-7-test-files-2f86d73d36508114ad28d82e72a3a5e9)
by [Unito](https://www.unito.io)
253 lines
7.7 KiB
TypeScript
253 lines
7.7 KiB
TypeScript
import { defineStore } from 'pinia'
|
|
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. */
|
|
function _findInMetadata(
|
|
metadata: Record<string, string | null>,
|
|
...keys: string[]
|
|
): string | null {
|
|
for (const key of keys) {
|
|
if (key in metadata) {
|
|
const value = metadata[key]
|
|
return value || null
|
|
}
|
|
for (const k in metadata) {
|
|
if (k.endsWith(key)) {
|
|
const value = metadata[k]
|
|
return value || null
|
|
}
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
/** Defines and holds metadata for a model */
|
|
export class ComfyModelDef {
|
|
/** Path to the model */
|
|
readonly path_index: number
|
|
/** Proper filename of the model */
|
|
readonly file_name: string
|
|
/** Normalized filename of the model, with all backslashes replaced with forward slashes */
|
|
readonly normalized_file_name: string
|
|
/** Directory containing the model, eg 'checkpoints' */
|
|
readonly directory: string
|
|
/** Simplified copy of name, used as a default title. Excludes the directory and the '.safetensors' file extension */
|
|
readonly simplified_file_name: string
|
|
/** Key for the model, used to uniquely identify the model. */
|
|
readonly key: string
|
|
/** Title / display name of the model, sometimes same as the name but not always */
|
|
title: string
|
|
/** Metadata: architecture ID for the model, such as 'stable-diffusion-xl-v1-base' */
|
|
architecture_id: string = ''
|
|
/** Metadata: author of the model */
|
|
author: string = ''
|
|
/** Metadata: resolution of the model, eg '1024x1024' */
|
|
resolution: string = ''
|
|
/** Metadata: description of the model */
|
|
description: string = ''
|
|
/** Metadata: usage hint for the model */
|
|
usage_hint: string = ''
|
|
/** Metadata: trigger phrase for the model */
|
|
trigger_phrase: string = ''
|
|
/** Metadata: tags list for the model */
|
|
tags: string[] = []
|
|
/** Metadata: image for the model */
|
|
image: string = ''
|
|
/** Whether the model metadata has been loaded from the server, used for `load()` */
|
|
has_loaded_metadata: boolean = false
|
|
/** If true, a metadata load request has been triggered, but may or may not yet have finished loading */
|
|
is_load_requested: boolean = false
|
|
/** A string full of auto-computed lowercase-only searchable text for this model */
|
|
searchable: string = ''
|
|
|
|
constructor(name: string, directory: string, pathIndex: number) {
|
|
this.path_index = pathIndex
|
|
this.file_name = name
|
|
this.normalized_file_name = name.replaceAll('\\', '/')
|
|
this.simplified_file_name = this.normalized_file_name.split('/').pop() ?? ''
|
|
if (this.simplified_file_name.endsWith('.safetensors')) {
|
|
this.simplified_file_name = this.simplified_file_name.slice(
|
|
0,
|
|
-'.safetensors'.length
|
|
)
|
|
}
|
|
this.title = this.simplified_file_name
|
|
this.directory = directory
|
|
this.key = `${directory}/${this.normalized_file_name}`
|
|
this.updateSearchable()
|
|
}
|
|
|
|
updateSearchable() {
|
|
this.searchable = [
|
|
this.file_name,
|
|
this.title,
|
|
this.author,
|
|
this.description,
|
|
this.trigger_phrase,
|
|
this.tags.join(', ')
|
|
]
|
|
.join('\n')
|
|
.toLowerCase()
|
|
}
|
|
|
|
/** Loads the model metadata from the server, filling in this object if data is available */
|
|
async load(): Promise<void> {
|
|
if (this.has_loaded_metadata || this.is_load_requested) {
|
|
return
|
|
}
|
|
this.is_load_requested = true
|
|
try {
|
|
const metadata = await api.viewMetadata(this.directory, this.file_name)
|
|
if (!metadata) {
|
|
return
|
|
}
|
|
this.title =
|
|
_findInMetadata(
|
|
metadata,
|
|
'modelspec.title',
|
|
'title',
|
|
'display_name',
|
|
'name'
|
|
) || this.title
|
|
this.architecture_id =
|
|
_findInMetadata(metadata, 'modelspec.architecture', 'architecture') ||
|
|
''
|
|
this.author =
|
|
_findInMetadata(metadata, 'modelspec.author', 'author') || ''
|
|
this.description =
|
|
_findInMetadata(metadata, 'modelspec.description', 'description') || ''
|
|
this.resolution =
|
|
_findInMetadata(metadata, 'modelspec.resolution', 'resolution') || ''
|
|
this.usage_hint =
|
|
_findInMetadata(metadata, 'modelspec.usage_hint', 'usage_hint') || ''
|
|
this.trigger_phrase =
|
|
_findInMetadata(
|
|
metadata,
|
|
'modelspec.trigger_phrase',
|
|
'trigger_phrase'
|
|
) || ''
|
|
this.image =
|
|
_findInMetadata(
|
|
metadata,
|
|
'modelspec.thumbnail',
|
|
'thumbnail',
|
|
'image',
|
|
'icon'
|
|
) || ''
|
|
const tagsCommaSeparated =
|
|
_findInMetadata(metadata, 'modelspec.tags', 'tags') || ''
|
|
this.tags = tagsCommaSeparated.split(',').map((tag) => tag.trim())
|
|
this.has_loaded_metadata = true
|
|
this.updateSearchable()
|
|
} catch (error) {
|
|
console.error('Error loading model metadata', this.file_name, this, error)
|
|
}
|
|
}
|
|
}
|
|
|
|
export enum ResourceState {
|
|
Uninitialized,
|
|
Loading,
|
|
Loaded
|
|
}
|
|
|
|
export class ModelFolder {
|
|
/** Models in this folder */
|
|
models: Record<string, ComfyModelDef> = {}
|
|
state: ResourceState = ResourceState.Uninitialized
|
|
|
|
constructor(
|
|
public directory: string,
|
|
private getModelsFunc: (folder: string) => Promise<ModelFile[]>
|
|
) {}
|
|
|
|
get key(): string {
|
|
return this.directory + '/'
|
|
}
|
|
|
|
/**
|
|
* Loads the models in this folder from the server
|
|
*/
|
|
async load() {
|
|
if (this.state !== ResourceState.Uninitialized) {
|
|
return this
|
|
}
|
|
this.state = ResourceState.Loading
|
|
const models = await this.getModelsFunc(this.directory)
|
|
for (const model of models) {
|
|
this.models[`${model.pathIndex}/${model.name}`] = new ComfyModelDef(
|
|
model.name,
|
|
this.directory,
|
|
model.pathIndex
|
|
)
|
|
}
|
|
this.state = ResourceState.Loaded
|
|
return this
|
|
}
|
|
}
|
|
|
|
/** 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[]>(() =>
|
|
modelFolderNames.value.map(
|
|
(folderName) => modelFolderByName.value[folderName]
|
|
)
|
|
)
|
|
const models = computed<ComfyModelDef[]>(() =>
|
|
modelFolders.value.flatMap((folder) => Object.values(folder.models))
|
|
)
|
|
|
|
/**
|
|
* 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()
|
|
modelFolderNames.value = resData.map((folder) => folder.name)
|
|
modelFolderByName.value = {}
|
|
for (const folderName of modelFolderNames.value) {
|
|
const getModelsFunc = useAssetAPI
|
|
? (folder: string) => assetService.getAssetModels(folder)
|
|
: (folder: string) => api.getModels(folder)
|
|
|
|
modelFolderByName.value[folderName] = new ModelFolder(
|
|
folderName,
|
|
getModelsFunc
|
|
)
|
|
}
|
|
}
|
|
|
|
async function getLoadedModelFolder(
|
|
folderName: string
|
|
): Promise<ModelFolder | null> {
|
|
const folder = modelFolderByName.value[folderName]
|
|
return folder ? await folder.load() : null
|
|
}
|
|
|
|
/**
|
|
* Loads all model folders' contents from the server
|
|
*/
|
|
async function loadModels() {
|
|
return Promise.all(modelFolders.value.map((folder) => folder.load()))
|
|
}
|
|
|
|
return {
|
|
models,
|
|
modelFolders,
|
|
loadModelFolders,
|
|
loadModels,
|
|
getLoadedModelFolder
|
|
}
|
|
})
|