mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 10:59:53 +00:00
refactor: improve TypeScript patterns in test files (Group 1/8) (#8253)
## 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<HTMLElement>` 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>
This commit is contained in:
committed by
GitHub
parent
9efcbe682f
commit
941cd2b4a5
@@ -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]],
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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' ||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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<typeof FormRadioGroup>
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ import type { SettingOption } from '@/platform/settings/types'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: any
|
||||
options: (SettingOption | string)[]
|
||||
options?: (string | SettingOption | Record<string, string>)[]
|
||||
optionLabel?: string
|
||||
optionValue?: string
|
||||
id?: string
|
||||
|
||||
@@ -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<typeof UrlInput> & {
|
||||
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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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<typeof UserAvatar> = {}) => {
|
||||
return mount(UserAvatar, {
|
||||
global: {
|
||||
plugins: [PrimeVue, i18n],
|
||||
|
||||
@@ -18,7 +18,7 @@ vi.mock('@/utils/formatUtil', () => ({
|
||||
}))
|
||||
|
||||
describe('SettingItem', () => {
|
||||
const mountComponent = (props: any, options = {}): any => {
|
||||
const mountComponent = (props: Record<string, unknown>, 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'
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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<typeof UsageLogsTable> & {
|
||||
loading: boolean
|
||||
error: string | null
|
||||
events: any[]
|
||||
events: Partial<AuditLog>[]
|
||||
pagination: {
|
||||
page: number
|
||||
limit: number
|
||||
|
||||
@@ -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<typeof ApiKeyForm> = {}) => {
|
||||
return mount(ApiKeyForm, {
|
||||
global: {
|
||||
plugins: [PrimeVue, createPinia(), i18n],
|
||||
|
||||
@@ -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<HTMLElement> = { 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<HTMLElement> = { 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<HTMLElement> = { focus: mockFocus }
|
||||
vi.spyOn(document, 'getElementById').mockReturnValue(
|
||||
mockElement as HTMLElement
|
||||
)
|
||||
|
||||
// Call handleForgotPassword with valid email
|
||||
await component.handleForgotPassword('test@example.com', true)
|
||||
|
||||
@@ -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!
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,17 @@ export type {
|
||||
ToastMessageOptions
|
||||
}
|
||||
|
||||
interface CapturedMessages {
|
||||
clientFeatureFlags: { type: string; data: Record<string, unknown> } | null
|
||||
serverFeatureFlags: Record<string, unknown> | 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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user