mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-13 09:00:16 +00:00
290 lines
8.2 KiB
TypeScript
290 lines
8.2 KiB
TypeScript
import { expect } from 'vitest'
|
|
|
|
import type { CapturedEvent } from './subgraphHelpers'
|
|
|
|
/**
|
|
* Extended captured event with additional metadata not in the base infrastructure
|
|
*/
|
|
export interface ExtendedCapturedEvent<T = unknown> extends CapturedEvent<T> {
|
|
defaultPrevented: boolean
|
|
bubbles: boolean
|
|
cancelable: boolean
|
|
}
|
|
|
|
/**
|
|
* Creates an enhanced event capture that includes additional event properties
|
|
* This extends the basic createEventCapture with more metadata
|
|
*/
|
|
export function createExtendedEventCapture<T = unknown>(
|
|
eventTarget: EventTarget,
|
|
eventTypes: string[]
|
|
) {
|
|
const capturedEvents: ExtendedCapturedEvent<T>[] = []
|
|
const listeners: Array<() => void> = []
|
|
|
|
for (const eventType of eventTypes) {
|
|
const listener = (event: Event) => {
|
|
capturedEvents.push({
|
|
type: eventType,
|
|
detail: (event as CustomEvent<T>).detail,
|
|
timestamp: Date.now(),
|
|
defaultPrevented: event.defaultPrevented,
|
|
bubbles: event.bubbles,
|
|
cancelable: event.cancelable
|
|
})
|
|
}
|
|
|
|
eventTarget.addEventListener(eventType, listener)
|
|
listeners.push(() => eventTarget.removeEventListener(eventType, listener))
|
|
}
|
|
|
|
return {
|
|
events: capturedEvents,
|
|
clear: () => {
|
|
capturedEvents.length = 0
|
|
},
|
|
cleanup: () => {
|
|
for (const cleanup of listeners) cleanup()
|
|
},
|
|
getEventsByType: (type: string) =>
|
|
capturedEvents.filter((e) => e.type === type),
|
|
getLatestEvent: () => capturedEvents.at(-1),
|
|
getFirstEvent: () => capturedEvents[0],
|
|
|
|
/**
|
|
* Wait for a specific event type to be captured
|
|
*/
|
|
async waitForEvent(
|
|
type: string,
|
|
timeoutMs: number = 1000
|
|
): Promise<ExtendedCapturedEvent<T>> {
|
|
const existingEvent = capturedEvents.find((e) => e.type === type)
|
|
if (existingEvent) return existingEvent
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const timeout = setTimeout(() => {
|
|
eventTarget.removeEventListener(type, eventListener)
|
|
reject(new Error(`Event ${type} not received within ${timeoutMs}ms`))
|
|
}, timeoutMs)
|
|
|
|
const eventListener = (_event: Event) => {
|
|
const capturedEvent = capturedEvents.find((e) => e.type === type)
|
|
if (capturedEvent) {
|
|
clearTimeout(timeout)
|
|
eventTarget.removeEventListener(type, eventListener)
|
|
resolve(capturedEvent)
|
|
}
|
|
}
|
|
|
|
eventTarget.addEventListener(type, eventListener)
|
|
})
|
|
},
|
|
|
|
/**
|
|
* Wait for a sequence of events to occur in order
|
|
*/
|
|
async waitForSequence(
|
|
expectedSequence: string[],
|
|
timeoutMs: number = 1000
|
|
): Promise<ExtendedCapturedEvent<T>[]> {
|
|
// Check if sequence is already complete
|
|
if (capturedEvents.length >= expectedSequence.length) {
|
|
const actualSequence = capturedEvents
|
|
.slice(0, expectedSequence.length)
|
|
.map((e) => e.type)
|
|
if (
|
|
JSON.stringify(actualSequence) === JSON.stringify(expectedSequence)
|
|
) {
|
|
return capturedEvents.slice(0, expectedSequence.length)
|
|
}
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const timeout = setTimeout(() => {
|
|
cleanup()
|
|
const actual = capturedEvents.map((e) => e.type).join(', ')
|
|
const expected = expectedSequence.join(', ')
|
|
reject(
|
|
new Error(
|
|
`Event sequence not completed within ${timeoutMs}ms. Expected: ${expected}, Got: ${actual}`
|
|
)
|
|
)
|
|
}, timeoutMs)
|
|
|
|
const checkSequence = () => {
|
|
if (capturedEvents.length >= expectedSequence.length) {
|
|
const actualSequence = capturedEvents
|
|
.slice(0, expectedSequence.length)
|
|
.map((e) => e.type)
|
|
if (
|
|
JSON.stringify(actualSequence) ===
|
|
JSON.stringify(expectedSequence)
|
|
) {
|
|
cleanup()
|
|
resolve(capturedEvents.slice(0, expectedSequence.length))
|
|
}
|
|
}
|
|
}
|
|
|
|
const eventListener = () => checkSequence()
|
|
|
|
const cleanup = () => {
|
|
clearTimeout(timeout)
|
|
for (const type of expectedSequence) {
|
|
eventTarget.removeEventListener(type, eventListener)
|
|
}
|
|
}
|
|
|
|
// Listen for all expected event types
|
|
for (const type of expectedSequence) {
|
|
eventTarget.addEventListener(type, eventListener)
|
|
}
|
|
|
|
// Initial check in case events already exist
|
|
checkSequence()
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Options for memory leak testing
|
|
*/
|
|
export interface MemoryLeakTestOptions {
|
|
cycles?: number
|
|
instancesPerCycle?: number
|
|
gcAfterEach?: boolean
|
|
maxMemoryGrowth?: number
|
|
}
|
|
|
|
/**
|
|
* Creates a memory leak test factory
|
|
* Useful for testing that event listeners and references are properly cleaned up
|
|
*/
|
|
export function createMemoryLeakTest<T>(
|
|
// @ts-expect-error TODO: Fix after merge - T does not satisfy constraint 'object'
|
|
setupFn: () => { ref: WeakRef<T>; cleanup: () => void },
|
|
options: MemoryLeakTestOptions = {}
|
|
) {
|
|
const {
|
|
cycles = 1,
|
|
instancesPerCycle = 1,
|
|
gcAfterEach = true,
|
|
maxMemoryGrowth = 0
|
|
} = options
|
|
|
|
return async () => {
|
|
// @ts-expect-error Type 'T' does not satisfy the constraint 'object'
|
|
const refs: WeakRef<T>[] = []
|
|
const initialMemory = process.memoryUsage?.()?.heapUsed || 0
|
|
|
|
for (let cycle = 0; cycle < cycles; cycle++) {
|
|
// @ts-expect-error Type 'T' does not satisfy the constraint 'object'
|
|
const cycleRefs: WeakRef<T>[] = []
|
|
|
|
for (let instance = 0; instance < instancesPerCycle; instance++) {
|
|
const { ref, cleanup } = setupFn()
|
|
cycleRefs.push(ref)
|
|
cleanup()
|
|
}
|
|
|
|
refs.push(...cycleRefs)
|
|
|
|
if (gcAfterEach && global.gc) {
|
|
global.gc()
|
|
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
}
|
|
}
|
|
|
|
// Final garbage collection
|
|
if (global.gc) {
|
|
global.gc()
|
|
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
|
|
// Check if objects were collected
|
|
const uncollectedRefs = refs.filter((ref) => ref.deref() !== undefined)
|
|
if (uncollectedRefs.length > 0) {
|
|
console.warn(
|
|
`${uncollectedRefs.length} objects were not garbage collected`
|
|
)
|
|
}
|
|
}
|
|
|
|
// Memory growth check
|
|
if (maxMemoryGrowth > 0 && process.memoryUsage) {
|
|
const finalMemory = process.memoryUsage().heapUsed
|
|
const memoryGrowth = finalMemory - initialMemory
|
|
|
|
if (memoryGrowth > maxMemoryGrowth) {
|
|
throw new Error(
|
|
`Memory growth ${memoryGrowth} bytes exceeds limit ${maxMemoryGrowth} bytes`
|
|
)
|
|
}
|
|
}
|
|
|
|
return refs
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a performance monitor for event operations
|
|
*/
|
|
export function createEventPerformanceMonitor() {
|
|
const measurements: Array<{
|
|
operation: string
|
|
duration: number
|
|
timestamp: number
|
|
}> = []
|
|
|
|
return {
|
|
measure: <T>(operation: string, fn: () => T): T => {
|
|
const start = performance.now()
|
|
const result = fn()
|
|
const end = performance.now()
|
|
|
|
measurements.push({
|
|
operation,
|
|
duration: end - start,
|
|
timestamp: start
|
|
})
|
|
|
|
return result
|
|
},
|
|
|
|
getMeasurements: () => [...measurements],
|
|
|
|
getAverageDuration: (operation: string) => {
|
|
const operationMeasurements = measurements.filter(
|
|
(m) => m.operation === operation
|
|
)
|
|
if (operationMeasurements.length === 0) return 0
|
|
|
|
const totalDuration = operationMeasurements.reduce(
|
|
(sum, m) => sum + m.duration,
|
|
0
|
|
)
|
|
return totalDuration / operationMeasurements.length
|
|
},
|
|
|
|
clear: () => {
|
|
measurements.length = 0
|
|
},
|
|
|
|
assertPerformance: (operation: string, maxDuration: number) => {
|
|
// @ts-expect-error 'this' implicitly has type 'any'
|
|
const measurements = this.getMeasurements()
|
|
const relevantMeasurements = measurements.filter(
|
|
// @ts-expect-error Parameter 'm' implicitly has an 'any' type
|
|
(m) => m.operation === operation
|
|
)
|
|
if (relevantMeasurements.length === 0) return
|
|
|
|
const avgDuration =
|
|
// @ts-expect-error Parameter 'sum' and 'm' implicitly have 'any' type
|
|
relevantMeasurements.reduce((sum, m) => sum + m.duration, 0) /
|
|
relevantMeasurements.length
|
|
expect(avgDuration).toBeLessThan(maxDuration)
|
|
}
|
|
}
|
|
}
|