From 941cd2b4a534f2c13a46da92340951bf78756844 Mon Sep 17 00:00:00 2001 From: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com> Date: Fri, 23 Jan 2026 02:35:09 +0100 Subject: [PATCH] refactor: improve TypeScript patterns in test files (Group 1/8) (#8253) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Improves type safety in test files by replacing unsafe type patterns with proper TypeScript idioms. ## Changes - Define typed `TestWindow` interface extending `Window` for Playwright tests with custom properties - Use `Partial` with single type assertion for DOM element mocks - Remove redundant type imports - Fix `console.log` → `console.warn` in test fixture ## Files Changed 16 test files across browser_tests, packages, and src/components ## Test Plan - ✅ `pnpm typecheck` passes - ✅ No new `any` types introduced - ✅ All pre-commit hooks pass ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8253-refactor-improve-TypeScript-patterns-in-test-files-Group-1-8-2f16d73d365081548f9ece7bcf0525ee) by [Unito](https://www.unito.io) --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../fixtures/utils/litegraphUtils.ts | 3 +- browser_tests/tests/featureFlags.spec.ts | 94 +++++++++++-------- browser_tests/tests/nodeHelp.spec.ts | 8 +- .../tests/selectionToolboxSubmenus.spec.ts | 3 +- .../links/linkInteraction.spec.ts | 4 +- .../src/formatUtil.test.ts | 4 +- .../shared-frontend-utils/src/formatUtil.ts | 4 +- src/base/common/async.ts | 5 +- .../tabs/terminal/BaseTerminal.test.ts | 5 +- src/components/common/FormRadioGroup.test.ts | 24 +++-- src/components/common/FormRadioGroup.vue | 2 +- src/components/common/UrlInput.test.ts | 23 +++-- src/components/common/UserAvatar.test.ts | 4 +- .../content/setting/SettingItem.test.ts | 11 ++- .../content/setting/UsageLogsTable.test.ts | 3 +- .../dialog/content/signin/ApiKeyForm.test.ts | 4 +- .../dialog/content/signin/SignInForm.test.ts | 43 ++++++--- src/scripts/app.ts | 2 +- src/types/index.ts | 17 ++++ 19 files changed, 167 insertions(+), 96 deletions(-) diff --git a/browser_tests/fixtures/utils/litegraphUtils.ts b/browser_tests/fixtures/utils/litegraphUtils.ts index 30b0da08e..ea5a0b78f 100644 --- a/browser_tests/fixtures/utils/litegraphUtils.ts +++ b/browser_tests/fixtures/utils/litegraphUtils.ts @@ -119,8 +119,7 @@ class NodeSlotReference { window['app'].canvas.ds.convertOffsetToCanvas(rawPos) // Debug logging - convert Float64Arrays to regular arrays for visibility - // eslint-disable-next-line no-console - console.log( + console.warn( `NodeSlotReference debug for ${type} slot ${index} on node ${id}:`, { nodePos: [node.pos[0], node.pos[1]], diff --git a/browser_tests/tests/featureFlags.spec.ts b/browser_tests/tests/featureFlags.spec.ts index 707976829..9155ae910 100644 --- a/browser_tests/tests/featureFlags.spec.ts +++ b/browser_tests/tests/featureFlags.spec.ts @@ -27,7 +27,7 @@ test.describe('Feature Flags', () => { try { const parsed = JSON.parse(data) if (parsed.type === 'feature_flags') { - window.__capturedMessages.clientFeatureFlags = parsed + window.__capturedMessages!.clientFeatureFlags = parsed } } catch (e) { // Not JSON, ignore @@ -41,7 +41,7 @@ test.describe('Feature Flags', () => { window['app']?.api?.serverFeatureFlags && Object.keys(window['app'].api.serverFeatureFlags).length > 0 ) { - window.__capturedMessages.serverFeatureFlags = + window.__capturedMessages!.serverFeatureFlags = window['app'].api.serverFeatureFlags clearInterval(checkInterval) } @@ -57,8 +57,8 @@ test.describe('Feature Flags', () => { // Wait for both client and server feature flags await newPage.waitForFunction( () => - window.__capturedMessages.clientFeatureFlags !== null && - window.__capturedMessages.serverFeatureFlags !== null, + window.__capturedMessages!.clientFeatureFlags !== null && + window.__capturedMessages!.serverFeatureFlags !== null, { timeout: 10000 } ) @@ -66,27 +66,27 @@ test.describe('Feature Flags', () => { const messages = await newPage.evaluate(() => window.__capturedMessages) // Verify client sent feature flags - expect(messages.clientFeatureFlags).toBeTruthy() - expect(messages.clientFeatureFlags).toHaveProperty('type', 'feature_flags') - expect(messages.clientFeatureFlags).toHaveProperty('data') - expect(messages.clientFeatureFlags.data).toHaveProperty( + expect(messages!.clientFeatureFlags).toBeTruthy() + expect(messages!.clientFeatureFlags).toHaveProperty('type', 'feature_flags') + expect(messages!.clientFeatureFlags).toHaveProperty('data') + expect(messages!.clientFeatureFlags!.data).toHaveProperty( 'supports_preview_metadata' ) expect( - typeof messages.clientFeatureFlags.data.supports_preview_metadata + typeof messages!.clientFeatureFlags!.data.supports_preview_metadata ).toBe('boolean') // Verify server sent feature flags back - expect(messages.serverFeatureFlags).toBeTruthy() - expect(messages.serverFeatureFlags).toHaveProperty( + expect(messages!.serverFeatureFlags).toBeTruthy() + expect(messages!.serverFeatureFlags).toHaveProperty( 'supports_preview_metadata' ) - expect(typeof messages.serverFeatureFlags.supports_preview_metadata).toBe( + expect(typeof messages!.serverFeatureFlags!.supports_preview_metadata).toBe( 'boolean' ) - expect(messages.serverFeatureFlags).toHaveProperty('max_upload_size') - expect(typeof messages.serverFeatureFlags.max_upload_size).toBe('number') - expect(Object.keys(messages.serverFeatureFlags).length).toBeGreaterThan(0) + expect(messages!.serverFeatureFlags).toHaveProperty('max_upload_size') + expect(typeof messages!.serverFeatureFlags!.max_upload_size).toBe('number') + expect(Object.keys(messages!.serverFeatureFlags!).length).toBeGreaterThan(0) await newPage.close() }) @@ -96,7 +96,7 @@ test.describe('Feature Flags', () => { }) => { // Get the actual server feature flags from the backend const serverFlags = await comfyPage.page.evaluate(() => { - return window['app'].api.serverFeatureFlags + return window['app']!.api.serverFeatureFlags }) // Verify we received real feature flags from the backend @@ -115,7 +115,7 @@ test.describe('Feature Flags', () => { }) => { // Test serverSupportsFeature with real backend flags const supportsPreviewMetadata = await comfyPage.page.evaluate(() => { - return window['app'].api.serverSupportsFeature( + return window['app']!.api.serverSupportsFeature( 'supports_preview_metadata' ) }) @@ -124,15 +124,17 @@ test.describe('Feature Flags', () => { // Test non-existent feature - should always return false const supportsNonExistent = await comfyPage.page.evaluate(() => { - return window['app'].api.serverSupportsFeature('non_existent_feature_xyz') + return window['app']!.api.serverSupportsFeature( + 'non_existent_feature_xyz' + ) }) expect(supportsNonExistent).toBe(false) // Test that the method only returns true for boolean true values const testResults = await comfyPage.page.evaluate(() => { // Temporarily modify serverFeatureFlags to test behavior - const original = window['app'].api.serverFeatureFlags - window['app'].api.serverFeatureFlags = { + const original = window['app']!.api.serverFeatureFlags + window['app']!.api.serverFeatureFlags = { bool_true: true, bool_false: false, string_value: 'yes', @@ -141,15 +143,15 @@ test.describe('Feature Flags', () => { } const results = { - bool_true: window['app'].api.serverSupportsFeature('bool_true'), - bool_false: window['app'].api.serverSupportsFeature('bool_false'), - string_value: window['app'].api.serverSupportsFeature('string_value'), - number_value: window['app'].api.serverSupportsFeature('number_value'), - null_value: window['app'].api.serverSupportsFeature('null_value') + bool_true: window['app']!.api.serverSupportsFeature('bool_true'), + bool_false: window['app']!.api.serverSupportsFeature('bool_false'), + string_value: window['app']!.api.serverSupportsFeature('string_value'), + number_value: window['app']!.api.serverSupportsFeature('number_value'), + null_value: window['app']!.api.serverSupportsFeature('null_value') } // Restore original - window['app'].api.serverFeatureFlags = original + window['app']!.api.serverFeatureFlags = original return results }) @@ -166,20 +168,20 @@ test.describe('Feature Flags', () => { }) => { // Test getServerFeature method const previewMetadataValue = await comfyPage.page.evaluate(() => { - return window['app'].api.getServerFeature('supports_preview_metadata') + return window['app']!.api.getServerFeature('supports_preview_metadata') }) expect(typeof previewMetadataValue).toBe('boolean') // Test getting max_upload_size const maxUploadSize = await comfyPage.page.evaluate(() => { - return window['app'].api.getServerFeature('max_upload_size') + return window['app']!.api.getServerFeature('max_upload_size') }) expect(typeof maxUploadSize).toBe('number') expect(maxUploadSize).toBeGreaterThan(0) // Test getServerFeature with default value for non-existent feature const defaultValue = await comfyPage.page.evaluate(() => { - return window['app'].api.getServerFeature( + return window['app']!.api.getServerFeature( 'non_existent_feature_xyz', 'default' ) @@ -192,7 +194,7 @@ test.describe('Feature Flags', () => { }) => { // Test getServerFeatures returns all flags const allFeatures = await comfyPage.page.evaluate(() => { - return window['app'].api.getServerFeatures() + return window['app']!.api.getServerFeatures() }) expect(allFeatures).toBeTruthy() @@ -205,14 +207,14 @@ test.describe('Feature Flags', () => { test('Client feature flags are immutable', async ({ comfyPage }) => { // Test that getClientFeatureFlags returns a copy const immutabilityTest = await comfyPage.page.evaluate(() => { - const flags1 = window['app'].api.getClientFeatureFlags() - const flags2 = window['app'].api.getClientFeatureFlags() + const flags1 = window['app']!.api.getClientFeatureFlags() + const flags2 = window['app']!.api.getClientFeatureFlags() // Modify the first object flags1.test_modification = true // Get flags again to check if original was modified - const flags3 = window['app'].api.getClientFeatureFlags() + const flags3 = window['app']!.api.getClientFeatureFlags() return { areEqual: flags1 === flags2, @@ -238,14 +240,14 @@ test.describe('Feature Flags', () => { }) => { const immutabilityTest = await comfyPage.page.evaluate(() => { // Get a copy of server features - const features1 = window['app'].api.getServerFeatures() + const features1 = window['app']!.api.getServerFeatures() // Try to modify it features1.supports_preview_metadata = false features1.new_feature = 'added' // Get another copy - const features2 = window['app'].api.getServerFeatures() + const features2 = window['app']!.api.getServerFeatures() return { modifiedValue: features1.supports_preview_metadata, @@ -274,7 +276,8 @@ test.describe('Feature Flags', () => { // Set up monitoring before navigation await newPage.addInitScript(() => { // Track when various app components are ready - ;(window as any).__appReadiness = { + + window.__appReadiness = { featureFlagsReceived: false, apiInitialized: false, appInitialized: false @@ -286,7 +289,10 @@ test.describe('Feature Flags', () => { window['app']?.api?.serverFeatureFlags?.supports_preview_metadata !== undefined ) { - ;(window as any).__appReadiness.featureFlagsReceived = true + window.__appReadiness = { + ...window.__appReadiness, + featureFlagsReceived: true + } clearInterval(checkFeatureFlags) } }, 10) @@ -294,7 +300,10 @@ test.describe('Feature Flags', () => { // Monitor API initialization const checkApi = setInterval(() => { if (window['app']?.api) { - ;(window as any).__appReadiness.apiInitialized = true + window.__appReadiness = { + ...window.__appReadiness, + apiInitialized: true + } clearInterval(checkApi) } }, 10) @@ -302,7 +311,10 @@ test.describe('Feature Flags', () => { // Monitor app initialization const checkApp = setInterval(() => { if (window['app']?.graph) { - ;(window as any).__appReadiness.appInitialized = true + window.__appReadiness = { + ...window.__appReadiness, + appInitialized: true + } clearInterval(checkApp) } }, 10) @@ -331,8 +343,8 @@ test.describe('Feature Flags', () => { // Get readiness state const readiness = await newPage.evaluate(() => { return { - ...(window as any).__appReadiness, - currentFlags: window['app'].api.serverFeatureFlags + ...window.__appReadiness, + currentFlags: window['app']!.api.serverFeatureFlags } }) diff --git a/browser_tests/tests/nodeHelp.spec.ts b/browser_tests/tests/nodeHelp.spec.ts index cfb04bc46..709c19a4b 100644 --- a/browser_tests/tests/nodeHelp.spec.ts +++ b/browser_tests/tests/nodeHelp.spec.ts @@ -2,15 +2,17 @@ import { comfyExpect as expect, comfyPageFixture as test } from '../fixtures/ComfyPage' +import type { ComfyPage } from '../fixtures/ComfyPage' import { fitToViewInstant } from '../helpers/fitToView' +import type { NodeReference } from '../fixtures/utils/litegraphUtils' // TODO: there might be a better solution for this // Helper function to pan canvas and select node -async function selectNodeWithPan(comfyPage: any, nodeRef: any) { +async function selectNodeWithPan(comfyPage: ComfyPage, nodeRef: NodeReference) { const nodePos = await nodeRef.getPosition() await comfyPage.page.evaluate((pos) => { - const app = window['app'] + const app = window['app']! const canvas = app.canvas canvas.ds.offset[0] = -pos.x + canvas.canvas.width / 2 canvas.ds.offset[1] = -pos.y + canvas.canvas.height / 2 + 100 @@ -345,7 +347,7 @@ This is documentation for a custom node. // Find and select a custom/group node const nodeRefs = await comfyPage.page.evaluate(() => { - return window['app'].graph.nodes.map((n: any) => n.id) + return window['app']!.graph!.nodes.map((n) => n.id) }) if (nodeRefs.length > 0) { const firstNode = await comfyPage.getNodeRefById(nodeRefs[0]) diff --git a/browser_tests/tests/selectionToolboxSubmenus.spec.ts b/browser_tests/tests/selectionToolboxSubmenus.spec.ts index f526b07e0..fd4c55faf 100644 --- a/browser_tests/tests/selectionToolboxSubmenus.spec.ts +++ b/browser_tests/tests/selectionToolboxSubmenus.spec.ts @@ -1,6 +1,7 @@ import { expect } from '@playwright/test' import { comfyPageFixture as test } from '../fixtures/ComfyPage' +import type { ComfyPage } from '../fixtures/ComfyPage' test.beforeEach(async ({ comfyPage }) => { await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled') @@ -15,7 +16,7 @@ test.describe('Selection Toolbox - More Options Submenus', () => { await comfyPage.nextFrame() }) - const openMoreOptions = async (comfyPage: any) => { + const openMoreOptions = async (comfyPage: ComfyPage) => { const ksamplerNodes = await comfyPage.getNodeRefsByTitle('KSampler') if (ksamplerNodes.length === 0) { throw new Error('No KSampler nodes found') diff --git a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts index 315758f5c..9b007afb2 100644 --- a/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts +++ b/browser_tests/tests/vueNodes/interactions/links/linkInteraction.spec.ts @@ -419,7 +419,7 @@ test.describe('Vue Node Link Interaction', () => { // This avoids relying on an exact path hit-test position. await comfyPage.page.evaluate( ([targetNodeId, targetSlot, clientPoint]) => { - const app = (window as any)['app'] + const app = window['app'] const graph = app?.canvas?.graph ?? app?.graph if (!graph) throw new Error('Graph not available') const node = graph.getNodeById(targetNodeId) @@ -505,7 +505,7 @@ test.describe('Vue Node Link Interaction', () => { // This avoids relying on an exact path hit-test position. await comfyPage.page.evaluate( ([targetNodeId, targetSlot, clientPoint]) => { - const app = (window as any)['app'] + const app = window['app'] const graph = app?.canvas?.graph ?? app?.graph if (!graph) throw new Error('Graph not available') const node = graph.getNodeById(targetNodeId) diff --git a/packages/shared-frontend-utils/src/formatUtil.test.ts b/packages/shared-frontend-utils/src/formatUtil.test.ts index ac15a78b1..75d8b8db2 100644 --- a/packages/shared-frontend-utils/src/formatUtil.test.ts +++ b/packages/shared-frontend-utils/src/formatUtil.test.ts @@ -120,8 +120,8 @@ describe('formatUtil', () => { }) it('should handle null and undefined gracefully', () => { - expect(getMediaTypeFromFilename(null as any)).toBe('image') - expect(getMediaTypeFromFilename(undefined as any)).toBe('image') + expect(getMediaTypeFromFilename(null)).toBe('image') + expect(getMediaTypeFromFilename(undefined)).toBe('image') }) it('should handle special characters in filenames', () => { diff --git a/packages/shared-frontend-utils/src/formatUtil.ts b/packages/shared-frontend-utils/src/formatUtil.ts index 032d1c9ed..fde399eb9 100644 --- a/packages/shared-frontend-utils/src/formatUtil.ts +++ b/packages/shared-frontend-utils/src/formatUtil.ts @@ -537,7 +537,9 @@ export function truncateFilename( * @param filename The filename to analyze * @returns The media type: 'image', 'video', 'audio', or '3D' */ -export function getMediaTypeFromFilename(filename: string): MediaType { +export function getMediaTypeFromFilename( + filename: string | null | undefined +): MediaType { if (!filename) return 'image' const ext = filename.split('.').pop()?.toLowerCase() if (!ext) return 'image' diff --git a/src/base/common/async.ts b/src/base/common/async.ts index a97f6f1bd..d5aa30679 100644 --- a/src/base/common/async.ts +++ b/src/base/common/async.ts @@ -14,6 +14,7 @@ interface IdleDeadline { interface IDisposable { dispose(): void } +type GlobalWindow = typeof globalThis /** * Internal implementation function that handles the actual scheduling logic. @@ -21,7 +22,7 @@ interface IDisposable { * or fall back to setTimeout-based implementation. */ let _runWhenIdle: ( - targetWindow: any, + targetWindow: GlobalWindow, callback: (idle: IdleDeadline) => void, timeout?: number ) => IDisposable @@ -37,7 +38,7 @@ export let runWhenGlobalIdle: ( // Self-invoking function to set up the idle callback implementation ;(function () { - const safeGlobal: any = globalThis + const safeGlobal: GlobalWindow = globalThis as GlobalWindow if ( typeof safeGlobal.requestIdleCallback !== 'function' || diff --git a/src/components/bottomPanel/tabs/terminal/BaseTerminal.test.ts b/src/components/bottomPanel/tabs/terminal/BaseTerminal.test.ts index faa20f654..b99e54ea1 100644 --- a/src/components/bottomPanel/tabs/terminal/BaseTerminal.test.ts +++ b/src/components/bottomPanel/tabs/terminal/BaseTerminal.test.ts @@ -1,6 +1,7 @@ import { createTestingPinia } from '@pinia/testing' import type { VueWrapper } from '@vue/test-utils' import { mount } from '@vue/test-utils' +import type { Mock } from 'vitest' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { nextTick } from 'vue' import { createI18n } from 'vue-i18n' @@ -151,8 +152,8 @@ describe('BaseTerminal', () => { // Trigger the selection change callback that was registered during mount expect(mockTerminal.onSelectionChange).toHaveBeenCalled() // Access the mock calls - TypeScript can't infer the mock structure dynamically - const selectionCallback = (mockTerminal.onSelectionChange as any).mock - .calls[0][0] + const mockCalls = (mockTerminal.onSelectionChange as Mock).mock.calls + const selectionCallback = mockCalls[0][0] as () => void selectionCallback() await nextTick() diff --git a/src/components/common/FormRadioGroup.test.ts b/src/components/common/FormRadioGroup.test.ts index a014b8ecd..2ec26e9af 100644 --- a/src/components/common/FormRadioGroup.test.ts +++ b/src/components/common/FormRadioGroup.test.ts @@ -7,6 +7,7 @@ import { createApp } from 'vue' import type { SettingOption } from '@/platform/settings/types' import FormRadioGroup from './FormRadioGroup.vue' +import type { ComponentProps } from 'vue-component-type-helpers' describe('FormRadioGroup', () => { beforeAll(() => { @@ -14,7 +15,8 @@ describe('FormRadioGroup', () => { app.use(PrimeVue) }) - const mountComponent = (props: any, options = {}) => { + type FormRadioGroupProps = ComponentProps + const mountComponent = (props: FormRadioGroupProps, options = {}) => { return mount(FormRadioGroup, { global: { plugins: [PrimeVue], @@ -92,9 +94,9 @@ describe('FormRadioGroup', () => { it('handles custom object with optionLabel and optionValue', () => { const options = [ - { name: 'First Option', id: 1 }, - { name: 'Second Option', id: 2 }, - { name: 'Third Option', id: 3 } + { name: 'First Option', id: '1' }, + { name: 'Second Option', id: '2' }, + { name: 'Third Option', id: '3' } ] const wrapper = mountComponent({ @@ -108,9 +110,9 @@ describe('FormRadioGroup', () => { const radioButtons = wrapper.findAllComponents(RadioButton) expect(radioButtons).toHaveLength(3) - expect(radioButtons[0].props('value')).toBe(1) - expect(radioButtons[1].props('value')).toBe(2) - expect(radioButtons[2].props('value')).toBe(3) + expect(radioButtons[0].props('value')).toBe('1') + expect(radioButtons[1].props('value')).toBe('2') + expect(radioButtons[2].props('value')).toBe('3') const labels = wrapper.findAll('label') expect(labels[0].text()).toBe('First Option') @@ -167,10 +169,7 @@ describe('FormRadioGroup', () => { }) it('handles object with missing properties gracefully', () => { - const options = [ - { label: 'Option 1', val: 'opt1' }, - { text: 'Option 2', value: 'opt2' } - ] + const options = [{ label: 'Option 1', val: 'opt1' }] const wrapper = mountComponent({ modelValue: 'opt1', @@ -179,11 +178,10 @@ describe('FormRadioGroup', () => { }) const radioButtons = wrapper.findAllComponents(RadioButton) - expect(radioButtons).toHaveLength(2) + expect(radioButtons).toHaveLength(1) const labels = wrapper.findAll('label') expect(labels[0].text()).toBe('Unknown') - expect(labels[1].text()).toBe('Option 2') }) }) diff --git a/src/components/common/FormRadioGroup.vue b/src/components/common/FormRadioGroup.vue index 19889132b..6155bed0f 100644 --- a/src/components/common/FormRadioGroup.vue +++ b/src/components/common/FormRadioGroup.vue @@ -28,7 +28,7 @@ import type { SettingOption } from '@/platform/settings/types' const props = defineProps<{ modelValue: any - options: (SettingOption | string)[] + options?: (string | SettingOption | Record)[] optionLabel?: string optionValue?: string id?: string diff --git a/src/components/common/UrlInput.test.ts b/src/components/common/UrlInput.test.ts index e3fc81d29..9c34c11c5 100644 --- a/src/components/common/UrlInput.test.ts +++ b/src/components/common/UrlInput.test.ts @@ -7,6 +7,7 @@ import { beforeEach, describe, expect, it } from 'vitest' import { createApp, nextTick } from 'vue' import UrlInput from './UrlInput.vue' +import type { ComponentProps } from 'vue-component-type-helpers' describe('UrlInput', () => { beforeEach(() => { @@ -14,7 +15,13 @@ describe('UrlInput', () => { app.use(PrimeVue) }) - const mountComponent = (props: any, options = {}) => { + const mountComponent = ( + props: ComponentProps & { + placeholder?: string + disabled?: boolean + }, + options = {} + ) => { return mount(UrlInput, { global: { plugins: [PrimeVue], @@ -169,25 +176,25 @@ describe('UrlInput', () => { await input.setValue(' https://leading-space.com') await input.trigger('input') await nextTick() - expect(wrapper.vm.internalValue).toBe('https://leading-space.com') + expect(input.element.value).toBe('https://leading-space.com') // Test trailing whitespace await input.setValue('https://trailing-space.com ') await input.trigger('input') await nextTick() - expect(wrapper.vm.internalValue).toBe('https://trailing-space.com') + expect(input.element.value).toBe('https://trailing-space.com') // Test both leading and trailing whitespace await input.setValue(' https://both-spaces.com ') await input.trigger('input') await nextTick() - expect(wrapper.vm.internalValue).toBe('https://both-spaces.com') + expect(input.element.value).toBe('https://both-spaces.com') // Test whitespace in the middle of the URL await input.setValue('https:// middle-space.com') await input.trigger('input') await nextTick() - expect(wrapper.vm.internalValue).toBe('https://middle-space.com') + expect(input.element.value).toBe('https://middle-space.com') }) it('trims whitespace when value set externally', async () => { @@ -196,15 +203,17 @@ describe('UrlInput', () => { placeholder: 'Enter URL' }) + const input = wrapper.find('input') + // Check initial value is trimmed - expect(wrapper.vm.internalValue).toBe('https://initial-value.com') + expect(input.element.value).toBe('https://initial-value.com') // Update props with whitespace await wrapper.setProps({ modelValue: ' https://updated-value.com ' }) await nextTick() // Check updated value is trimmed - expect(wrapper.vm.internalValue).toBe('https://updated-value.com') + expect(input.element.value).toBe('https://updated-value.com') }) }) }) diff --git a/src/components/common/UserAvatar.test.ts b/src/components/common/UserAvatar.test.ts index 0c6b26e4e..0b5df7052 100644 --- a/src/components/common/UserAvatar.test.ts +++ b/src/components/common/UserAvatar.test.ts @@ -1,3 +1,5 @@ +import type { ComponentProps } from 'vue-component-type-helpers' + import { mount } from '@vue/test-utils' import Avatar from 'primevue/avatar' import PrimeVue from 'primevue/config' @@ -27,7 +29,7 @@ describe('UserAvatar', () => { app.use(PrimeVue) }) - const mountComponent = (props: any = {}) => { + const mountComponent = (props: ComponentProps = {}) => { return mount(UserAvatar, { global: { plugins: [PrimeVue, i18n], diff --git a/src/components/dialog/content/setting/SettingItem.test.ts b/src/components/dialog/content/setting/SettingItem.test.ts index 17e3bab06..673fe5894 100644 --- a/src/components/dialog/content/setting/SettingItem.test.ts +++ b/src/components/dialog/content/setting/SettingItem.test.ts @@ -18,7 +18,7 @@ vi.mock('@/utils/formatUtil', () => ({ })) describe('SettingItem', () => { - const mountComponent = (props: any, options = {}): any => { + const mountComponent = (props: Record, options = {}) => { return mount(SettingItem, { global: { plugins: [PrimeVue, i18n, createPinia()], @@ -32,6 +32,7 @@ describe('SettingItem', () => { 'i-material-symbols:experiment-outline': true } }, + // @ts-expect-error - Test utility accepts flexible props for testing edge cases props, ...options }) @@ -48,8 +49,9 @@ describe('SettingItem', () => { } }) - // Get the options property of the FormItem - const options = wrapper.vm.formItem.options + // Check the FormItem component's item prop for the options + const formItem = wrapper.findComponent({ name: 'FormItem' }) + const options = formItem.props('item').options expect(options).toEqual([ { text: 'Correctly Translated', value: 'Correctly Translated' } ]) @@ -67,7 +69,8 @@ describe('SettingItem', () => { }) // Should not throw an error and tooltip should be preserved as-is - expect(wrapper.vm.formItem.tooltip).toBe( + const formItem = wrapper.findComponent({ name: 'FormItem' }) + expect(formItem.props('item').tooltip).toBe( 'This will load a larger version of @mtb/markdown-parser that bundles shiki' ) }) diff --git a/src/components/dialog/content/setting/UsageLogsTable.test.ts b/src/components/dialog/content/setting/UsageLogsTable.test.ts index b98664668..72a4fbdba 100644 --- a/src/components/dialog/content/setting/UsageLogsTable.test.ts +++ b/src/components/dialog/content/setting/UsageLogsTable.test.ts @@ -12,6 +12,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { nextTick } from 'vue' import { createI18n } from 'vue-i18n' +import type { AuditLog } from '@/services/customerEventsService' import { EventType } from '@/services/customerEventsService' import UsageLogsTable from './UsageLogsTable.vue' @@ -19,7 +20,7 @@ import UsageLogsTable from './UsageLogsTable.vue' type ComponentInstance = InstanceType & { loading: boolean error: string | null - events: any[] + events: Partial[] pagination: { page: number limit: number diff --git a/src/components/dialog/content/signin/ApiKeyForm.test.ts b/src/components/dialog/content/signin/ApiKeyForm.test.ts index 5d6a726b8..4d073cb08 100644 --- a/src/components/dialog/content/signin/ApiKeyForm.test.ts +++ b/src/components/dialog/content/signin/ApiKeyForm.test.ts @@ -1,3 +1,5 @@ +import type { ComponentProps } from 'vue-component-type-helpers' + import { Form } from '@primevue/forms' import { mount } from '@vue/test-utils' import { createPinia } from 'pinia' @@ -63,7 +65,7 @@ describe('ApiKeyForm', () => { mockLoading.mockReset() }) - const mountComponent = (props: any = {}) => { + const mountComponent = (props: ComponentProps = {}) => { return mount(ApiKeyForm, { global: { plugins: [PrimeVue, createPinia(), i18n], diff --git a/src/components/dialog/content/signin/SignInForm.test.ts b/src/components/dialog/content/signin/SignInForm.test.ts index da898532a..c27d15929 100644 --- a/src/components/dialog/content/signin/SignInForm.test.ts +++ b/src/components/dialog/content/signin/SignInForm.test.ts @@ -112,8 +112,10 @@ describe('SignInForm', () => { // Mock getElementById to track focus const mockFocus = vi.fn() - const mockElement = { focus: mockFocus } - vi.spyOn(document, 'getElementById').mockReturnValue(mockElement as any) + const mockElement: Partial = { focus: mockFocus } + vi.spyOn(document, 'getElementById').mockReturnValue( + mockElement as HTMLElement + ) // Click forgot password link while email is empty await forgotPasswordSpan.trigger('click') @@ -138,7 +140,10 @@ describe('SignInForm', () => { it('calls handleForgotPassword with email when link is clicked', async () => { const wrapper = mountComponent() - const component = wrapper.vm as any + const component = wrapper.vm as typeof wrapper.vm & { + handleForgotPassword: (email: string, valid: boolean) => void + onSubmit: (data: { valid: boolean; values: unknown }) => void + } // Spy on handleForgotPassword const handleForgotPasswordSpy = vi.spyOn( @@ -161,7 +166,10 @@ describe('SignInForm', () => { describe('Form Submission', () => { it('emits submit event when onSubmit is called with valid data', async () => { const wrapper = mountComponent() - const component = wrapper.vm as any + const component = wrapper.vm as typeof wrapper.vm & { + handleForgotPassword: (email: string, valid: boolean) => void + onSubmit: (data: { valid: boolean; values: unknown }) => void + } // Call onSubmit directly with valid data component.onSubmit({ @@ -181,7 +189,10 @@ describe('SignInForm', () => { it('does not emit submit event when form is invalid', async () => { const wrapper = mountComponent() - const component = wrapper.vm as any + const component = wrapper.vm as typeof wrapper.vm & { + handleForgotPassword: (email: string, valid: boolean) => void + onSubmit: (data: { valid: boolean; values: unknown }) => void + } // Call onSubmit with invalid form component.onSubmit({ valid: false, values: {} }) @@ -254,12 +265,17 @@ describe('SignInForm', () => { describe('Focus Behavior', () => { it('focuses email input when handleForgotPassword is called with invalid email', async () => { const wrapper = mountComponent() - const component = wrapper.vm as any + const component = wrapper.vm as typeof wrapper.vm & { + handleForgotPassword: (email: string, valid: boolean) => void + onSubmit: (data: { valid: boolean; values: unknown }) => void + } // Mock getElementById to track focus const mockFocus = vi.fn() - const mockElement = { focus: mockFocus } - vi.spyOn(document, 'getElementById').mockReturnValue(mockElement as any) + const mockElement: Partial = { focus: mockFocus } + vi.spyOn(document, 'getElementById').mockReturnValue( + mockElement as HTMLElement + ) // Call handleForgotPassword with no email await component.handleForgotPassword('', false) @@ -273,12 +289,17 @@ describe('SignInForm', () => { it('does not focus email input when valid email is provided', async () => { const wrapper = mountComponent() - const component = wrapper.vm as any + const component = wrapper.vm as typeof wrapper.vm & { + handleForgotPassword: (email: string, valid: boolean) => void + onSubmit: (data: { valid: boolean; values: unknown }) => void + } // Mock getElementById const mockFocus = vi.fn() - const mockElement = { focus: mockFocus } - vi.spyOn(document, 'getElementById').mockReturnValue(mockElement as any) + const mockElement: Partial = { focus: mockFocus } + vi.spyOn(document, 'getElementById').mockReturnValue( + mockElement as HTMLElement + ) // Call handleForgotPassword with valid email await component.handleForgotPassword('test@example.com', true) diff --git a/src/scripts/app.ts b/src/scripts/app.ts index a9427f85b..0a794ab9d 100644 --- a/src/scripts/app.ts +++ b/src/scripts/app.ts @@ -162,7 +162,7 @@ export class ComfyApp { // TODO: Migrate internal usage to the /** @deprecated Use {@link rootGraph} instead */ - get graph(): unknown { + get graph(): LGraph | undefined { return this.rootGraphInternal! } diff --git a/src/types/index.ts b/src/types/index.ts index 25f8e2090..947c9b3ad 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -64,6 +64,17 @@ export type { ToastMessageOptions } +interface CapturedMessages { + clientFeatureFlags: { type: string; data: Record } | null + serverFeatureFlags: Record | null +} + +interface AppReadiness { + featureFlagsReceived: boolean + apiInitialized: boolean + appInitialized: boolean +} + declare global { interface Window { /** For use by extensions and in the browser console. Where possible, import `app` from '@/scripts/app' instead. */ @@ -71,5 +82,11 @@ declare global { /** For use by extensions and in the browser console. Where possible, import `app` and access via `app.graph` instead. */ graph?: unknown + + /** For use in tests to capture WebSocket messages */ + __capturedMessages?: CapturedMessages + + /** For use in tests to track app initialization state */ + __appReadiness?: AppReadiness } }