Files
ComfyUI_frontend/apps/desktop-ui/src/views/InstallView.stories.ts
Johnpaul Chiwetelu 90a701dd67 Road to No Explicit Any Part 11 (#8565)
## Summary

This PR removes `any` types from widgets, services, stores, and test
files, replacing them with proper TypeScript types.

### Key Changes

#### Type Safety Improvements
- Replaced `any` with `unknown`, explicit types, or proper interfaces
across widgets and services
- Added proper type imports (TgpuRoot, Point, StyleValue, etc.)
- Created typed interfaces (NumericWidgetOptions, TestWindow,
ImportFailureDetail, etc.)
- Fixed function return types to be non-nullable where appropriate
- Added type guards and null checks instead of non-null assertions
- Used `ComponentProps` from vue-component-type-helpers for component
testing

#### Widget System
- Added index signature to IWidgetOptions for Record compatibility
- Centralized disabled logic in WidgetInputNumberInput
- Moved template type assertions to computed properties
- Fixed ComboWidget getOptionLabel type assertions
- Improved remote widget type handling with runtime checks

#### Services & Stores
- Fixed getOrCreateViewer to return non-nullable values
- Updated addNodeOnGraph to use specific options type `{ pos?: Point }`
- Added proper type assertions for settings store retrieval
- Fixed executionIdToCurrentId return type (string | undefined)

#### Test Infrastructure
- Exported GraphOrSubgraph from litegraph barrel to avoid circular
dependencies
- Updated test fixtures with proper TypeScript types (TestInfo,
LGraphNode)
- Replaced loose Record types with ComponentProps in tests
- Added proper error handling in WebSocket fixture

#### Code Organization
- Created shared i18n-types module for locale data types
- Made ImportFailureDetail non-exported (internal use only)
- Added @public JSDoc tag to ElectronWindow type
- Fixed console.log usage in scripts to use allowed methods

### Files Changed

**Widgets & Components:**
-
src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.vue
-
src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDefault.vue
-
src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue
- src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue
-
src/renderer/extensions/vueNodes/widgets/composables/useRemoteWidget.ts
- src/lib/litegraph/src/widgets/ComboWidget.ts
- src/lib/litegraph/src/types/widgets.ts
- src/components/common/LazyImage.vue
- src/components/load3d/Load3dViewerContent.vue

**Services & Stores:**
- src/services/litegraphService.ts
- src/services/load3dService.ts
- src/services/colorPaletteService.ts
- src/stores/maskEditorStore.ts
- src/stores/nodeDefStore.ts
- src/platform/settings/settingStore.ts
- src/platform/workflow/management/stores/workflowStore.ts

**Composables & Utils:**
- src/composables/node/useWatchWidget.ts
- src/composables/useCanvasDrop.ts
- src/utils/widgetPropFilter.ts
- src/utils/queueDisplay.ts
- src/utils/envUtil.ts

**Test Files:**
- browser_tests/fixtures/ComfyPage.ts
- browser_tests/fixtures/ws.ts
- browser_tests/tests/actionbar.spec.ts
-
src/workbench/extensions/manager/components/manager/skeleton/PackCardGridSkeleton.test.ts
- src/lib/litegraph/src/subgraph/subgraphUtils.test.ts
- src/components/rightSidePanel/shared.test.ts
- src/platform/cloud/subscription/composables/useSubscription.test.ts
-
src/platform/workflow/persistence/composables/useWorkflowPersistence.test.ts

**Scripts & Types:**
- scripts/i18n-types.ts (new shared module)
- scripts/diff-i18n.ts
- scripts/check-unused-i18n-keys.ts
- src/workbench/extensions/manager/types/conflictDetectionTypes.ts
- src/types/algoliaTypes.ts
- src/types/simplifiedWidget.ts

**Infrastructure:**
- src/lib/litegraph/src/litegraph.ts (added GraphOrSubgraph export)
- src/lib/litegraph/src/infrastructure/CustomEventTarget.ts
- src/platform/assets/services/assetService.ts

**Stories:**
- apps/desktop-ui/src/views/InstallView.stories.ts
- src/components/queue/job/JobDetailsPopover.stories.ts

**Extension Manager:**
- src/workbench/extensions/manager/composables/useConflictDetection.ts
- src/workbench/extensions/manager/composables/useManagerQueue.ts
- src/workbench/extensions/manager/services/comfyManagerService.ts
- src/workbench/extensions/manager/utils/conflictMessageUtil.ts

### Testing

- [x] All TypeScript type checking passes (`pnpm typecheck`)
- [x] ESLint passes without errors (`pnpm lint`)
- [x] Format checks pass (`pnpm format:check`)
- [x] Knip (unused exports) passes (`pnpm knip`)
- [x] Pre-commit and pre-push hooks pass

Part of the "Road to No Explicit Any" initiative.

### 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
- Part 8 Group 8: #8496
- Part 9: #8498
- Part 10: #8499

---------

Co-authored-by: Comfy Org PR Bot <snomiao+comfy-pr@gmail.com>
Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-02-05 16:29:28 -08:00

425 lines
11 KiB
TypeScript

// eslint-disable-next-line storybook/no-renderer-packages
import type { Meta, StoryObj } from '@storybook/vue3'
import type { ElectronAPI } from '@comfyorg/comfyui-electron-types'
import { nextTick, provide } from 'vue'
import type { ElectronWindow } from '@/utils/envUtil'
import { createMemoryHistory, createRouter } from 'vue-router'
import InstallView from './InstallView.vue'
// Create a mock router for stories
const createMockRouter = () =>
createRouter({
history: createMemoryHistory(),
routes: [
{ path: '/', component: { template: '<div>Home</div>' } },
{
path: '/server-start',
component: { template: '<div>Server Start</div>' }
},
{
path: '/manual-configuration',
component: { template: '<div>Manual Configuration</div>' }
}
]
})
const meta: Meta<typeof InstallView> = {
title: 'Desktop/Views/InstallView',
component: InstallView,
parameters: {
layout: 'fullscreen',
backgrounds: {
default: 'dark',
values: [
{ name: 'dark', value: '#0a0a0a' },
{ name: 'neutral-900', value: '#171717' },
{ name: 'neutral-950', value: '#0a0a0a' }
]
}
},
decorators: [
(story) => {
// Create router for this story
const router = createMockRouter()
// Mock electron API
;(window as ElectronWindow).electronAPI = {
getPlatform: () => 'darwin',
Config: {
getDetectedGpu: () => Promise.resolve('mps')
},
Events: {
trackEvent: (
_eventName: string,
_data?: Record<string, unknown>
) => {}
},
installComfyUI: (
_options: Parameters<ElectronAPI['installComfyUI']>[0]
) => {},
changeTheme: (_theme: Parameters<ElectronAPI['changeTheme']>[0]) => {},
getSystemPaths: () =>
Promise.resolve({
defaultInstallPath: '/Users/username/ComfyUI'
}),
validateInstallPath: () =>
Promise.resolve({
isValid: true,
exists: false,
canWrite: true,
freeSpace: 100000000000,
requiredSpace: 10000000000,
isNonDefaultDrive: false
}),
validateComfyUISource: () =>
Promise.resolve({
isValid: true
}),
showDirectoryPicker: () => Promise.resolve('/Users/username/ComfyUI')
}
return {
setup() {
// Provide router for all child components
provide('router', router)
return {
story
}
},
template: '<div style="width: 100vw; height: 100vh;"><story /></div>'
}
}
]
}
export default meta
type Story = StoryObj<typeof meta>
// Default story - start at GPU selection
export const GpuSelection: Story = {
render: () => ({
components: { InstallView },
setup() {
// The component will automatically start at step 1
return {}
},
template: '<InstallView />'
})
}
// Story showing the install location step
export const InstallLocation: Story = {
render: () => ({
components: { InstallView },
setup() {
return {}
},
async mounted() {
// Wait for component to be fully mounted
await nextTick()
// Select Apple Metal option to enable navigation
const hardwareOptions = this.$el.querySelectorAll(
'.p-selectbutton-option'
)
if (hardwareOptions.length > 0) {
hardwareOptions[0].click() // Click Apple Metal (first option)
}
await nextTick()
// Click Next to go to step 2
const buttons = Array.from(
this.$el.querySelectorAll('button')
) as HTMLButtonElement[]
const nextBtn = buttons.find((btn) => btn.textContent?.includes('Next'))
if (nextBtn) {
nextBtn.click()
}
},
template: '<InstallView />'
})
}
// Story showing the migration step (currently empty)
export const MigrationStep: Story = {
render: () => ({
components: { InstallView },
setup() {
return {}
},
async mounted() {
// Wait for component to be fully mounted
await nextTick()
// Select Apple Metal option to enable navigation
const hardwareOptions = this.$el.querySelectorAll(
'.p-selectbutton-option'
)
if (hardwareOptions.length > 0) {
hardwareOptions[0].click() // Click Apple Metal (first option)
}
await nextTick()
// Click Next to go to step 2
const buttons1 = Array.from(
this.$el.querySelectorAll('button')
) as HTMLButtonElement[]
const nextBtn1 = buttons1.find((btn) => btn.textContent?.includes('Next'))
if (nextBtn1) {
nextBtn1.click()
}
await nextTick()
// Click Next again to go to step 3
const buttons2 = Array.from(
this.$el.querySelectorAll('button')
) as HTMLButtonElement[]
const nextBtn2 = buttons2.find((btn) => btn.textContent?.includes('Next'))
if (nextBtn2) {
nextBtn2.click()
}
},
template: '<InstallView />'
})
}
// Story showing the desktop settings configuration
export const DesktopSettings: Story = {
render: () => ({
components: { InstallView },
setup() {
return {}
},
async mounted() {
// Wait for component to be fully mounted
await nextTick()
// Select Apple Metal option to enable navigation
const hardwareOptions = this.$el.querySelectorAll(
'.p-selectbutton-option'
)
if (hardwareOptions.length > 0) {
hardwareOptions[0].click() // Click Apple Metal (first option)
}
await nextTick()
// Click Next to go to step 2
const buttons1 = Array.from(
this.$el.querySelectorAll('button')
) as HTMLButtonElement[]
const nextBtn1 = buttons1.find((btn) => btn.textContent?.includes('Next'))
if (nextBtn1) {
nextBtn1.click()
}
await nextTick()
// Click Next again to go to step 3
const buttons2 = Array.from(
this.$el.querySelectorAll('button')
) as HTMLButtonElement[]
const nextBtn2 = buttons2.find((btn) => btn.textContent?.includes('Next'))
if (nextBtn2) {
nextBtn2.click()
}
await nextTick()
// Click Next again to go to step 4
const buttons3 = Array.from(
this.$el.querySelectorAll('button')
) as HTMLButtonElement[]
const nextBtn3 = buttons3.find((btn) => btn.textContent?.includes('Next'))
if (nextBtn3) {
nextBtn3.click()
}
},
template: '<InstallView />'
})
}
// Story with Windows platform (no Apple Metal option)
export const WindowsPlatform: Story = {
render: () => {
// Override the platform to Windows
;(window as ElectronWindow).electronAPI.getPlatform = () => 'win32'
;(window as ElectronWindow).electronAPI.Config.getDetectedGpu = () =>
Promise.resolve('nvidia')
return {
components: { InstallView },
setup() {
return {}
},
template: '<InstallView />'
}
}
}
// Story with macOS platform (Apple Metal option)
export const MacOSPlatform: Story = {
name: 'macOS Platform',
render: () => {
// Override the platform to macOS
;(window as ElectronWindow).electronAPI.getPlatform = () => 'darwin'
;(window as ElectronWindow).electronAPI.Config.getDetectedGpu = () =>
Promise.resolve('mps')
return {
components: { InstallView },
setup() {
return {}
},
template: '<InstallView />'
}
}
}
// Story with CPU selected
export const CpuSelected: Story = {
render: () => ({
components: { InstallView },
setup() {
return {}
},
async mounted() {
// Wait for component to be fully mounted
await nextTick()
// Find and click the CPU hardware option
const hardwareButtons = this.$el.querySelectorAll('.hardware-option')
// CPU is the button with "CPU" text
for (const button of hardwareButtons) {
if (button.textContent?.includes('CPU')) {
button.click()
break
}
}
},
template: '<InstallView />'
})
}
// Story with manual install selected
export const ManualInstall: Story = {
render: () => ({
components: { InstallView },
setup() {
return {}
},
async mounted() {
// Wait for component to be fully mounted
await nextTick()
// Find and click the Manual Install hardware option
const hardwareButtons = this.$el.querySelectorAll('.hardware-option')
// Manual Install is the button with "Manual Install" text
for (const button of hardwareButtons) {
if (button.textContent?.includes('Manual Install')) {
button.click()
break
}
}
},
template: '<InstallView />'
})
}
// Story with error state (invalid install path)
export const ErrorState: Story = {
render: () => {
// Override validation to return an error
;(window as ElectronWindow).electronAPI.validateInstallPath = () =>
Promise.resolve({
isValid: false,
exists: false,
canWrite: false,
freeSpace: 100000000000,
requiredSpace: 10000000000,
isNonDefaultDrive: false,
error: 'Story mock: Example error state'
})
return {
components: { InstallView },
setup() {
return {}
},
async mounted() {
// Wait for component to be fully mounted
await nextTick()
// Select Apple Metal option to enable navigation
const hardwareOptions = this.$el.querySelectorAll(
'.p-selectbutton-option'
)
if (hardwareOptions.length > 0) {
hardwareOptions[0].click() // Click Apple Metal (first option)
}
await nextTick()
// Click Next to go to step 2 where error will be shown
const buttons = Array.from(
this.$el.querySelectorAll('button')
) as HTMLButtonElement[]
const nextBtn = buttons.find((btn) => btn.textContent?.includes('Next'))
if (nextBtn) {
nextBtn.click()
}
},
template: '<InstallView />'
}
}
}
// Story with warning state (non-default drive)
export const WarningState: Story = {
render: () => {
// Override validation to return a warning about non-default drive
;(window as ElectronWindow).electronAPI.validateInstallPath = () =>
Promise.resolve({
isValid: true,
exists: false,
canWrite: true,
freeSpace: 500_000_000_000,
requiredSpace: 10_000_000_000,
isNonDefaultDrive: true
})
return {
components: { InstallView },
setup() {
return {}
},
async mounted() {
// Wait for component to be fully mounted
await nextTick()
// Select Apple Metal option to enable navigation
const hardwareOptions = this.$el.querySelectorAll('.hardware-option')
if (hardwareOptions.length > 0) {
hardwareOptions[0].click() // Click Apple Metal (first option)
}
await nextTick()
// Click Next to go to step 2 where warning will be shown
const buttons = Array.from(
this.$el.querySelectorAll('button')
) as HTMLButtonElement[]
const nextBtn = buttons.find((btn) => btn.textContent?.includes('Next'))
if (nextBtn) {
nextBtn.click()
}
},
template: '<InstallView />'
}
}
}