diff --git a/.storybook/preview.ts b/.storybook/preview.ts
index 95c33709026..5463b2ad46f 100644
--- a/.storybook/preview.ts
+++ b/.storybook/preview.ts
@@ -15,7 +15,7 @@ import '@/assets/css/style.css'
const ComfyUIPreset = definePreset(Aura, {
semantic: {
- // @ts-expect-error fix me
+ // @ts-expect-error PrimeVue type issue
primary: Aura['primitive'].blue
}
})
diff --git a/AGENTS.md b/AGENTS.md
index 743572be3c3..a60cb37892b 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -200,6 +200,7 @@ See @docs/testing/*.md for detailed patterns.
## External Resources
- Vue:
+- @docs/typescript/type-safety.md
- Tailwind:
- VueUse:
- shadcn/vue:
diff --git a/apps/desktop-ui/.storybook/preview.ts b/apps/desktop-ui/.storybook/preview.ts
index a0ead30cc11..0f7e8de18fa 100644
--- a/apps/desktop-ui/.storybook/preview.ts
+++ b/apps/desktop-ui/.storybook/preview.ts
@@ -14,7 +14,7 @@ import { i18n } from '@/i18n'
const ComfyUIPreset = definePreset(Aura, {
semantic: {
- // @ts-expect-error prime type quirk
+ // @ts-expect-error PrimeVue type issue
primary: Aura['primitive'].blue
}
})
diff --git a/apps/desktop-ui/src/main.ts b/apps/desktop-ui/src/main.ts
index a04ff8f8109..bcee9405670 100644
--- a/apps/desktop-ui/src/main.ts
+++ b/apps/desktop-ui/src/main.ts
@@ -15,7 +15,7 @@ import router from './router'
const ComfyUIPreset = definePreset(Aura, {
semantic: {
- // @ts-expect-error fixme ts strict error
+ // @ts-expect-error PrimeVue type issue
primary: Aura['primitive'].blue
}
})
diff --git a/browser_tests/fixtures/ComfyPage.ts b/browser_tests/fixtures/ComfyPage.ts
index 08cdd5ccea3..aa812cd7bf0 100644
--- a/browser_tests/fixtures/ComfyPage.ts
+++ b/browser_tests/fixtures/ComfyPage.ts
@@ -81,11 +81,14 @@ class ComfyMenu {
await this.themeToggleButton.click()
await this.page.evaluate(() => {
return new Promise((resolve) => {
- window['app'].ui.settings.addEventListener(
- 'Comfy.ColorPalette.change',
- resolve,
- { once: true }
- )
+ const app = window.app
+ if (!app) {
+ resolve(undefined)
+ return
+ }
+ app.ui.settings.addEventListener('Comfy.ColorPalette.change', resolve, {
+ once: true
+ })
setTimeout(resolve, 5000)
})
@@ -94,9 +97,9 @@ class ComfyMenu {
async getThemeId() {
return await this.page.evaluate(async () => {
- return await window['app'].ui.settings.getSettingValue(
- 'Comfy.ColorPalette'
- )
+ const app = window.app
+ if (!app) return undefined
+ return await app.ui.settings.getSettingValue('Comfy.ColorPalette')
})
}
}
@@ -138,7 +141,14 @@ class ConfirmDialog {
// Wait for workflow service to finish if it's busy
await this.page.waitForFunction(
- () => window['app']?.extensionManager?.workflow?.isBusy === false,
+ () => {
+ const app = window.app
+ if (!app) return true
+ const extMgr = app.extensionManager as {
+ workflow?: { isBusy?: boolean }
+ }
+ return extMgr.workflow?.isBusy === false
+ },
undefined,
{ timeout: 3000 }
)
@@ -256,7 +266,12 @@ export class ComfyPage {
}
await this.page.evaluate(async () => {
- await window['app'].extensionManager.workflow.syncWorkflows()
+ const app = window.app
+ if (!app) return
+ const extMgr = app.extensionManager as {
+ workflow?: { syncWorkflows: () => Promise }
+ }
+ if (extMgr.workflow) await extMgr.workflow.syncWorkflows()
})
// Wait for Vue to re-render the workflow list
@@ -360,7 +375,9 @@ export class ComfyPage {
async executeCommand(commandId: string) {
await this.page.evaluate((id: string) => {
- return window['app'].extensionManager.command.execute(id)
+ const app = window.app
+ if (!app) return
+ return app.extensionManager.command.execute(id)
}, commandId)
}
@@ -370,7 +387,8 @@ export class ComfyPage {
) {
await this.page.evaluate(
({ commandId, commandStr }) => {
- const app = window['app']
+ const app = window.app
+ if (!app) return
const randomSuffix = Math.random().toString(36).substring(2, 8)
const extensionName = `TestExtension_${randomSuffix}`
@@ -391,7 +409,8 @@ export class ComfyPage {
async registerKeybinding(keyCombo: KeyCombo, command: () => void) {
await this.page.evaluate(
({ keyCombo, commandStr }) => {
- const app = window['app']
+ const app = window.app
+ if (!app) return
const randomSuffix = Math.random().toString(36).substring(2, 8)
const extensionName = `TestExtension_${randomSuffix}`
const commandId = `TestCommand_${randomSuffix}`
@@ -419,7 +438,9 @@ export class ComfyPage {
async setSetting(settingId: string, settingValue: any) {
return await this.page.evaluate(
async ({ id, value }) => {
- await window['app'].extensionManager.setting.set(id, value)
+ const app = window.app
+ if (!app) return
+ await app.extensionManager.setting.set(id, value)
},
{ id: settingId, value: settingValue }
)
@@ -427,7 +448,9 @@ export class ComfyPage {
async getSetting(settingId: string) {
return await this.page.evaluate(async (id) => {
- return await window['app'].extensionManager.setting.get(id)
+ const app = window.app
+ if (!app) return undefined
+ return await app.extensionManager.setting.get(id)
}, settingId)
}
@@ -873,8 +896,10 @@ export class ComfyPage {
const foundSlot = await this.page.evaluate(
async (params) => {
const { slotType, action, targetSlotName } = params
- const app = window['app']
+ const app = window.app
+ if (!app) throw new Error('App not initialized')
const currentGraph = app.canvas.graph
+ if (!currentGraph) throw new Error('No graph available')
// Check if we're in a subgraph
if (currentGraph.constructor.name !== 'Subgraph') {
@@ -883,13 +908,24 @@ export class ComfyPage {
)
}
- // Get the appropriate node and slots
+ // Get the appropriate node and slots (these are Subgraph-specific properties)
+ const subgraph = currentGraph as {
+ inputNode?: { onPointerDown?: (...args: unknown[]) => void }
+ outputNode?: { onPointerDown?: (...args: unknown[]) => void }
+ inputs?: Array<{
+ name: string
+ pos?: number[]
+ boundingRect?: number[]
+ }>
+ outputs?: Array<{
+ name: string
+ pos?: number[]
+ boundingRect?: number[]
+ }>
+ }
const node =
- slotType === 'input'
- ? currentGraph.inputNode
- : currentGraph.outputNode
- const slots =
- slotType === 'input' ? currentGraph.inputs : currentGraph.outputs
+ slotType === 'input' ? subgraph.inputNode : subgraph.outputNode
+ const slots = slotType === 'input' ? subgraph.inputs : subgraph.outputs
if (!node) {
throw new Error(`No ${slotType} node found in subgraph`)
@@ -970,6 +1006,7 @@ export class ComfyPage {
}
if (node.onPointerDown) {
+ // Call onPointerDown for test simulation
node.onPointerDown(
event,
app.canvas.pointer,
@@ -977,8 +1014,11 @@ export class ComfyPage {
)
// Trigger double-click
- if (app.canvas.pointer.onDoubleClick) {
- app.canvas.pointer.onDoubleClick(event)
+ const onDoubleClick = app.canvas.pointer.onDoubleClick as
+ | ((e: unknown) => void)
+ | undefined
+ if (onDoubleClick) {
+ onDoubleClick(event)
}
}
@@ -1574,7 +1614,9 @@ export class ComfyPage {
async convertOffsetToCanvas(pos: [number, number]) {
return this.page.evaluate((pos) => {
- return window['app'].canvas.ds.convertOffsetToCanvas(pos)
+ const app = window.app
+ if (!app) return pos
+ return app.canvas.ds.convertOffsetToCanvas(pos)
}, pos)
}
@@ -1588,14 +1630,18 @@ export class ComfyPage {
}
async getNodes(): Promise {
return await this.page.evaluate(() => {
- return window['app'].graph.nodes
+ const app = window.app
+ if (!app?.graph?.nodes) return []
+ return app.graph.nodes
})
}
async getNodeRefsByType(type: string): Promise {
return Promise.all(
(
await this.page.evaluate((type) => {
- return window['app'].graph.nodes
+ const app = window.app
+ if (!app?.graph?.nodes) return []
+ return app.graph.nodes
.filter((n: LGraphNode) => n.type === type)
.map((n: LGraphNode) => n.id)
}, type)
@@ -1606,7 +1652,9 @@ export class ComfyPage {
return Promise.all(
(
await this.page.evaluate((title) => {
- return window['app'].graph.nodes
+ const app = window.app
+ if (!app?.graph?.nodes) return []
+ return app.graph.nodes
.filter((n: LGraphNode) => n.title === title)
.map((n: LGraphNode) => n.id)
}, title)
@@ -1616,7 +1664,9 @@ export class ComfyPage {
async getFirstNodeRef(): Promise {
const id = await this.page.evaluate(() => {
- return window['app'].graph.nodes[0]?.id
+ const app = window.app
+ if (!app?.graph?.nodes) return undefined
+ return app.graph.nodes[0]?.id
})
if (!id) return null
return this.getNodeRefById(id)
@@ -1626,32 +1676,41 @@ export class ComfyPage {
}
async getUndoQueueSize() {
return this.page.evaluate(() => {
- const workflow = (window['app'].extensionManager as WorkspaceStore)
- .workflow.activeWorkflow
- return workflow?.changeTracker.undoQueue.length
+ const app = window.app
+ if (!app) return 0
+ const extMgr = app.extensionManager as WorkspaceStore
+ return extMgr.workflow.activeWorkflow?.changeTracker.undoQueue.length ?? 0
})
}
async getRedoQueueSize() {
return this.page.evaluate(() => {
- const workflow = (window['app'].extensionManager as WorkspaceStore)
- .workflow.activeWorkflow
- return workflow?.changeTracker.redoQueue.length
+ const app = window.app
+ if (!app) return 0
+ const extMgr = app.extensionManager as WorkspaceStore
+ return extMgr.workflow.activeWorkflow?.changeTracker.redoQueue.length ?? 0
})
}
async isCurrentWorkflowModified() {
return this.page.evaluate(() => {
- return (window['app'].extensionManager as WorkspaceStore).workflow
- .activeWorkflow?.isModified
+ const app = window.app
+ if (!app) return false
+ const extMgr = app.extensionManager as WorkspaceStore
+ return extMgr.workflow.activeWorkflow?.isModified ?? false
})
}
async getExportedWorkflow({ api = false }: { api?: boolean } = {}) {
return this.page.evaluate(async (api) => {
- return (await window['app'].graphToPrompt())[api ? 'output' : 'workflow']
+ const app = window.app
+ if (!app) return undefined
+ return (await app.graphToPrompt())[api ? 'output' : 'workflow']
}, api)
}
async setFocusMode(focusMode: boolean) {
await this.page.evaluate((focusMode) => {
- window['app'].extensionManager.focusMode = focusMode
+ const app = window.app
+ if (!app) return
+ const extMgr = app.extensionManager as { focusMode?: boolean }
+ extMgr.focusMode = focusMode
}, focusMode)
await this.nextFrame()
}
@@ -1664,7 +1723,9 @@ export class ComfyPage {
*/
async getGroupPosition(title: string): Promise {
const pos = await this.page.evaluate((title) => {
- const groups = window['app'].graph.groups
+ const app = window.app
+ if (!app?.graph?.groups) return null
+ const groups = app.graph.groups
const group = groups.find((g: { title: string }) => g.title === title)
if (!group) return null
return { x: group.pos[0], y: group.pos[1] }
@@ -1686,7 +1747,8 @@ export class ComfyPage {
}): Promise {
const { name, deltaX, deltaY } = options
const screenPos = await this.page.evaluate((title) => {
- const app = window['app']
+ const app = window.app
+ if (!app?.graph?.groups) return null
const groups = app.graph.groups
const group = groups.find((g: { title: string }) => g.title === title)
if (!group) return null
@@ -1754,11 +1816,16 @@ export const comfyPageFixture = base.extend<{
}
})
+interface MatcherContext {
+ isNot: boolean
+}
+
const makeMatcher = function (
getValue: (node: NodeReference) => Promise | T,
type: string
) {
return async function (
+ this: MatcherContext,
node: NodeReference,
options?: { timeout?: number; intervals?: number[] }
) {
@@ -1784,7 +1851,11 @@ export const comfyExpect = expect.extend({
toBePinned: makeMatcher((n) => n.isPinned(), 'pinned'),
toBeBypassed: makeMatcher((n) => n.isBypassed(), 'bypassed'),
toBeCollapsed: makeMatcher((n) => n.isCollapsed(), 'collapsed'),
- async toHaveFocus(locator: Locator, options = { timeout: 256 }) {
+ async toHaveFocus(
+ this: MatcherContext,
+ locator: Locator,
+ options = { timeout: 256 }
+ ) {
const isFocused = await locator.evaluate(
(el) => el === document.activeElement
)
diff --git a/browser_tests/fixtures/components/SidebarTab.ts b/browser_tests/fixtures/components/SidebarTab.ts
index 3254e27c84a..18231591164 100644
--- a/browser_tests/fixtures/components/SidebarTab.ts
+++ b/browser_tests/fixtures/components/SidebarTab.ts
@@ -31,7 +31,7 @@ class SidebarTab {
}
export class NodeLibrarySidebarTab extends SidebarTab {
- constructor(public readonly page: Page) {
+ constructor(public override readonly page: Page) {
super(page, 'node-library')
}
@@ -55,12 +55,12 @@ export class NodeLibrarySidebarTab extends SidebarTab {
return this.tabContainer.locator('.new-folder-button')
}
- async open() {
+ override async open() {
await super.open()
await this.nodeLibraryTree.waitFor({ state: 'visible' })
}
- async close() {
+ override async close() {
if (!this.tabButton.isVisible()) {
return
}
@@ -87,7 +87,7 @@ export class NodeLibrarySidebarTab extends SidebarTab {
}
export class WorkflowsSidebarTab extends SidebarTab {
- constructor(public readonly page: Page) {
+ constructor(public override readonly page: Page) {
super(page, 'workflows')
}
@@ -140,7 +140,14 @@ export class WorkflowsSidebarTab extends SidebarTab {
// Wait for workflow service to finish renaming
await this.page.waitForFunction(
- () => !window['app']?.extensionManager?.workflow?.isBusy,
+ () => {
+ const app = window.app
+ if (!app) return true
+ const extMgr = app.extensionManager as {
+ workflow?: { isBusy?: boolean }
+ }
+ return !extMgr.workflow?.isBusy
+ },
undefined,
{ timeout: 3000 }
)
diff --git a/browser_tests/fixtures/components/Topbar.ts b/browser_tests/fixtures/components/Topbar.ts
index c5e7c81550b..d809c4292b1 100644
--- a/browser_tests/fixtures/components/Topbar.ts
+++ b/browser_tests/fixtures/components/Topbar.ts
@@ -86,7 +86,14 @@ export class Topbar {
// Wait for workflow service to finish saving
await this.page.waitForFunction(
- () => !window['app'].extensionManager.workflow.isBusy,
+ () => {
+ const app = window.app
+ if (!app) return true
+ const extMgr = app.extensionManager as {
+ workflow?: { isBusy?: boolean }
+ }
+ return !extMgr.workflow?.isBusy
+ },
undefined,
{ timeout: 3000 }
)
diff --git a/browser_tests/fixtures/utils/litegraphUtils.ts b/browser_tests/fixtures/utils/litegraphUtils.ts
index 832db632498..2dbf0ba5479 100644
--- a/browser_tests/fixtures/utils/litegraphUtils.ts
+++ b/browser_tests/fixtures/utils/litegraphUtils.ts
@@ -22,7 +22,10 @@ export class SubgraphSlotReference {
async getPosition(): Promise {
const pos: [number, number] = await this.comfyPage.page.evaluate(
([type, slotName]) => {
- const currentGraph = window['app'].canvas.graph
+ const app = window.app
+ if (!app) throw new Error('App not initialized')
+ const currentGraph = app.canvas.graph
+ if (!currentGraph) throw new Error('No graph available')
// Check if we're in a subgraph
if (currentGraph.constructor.name !== 'Subgraph') {
@@ -31,15 +34,18 @@ export class SubgraphSlotReference {
)
}
- const slots =
- type === 'input' ? currentGraph.inputs : currentGraph.outputs
+ const subgraph = currentGraph as {
+ inputs?: Array<{ name: string; pos?: number[] }>
+ outputs?: Array<{ name: string; pos?: number[] }>
+ }
+ const slots = type === 'input' ? subgraph.inputs : subgraph.outputs
if (!slots || slots.length === 0) {
throw new Error(`No ${type} slots found in subgraph`)
}
// Find the specific slot or use the first one if no name specified
const slot = slotName
- ? slots.find((s) => s.name === slotName)
+ ? slots.find((s: { name: string }) => s.name === slotName)
: slots[0]
if (!slot) {
@@ -51,7 +57,7 @@ export class SubgraphSlotReference {
}
// Convert from offset to canvas coordinates
- const canvasPos = window['app'].canvas.ds.convertOffsetToCanvas([
+ const canvasPos = app.canvas.ds.convertOffsetToCanvas([
slot.pos[0],
slot.pos[1]
])
@@ -69,7 +75,10 @@ export class SubgraphSlotReference {
async getOpenSlotPosition(): Promise {
const pos: [number, number] = await this.comfyPage.page.evaluate(
([type]) => {
- const currentGraph = window['app'].canvas.graph
+ const app = window.app
+ if (!app) throw new Error('App not initialized')
+ const currentGraph = app.canvas.graph
+ if (!currentGraph) throw new Error('No graph available')
if (currentGraph.constructor.name !== 'Subgraph') {
throw new Error(
@@ -77,35 +86,35 @@ export class SubgraphSlotReference {
)
}
- const node =
- type === 'input' ? currentGraph.inputNode : currentGraph.outputNode
- const slots =
- type === 'input' ? currentGraph.inputs : currentGraph.outputs
+ const subgraph = currentGraph as {
+ inputNode?: { pos: number[]; size: number[] }
+ outputNode?: { pos: number[]; size: number[] }
+ inputs?: Array<{ pos?: number[] }>
+ outputs?: Array<{ pos?: number[] }>
+ slotAnchorX?: number
+ }
+ const node = type === 'input' ? subgraph.inputNode : subgraph.outputNode
+ const slots = type === 'input' ? subgraph.inputs : subgraph.outputs
if (!node) {
throw new Error(`No ${type} node found in subgraph`)
}
- // Calculate position for next available slot
- // const nextSlotIndex = slots?.length || 0
- // const slotHeight = 20
- // const slotY = node.pos[1] + 30 + nextSlotIndex * slotHeight
-
// Find last slot position
- const lastSlot = slots.at(-1)
+ const lastSlot = slots?.at(-1)
let slotX: number
let slotY: number
- if (lastSlot) {
+ if (lastSlot?.pos) {
// If there are existing slots, position the new one below the last one
const gapHeight = 20
slotX = lastSlot.pos[0]
slotY = lastSlot.pos[1] + gapHeight
} else {
// No existing slots - use slotAnchorX if available, otherwise calculate from node position
- if (currentGraph.slotAnchorX !== undefined) {
+ if (subgraph.slotAnchorX !== undefined) {
// The actual slot X position seems to be slotAnchorX - 10
- slotX = currentGraph.slotAnchorX - 10
+ slotX = subgraph.slotAnchorX - 10
} else {
// Fallback: calculate from node edge
slotX =
@@ -118,10 +127,7 @@ export class SubgraphSlotReference {
}
// Convert from offset to canvas coordinates
- const canvasPos = window['app'].canvas.ds.convertOffsetToCanvas([
- slotX,
- slotY
- ])
+ const canvasPos = app.canvas.ds.convertOffsetToCanvas([slotX, slotY])
return canvasPos
},
[this.type] as const
@@ -143,26 +149,15 @@ class NodeSlotReference {
async getPosition() {
const pos: [number, number] = await this.node.comfyPage.page.evaluate(
([type, id, index]) => {
+ const app = window.app
+ if (!app?.canvas?.graph) throw new Error('App not initialized')
+ const graph = app.canvas.graph
// Use canvas.graph to get the current graph (works in both main graph and subgraphs)
- const node = window['app'].canvas.graph.getNodeById(id)
+ const node = graph.getNodeById(id)
if (!node) throw new Error(`Node ${id} not found.`)
const rawPos = node.getConnectionPos(type === 'input', index)
- const convertedPos =
- window['app'].canvas.ds.convertOffsetToCanvas(rawPos)
-
- // Debug logging - convert Float64Arrays to regular arrays for visibility
- // eslint-disable-next-line no-console
- console.log(
- `NodeSlotReference debug for ${type} slot ${index} on node ${id}:`,
- {
- nodePos: [node.pos[0], node.pos[1]],
- nodeSize: [node.size[0], node.size[1]],
- rawConnectionPos: [rawPos[0], rawPos[1]],
- convertedPos: [convertedPos[0], convertedPos[1]],
- currentGraphType: window['app'].canvas.graph.constructor.name
- }
- )
+ const convertedPos = app.canvas.ds.convertOffsetToCanvas(rawPos)
return convertedPos
},
@@ -176,7 +171,9 @@ class NodeSlotReference {
async getLinkCount() {
return await this.node.comfyPage.page.evaluate(
([type, id, index]) => {
- const node = window['app'].canvas.graph.getNodeById(id)
+ const app = window.app
+ if (!app?.canvas?.graph) throw new Error('App not initialized')
+ const node = app.canvas.graph.getNodeById(id)
if (!node) throw new Error(`Node ${id} not found.`)
if (type === 'input') {
return node.inputs[index].link == null ? 0 : 1
@@ -189,7 +186,9 @@ class NodeSlotReference {
async removeLinks() {
await this.node.comfyPage.page.evaluate(
([type, id, index]) => {
- const node = window['app'].canvas.graph.getNodeById(id)
+ const app = window.app
+ if (!app?.canvas?.graph) throw new Error('App not initialized')
+ const node = app.canvas.graph.getNodeById(id)
if (!node) throw new Error(`Node ${id} not found.`)
if (type === 'input') {
node.disconnectInput(index)
@@ -214,15 +213,19 @@ class NodeWidgetReference {
async getPosition(): Promise {
const pos: [number, number] = await this.node.comfyPage.page.evaluate(
([id, index]) => {
- const node = window['app'].canvas.graph.getNodeById(id)
+ const app = window.app
+ if (!app?.canvas?.graph) throw new Error('App not initialized')
+ const node = app.canvas.graph.getNodeById(id)
if (!node) throw new Error(`Node ${id} not found.`)
+ if (!node.widgets) throw new Error(`Node ${id} has no widgets.`)
const widget = node.widgets[index]
if (!widget) throw new Error(`Widget ${index} not found.`)
- const [x, y, w, h] = node.getBounding()
- return window['app'].canvasPosToClientPos([
+ const [x, y, w] = node.getBounding()
+ const titleHeight = window.LiteGraph?.NODE_TITLE_HEIGHT ?? 20
+ return app.canvasPosToClientPos([
x + w / 2,
- y + window['LiteGraph']['NODE_TITLE_HEIGHT'] + widget.last_y + 1
+ y + titleHeight + (widget.last_y ?? 0) + 1
])
},
[this.node.id, this.index] as const
@@ -239,8 +242,11 @@ class NodeWidgetReference {
async getSocketPosition(): Promise {
const pos: [number, number] = await this.node.comfyPage.page.evaluate(
([id, index]) => {
- const node = window['app'].graph.getNodeById(id)
+ const app = window.app
+ if (!app?.canvas?.graph) throw new Error('App not initialized')
+ const node = app.canvas.graph.getNodeById(id)
if (!node) throw new Error(`Node ${id} not found.`)
+ if (!node.widgets) throw new Error(`Node ${id} has no widgets.`)
const widget = node.widgets[index]
if (!widget) throw new Error(`Widget ${index} not found.`)
@@ -248,11 +254,13 @@ class NodeWidgetReference {
(slot) => slot.widget?.name === widget.name
)
if (!slot) throw new Error(`Socket ${widget.name} not found.`)
+ if (!slot.pos) throw new Error(`Socket ${widget.name} has no position.`)
const [x, y] = node.getBounding()
- return window['app'].canvasPosToClientPos([
+ const titleHeight = window.LiteGraph?.NODE_TITLE_HEIGHT ?? 20
+ return app.canvasPosToClientPos([
x + slot.pos[0],
- y + slot.pos[1] + window['LiteGraph']['NODE_TITLE_HEIGHT']
+ y + slot.pos[1] + titleHeight
])
},
[this.node.id, this.index] as const
@@ -288,8 +296,11 @@ class NodeWidgetReference {
async getValue() {
return await this.node.comfyPage.page.evaluate(
([id, index]) => {
- const node = window['app'].graph.getNodeById(id)
+ const app = window.app
+ if (!app?.canvas?.graph) throw new Error('App not initialized')
+ const node = app.canvas.graph.getNodeById(id)
if (!node) throw new Error(`Node ${id} not found.`)
+ if (!node.widgets) throw new Error(`Node ${id} has no widgets.`)
const widget = node.widgets[index]
if (!widget) throw new Error(`Widget ${index} not found.`)
return widget.value
@@ -305,7 +316,9 @@ export class NodeReference {
) {}
async exists(): Promise {
return await this.comfyPage.page.evaluate((id) => {
- const node = window['app'].canvas.graph.getNodeById(id)
+ const app = window.app
+ if (!app?.canvas?.graph) return false
+ const node = app.canvas.graph.getNodeById(id)
return !!node
}, this.id)
}
@@ -322,17 +335,19 @@ export class NodeReference {
}
}
async getBounding(): Promise {
- const [x, y, width, height]: [number, number, number, number] =
- await this.comfyPage.page.evaluate((id) => {
- const node = window['app'].canvas.graph.getNodeById(id)
- if (!node) throw new Error('Node not found')
- return node.getBounding()
- }, this.id)
+ const bounding = await this.comfyPage.page.evaluate((id) => {
+ const app = window.app
+ if (!app?.canvas?.graph) throw new Error('App not initialized')
+ const node = app.canvas.graph.getNodeById(id)
+ if (!node) throw new Error('Node not found')
+ const b = node.getBounding()
+ return [b[0], b[1], b[2], b[3]] as [number, number, number, number]
+ }, this.id)
return {
- x,
- y,
- width,
- height
+ x: bounding[0],
+ y: bounding[1],
+ width: bounding[2],
+ height: bounding[3]
}
}
async getSize(): Promise {
@@ -355,14 +370,16 @@ export class NodeReference {
return (await this.getProperty('mode')) === 4
}
async getProperty(prop: string): Promise {
- return await this.comfyPage.page.evaluate(
+ return (await this.comfyPage.page.evaluate(
([id, prop]) => {
- const node = window['app'].canvas.graph.getNodeById(id)
+ const app = window.app
+ if (!app?.canvas?.graph) throw new Error('App not initialized')
+ const node = app.canvas.graph.getNodeById(id)
if (!node) throw new Error('Node not found')
- return node[prop]
+ return (node as unknown as Record)[prop]
},
[this.id, prop] as const
- )
+ )) as T
}
async getOutput(index: number) {
return new NodeSlotReference('output', index, this)
@@ -480,7 +497,7 @@ export class NodeReference {
}
async navigateIntoSubgraph() {
const titleHeight = await this.comfyPage.page.evaluate(() => {
- return window['LiteGraph']['NODE_TITLE_HEIGHT']
+ return window.LiteGraph?.NODE_TITLE_HEIGHT ?? 20
})
const nodePos = await this.getPosition()
const nodeSize = await this.getSize()
@@ -513,7 +530,9 @@ export class NodeReference {
// Check if we successfully entered the subgraph
isInSubgraph = await this.comfyPage.page.evaluate(() => {
- const graph = window['app'].canvas.graph
+ const app = window.app
+ if (!app) return false
+ const graph = app.canvas.graph
return graph?.constructor?.name === 'Subgraph'
})
diff --git a/browser_tests/fixtures/utils/subgraphUtils.ts b/browser_tests/fixtures/utils/subgraphUtils.ts
new file mode 100644
index 00000000000..876fc1c8e70
--- /dev/null
+++ b/browser_tests/fixtures/utils/subgraphUtils.ts
@@ -0,0 +1,67 @@
+/**
+ * Represents a subgraph's graph object with inputs and outputs slots.
+ */
+export interface SubgraphGraph {
+ inputs: SubgraphSlot[]
+ outputs: SubgraphSlot[]
+}
+
+export interface SubgraphSlot {
+ label?: string
+ name?: string
+ displayName?: string
+ labelPos?: [number, number]
+}
+
+export interface SubgraphInputNode {
+ onPointerDown?: (e: unknown, pointer: unknown, linkConnector: unknown) => void
+}
+
+export interface SubgraphGraphWithNodes extends SubgraphGraph {
+ inputNode?: SubgraphInputNode
+}
+
+/**
+ * Type guard to check if a graph object is a subgraph.
+ */
+export function isSubgraph(graph: unknown): graph is SubgraphGraph {
+ return (
+ graph !== null &&
+ typeof graph === 'object' &&
+ 'inputs' in graph &&
+ 'outputs' in graph &&
+ Array.isArray((graph as SubgraphGraph).inputs) &&
+ Array.isArray((graph as SubgraphGraph).outputs)
+ )
+}
+
+/**
+ * Assertion function that throws if the graph is not a subgraph.
+ */
+export function assertSubgraph(
+ graph: unknown,
+ message = 'Not in subgraph'
+): asserts graph is SubgraphGraph {
+ if (!isSubgraph(graph)) {
+ throw new Error(message)
+ }
+}
+
+/**
+ * Inline assertion for use inside page.evaluate() browser context.
+ * Returns a string that can be used with Function constructor or eval.
+ */
+export const SUBGRAPH_ASSERT_INLINE = `
+ const assertSubgraph = (graph) => {
+ if (
+ graph === null ||
+ typeof graph !== 'object' ||
+ !('inputs' in graph) ||
+ !('outputs' in graph) ||
+ !Array.isArray(graph.inputs) ||
+ !Array.isArray(graph.outputs)
+ ) {
+ throw new Error('Not in subgraph');
+ }
+ };
+`
diff --git a/browser_tests/fixtures/utils/taskHistory.ts b/browser_tests/fixtures/utils/taskHistory.ts
index 01dfb1a4aa5..7b60211b3f3 100644
--- a/browser_tests/fixtures/utils/taskHistory.ts
+++ b/browser_tests/fixtures/utils/taskHistory.ts
@@ -120,7 +120,7 @@ export default class TaskHistory {
filenames: string[],
filetype: OutputFileType
): TaskOutput {
- return filenames.reduce((outputs, filename, i) => {
+ return filenames.reduce((outputs, filename, i) => {
const nodeId = `${i + 1}`
outputs[nodeId] = {
[filetype]: [{ filename, subfolder: '', type: 'output' }]
diff --git a/browser_tests/fixtures/utils/workflowUtils.ts b/browser_tests/fixtures/utils/workflowUtils.ts
new file mode 100644
index 00000000000..dc89bf60960
--- /dev/null
+++ b/browser_tests/fixtures/utils/workflowUtils.ts
@@ -0,0 +1,22 @@
+interface ExtensionManagerWorkflow {
+ workflow?: {
+ activeWorkflow?: {
+ filename?: string
+ delete?: () => Promise
+ }
+ }
+}
+
+interface AppWithExtensionManager {
+ extensionManager: ExtensionManagerWorkflow
+}
+
+export function getActiveWorkflowFilename(app: unknown): string | undefined {
+ const extMgr = (app as AppWithExtensionManager).extensionManager
+ return extMgr.workflow?.activeWorkflow?.filename
+}
+
+export async function deleteActiveWorkflow(app: unknown): Promise {
+ const extMgr = (app as AppWithExtensionManager).extensionManager
+ await extMgr.workflow?.activeWorkflow?.delete?.()
+}
diff --git a/browser_tests/globalSetup.ts b/browser_tests/globalSetup.ts
index 881ef11c439..b43b77c6a6a 100644
--- a/browser_tests/globalSetup.ts
+++ b/browser_tests/globalSetup.ts
@@ -5,7 +5,7 @@ import { backupPath } from './utils/backupUtils'
dotenv.config()
-export default function globalSetup(config: FullConfig) {
+export default function globalSetup(_config: FullConfig) {
if (!process.env.CI) {
if (process.env.TEST_COMFYUI_DIR) {
backupPath([process.env.TEST_COMFYUI_DIR, 'user'])
diff --git a/browser_tests/globalTeardown.ts b/browser_tests/globalTeardown.ts
index aeed77294c2..c69f563df69 100644
--- a/browser_tests/globalTeardown.ts
+++ b/browser_tests/globalTeardown.ts
@@ -5,7 +5,7 @@ import { restorePath } from './utils/backupUtils'
dotenv.config()
-export default function globalTeardown(config: FullConfig) {
+export default function globalTeardown(_config: FullConfig) {
if (!process.env.CI && process.env.TEST_COMFYUI_DIR) {
restorePath([process.env.TEST_COMFYUI_DIR, 'user'])
restorePath([process.env.TEST_COMFYUI_DIR, 'models'])
diff --git a/browser_tests/helpers/actionbar.ts b/browser_tests/helpers/actionbar.ts
index 6c368c4d650..783853f36aa 100644
--- a/browser_tests/helpers/actionbar.ts
+++ b/browser_tests/helpers/actionbar.ts
@@ -42,13 +42,26 @@ class ComfyQueueButtonOptions {
public async setMode(mode: AutoQueueMode) {
await this.page.evaluate((mode) => {
- window['app'].extensionManager.queueSettings.mode = mode
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ const extMgr = app.extensionManager as {
+ queueSettings?: { mode: string }
+ }
+ if (!extMgr.queueSettings) {
+ throw new Error('queueSettings not initialized')
+ }
+ extMgr.queueSettings.mode = mode
}, mode)
}
public async getMode() {
return await this.page.evaluate(() => {
- return window['app'].extensionManager.queueSettings.mode
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ const extMgr = app.extensionManager as {
+ queueSettings?: { mode: string }
+ }
+ return extMgr.queueSettings?.mode
})
}
}
diff --git a/browser_tests/helpers/templates.ts b/browser_tests/helpers/templates.ts
index ca74096ad2f..9fe72223afc 100644
--- a/browser_tests/helpers/templates.ts
+++ b/browser_tests/helpers/templates.ts
@@ -32,9 +32,11 @@ export class ComfyTemplates {
}
async getAllTemplates(): Promise {
- const templates: WorkflowTemplates[] = await this.page.evaluate(() =>
- window['app'].api.getCoreWorkflowTemplates()
- )
+ const templates: WorkflowTemplates[] = await this.page.evaluate(() => {
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ return app.api.getCoreWorkflowTemplates()
+ })
return templates.flatMap((t) => t.templates)
}
diff --git a/browser_tests/tests/actionbar.spec.ts b/browser_tests/tests/actionbar.spec.ts
index bd086b46102..8c4b77ca3f4 100644
--- a/browser_tests/tests/actionbar.spec.ts
+++ b/browser_tests/tests/actionbar.spec.ts
@@ -1,9 +1,9 @@
import type { Response } from '@playwright/test'
import { expect, mergeTests } from '@playwright/test'
-import type { StatusWsMessage } from '../../src/schemas/apiSchema.ts'
-import { comfyPageFixture } from '../fixtures/ComfyPage.ts'
-import { webSocketFixture } from '../fixtures/ws.ts'
+import type { StatusWsMessage } from '../../src/schemas/apiSchema'
+import { comfyPageFixture } from '../fixtures/ComfyPage'
+import { webSocketFixture } from '../fixtures/ws'
const test = mergeTests(comfyPageFixture, webSocketFixture)
@@ -49,13 +49,19 @@ test.describe('Actionbar', () => {
// Find and set the width on the latent node
const triggerChange = async (value: number) => {
return await comfyPage.page.evaluate((value) => {
- const node = window['app'].graph._nodes.find(
- (n) => n.type === 'EmptyLatentImage'
+ const app = window['app']
+ if (!app?.graph) throw new Error('App not initialized')
+ const node = app.graph._nodes.find(
+ (n: { type: string }) => n.type === 'EmptyLatentImage'
)
+ if (!node?.widgets?.[0]) throw new Error('Node or widget not found')
node.widgets[0].value = value
- window[
- 'app'
- ].extensionManager.workflow.activeWorkflow.changeTracker.checkState()
+ const extMgr = app.extensionManager as {
+ workflow?: {
+ activeWorkflow?: { changeTracker?: { checkState?: () => void } }
+ }
+ }
+ extMgr.workflow?.activeWorkflow?.changeTracker?.checkState?.()
}, value)
}
diff --git a/browser_tests/tests/browserTabTitle.spec.ts b/browser_tests/tests/browserTabTitle.spec.ts
index fd855ea4af3..45bd461d1c1 100644
--- a/browser_tests/tests/browserTabTitle.spec.ts
+++ b/browser_tests/tests/browserTabTitle.spec.ts
@@ -1,6 +1,10 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
+import {
+ deleteActiveWorkflow,
+ getActiveWorkflowFilename
+} from '../fixtures/utils/workflowUtils'
test.describe('Browser tab title', () => {
test.describe('Beta Menu', () => {
@@ -9,9 +13,11 @@ test.describe('Browser tab title', () => {
})
test('Can display workflow name', async ({ comfyPage }) => {
- const workflowName = await comfyPage.page.evaluate(async () => {
- return window['app'].extensionManager.workflow.activeWorkflow.filename
- })
+ const workflowName = await comfyPage.page.evaluate(() => {
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ return getActiveWorkflowFilename(app)
+ }, getActiveWorkflowFilename)
expect(await comfyPage.page.title()).toBe(`*${workflowName} - ComfyUI`)
})
@@ -20,9 +26,11 @@ test.describe('Browser tab title', () => {
test.skip('Can display workflow name with unsaved changes', async ({
comfyPage
}) => {
- const workflowName = await comfyPage.page.evaluate(async () => {
- return window['app'].extensionManager.workflow.activeWorkflow.filename
- })
+ const workflowName = await comfyPage.page.evaluate(() => {
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ return getActiveWorkflowFilename(app)
+ }, getActiveWorkflowFilename)
expect(await comfyPage.page.title()).toBe(`${workflowName} - ComfyUI`)
await comfyPage.menu.topbar.saveWorkflow('test')
@@ -35,8 +43,10 @@ test.describe('Browser tab title', () => {
// Delete the saved workflow for cleanup.
await comfyPage.page.evaluate(async () => {
- return window['app'].extensionManager.workflow.activeWorkflow.delete()
- })
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ await deleteActiveWorkflow(app)
+ }, deleteActiveWorkflow)
})
})
diff --git a/browser_tests/tests/changeTracker.spec.ts b/browser_tests/tests/changeTracker.spec.ts
index 8e39154f158..0ab11c34344 100644
--- a/browser_tests/tests/changeTracker.spec.ts
+++ b/browser_tests/tests/changeTracker.spec.ts
@@ -6,12 +6,16 @@ import {
async function beforeChange(comfyPage: ComfyPage) {
await comfyPage.page.evaluate(() => {
- window['app'].canvas.emitBeforeChange()
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ app.canvas.emitBeforeChange()
})
}
async function afterChange(comfyPage: ComfyPage) {
await comfyPage.page.evaluate(() => {
- window['app'].canvas.emitAfterChange()
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ app.canvas.emitAfterChange()
})
}
@@ -156,7 +160,9 @@ test.describe('Change Tracker', () => {
test('Can detect changes in workflow.extra', async ({ comfyPage }) => {
expect(await comfyPage.getUndoQueueSize()).toBe(0)
await comfyPage.page.evaluate(() => {
- window['app'].graph.extra.foo = 'bar'
+ const app = window['app']
+ if (!app?.graph?.extra) throw new Error('App graph not initialized')
+ app.graph.extra.foo = 'bar'
})
// Click empty space to trigger a change detection.
await comfyPage.clickEmptySpace()
diff --git a/browser_tests/tests/colorPalette.spec.ts b/browser_tests/tests/colorPalette.spec.ts
index ce372ddfa69..b49ce68aa1b 100644
--- a/browser_tests/tests/colorPalette.spec.ts
+++ b/browser_tests/tests/colorPalette.spec.ts
@@ -7,7 +7,15 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
-const customColorPalettes: Record = {
+type TestPalette = Omit & {
+ colors: {
+ node_slot: Record
+ litegraph_base: Partial
+ comfy_base: Partial
+ }
+}
+
+const customColorPalettes: Record = {
obsidian: {
version: 102,
id: 'obsidian',
@@ -176,7 +184,20 @@ test.describe('Color Palette', () => {
test('Can add custom color palette', async ({ comfyPage }) => {
await comfyPage.page.evaluate((p) => {
- window['app'].extensionManager.colorPalette.addCustomColorPalette(p)
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ const extMgr = app.extensionManager as {
+ colorPalette?: { addCustomColorPalette?: (p: unknown) => void }
+ }
+ if (!extMgr.colorPalette) {
+ throw new Error('colorPalette extension not found on extensionManager')
+ }
+ if (!extMgr.colorPalette.addCustomColorPalette) {
+ throw new Error(
+ 'addCustomColorPalette method not found on colorPalette extension'
+ )
+ }
+ extMgr.colorPalette.addCustomColorPalette(p)
}, customColorPalettes.obsidian_dark)
expect(await comfyPage.getToastErrorCount()).toBe(0)
@@ -232,7 +253,6 @@ test.describe('Node Color Adjustments', () => {
}) => {
await comfyPage.setSetting('Comfy.Node.Opacity', 0.5)
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
- const saveWorkflowInterval = 1000
const workflow = await comfyPage.page.evaluate(() => {
return localStorage.getItem('workflow')
})
diff --git a/browser_tests/tests/commands.spec.ts b/browser_tests/tests/commands.spec.ts
index e271f2e15ca..78f2899541a 100644
--- a/browser_tests/tests/commands.spec.ts
+++ b/browser_tests/tests/commands.spec.ts
@@ -9,25 +9,25 @@ test.beforeEach(async ({ comfyPage }) => {
test.describe('Keybindings', () => {
test('Should execute command', async ({ comfyPage }) => {
await comfyPage.registerCommand('TestCommand', () => {
- window['foo'] = true
+ window.foo = true
})
await comfyPage.executeCommand('TestCommand')
- expect(await comfyPage.page.evaluate(() => window['foo'])).toBe(true)
+ expect(await comfyPage.page.evaluate(() => window.foo)).toBe(true)
})
test('Should execute async command', async ({ comfyPage }) => {
await comfyPage.registerCommand('TestCommand', async () => {
await new Promise((resolve) =>
setTimeout(() => {
- window['foo'] = true
+ window.foo = true
resolve()
}, 5)
)
})
await comfyPage.executeCommand('TestCommand')
- expect(await comfyPage.page.evaluate(() => window['foo'])).toBe(true)
+ expect(await comfyPage.page.evaluate(() => window.foo)).toBe(true)
})
test('Should handle command errors', async ({ comfyPage }) => {
@@ -41,7 +41,7 @@ test.describe('Keybindings', () => {
test('Should handle async command errors', async ({ comfyPage }) => {
await comfyPage.registerCommand('TestCommand', async () => {
- await new Promise((resolve, reject) =>
+ await new Promise((_resolve, reject) =>
setTimeout(() => {
reject(new Error('Test error'))
}, 5)
diff --git a/browser_tests/tests/dialog.spec.ts b/browser_tests/tests/dialog.spec.ts
index 081f4727530..8d4ba7981ac 100644
--- a/browser_tests/tests/dialog.spec.ts
+++ b/browser_tests/tests/dialog.spec.ts
@@ -331,7 +331,8 @@ test.describe('Error dialog', () => {
comfyPage
}) => {
await comfyPage.page.evaluate(() => {
- const graph = window['graph']
+ const graph = window['graph'] as { configure?: () => void } | undefined
+ if (!graph) throw new Error('Graph not initialized')
graph.configure = () => {
throw new Error('Error on configure!')
}
@@ -348,6 +349,7 @@ test.describe('Error dialog', () => {
}) => {
await comfyPage.page.evaluate(async () => {
const app = window['app']
+ if (!app) throw new Error('App not initialized')
app.api.queuePrompt = () => {
throw new Error('Error on queuePrompt!')
}
@@ -373,7 +375,9 @@ test.describe('Signin dialog', () => {
await textBox.press('Control+c')
await comfyPage.page.evaluate(() => {
- void window['app'].extensionManager.dialog.showSignInDialog()
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ void app.extensionManager.dialog.showSignInDialog()
})
const input = comfyPage.page.locator('#comfy-org-sign-in-password')
diff --git a/browser_tests/tests/extensionAPI.spec.ts b/browser_tests/tests/extensionAPI.spec.ts
index 38f4a6c1de0..beef8c4b49d 100644
--- a/browser_tests/tests/extensionAPI.spec.ts
+++ b/browser_tests/tests/extensionAPI.spec.ts
@@ -10,14 +10,16 @@ test.describe('Topbar commands', () => {
test('Should allow registering topbar commands', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
- window['app'].registerExtension({
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ app.registerExtension({
name: 'TestExtension1',
commands: [
{
id: 'foo',
label: 'foo-command',
function: () => {
- window['foo'] = true
+ window.foo = true
}
}
],
@@ -31,7 +33,7 @@ test.describe('Topbar commands', () => {
})
await comfyPage.menu.topbar.triggerTopbarCommand(['ext', 'foo-command'])
- expect(await comfyPage.page.evaluate(() => window['foo'])).toBe(true)
+ expect(await comfyPage.page.evaluate(() => window.foo)).toBe(true)
})
test('Should not allow register command defined in other extension', async ({
@@ -39,7 +41,9 @@ test.describe('Topbar commands', () => {
}) => {
await comfyPage.registerCommand('foo', () => alert(1))
await comfyPage.page.evaluate(() => {
- window['app'].registerExtension({
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ app.registerExtension({
name: 'TestExtension1',
menuCommands: [
{
@@ -57,13 +61,14 @@ test.describe('Topbar commands', () => {
test('Should allow registering keybindings', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
const app = window['app']
+ if (!app) throw new Error('App not initialized')
app.registerExtension({
name: 'TestExtension1',
commands: [
{
id: 'TestCommand',
function: () => {
- window['TestCommand'] = true
+ window.TestCommand = true
}
}
],
@@ -77,15 +82,15 @@ test.describe('Topbar commands', () => {
})
await comfyPage.page.keyboard.press('k')
- expect(await comfyPage.page.evaluate(() => window['TestCommand'])).toBe(
- true
- )
+ expect(await comfyPage.page.evaluate(() => window.TestCommand)).toBe(true)
})
test.describe('Settings', () => {
test('Should allow adding settings', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
- window['app'].registerExtension({
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ app.registerExtension({
name: 'TestExtension1',
settings: [
{
@@ -94,24 +99,30 @@ test.describe('Topbar commands', () => {
type: 'text',
defaultValue: 'Hello, world!',
onChange: () => {
- window['changeCount'] = (window['changeCount'] ?? 0) + 1
+ window.changeCount = (window.changeCount ?? 0) + 1
}
- }
+ } as unknown as SettingParams
]
})
})
// onChange is called when the setting is first added
- expect(await comfyPage.page.evaluate(() => window['changeCount'])).toBe(1)
- expect(await comfyPage.getSetting('TestSetting')).toBe('Hello, world!')
+ expect(await comfyPage.page.evaluate(() => window.changeCount)).toBe(1)
+ expect(await comfyPage.getSetting('TestSetting' as string)).toBe(
+ 'Hello, world!'
+ )
- await comfyPage.setSetting('TestSetting', 'Hello, universe!')
- expect(await comfyPage.getSetting('TestSetting')).toBe('Hello, universe!')
- expect(await comfyPage.page.evaluate(() => window['changeCount'])).toBe(2)
+ await comfyPage.setSetting('TestSetting' as string, 'Hello, universe!')
+ expect(await comfyPage.getSetting('TestSetting' as string)).toBe(
+ 'Hello, universe!'
+ )
+ expect(await comfyPage.page.evaluate(() => window.changeCount)).toBe(2)
})
test('Should allow setting boolean settings', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
- window['app'].registerExtension({
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ app.registerExtension({
name: 'TestExtension1',
settings: [
{
@@ -120,20 +131,26 @@ test.describe('Topbar commands', () => {
type: 'boolean',
defaultValue: false,
onChange: () => {
- window['changeCount'] = (window['changeCount'] ?? 0) + 1
+ window.changeCount = (window.changeCount ?? 0) + 1
}
- }
+ } as unknown as SettingParams
]
})
})
- expect(await comfyPage.getSetting('Comfy.TestSetting')).toBe(false)
- expect(await comfyPage.page.evaluate(() => window['changeCount'])).toBe(1)
+ expect(await comfyPage.getSetting('Comfy.TestSetting' as string)).toBe(
+ false
+ )
+ expect(await comfyPage.page.evaluate(() => window.changeCount)).toBe(1)
await comfyPage.settingDialog.open()
- await comfyPage.settingDialog.toggleBooleanSetting('Comfy.TestSetting')
- expect(await comfyPage.getSetting('Comfy.TestSetting')).toBe(true)
- expect(await comfyPage.page.evaluate(() => window['changeCount'])).toBe(2)
+ await comfyPage.settingDialog.toggleBooleanSetting(
+ 'Comfy.TestSetting' as string
+ )
+ expect(await comfyPage.getSetting('Comfy.TestSetting' as string)).toBe(
+ true
+ )
+ expect(await comfyPage.page.evaluate(() => window.changeCount)).toBe(2)
})
test.describe('Passing through attrs to setting components', () => {
@@ -191,7 +208,9 @@ test.describe('Topbar commands', () => {
comfyPage
}) => {
await comfyPage.page.evaluate((config) => {
- window['app'].registerExtension({
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ app.registerExtension({
name: 'TestExtension1',
settings: [
{
@@ -200,7 +219,7 @@ test.describe('Topbar commands', () => {
// The `disabled` attr is common to all settings components
attrs: { disabled: true },
...config
- }
+ } as SettingParams
]
})
}, config)
@@ -224,7 +243,9 @@ test.describe('Topbar commands', () => {
test.describe('About panel', () => {
test('Should allow adding badges', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
- window['app'].registerExtension({
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ app.registerExtension({
name: 'TestExtension1',
aboutPageBadges: [
{
@@ -247,18 +268,20 @@ test.describe('Topbar commands', () => {
test.describe('Dialog', () => {
test('Should allow showing a prompt dialog', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
- void window['app'].extensionManager.dialog
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ void app.extensionManager.dialog
.prompt({
title: 'Test Prompt',
message: 'Test Prompt Message'
})
- .then((value: string) => {
- window['value'] = value
+ .then((value: string | null) => {
+ window.value = value
})
})
await comfyPage.fillPromptDialog('Hello, world!')
- expect(await comfyPage.page.evaluate(() => window['value'])).toBe(
+ expect(await comfyPage.page.evaluate(() => window.value)).toBe(
'Hello, world!'
)
})
@@ -267,35 +290,39 @@ test.describe('Topbar commands', () => {
comfyPage
}) => {
await comfyPage.page.evaluate(() => {
- void window['app'].extensionManager.dialog
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ void app.extensionManager.dialog
.confirm({
title: 'Test Confirm',
message: 'Test Confirm Message'
})
- .then((value: boolean) => {
- window['value'] = value
+ .then((value: boolean | null) => {
+ window.value = value
})
})
await comfyPage.confirmDialog.click('confirm')
- expect(await comfyPage.page.evaluate(() => window['value'])).toBe(true)
+ expect(await comfyPage.page.evaluate(() => window.value)).toBe(true)
})
test('Should allow dismissing a dialog', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
- window['value'] = 'foo'
- void window['app'].extensionManager.dialog
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ window.value = 'foo'
+ void app.extensionManager.dialog
.confirm({
title: 'Test Confirm',
message: 'Test Confirm Message'
})
- .then((value: boolean) => {
- window['value'] = value
+ .then((value: boolean | null) => {
+ window.value = value
})
})
await comfyPage.confirmDialog.click('reject')
- expect(await comfyPage.page.evaluate(() => window['value'])).toBeNull()
+ expect(await comfyPage.page.evaluate(() => window.value)).toBeNull()
})
})
@@ -309,7 +336,9 @@ test.describe('Topbar commands', () => {
}) => {
// Register an extension with a selection toolbox command
await comfyPage.page.evaluate(() => {
- window['app'].registerExtension({
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ app.registerExtension({
name: 'TestExtension1',
commands: [
{
@@ -317,7 +346,7 @@ test.describe('Topbar commands', () => {
label: 'Test Command',
icon: 'pi pi-star',
function: () => {
- window['selectionCommandExecuted'] = true
+ window.selectionCommandExecuted = true
}
}
],
@@ -335,7 +364,7 @@ test.describe('Topbar commands', () => {
// Verify the command was executed
expect(
- await comfyPage.page.evaluate(() => window['selectionCommandExecuted'])
+ await comfyPage.page.evaluate(() => window.selectionCommandExecuted)
).toBe(true)
})
})
diff --git a/browser_tests/tests/featureFlags.spec.ts b/browser_tests/tests/featureFlags.spec.ts
index 7079768291d..c3ee28fdc68 100644
--- a/browser_tests/tests/featureFlags.spec.ts
+++ b/browser_tests/tests/featureFlags.spec.ts
@@ -2,6 +2,19 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
+declare const window: Window &
+ typeof globalThis & {
+ __capturedMessages?: {
+ clientFeatureFlags: unknown
+ serverFeatureFlags: unknown
+ }
+ __appReadiness?: {
+ featureFlagsReceived: boolean
+ apiInitialized: boolean
+ appInitialized: boolean
+ }
+ }
+
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
@@ -15,8 +28,17 @@ test.describe('Feature Flags', () => {
// Set up monitoring before navigation
await newPage.addInitScript(() => {
+ type WindowWithMessages = Window &
+ typeof globalThis & {
+ __capturedMessages: {
+ clientFeatureFlags: unknown
+ serverFeatureFlags: unknown
+ }
+ app?: { api?: { serverFeatureFlags?: Record } }
+ }
+ const win = window as WindowWithMessages
// This runs before any page scripts
- window.__capturedMessages = {
+ win.__capturedMessages = {
clientFeatureFlags: null,
serverFeatureFlags: null
}
@@ -25,11 +47,13 @@ test.describe('Feature Flags', () => {
const originalSend = WebSocket.prototype.send
WebSocket.prototype.send = function (data) {
try {
- const parsed = JSON.parse(data)
- if (parsed.type === 'feature_flags') {
- window.__capturedMessages.clientFeatureFlags = parsed
+ if (typeof data === 'string') {
+ const parsed = JSON.parse(data)
+ if (parsed.type === 'feature_flags') {
+ win.__capturedMessages.clientFeatureFlags = parsed
+ }
}
- } catch (e) {
+ } catch {
// Not JSON, ignore
}
return originalSend.call(this, data)
@@ -37,12 +61,9 @@ test.describe('Feature Flags', () => {
// Monitor for server feature flags
const checkInterval = setInterval(() => {
- if (
- window['app']?.api?.serverFeatureFlags &&
- Object.keys(window['app'].api.serverFeatureFlags).length > 0
- ) {
- window.__capturedMessages.serverFeatureFlags =
- window['app'].api.serverFeatureFlags
+ const serverFlags = win.app?.api?.serverFeatureFlags
+ if (serverFlags && Object.keys(serverFlags).length > 0) {
+ win.__capturedMessages.serverFeatureFlags = serverFlags
clearInterval(checkInterval)
}
}, 100)
@@ -56,37 +77,58 @@ test.describe('Feature Flags', () => {
// Wait for both client and server feature flags
await newPage.waitForFunction(
- () =>
- window.__capturedMessages.clientFeatureFlags !== null &&
- window.__capturedMessages.serverFeatureFlags !== null,
+ () => {
+ type WindowWithMessages = Window &
+ typeof globalThis & {
+ __capturedMessages?: {
+ clientFeatureFlags: unknown
+ serverFeatureFlags: unknown
+ }
+ }
+ const win = window as WindowWithMessages
+ return (
+ win.__capturedMessages?.clientFeatureFlags !== null &&
+ win.__capturedMessages?.serverFeatureFlags !== null
+ )
+ },
{ timeout: 10000 }
)
// Get the captured messages
- const messages = await newPage.evaluate(() => window.__capturedMessages)
+ const messages = await newPage.evaluate(() => {
+ type WindowWithMessages = Window &
+ typeof globalThis & {
+ __capturedMessages?: {
+ clientFeatureFlags: { type: string; data: Record }
+ serverFeatureFlags: Record
+ }
+ }
+ return (window as WindowWithMessages).__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).toBeTruthy()
+ 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 +138,9 @@ test.describe('Feature Flags', () => {
}) => {
// Get the actual server feature flags from the backend
const serverFlags = await comfyPage.page.evaluate(() => {
- return window['app'].api.serverFeatureFlags
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ return app.api.serverFeatureFlags
})
// Verify we received real feature flags from the backend
@@ -115,24 +159,28 @@ test.describe('Feature Flags', () => {
}) => {
// Test serverSupportsFeature with real backend flags
const supportsPreviewMetadata = await comfyPage.page.evaluate(() => {
- return window['app'].api.serverSupportsFeature(
- 'supports_preview_metadata'
- )
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ return app.api.serverSupportsFeature('supports_preview_metadata')
})
// The method should return a boolean based on the backend's value
expect(typeof supportsPreviewMetadata).toBe('boolean')
// Test non-existent feature - should always return false
const supportsNonExistent = await comfyPage.page.evaluate(() => {
- return window['app'].api.serverSupportsFeature('non_existent_feature_xyz')
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ return 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(() => {
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
// Temporarily modify serverFeatureFlags to test behavior
- const original = window['app'].api.serverFeatureFlags
- window['app'].api.serverFeatureFlags = {
+ const original = app.api.serverFeatureFlags
+ app.api.serverFeatureFlags = {
bool_true: true,
bool_false: false,
string_value: 'yes',
@@ -141,15 +189,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: app.api.serverSupportsFeature('bool_true'),
+ bool_false: app.api.serverSupportsFeature('bool_false'),
+ string_value: app.api.serverSupportsFeature('string_value'),
+ number_value: app.api.serverSupportsFeature('number_value'),
+ null_value: app.api.serverSupportsFeature('null_value')
}
// Restore original
- window['app'].api.serverFeatureFlags = original
+ app.api.serverFeatureFlags = original
return results
})
@@ -166,23 +214,26 @@ test.describe('Feature Flags', () => {
}) => {
// Test getServerFeature method
const previewMetadataValue = await comfyPage.page.evaluate(() => {
- return window['app'].api.getServerFeature('supports_preview_metadata')
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ return 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')
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ return 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(
- 'non_existent_feature_xyz',
- 'default'
- )
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ return app.api.getServerFeature('non_existent_feature_xyz', 'default')
})
expect(defaultValue).toBe('default')
})
@@ -192,7 +243,9 @@ test.describe('Feature Flags', () => {
}) => {
// Test getServerFeatures returns all flags
const allFeatures = await comfyPage.page.evaluate(() => {
- return window['app'].api.getServerFeatures()
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ return app.api.getServerFeatures()
})
expect(allFeatures).toBeTruthy()
@@ -205,14 +258,16 @@ 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 app = window['app']
+ if (!app) throw new Error('App not initialized')
+ const flags1 = app.api.getClientFeatureFlags()
+ const flags2 = 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 = app.api.getClientFeatureFlags()
return {
areEqual: flags1 === flags2,
@@ -237,15 +292,17 @@ test.describe('Feature Flags', () => {
comfyPage
}) => {
const immutabilityTest = await comfyPage.page.evaluate(() => {
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
// Get a copy of server features
- const features1 = window['app'].api.getServerFeatures()
+ const features1 = 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 = app.api.getServerFeatures()
return {
modifiedValue: features1.supports_preview_metadata,
@@ -330,9 +387,11 @@ test.describe('Feature Flags', () => {
// Get readiness state
const readiness = await newPage.evaluate(() => {
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
return {
...(window as any).__appReadiness,
- currentFlags: window['app'].api.serverFeatureFlags
+ currentFlags: app.api.serverFeatureFlags
}
})
diff --git a/browser_tests/tests/graph.spec.ts b/browser_tests/tests/graph.spec.ts
index 0c021b5f2be..ebabf289232 100644
--- a/browser_tests/tests/graph.spec.ts
+++ b/browser_tests/tests/graph.spec.ts
@@ -13,7 +13,9 @@ test.describe('Graph', () => {
await comfyPage.loadWorkflow('inputs/input_order_swap')
expect(
await comfyPage.page.evaluate(() => {
- return window['app'].graph.links.get(1)?.target_slot
+ const app = window['app']
+ if (!app?.graph) throw new Error('App not initialized')
+ return app.graph.links.get(1)?.target_slot
})
).toBe(1)
})
diff --git a/browser_tests/tests/graphCanvasMenu.spec.ts b/browser_tests/tests/graphCanvasMenu.spec.ts
index fc8385e497c..84c9477d6fe 100644
--- a/browser_tests/tests/graphCanvasMenu.spec.ts
+++ b/browser_tests/tests/graphCanvasMenu.spec.ts
@@ -23,7 +23,9 @@ test.describe('Graph Canvas Menu', () => {
'canvas-with-hidden-links.png'
)
const hiddenLinkRenderMode = await comfyPage.page.evaluate(() => {
- return window['LiteGraph'].HIDDEN_LINK
+ const LiteGraph = window['LiteGraph']
+ if (!LiteGraph) throw new Error('LiteGraph not initialized')
+ return LiteGraph.HIDDEN_LINK
})
expect(await comfyPage.getSetting('Comfy.LinkRenderMode')).toBe(
hiddenLinkRenderMode
diff --git a/browser_tests/tests/groupNode.spec.ts b/browser_tests/tests/groupNode.spec.ts
index 9d67a0951a5..4dad05254e8 100644
--- a/browser_tests/tests/groupNode.spec.ts
+++ b/browser_tests/tests/groupNode.spec.ts
@@ -2,6 +2,7 @@ import { expect } from '@playwright/test'
import type { ComfyPage } from '../fixtures/ComfyPage'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
+import type { NodeLibrarySidebarTab } from '../fixtures/components/SidebarTab'
import type { NodeReference } from '../fixtures/utils/litegraphUtils'
test.beforeEach(async ({ comfyPage }) => {
@@ -13,7 +14,7 @@ test.describe('Group Node', () => {
const groupNodeName = 'DefautWorkflowGroupNode'
const groupNodeCategory = 'group nodes>workflow'
const groupNodeBookmarkName = `workflow>${groupNodeName}`
- let libraryTab
+ let libraryTab: NodeLibrarySidebarTab
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
@@ -22,7 +23,7 @@ test.describe('Group Node', () => {
await libraryTab.open()
})
- test('Is added to node library sidebar', async ({ comfyPage }) => {
+ test('Is added to node library sidebar', async () => {
expect(await libraryTab.getFolder('group nodes').count()).toBe(1)
})
@@ -110,7 +111,7 @@ test.describe('Group Node', () => {
test('Manage group opens with the correct group selected', async ({
comfyPage
}) => {
- const makeGroup = async (name, type1, type2) => {
+ const makeGroup = async (name: string, type1: string, type2: string) => {
const node1 = (await comfyPage.getNodeRefsByType(type1))[0]
const node2 = (await comfyPage.getNodeRefsByType(type2))[0]
await node1.click('title')
@@ -149,17 +150,27 @@ test.describe('Group Node', () => {
const groupNodeName = 'two_VAE_decode'
const totalInputCount = await comfyPage.page.evaluate((nodeName) => {
- const {
- extra: { groupNodes }
- } = window['app'].graph
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ const graph = app.graph
+ if (!graph?.extra) throw new Error('Graph extra not initialized')
+ const groupNodes = graph.extra.groupNodes as
+ | Record }>
+ | undefined
+ if (!groupNodes?.[nodeName]) throw new Error('Group node not found')
const { nodes } = groupNodes[nodeName]
- return nodes.reduce((acc: number, node) => {
- return acc + node.inputs.length
- }, 0)
+ return nodes.reduce(
+ (acc: number, node: { inputs?: unknown[] }) =>
+ acc + (node.inputs?.length ?? 0),
+ 0
+ )
}, groupNodeName)
const visibleInputCount = await comfyPage.page.evaluate((id) => {
- const node = window['app'].graph.getNodeById(id)
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ const node = app.graph?.getNodeById(id)
+ if (!node) throw new Error('Node not found')
return node.inputs.length
}, groupNodeId)
@@ -226,7 +237,9 @@ test.describe('Group Node', () => {
const isRegisteredLitegraph = async (comfyPage: ComfyPage) => {
return await comfyPage.page.evaluate((nodeType: string) => {
- return !!window['LiteGraph'].registered_node_types[nodeType]
+ const lg = window['LiteGraph']
+ if (!lg) throw new Error('LiteGraph not initialized')
+ return !!lg.registered_node_types[nodeType]
}, GROUP_NODE_TYPE)
}
@@ -299,15 +312,20 @@ test.describe('Group Node', () => {
}) => {
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])
await comfyPage.ctrlV()
- const currentGraphState = await comfyPage.page.evaluate(() =>
- window['app'].graph.serialize()
- )
+ const currentGraphState = await comfyPage.page.evaluate(() => {
+ const app = window['app']
+ if (!app?.graph) throw new Error('App or graph not initialized')
+ return app.graph.serialize()
+ })
await test.step('Load workflow containing a group node pasted from a different workflow', async () => {
- await comfyPage.page.evaluate(
- (workflow) => window['app'].loadGraphData(workflow),
- currentGraphState
- )
+ await comfyPage.page.evaluate((workflow) => {
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ return app.loadGraphData(
+ workflow as Parameters[0]
+ )
+ }, currentGraphState)
await comfyPage.nextFrame()
await verifyNodeLoaded(comfyPage, 1)
})
diff --git a/browser_tests/tests/keybindings.spec.ts b/browser_tests/tests/keybindings.spec.ts
index f4244ae669a..dd03ca5909b 100644
--- a/browser_tests/tests/keybindings.spec.ts
+++ b/browser_tests/tests/keybindings.spec.ts
@@ -11,14 +11,14 @@ test.describe('Keybindings', () => {
comfyPage
}) => {
await comfyPage.registerKeybinding({ key: 'k' }, () => {
- window['TestCommand'] = true
+ window.TestCommand = true
})
const textBox = comfyPage.widgetTextBox
await textBox.click()
await textBox.fill('k')
await expect(textBox).toHaveValue('k')
- expect(await comfyPage.page.evaluate(() => window['TestCommand'])).toBe(
+ expect(await comfyPage.page.evaluate(() => window.TestCommand)).toBe(
undefined
)
})
@@ -27,7 +27,7 @@ test.describe('Keybindings', () => {
comfyPage
}) => {
await comfyPage.registerKeybinding({ key: 'k', ctrl: true }, () => {
- window['TestCommand'] = true
+ window.TestCommand = true
})
const textBox = comfyPage.widgetTextBox
@@ -35,23 +35,21 @@ test.describe('Keybindings', () => {
await textBox.fill('q')
await textBox.press('Control+k')
await expect(textBox).toHaveValue('q')
- expect(await comfyPage.page.evaluate(() => window['TestCommand'])).toBe(
- true
- )
+ expect(await comfyPage.page.evaluate(() => window.TestCommand)).toBe(true)
})
test('Should not trigger keybinding reserved by text input when typing in input fields', async ({
comfyPage
}) => {
await comfyPage.registerKeybinding({ key: 'Ctrl+v' }, () => {
- window['TestCommand'] = true
+ window.TestCommand = true
})
const textBox = comfyPage.widgetTextBox
await textBox.click()
await textBox.press('Control+v')
await expect(textBox).toBeFocused()
- expect(await comfyPage.page.evaluate(() => window['TestCommand'])).toBe(
+ expect(await comfyPage.page.evaluate(() => window.TestCommand)).toBe(
undefined
)
})
diff --git a/browser_tests/tests/lodThreshold.spec.ts b/browser_tests/tests/lodThreshold.spec.ts
index 154ac3c16f6..2c3778f0ef2 100644
--- a/browser_tests/tests/lodThreshold.spec.ts
+++ b/browser_tests/tests/lodThreshold.spec.ts
@@ -15,7 +15,9 @@ test.describe('LOD Threshold', () => {
// Get initial LOD state and settings
const initialState = await comfyPage.page.evaluate(() => {
- const canvas = window['app'].canvas
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ const canvas = app.canvas
return {
lowQuality: canvas.low_quality,
scale: canvas.ds.scale,
@@ -36,7 +38,9 @@ test.describe('LOD Threshold', () => {
await comfyPage.nextFrame()
const aboveThresholdState = await comfyPage.page.evaluate(() => {
- const canvas = window['app'].canvas
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ const canvas = app.canvas
return {
lowQuality: canvas.low_quality,
scale: canvas.ds.scale
@@ -54,7 +58,9 @@ test.describe('LOD Threshold', () => {
// Check that LOD is now active
const zoomedOutState = await comfyPage.page.evaluate(() => {
- const canvas = window['app'].canvas
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ const canvas = app.canvas
return {
lowQuality: canvas.low_quality,
scale: canvas.ds.scale
@@ -70,7 +76,9 @@ test.describe('LOD Threshold', () => {
// Check that LOD is now inactive
const zoomedInState = await comfyPage.page.evaluate(() => {
- const canvas = window['app'].canvas
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ const canvas = app.canvas
return {
lowQuality: canvas.low_quality,
scale: canvas.ds.scale
@@ -91,7 +99,9 @@ test.describe('LOD Threshold', () => {
// Check that font size updated
const newState = await comfyPage.page.evaluate(() => {
- const canvas = window['app'].canvas
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ const canvas = app.canvas
return {
minFontSize: canvas.min_font_size_for_lod
}
@@ -102,7 +112,9 @@ test.describe('LOD Threshold', () => {
// At default zoom, LOD should still be inactive (scale is exactly 1.0, not less than)
const lodState = await comfyPage.page.evaluate(() => {
- return window['app'].canvas.low_quality
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ return app.canvas.low_quality
})
expect(lodState).toBe(false)
@@ -111,7 +123,9 @@ test.describe('LOD Threshold', () => {
await comfyPage.nextFrame()
const afterZoom = await comfyPage.page.evaluate(() => {
- const canvas = window['app'].canvas
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ const canvas = app.canvas
return {
lowQuality: canvas.low_quality,
scale: canvas.ds.scale
@@ -136,7 +150,9 @@ test.describe('LOD Threshold', () => {
// LOD should remain disabled even at very low zoom
const state = await comfyPage.page.evaluate(() => {
- const canvas = window['app'].canvas
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ const canvas = app.canvas
return {
lowQuality: canvas.low_quality,
scale: canvas.ds.scale,
@@ -160,8 +176,10 @@ test.describe('LOD Threshold', () => {
// Zoom to target level
await comfyPage.page.evaluate((zoom) => {
- window['app'].canvas.ds.scale = zoom
- window['app'].canvas.setDirty(true, true)
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ app.canvas.ds.scale = zoom
+ app.canvas.setDirty(true, true)
}, targetZoom)
await comfyPage.nextFrame()
@@ -171,7 +189,9 @@ test.describe('LOD Threshold', () => {
)
const lowQualityState = await comfyPage.page.evaluate(() => {
- const canvas = window['app'].canvas
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ const canvas = app.canvas
return {
lowQuality: canvas.low_quality,
scale: canvas.ds.scale
@@ -189,7 +209,9 @@ test.describe('LOD Threshold', () => {
)
const highQualityState = await comfyPage.page.evaluate(() => {
- const canvas = window['app'].canvas
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ const canvas = app.canvas
return {
lowQuality: canvas.low_quality,
scale: canvas.ds.scale
diff --git a/browser_tests/tests/menu.spec.ts b/browser_tests/tests/menu.spec.ts
index 1ba1e55240d..0fc07c6f824 100644
--- a/browser_tests/tests/menu.spec.ts
+++ b/browser_tests/tests/menu.spec.ts
@@ -11,7 +11,9 @@ test.describe('Menu', () => {
const initialChildrenCount = await comfyPage.menu.buttons.count()
await comfyPage.page.evaluate(async () => {
- window['app'].extensionManager.registerSidebarTab({
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ app.extensionManager.registerSidebarTab({
id: 'search',
icon: 'pi pi-search',
title: 'search',
@@ -155,7 +157,9 @@ test.describe('Menu', () => {
test('Can catch error when executing command', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
- window['app'].registerExtension({
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ app.registerExtension({
name: 'TestExtension1',
commands: [
{
diff --git a/browser_tests/tests/nodeBadge.spec.ts b/browser_tests/tests/nodeBadge.spec.ts
index 111efe29cf8..76d4896404e 100644
--- a/browser_tests/tests/nodeBadge.spec.ts
+++ b/browser_tests/tests/nodeBadge.spec.ts
@@ -12,7 +12,9 @@ test.describe('Node Badge', () => {
test('Can add badge', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
const LGraphBadge = window['LGraphBadge']
+ if (!LGraphBadge) throw new Error('LGraphBadge not initialized')
const app = window['app'] as ComfyApp
+ if (!app?.graph) throw new Error('App not initialized')
const graph = app.graph
const nodes = graph.nodes
@@ -29,7 +31,9 @@ test.describe('Node Badge', () => {
test('Can add multiple badges', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
const LGraphBadge = window['LGraphBadge']
+ if (!LGraphBadge) throw new Error('LGraphBadge not initialized')
const app = window['app'] as ComfyApp
+ if (!app?.graph) throw new Error('App not initialized')
const graph = app.graph
const nodes = graph.nodes
@@ -49,7 +53,9 @@ test.describe('Node Badge', () => {
test('Can add badge left-side', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
const LGraphBadge = window['LGraphBadge']
+ if (!LGraphBadge) throw new Error('LGraphBadge not initialized')
const app = window['app'] as ComfyApp
+ if (!app?.graph) throw new Error('App not initialized')
const graph = app.graph
const nodes = graph.nodes
diff --git a/browser_tests/tests/nodeDisplay.spec.ts b/browser_tests/tests/nodeDisplay.spec.ts
index fdaae14bcb1..dec3a5c6bbf 100644
--- a/browser_tests/tests/nodeDisplay.spec.ts
+++ b/browser_tests/tests/nodeDisplay.spec.ts
@@ -1,5 +1,7 @@
import { expect } from '@playwright/test'
+import type { INodeInputSlot } from '@/lib/litegraph/src/interfaces'
+
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
@@ -49,20 +51,24 @@ test.describe('Optional input', () => {
test('Old workflow with converted input', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/old_workflow_converted_input')
const node = await comfyPage.getNodeRefById('1')
- const inputs = await node.getProperty('inputs')
- const vaeInput = inputs.find((w) => w.name === 'vae')
- const convertedInput = inputs.find((w) => w.name === 'strength')
+ const inputs = await node.getProperty('inputs')
+ const vaeInput = inputs.find((w: INodeInputSlot) => w.name === 'vae')
+ const convertedInput = inputs.find(
+ (w: INodeInputSlot) => w.name === 'strength'
+ )
expect(vaeInput).toBeDefined()
expect(convertedInput).toBeDefined()
- expect(vaeInput.link).toBeNull()
- expect(convertedInput.link).not.toBeNull()
+ expect(vaeInput!.link).toBeNull()
+ expect(convertedInput!.link).not.toBeNull()
})
test('Renamed converted input', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/renamed_converted_widget')
const node = await comfyPage.getNodeRefById('3')
- const inputs = await node.getProperty('inputs')
- const renamedInput = inputs.find((w) => w.name === 'breadth')
+ const inputs = await node.getProperty('inputs')
+ const renamedInput = inputs.find(
+ (w: INodeInputSlot) => w.name === 'breadth'
+ )
expect(renamedInput).toBeUndefined()
})
test('slider', async ({ comfyPage }) => {
diff --git a/browser_tests/tests/nodeHelp.spec.ts b/browser_tests/tests/nodeHelp.spec.ts
index cfb04bc4635..370f08f0e06 100644
--- a/browser_tests/tests/nodeHelp.spec.ts
+++ b/browser_tests/tests/nodeHelp.spec.ts
@@ -9,8 +9,9 @@ import { fitToViewInstant } from '../helpers/fitToView'
async function selectNodeWithPan(comfyPage: any, nodeRef: any) {
const nodePos = await nodeRef.getPosition()
- await comfyPage.page.evaluate((pos) => {
+ await comfyPage.page.evaluate((pos: { x: number; y: number }) => {
const app = window['app']
+ if (!app) throw new Error('App not initialized')
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 +346,10 @@ 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)
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ if (!app.graph) throw new Error('Graph not initialized')
+ return app.graph.nodes.map((n: any) => n.id)
})
if (nodeRefs.length > 0) {
const firstNode = await comfyPage.getNodeRefById(nodeRefs[0])
diff --git a/browser_tests/tests/nodeSearchBox.spec.ts b/browser_tests/tests/nodeSearchBox.spec.ts
index 2eea2232909..4395636db4d 100644
--- a/browser_tests/tests/nodeSearchBox.spec.ts
+++ b/browser_tests/tests/nodeSearchBox.spec.ts
@@ -1,3 +1,4 @@
+import type { ComfyPage } from '../fixtures/ComfyPage'
import {
comfyExpect as expect,
comfyPageFixture as test
@@ -126,7 +127,10 @@ test.describe('Node search box', () => {
})
test.describe('Filtering', () => {
- const expectFilterChips = async (comfyPage, expectedTexts: string[]) => {
+ const expectFilterChips = async (
+ comfyPage: ComfyPage,
+ expectedTexts: string[]
+ ) => {
const chips = comfyPage.searchBox.filterChips
// Check that the number of chips matches the expected count
diff --git a/browser_tests/tests/remoteWidgets.spec.ts b/browser_tests/tests/remoteWidgets.spec.ts
index 18cc77f4459..6da21501ada 100644
--- a/browser_tests/tests/remoteWidgets.spec.ts
+++ b/browser_tests/tests/remoteWidgets.spec.ts
@@ -25,24 +25,33 @@ test.describe('Remote COMBO Widget', () => {
comfyPage: ComfyPage,
nodeName: string
): Promise => {
- return await comfyPage.page.evaluate((name) => {
- const node = window['app'].graph.nodes.find((node) => node.title === name)
- return node.widgets[0].options.values
+ return await comfyPage.page.evaluate((name): string[] | undefined => {
+ const app = window['app']
+ if (!app?.graph) throw new Error('App not initialized')
+ const node = app.graph.nodes.find((node) => node.title === name)
+ if (!node?.widgets) throw new Error('Node or widgets not found')
+ return node.widgets[0].options.values as string[] | undefined
}, nodeName)
}
const getWidgetValue = async (comfyPage: ComfyPage, nodeName: string) => {
return await comfyPage.page.evaluate((name) => {
- const node = window['app'].graph.nodes.find((node) => node.title === name)
+ const app = window['app']
+ if (!app?.graph) throw new Error('App not initialized')
+ const node = app.graph.nodes.find((node) => node.title === name)
+ if (!node?.widgets) throw new Error('Node or widgets not found')
return node.widgets[0].value
}, nodeName)
}
const clickRefreshButton = (comfyPage: ComfyPage, nodeName: string) => {
return comfyPage.page.evaluate((name) => {
- const node = window['app'].graph.nodes.find((node) => node.title === name)
+ const app = window['app']
+ if (!app?.graph) throw new Error('App not initialized')
+ const node = app.graph.nodes.find((node) => node.title === name)
+ if (!node?.widgets) throw new Error('Node or widgets not found')
const buttonWidget = node.widgets.find((w) => w.name === 'refresh')
- return buttonWidget?.callback()
+ buttonWidget?.callback?.(buttonWidget.value, undefined, node)
}, nodeName)
}
@@ -92,7 +101,9 @@ test.describe('Remote COMBO Widget', () => {
await comfyPage.loadWorkflow('inputs/remote_widget')
const node = await comfyPage.page.evaluate((name) => {
- return window['app'].graph.nodes.find((node) => node.title === name)
+ const app = window['app']
+ if (!app?.graph) throw new Error('App not initialized')
+ return app.graph.nodes.find((node) => node.title === name)
}, nodeName)
expect(node).toBeDefined()
@@ -196,7 +207,7 @@ test.describe('Remote COMBO Widget', () => {
// Fulfill each request with a unique timestamp
await comfyPage.page.route(
'**/api/models/checkpoints**',
- async (route, request) => {
+ async (route, _request) => {
await route.fulfill({
body: JSON.stringify([Date.now()]),
status: 200
diff --git a/browser_tests/tests/sidebar/nodeLibrary.spec.ts b/browser_tests/tests/sidebar/nodeLibrary.spec.ts
index 4f476376ac1..b4fcc3eea82 100644
--- a/browser_tests/tests/sidebar/nodeLibrary.spec.ts
+++ b/browser_tests/tests/sidebar/nodeLibrary.spec.ts
@@ -265,13 +265,14 @@ test.describe('Node library sidebar', () => {
await comfyPage.nextFrame()
// Verify the color selection is saved
- const setting = await comfyPage.getSetting(
+ const setting = (await comfyPage.getSetting(
'Comfy.NodeLibrary.BookmarksCustomization'
- )
- await expect(setting).toHaveProperty(['foo/', 'color'])
- await expect(setting['foo/'].color).not.toBeNull()
- await expect(setting['foo/'].color).not.toBeUndefined()
- await expect(setting['foo/'].color).not.toBe('')
+ )) as Record | undefined
+ expect(setting).toHaveProperty(['foo/', 'color'])
+ expect(setting?.['foo/']).toBeDefined()
+ expect(setting?.['foo/']?.color).not.toBeNull()
+ expect(setting?.['foo/']?.color).not.toBeUndefined()
+ expect(setting?.['foo/']?.color).not.toBe('')
})
test('Can rename customized bookmark folder', async ({ comfyPage }) => {
diff --git a/browser_tests/tests/sidebar/workflows.spec.ts b/browser_tests/tests/sidebar/workflows.spec.ts
index 86f37b23db8..09c9bc53efc 100644
--- a/browser_tests/tests/sidebar/workflows.spec.ts
+++ b/browser_tests/tests/sidebar/workflows.spec.ts
@@ -139,12 +139,15 @@ test.describe('Workflows sidebar', () => {
api: false
})
expect(exportedWorkflow).toBeDefined()
- for (const node of exportedWorkflow.nodes) {
- for (const slot of node.inputs) {
+ if (!exportedWorkflow) return
+ const nodes = exportedWorkflow.nodes
+ if (!Array.isArray(nodes)) return
+ for (const node of nodes) {
+ for (const slot of node.inputs ?? []) {
expect(slot.localized_name).toBeUndefined()
expect(slot.label).toBeUndefined()
}
- for (const slot of node.outputs) {
+ for (const slot of node.outputs ?? []) {
expect(slot.localized_name).toBeUndefined()
expect(slot.label).toBeUndefined()
}
@@ -177,9 +180,13 @@ test.describe('Workflows sidebar', () => {
})
// Compare the exported workflow with the original
+ expect(downloadedContent).toBeDefined()
+ expect(downloadedContentZh).toBeDefined()
+ if (!downloadedContent || !downloadedContentZh) {
+ throw new Error('Downloaded workflow content is undefined')
+ }
delete downloadedContent.id
delete downloadedContentZh.id
- expect(downloadedContent).toBeDefined()
expect(downloadedContent).toEqual(downloadedContentZh)
})
diff --git a/browser_tests/tests/subgraph-rename-dialog.spec.ts b/browser_tests/tests/subgraph-rename-dialog.spec.ts
index 0bdb9766dff..fc57a981016 100644
--- a/browser_tests/tests/subgraph-rename-dialog.spec.ts
+++ b/browser_tests/tests/subgraph-rename-dialog.spec.ts
@@ -3,7 +3,6 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
// Constants
-const INITIAL_NAME = 'initial_slot_name'
const RENAMED_NAME = 'renamed_slot_name'
const SECOND_RENAMED_NAME = 'second_renamed_name'
@@ -12,6 +11,39 @@ const SELECTORS = {
promptDialog: '.graphdialog input'
} as const
+interface SubgraphSlot {
+ label?: string
+ name?: string
+ displayName?: string
+}
+
+interface SubgraphGraph {
+ inputs: SubgraphSlot[]
+ outputs: SubgraphSlot[]
+}
+
+function getSubgraph(): SubgraphGraph {
+ const assertSubgraph = (graph: unknown): asserts graph is SubgraphGraph => {
+ if (
+ graph === null ||
+ typeof graph !== 'object' ||
+ !('inputs' in graph) ||
+ !('outputs' in graph) ||
+ !Array.isArray((graph as { inputs: unknown }).inputs) ||
+ !Array.isArray((graph as { outputs: unknown }).outputs)
+ ) {
+ throw new Error('Not in subgraph')
+ }
+ }
+ const app = window['app']
+ if (!app) throw new Error('App not available')
+ const canvas = app.canvas
+ if (!canvas) throw new Error('Canvas not available')
+ const graph = canvas.graph
+ assertSubgraph(graph)
+ return graph
+}
+
test.describe('Subgraph Slot Rename Dialog', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
@@ -26,13 +58,13 @@ test.describe('Subgraph Slot Rename Dialog', () => {
await subgraphNode.navigateIntoSubgraph()
// Get initial slot label
- const initialInputLabel = await comfyPage.page.evaluate(() => {
- const graph = window['app'].canvas.graph
- return graph.inputs?.[0]?.label || graph.inputs?.[0]?.name || null
- })
+ const initialInputLabel = await comfyPage.page.evaluate((getSubgraph) => {
+ const graph = getSubgraph()
+ return graph.inputs[0]?.label || graph.inputs[0]?.name || null
+ }, getSubgraph)
// First rename
- await comfyPage.rightClickSubgraphInputSlot(initialInputLabel)
+ await comfyPage.rightClickSubgraphInputSlot(initialInputLabel ?? undefined)
await comfyPage.clickLitegraphContextMenuItem('Rename Slot')
await comfyPage.page.waitForSelector(SELECTORS.promptDialog, {
@@ -54,15 +86,15 @@ test.describe('Subgraph Slot Rename Dialog', () => {
await comfyPage.nextFrame()
// Verify the rename worked
- const afterFirstRename = await comfyPage.page.evaluate(() => {
- const graph = window['app'].canvas.graph
- const slot = graph.inputs?.[0]
+ const afterFirstRename = await comfyPage.page.evaluate((getSubgraph) => {
+ const graph = getSubgraph()
+ const slot = graph.inputs[0]
return {
label: slot?.label || null,
name: slot?.name || null,
displayName: slot?.displayName || slot?.label || slot?.name || null
}
- })
+ }, getSubgraph)
expect(afterFirstRename.label).toBe(RENAMED_NAME)
// Now rename again - this is where the bug would show
@@ -96,10 +128,10 @@ test.describe('Subgraph Slot Rename Dialog', () => {
await comfyPage.nextFrame()
// Verify the second rename worked
- const afterSecondRename = await comfyPage.page.evaluate(() => {
- const graph = window['app'].canvas.graph
- return graph.inputs?.[0]?.label || null
- })
+ const afterSecondRename = await comfyPage.page.evaluate((getSubgraph) => {
+ const graph = getSubgraph()
+ return graph.inputs[0]?.label || null
+ }, getSubgraph)
expect(afterSecondRename).toBe(SECOND_RENAMED_NAME)
})
@@ -112,13 +144,15 @@ test.describe('Subgraph Slot Rename Dialog', () => {
await subgraphNode.navigateIntoSubgraph()
// Get initial output slot label
- const initialOutputLabel = await comfyPage.page.evaluate(() => {
- const graph = window['app'].canvas.graph
- return graph.outputs?.[0]?.label || graph.outputs?.[0]?.name || null
- })
+ const initialOutputLabel = await comfyPage.page.evaluate((getSubgraph) => {
+ const graph = getSubgraph()
+ return graph.outputs[0]?.label || graph.outputs[0]?.name || null
+ }, getSubgraph)
// First rename
- await comfyPage.rightClickSubgraphOutputSlot(initialOutputLabel)
+ await comfyPage.rightClickSubgraphOutputSlot(
+ initialOutputLabel ?? undefined
+ )
await comfyPage.clickLitegraphContextMenuItem('Rename Slot')
await comfyPage.page.waitForSelector(SELECTORS.promptDialog, {
diff --git a/browser_tests/tests/subgraph.spec.ts b/browser_tests/tests/subgraph.spec.ts
index 6359156211c..8ae3580a78f 100644
--- a/browser_tests/tests/subgraph.spec.ts
+++ b/browser_tests/tests/subgraph.spec.ts
@@ -1,6 +1,7 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
+import type { SubgraphGraphWithNodes } from '../fixtures/utils/subgraphUtils'
// Constants
const RENAMED_INPUT_NAME = 'renamed_input'
@@ -26,8 +27,14 @@ test.describe('Subgraph Operations', () => {
comfyPage: typeof test.prototype.comfyPage,
type: 'inputs' | 'outputs'
): Promise {
- return await comfyPage.page.evaluate((slotType) => {
- return window['app'].canvas.graph[slotType]?.length || 0
+ return await comfyPage.page.evaluate((slotType: 'inputs' | 'outputs') => {
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ const graph = app.canvas.graph
+ if (!graph || !(slotType in graph)) return 0
+ return (
+ (graph as unknown as Record)[slotType]?.length || 0
+ )
}, type)
}
@@ -36,7 +43,11 @@ test.describe('Subgraph Operations', () => {
comfyPage: typeof test.prototype.comfyPage
): Promise {
return await comfyPage.page.evaluate(() => {
- return window['app'].canvas.graph.nodes?.length || 0
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ const graph = app.canvas.graph
+ if (!graph) return 0
+ return graph.nodes?.length || 0
})
}
@@ -45,7 +56,9 @@ test.describe('Subgraph Operations', () => {
comfyPage: typeof test.prototype.comfyPage
): Promise {
return await comfyPage.page.evaluate(() => {
- const graph = window['app'].canvas.graph
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ const graph = app.canvas.graph
return graph?.constructor?.name === 'Subgraph'
})
}
@@ -130,11 +143,16 @@ test.describe('Subgraph Operations', () => {
await subgraphNode.navigateIntoSubgraph()
const initialInputLabel = await comfyPage.page.evaluate(() => {
- const graph = window['app'].canvas.graph
- return graph.inputs?.[0]?.label || null
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ const graph = app.canvas.graph
+ if (!graph || !('inputs' in graph)) return null
+ return (graph.inputs as { label?: string }[])?.[0]?.label ?? null
})
- await comfyPage.rightClickSubgraphInputSlot(initialInputLabel)
+ await comfyPage.rightClickSubgraphInputSlot(
+ initialInputLabel ?? undefined
+ )
await comfyPage.clickLitegraphContextMenuItem('Rename Slot')
await comfyPage.page.waitForSelector(SELECTORS.promptDialog, {
@@ -148,8 +166,11 @@ test.describe('Subgraph Operations', () => {
await comfyPage.nextFrame()
const newInputName = await comfyPage.page.evaluate(() => {
- const graph = window['app'].canvas.graph
- return graph.inputs?.[0]?.label || null
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ const graph = app.canvas.graph
+ if (!graph || !('inputs' in graph)) return null
+ return (graph.inputs as { label?: string }[])?.[0]?.label ?? null
})
expect(newInputName).toBe(RENAMED_INPUT_NAME)
@@ -163,11 +184,16 @@ test.describe('Subgraph Operations', () => {
await subgraphNode.navigateIntoSubgraph()
const initialInputLabel = await comfyPage.page.evaluate(() => {
- const graph = window['app'].canvas.graph
- return graph.inputs?.[0]?.label || null
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ const graph = app.canvas.graph
+ if (!graph || !('inputs' in graph)) return null
+ return (graph.inputs as { label?: string }[])?.[0]?.label ?? null
})
- await comfyPage.doubleClickSubgraphInputSlot(initialInputLabel)
+ await comfyPage.doubleClickSubgraphInputSlot(
+ initialInputLabel ?? undefined
+ )
await comfyPage.page.waitForSelector(SELECTORS.promptDialog, {
state: 'visible'
@@ -180,8 +206,11 @@ test.describe('Subgraph Operations', () => {
await comfyPage.nextFrame()
const newInputName = await comfyPage.page.evaluate(() => {
- const graph = window['app'].canvas.graph
- return graph.inputs?.[0]?.label || null
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ const graph = app.canvas.graph
+ if (!graph || !('inputs' in graph)) return null
+ return (graph.inputs as { label?: string }[])?.[0]?.label ?? null
})
expect(newInputName).toBe(RENAMED_INPUT_NAME)
@@ -195,11 +224,16 @@ test.describe('Subgraph Operations', () => {
await subgraphNode.navigateIntoSubgraph()
const initialOutputLabel = await comfyPage.page.evaluate(() => {
- const graph = window['app'].canvas.graph
- return graph.outputs?.[0]?.label || null
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ const graph = app.canvas.graph
+ if (!graph || !('outputs' in graph)) return null
+ return (graph.outputs as { label?: string }[])?.[0]?.label ?? null
})
- await comfyPage.doubleClickSubgraphOutputSlot(initialOutputLabel)
+ await comfyPage.doubleClickSubgraphOutputSlot(
+ initialOutputLabel ?? undefined
+ )
await comfyPage.page.waitForSelector(SELECTORS.promptDialog, {
state: 'visible'
@@ -213,8 +247,11 @@ test.describe('Subgraph Operations', () => {
await comfyPage.nextFrame()
const newOutputName = await comfyPage.page.evaluate(() => {
- const graph = window['app'].canvas.graph
- return graph.outputs?.[0]?.label || null
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ const graph = app.canvas.graph
+ if (!graph || !('outputs' in graph)) return null
+ return (graph.outputs as { label?: string }[])?.[0]?.label ?? null
})
expect(newOutputName).toBe(renamedOutputName)
@@ -230,12 +267,17 @@ test.describe('Subgraph Operations', () => {
await subgraphNode.navigateIntoSubgraph()
const initialInputLabel = await comfyPage.page.evaluate(() => {
- const graph = window['app'].canvas.graph
- return graph.inputs?.[0]?.label || null
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ const graph = app.canvas.graph
+ if (!graph || !('inputs' in graph)) return null
+ return (graph.inputs as { label?: string }[])?.[0]?.label ?? null
})
// Test that right-click still works for renaming
- await comfyPage.rightClickSubgraphInputSlot(initialInputLabel)
+ await comfyPage.rightClickSubgraphInputSlot(
+ initialInputLabel ?? undefined
+ )
await comfyPage.clickLitegraphContextMenuItem('Rename Slot')
await comfyPage.page.waitForSelector(SELECTORS.promptDialog, {
@@ -250,8 +292,11 @@ test.describe('Subgraph Operations', () => {
await comfyPage.nextFrame()
const newInputName = await comfyPage.page.evaluate(() => {
- const graph = window['app'].canvas.graph
- return graph.inputs?.[0]?.label || null
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ const graph = app.canvas.graph
+ if (!graph || !('inputs' in graph)) return null
+ return (graph.inputs as { label?: string }[])?.[0]?.label ?? null
})
expect(newInputName).toBe(rightClickRenamedName)
@@ -267,14 +312,21 @@ test.describe('Subgraph Operations', () => {
await subgraphNode.navigateIntoSubgraph()
const initialInputLabel = await comfyPage.page.evaluate(() => {
- const graph = window['app'].canvas.graph
- return graph.inputs?.[0]?.label || null
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ const graph = app.canvas.graph
+ if (!graph || !('inputs' in graph)) return null
+ return (graph.inputs as { label?: string }[])?.[0]?.label ?? null
})
// Use direct pointer event approach to double-click on label
await comfyPage.page.evaluate(() => {
const app = window['app']
- const graph = app.canvas.graph
+ if (!app) throw new Error('App not initialized')
+ const graph = app.canvas.graph as SubgraphGraphWithNodes | null
+ if (!graph || !('inputs' in graph)) {
+ throw new Error('Not in a subgraph')
+ }
const input = graph.inputs?.[0]
if (!input?.labelPos) {
@@ -302,8 +354,11 @@ test.describe('Subgraph Operations', () => {
)
// Trigger double-click if pointer has the handler
- if (app.canvas.pointer.onDoubleClick) {
- app.canvas.pointer.onDoubleClick(leftClickEvent)
+ const pointer = app.canvas.pointer as {
+ onDoubleClick?: (e: unknown) => void
+ }
+ if (pointer.onDoubleClick) {
+ pointer.onDoubleClick(leftClickEvent)
}
}
})
@@ -322,8 +377,11 @@ test.describe('Subgraph Operations', () => {
await comfyPage.nextFrame()
const newInputName = await comfyPage.page.evaluate(() => {
- const graph = window['app'].canvas.graph
- return graph.inputs?.[0]?.label || null
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ const graph = app.canvas.graph
+ if (!graph || !('inputs' in graph)) return null
+ return (graph.inputs as { label?: string }[])?.[0]?.label ?? null
})
expect(newInputName).toBe(labelClickRenamedName)
@@ -334,7 +392,11 @@ test.describe('Subgraph Operations', () => {
}) => {
await comfyPage.loadWorkflow('subgraphs/subgraph-compressed-target-slot')
const step = await comfyPage.page.evaluate(() => {
- return window['app'].graph.nodes[0].widgets[0].options.step
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ const graph = app.graph
+ if (!graph?.nodes?.[0]) return undefined
+ return graph.nodes[0].widgets?.[0]?.options?.step
})
expect(step).toBe(10)
})
@@ -344,8 +406,6 @@ test.describe('Subgraph Operations', () => {
test('Can create subgraph from selected nodes', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('default')
- const initialNodeCount = await getGraphNodeCount(comfyPage)
-
await comfyPage.ctrlA()
await comfyPage.nextFrame()
@@ -453,8 +513,12 @@ test.describe('Subgraph Operations', () => {
const initialNodeCount = await getGraphNodeCount(comfyPage)
const nodesInSubgraph = await comfyPage.page.evaluate(() => {
- const nodes = window['app'].canvas.graph.nodes
- return nodes?.[0]?.id || null
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ const graph = app.canvas.graph
+ if (!graph) return null
+ const nodes = graph.nodes
+ return nodes?.[0]?.id ?? null
})
expect(nodesInSubgraph).not.toBeNull()
@@ -682,7 +746,11 @@ test.describe('Subgraph Operations', () => {
// Check that the subgraph node has no widgets after removing the text slot
const widgetCount = await comfyPage.page.evaluate(() => {
- return window['app'].canvas.graph.nodes[0].widgets?.length || 0
+ const app = window['app']
+ if (!app) throw new Error('App not initialized')
+ const graph = app.canvas.graph
+ if (!graph || !graph.nodes?.[0]) return 0
+ return graph.nodes[0].widgets?.length || 0
})
expect(widgetCount).toBe(0)
diff --git a/browser_tests/tests/useSettingSearch.spec.ts b/browser_tests/tests/useSettingSearch.spec.ts
index f022bb4bd26..5baeec69f8f 100644
--- a/browser_tests/tests/useSettingSearch.spec.ts
+++ b/browser_tests/tests/useSettingSearch.spec.ts
@@ -1,5 +1,6 @@
import { expect } from '@playwright/test'
+import type { SettingParams } from '../../src/platform/settings/types'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
@@ -10,7 +11,7 @@ test.describe('Settings Search functionality', () => {
test.beforeEach(async ({ comfyPage }) => {
// Register test settings to verify hidden/deprecated filtering
await comfyPage.page.evaluate(() => {
- window['app'].registerExtension({
+ window['app']?.registerExtension({
name: 'TestSettingsExtension',
settings: [
{
@@ -19,7 +20,7 @@ test.describe('Settings Search functionality', () => {
type: 'hidden',
defaultValue: 'hidden_value',
category: ['Test', 'Hidden']
- },
+ } as unknown as SettingParams,
{
id: 'TestDeprecatedSetting',
name: 'Test Deprecated Setting',
@@ -27,14 +28,14 @@ test.describe('Settings Search functionality', () => {
defaultValue: 'deprecated_value',
deprecated: true,
category: ['Test', 'Deprecated']
- },
+ } as unknown as SettingParams,
{
id: 'TestVisibleSetting',
name: 'Test Visible Setting',
type: 'text',
defaultValue: 'visible_value',
category: ['Test', 'Visible']
- }
+ } as unknown as SettingParams
]
})
})
diff --git a/browser_tests/tests/vueNodes/widgets/widgetReactivity.spec.ts b/browser_tests/tests/vueNodes/widgets/widgetReactivity.spec.ts
index 6f3701c1276..c0c1655ec8b 100644
--- a/browser_tests/tests/vueNodes/widgets/widgetReactivity.spec.ts
+++ b/browser_tests/tests/vueNodes/widgets/widgetReactivity.spec.ts
@@ -3,6 +3,10 @@ import {
comfyPageFixture as test
} from '../../../fixtures/ComfyPage'
+interface GraphWithNodes {
+ _nodes_by_id: Record
+}
+
test.describe('Vue Widget Reactivity', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
@@ -13,17 +17,20 @@ test.describe('Vue Widget Reactivity', () => {
'css=[data-testid="node-body-4"] > .lg-node-widgets > div'
)
await comfyPage.page.evaluate(() => {
- const node = window['graph']._nodes_by_id['4']
+ const graph = window['graph'] as GraphWithNodes
+ const node = graph._nodes_by_id['4']
node.widgets.push(node.widgets[0])
})
await expect(loadCheckpointNode).toHaveCount(2)
await comfyPage.page.evaluate(() => {
- const node = window['graph']._nodes_by_id['4']
+ const graph = window['graph'] as GraphWithNodes
+ const node = graph._nodes_by_id['4']
node.widgets[2] = node.widgets[0]
})
await expect(loadCheckpointNode).toHaveCount(3)
await comfyPage.page.evaluate(() => {
- const node = window['graph']._nodes_by_id['4']
+ const graph = window['graph'] as GraphWithNodes
+ const node = graph._nodes_by_id['4']
node.widgets.splice(0, 0, node.widgets[0])
})
await expect(loadCheckpointNode).toHaveCount(4)
@@ -33,17 +40,20 @@ test.describe('Vue Widget Reactivity', () => {
'css=[data-testid="node-body-3"] > .lg-node-widgets > div'
)
await comfyPage.page.evaluate(() => {
- const node = window['graph']._nodes_by_id['3']
+ const graph = window['graph'] as GraphWithNodes
+ const node = graph._nodes_by_id['3']
node.widgets.pop()
})
await expect(loadCheckpointNode).toHaveCount(5)
await comfyPage.page.evaluate(() => {
- const node = window['graph']._nodes_by_id['3']
+ const graph = window['graph'] as GraphWithNodes
+ const node = graph._nodes_by_id['3']
node.widgets.length--
})
await expect(loadCheckpointNode).toHaveCount(4)
await comfyPage.page.evaluate(() => {
- const node = window['graph']._nodes_by_id['3']
+ const graph = window['graph'] as GraphWithNodes
+ const node = graph._nodes_by_id['3']
node.widgets.splice(0, 1)
})
await expect(loadCheckpointNode).toHaveCount(3)
diff --git a/browser_tests/tests/widget.spec.ts b/browser_tests/tests/widget.spec.ts
index c8448f36c54..84dce7bc3c6 100644
--- a/browser_tests/tests/widget.spec.ts
+++ b/browser_tests/tests/widget.spec.ts
@@ -36,10 +36,15 @@ test.describe('Combo text widget', () => {
}) => {
const getComboValues = async () =>
comfyPage.page.evaluate(() => {
- return window['app'].graph.nodes
- .find((node) => node.title === 'Node With Optional Combo Input')
- .widgets.find((widget) => widget.name === 'optional_combo_input')
- .options.values
+ const app = window['app']
+ if (!app?.graph) throw new Error('app or graph not found')
+ const node = app.graph.nodes.find(
+ (n: { title: string }) => n.title === 'Node With Optional Combo Input'
+ ) as { widgets: Array<{ name: string; options: { values: unknown } }> }
+ const widget = node?.widgets?.find(
+ (w) => w.name === 'optional_combo_input'
+ )
+ return widget?.options?.values
})
await comfyPage.loadWorkflow('inputs/optional_combo_input')
@@ -71,9 +76,13 @@ test.describe('Combo text widget', () => {
await comfyPage.nextFrame()
// get the combo widget's values
const comboValues = await comfyPage.page.evaluate(() => {
- return window['app'].graph.nodes
- .find((node) => node.title === 'Node With V2 Combo Input')
- .widgets.find((widget) => widget.name === 'combo_input').options.values
+ const app = window['app']
+ if (!app?.graph) throw new Error('app or graph not found')
+ const node = app.graph.nodes.find(
+ (n: { title: string }) => n.title === 'Node With V2 Combo Input'
+ ) as { widgets: Array<{ name: string; options: { values: unknown } }> }
+ const widget = node?.widgets?.find((w) => w.name === 'combo_input')
+ return widget?.options?.values
})
expect(comboValues).toEqual(['A', 'B'])
})
@@ -99,16 +108,20 @@ test.describe('Slider widget', () => {
const widget = await node.getWidget(0)
await comfyPage.page.evaluate(() => {
- const widget = window['app'].graph.nodes[0].widgets[0]
+ const app = window['app']
+ if (!app?.graph?.nodes?.[0]?.widgets?.[0]) {
+ throw new Error('widget not found')
+ }
+ const widget = app.graph.nodes[0].widgets[0]
widget.callback = (value: number) => {
- window['widgetValue'] = value
+ window.widgetValue = value
}
})
await widget.dragHorizontal(50)
await expect(comfyPage.canvas).toHaveScreenshot('slider_widget_dragged.png')
expect(
- await comfyPage.page.evaluate(() => window['widgetValue'])
+ await comfyPage.page.evaluate(() => window.widgetValue)
).toBeDefined()
})
})
@@ -120,16 +133,18 @@ test.describe('Number widget', () => {
const node = (await comfyPage.getFirstNodeRef())!
const widget = await node.getWidget(0)
await comfyPage.page.evaluate(() => {
- const widget = window['app'].graph.nodes[0].widgets[0]
+ const app = window['app']
+ if (!app?.graph?.nodes?.[0]?.widgets?.[0]) return
+ const widget = app.graph.nodes[0].widgets[0]
widget.callback = (value: number) => {
- window['widgetValue'] = value
+ window.widgetValue = value
}
})
await widget.dragHorizontal(50)
await expect(comfyPage.canvas).toHaveScreenshot('seed_widget_dragged.png')
expect(
- await comfyPage.page.evaluate(() => window['widgetValue'])
+ await comfyPage.page.evaluate(() => window.widgetValue)
).toBeDefined()
})
})
@@ -141,8 +156,16 @@ test.describe('Dynamic widget manipulation', () => {
await comfyPage.loadWorkflow('nodes/single_ksampler')
await comfyPage.page.evaluate(() => {
- window['graph'].nodes[0].addWidget('number', 'new_widget', 10)
- window['graph'].setDirtyCanvas(true, true)
+ type GraphWithNodes = {
+ nodes: Array<{
+ addWidget: (type: string, name: string, value: number) => void
+ }>
+ setDirtyCanvas: (fg: boolean, bg: boolean) => void
+ }
+ const graph = window['graph'] as GraphWithNodes | undefined
+ if (!graph?.nodes?.[0]) return
+ graph.nodes[0].addWidget('number', 'new_widget', 10)
+ graph.setDirtyCanvas(true, true)
})
await expect(comfyPage.canvas).toHaveScreenshot('ksampler_widget_added.png')
@@ -209,6 +232,23 @@ test.describe('Image widget', () => {
comfyPage
}) => {
const [x, y] = await comfyPage.page.evaluate(() => {
+ type TestNode = {
+ pos: [number, number]
+ size: [number, number]
+ widgets: Array<{ last_y: number }>
+ imgs?: HTMLImageElement[]
+ imageIndex?: number
+ }
+ type TestGraph = { nodes: TestNode[] }
+ type TestApp = {
+ canvas: { setDirty: (dirty: boolean) => void }
+ canvasPosToClientPos: (pos: [number, number]) => [number, number]
+ }
+ const graph = window['graph'] as TestGraph | undefined
+ const app = window['app'] as TestApp | undefined
+ if (!graph?.nodes?.[6] || !app?.canvas) {
+ throw new Error('graph or app not found')
+ }
const src =
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='768' height='512' viewBox='0 0 1 1'%3E%3Crect width='1' height='1' stroke='black'/%3E%3C/svg%3E"
const image1 = new Image()
@@ -220,8 +260,9 @@ test.describe('Image widget', () => {
targetNode.imageIndex = 1
app.canvas.setDirty(true)
+ const lastWidget = targetNode.widgets.at(-1)
const x = targetNode.pos[0] + targetNode.size[0] - 41
- const y = targetNode.pos[1] + targetNode.widgets.at(-1).last_y + 30
+ const y = targetNode.pos[1] + (lastWidget?.last_y ?? 0) + 30
return app.canvasPosToClientPos([x, y])
})
@@ -313,8 +354,10 @@ test.describe('Animated image widget', () => {
// Simulate the graph executing
await comfyPage.page.evaluate(
([loadId, saveId]) => {
+ const app = window['app']
+ if (!app?.nodeOutputs || !app?.canvas) return
// Set the output of the SaveAnimatedWEBP node to equal the loader node's image
- window['app'].nodeOutputs[saveId] = window['app'].nodeOutputs[loadId]
+ app.nodeOutputs[saveId] = app.nodeOutputs[loadId]
app.canvas.setDirty(true)
},
[loadAnimatedWebpNode.id, saveAnimatedWebpNode.id]
diff --git a/browser_tests/tsconfig.json b/browser_tests/tsconfig.json
index 391298333bc..334841eec79 100644
--- a/browser_tests/tsconfig.json
+++ b/browser_tests/tsconfig.json
@@ -9,5 +9,6 @@
},
"include": [
"**/*.ts",
+ "types/**/*.d.ts"
]
}
diff --git a/browser_tests/types/global.d.ts b/browser_tests/types/global.d.ts
new file mode 100644
index 00000000000..8ca92ba7ab9
--- /dev/null
+++ b/browser_tests/types/global.d.ts
@@ -0,0 +1,21 @@
+import type { ComfyApp } from '@/scripts/app'
+import type { LGraphBadge, LiteGraph } from '@/lib/litegraph/src/litegraph'
+
+declare global {
+ interface Window {
+ // Application globals
+ app?: ComfyApp
+ LiteGraph?: typeof LiteGraph
+ LGraphBadge?: typeof LGraphBadge
+
+ // Test-only properties used in browser tests
+ TestCommand?: boolean
+ foo?: unknown
+ value?: unknown
+ widgetValue?: unknown
+ selectionCommandExecuted?: boolean
+ changeCount?: number
+ }
+}
+
+export {}
diff --git a/docs/typescript/type-safety.md b/docs/typescript/type-safety.md
new file mode 100644
index 00000000000..3ebf840d6cb
--- /dev/null
+++ b/docs/typescript/type-safety.md
@@ -0,0 +1,394 @@
+---
+globs:
+ - '**/*.ts'
+ - '**/*.tsx'
+ - '**/*.vue'
+---
+
+# Type Safety Rules
+
+## Never Use Type Assertions (`as`)
+
+Type assertions bypass TypeScript's type checking. Instead, use proper type guards and narrowing.
+
+### DOM Elements
+
+❌ **Wrong:**
+
+```typescript
+const el = e.target as HTMLInputElement
+el.value
+```
+
+✅ **Correct:**
+
+```typescript
+if (e.target instanceof HTMLInputElement) {
+ e.target.value
+}
+```
+
+### Optional Properties on Objects
+
+❌ **Wrong:**
+
+```typescript
+const obj = value as { prop?: string }
+if (obj.prop) { ... }
+```
+
+✅ **Correct:**
+
+```typescript
+if ('prop' in value && typeof value.prop === 'string') {
+ value.prop
+}
+```
+
+### Constructor/Static Properties
+
+❌ **Wrong:**
+
+```typescript
+const ctor = node.constructor as { type?: string }
+const type = ctor.type
+```
+
+✅ **Correct:**
+
+```typescript
+const ctor = node.constructor
+const type = 'type' in ctor && typeof ctor.type === 'string' ? ctor.type : undefined
+```
+
+## Fix the Source Type, Don't Cast
+
+When you find yourself needing a cast, ask: "Can I fix the type definition instead?"
+
+### Missing Interface Properties
+
+If a property exists at runtime but not in the type, add it to the interface:
+
+❌ **Wrong:**
+
+```typescript
+const box = this.search_box as { close?: () => void }
+box?.close?.()
+```
+
+✅ **Correct:**
+
+```typescript
+// Update the type definition
+search_box?: HTMLDivElement & ICloseable
+
+// Then use directly
+this.search_box?.close()
+```
+
+### Callback Parameter Types
+
+If a callback receives a different type than declared, fix the callback signature:
+
+❌ **Wrong:**
+
+```typescript
+onDrawTooltip?: (link: LLink) => void
+// Later...
+onDrawTooltip(link as LLink) // link is actually LinkSegment
+```
+
+✅ **Correct:**
+
+```typescript
+onDrawTooltip?: (link: LinkSegment) => void
+// Later...
+onDrawTooltip(link) // No cast needed
+```
+
+## Create Type Guard Functions
+
+For repeated type checks, create reusable type guards:
+
+```typescript
+interface IPanel extends Element, ICloseable {
+ node?: LGraphNode
+ graph?: LGraph
+}
+
+function isPanel(el: Element): el is IPanel {
+ return 'close' in el && typeof el.close === 'function'
+}
+
+// Usage
+for (const panel of panels) {
+ if (!isPanel(panel)) continue
+ panel.close() // TypeScript knows panel is IPanel
+}
+```
+
+## Never Use `@ts-ignore` or `@ts-expect-error`
+
+These directives suppress all errors on a line, making it easy to accidentally mask serious bugs. Instead:
+
+1. Fix the underlying type issue
+2. Use a type guard to narrow the type
+3. If truly unavoidable, use a targeted cast with explanation
+
+❌ **Wrong:**
+
+```typescript
+// @ts-expect-error - doesn't work otherwise
+node.customProperty = value
+```
+
+✅ **Correct:**
+
+```typescript
+interface ExtendedNode extends LGraphNode {
+ customProperty?: string
+}
+
+function isExtendedNode(node: LGraphNode): node is ExtendedNode {
+ return 'customProperty' in node
+}
+
+if (isExtendedNode(node)) {
+ node.customProperty = value
+}
+```
+
+## Use `unknown` Instead of `any`
+
+When you don't know the type, use `unknown` and narrow with type guards:
+
+❌ **Wrong:**
+
+```typescript
+function process(data: any) {
+ return data.value // No type checking
+}
+```
+
+✅ **Correct:**
+
+```typescript
+function process(data: unknown) {
+ if (typeof data === 'object' && data !== null && 'value' in data) {
+ return data.value
+ }
+ return undefined
+}
+```
+
+## Use Type Annotations for Object Literals
+
+Prefer type annotations over assertions for object literals - this catches refactoring bugs:
+
+❌ **Wrong:**
+
+```typescript
+const config = {
+ naem: 'test' // Typo not caught
+} as Config
+```
+
+✅ **Correct:**
+
+```typescript
+const config: Config = {
+ naem: 'test' // Error: 'naem' does not exist on type 'Config'
+}
+```
+
+## Prefer Interfaces Over Type Aliases for Objects
+
+Use interfaces for object types. While modern TypeScript has narrowed performance differences between interfaces and type aliases, interfaces still offer clearer error messages and support declaration merging:
+
+❌ **Avoid for object types:**
+
+```typescript
+type NodeConfig = {
+ id: string
+ name: string
+}
+```
+
+✅ **Preferred:**
+
+```typescript
+interface NodeConfig {
+ id: string
+ name: string
+}
+```
+
+Use type aliases for unions, primitives, and tuples where interfaces don't apply.
+
+## Prefer Specific Over General
+
+Use the most specific type possible:
+
+❌ **Wrong:**
+
+```typescript
+const value = obj as any
+const value = obj as unknown
+const value = obj as Record
+```
+
+✅ **Correct:**
+
+```typescript
+// Use the actual expected type
+const value: ISerialisedNode = obj
+// Or use type guards to narrow
+if (isSerialisedNode(obj)) { ... }
+```
+
+## Union Types and Narrowing
+
+For union types, use proper narrowing instead of casting:
+
+❌ **Wrong:**
+
+```typescript
+// NodeId = number | string
+(node.id as number) - (other.id as number)
+```
+
+✅ **Correct:**
+
+```typescript
+if (typeof node.id === 'number' && typeof other.id === 'number') {
+ node.id - other.id
+}
+```
+
+## Primitive Types
+
+Use lowercase primitive types, never boxed object types:
+
+❌ **Wrong:**
+
+```typescript
+function greet(name: String): String
+function count(items: Object): Number
+```
+
+✅ **Correct:**
+
+```typescript
+function greet(name: string): string
+function count(items: object): number
+```
+
+## Callback Types
+
+### Return Types
+
+Use `void` for callbacks whose return value is ignored:
+
+❌ **Wrong:**
+
+```typescript
+interface Options {
+ onComplete?: () => any
+}
+```
+
+✅ **Correct:**
+
+```typescript
+interface Options {
+ onComplete?: () => void
+}
+```
+
+### Optional Parameters
+
+Don't make callback parameters optional unless you intend to invoke the callback with varying argument counts:
+
+❌ **Wrong:**
+
+```typescript
+interface Callbacks {
+ onProgress?: (current: number, total?: number) => void
+}
+```
+
+✅ **Correct:**
+
+```typescript
+interface Callbacks {
+ onProgress?: (current: number, total: number) => void
+}
+```
+
+Callbacks can always ignore parameters they don't need.
+
+## Function Overloads
+
+### Ordering
+
+Put specific overloads before general ones (TypeScript uses first match):
+
+❌ **Wrong:**
+
+```typescript
+declare function fn(x: unknown): unknown
+declare function fn(x: HTMLElement): HTMLElement
+```
+
+✅ **Correct:**
+
+```typescript
+declare function fn(x: HTMLElement): HTMLElement
+declare function fn(x: unknown): unknown
+```
+
+### Prefer Optional Parameters Over Overloads
+
+❌ **Wrong:**
+
+```typescript
+interface Example {
+ diff(one: string): number
+ diff(one: string, two: string): number
+ diff(one: string, two: string, three: string): number
+}
+```
+
+✅ **Correct:**
+
+```typescript
+interface Example {
+ diff(one: string, two?: string, three?: string): number
+}
+```
+
+## Generics
+
+Never create a generic type that doesn't use its type parameter:
+
+❌ **Wrong:**
+
+```typescript
+declare function fn(): void
+```
+
+✅ **Correct:**
+
+```typescript
+declare function fn(arg: T): T
+```
+
+## Summary
+
+1. **Never use `as`** outside of custom type guards (exception: test files may use `as Partial as T` for partial mocks)
+2. **Use `instanceof`** for class/DOM element checks
+3. **Use `'prop' in obj`** for property existence checks
+4. **Use `typeof`** for primitive type checks
+5. **Fix source types** instead of casting at usage sites
+6. **Create type guards** for repeated patterns
+7. **Use `void`** for ignored callback returns
diff --git a/scripts/collect-i18n-node-defs.ts b/scripts/collect-i18n-node-defs.ts
index 46d85ad7347..ee328dafaf6 100644
--- a/scripts/collect-i18n-node-defs.ts
+++ b/scripts/collect-i18n-node-defs.ts
@@ -1,14 +1,38 @@
import * as fs from 'fs'
import type { ComfyNodeDef } from '@/schemas/nodeDefSchema'
+import type { ComfyApp } from '@/scripts/app'
+
+import type { LiteGraphGlobal } from '../src/lib/litegraph/src/litegraph'
import { comfyPageFixture as test } from '../browser_tests/fixtures/ComfyPage'
-import { normalizeI18nKey } from '../packages/shared-frontend-utils/src/formatUtil'
+import { normalizeI18nKey } from '@comfyorg/shared-frontend-utils/formatUtil'
import type { ComfyNodeDefImpl } from '../src/stores/nodeDefStore'
const localePath = './src/locales/en/main.json'
const nodeDefsPath = './src/locales/en/nodeDefs.json'
+interface ComfyWindow extends Window {
+ app: ComfyApp
+ LiteGraph: LiteGraphGlobal
+}
+
+function isComfyWindow(win: Window): win is ComfyWindow {
+ return (
+ 'app' in win &&
+ win.app != null &&
+ 'LiteGraph' in win &&
+ win.LiteGraph != null
+ )
+}
+
+function getComfyWindow(): ComfyWindow {
+ if (!isComfyWindow(window)) {
+ throw new Error('ComfyApp or LiteGraph not found on window')
+ }
+ return window
+}
+
interface WidgetInfo {
name?: string
label?: string
@@ -30,8 +54,8 @@ test('collect-i18n-node-defs', async ({ comfyPage }) => {
const nodeDefs: ComfyNodeDefImpl[] = await comfyPage.page.evaluate(
async () => {
- // @ts-expect-error - app is dynamically added to window
- const api = window['app'].api
+ const comfyWindow = getComfyWindow()
+ const api = comfyWindow.app.api
const rawNodeDefs = await api.getNodeDefs()
const { ComfyNodeDefImpl } = await import('../src/stores/nodeDefStore')
@@ -44,7 +68,7 @@ test('collect-i18n-node-defs', async ({ comfyPage }) => {
}
)
- console.log(`Collected ${nodeDefs.length} node definitions`)
+ console.warn(`Collected ${nodeDefs.length} node definitions`)
const allDataTypesLocale = Object.fromEntries(
nodeDefs
@@ -80,9 +104,9 @@ test('collect-i18n-node-defs', async ({ comfyPage }) => {
const widgetsMappings = await comfyPage.page.evaluate(
(args) => {
const [nodeName, displayName, inputNames] = args
- // @ts-expect-error - LiteGraph is dynamically added to window
- const node = window['LiteGraph'].createNode(nodeName, displayName)
- if (!node.widgets?.length) return {}
+ const comfyWindow = getComfyWindow()
+ const node = comfyWindow.LiteGraph.createNode(nodeName, displayName)
+ if (!node?.widgets?.length) return {}
return Object.fromEntries(
node.widgets
.filter(
diff --git a/src/components/common/CustomizationDialog.vue b/src/components/common/CustomizationDialog.vue
index f5df6d84d75..b4e9bcad0a4 100644
--- a/src/components/common/CustomizationDialog.vue
+++ b/src/components/common/CustomizationDialog.vue
@@ -92,18 +92,16 @@ const colorOptions = [
const defaultIcon = iconOptions.find(
(option) => option.value === nodeBookmarkStore.defaultBookmarkIcon
-)
+) ?? { name: '', value: '' }
-// @ts-expect-error fixme ts strict error
const selectedIcon = ref<{ name: string; value: string }>(defaultIcon)
const finalColor = ref(
props.initialColor || nodeBookmarkStore.defaultBookmarkColor
)
const resetCustomization = () => {
- // @ts-expect-error fixme ts strict error
selectedIcon.value =
- iconOptions.find((option) => option.value === props.initialIcon) ||
+ iconOptions.find((option) => option.value === props.initialIcon) ??
defaultIcon
finalColor.value =
props.initialColor || nodeBookmarkStore.defaultBookmarkColor
diff --git a/src/components/common/EditableText.test.ts b/src/components/common/EditableText.test.ts
index 2d31123b987..0c1c234cfed 100644
--- a/src/components/common/EditableText.test.ts
+++ b/src/components/common/EditableText.test.ts
@@ -13,8 +13,10 @@ describe('EditableText', () => {
app.use(PrimeVue)
})
- // @ts-expect-error fixme ts strict error
- const mountComponent = (props, options = {}) => {
+ const mountComponent = (
+ props: { modelValue: string; isEditing: boolean },
+ options = {}
+ ) => {
return mount(EditableText, {
global: {
plugins: [PrimeVue],
@@ -65,8 +67,7 @@ describe('EditableText', () => {
})
await wrapper.findComponent(InputText).trigger('blur')
expect(wrapper.emitted('edit')).toBeTruthy()
- // @ts-expect-error fixme ts strict error
- expect(wrapper.emitted('edit')[0]).toEqual(['Test Text'])
+ expect(wrapper.emitted('edit')?.[0]).toEqual(['Test Text'])
})
it('cancels editing on escape key', async () => {
@@ -124,8 +125,7 @@ describe('EditableText', () => {
// Trigger blur that happens after enter
await enterWrapper.findComponent(InputText).trigger('blur')
expect(enterWrapper.emitted('edit')).toBeTruthy()
- // @ts-expect-error fixme ts strict error
- expect(enterWrapper.emitted('edit')[0]).toEqual(['Saved Text'])
+ expect(enterWrapper.emitted('edit')?.[0]).toEqual(['Saved Text'])
// Test Escape key cancels changes with a fresh wrapper
const escapeWrapper = mountComponent({
diff --git a/src/components/common/EditableText.vue b/src/components/common/EditableText.vue
index 322d332a460..91b53541b59 100644
--- a/src/components/common/EditableText.vue
+++ b/src/components/common/EditableText.vue
@@ -44,12 +44,13 @@ const {
const emit = defineEmits(['edit', 'cancel'])
const inputValue = ref(modelValue)
-const inputRef = ref | undefined>()
+const inputRef = ref<
+ (InstanceType & { $el?: HTMLElement }) | undefined
+>()
const isCanceling = ref(false)
const blurInputElement = () => {
- // @ts-expect-error - $el is an internal property of the InputText component
- inputRef.value?.$el.blur()
+ inputRef.value?.$el?.blur()
}
const finishEditing = () => {
// Don't save if we're canceling
@@ -74,15 +75,14 @@ watch(
if (newVal) {
inputValue.value = modelValue
await nextTick(() => {
- if (!inputRef.value) return
+ const el = inputRef.value?.$el
+ if (!el || !(el instanceof HTMLInputElement)) return
const fileName = inputValue.value.includes('.')
? inputValue.value.split('.').slice(0, -1).join('.')
: inputValue.value
const start = 0
const end = fileName.length
- // @ts-expect-error - $el is an internal property of the InputText component
- const inputElement = inputRef.value.$el
- inputElement.setSelectionRange?.(start, end)
+ el.setSelectionRange(start, end)
})
}
},
diff --git a/src/components/common/ElectronFileDownload.vue b/src/components/common/ElectronFileDownload.vue
index 0a85a197b70..34916d2434d 100644
--- a/src/components/common/ElectronFileDownload.vue
+++ b/src/components/common/ElectronFileDownload.vue
@@ -116,17 +116,16 @@ const fileSize = computed(() =>
)
const { copyToClipboard } = useCopyToClipboard()
const electronDownloadStore = useElectronDownloadStore()
-// @ts-expect-error fixme ts strict error
-const [savePath, filename] = props.label.split('/')
+const [savePath = '', filename = ''] = props.label?.split('/') ?? []
electronDownloadStore.$subscribe((_, { downloads }) => {
- const download = downloads.find((download) => props.url === download.url)
+ const foundDownload = downloads.find((download) => props.url === download.url)
- if (download) {
- // @ts-expect-error fixme ts strict error
- downloadProgress.value = Number((download.progress * 100).toFixed(1))
- // @ts-expect-error fixme ts strict error
- status.value = download.status
+ if (foundDownload) {
+ downloadProgress.value = Number(
+ ((foundDownload.progress ?? 0) * 100).toFixed(1)
+ )
+ status.value = foundDownload.status ?? null
}
})
diff --git a/src/components/common/FormItem.vue b/src/components/common/FormItem.vue
index 011f8075378..d7f8cc82fec 100644
--- a/src/components/common/FormItem.vue
+++ b/src/components/common/FormItem.vue
@@ -68,19 +68,23 @@ function getFormAttrs(item: FormItem) {
}
switch (item.type) {
case 'combo':
- case 'radio':
- attrs['options'] =
+ case 'radio': {
+ const options =
typeof item.options === 'function'
- ? // @ts-expect-error: Audit and deprecate usage of legacy options type:
- // (value) => [string | {text: string, value: string}]
- item.options(formValue.value)
+ ? item.options(formValue.value)
: item.options
+ attrs['options'] = options
- if (typeof item.options?.[0] !== 'string') {
+ if (
+ Array.isArray(options) &&
+ options.length > 0 &&
+ typeof options[0] !== 'string'
+ ) {
attrs['optionLabel'] = 'text'
attrs['optionValue'] = 'value'
}
break
+ }
}
return attrs
}
diff --git a/src/components/common/TreeExplorerTreeNode.test.ts b/src/components/common/TreeExplorerTreeNode.test.ts
index d00d3e401e2..192cce6bca4 100644
--- a/src/components/common/TreeExplorerTreeNode.test.ts
+++ b/src/components/common/TreeExplorerTreeNode.test.ts
@@ -61,8 +61,8 @@ describe('TreeExplorerTreeNode', () => {
expect(wrapper.findComponent(EditableText).props('modelValue')).toBe(
'Test Node'
)
- // @ts-expect-error fixme ts strict error
- expect(wrapper.findComponent(Badge).props()['value'].toString()).toBe('3')
+ const badgeValue = wrapper.findComponent(Badge).props('value')
+ expect(String(badgeValue)).toBe('3')
})
it('makes node label editable when renamingEditingNode matches', async () => {
diff --git a/src/components/dialog/content/MissingCoreNodesMessage.test.ts b/src/components/dialog/content/MissingCoreNodesMessage.test.ts
index afcb90996e4..d8b481cdd25 100644
--- a/src/components/dialog/content/MissingCoreNodesMessage.test.ts
+++ b/src/components/dialog/content/MissingCoreNodesMessage.test.ts
@@ -6,6 +6,7 @@ import { nextTick } from 'vue'
import MissingCoreNodesMessage from '@/components/dialog/content/MissingCoreNodesMessage.vue'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
+import type { SystemStats } from '@/schemas/apiSchema'
import { useSystemStatsStore } from '@/stores/systemStatsStore'
// Mock the stores
@@ -13,10 +14,8 @@ vi.mock('@/stores/systemStatsStore', () => ({
useSystemStatsStore: vi.fn()
}))
-const createMockNode = (type: string, version?: string): LGraphNode =>
- // @ts-expect-error - Creating a partial mock of LGraphNode for testing purposes.
- // We only need specific properties for our tests, not the full LGraphNode interface.
- ({
+function createMockNode(type: string, version?: string): LGraphNode {
+ return Object.assign(Object.create(null), {
type,
properties: { cnr_id: 'comfy-core', ver: version },
id: 1,
@@ -29,21 +28,58 @@ const createMockNode = (type: string, version?: string): LGraphNode =>
inputs: [],
outputs: []
})
+}
+
+interface MockSystemStatsStore {
+ systemStats: SystemStats | null
+ isLoading: boolean
+ error: Error | undefined
+ isInitialized: boolean
+ refetchSystemStats: ReturnType
+ getFormFactor: () => string
+}
+
+function createMockSystemStats(
+ overrides: Partial = {}
+): SystemStats {
+ return {
+ system: {
+ os: 'linux',
+ python_version: '3.10.0',
+ embedded_python: false,
+ comfyui_version: '1.0.0',
+ pytorch_version: '2.0.0',
+ argv: [],
+ ram_total: 16000000000,
+ ram_free: 8000000000,
+ ...overrides
+ },
+ devices: []
+ }
+}
+
+function createMockSystemStatsStore(): MockSystemStatsStore {
+ return {
+ systemStats: null,
+ isLoading: false,
+ error: undefined,
+ isInitialized: true,
+ refetchSystemStats: vi.fn(),
+ getFormFactor: () => 'other'
+ }
+}
describe('MissingCoreNodesMessage', () => {
- const mockSystemStatsStore = {
- systemStats: null as { system?: { comfyui_version?: string } } | null,
- refetchSystemStats: vi.fn()
- }
+ let mockSystemStatsStore: MockSystemStatsStore
beforeEach(() => {
vi.clearAllMocks()
- // Reset the mock store state
- mockSystemStatsStore.systemStats = null
- mockSystemStatsStore.refetchSystemStats = vi.fn()
- // @ts-expect-error - Mocking the return value of useSystemStatsStore for testing.
- // The actual store has more properties, but we only need these for our tests.
- useSystemStatsStore.mockReturnValue(mockSystemStatsStore)
+ mockSystemStatsStore = createMockSystemStatsStore()
+ vi.mocked(useSystemStatsStore).mockReturnValue(
+ mockSystemStatsStore as Partial<
+ ReturnType
+ > as ReturnType
+ )
})
const mountComponent = (props = {}) => {
@@ -88,9 +124,9 @@ describe('MissingCoreNodesMessage', () => {
it('displays current ComfyUI version when available', async () => {
// Set systemStats directly (store auto-fetches with useAsyncState)
- mockSystemStatsStore.systemStats = {
- system: { comfyui_version: '1.0.0' }
- }
+ mockSystemStatsStore.systemStats = createMockSystemStats({
+ comfyui_version: '1.0.0'
+ })
const missingCoreNodes = {
'1.2.0': [createMockNode('TestNode', '1.2.0')]
diff --git a/src/components/dialog/content/PromptDialogContent.vue b/src/components/dialog/content/PromptDialogContent.vue
index 92c721a244b..e96275cf4c6 100644
--- a/src/components/dialog/content/PromptDialogContent.vue
+++ b/src/components/dialog/content/PromptDialogContent.vue
@@ -39,11 +39,13 @@ const onConfirm = () => {
useDialogStore().closeDialog()
}
-const inputRef = ref | undefined>()
+const inputRef = ref<
+ (InstanceType & { $el?: HTMLElement }) | undefined
+>()
const selectAllText = () => {
- if (!inputRef.value) return
- // @ts-expect-error - $el is an internal property of the InputText component
- const inputElement = inputRef.value.$el
- inputElement.setSelectionRange(0, inputElement.value.length)
+ const el = inputRef.value?.$el
+ if (el instanceof HTMLInputElement) {
+ el.setSelectionRange(0, el.value.length)
+ }
}
diff --git a/src/components/dialog/content/setting/KeybindingPanel.vue b/src/components/dialog/content/setting/KeybindingPanel.vue
index 03ca3df47c3..c97eb88640e 100644
--- a/src/components/dialog/content/setting/KeybindingPanel.vue
+++ b/src/components/dialog/content/setting/KeybindingPanel.vue
@@ -194,7 +194,9 @@ const selectedCommandData = ref(null)
const editDialogVisible = ref(false)
const newBindingKeyCombo = ref(null)
const currentEditingCommand = ref(null)
-const keybindingInput = ref | null>(null)
+const keybindingInput = ref<
+ (InstanceType & { $el?: HTMLElement }) | null
+>(null)
const existingKeybindingOnCombo = computed(() => {
if (!currentEditingCommand.value) {
@@ -229,8 +231,8 @@ watchEffect(() => {
if (editDialogVisible.value) {
// nextTick doesn't work here, so we use a timeout instead
setTimeout(() => {
- // @ts-expect-error - $el is an internal property of the InputText component
- keybindingInput.value?.$el?.focus()
+ const el = keybindingInput.value?.$el
+ if (el instanceof HTMLElement) el.focus()
}, 300)
}
})
diff --git a/src/components/graph/GraphCanvas.vue b/src/components/graph/GraphCanvas.vue
index 48d82e44b4b..83627682257 100644
--- a/src/components/graph/GraphCanvas.vue
+++ b/src/components/graph/GraphCanvas.vue
@@ -428,7 +428,12 @@ onMounted(async () => {
await newUserService().initializeIfNewUser(settingStore)
- // @ts-expect-error fixme ts strict error
+ if (!canvasRef.value) {
+ console.error(
+ '[GraphCanvas] canvasRef.value is null during onMounted - comfyApp.setup was skipped'
+ )
+ return
+ }
await comfyApp.setup(canvasRef.value)
canvasStore.canvas = comfyApp.canvas
canvasStore.canvas.render_canvas_border = false
diff --git a/src/components/graph/NodeContextMenu.vue b/src/components/graph/NodeContextMenu.vue
index 97ee1b9c99e..e38b4b4a18a 100644
--- a/src/components/graph/NodeContextMenu.vue
+++ b/src/components/graph/NodeContextMenu.vue
@@ -42,7 +42,14 @@
import { useElementBounding, useEventListener, useRafFn } from '@vueuse/core'
import ContextMenu from 'primevue/contextmenu'
import type { MenuItem } from 'primevue/menuitem'
-import { computed, onMounted, onUnmounted, ref, watchEffect } from 'vue'
+import {
+ computed,
+ onMounted,
+ onUnmounted,
+ ref,
+ useTemplateRef,
+ watchEffect
+} from 'vue'
import {
registerNodeOptionsInstance,
@@ -56,14 +63,29 @@ import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import ColorPickerMenu from './selectionToolbox/ColorPickerMenu.vue'
+function getMenuElement(
+ menu: InstanceType | null
+): HTMLElement | undefined {
+ if (!menu) return undefined
+ if ('container' in menu && menu.container instanceof HTMLElement) {
+ return menu.container
+ }
+ if ('$el' in menu && menu.$el instanceof HTMLElement) {
+ return menu.$el
+ }
+ return undefined
+}
+
interface ExtendedMenuItem extends MenuItem {
isColorSubmenu?: boolean
shortcut?: string
originalOption?: MenuOption
}
-const contextMenu = ref>()
-const colorPickerMenu = ref>()
+const contextMenu =
+ useTemplateRef>('contextMenu')
+const colorPickerMenu =
+ useTemplateRef>('colorPickerMenu')
const isOpen = ref(false)
const { menuOptions, bump } = useMoreOptionsMenu()
@@ -85,10 +107,7 @@ let lastOffsetY = 0
const updateMenuPosition = () => {
if (!isOpen.value) return
- const menuInstance = contextMenu.value as unknown as {
- container?: HTMLElement
- }
- const menuEl = menuInstance?.container
+ const menuEl = getMenuElement(contextMenu.value)
if (!menuEl) return
const { scale, offset } = lgCanvas.ds
@@ -137,11 +156,7 @@ useEventListener(
if (!isOpen.value || !contextMenu.value) return
const target = event.target as Node
- const contextMenuInstance = contextMenu.value as unknown as {
- container?: HTMLElement
- $el?: HTMLElement
- }
- const menuEl = contextMenuInstance.container || contextMenuInstance.$el
+ const menuEl = getMenuElement(contextMenu.value)
if (menuEl && !menuEl.contains(target)) {
hide()
diff --git a/src/components/load3d/Load3DControls.vue b/src/components/load3d/Load3DControls.vue
index 68251c74122..1c13d5e9c3b 100644
--- a/src/components/load3d/Load3DControls.vue
+++ b/src/components/load3d/Load3DControls.vue
@@ -161,15 +161,14 @@ const selectCategory = (category: string) => {
}
const getCategoryIcon = (category: string) => {
- const icons = {
+ const icons: Record = {
scene: 'pi pi-image',
model: 'pi pi-box',
camera: 'pi pi-camera',
light: 'pi pi-sun',
export: 'pi pi-download'
}
- // @ts-expect-error fixme ts strict error
- return `${icons[category]} text-base-foreground text-lg`
+ return `${icons[category] ?? ''} text-base-foreground text-lg`
}
const emit = defineEmits<{
diff --git a/src/components/sidebar/tabs/ModelLibrarySidebarTab.vue b/src/components/sidebar/tabs/ModelLibrarySidebarTab.vue
index 21865523142..c7bebb19106 100644
--- a/src/components/sidebar/tabs/ModelLibrarySidebarTab.vue
+++ b/src/components/sidebar/tabs/ModelLibrarySidebarTab.vue
@@ -145,17 +145,16 @@ const renderedRoot = computed>(() => {
children,
draggable: node.leaf,
handleClick(e: MouseEvent) {
- if (this.leaf) {
- // @ts-expect-error fixme ts strict error
+ if (this.leaf && model) {
const provider = modelToNodeStore.getNodeProvider(model.directory)
if (provider) {
- const node = useLitegraphService().addNodeOnGraph(provider.nodeDef)
- // @ts-expect-error fixme ts strict error
- const widget = node.widgets.find(
+ const addedNode = useLitegraphService().addNodeOnGraph(
+ provider.nodeDef
+ )
+ const widget = addedNode?.widgets?.find(
(widget) => widget.name === provider.key
)
if (widget) {
- // @ts-expect-error fixme ts strict error
widget.value = model.file_name
}
}
diff --git a/src/components/sidebar/tabs/NodeLibrarySidebarTab.vue b/src/components/sidebar/tabs/NodeLibrarySidebarTab.vue
index 2240a60b1a9..1b74d3e9316 100644
--- a/src/components/sidebar/tabs/NodeLibrarySidebarTab.vue
+++ b/src/components/sidebar/tabs/NodeLibrarySidebarTab.vue
@@ -278,8 +278,7 @@ const renderedRoot = computed>(() => {
}
},
handleClick(e: MouseEvent) {
- if (this.leaf) {
- // @ts-expect-error fixme ts strict error
+ if (this.leaf && this.data) {
useLitegraphService().addNodeOnGraph(this.data)
} else {
toggleNodeOnEvent(e, this)
@@ -336,8 +335,7 @@ const onAddFilter = async (
await handleSearch(searchQuery.value)
}
-// @ts-expect-error fixme ts strict error
-const onRemoveFilter = async (filterAndValue) => {
+const onRemoveFilter = async (filterAndValue: SearchFilter) => {
const index = filters.value.findIndex((f) => f === filterAndValue)
if (index !== -1) {
filters.value.splice(index, 1)
diff --git a/src/components/sidebar/tabs/nodeLibrary/NodeBookmarkTreeExplorer.vue b/src/components/sidebar/tabs/nodeLibrary/NodeBookmarkTreeExplorer.vue
index 646f841bb4c..3e5e31387ab 100644
--- a/src/components/sidebar/tabs/nodeLibrary/NodeBookmarkTreeExplorer.vue
+++ b/src/components/sidebar/tabs/nodeLibrary/NodeBookmarkTreeExplorer.vue
@@ -162,20 +162,17 @@ const renderedBookmarkedRoot = computed>(
droppable: !node.leaf,
async handleDrop(data: TreeExplorerDragAndDropData) {
const nodeDefToAdd = data.data.data
+ if (!nodeDefToAdd) return
// Remove bookmark if the source is the top level bookmarked node.
- // @ts-expect-error fixme ts strict error
if (nodeBookmarkStore.isBookmarked(nodeDefToAdd)) {
- // @ts-expect-error fixme ts strict error
await nodeBookmarkStore.toggleBookmark(nodeDefToAdd)
}
const folderNodeDef = node.data as ComfyNodeDefImpl
- // @ts-expect-error fixme ts strict error
const nodePath = folderNodeDef.category + '/' + nodeDefToAdd.name
await nodeBookmarkStore.addBookmark(nodePath)
},
handleClick(e: MouseEvent) {
- if (this.leaf) {
- // @ts-expect-error fixme ts strict error
+ if (this.leaf && this.data) {
useLitegraphService().addNodeOnGraph(this.data)
} else {
toggleNodeOnEvent(e, node)
@@ -194,8 +191,9 @@ const renderedBookmarkedRoot = computed>(
}
},
async handleDelete() {
- // @ts-expect-error fixme ts strict error
- await nodeBookmarkStore.deleteBookmarkFolder(this.data)
+ if (this.data) {
+ await nodeBookmarkStore.deleteBookmarkFolder(this.data)
+ }
}
})
}
diff --git a/src/components/topbar/CurrentUserButton.test.ts b/src/components/topbar/CurrentUserButton.test.ts
index db5349b49be..a41455ec992 100644
--- a/src/components/topbar/CurrentUserButton.test.ts
+++ b/src/components/topbar/CurrentUserButton.test.ts
@@ -1,5 +1,7 @@
import type { VueWrapper } from '@vue/test-utils'
import { mount } from '@vue/test-utils'
+import type { ComponentExposed } from 'vue-component-type-helpers'
+
import Button from '@/components/ui/button/Button.vue'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { h } from 'vue'
@@ -9,6 +11,8 @@ import enMessages from '@/locales/en/main.json' with { type: 'json' }
import CurrentUserButton from './CurrentUserButton.vue'
+type CurrentUserButtonInstance = ComponentExposed
+
// Mock all firebase modules
vi.mock('firebase/app', () => ({
initializeApp: vi.fn(),
@@ -98,8 +102,9 @@ describe('CurrentUserButton', () => {
const popoverToggleSpy = vi.fn()
// Override the ref with a mock implementation
- // @ts-expect-error - accessing internal Vue component vm
- wrapper.vm.popover = { toggle: popoverToggleSpy }
+ Object.assign(wrapper.vm, {
+ popover: { toggle: popoverToggleSpy }
+ })
await wrapper.findComponent(Button).trigger('click')
expect(popoverToggleSpy).toHaveBeenCalled()
@@ -110,12 +115,14 @@ describe('CurrentUserButton', () => {
// Replace the popover.hide method with a spy
const popoverHideSpy = vi.fn()
- // @ts-expect-error - accessing internal Vue component vm
- wrapper.vm.popover = { hide: popoverHideSpy }
+ Object.assign(wrapper.vm, {
+ popover: { hide: popoverHideSpy }
+ })
// Directly call the closePopover method through the component instance
- // @ts-expect-error - accessing internal Vue component vm
- wrapper.vm.closePopover()
+ // closePopover is exposed via defineExpose in the component
+ const vm = wrapper.vm as CurrentUserButtonInstance
+ vm.closePopover()
// Verify that popover.hide was called
expect(popoverHideSpy).toHaveBeenCalled()
diff --git a/src/components/topbar/CurrentUserButton.vue b/src/components/topbar/CurrentUserButton.vue
index 44233aac5c7..026c6be1fcf 100644
--- a/src/components/topbar/CurrentUserButton.vue
+++ b/src/components/topbar/CurrentUserButton.vue
@@ -62,4 +62,14 @@ const photoURL = computed(
const closePopover = () => {
popover.value?.hide()
}
+
+const openPopover = (event: Event) => {
+ popover.value?.show(event)
+}
+
+const togglePopover = (event: Event) => {
+ popover.value?.toggle(event)
+}
+
+defineExpose({ closePopover, openPopover, togglePopover })
diff --git a/src/composables/canvas/useSelectedLiteGraphItems.test.ts b/src/composables/canvas/useSelectedLiteGraphItems.test.ts
index 23e1e8dd31e..e70b98f01a3 100644
--- a/src/composables/canvas/useSelectedLiteGraphItems.test.ts
+++ b/src/composables/canvas/useSelectedLiteGraphItems.test.ts
@@ -28,11 +28,12 @@ vi.mock('@/lib/litegraph/src/litegraph', () => ({
}
}))
-// Mock Positionable objects
-// @ts-expect-error - Mock implementation for testing
+// Mock Positionable objects - full implementation for testing
class MockNode implements Positionable {
+ readonly id = 1
pos: [number, number]
size: [number, number]
+ readonly boundingRect: [number, number, number, number]
constructor(
pos: [number, number] = [0, 0],
@@ -40,25 +41,52 @@ class MockNode implements Positionable {
) {
this.pos = pos
this.size = size
+ this.boundingRect = [pos[0], pos[1], size[0], size[1]]
+ }
+
+ move(): void {}
+ snapToGrid(): boolean {
+ return false
}
}
-class MockReroute extends Reroute implements Positionable {
- // @ts-expect-error - Override for testing
- override pos: [number, number]
+// MockReroute - passes instanceof Reroute check
+class MockReroute implements Positionable {
+ readonly id = 1
+ pos: [number, number]
size: [number, number]
+ readonly boundingRect: [number, number, number, number]
constructor(
pos: [number, number] = [0, 0],
size: [number, number] = [20, 20]
) {
- // @ts-expect-error - Mock constructor
- super()
+ Object.setPrototypeOf(this, Reroute.prototype)
this.pos = pos
this.size = size
+ this.boundingRect = [pos[0], pos[1], size[0], size[1]]
+ }
+
+ move(): void {}
+ snapToGrid(): boolean {
+ return false
}
}
+// Helper to create mock LGraphNode objects
+function createMockLGraphNode(
+ id: number,
+ mode: number,
+ subgraphNodes?: LGraphNode[]
+): LGraphNode {
+ return Object.assign(Object.create(null), {
+ id,
+ mode,
+ isSubgraphNode: subgraphNodes ? () => true : undefined,
+ subgraph: subgraphNodes ? { nodes: subgraphNodes } : undefined
+ })
+}
+
describe('useSelectedLiteGraphItems', () => {
let canvasStore: ReturnType
let mockCanvas: any
@@ -86,7 +114,6 @@ describe('useSelectedLiteGraphItems', () => {
it('should return false for non-Reroute items', () => {
const { isIgnoredItem } = useSelectedLiteGraphItems()
const node = new MockNode()
- // @ts-expect-error - Test mock
expect(isIgnoredItem(node)).toBe(false)
})
})
@@ -98,14 +125,11 @@ describe('useSelectedLiteGraphItems', () => {
const node2 = new MockNode([100, 100])
const reroute = new MockReroute([50, 50])
- // @ts-expect-error - Test mocks
const items = new Set([node1, node2, reroute])
const filtered = filterSelectableItems(items)
expect(filtered.size).toBe(2)
- // @ts-expect-error - Test mocks
expect(filtered.has(node1)).toBe(true)
- // @ts-expect-error - Test mocks
expect(filtered.has(node2)).toBe(true)
expect(filtered.has(reroute)).toBe(false)
})
@@ -143,9 +167,7 @@ describe('useSelectedLiteGraphItems', () => {
const selectableItems = getSelectableItems()
expect(selectableItems.size).toBe(2)
- // @ts-expect-error - Test mock
expect(selectableItems.has(node1)).toBe(true)
- // @ts-expect-error - Test mock
expect(selectableItems.has(node2)).toBe(true)
expect(selectableItems.has(reroute)).toBe(false)
})
@@ -200,8 +222,8 @@ describe('useSelectedLiteGraphItems', () => {
describe('node-specific methods', () => {
it('getSelectedNodes should return only LGraphNode instances', () => {
const { getSelectedNodes } = useSelectedLiteGraphItems()
- const node1 = { id: 1, mode: LGraphEventMode.ALWAYS } as LGraphNode
- const node2 = { id: 2, mode: LGraphEventMode.NEVER } as LGraphNode
+ const node1 = createMockLGraphNode(1, LGraphEventMode.ALWAYS)
+ const node2 = createMockLGraphNode(2, LGraphEventMode.NEVER)
// Mock app.canvas.selected_nodes
app.canvas.selected_nodes = { '0': node1, '1': node2 }
@@ -215,8 +237,7 @@ describe('useSelectedLiteGraphItems', () => {
it('getSelectedNodes should return empty array when no nodes selected', () => {
const { getSelectedNodes } = useSelectedLiteGraphItems()
- // @ts-expect-error - Testing null case
- app.canvas.selected_nodes = null
+ Object.assign(app.canvas, { selected_nodes: null })
const selectedNodes = getSelectedNodes()
expect(selectedNodes).toHaveLength(0)
@@ -224,8 +245,8 @@ describe('useSelectedLiteGraphItems', () => {
it('toggleSelectedNodesMode should toggle node modes correctly', () => {
const { toggleSelectedNodesMode } = useSelectedLiteGraphItems()
- const node1 = { id: 1, mode: LGraphEventMode.ALWAYS } as LGraphNode
- const node2 = { id: 2, mode: LGraphEventMode.NEVER } as LGraphNode
+ const node1 = createMockLGraphNode(1, LGraphEventMode.ALWAYS)
+ const node2 = createMockLGraphNode(2, LGraphEventMode.NEVER)
app.canvas.selected_nodes = { '0': node1, '1': node2 }
@@ -240,7 +261,7 @@ describe('useSelectedLiteGraphItems', () => {
it('toggleSelectedNodesMode should set mode to ALWAYS when already in target mode', () => {
const { toggleSelectedNodesMode } = useSelectedLiteGraphItems()
- const node = { id: 1, mode: LGraphEventMode.BYPASS } as LGraphNode
+ const node = createMockLGraphNode(1, LGraphEventMode.BYPASS)
app.canvas.selected_nodes = { '0': node }
@@ -253,17 +274,13 @@ describe('useSelectedLiteGraphItems', () => {
it('getSelectedNodes should include nodes from subgraphs', () => {
const { getSelectedNodes } = useSelectedLiteGraphItems()
- const subNode1 = { id: 11, mode: LGraphEventMode.ALWAYS } as LGraphNode
- const subNode2 = { id: 12, mode: LGraphEventMode.NEVER } as LGraphNode
- const subgraphNode = {
- id: 1,
- mode: LGraphEventMode.ALWAYS,
- isSubgraphNode: () => true,
- subgraph: {
- nodes: [subNode1, subNode2]
- }
- } as unknown as LGraphNode
- const regularNode = { id: 2, mode: LGraphEventMode.NEVER } as LGraphNode
+ const subNode1 = createMockLGraphNode(11, LGraphEventMode.ALWAYS)
+ const subNode2 = createMockLGraphNode(12, LGraphEventMode.NEVER)
+ const subgraphNode = createMockLGraphNode(1, LGraphEventMode.ALWAYS, [
+ subNode1,
+ subNode2
+ ])
+ const regularNode = createMockLGraphNode(2, LGraphEventMode.NEVER)
app.canvas.selected_nodes = { '0': subgraphNode, '1': regularNode }
@@ -277,17 +294,13 @@ describe('useSelectedLiteGraphItems', () => {
it('toggleSelectedNodesMode should apply unified state to subgraph children', () => {
const { toggleSelectedNodesMode } = useSelectedLiteGraphItems()
- const subNode1 = { id: 11, mode: LGraphEventMode.ALWAYS } as LGraphNode
- const subNode2 = { id: 12, mode: LGraphEventMode.NEVER } as LGraphNode
- const subgraphNode = {
- id: 1,
- mode: LGraphEventMode.ALWAYS,
- isSubgraphNode: () => true,
- subgraph: {
- nodes: [subNode1, subNode2]
- }
- } as unknown as LGraphNode
- const regularNode = { id: 2, mode: LGraphEventMode.BYPASS } as LGraphNode
+ const subNode1 = createMockLGraphNode(11, LGraphEventMode.ALWAYS)
+ const subNode2 = createMockLGraphNode(12, LGraphEventMode.NEVER)
+ const subgraphNode = createMockLGraphNode(1, LGraphEventMode.ALWAYS, [
+ subNode1,
+ subNode2
+ ])
+ const regularNode = createMockLGraphNode(2, LGraphEventMode.BYPASS)
app.canvas.selected_nodes = { '0': subgraphNode, '1': regularNode }
@@ -308,16 +321,13 @@ describe('useSelectedLiteGraphItems', () => {
it('toggleSelectedNodesMode should toggle to ALWAYS when subgraph is already in target mode', () => {
const { toggleSelectedNodesMode } = useSelectedLiteGraphItems()
- const subNode1 = { id: 11, mode: LGraphEventMode.ALWAYS } as LGraphNode
- const subNode2 = { id: 12, mode: LGraphEventMode.BYPASS } as LGraphNode
- const subgraphNode = {
- id: 1,
- mode: LGraphEventMode.NEVER, // Already in NEVER mode
- isSubgraphNode: () => true,
- subgraph: {
- nodes: [subNode1, subNode2]
- }
- } as unknown as LGraphNode
+ const subNode1 = createMockLGraphNode(11, LGraphEventMode.ALWAYS)
+ const subNode2 = createMockLGraphNode(12, LGraphEventMode.BYPASS)
+ // subgraphNode already in NEVER mode
+ const subgraphNode = createMockLGraphNode(1, LGraphEventMode.NEVER, [
+ subNode1,
+ subNode2
+ ])
app.canvas.selected_nodes = { '0': subgraphNode }
diff --git a/src/composables/graph/useSelectionState.test.ts b/src/composables/graph/useSelectionState.test.ts
index cf0e4bd841a..1ba6d4d79ae 100644
--- a/src/composables/graph/useSelectionState.test.ts
+++ b/src/composables/graph/useSelectionState.test.ts
@@ -1,19 +1,24 @@
-import { createPinia, setActivePinia } from 'pinia'
+import { createTestingPinia } from '@pinia/testing'
+import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, test, vi } from 'vitest'
-import { ref } from 'vue'
-import type { Ref } from 'vue'
import { useSelectionState } from '@/composables/graph/useSelectionState'
-import { useNodeLibrarySidebarTab } from '@/composables/sidebarTabs/useNodeLibrarySidebarTab'
+import type { Positionable } from '@/lib/litegraph/src/interfaces'
import { LGraphEventMode } from '@/lib/litegraph/src/litegraph'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
-import { useNodeDefStore } from '@/stores/nodeDefStore'
-import { useNodeHelpStore } from '@/stores/workspace/nodeHelpStore'
-import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
import { isImageNode, isLGraphNode } from '@/utils/litegraphUtil'
import { filterOutputNodes } from '@/utils/nodeFilterUtil'
-// Test interfaces
+vi.mock('@/utils/litegraphUtil', () => ({
+ isLGraphNode: vi.fn(),
+ isImageNode: vi.fn(),
+ isLoad3dNode: vi.fn(() => false)
+}))
+
+vi.mock('@/utils/nodeFilterUtil', () => ({
+ filterOutputNodes: vi.fn()
+}))
+
interface TestNodeConfig {
type?: string
mode?: LGraphEventMode
@@ -22,163 +27,69 @@ interface TestNodeConfig {
removable?: boolean
}
-interface TestNode {
+class MockPositionable implements Positionable {
+ readonly id = 0
+ readonly pos: [number, number] = [0, 0]
+ readonly boundingRect = [0, 0, 100, 100] as const
type: string
mode: LGraphEventMode
flags?: { collapsed?: boolean }
pinned?: boolean
removable?: boolean
- isSubgraphNode: () => boolean
-}
-
-type MockedItem = TestNode | { type: string; isNode: boolean }
-// Mock all stores
-vi.mock('@/renderer/core/canvas/canvasStore', () => ({
- useCanvasStore: vi.fn()
-}))
-
-vi.mock('@/stores/nodeDefStore', () => ({
- useNodeDefStore: vi.fn()
-}))
-
-vi.mock('@/stores/workspace/sidebarTabStore', () => ({
- useSidebarTabStore: vi.fn()
-}))
+ constructor(config: TestNodeConfig = {}) {
+ this.type = config.type ?? 'TestNode'
+ this.mode = config.mode ?? LGraphEventMode.ALWAYS
+ this.flags = config.flags
+ this.pinned = config.pinned
+ this.removable = config.removable
+ }
-vi.mock('@/stores/workspace/nodeHelpStore', () => ({
- useNodeHelpStore: vi.fn()
-}))
+ move(): void {}
+ snapToGrid(): boolean {
+ return false
+ }
+ isSubgraphNode(): boolean {
+ return false
+ }
+}
-vi.mock('@/composables/sidebarTabs/useNodeLibrarySidebarTab', () => ({
- useNodeLibrarySidebarTab: vi.fn()
-}))
+function createTestNode(config: TestNodeConfig = {}): MockPositionable {
+ return new MockPositionable(config)
+}
-vi.mock('@/utils/litegraphUtil', () => ({
- isLGraphNode: vi.fn(),
- isImageNode: vi.fn()
-}))
+class MockNonNode implements Positionable {
+ readonly id = 0
+ readonly pos: [number, number] = [0, 0]
+ readonly boundingRect = [0, 0, 100, 100] as const
+ readonly isNode = false
+ type: string
-vi.mock('@/utils/nodeFilterUtil', () => ({
- filterOutputNodes: vi.fn()
-}))
+ constructor(type: string) {
+ this.type = type
+ }
-const createTestNode = (config: TestNodeConfig = {}): TestNode => {
- return {
- type: config.type || 'TestNode',
- mode: config.mode || LGraphEventMode.ALWAYS,
- flags: config.flags,
- pinned: config.pinned,
- removable: config.removable,
- isSubgraphNode: () => false
+ move(): void {}
+ snapToGrid(): boolean {
+ return false
}
}
-// Mock comment/connection objects
-const mockComment = { type: 'comment', isNode: false }
-const mockConnection = { type: 'connection', isNode: false }
+const mockComment = new MockNonNode('comment')
+const mockConnection = new MockNonNode('connection')
describe('useSelectionState', () => {
- // Mock store instances
- let mockSelectedItems: Ref
-
beforeEach(() => {
vi.clearAllMocks()
- setActivePinia(createPinia())
-
- // Setup mock canvas store with proper ref
- mockSelectedItems = ref([])
- vi.mocked(useCanvasStore).mockReturnValue({
- selectedItems: mockSelectedItems,
- // Add minimal required properties for the store
- $id: 'canvas',
- $state: {} as any,
- $patch: vi.fn(),
- $reset: vi.fn(),
- $subscribe: vi.fn(),
- $onAction: vi.fn(),
- $dispose: vi.fn(),
- _customProperties: new Set(),
- _p: {} as any
- } as any)
-
- // Setup mock node def store
- vi.mocked(useNodeDefStore).mockReturnValue({
- fromLGraphNode: vi.fn((node: TestNode) => {
- if (node?.type === 'TestNode') {
- return { nodePath: 'test.TestNode', name: 'TestNode' }
- }
- return null
- }),
- // Add minimal required properties for the store
- $id: 'nodeDef',
- $state: {} as any,
- $patch: vi.fn(),
- $reset: vi.fn(),
- $subscribe: vi.fn(),
- $onAction: vi.fn(),
- $dispose: vi.fn(),
- _customProperties: new Set(),
- _p: {} as any
- } as any)
+ setActivePinia(createTestingPinia({ stubActions: false }))
- // Setup mock sidebar tab store
- const mockToggleSidebarTab = vi.fn()
- vi.mocked(useSidebarTabStore).mockReturnValue({
- activeSidebarTabId: null,
- toggleSidebarTab: mockToggleSidebarTab,
- // Add minimal required properties for the store
- $id: 'sidebarTab',
- $state: {} as any,
- $patch: vi.fn(),
- $reset: vi.fn(),
- $subscribe: vi.fn(),
- $onAction: vi.fn(),
- $dispose: vi.fn(),
- _customProperties: new Set(),
- _p: {} as any
- } as any)
-
- // Setup mock node help store
- const mockOpenHelp = vi.fn()
- const mockCloseHelp = vi.fn()
- const mockNodeHelpStore = {
- isHelpOpen: false,
- currentHelpNode: null,
- openHelp: mockOpenHelp,
- closeHelp: mockCloseHelp,
- // Add minimal required properties for the store
- $id: 'nodeHelp',
- $state: {} as any,
- $patch: vi.fn(),
- $reset: vi.fn(),
- $subscribe: vi.fn(),
- $onAction: vi.fn(),
- $dispose: vi.fn(),
- _customProperties: new Set(),
- _p: {} as any
- }
- vi.mocked(useNodeHelpStore).mockReturnValue(mockNodeHelpStore as any)
-
- // Setup mock composables
- vi.mocked(useNodeLibrarySidebarTab).mockReturnValue({
- id: 'node-library-tab',
- title: 'Node Library',
- type: 'custom',
- render: () => null
- } as any)
-
- // Setup mock utility functions
vi.mocked(isLGraphNode).mockImplementation((item: unknown) => {
- const typedItem = item as { isNode?: boolean }
- return typedItem?.isNode !== false
- })
- vi.mocked(isImageNode).mockImplementation((node: unknown) => {
- const typedNode = node as { type?: string }
- return typedNode?.type === 'ImageNode'
+ if (typeof item !== 'object' || item === null) return false
+ return !('isNode' in item && item.isNode === false)
})
- vi.mocked(filterOutputNodes).mockImplementation(
- (nodes: TestNode[]) => nodes.filter((n) => n.type === 'OutputNode') as any
+ vi.mocked(isImageNode).mockReturnValue(false)
+ vi.mocked(filterOutputNodes).mockImplementation((nodes) =>
+ nodes.filter((n) => n.type === 'OutputNode')
)
})
@@ -189,10 +100,10 @@ describe('useSelectionState', () => {
})
test('should return true when items selected', () => {
- // Update the mock data before creating the composable
+ const canvasStore = useCanvasStore()
const node1 = createTestNode()
const node2 = createTestNode()
- mockSelectedItems.value = [node1, node2]
+ canvasStore.selectedItems.push(node1, node2)
const { hasAnySelection } = useSelectionState()
expect(hasAnySelection.value).toBe(true)
@@ -201,9 +112,9 @@ describe('useSelectionState', () => {
describe('Node Type Filtering', () => {
test('should pick only LGraphNodes from mixed selections', () => {
- // Update the mock data before creating the composable
+ const canvasStore = useCanvasStore()
const graphNode = createTestNode()
- mockSelectedItems.value = [graphNode, mockComment, mockConnection]
+ canvasStore.selectedItems.push(graphNode, mockComment, mockConnection)
const { selectedNodes } = useSelectionState()
expect(selectedNodes.value).toHaveLength(1)
@@ -213,9 +124,9 @@ describe('useSelectionState', () => {
describe('Node State Computation', () => {
test('should detect bypassed nodes', () => {
- // Update the mock data before creating the composable
+ const canvasStore = useCanvasStore()
const bypassedNode = createTestNode({ mode: LGraphEventMode.BYPASS })
- mockSelectedItems.value = [bypassedNode]
+ canvasStore.selectedItems.push(bypassedNode)
const { selectedNodes } = useSelectionState()
const isBypassed = selectedNodes.value.some(
@@ -225,10 +136,10 @@ describe('useSelectionState', () => {
})
test('should detect pinned/collapsed states', () => {
- // Update the mock data before creating the composable
+ const canvasStore = useCanvasStore()
const pinnedNode = createTestNode({ pinned: true })
const collapsedNode = createTestNode({ flags: { collapsed: true } })
- mockSelectedItems.value = [pinnedNode, collapsedNode]
+ canvasStore.selectedItems.push(pinnedNode, collapsedNode)
const { selectedNodes } = useSelectionState()
const isPinned = selectedNodes.value.some((n) => n.pinned === true)
@@ -244,9 +155,9 @@ describe('useSelectionState', () => {
})
test('should provide non-reactive state computation', () => {
- // Update the mock data before creating the composable
+ const canvasStore = useCanvasStore()
const node = createTestNode({ pinned: true })
- mockSelectedItems.value = [node]
+ canvasStore.selectedItems.push(node)
const { selectedNodes } = useSelectionState()
const isPinned = selectedNodes.value.some((n) => n.pinned === true)
@@ -261,8 +172,7 @@ describe('useSelectionState', () => {
expect(isCollapsed).toBe(false)
expect(isBypassed).toBe(false)
- // Test with empty selection using new composable instance
- mockSelectedItems.value = []
+ canvasStore.selectedItems.length = 0
const { selectedNodes: newSelectedNodes } = useSelectionState()
const newIsPinned = newSelectedNodes.value.some((n) => n.pinned === true)
expect(newIsPinned).toBe(false)
diff --git a/src/composables/maskeditor/useCanvasHistory.test.ts b/src/composables/maskeditor/useCanvasHistory.test.ts
index 6281baf3964..bf81196fa88 100644
--- a/src/composables/maskeditor/useCanvasHistory.test.ts
+++ b/src/composables/maskeditor/useCanvasHistory.test.ts
@@ -67,7 +67,10 @@ vi.mock('@/stores/maskEditorStore', () => ({
// Mock ImageBitmap using safe global augmentation pattern
if (typeof globalThis.ImageBitmap === 'undefined') {
- globalThis.ImageBitmap = class ImageBitmap {
+ class MockImageBitmap implements Pick<
+ ImageBitmap,
+ 'width' | 'height' | 'close'
+ > {
width: number
height: number
constructor(width = 100, height = 100) {
@@ -75,7 +78,8 @@ if (typeof globalThis.ImageBitmap === 'undefined') {
this.height = height
}
close() {}
- } as unknown as typeof globalThis.ImageBitmap
+ }
+ Object.defineProperty(globalThis, 'ImageBitmap', { value: MockImageBitmap })
}
describe('useCanvasHistory', () => {
diff --git a/src/composables/maskeditor/useCanvasManager.test.ts b/src/composables/maskeditor/useCanvasManager.test.ts
index 4fe40df6e99..ebb3c746620 100644
--- a/src/composables/maskeditor/useCanvasManager.test.ts
+++ b/src/composables/maskeditor/useCanvasManager.test.ts
@@ -1,20 +1,112 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
-import { MaskBlendMode } from '@/extensions/core/maskeditor/types'
import { useCanvasManager } from '@/composables/maskeditor/useCanvasManager'
-const mockStore = {
- imgCanvas: null as any,
- maskCanvas: null as any,
- rgbCanvas: null as any,
- imgCtx: null as any,
- maskCtx: null as any,
- rgbCtx: null as any,
- canvasBackground: null as any,
- maskColor: { r: 0, g: 0, b: 0 },
- maskBlendMode: MaskBlendMode.Black,
- maskOpacity: 0.8
+import { MaskBlendMode } from '@/extensions/core/maskeditor/types'
+
+interface MockCanvasStyle {
+ mixBlendMode: string
+ opacity: string
+ backgroundColor: string
+}
+
+interface MockCanvas {
+ width: number
+ height: number
+ style: Partial
+}
+
+interface MockContext {
+ drawImage: ReturnType
+ getImageData?: ReturnType
+ putImageData?: ReturnType
+ globalCompositeOperation?: string
+ fillStyle?: string
+}
+
+interface MockStore {
+ imgCanvas: MockCanvas | null
+ maskCanvas: MockCanvas | null
+ rgbCanvas: MockCanvas | null
+ imgCtx: MockContext | null
+ maskCtx: MockContext | null
+ rgbCtx: MockContext | null
+ canvasBackground: { style: Partial } | null
+ maskColor: { r: number; g: number; b: number }
+ maskBlendMode: MaskBlendMode
+ maskOpacity: number
}
+const {
+ mockStore,
+ getImgCanvas,
+ getMaskCanvas,
+ getRgbCanvas,
+ getImgCtx,
+ getMaskCtx,
+ getRgbCtx,
+ getCanvasBackground
+} = vi.hoisted(() => {
+ const mockStore: MockStore = {
+ imgCanvas: null,
+ maskCanvas: null,
+ rgbCanvas: null,
+ imgCtx: null,
+ maskCtx: null,
+ rgbCtx: null,
+ canvasBackground: null,
+ maskColor: { r: 0, g: 0, b: 0 },
+ maskBlendMode: 'black' as MaskBlendMode,
+ maskOpacity: 0.8
+ }
+
+ function getImgCanvas(): MockCanvas {
+ if (!mockStore.imgCanvas) throw new Error('imgCanvas not initialized')
+ return mockStore.imgCanvas
+ }
+
+ function getMaskCanvas(): MockCanvas {
+ if (!mockStore.maskCanvas) throw new Error('maskCanvas not initialized')
+ return mockStore.maskCanvas
+ }
+
+ function getRgbCanvas(): MockCanvas {
+ if (!mockStore.rgbCanvas) throw new Error('rgbCanvas not initialized')
+ return mockStore.rgbCanvas
+ }
+
+ function getImgCtx(): MockContext {
+ if (!mockStore.imgCtx) throw new Error('imgCtx not initialized')
+ return mockStore.imgCtx
+ }
+
+ function getMaskCtx(): MockContext {
+ if (!mockStore.maskCtx) throw new Error('maskCtx not initialized')
+ return mockStore.maskCtx
+ }
+
+ function getRgbCtx(): MockContext {
+ if (!mockStore.rgbCtx) throw new Error('rgbCtx not initialized')
+ return mockStore.rgbCtx
+ }
+
+ function getCanvasBackground(): { style: Partial } {
+ if (!mockStore.canvasBackground)
+ throw new Error('canvasBackground not initialized')
+ return mockStore.canvasBackground
+ }
+
+ return {
+ mockStore,
+ getImgCanvas,
+ getMaskCanvas,
+ getRgbCanvas,
+ getImgCtx,
+ getMaskCtx,
+ getRgbCtx,
+ getCanvasBackground
+ }
+})
+
vi.mock('@/stores/maskEditorStore', () => ({
useMaskEditorStore: vi.fn(() => mockStore)
}))
@@ -56,7 +148,8 @@ describe('useCanvasManager', () => {
mockStore.imgCanvas = {
width: 0,
- height: 0
+ height: 0,
+ style: {}
}
mockStore.maskCanvas = {
@@ -70,7 +163,8 @@ describe('useCanvasManager', () => {
mockStore.rgbCanvas = {
width: 0,
- height: 0
+ height: 0,
+ style: {}
}
mockStore.canvasBackground = {
@@ -93,12 +187,12 @@ describe('useCanvasManager', () => {
await manager.invalidateCanvas(origImage, maskImage, null)
- expect(mockStore.imgCanvas.width).toBe(512)
- expect(mockStore.imgCanvas.height).toBe(512)
- expect(mockStore.maskCanvas.width).toBe(512)
- expect(mockStore.maskCanvas.height).toBe(512)
- expect(mockStore.rgbCanvas.width).toBe(512)
- expect(mockStore.rgbCanvas.height).toBe(512)
+ expect(getImgCanvas().width).toBe(512)
+ expect(getImgCanvas().height).toBe(512)
+ expect(getMaskCanvas().width).toBe(512)
+ expect(getMaskCanvas().height).toBe(512)
+ expect(getRgbCanvas().width).toBe(512)
+ expect(getRgbCanvas().height).toBe(512)
})
it('should draw original image', async () => {
@@ -109,7 +203,7 @@ describe('useCanvasManager', () => {
await manager.invalidateCanvas(origImage, maskImage, null)
- expect(mockStore.imgCtx.drawImage).toHaveBeenCalledWith(
+ expect(getImgCtx().drawImage).toHaveBeenCalledWith(
origImage,
0,
0,
@@ -127,7 +221,7 @@ describe('useCanvasManager', () => {
await manager.invalidateCanvas(origImage, maskImage, paintImage)
- expect(mockStore.rgbCtx.drawImage).toHaveBeenCalledWith(
+ expect(getRgbCtx().drawImage).toHaveBeenCalledWith(
paintImage,
0,
0,
@@ -144,7 +238,7 @@ describe('useCanvasManager', () => {
await manager.invalidateCanvas(origImage, maskImage, null)
- expect(mockStore.rgbCtx.drawImage).not.toHaveBeenCalled()
+ expect(getRgbCtx().drawImage).not.toHaveBeenCalled()
})
it('should prepare mask', async () => {
@@ -155,9 +249,9 @@ describe('useCanvasManager', () => {
await manager.invalidateCanvas(origImage, maskImage, null)
- expect(mockStore.maskCtx.drawImage).toHaveBeenCalled()
- expect(mockStore.maskCtx.getImageData).toHaveBeenCalled()
- expect(mockStore.maskCtx.putImageData).toHaveBeenCalled()
+ expect(getMaskCtx().drawImage).toHaveBeenCalled()
+ expect(getMaskCtx().getImageData).toHaveBeenCalled()
+ expect(getMaskCtx().putImageData).toHaveBeenCalled()
})
it('should throw error when canvas missing', async () => {
@@ -196,12 +290,10 @@ describe('useCanvasManager', () => {
await manager.updateMaskColor()
- expect(mockStore.maskCtx.fillStyle).toBe('rgb(0, 0, 0)')
- expect(mockStore.maskCanvas.style.mixBlendMode).toBe('initial')
- expect(mockStore.maskCanvas.style.opacity).toBe('0.8')
- expect(mockStore.canvasBackground.style.backgroundColor).toBe(
- 'rgba(0,0,0,1)'
- )
+ expect(getMaskCtx().fillStyle).toBe('rgb(0, 0, 0)')
+ expect(getMaskCanvas().style.mixBlendMode).toBe('initial')
+ expect(getMaskCanvas().style.opacity).toBe('0.8')
+ expect(getCanvasBackground().style.backgroundColor).toBe('rgba(0,0,0,1)')
})
it('should update mask color for white blend mode', async () => {
@@ -212,9 +304,9 @@ describe('useCanvasManager', () => {
await manager.updateMaskColor()
- expect(mockStore.maskCtx.fillStyle).toBe('rgb(255, 255, 255)')
- expect(mockStore.maskCanvas.style.mixBlendMode).toBe('initial')
- expect(mockStore.canvasBackground.style.backgroundColor).toBe(
+ expect(getMaskCtx().fillStyle).toBe('rgb(255, 255, 255)')
+ expect(getMaskCanvas().style.mixBlendMode).toBe('initial')
+ expect(getCanvasBackground().style.backgroundColor).toBe(
'rgba(255,255,255,1)'
)
})
@@ -227,9 +319,9 @@ describe('useCanvasManager', () => {
await manager.updateMaskColor()
- expect(mockStore.maskCanvas.style.mixBlendMode).toBe('difference')
- expect(mockStore.maskCanvas.style.opacity).toBe('1')
- expect(mockStore.canvasBackground.style.backgroundColor).toBe(
+ expect(getMaskCanvas().style.mixBlendMode).toBe('difference')
+ expect(getMaskCanvas().style.opacity).toBe('1')
+ expect(getCanvasBackground().style.backgroundColor).toBe(
'rgba(255,255,255,1)'
)
})
@@ -238,8 +330,8 @@ describe('useCanvasManager', () => {
const manager = useCanvasManager()
mockStore.maskColor = { r: 128, g: 64, b: 32 }
- mockStore.maskCanvas.width = 100
- mockStore.maskCanvas.height = 100
+ getMaskCanvas().width = 100
+ getMaskCanvas().height = 100
await manager.updateMaskColor()
@@ -249,7 +341,7 @@ describe('useCanvasManager', () => {
expect(mockImageData.data[i + 2]).toBe(32)
}
- expect(mockStore.maskCtx.putImageData).toHaveBeenCalledWith(
+ expect(getMaskCtx().putImageData).toHaveBeenCalledWith(
mockImageData,
0,
0
@@ -258,22 +350,24 @@ describe('useCanvasManager', () => {
it('should return early when canvas missing', async () => {
const manager = useCanvasManager()
+ const maskCtxBeforeNull = getMaskCtx()
mockStore.maskCanvas = null
await manager.updateMaskColor()
- expect(mockStore.maskCtx.getImageData).not.toHaveBeenCalled()
+ expect(maskCtxBeforeNull.getImageData).not.toHaveBeenCalled()
})
it('should return early when context missing', async () => {
const manager = useCanvasManager()
+ const canvasBgBeforeNull = getCanvasBackground()
mockStore.maskCtx = null
await manager.updateMaskColor()
- expect(mockStore.canvasBackground.style.backgroundColor).toBe('')
+ expect(canvasBgBeforeNull.style.backgroundColor).toBe('')
})
it('should handle different opacity values', async () => {
@@ -283,7 +377,7 @@ describe('useCanvasManager', () => {
await manager.updateMaskColor()
- expect(mockStore.maskCanvas.style.opacity).toBe('0.5')
+ expect(getMaskCanvas().style.opacity).toBe('0.5')
})
})
@@ -330,7 +424,7 @@ describe('useCanvasManager', () => {
await manager.invalidateCanvas(origImage, maskImage, null)
- expect(mockStore.maskCtx.globalCompositeOperation).toBe('source-over')
+ expect(getMaskCtx().globalCompositeOperation).toBe('source-over')
})
})
})
diff --git a/src/composables/maskeditor/useCanvasTransform.test.ts b/src/composables/maskeditor/useCanvasTransform.test.ts
index 95c153d29c6..aaee9e2e27b 100644
--- a/src/composables/maskeditor/useCanvasTransform.test.ts
+++ b/src/composables/maskeditor/useCanvasTransform.test.ts
@@ -63,7 +63,7 @@ vi.mock('@/stores/maskEditorStore', () => ({
// Mock ImageData with improved type safety
if (typeof globalThis.ImageData === 'undefined') {
- globalThis.ImageData = class ImageData {
+ class MockImageData {
data: Uint8ClampedArray
width: number
height: number
@@ -95,12 +95,16 @@ if (typeof globalThis.ImageData === 'undefined') {
this.data = new Uint8ClampedArray(dataOrWidth * widthOrHeight * 4)
}
}
- } as unknown as typeof globalThis.ImageData
+ }
+ Object.defineProperty(globalThis, 'ImageData', { value: MockImageData })
}
// Mock ImageBitmap for test environment using safe type casting
if (typeof globalThis.ImageBitmap === 'undefined') {
- globalThis.ImageBitmap = class ImageBitmap {
+ class MockImageBitmap implements Pick<
+ ImageBitmap,
+ 'width' | 'height' | 'close'
+ > {
width: number
height: number
constructor(width = 100, height = 100) {
@@ -108,7 +112,8 @@ if (typeof globalThis.ImageBitmap === 'undefined') {
this.height = height
}
close() {}
- } as unknown as typeof globalThis.ImageBitmap
+ }
+ Object.defineProperty(globalThis, 'ImageBitmap', { value: MockImageBitmap })
}
describe('useCanvasTransform', () => {
diff --git a/src/composables/useGlobalLitegraph.ts b/src/composables/useGlobalLitegraph.ts
index 107f8e28928..21c0b38cb21 100644
--- a/src/composables/useGlobalLitegraph.ts
+++ b/src/composables/useGlobalLitegraph.ts
@@ -6,30 +6,21 @@ import {
LGraphCanvas,
LGraphGroup,
LGraphNode,
- LLink,
- LiteGraph
+ LiteGraph,
+ LLink
} from '@/lib/litegraph/src/litegraph'
/**
* Assign all properties of LiteGraph to window to make it backward compatible.
*/
-export const useGlobalLitegraph = () => {
- // @ts-expect-error fixme ts strict error
- window['LiteGraph'] = LiteGraph
- // @ts-expect-error fixme ts strict error
- window['LGraph'] = LGraph
- // @ts-expect-error fixme ts strict error
- window['LLink'] = LLink
- // @ts-expect-error fixme ts strict error
- window['LGraphNode'] = LGraphNode
- // @ts-expect-error fixme ts strict error
- window['LGraphGroup'] = LGraphGroup
- // @ts-expect-error fixme ts strict error
- window['DragAndScale'] = DragAndScale
- // @ts-expect-error fixme ts strict error
- window['LGraphCanvas'] = LGraphCanvas
- // @ts-expect-error fixme ts strict error
- window['ContextMenu'] = ContextMenu
- // @ts-expect-error fixme ts strict error
- window['LGraphBadge'] = LGraphBadge
+export function useGlobalLitegraph() {
+ window.LiteGraph = LiteGraph
+ window.LGraph = LGraph
+ window.LLink = LLink
+ window.LGraphNode = LGraphNode
+ window.LGraphGroup = LGraphGroup
+ window.DragAndScale = DragAndScale
+ window.LGraphCanvas = LGraphCanvas
+ window.ContextMenu = ContextMenu
+ window.LGraphBadge = LGraphBadge
}
diff --git a/src/composables/usePaste.test.ts b/src/composables/usePaste.test.ts
index 4e1ac3503c5..d48e9e114de 100644
--- a/src/composables/usePaste.test.ts
+++ b/src/composables/usePaste.test.ts
@@ -1,7 +1,6 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type {
LGraphCanvas,
- LGraph,
LGraphGroup,
LGraphNode
} from '@/lib/litegraph/src/litegraph'
@@ -10,11 +9,19 @@ import { app } from '@/scripts/app'
import { isImageNode } from '@/utils/litegraphUtil'
import { pasteImageNode, usePaste } from './usePaste'
-function createMockNode() {
+interface MockPasteNode {
+ pos: [number, number]
+ pasteFile: (file: File) => void
+ pasteFiles: (files: File[]) => void
+ is_selected?: boolean
+}
+
+function createMockNode(options?: Partial): MockPasteNode {
return {
pos: [0, 0],
- pasteFile: vi.fn(),
- pasteFiles: vi.fn()
+ pasteFile: vi.fn<(file: File) => void>(),
+ pasteFiles: vi.fn<(files: File[]) => void>(),
+ ...options
}
}
@@ -38,16 +45,31 @@ function createDataTransfer(files: File[] = []): DataTransfer {
return dataTransfer
}
-const mockCanvas = {
- current_node: null as LGraphNode | null,
- graph: {
- add: vi.fn(),
- change: vi.fn()
- } as Partial as LGraph,
+interface MockGraph {
+ add: ReturnType
+ change: ReturnType
+}
+
+interface MockCanvas {
+ current_node: LGraphNode | null
+ graph: MockGraph
+ graph_mouse: [number, number]
+ pasteFromClipboard: ReturnType
+ _deserializeItems: ReturnType
+}
+
+const mockGraph: MockGraph = {
+ add: vi.fn(),
+ change: vi.fn()
+}
+
+const mockCanvas: MockCanvas = {
+ current_node: null,
+ graph: mockGraph,
graph_mouse: [100, 200],
pasteFromClipboard: vi.fn(),
_deserializeItems: vi.fn()
-} as Partial as LGraphCanvas
+}
const mockCanvasStore = {
canvas: mockCanvas,
@@ -81,7 +103,7 @@ vi.mock('@/scripts/app', () => ({
vi.mock('@/lib/litegraph/src/litegraph', () => ({
LiteGraph: {
- createNode: vi.fn()
+ createNode: vi.fn<(type: string) => LGraphNode | undefined>()
}
}))
@@ -95,30 +117,38 @@ vi.mock('@/workbench/eventHelpers', () => ({
shouldIgnoreCopyPaste: vi.fn()
}))
+function asLGraphCanvas(canvas: MockCanvas): LGraphCanvas {
+ return Object.assign(Object.create(null), canvas)
+}
+
+function asLGraphNode(node: MockPasteNode): LGraphNode {
+ return Object.assign(Object.create(null), node)
+}
+
describe('pasteImageNode', () => {
beforeEach(() => {
vi.clearAllMocks()
- vi.mocked(mockCanvas.graph!.add).mockImplementation(
- (node: LGraphNode | LGraphGroup) => node as LGraphNode
- )
+ mockGraph.add.mockImplementation((node: LGraphNode | LGraphGroup) => node)
})
it('should create new LoadImage node when no image node provided', () => {
const mockNode = createMockNode()
- vi.mocked(LiteGraph.createNode).mockReturnValue(
- mockNode as unknown as LGraphNode
- )
+ const createdNode = asLGraphNode(mockNode)
+ vi.mocked(LiteGraph.createNode).mockReturnValue(createdNode)
const file = createImageFile()
const dataTransfer = createDataTransfer([file])
- pasteImageNode(mockCanvas as unknown as LGraphCanvas, dataTransfer.items)
+ pasteImageNode(asLGraphCanvas(mockCanvas), dataTransfer.items)
expect(LiteGraph.createNode).toHaveBeenCalledWith('LoadImage')
- expect(mockNode.pos).toEqual([100, 200])
- expect(mockCanvas.graph!.add).toHaveBeenCalledWith(mockNode)
- expect(mockCanvas.graph!.change).toHaveBeenCalled()
- expect(mockNode.pasteFile).toHaveBeenCalledWith(file)
+ // Verify pos was set on the created node (not on mockNode since Object.assign copies)
+ expect(createdNode.pos).toEqual([100, 200])
+ expect(mockGraph.add).toHaveBeenCalled()
+ expect(mockGraph.change).toHaveBeenCalled()
+ // pasteFile was called on the node returned by graph.add
+ const addedNode = mockGraph.add.mock.results[0].value
+ expect(addedNode.pasteFile).toHaveBeenCalledWith(file)
})
it('should use existing image node when provided', () => {
@@ -126,11 +156,7 @@ describe('pasteImageNode', () => {
const file = createImageFile()
const dataTransfer = createDataTransfer([file])
- pasteImageNode(
- mockCanvas as unknown as LGraphCanvas,
- dataTransfer.items,
- mockNode as unknown as LGraphNode
- )
+ pasteImageNode(asLGraphCanvas(mockCanvas), dataTransfer.items, mockNode)
expect(mockNode.pasteFile).toHaveBeenCalledWith(file)
expect(mockNode.pasteFiles).toHaveBeenCalledWith([file])
@@ -142,11 +168,7 @@ describe('pasteImageNode', () => {
const file2 = createImageFile('test2.jpg', 'image/jpeg')
const dataTransfer = createDataTransfer([file1, file2])
- pasteImageNode(
- mockCanvas as unknown as LGraphCanvas,
- dataTransfer.items,
- mockNode as unknown as LGraphNode
- )
+ pasteImageNode(asLGraphCanvas(mockCanvas), dataTransfer.items, mockNode)
expect(mockNode.pasteFile).toHaveBeenCalledWith(file1)
expect(mockNode.pasteFiles).toHaveBeenCalledWith([file1, file2])
@@ -156,11 +178,7 @@ describe('pasteImageNode', () => {
const mockNode = createMockNode()
const dataTransfer = createDataTransfer()
- pasteImageNode(
- mockCanvas as unknown as LGraphCanvas,
- dataTransfer.items,
- mockNode as unknown as LGraphNode
- )
+ pasteImageNode(asLGraphCanvas(mockCanvas), dataTransfer.items, mockNode)
expect(mockNode.pasteFile).not.toHaveBeenCalled()
expect(mockNode.pasteFiles).not.toHaveBeenCalled()
@@ -172,11 +190,7 @@ describe('pasteImageNode', () => {
const textFile = new File([''], 'test.txt', { type: 'text/plain' })
const dataTransfer = createDataTransfer([textFile, imageFile])
- pasteImageNode(
- mockCanvas as unknown as LGraphCanvas,
- dataTransfer.items,
- mockNode as unknown as LGraphNode
- )
+ pasteImageNode(asLGraphCanvas(mockCanvas), dataTransfer.items, mockNode)
expect(mockNode.pasteFile).toHaveBeenCalledWith(imageFile)
expect(mockNode.pasteFiles).toHaveBeenCalledWith([imageFile])
@@ -188,16 +202,12 @@ describe('usePaste', () => {
vi.clearAllMocks()
mockCanvas.current_node = null
mockWorkspaceStore.shiftDown = false
- vi.mocked(mockCanvas.graph!.add).mockImplementation(
- (node: LGraphNode | LGraphGroup) => node as LGraphNode
- )
+ mockGraph.add.mockImplementation((node: LGraphNode | LGraphGroup) => node)
})
it('should handle image paste', async () => {
const mockNode = createMockNode()
- vi.mocked(LiteGraph.createNode).mockReturnValue(
- mockNode as unknown as LGraphNode
- )
+ vi.mocked(LiteGraph.createNode).mockReturnValue(asLGraphNode(mockNode))
usePaste()
@@ -214,9 +224,7 @@ describe('usePaste', () => {
it('should handle audio paste', async () => {
const mockNode = createMockNode()
- vi.mocked(LiteGraph.createNode).mockReturnValue(
- mockNode as unknown as LGraphNode
- )
+ vi.mocked(LiteGraph.createNode).mockReturnValue(asLGraphNode(mockNode))
usePaste()
@@ -261,12 +269,8 @@ describe('usePaste', () => {
})
it('should use existing image node when selected', () => {
- const mockNode = {
- is_selected: true,
- pasteFile: vi.fn(),
- pasteFiles: vi.fn()
- } as unknown as Partial as LGraphNode
- mockCanvas.current_node = mockNode
+ const mockNode = createMockNode({ is_selected: true })
+ mockCanvas.current_node = asLGraphNode(mockNode)
vi.mocked(isImageNode).mockReturnValue(true)
usePaste()
diff --git a/src/composables/usePaste.ts b/src/composables/usePaste.ts
index 1809eb838d7..5a0620275fb 100644
--- a/src/composables/usePaste.ts
+++ b/src/composables/usePaste.ts
@@ -9,6 +9,12 @@ import { useWorkspaceStore } from '@/stores/workspaceStore'
import { isAudioNode, isImageNode, isVideoNode } from '@/utils/litegraphUtil'
import { shouldIgnoreCopyPaste } from '@/workbench/eventHelpers'
+/** A node that supports pasting files */
+interface PasteableNode {
+ pasteFile?(file: File): void
+ pasteFiles?(files: File[]): void
+}
+
function pasteClipboardItems(data: DataTransfer): boolean {
const rawData = data.getData('text/html')
const match = rawData.match(/data-metadata="([A-Za-z0-9+/=]+)"/)?.[1]
@@ -28,7 +34,7 @@ function pasteClipboardItems(data: DataTransfer): boolean {
function pasteItemsOnNode(
items: DataTransferItemList,
- node: LGraphNode | null,
+ node: PasteableNode | null,
contentType: string
): void {
if (!node) return
@@ -51,7 +57,7 @@ function pasteItemsOnNode(
export function pasteImageNode(
canvas: LGraphCanvas,
items: DataTransferItemList,
- imageNode: LGraphNode | null = null
+ imageNode: PasteableNode | null = null
): void {
const {
graph,
diff --git a/src/extensions/core/contextMenuFilter.ts b/src/extensions/core/contextMenuFilter.ts
index 5cb0e98c585..5b1cd5a1264 100644
--- a/src/extensions/core/contextMenuFilter.ts
+++ b/src/extensions/core/contextMenuFilter.ts
@@ -1,179 +1,188 @@
import {
+ ContextMenu,
LGraphCanvas,
LiteGraph,
isComboWidget
} from '@/lib/litegraph/src/litegraph'
+import type {
+ IContextMenuOptions,
+ IContextMenuValue
+} from '@/lib/litegraph/src/litegraph'
import { app } from '../../scripts/app'
// Adds filtering to combo context menus
-const ext = {
- name: 'Comfy.ContextMenuFilter',
- init() {
- const ctxMenu = LiteGraph.ContextMenu
-
- // @ts-expect-error TODO Very hacky way to modify Litegraph behaviour. Fix ctx later.
- LiteGraph.ContextMenu = function (values, options) {
- const ctx = new ctxMenu(values, options)
-
- // If we are a dark menu (only used for combo boxes) then add a filter input
- if (options?.className === 'dark' && values?.length > 4) {
- const filter = document.createElement('input')
- filter.classList.add('comfy-context-menu-filter')
- filter.placeholder = 'Filter list'
-
- ctx.root.prepend(filter)
-
- const items = Array.from(
- ctx.root.querySelectorAll('.litemenu-entry')
- ) as HTMLElement[]
- let displayedItems = [...items]
- let itemCount = displayedItems.length
-
- // We must request an animation frame for the current node of the active canvas to update.
- requestAnimationFrame(() => {
- const currentNode = LGraphCanvas.active_canvas.current_node
- const clickedComboValue = currentNode?.widgets
- ?.filter(
- (w) =>
- isComboWidget(w) && w.options.values?.length === values.length
- )
- .find((w) =>
- // @ts-expect-error Poorly typed; filter above "should" mitigate exceptions
- w.options.values?.every((v, i) => v === values[i])
- )?.value
-
- let selectedIndex = clickedComboValue
- ? values.findIndex((v: string) => v === clickedComboValue)
- : 0
- if (selectedIndex < 0) {
- selectedIndex = 0
- }
- let selectedItem = displayedItems[selectedIndex]
- updateSelected()
-
- // Apply highlighting to the selected item
- function updateSelected() {
- selectedItem?.style.setProperty('background-color', '')
- selectedItem?.style.setProperty('color', '')
- selectedItem = displayedItems[selectedIndex]
- selectedItem?.style.setProperty(
- 'background-color',
- '#ccc',
- 'important'
+class FilteredContextMenu extends ContextMenu {
+ constructor(
+ values: readonly (string | IContextMenuValue | null)[],
+ options: IContextMenuOptions
+ ) {
+ super(values, options)
+
+ // If we are a dark menu (only used for combo boxes) then add a filter input
+ if (options?.className === 'dark' && values?.length > 4) {
+ const filter = document.createElement('input')
+ filter.classList.add('comfy-context-menu-filter')
+ filter.placeholder = 'Filter list'
+
+ this.root.prepend(filter)
+
+ const items = Array.from(
+ this.root.querySelectorAll('.litemenu-entry')
+ )
+ let displayedItems = [...items]
+ let itemCount = displayedItems.length
+
+ // We must request an animation frame for the current node of the active canvas to update.
+ requestAnimationFrame(() => {
+ const activeCanvas = LGraphCanvas.active_canvas
+ if (!activeCanvas) return
+
+ const currentNode = activeCanvas.current_node
+ const clickedComboValue = currentNode?.widgets
+ ?.filter(
+ (w) =>
+ isComboWidget(w) && w.options.values?.length === values.length
+ )
+ .find((w) => {
+ const widgetValues = w.options.values
+ return (
+ Array.isArray(widgetValues) &&
+ widgetValues.every((v, i) => v === values[i])
)
- selectedItem?.style.setProperty('color', '#000', 'important')
+ })?.value
+
+ let selectedIndex = clickedComboValue
+ ? values.findIndex((v) => v === clickedComboValue)
+ : 0
+ if (selectedIndex < 0) {
+ selectedIndex = 0
+ }
+ let selectedItem = displayedItems[selectedIndex]
+ updateSelected()
+
+ // Apply highlighting to the selected item
+ function updateSelected() {
+ selectedItem?.style.setProperty('background-color', '')
+ selectedItem?.style.setProperty('color', '')
+ selectedItem = displayedItems[selectedIndex]
+ selectedItem?.style.setProperty(
+ 'background-color',
+ '#ccc',
+ 'important'
+ )
+ selectedItem?.style.setProperty('color', '#000', 'important')
+ }
+
+ const positionList = () => {
+ const rect = this.root.getBoundingClientRect()
+
+ // If the top is off-screen then shift the element with scaling applied
+ if (rect.top < 0) {
+ const scale =
+ 1 -
+ this.root.getBoundingClientRect().height / this.root.clientHeight
+
+ const shift = (this.root.clientHeight * scale) / 2
+
+ this.root.style.top = -shift + 'px'
}
-
- const positionList = () => {
- const rect = ctx.root.getBoundingClientRect()
-
- // If the top is off-screen then shift the element with scaling applied
- if (rect.top < 0) {
- const scale =
- 1 -
- ctx.root.getBoundingClientRect().height / ctx.root.clientHeight
-
- const shift = (ctx.root.clientHeight * scale) / 2
-
- ctx.root.style.top = -shift + 'px'
- }
- }
-
- // Arrow up/down to select items
- filter.addEventListener('keydown', (event) => {
- switch (event.key) {
- case 'ArrowUp':
- event.preventDefault()
- if (selectedIndex === 0) {
- selectedIndex = itemCount - 1
- } else {
- selectedIndex--
- }
- updateSelected()
- break
- case 'ArrowRight':
- event.preventDefault()
+ }
+
+ // Arrow up/down to select items
+ filter.addEventListener('keydown', (event) => {
+ switch (event.key) {
+ case 'ArrowUp':
+ event.preventDefault()
+ if (selectedIndex === 0) {
selectedIndex = itemCount - 1
- updateSelected()
- break
- case 'ArrowDown':
- event.preventDefault()
- if (selectedIndex === itemCount - 1) {
- selectedIndex = 0
- } else {
- selectedIndex++
- }
- updateSelected()
- break
- case 'ArrowLeft':
- event.preventDefault()
+ } else {
+ selectedIndex--
+ }
+ updateSelected()
+ break
+ case 'ArrowRight':
+ event.preventDefault()
+ selectedIndex = itemCount - 1
+ updateSelected()
+ break
+ case 'ArrowDown':
+ event.preventDefault()
+ if (selectedIndex === itemCount - 1) {
selectedIndex = 0
- updateSelected()
- break
- case 'Enter':
- selectedItem?.click()
- break
- case 'Escape':
- ctx.close()
- break
- }
- })
+ } else {
+ selectedIndex++
+ }
+ updateSelected()
+ break
+ case 'ArrowLeft':
+ event.preventDefault()
+ selectedIndex = 0
+ updateSelected()
+ break
+ case 'Enter':
+ selectedItem?.click()
+ break
+ case 'Escape':
+ this.close()
+ break
+ }
+ })
- filter.addEventListener('input', () => {
- // Hide all items that don't match our filter
- const term = filter.value.toLocaleLowerCase()
- // When filtering, recompute which items are visible for arrow up/down and maintain selection.
- displayedItems = items.filter((item) => {
- const isVisible =
- !term || item.textContent?.toLocaleLowerCase().includes(term)
- item.style.display = isVisible ? 'block' : 'none'
- return isVisible
- })
-
- selectedIndex = 0
- if (displayedItems.includes(selectedItem)) {
- selectedIndex = displayedItems.findIndex(
- (d) => d === selectedItem
- )
- }
- itemCount = displayedItems.length
+ filter.addEventListener('input', () => {
+ // Hide all items that don't match our filter
+ const term = filter.value.toLocaleLowerCase()
+ // When filtering, recompute which items are visible for arrow up/down and maintain selection.
+ displayedItems = items.filter((item) => {
+ const isVisible =
+ !term || item.textContent?.toLocaleLowerCase().includes(term)
+ item.style.display = isVisible ? 'block' : 'none'
+ return isVisible
+ })
- updateSelected()
+ selectedIndex = 0
+ if (displayedItems.includes(selectedItem)) {
+ selectedIndex = displayedItems.findIndex((d) => d === selectedItem)
+ }
+ itemCount = displayedItems.length
- // If we have an event then we can try and position the list under the source
- if (options.event) {
- let top = options.event.clientY - 10
+ updateSelected()
- const bodyRect = document.body.getBoundingClientRect()
+ // If we have an event then we can try and position the list under the source
+ if (options.event) {
+ let top = options.event.clientY - 10
- const rootRect = ctx.root.getBoundingClientRect()
- if (
- bodyRect.height &&
- top > bodyRect.height - rootRect.height - 10
- ) {
- top = Math.max(0, bodyRect.height - rootRect.height - 10)
- }
+ const bodyRect = document.body.getBoundingClientRect()
- ctx.root.style.top = top + 'px'
- positionList()
+ const rootRect = this.root.getBoundingClientRect()
+ if (
+ bodyRect.height &&
+ top > bodyRect.height - rootRect.height - 10
+ ) {
+ top = Math.max(0, bodyRect.height - rootRect.height - 10)
}
- })
-
- requestAnimationFrame(() => {
- // Focus the filter box when opening
- filter.focus()
+ this.root.style.top = top + 'px'
positionList()
- })
+ }
})
- }
- return ctx
+ requestAnimationFrame(() => {
+ // Focus the filter box when opening
+ filter.focus()
+
+ positionList()
+ })
+ })
}
+ }
+}
- LiteGraph.ContextMenu.prototype = ctxMenu.prototype
+const ext = {
+ name: 'Comfy.ContextMenuFilter',
+ init() {
+ LiteGraph.ContextMenu = FilteredContextMenu
}
}
diff --git a/src/extensions/core/editAttention.ts b/src/extensions/core/editAttention.ts
index 119b2775874..07ca38d649d 100644
--- a/src/extensions/core/editAttention.ts
+++ b/src/extensions/core/editAttention.ts
@@ -78,11 +78,12 @@ app.registerExtension({
}
function editAttention(event: KeyboardEvent) {
- // @ts-expect-error Runtime narrowing not impl.
- const inputField: HTMLTextAreaElement = event.composedPath()[0]
- const delta = parseFloat(editAttentionDelta.value)
+ const composedPath = event.composedPath()
+ const target = composedPath[0]
+ if (!(target instanceof HTMLTextAreaElement)) return
- if (inputField.tagName !== 'TEXTAREA') return
+ const inputField = target
+ const delta = parseFloat(editAttentionDelta.value)
if (!(event.key === 'ArrowUp' || event.key === 'ArrowDown')) return
if (!event.ctrlKey && !event.metaKey) return
diff --git a/src/extensions/core/groupNode.ts b/src/extensions/core/groupNode.ts
index a7af7361a30..61b9c34ba90 100644
--- a/src/extensions/core/groupNode.ts
+++ b/src/extensions/core/groupNode.ts
@@ -1,12 +1,20 @@
import { PREFIX, SEPARATOR } from '@/constants/groupNodeConstants'
import { t } from '@/i18n'
import type { GroupNodeWorkflowData } from '@/lib/litegraph/src/LGraph'
-import type { SerialisedLLinkArray } from '@/lib/litegraph/src/LLink'
+import type {
+ GroupNodeInputConfig,
+ GroupNodeInputsSpec,
+ GroupNodeInternalLink,
+ GroupNodeOutputType,
+ PartialLinkInfo
+} from './groupNodeTypes'
+import { LLink } from '@/lib/litegraph/src/LLink'
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces'
import {
type ExecutableLGraphNode,
type ExecutionId,
+ type ISerialisedNode,
LGraphNode,
type LGraphNodeConstructor,
LiteGraph,
@@ -32,7 +40,7 @@ import { app } from '../../scripts/app'
import { ManageGroupDialog } from './groupNodeManage'
import { mergeIfValid } from './widgetInputs'
-type GroupNodeLink = SerialisedLLinkArray
+type GroupNodeLink = GroupNodeInternalLink
type LinksFromMap = Record>
type LinksToMap = Record>
type ExternalFromMap = Record>
@@ -54,9 +62,8 @@ interface GroupNodeOutput {
interface GroupNodeData extends Omit<
GroupNodeWorkflowData['nodes'][number],
- 'inputs' | 'outputs'
+ 'inputs' | 'outputs' | 'widgets_values'
> {
- title?: string
widgets_values?: unknown[]
inputs?: GroupNodeInput[]
outputs?: GroupNodeOutput[]
@@ -241,7 +248,13 @@ export class GroupNodeConfig {
>
nodeInputs: Record>
outputVisibility: boolean[]
- nodeDef: (ComfyNodeDef & { [GROUP]: GroupNodeConfig }) | undefined
+ nodeDef:
+ | (Omit & {
+ input: GroupNodeInputsSpec
+ output: GroupNodeOutputType[]
+ [GROUP]: GroupNodeConfig
+ })
+ | undefined
inputs!: unknown[]
linksFrom!: LinksFromMap
linksTo!: LinksToMap
@@ -297,8 +310,11 @@ export class GroupNodeConfig {
}
this.#convertedToProcess = []
if (!this.nodeDef) return
- await app.registerNodeDef(`${PREFIX}${SEPARATOR}` + this.name, this.nodeDef)
- useNodeDefStore().addNodeDef(this.nodeDef)
+ const finalizedDef = this.nodeDef as ComfyNodeDef & {
+ [GROUP]: GroupNodeConfig
+ }
+ await app.registerNodeDef(`${PREFIX}${SEPARATOR}` + this.name, finalizedDef)
+ useNodeDefStore().addNodeDef(finalizedDef)
}
getLinks() {
@@ -520,9 +536,13 @@ export class GroupNodeConfig {
node: GroupNodeData,
inputName: string,
seenInputs: Record,
- config: unknown[],
+ inputConfig: unknown[],
extra?: Record
- ) {
+ ): {
+ name: string
+ config: GroupNodeInputConfig
+ customConfig: { name?: string; visible?: boolean } | undefined
+ } {
const nodeConfig = this.nodeData.config?.[node.index ?? -1] as
| NodeConfigEntry
| undefined
@@ -543,28 +563,34 @@ export class GroupNodeConfig {
}
seenInputs[key] = (seenInputs[key] ?? 1) + 1
+ const typeName = String(inputConfig[0])
+ let options =
+ typeof inputConfig[1] === 'object' && inputConfig[1] !== null
+ ? (inputConfig[1] as Record)
+ : undefined
+
if (inputName === 'seed' || inputName === 'noise_seed') {
if (!extra) extra = {}
extra.control_after_generate = `${prefix}control_after_generate`
}
- if (config[0] === 'IMAGEUPLOAD') {
+ if (typeName === 'IMAGEUPLOAD') {
if (!extra) extra = {}
const nodeIndex = node.index ?? -1
- const configOptions =
- typeof config[1] === 'object' && config[1] !== null ? config[1] : {}
const widgetKey =
- 'widget' in configOptions && typeof configOptions.widget === 'string'
- ? configOptions.widget
+ options && 'widget' in options && typeof options.widget === 'string'
+ ? options.widget
: 'image'
extra.widget = this.oldToNewWidgetMap[nodeIndex]?.[widgetKey] ?? 'image'
}
if (extra) {
- const configObj =
- typeof config[1] === 'object' && config[1] ? config[1] : {}
- config = [config[0], { ...configObj, ...extra }]
+ options = { ...(options ?? {}), ...extra }
}
+ const config: GroupNodeInputConfig = options
+ ? [typeName, options]
+ : [typeName]
+
return { name, config, customConfig }
}
@@ -608,7 +634,6 @@ export class GroupNodeConfig {
inputs[inputName] as unknown[]
)
if (this.nodeDef?.input?.required) {
- // @ts-expect-error legacy dynamic input assignment
this.nodeDef.input.required[name] = config
}
widgetMap[inputName] = name
@@ -641,14 +666,18 @@ export class GroupNodeConfig {
unknown,
Record
]
- const output = { widget: primitiveConfig }
+ // Intentional casts: widget config types are dynamic at runtime and the
+ // compiler cannot infer the union shapes expected by mergeIfValid.
+ const output = { widget: primitiveConfig } as unknown as Parameters<
+ typeof mergeIfValid
+ >[0]
+ // Intentional casts for targetWidget and primitiveConfig — see mergeIfValid.
const config = mergeIfValid(
- // @ts-expect-error slot type mismatch - legacy API
output,
- targetWidget,
+ targetWidget as Parameters[1],
false,
undefined,
- primitiveConfig
+ primitiveConfig as Parameters[4]
)
const inputConfig = inputs[inputName]?.[1]
primitiveConfig[1] =
@@ -713,7 +742,6 @@ export class GroupNodeConfig {
if (customConfig?.visible === false) continue
if (this.nodeDef?.input?.required) {
- // @ts-expect-error legacy dynamic input assignment
this.nodeDef.input.required[name] = config
}
inputMap[i] = this.inputCount++
@@ -757,7 +785,6 @@ export class GroupNodeConfig {
)
if (this.nodeDef?.input?.required) {
- // @ts-expect-error legacy dynamic input assignment
this.nodeDef.input.required[name] = config
}
this.newToOldWidgetMap[name] = { node, inputName }
@@ -851,8 +878,7 @@ export class GroupNodeConfig {
node,
slot: outputId
}
- // @ts-expect-error legacy dynamic output type assignment
- this.nodeDef.output.push(defOutput[outputId])
+ this.nodeDef.output.push(defOutput[outputId] as GroupNodeOutputType)
this.nodeDef.output_is_list?.push(
def.output_is_list?.[outputId] ?? false
)
@@ -951,8 +977,13 @@ export class GroupNodeHandler {
for (const w of innerNode.widgets ?? []) {
if (w.type === 'converted-widget') {
- // @ts-expect-error legacy widget property for converted widgets
- w.serializeValue = w.origSerializeValue
+ type SerializeValueFn = (node: LGraphNode, index: number) => unknown
+ const convertedWidget = w as typeof w & {
+ origSerializeValue?: SerializeValueFn
+ }
+ if (convertedWidget.origSerializeValue) {
+ w.serializeValue = convertedWidget.origSerializeValue
+ }
}
}
@@ -978,20 +1009,18 @@ export class GroupNodeHandler {
return inputNode
}
- // @ts-expect-error returns partial link object, not full LLink
- innerNode.getInputLink = (slot: number) => {
+ innerNode.getInputLink = (slot: number): PartialLinkInfo | null => {
const nodeIdx = innerNode.index ?? 0
const externalSlot = this.groupData.oldToNewInputMap[nodeIdx]?.[slot]
if (externalSlot != null) {
- // The inner node is connected via the group node inputs
const linkId = this.node.inputs[externalSlot].link
if (linkId == null) return null
const existingLink = app.rootGraph.links[linkId]
if (!existingLink) return null
- // Use the outer link, but update the target to the inner node
return {
- ...existingLink,
+ origin_id: existingLink.origin_id,
+ origin_slot: existingLink.origin_slot,
target_id: innerNode.id,
target_slot: +slot
}
@@ -1000,11 +1029,11 @@ export class GroupNodeHandler {
const innerLink = this.groupData.linksTo[nodeIdx]?.[slot]
if (!innerLink) return null
const linkSrcIdx = innerLink[0]
- if (linkSrcIdx == null) return null
- // Use the inner link, but update the origin node to be inner node id
+ const linkSrcSlot = innerLink[1]
+ if (linkSrcIdx == null || linkSrcSlot == null) return null
return {
origin_id: innerNodes[Number(linkSrcIdx)].id,
- origin_slot: innerLink[1],
+ origin_slot: linkSrcSlot,
target_id: innerNode.id,
target_slot: +slot
}
@@ -1012,15 +1041,13 @@ export class GroupNodeHandler {
}
}
- this.node.updateLink = (link) => {
- // Replace the group node reference with the internal node
- // @ts-expect-error Can this be removed? Or replaced with: LLink.create(link.asSerialisable())
- link = { ...link }
+ this.node.updateLink = (inputLink) => {
+ const link = LLink.create(inputLink.asSerialisable())
const output = this.groupData.newToOldOutputMap[link.origin_slot]
if (!output || !this.innerNodes) return null
const nodeIdx = output.node.index ?? 0
let innerNode: LGraphNode | null = this.innerNodes[nodeIdx]
- let l
+ let l = innerNode?.getInputLink(0)
while (innerNode?.type === 'Reroute') {
l = innerNode.getInputLink(0)
innerNode = innerNode.getInputNode(0)
@@ -1031,7 +1058,7 @@ export class GroupNodeHandler {
}
if (
- l &&
+ l instanceof LLink &&
GroupNodeHandler.isGroupNode(innerNode) &&
innerNode.updateLink
) {
@@ -1063,8 +1090,7 @@ export class GroupNodeHandler {
if (!n.type) return null
const innerNode = LiteGraph.createNode(n.type)
if (!innerNode) return null
- // @ts-expect-error legacy node data format used for configure
- innerNode.configure(n)
+ innerNode.configure(n as ISerialisedNode)
innerNode.id = `${this.node.id}:${i}`
innerNode.graph = this.node.graph
return innerNode
@@ -1077,18 +1103,16 @@ export class GroupNodeHandler {
const subgraphInstanceIdPath = [...subgraphNodePath, this.node.id]
- // Assertion: Deprecated, does not matter.
- const subgraphNode = (this.node.graph?.getNodeById(
- subgraphNodePath.at(-1)
- ) ?? undefined) as SubgraphNode | undefined
+ // Get the parent subgraph node if we're inside a subgraph
+ const parentNode = this.node.graph?.getNodeById(subgraphNodePath.at(-1))
+ const subgraphNode = parentNode?.isSubgraphNode() ? parentNode : undefined
for (const node of this.innerNodes ?? []) {
node.graph ??= this.node.graph
- // Create minimal DTOs rather than cloning the node
const currentId = String(node.id)
- // @ts-expect-error temporary id reassignment for DTO creation
- node.id = currentId.split(':').at(-1)
+ const shortId = currentId.split(':').at(-1) ?? currentId
+ node.id = shortId
const aVeryRealNode = new ExecutableGroupNodeChildDTO(
node,
subgraphInstanceIdPath,
@@ -1103,7 +1127,6 @@ export class GroupNodeHandler {
return nodes
}
- // @ts-expect-error recreate returns null if creation fails
this.node.recreate = async () => {
const id = this.node.id
const sz = this.node.size
@@ -1139,11 +1162,9 @@ export class GroupNodeHandler {
this.node as LGraphNode & { convertToNodes: () => LGraphNode[] }
).convertToNodes = () => {
const addInnerNodes = () => {
- // Clone the node data so we dont mutate it for other nodes
const c = { ...this.groupData.nodeData }
c.nodes = [...c.nodes]
- // @ts-expect-error getInnerNodes called without args in legacy conversion code
- const innerNodes = this.node.getInnerNodes?.()
+ const innerNodes = this.innerNodes
const ids: (string | number)[] = []
for (let i = 0; i < c.nodes.length; i++) {
let id: string | number | undefined = innerNodes?.[i]?.id
@@ -1153,7 +1174,6 @@ export class GroupNodeHandler {
} else {
ids.push(id)
}
- // @ts-expect-error adding id to node copy for serialization
c.nodes[i] = { ...c.nodes[i], id }
}
deserialiseAndCreate(JSON.stringify(c), app.canvas)
@@ -1182,7 +1202,6 @@ export class GroupNodeHandler {
if (!newNode.widgets || !innerNode) continue
- // @ts-expect-error index property access on ExecutableLGraphNode
const map = this.groupData.oldToNewWidgetMap[innerNode.index ?? 0]
if (map) {
const widgets = Object.keys(map)
@@ -1305,14 +1324,13 @@ export class GroupNodeHandler {
null,
{
content: 'Convert to nodes',
- // @ts-expect-error async callback not expected by legacy menu API
callback: async () => {
const convertFn = (
handlerNode as LGraphNode & {
convertToNodes?: () => LGraphNode[]
}
).convertToNodes
- return convertFn?.()
+ convertFn?.()
}
},
{
@@ -1423,13 +1441,9 @@ export class GroupNodeHandler {
type EventDetail = { display_node?: string; node?: string } | string
const handleEvent = (
- type: string,
+ type: 'executing' | 'executed',
getId: (detail: EventDetail) => string | undefined,
- getEvent: (
- detail: EventDetail,
- id: string,
- node: LGraphNode
- ) => EventDetail
+ getEvent: (detail: EventDetail, id: string, node: LGraphNode) => unknown
) => {
const handler = ({ detail }: CustomEvent) => {
const id = getId(detail)
@@ -1443,16 +1457,14 @@ export class GroupNodeHandler {
;(
this.node as LGraphNode & { runningInternalNodeId?: number }
).runningInternalNodeId = innerNodeIndex
+ // Cast needed: dispatching synthetic events for inner nodes with transformed payloads
api.dispatchCustomEvent(
- type as 'executing',
+ type,
getEvent(detail, `${this.node.id}`, this.node) as string
)
}
}
- api.addEventListener(
- type as 'executing' | 'executed',
- handler as EventListener
- )
+ api.addEventListener(type, handler as EventListener)
return handler
}
@@ -1519,7 +1531,6 @@ export class GroupNodeHandler {
if (!innerNode) continue
if (innerNode.type === 'PrimitiveNode') {
- // @ts-expect-error primitiveValue is a custom property on PrimitiveNode
innerNode.primitiveValue = newValue
const primitiveLinked = this.groupData.primitiveToWidget[nodeIdx]
for (const linked of primitiveLinked ?? []) {
@@ -1748,12 +1759,7 @@ export class GroupNodeHandler {
this.groupData.oldToNewInputMap[Number(targetId)]?.[Number(targetSlot)]
if (mappedSlot == null) continue
if (typeof originSlot === 'number' || typeof originSlot === 'string') {
- originNode.connect(
- originSlot,
- // @ts-expect-error Valid - uses deprecated interface (node ID instead of node reference)
- this.node.id,
- mappedSlot
- )
+ originNode.connect(originSlot, this.node, mappedSlot)
}
}
}
@@ -1761,9 +1767,10 @@ export class GroupNodeHandler {
static getGroupData(
node: LGraphNodeConstructor
): GroupNodeConfig | undefined
+ static getGroupData(node: typeof LGraphNode): GroupNodeConfig | undefined
static getGroupData(node: LGraphNode): GroupNodeConfig | undefined
static getGroupData(
- node: LGraphNode | LGraphNodeConstructor
+ node: LGraphNode | LGraphNodeConstructor | typeof LGraphNode
): GroupNodeConfig | undefined {
// Check if this is a constructor (function) or an instance
if (typeof node === 'function') {
@@ -1783,13 +1790,13 @@ export class GroupNodeHandler {
}
static getHandler(node: LGraphNode): GroupNodeHandler | undefined {
- // @ts-expect-error GROUP symbol indexing on LGraphNode
- let handler = node[GROUP] as GroupNodeHandler | undefined
- // Handler may not be set yet if nodeCreated async hook hasn't run
- // Create it synchronously if needed
+ type GroupNodeWithHandler = LGraphNode & {
+ [GROUP]?: GroupNodeHandler
+ }
+ let handler = (node as GroupNodeWithHandler)[GROUP]
if (!handler && GroupNodeHandler.isGroupNode(node)) {
handler = new GroupNodeHandler(node)
- ;(node as LGraphNode & { [GROUP]: GroupNodeHandler })[GROUP] = handler
+ ;(node as GroupNodeWithHandler)[GROUP] = handler
}
return handler
}
@@ -1948,8 +1955,9 @@ const ext: ComfyExtension = {
items.push({
content: `Convert to Group Node (Deprecated)`,
disabled: !convertEnabled,
- // @ts-expect-error async callback - legacy menu API doesn't expect Promise
- callback: async () => convertSelectedNodesToGroupNode()
+ callback: async () => {
+ await convertSelectedNodesToGroupNode()
+ }
})
const groups = canvas.graph?.extra?.groupNodes
@@ -1975,8 +1983,9 @@ const ext: ComfyExtension = {
{
content: `Convert to Group Node (Deprecated)`,
disabled: !convertEnabled,
- // @ts-expect-error async callback - legacy menu API doesn't expect Promise
- callback: async () => convertSelectedNodesToGroupNode()
+ callback: async () => {
+ await convertSelectedNodesToGroupNode()
+ }
}
]
},
diff --git a/src/extensions/core/groupNodeManage.ts b/src/extensions/core/groupNodeManage.ts
index 0e91af317ff..ca73f53572c 100644
--- a/src/extensions/core/groupNodeManage.ts
+++ b/src/extensions/core/groupNodeManage.ts
@@ -1,9 +1,11 @@
+import { merge } from 'es-toolkit'
+
import { PREFIX, SEPARATOR } from '@/constants/groupNodeConstants'
-import {
- type LGraphNode,
- type LGraphNodeConstructor,
- LiteGraph
+import type {
+ GroupNodeWorkflowData,
+ LGraphNode
} from '@/lib/litegraph/src/litegraph'
+import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { type ComfyApp, app } from '../../scripts/app'
@@ -13,70 +15,69 @@ import { DraggableList } from '../../scripts/ui/draggableList'
import { GroupNodeConfig, GroupNodeHandler } from './groupNode'
import './groupNodeManage.css'
-const ORDER: symbol = Symbol()
-
-// @ts-expect-error fixme ts strict error
-function merge(target, source) {
- if (typeof target === 'object' && typeof source === 'object') {
- for (const key in source) {
- const sv = source[key]
- if (typeof sv === 'object') {
- let tv = target[key]
- if (!tv) tv = target[key] = {}
- merge(tv, source[key])
- } else {
- target[key] = sv
- }
- }
- }
+/** Group node types have nodeData of type GroupNodeWorkflowData */
+interface GroupNodeType {
+ nodeData: GroupNodeWorkflowData
+}
- return target
+type GroupNodeConstructor = typeof LGraphNode & GroupNodeType
+
+function hasNodeData(
+ nodeType: typeof LGraphNode | undefined
+): nodeType is GroupNodeConstructor {
+ return nodeType != null && 'nodeData' in nodeType
}
+const ORDER: unique symbol = Symbol('ORDER')
+
+interface NodeModification {
+ name?: string
+ visible?: boolean
+}
+
+interface OrderModification {
+ order: number
+}
+
+type NodeModifications = Record & {
+ [ORDER]?: OrderModification
+}
+
+type DragEndEvent = CustomEvent<{
+ element: Element
+ oldPosition: number
+ newPosition: number
+}>
+
export class ManageGroupDialog extends ComfyDialog {
- // @ts-expect-error fixme ts strict error
- tabs: Record<
+ tabs!: Record<
'Inputs' | 'Outputs' | 'Widgets',
{ tab: HTMLAnchorElement; page: HTMLElement }
>
selectedNodeIndex: number | null | undefined
selectedTab: keyof ManageGroupDialog['tabs'] = 'Inputs'
selectedGroup: string | undefined
- modifications: Record<
- string,
- Record<
- string,
- Record<
- string,
- { name?: string | undefined; visible?: boolean | undefined }
- >
- >
- > = {}
- // @ts-expect-error fixme ts strict error
- nodeItems: any[]
+ modifications: Record }> =
+ {}
+ nodeItems: HTMLLIElement[] = []
app: ComfyApp
- // @ts-expect-error fixme ts strict error
- groupNodeType: LGraphNodeConstructor
- groupNodeDef: any
- groupData: any
-
- // @ts-expect-error fixme ts strict error
- innerNodesList: HTMLUListElement
- // @ts-expect-error fixme ts strict error
- widgetsPage: HTMLElement
- // @ts-expect-error fixme ts strict error
- inputsPage: HTMLElement
- // @ts-expect-error fixme ts strict error
- outputsPage: HTMLElement
- draggable: any
-
- get selectedNodeInnerIndex() {
- // @ts-expect-error fixme ts strict error
- return +this.nodeItems[this.selectedNodeIndex].dataset.nodeindex
+ groupNodeType!: GroupNodeConstructor
+ groupNodeDef: unknown
+ groupData: ReturnType | null = null
+
+ innerNodesList!: HTMLUListElement
+ widgetsPage!: HTMLElement
+ inputsPage!: HTMLElement
+ outputsPage!: HTMLElement
+ draggable: DraggableList | null = null
+
+ get selectedNodeInnerIndex(): number {
+ if (this.selectedNodeIndex == null) return 0
+ const item = this.nodeItems[this.selectedNodeIndex]
+ return +(item?.dataset?.nodeindex ?? 0)
}
- // @ts-expect-error fixme ts strict error
- constructor(app) {
+ constructor(app: ComfyApp) {
super()
this.app = app
this.element = $el('dialog.comfy-group-manage', {
@@ -84,19 +85,15 @@ export class ManageGroupDialog extends ComfyDialog {
}) as HTMLDialogElement
}
- // @ts-expect-error fixme ts strict error
- changeTab(tab) {
+ changeTab(tab: keyof ManageGroupDialog['tabs']) {
this.tabs[this.selectedTab].tab.classList.remove('active')
this.tabs[this.selectedTab].page.classList.remove('active')
- // @ts-expect-error fixme ts strict error
this.tabs[tab].tab.classList.add('active')
- // @ts-expect-error fixme ts strict error
this.tabs[tab].page.classList.add('active')
this.selectedTab = tab
}
- // @ts-expect-error fixme ts strict error
- changeNode(index, force?) {
+ changeNode(index: number, force?: boolean) {
if (!force && this.selectedNodeIndex === index) return
if (this.selectedNodeIndex != null) {
@@ -119,26 +116,29 @@ export class ManageGroupDialog extends ComfyDialog {
}
getGroupData() {
- this.groupNodeType = LiteGraph.registered_node_types[
- `${PREFIX}${SEPARATOR}` + this.selectedGroup
- ] as unknown as LGraphNodeConstructor
+ const nodeType =
+ LiteGraph.registered_node_types[
+ `${PREFIX}${SEPARATOR}` + this.selectedGroup
+ ]
+ if (!hasNodeData(nodeType)) {
+ throw new Error(`Group node type not found: ${this.selectedGroup}`)
+ }
+ this.groupNodeType = nodeType
this.groupNodeDef = this.groupNodeType.nodeData
this.groupData = GroupNodeHandler.getGroupData(this.groupNodeType)
}
- // @ts-expect-error fixme ts strict error
- changeGroup(group, reset = true) {
+ changeGroup(group: string, reset = true) {
this.selectedGroup = group
this.getGroupData()
- const nodes = this.groupData.nodeData.nodes
- // @ts-expect-error fixme ts strict error
+ const nodes = this.groupData?.nodeData.nodes ?? []
this.nodeItems = nodes.map((n, i) =>
$el(
'li.draggable-item',
{
dataset: {
- nodeindex: n.index + ''
+ nodeindex: String(n.index ?? i)
},
onclick: () => {
this.changeNode(i)
@@ -159,7 +159,7 @@ export class ManageGroupDialog extends ComfyDialog {
)
]
)
- )
+ ) as HTMLLIElement[]
this.innerNodesList.replaceChildren(...this.nodeItems)
@@ -167,63 +167,76 @@ export class ManageGroupDialog extends ComfyDialog {
this.selectedNodeIndex = null
this.changeNode(0)
} else {
- const items = this.draggable.getAllItems()
- // @ts-expect-error fixme ts strict error
- let index = items.findIndex((item) => item.classList.contains('selected'))
- if (index === -1) index = this.selectedNodeIndex
+ const items = this.draggable?.getAllItems() ?? []
+ let index = items.findIndex((item: Element) =>
+ item.classList.contains('selected')
+ )
+ if (index === -1) index = this.selectedNodeIndex ?? 0
this.changeNode(index, true)
}
const ordered = [...nodes]
this.draggable?.dispose()
this.draggable = new DraggableList(this.innerNodesList, 'li')
- this.draggable.addEventListener(
- 'dragend',
- // @ts-expect-error fixme ts strict error
- ({ detail: { oldPosition, newPosition } }) => {
- if (oldPosition === newPosition) return
- ordered.splice(newPosition, 0, ordered.splice(oldPosition, 1)[0])
- for (let i = 0; i < ordered.length; i++) {
- this.storeModification({
- nodeIndex: ordered[i].index,
- section: ORDER,
- prop: 'order',
- value: i
- })
- }
+ this.draggable.addEventListener('dragend', (e: Event) => {
+ const detail = (e as DragEndEvent).detail
+ const { oldPosition, newPosition } = detail
+ if (oldPosition === newPosition) return
+ ordered.splice(newPosition, 0, ordered.splice(oldPosition, 1)[0])
+ for (let i = 0; i < ordered.length; i++) {
+ const nodeIndex = ordered[i].index
+ if (nodeIndex == null) continue
+ this.storeModification({
+ nodeIndex,
+ section: ORDER,
+ prop: 'order',
+ value: i
+ })
}
- )
+ })
}
storeModification(props: {
nodeIndex?: number
- section: symbol
+ section: string | typeof ORDER
prop: string
- value: any
+ value: unknown
}) {
const { nodeIndex, section, prop, value } = props
- // @ts-expect-error fixme ts strict error
+ if (!this.selectedGroup) return
+
const groupMod = (this.modifications[this.selectedGroup] ??= {})
const nodesMod = (groupMod.nodes ??= {})
- const nodeMod = (nodesMod[nodeIndex ?? this.selectedNodeInnerIndex] ??= {})
- const typeMod = (nodeMod[section] ??= {})
- if (typeof value === 'object') {
- const objMod = (typeMod[prop] ??= {})
- Object.assign(objMod, value)
+ const nodeKey = String(nodeIndex ?? this.selectedNodeInnerIndex)
+ const nodeMod = (nodesMod[nodeKey] ??= {} as NodeModifications)
+
+ if (section === ORDER) {
+ nodeMod[ORDER] = { order: value as number }
} else {
- typeMod[prop] = value
+ const sectionMod = (nodeMod[section] ??= {})
+ if (typeof value === 'object' && value !== null) {
+ Object.assign(sectionMod, value)
+ } else {
+ Object.assign(sectionMod, { [prop]: value })
+ }
}
}
- // @ts-expect-error fixme ts strict error
- getEditElement(section, prop, value, placeholder, checked, checkable = true) {
+ getEditElement(
+ section: string,
+ prop: string | number,
+ value: string,
+ placeholder: string,
+ checked: boolean,
+ checkable = true
+ ) {
if (value === placeholder) value = ''
- const mods =
- // @ts-expect-error fixme ts strict error
- this.modifications[this.selectedGroup]?.nodes?.[
- this.selectedNodeInnerIndex
- ]?.[section]?.[prop]
+ const mods = this.selectedGroup
+ ? this.modifications[this.selectedGroup]?.nodes?.[
+ this.selectedNodeInnerIndex
+ ]?.[section]
+ : undefined
if (mods) {
if (mods.name != null) {
value = mods.name
@@ -238,12 +251,11 @@ export class ManageGroupDialog extends ComfyDialog {
value,
placeholder,
type: 'text',
- // @ts-expect-error fixme ts strict error
- onchange: (e) => {
+ onchange: (e: Event) => {
this.storeModification({
section,
- prop,
- value: { name: e.target.value }
+ prop: String(prop),
+ value: { name: (e.target as HTMLInputElement).value }
})
}
}),
@@ -252,12 +264,11 @@ export class ManageGroupDialog extends ComfyDialog {
type: 'checkbox',
checked,
disabled: !checkable,
- // @ts-expect-error fixme ts strict error
- onchange: (e) => {
+ onchange: (e: Event) => {
this.storeModification({
section,
- prop,
- value: { visible: !!e.target.checked }
+ prop: String(prop),
+ value: { visible: !!(e.target as HTMLInputElement).checked }
})
}
})
@@ -267,17 +278,22 @@ export class ManageGroupDialog extends ComfyDialog {
buildWidgetsPage() {
const widgets =
- this.groupData.oldToNewWidgetMap[this.selectedNodeInnerIndex]
+ this.groupData?.oldToNewWidgetMap[this.selectedNodeInnerIndex]
const items = Object.keys(widgets ?? {})
- // @ts-expect-error fixme ts strict error
- const type = app.rootGraph.extra.groupNodes[this.selectedGroup]
- const config = type.config?.[this.selectedNodeInnerIndex]?.input
+ const type = this.selectedGroup
+ ? app.rootGraph.extra?.groupNodes?.[this.selectedGroup]
+ : undefined
+ const config = (
+ type?.config as
+ | Record }>
+ | undefined
+ )?.[this.selectedNodeInnerIndex]?.input
this.widgetsPage.replaceChildren(
...items.map((oldName) => {
return this.getEditElement(
'input',
oldName,
- widgets[oldName],
+ widgets?.[oldName] ?? '',
oldName,
config?.[oldName]?.visible !== false
)
@@ -287,54 +303,68 @@ export class ManageGroupDialog extends ComfyDialog {
}
buildInputsPage() {
- const inputs = this.groupData.nodeInputs[this.selectedNodeInnerIndex]
- const items = Object.keys(inputs ?? {})
- // @ts-expect-error fixme ts strict error
- const type = app.rootGraph.extra.groupNodes[this.selectedGroup]
- const config = type.config?.[this.selectedNodeInnerIndex]?.input
- this.inputsPage.replaceChildren(
- // @ts-expect-error fixme ts strict error
- ...items
- .map((oldName) => {
- let value = inputs[oldName]
- if (!value) {
- return
- }
+ const inputs = this.groupData?.nodeInputs[this.selectedNodeInnerIndex] ?? {}
+ const items = Object.keys(inputs)
+ const type = this.selectedGroup
+ ? app.rootGraph.extra?.groupNodes?.[this.selectedGroup]
+ : undefined
+ const config = (
+ type?.config as
+ | Record }>
+ | undefined
+ )?.[this.selectedNodeInnerIndex]?.input
+ const filteredElements = items
+ .map((oldName) => {
+ const value = inputs[oldName]
+ if (!value) {
+ return null
+ }
- return this.getEditElement(
- 'input',
- oldName,
- value,
- oldName,
- config?.[oldName]?.visible !== false
- )
- })
- .filter(Boolean)
- )
+ return this.getEditElement(
+ 'input',
+ oldName,
+ value as string,
+ oldName,
+ config?.[oldName]?.visible !== false
+ )
+ })
+ .filter((el): el is HTMLDivElement => el !== null)
+ this.inputsPage.replaceChildren(...filteredElements)
return !!items.length
}
buildOutputsPage() {
- const nodes = this.groupData.nodeData.nodes
- const innerNodeDef = this.groupData.getNodeDef(
- nodes[this.selectedNodeInnerIndex]
- )
- const outputs = innerNodeDef?.output ?? []
+ const nodes = this.groupData?.nodeData.nodes ?? []
+ const nodeData = nodes[this.selectedNodeInnerIndex]
+ const innerNodeDef = nodeData
+ ? this.groupData?.getNodeDef(
+ nodeData as Parameters[0]
+ )
+ : undefined
+ const outputs = (innerNodeDef?.output ?? []) as string[]
const groupOutputs =
- this.groupData.oldToNewOutputMap[this.selectedNodeInnerIndex]
-
- // @ts-expect-error fixme ts strict error
- const type = app.rootGraph.extra.groupNodes[this.selectedGroup]
- const config = type.config?.[this.selectedNodeInnerIndex]?.output
- const node = this.groupData.nodeData.nodes[this.selectedNodeInnerIndex]
- const checkable = node.type !== 'PrimitiveNode'
+ this.groupData?.oldToNewOutputMap[this.selectedNodeInnerIndex]
+
+ const workflowType = this.selectedGroup
+ ? app.rootGraph.extra?.groupNodes?.[this.selectedGroup]
+ : undefined
+ const config = (
+ workflowType?.config as
+ | Record<
+ number,
+ { output?: Record }
+ >
+ | undefined
+ )?.[this.selectedNodeInnerIndex]?.output
+ const node = nodes[this.selectedNodeInnerIndex]
+ const checkable = node?.type !== 'PrimitiveNode'
this.outputsPage.replaceChildren(
...outputs
- // @ts-expect-error fixme ts strict error
- .map((type, slot) => {
+ .map((outputType: string, slot: number) => {
const groupOutputIndex = groupOutputs?.[slot]
- const oldName = innerNodeDef.output_name?.[slot] ?? type
- let value = config?.[slot]?.name
+ const oldName = (innerNodeDef?.output_name?.[slot] ??
+ outputType) as string
+ let value = config?.[slot]?.name ?? ''
const visible = config?.[slot]?.visible || groupOutputIndex != null
if (!value || value === oldName) {
value = ''
@@ -353,8 +383,7 @@ export class ManageGroupDialog extends ComfyDialog {
return !!outputs.length
}
- // @ts-expect-error fixme ts strict error
- show(type?) {
+ override show(type?: string) {
const groupNodes = Object.keys(app.rootGraph.extra?.groupNodes ?? {}).sort(
(a, b) => a.localeCompare(b)
)
@@ -371,24 +400,28 @@ export class ManageGroupDialog extends ComfyDialog {
this.outputsPage
])
- this.tabs = [
- ['Inputs', this.inputsPage],
- ['Widgets', this.widgetsPage],
- ['Outputs', this.outputsPage]
- // @ts-expect-error fixme ts strict error
- ].reduce((p, [name, page]: [string, HTMLElement]) => {
- // @ts-expect-error fixme ts strict error
- p[name] = {
- tab: $el('a', {
- onclick: () => {
- this.changeTab(name)
- },
- textContent: name
- }),
- page
- }
- return p
- }, {}) as any
+ type TabName = keyof ManageGroupDialog['tabs']
+ this.tabs = (
+ [
+ ['Inputs', this.inputsPage],
+ ['Widgets', this.widgetsPage],
+ ['Outputs', this.outputsPage]
+ ] as [TabName, HTMLElement][]
+ ).reduce(
+ (p, [name, page]) => {
+ p[name] = {
+ tab: $el('a', {
+ onclick: () => {
+ this.changeTab(name)
+ },
+ textContent: name
+ }) as HTMLAnchorElement,
+ page
+ }
+ return p
+ },
+ {} as ManageGroupDialog['tabs']
+ )
const outer = $el('div.comfy-group-manage-outer', [
$el('header', [
@@ -396,9 +429,8 @@ export class ManageGroupDialog extends ComfyDialog {
$el(
'select',
{
- // @ts-expect-error fixme ts strict error
- onchange: (e) => {
- this.changeGroup(e.target.value)
+ onchange: (e: Event) => {
+ this.changeGroup((e.target as HTMLSelectElement).value)
}
},
groupNodes.map((g) =>
@@ -439,8 +471,9 @@ export class ManageGroupDialog extends ComfyDialog {
`Are you sure you want to remove the node: "${this.selectedGroup}"`
)
) {
- // @ts-expect-error fixme ts strict error
- delete app.rootGraph.extra.groupNodes[this.selectedGroup]
+ if (this.selectedGroup && app.rootGraph.extra?.groupNodes) {
+ delete app.rootGraph.extra.groupNodes[this.selectedGroup]
+ }
LiteGraph.unregisterNodeType(
`${PREFIX}${SEPARATOR}` + this.selectedGroup
)
@@ -454,97 +487,105 @@ export class ManageGroupDialog extends ComfyDialog {
'button.comfy-btn',
{
onclick: async () => {
- let nodesByType
- let recreateNodes = []
- const types = {}
+ let nodesByType: Record | null = null
+ const recreateNodes: LGraphNode[] = []
+ const types: Record = {}
for (const g in this.modifications) {
- // @ts-expect-error fixme ts strict error
- const type = app.rootGraph.extra.groupNodes[g]
- let config = (type.config ??= {})
+ const groupNodeData = app.rootGraph.extra?.groupNodes?.[g]
+ if (!groupNodeData) continue
+
+ let config = (groupNodeData.config ??= {}) as Record<
+ number,
+ unknown
+ >
let nodeMods = this.modifications[g]?.nodes
if (nodeMods) {
const keys = Object.keys(nodeMods)
- // @ts-expect-error fixme ts strict error
- if (nodeMods[keys[0]][ORDER]) {
+ const firstMod = nodeMods[keys[0]]
+ if (firstMod?.[ORDER]) {
// If any node is reordered, they will all need sequencing
- const orderedNodes = []
- const orderedMods = {}
- const orderedConfig = {}
+ const orderedNodes: typeof groupNodeData.nodes = []
+ const orderedMods: Record = {}
+ const orderedConfig: Record = {}
for (const n of keys) {
- // @ts-expect-error fixme ts strict error
- const order = nodeMods[n][ORDER].order
- orderedNodes[order] = type.nodes[+n]
- // @ts-expect-error fixme ts strict error
+ const order = nodeMods[n]?.[ORDER]?.order ?? 0
+ orderedNodes[order] = groupNodeData.nodes[+n]
orderedMods[order] = nodeMods[n]
orderedNodes[order].index = order
}
// Rewrite links
- for (const l of type.links) {
- // @ts-expect-error l[0]/l[2] used as node index
- if (l[0] != null) l[0] = type.nodes[l[0]].index
- // @ts-expect-error l[0]/l[2] used as node index
- if (l[2] != null) l[2] = type.nodes[l[2]].index
+ for (const l of groupNodeData.links) {
+ const srcIdx = l[1]
+ const tgtIdx = l[3]
+ if (srcIdx != null)
+ l[1] =
+ groupNodeData.nodes[srcIdx as number]?.index ?? srcIdx
+ if (tgtIdx != null)
+ l[3] =
+ groupNodeData.nodes[tgtIdx as number]?.index ?? tgtIdx
}
// Rewrite externals
- if (type.external) {
- for (const ext of type.external) {
+ if (groupNodeData.external) {
+ for (const ext of groupNodeData.external) {
if (ext[0] != null) {
- // @ts-expect-error ext[0] used as node index
- ext[0] = type.nodes[ext[0]].index
+ ext[0] =
+ groupNodeData.nodes[ext[0] as number]?.index ??
+ ext[0]
}
}
}
// Rewrite modifications
for (const id of keys) {
- // @ts-expect-error id used as node index
- if (config[id]) {
- // @ts-expect-error fixme ts strict error
- orderedConfig[type.nodes[id].index] = config[id]
+ const nodeIdx = +id
+ if (config[nodeIdx]) {
+ const newIdx =
+ groupNodeData.nodes[nodeIdx]?.index ?? nodeIdx
+ orderedConfig[newIdx] = config[nodeIdx]
}
- // @ts-expect-error id used as config key
- delete config[id]
+ delete config[nodeIdx]
}
- type.nodes = orderedNodes
+ groupNodeData.nodes = orderedNodes
nodeMods = orderedMods
- type.config = config = orderedConfig
+ groupNodeData.config = config = orderedConfig
}
- merge(config, nodeMods)
+ merge(config, nodeMods as Record)
}
- // @ts-expect-error fixme ts strict error
- types[g] = type
+ types[g] = groupNodeData
if (!nodesByType) {
- nodesByType = app.rootGraph.nodes.reduce((p, n) => {
- // @ts-expect-error fixme ts strict error
- p[n.type] ??= []
- // @ts-expect-error fixme ts strict error
- p[n.type].push(n)
- return p
- }, {})
+ nodesByType = app.rootGraph.nodes.reduce(
+ (p, n) => {
+ const nodeType = n.type ?? ''
+ ;(p[nodeType] ??= []).push(n)
+ return p
+ },
+ {} as Record
+ )
}
- // @ts-expect-error fixme ts strict error
- const nodes = nodesByType[`${PREFIX}${SEPARATOR}` + g]
- if (nodes) recreateNodes.push(...nodes)
+ const groupTypeNodes = nodesByType[`${PREFIX}${SEPARATOR}` + g]
+ if (groupTypeNodes) recreateNodes.push(...groupTypeNodes)
}
await GroupNodeConfig.registerFromWorkflow(types, [])
for (const node of recreateNodes) {
- node.recreate()
+ ;(node as LGraphNode & { recreate?: () => void }).recreate?.()
}
this.modifications = {}
this.app.canvas.setDirty(true, true)
- this.changeGroup(this.selectedGroup, false)
+ if (this.selectedGroup) {
+ this.changeGroup(this.selectedGroup, false)
+ }
}
},
'Save'
diff --git a/src/extensions/core/groupNodeTypes.ts b/src/extensions/core/groupNodeTypes.ts
new file mode 100644
index 00000000000..badabbe63bd
--- /dev/null
+++ b/src/extensions/core/groupNodeTypes.ts
@@ -0,0 +1,66 @@
+import type { ILinkRouting } from '@/lib/litegraph/src/interfaces'
+import type { ISlotType } from '@/lib/litegraph/src/interfaces'
+import type { ISerialisedNode } from '@/lib/litegraph/src/types/serialisation'
+
+/**
+ * Group node internal link format.
+ * This differs from standard SerialisedLLinkArray - indices represent node/slot positions within the group.
+ * Format: [sourceNodeIndex, sourceSlot, targetNodeIndex, targetSlot, ...optionalData]
+ * The type (ISlotType) may be at index 5 if present.
+ */
+export type GroupNodeInternalLink = [
+ sourceNodeIndex: number | null,
+ sourceSlot: number | null,
+ targetNodeIndex: number | null,
+ targetSlot: number | null,
+ ...rest: (number | string | ISlotType | null | undefined)[]
+]
+
+/** Serialized node data within a group node workflow, with group-specific index */
+interface GroupNodeSerializedNode extends Partial {
+ /** Position of this node within the group */
+ index?: number
+}
+
+export interface GroupNodeWorkflowData {
+ external: (number | string)[][]
+ links: GroupNodeInternalLink[]
+ nodes: GroupNodeSerializedNode[]
+ config?: Record
+}
+
+/**
+ * Input config tuple type for group nodes.
+ * First element is the input type name (e.g. 'INT', 'FLOAT', 'MODEL', etc.)
+ * Second element (optional) is the input options object.
+ */
+export type GroupNodeInputConfig = [string, Record?]
+
+/**
+ * Mutable inputs specification for group nodes that are built dynamically.
+ * Uses a more permissive type than ComfyInputsSpec to allow dynamic assignment.
+ */
+export interface GroupNodeInputsSpec {
+ required: Record
+ optional?: Record
+}
+
+/**
+ * Output type for group nodes - can be a type string or an array of combo options.
+ */
+export type GroupNodeOutputType = string | (string | number)[]
+
+/**
+ * Represents a partial or synthetic link used internally by group node's
+ * `getInputLink` override when resolving connections through collapsed group nodes.
+ *
+ * Unlike a full `ILinkRouting`, this represents a computed/virtual link that may not
+ * correspond to an actual link in the graph's link registry. It's constructed on-the-fly
+ * to represent the logical connection path through group node boundaries.
+ *
+ * This type aliases `ILinkRouting` (rather than narrowing it) because the consuming code
+ * expects the same shape for both real and synthetic links. The distinction is purely
+ * semantic: callers should be aware that these links are transient and may not have
+ * valid `link_id` references in the global link map.
+ */
+export interface PartialLinkInfo extends ILinkRouting {}
diff --git a/src/extensions/core/groupOptions.ts b/src/extensions/core/groupOptions.ts
index 7e3240bcf70..c41f42e964d 100644
--- a/src/extensions/core/groupOptions.ts
+++ b/src/extensions/core/groupOptions.ts
@@ -25,11 +25,10 @@ function addNodesToGroup(group: LGraphGroup, items: Iterable) {
const ext: ComfyExtension = {
name: 'Comfy.GroupOptions',
- getCanvasMenuItems(canvas: LGraphCanvas): IContextMenuValue[] {
- const items: IContextMenuValue[] = []
+ getCanvasMenuItems(canvas: LGraphCanvas): (IContextMenuValue | null)[] {
+ const items: (IContextMenuValue | null)[] = []
- // @ts-expect-error fixme ts strict error
- const group = canvas.graph.getGroupOnPos(
+ const group = canvas.graph?.getGroupOnPos(
canvas.graph_mouse[0],
canvas.graph_mouse[1]
)
@@ -41,10 +40,8 @@ const ext: ComfyExtension = {
callback: () => {
const group = new LGraphGroup()
addNodesToGroup(group, canvas.selectedItems)
- // @ts-expect-error fixme ts strict error
- canvas.graph.add(group)
- // @ts-expect-error fixme ts strict error
- canvas.graph.change()
+ canvas.graph?.add(group)
+ canvas.graph?.change()
group.recomputeInsideNodes()
}
@@ -63,8 +60,7 @@ const ext: ComfyExtension = {
disabled: !canvas.selectedItems?.size,
callback: () => {
addNodesToGroup(group, canvas.selectedItems)
- // @ts-expect-error fixme ts strict error
- canvas.graph.change()
+ canvas.graph?.change()
}
})
@@ -73,7 +69,6 @@ const ext: ComfyExtension = {
return items
} else {
// Add a separator between the default options and the group options
- // @ts-expect-error fixme ts strict error
items.push(null)
}
@@ -94,8 +89,7 @@ const ext: ComfyExtension = {
'Comfy.GroupSelectedNodes.Padding'
)
group.resizeTo(group.children, padding)
- // @ts-expect-error fixme ts strict error
- canvas.graph.change()
+ canvas.graph?.change()
}
})
@@ -103,8 +97,7 @@ const ext: ComfyExtension = {
content: 'Select Nodes',
callback: () => {
canvas.selectNodes(nodesInGroup)
- // @ts-expect-error fixme ts strict error
- canvas.graph.change()
+ canvas.graph?.change()
canvas.canvas.focus()
}
})
diff --git a/src/extensions/core/load3d.ts b/src/extensions/core/load3d.ts
index 55358d62caa..5de1b53a882 100644
--- a/src/extensions/core/load3d.ts
+++ b/src/extensions/core/load3d.ts
@@ -225,7 +225,6 @@ useExtensionService().registerExtension({
if (!isLoad3dNode(selectedNode)) return
ComfyApp.copyToClipspace(selectedNode)
- // @ts-expect-error clipspace_return_node is an extension property added at runtime
ComfyApp.clipspace_return_node = selectedNode
const props = { node: selectedNode }
@@ -412,9 +411,8 @@ useExtensionService().registerExtension({
name: 'Comfy.Preview3D',
async beforeRegisterNodeDef(_nodeType, nodeData) {
- if ('Preview3D' === nodeData.name) {
- // @ts-expect-error InputSpec is not typed correctly
- nodeData.input.required.image = ['PREVIEW_3D']
+ if ('Preview3D' === nodeData.name && nodeData.input?.required) {
+ nodeData.input.required.image = ['PREVIEW_3D', {}]
}
},
diff --git a/src/extensions/core/nodeTemplates.ts b/src/extensions/core/nodeTemplates.ts
index 040b54526e5..9cc374a5412 100644
--- a/src/extensions/core/nodeTemplates.ts
+++ b/src/extensions/core/nodeTemplates.ts
@@ -32,11 +32,15 @@ import { GroupNodeConfig, GroupNodeHandler } from './groupNode'
const id = 'Comfy.NodeTemplates'
const file = 'comfy.templates.json'
+interface NodeTemplate {
+ name: string
+ data: string
+}
+
class ManageTemplates extends ComfyDialog {
- // @ts-expect-error fixme ts strict error
- templates: any[]
+ templates: NodeTemplate[] = []
draggedEl: HTMLElement | null
- saveVisualCue: number | null
+ saveVisualCue: ReturnType | null
emptyImg: HTMLImageElement
importInput: HTMLInputElement
@@ -67,8 +71,9 @@ class ManageTemplates extends ComfyDialog {
const btns = super.createButtons()
btns[0].textContent = 'Close'
btns[0].onclick = () => {
- // @ts-expect-error fixme ts strict error
- clearTimeout(this.saveVisualCue)
+ if (this.saveVisualCue !== null) {
+ clearTimeout(this.saveVisualCue)
+ }
this.close()
}
btns.unshift(
@@ -109,14 +114,17 @@ class ManageTemplates extends ComfyDialog {
await api.storeUserData(file, templates, { stringify: false })
} catch (error) {
console.error(error)
- // @ts-expect-error fixme ts strict error
- useToastStore().addAlert(error.message)
+ useToastStore().addAlert(
+ error instanceof Error ? error.message : String(error)
+ )
}
}
async importAll() {
- // @ts-expect-error fixme ts strict error
- for (const file of this.importInput.files) {
+ const files = this.importInput.files
+ if (!files) return
+
+ for (const file of files) {
if (file.type === 'application/json' || file.name.endsWith('.json')) {
const reader = new FileReader()
reader.onload = async () => {
@@ -134,8 +142,7 @@ class ManageTemplates extends ComfyDialog {
}
}
- // @ts-expect-error fixme ts strict error
- this.importInput.value = null
+ this.importInput.value = ''
this.close()
}
@@ -158,8 +165,7 @@ class ManageTemplates extends ComfyDialog {
'div',
{},
this.templates.flatMap((t, i) => {
- // @ts-expect-error fixme ts strict error
- let nameInput
+ let nameInput: HTMLInputElement | undefined
return [
$el(
'div',
@@ -173,55 +179,58 @@ class ManageTemplates extends ComfyDialog {
gap: '5px',
backgroundColor: 'var(--comfy-menu-bg)'
},
- // @ts-expect-error fixme ts strict error
- ondragstart: (e) => {
- this.draggedEl = e.currentTarget
- e.currentTarget.style.opacity = '0.6'
- e.currentTarget.style.border = '1px dashed yellow'
- e.dataTransfer.effectAllowed = 'move'
- e.dataTransfer.setDragImage(this.emptyImg, 0, 0)
+ ondragstart: (e: DragEvent) => {
+ const target = e.currentTarget
+ if (!(target instanceof HTMLElement)) return
+ this.draggedEl = target
+ target.style.opacity = '0.6'
+ target.style.border = '1px dashed yellow'
+ if (e.dataTransfer) {
+ e.dataTransfer.effectAllowed = 'move'
+ e.dataTransfer.setDragImage(this.emptyImg, 0, 0)
+ }
},
- // @ts-expect-error fixme ts strict error
- ondragend: (e) => {
- e.target.style.opacity = '1'
- e.currentTarget.style.border = '1px dashed transparent'
- e.currentTarget.removeAttribute('draggable')
+ ondragend: (e: DragEvent) => {
+ const target = e.currentTarget
+ if (!(target instanceof HTMLElement)) return
+ target.style.opacity = '1'
+ target.style.border = '1px dashed transparent'
+ target.removeAttribute('draggable')
// rearrange the elements
this.element
.querySelectorAll('.templateManagerRow')
- // @ts-expect-error fixme ts strict error
- .forEach((el: HTMLElement, i) => {
- // @ts-expect-error fixme ts strict error
- var prev_i = Number.parseInt(el.dataset.id)
+ .forEach((el, index) => {
+ if (!(el instanceof HTMLElement)) return
+ const prev_i = Number.parseInt(el.dataset.id ?? '0')
- if (el == this.draggedEl && prev_i != i) {
+ if (el === this.draggedEl && prev_i !== index) {
this.templates.splice(
- i,
+ index,
0,
this.templates.splice(prev_i, 1)[0]
)
}
- el.dataset.id = i.toString()
+ el.dataset.id = index.toString()
})
this.store()
},
- // @ts-expect-error fixme ts strict error
- ondragover: (e) => {
+ ondragover: (e: DragEvent) => {
e.preventDefault()
- if (e.currentTarget == this.draggedEl) return
-
- let rect = e.currentTarget.getBoundingClientRect()
- if (e.clientY > rect.top + rect.height / 2) {
- e.currentTarget.parentNode.insertBefore(
- this.draggedEl,
- e.currentTarget.nextSibling
- )
- } else {
- e.currentTarget.parentNode.insertBefore(
- this.draggedEl,
- e.currentTarget
- )
+ const target = e.currentTarget
+ if (!(target instanceof HTMLElement)) return
+ if (target === this.draggedEl) return
+
+ const rect = target.getBoundingClientRect()
+ if (this.draggedEl) {
+ if (e.clientY > rect.top + rect.height / 2) {
+ target.parentNode?.insertBefore(
+ this.draggedEl,
+ target.nextSibling
+ )
+ } else {
+ target.parentNode?.insertBefore(this.draggedEl, target)
+ }
}
}
},
@@ -233,11 +242,18 @@ class ManageTemplates extends ComfyDialog {
style: {
cursor: 'grab'
},
- // @ts-expect-error fixme ts strict error
- onmousedown: (e) => {
+ onmousedown: (e: MouseEvent) => {
// enable dragging only from the label
- if (e.target.localName == 'label')
- e.currentTarget.parentNode.draggable = 'true'
+ const target = e.target
+ const currentTarget = e.currentTarget
+ if (
+ target instanceof HTMLElement &&
+ target.localName === 'label' &&
+ currentTarget instanceof HTMLElement &&
+ currentTarget.parentNode instanceof HTMLElement
+ ) {
+ currentTarget.parentNode.draggable = true
+ }
}
},
[
@@ -248,33 +264,39 @@ class ManageTemplates extends ComfyDialog {
transitionProperty: 'background-color',
transitionDuration: '0s'
},
- // @ts-expect-error fixme ts strict error
- onchange: (e) => {
- // @ts-expect-error fixme ts strict error
- clearTimeout(this.saveVisualCue)
- var el = e.target
- var row = el.parentNode.parentNode
- this.templates[row.dataset.id].name =
- el.value.trim() || 'untitled'
+ onchange: (e: Event) => {
+ if (this.saveVisualCue !== null) {
+ clearTimeout(this.saveVisualCue)
+ }
+ const el = e.target
+ if (!(el instanceof HTMLInputElement)) return
+ const row = el.parentNode?.parentNode
+ if (!(row instanceof HTMLElement) || !row.dataset.id)
+ return
+ const idx = Number.parseInt(row.dataset.id)
+ this.templates[idx].name = el.value.trim() || 'untitled'
this.store()
el.style.backgroundColor = 'rgb(40, 95, 40)'
el.style.transitionDuration = '0s'
- // @ts-expect-error
- // In browser env the return value is number.
this.saveVisualCue = setTimeout(function () {
el.style.transitionDuration = '.7s'
el.style.backgroundColor = 'var(--comfy-input-bg)'
}, 15)
},
- // @ts-expect-error fixme ts strict error
- onkeypress: (e) => {
- var el = e.target
- // @ts-expect-error fixme ts strict error
- clearTimeout(this.saveVisualCue)
+ onkeypress: (e: KeyboardEvent) => {
+ const el = e.target
+ if (!(el instanceof HTMLInputElement)) return
+ if (this.saveVisualCue !== null) {
+ clearTimeout(this.saveVisualCue)
+ }
el.style.transitionDuration = '0s'
el.style.backgroundColor = 'var(--comfy-input-bg)'
},
- $: (el) => (nameInput = el)
+ $: (el) => {
+ if (el instanceof HTMLInputElement) {
+ nameInput = el
+ }
+ }
})
]
),
@@ -286,12 +308,11 @@ class ManageTemplates extends ComfyDialog {
fontWeight: 'normal'
},
onclick: () => {
- const json = JSON.stringify({ templates: [t] }, null, 2) // convert the data to a JSON string
+ const json = JSON.stringify({ templates: [t] }, null, 2)
const blob = new Blob([json], {
type: 'application/json'
})
- // @ts-expect-error fixme ts strict error
- const name = (nameInput.value || t.name) + '.json'
+ const name = (nameInput?.value || t.name) + '.json'
downloadBlob(name, blob)
}
}),
@@ -302,20 +323,23 @@ class ManageTemplates extends ComfyDialog {
color: 'red',
fontWeight: 'normal'
},
- // @ts-expect-error fixme ts strict error
- onclick: (e) => {
- const item = e.target.parentNode.parentNode
- item.parentNode.removeChild(item)
- this.templates.splice(item.dataset.id * 1, 1)
+ onclick: (e: MouseEvent) => {
+ const target = e.target
+ if (!(target instanceof HTMLElement)) return
+ const item = target.parentNode?.parentNode
+ if (!(item instanceof HTMLElement) || !item.dataset.id)
+ return
+ item.parentNode?.removeChild(item)
+ this.templates.splice(Number.parseInt(item.dataset.id), 1)
this.store()
// update the rows index, setTimeout ensures that the list is updated
- var that = this
- setTimeout(function () {
- that.element
+ setTimeout(() => {
+ this.element
.querySelectorAll('.templateManagerRow')
- // @ts-expect-error fixme ts strict error
- .forEach((el: HTMLElement, i) => {
- el.dataset.id = i.toString()
+ .forEach((el, index) => {
+ if (el instanceof HTMLElement) {
+ el.dataset.id = index.toString()
+ }
})
}, 0)
}
@@ -332,23 +356,24 @@ class ManageTemplates extends ComfyDialog {
const manage = new ManageTemplates()
-// @ts-expect-error fixme ts strict error
-const clipboardAction = async (cb) => {
+const clipboardAction = async (cb: () => void | Promise) => {
// We use the clipboard functions but dont want to overwrite the current user clipboard
// Restore it after we've run our callback
const old = localStorage.getItem('litegrapheditor_clipboard')
await cb()
- // @ts-expect-error fixme ts strict error
- localStorage.setItem('litegrapheditor_clipboard', old)
+ if (old !== null) {
+ localStorage.setItem('litegrapheditor_clipboard', old)
+ } else {
+ localStorage.removeItem('litegrapheditor_clipboard')
+ }
}
const ext: ComfyExtension = {
name: id,
- getCanvasMenuItems(_canvas: LGraphCanvas): IContextMenuValue[] {
- const items: IContextMenuValue[] = []
+ getCanvasMenuItems(_canvas: LGraphCanvas): (IContextMenuValue | null)[] {
+ const items: (IContextMenuValue | null)[] = []
- // @ts-expect-error fixme ts strict error
items.push(null)
items.push({
content: `Save Selected as Template`,
@@ -363,8 +388,11 @@ const ext: ComfyExtension = {
clipboardAction(() => {
app.canvas.copyToClipboard()
- let data = localStorage.getItem('litegrapheditor_clipboard')
- data = JSON.parse(data || '{}')
+ const rawData = localStorage.getItem('litegrapheditor_clipboard')
+ const data = JSON.parse(rawData || '{}') as {
+ groupNodes?: Record
+ nodes?: Array<{ type: string }>
+ }
const nodeIds = Object.keys(app.canvas.selected_nodes)
for (let i = 0; i < nodeIds.length; i++) {
const node = app.canvas.graph?.getNodeById(nodeIds[i])
@@ -374,16 +402,14 @@ const ext: ComfyExtension = {
const groupConfig = GroupNodeHandler.getGroupData(node)
if (groupConfig) {
const groupData = groupConfig.nodeData
- // @ts-expect-error
if (!data.groupNodes) {
- // @ts-expect-error
data.groupNodes = {}
}
if (nodeData == null) throw new TypeError('nodeData is not set')
- // @ts-expect-error
data.groupNodes[nodeData.name] = groupData
- // @ts-expect-error
- data.nodes[i].type = nodeData.name
+ if (data.nodes?.[i]) {
+ data.nodes[i].type = nodeData.name
+ }
}
}
@@ -397,7 +423,7 @@ const ext: ComfyExtension = {
})
// Map each template to a menu item
- const subItems = manage.templates.map((t) => {
+ const subItems: (IContextMenuValue | null)[] = manage.templates.map((t) => {
return {
content: t.name,
callback: () => {
@@ -420,7 +446,6 @@ const ext: ComfyExtension = {
}
})
- // @ts-expect-error fixme ts strict error
subItems.push(null, {
content: 'Manage',
callback: () => manage.show()
diff --git a/src/extensions/core/saveImageExtraOutput.ts b/src/extensions/core/saveImageExtraOutput.ts
index f216f31a7db..00f69df55b6 100644
--- a/src/extensions/core/saveImageExtraOutput.ts
+++ b/src/extensions/core/saveImageExtraOutput.ts
@@ -1,3 +1,4 @@
+import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { applyTextReplacements } from '@/utils/searchAndReplace'
import { app } from '../../scripts/app'
@@ -25,18 +26,15 @@ app.registerExtension({
if (saveNodeTypes.has(nodeData.name)) {
const onNodeCreated = nodeType.prototype.onNodeCreated
// When the SaveImage node is created we want to override the serialization of the output name widget to run our S&R
- nodeType.prototype.onNodeCreated = function () {
- const r = onNodeCreated
- ? // @ts-expect-error fixme ts strict error
- onNodeCreated.apply(this, arguments)
- : undefined
-
- // @ts-expect-error fixme ts strict error
- const widget = this.widgets.find((w) => w.name === 'filename_prefix')
- // @ts-expect-error fixme ts strict error
- widget.serializeValue = () => {
- // @ts-expect-error fixme ts strict error
- return applyTextReplacements(app.graph, widget.value)
+ nodeType.prototype.onNodeCreated = function (this: LGraphNode) {
+ const r = onNodeCreated?.call(this)
+
+ const widget = this.widgets?.find((w) => w.name === 'filename_prefix')
+ if (widget) {
+ widget.serializeValue = () => {
+ const value = typeof widget.value === 'string' ? widget.value : ''
+ return applyTextReplacements(app.rootGraph, value)
+ }
}
return r
@@ -44,11 +42,8 @@ app.registerExtension({
} else {
// When any other node is created add a property to alias the node
const onNodeCreated = nodeType.prototype.onNodeCreated
- nodeType.prototype.onNodeCreated = function () {
- const r = onNodeCreated
- ? // @ts-expect-error fixme ts strict error
- onNodeCreated.apply(this, arguments)
- : undefined
+ nodeType.prototype.onNodeCreated = function (this: LGraphNode) {
+ const r = onNodeCreated?.call(this)
if (!this.properties || !('Node name for S&R' in this.properties)) {
this.addProperty('Node name for S&R', this.constructor.type, 'string')
diff --git a/src/extensions/core/saveMesh.ts b/src/extensions/core/saveMesh.ts
index 9fb33ef94cd..2ca613eb422 100644
--- a/src/extensions/core/saveMesh.ts
+++ b/src/extensions/core/saveMesh.ts
@@ -21,9 +21,8 @@ useExtensionService().registerExtension({
name: 'Comfy.SaveGLB',
async beforeRegisterNodeDef(_nodeType, nodeData) {
- if ('SaveGLB' === nodeData.name) {
- // @ts-expect-error InputSpec is not typed correctly
- nodeData.input.required.image = ['PREVIEW_3D']
+ if ('SaveGLB' === nodeData.name && nodeData.input?.required) {
+ nodeData.input.required.image = ['PREVIEW_3D', {}]
}
},
diff --git a/src/extensions/core/simpleTouchSupport.ts b/src/extensions/core/simpleTouchSupport.ts
index 67215f47198..a014614837d 100644
--- a/src/extensions/core/simpleTouchSupport.ts
+++ b/src/extensions/core/simpleTouchSupport.ts
@@ -120,7 +120,6 @@ app.registerExtension({
touchZooming = true
LiteGraph.closeAllContextMenus(window)
- // @ts-expect-error
app.canvas.search_box?.close()
const newTouchDist = getMultiTouchPos(e)
diff --git a/src/extensions/core/slotDefaults.ts b/src/extensions/core/slotDefaults.ts
index f7ca293a445..7fa7266accc 100644
--- a/src/extensions/core/slotDefaults.ts
+++ b/src/extensions/core/slotDefaults.ts
@@ -55,10 +55,11 @@ app.registerExtension({
if (!(lowerType in LiteGraph.registered_slot_in_types)) {
LiteGraph.registered_slot_in_types[lowerType] = { nodes: [] }
}
- LiteGraph.registered_slot_in_types[lowerType].nodes.push(
- // @ts-expect-error ComfyNode
- nodeType.comfyClass
- )
+ if ('comfyClass' in nodeType && typeof nodeType.comfyClass === 'string') {
+ LiteGraph.registered_slot_in_types[lowerType].nodes.push(
+ nodeType.comfyClass
+ )
+ }
}
var outputs = nodeData['output'] ?? []
@@ -75,8 +76,11 @@ app.registerExtension({
if (!(type in LiteGraph.registered_slot_out_types)) {
LiteGraph.registered_slot_out_types[type] = { nodes: [] }
}
- // @ts-expect-error ComfyNode
- LiteGraph.registered_slot_out_types[type].nodes.push(nodeType.comfyClass)
+ if ('comfyClass' in nodeType && typeof nodeType.comfyClass === 'string') {
+ LiteGraph.registered_slot_out_types[type].nodes.push(
+ nodeType.comfyClass
+ )
+ }
if (!LiteGraph.slot_types_out.includes(type)) {
LiteGraph.slot_types_out.push(type)
diff --git a/src/extensions/core/uploadAudio.ts b/src/extensions/core/uploadAudio.ts
index 71c2295d106..83cd31cd405 100644
--- a/src/extensions/core/uploadAudio.ts
+++ b/src/extensions/core/uploadAudio.ts
@@ -1,3 +1,4 @@
+import type { IMediaRecorder } from 'extendable-media-recorder'
import { MediaRecorder as ExtendableMediaRecorder } from 'extendable-media-recorder'
import { useChainCallback } from '@/composables/functional/useChainCallback'
@@ -47,10 +48,9 @@ async function uploadFile(
let path = data.name
if (data.subfolder) path = data.subfolder + '/' + path
- // @ts-expect-error fixme ts strict error
- if (!audioWidget.options.values.includes(path)) {
- // @ts-expect-error fixme ts strict error
- audioWidget.options.values.push(path)
+ const values = audioWidget.options.values
+ if (Array.isArray(values) && !values.includes(path)) {
+ values.push(path)
}
if (updateNode) {
@@ -66,8 +66,9 @@ async function uploadFile(
useToastStore().addAlert(resp.status + ' - ' + resp.statusText)
}
} catch (error) {
- // @ts-expect-error fixme ts strict error
- useToastStore().addAlert(error)
+ useToastStore().addAlert(
+ error instanceof Error ? error.message : String(error)
+ )
}
}
@@ -83,13 +84,11 @@ app.registerExtension({
'PreviewAudio',
'SaveAudioMP3',
'SaveAudioOpus'
- ].includes(
- // @ts-expect-error fixme ts strict error
- nodeType.prototype.comfyClass
- )
+ ].includes(nodeType.prototype.comfyClass ?? '')
) {
- // @ts-expect-error fixme ts strict error
- nodeData.input.required.audioUI = ['AUDIO_UI', {}]
+ if (nodeData.input?.required) {
+ nodeData.input.required.audioUI = ['AUDIO_UI', {}]
+ }
}
},
getCustomWidgets() {
@@ -113,8 +112,7 @@ app.registerExtension({
// Populate the audio widget UI on node execution.
const onExecuted = node.onExecuted
node.onExecuted = function (message: any) {
- // @ts-expect-error fixme ts strict error
- onExecuted?.apply(this, arguments)
+ onExecuted?.call(this, message)
const audios = message.audio
if (!audios) return
const audio = audios[0]
@@ -145,10 +143,10 @@ app.registerExtension({
const node = getNodeByLocatorId(app.rootGraph, nodeLocatorId)
if (!node) continue
- // @ts-expect-error fixme ts strict error
- const audioUIWidget = node.widgets.find(
+ const audioUIWidget = node.widgets?.find(
(w) => w.name === 'audioUI'
- ) as unknown as DOMWidget
+ ) as DOMWidget | undefined
+ if (!audioUIWidget) continue
const audio = output.audio[0]
audioUIWidget.element.src = api.apiURL(
getResourceURL(audio.subfolder, audio.filename, audio.type)
@@ -170,14 +168,16 @@ app.registerExtension({
return {
AUDIOUPLOAD(node, inputName: string) {
// The widget that allows user to select file.
- // @ts-expect-error fixme ts strict error
- const audioWidget = node.widgets.find(
- (w) => w.name === 'audio'
- ) as IStringWidget
- // @ts-expect-error fixme ts strict error
- const audioUIWidget = node.widgets.find(
+ const audioWidget = node.widgets?.find((w) => w.name === 'audio') as
+ | IStringWidget
+ | undefined
+ const audioUIWidget = node.widgets?.find(
(w) => w.name === 'audioUI'
- ) as unknown as DOMWidget
+ ) as DOMWidget | undefined
+
+ if (!audioWidget || !audioUIWidget) {
+ throw new Error('Required audio widgets not found')
+ }
audioUIWidget.options.canvasOnly = true
const onAudioWidgetUpdate = () => {
@@ -195,8 +195,7 @@ app.registerExtension({
// Load saved audio file widget values if restoring from workflow
const onGraphConfigured = node.onGraphConfigured
node.onGraphConfigured = function () {
- // @ts-expect-error fixme ts strict error
- onGraphConfigured?.apply(this, arguments)
+ onGraphConfigured?.call(this)
if (audioWidget.value) {
onAudioWidgetUpdate()
}
@@ -258,7 +257,7 @@ app.registerExtension({
node.addDOMWidget(inputName, /* name=*/ 'audioUI', audio)
audioUIWidget.options.canvasOnly = false
- let mediaRecorder: MediaRecorder | null = null
+ let mediaRecorder: IMediaRecorder | null = null
let isRecording = false
let audioChunks: Blob[] = []
let currentStream: MediaStream | null = null
@@ -303,7 +302,7 @@ app.registerExtension({
mediaRecorder = new ExtendableMediaRecorder(currentStream, {
mimeType: 'audio/wav'
- }) as unknown as MediaRecorder
+ })
audioChunks = []
diff --git a/src/extensions/core/webcamCapture.ts b/src/extensions/core/webcamCapture.ts
index 6c02229c5f2..14e84ed86cb 100644
--- a/src/extensions/core/webcamCapture.ts
+++ b/src/extensions/core/webcamCapture.ts
@@ -1,4 +1,6 @@
import { t } from '@/i18n'
+import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
+import type { INumericWidget } from '@/lib/litegraph/src/types/widgets'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { api } from '../../scripts/api'
@@ -6,15 +8,19 @@ import { app } from '../../scripts/app'
const WEBCAM_READY = Symbol()
+interface WebcamNode extends LGraphNode {
+ [WEBCAM_READY]?: Promise
+}
+
app.registerExtension({
name: 'Comfy.WebcamCapture',
getCustomWidgets() {
return {
- WEBCAM(node, inputName) {
- // @ts-expect-error fixme ts strict error
- let res
- // @ts-expect-error fixme ts strict error
- node[WEBCAM_READY] = new Promise((resolve) => (res = resolve))
+ WEBCAM(node: WebcamNode, inputName: string) {
+ let resolveVideo: (video: HTMLVideoElement) => void
+ node[WEBCAM_READY] = new Promise((resolve) => {
+ resolveVideo = resolve
+ })
const container = document.createElement('div')
container.style.background = 'rgba(0,0,0,0.25)'
@@ -31,10 +37,15 @@ app.registerExtension({
})
container.replaceChildren(video)
- // @ts-expect-error fixme ts strict error
- setTimeout(() => res(video), 500) // Fallback as loadedmetadata doesnt fire sometimes?
- // @ts-expect-error fixme ts strict error
- video.addEventListener('loadedmetadata', () => res(video), false)
+ let resolved = false
+ const resolve = () => {
+ if (resolved) return
+ resolved = true
+ clearTimeout(fallbackTimeout)
+ resolveVideo(video)
+ }
+ const fallbackTimeout = setTimeout(resolve, 500)
+ video.addEventListener('loadedmetadata', resolve, { once: true })
video.srcObject = stream
video.play()
} catch (error) {
@@ -44,16 +55,16 @@ app.registerExtension({
label.style.maxHeight = '100%'
label.style.whiteSpace = 'pre-wrap'
+ const errorMessage =
+ error instanceof Error ? error.message : String(error)
if (window.isSecureContext) {
label.textContent =
'Unable to load webcam, please ensure access is granted:\n' +
- // @ts-expect-error fixme ts strict error
- error.message
+ errorMessage
} else {
label.textContent =
'Unable to load webcam. A secure context is required, if you are not accessing ComfyUI on localhost (127.0.0.1) you will have to enable TLS (https)\n\n' +
- // @ts-expect-error fixme ts strict error
- error.message
+ errorMessage
}
container.replaceChildren(label)
@@ -66,32 +77,31 @@ app.registerExtension({
}
}
},
- nodeCreated(node) {
+ nodeCreated(node: WebcamNode) {
if ((node.type, node.constructor.comfyClass !== 'WebcamCapture')) return
- // @ts-expect-error fixme ts strict error
- let video
- // @ts-expect-error fixme ts strict error
- const camera = node.widgets.find((w) => w.name === 'image')
- // @ts-expect-error fixme ts strict error
- const w = node.widgets.find((w) => w.name === 'width')
- // @ts-expect-error fixme ts strict error
- const h = node.widgets.find((w) => w.name === 'height')
- // @ts-expect-error fixme ts strict error
- const captureOnQueue = node.widgets.find(
+ let video: HTMLVideoElement | undefined
+ const camera = node.widgets?.find((w) => w.name === 'image')
+ const widthWidget = node.widgets?.find((w) => w.name === 'width') as
+ | INumericWidget
+ | undefined
+ const heightWidget = node.widgets?.find((w) => w.name === 'height') as
+ | INumericWidget
+ | undefined
+ const captureOnQueue = node.widgets?.find(
(w) => w.name === 'capture_on_queue'
)
const canvas = document.createElement('canvas')
const capture = () => {
- // @ts-expect-error widget value type narrow down
- canvas.width = w.value
- // @ts-expect-error widget value type narrow down
- canvas.height = h.value
+ if (!widthWidget || !heightWidget || !video) return
+ const width = widthWidget.value ?? 640
+ const height = heightWidget.value ?? 480
+ canvas.width = width
+ canvas.height = height
const ctx = canvas.getContext('2d')
- // @ts-expect-error widget value type narrow down
- ctx.drawImage(video, 0, 0, w.value, h.value)
+ ctx?.drawImage(video, 0, 0, width, height)
const data = canvas.toDataURL('image/png')
const img = new Image()
@@ -112,48 +122,47 @@ app.registerExtension({
btn.disabled = true
btn.serializeValue = () => undefined
- // @ts-expect-error fixme ts strict error
- camera.serializeValue = async () => {
- // @ts-expect-error fixme ts strict error
- if (captureOnQueue.value) {
- capture()
- } else if (!node.imgs?.length) {
- const err = `No webcam image captured`
- useToastStore().addAlert(err)
- throw new Error(err)
- }
+ if (camera) {
+ camera.serializeValue = async () => {
+ if (captureOnQueue?.value) {
+ capture()
+ } else if (!node.imgs?.length) {
+ const err = `No webcam image captured`
+ useToastStore().addAlert(err)
+ throw new Error(err)
+ }
- // Upload image to temp storage
- // @ts-expect-error fixme ts strict error
- const blob = await new Promise((r) => canvas.toBlob(r))
- const name = `${+new Date()}.png`
- const file = new File([blob], name)
- const body = new FormData()
- body.append('image', file)
- body.append('subfolder', 'webcam')
- body.append('type', 'temp')
- const resp = await api.fetchApi('/upload/image', {
- method: 'POST',
- body
- })
- if (resp.status !== 200) {
- const err = `Error uploading camera image: ${resp.status} - ${resp.statusText}`
- useToastStore().addAlert(err)
- throw new Error(err)
+ // Upload image to temp storage
+ const blob = await new Promise((resolve, reject) => {
+ canvas.toBlob((b) => (b ? resolve(b) : reject(new Error('No blob'))))
+ })
+ const name = `${+new Date()}.png`
+ const file = new File([blob], name)
+ const body = new FormData()
+ body.append('image', file)
+ body.append('subfolder', 'webcam')
+ body.append('type', 'temp')
+ const resp = await api.fetchApi('/upload/image', {
+ method: 'POST',
+ body
+ })
+ if (resp.status !== 200) {
+ const err = `Error uploading camera image: ${resp.status} - ${resp.statusText}`
+ useToastStore().addAlert(err)
+ throw new Error(err)
+ }
+ return `webcam/${name} [temp]`
}
- return `webcam/${name} [temp]`
}
- // @ts-expect-error fixme ts strict error
- node[WEBCAM_READY].then((v) => {
+ node[WEBCAM_READY]?.then((v) => {
video = v
// If width isn't specified then use video output resolution
- // @ts-expect-error fixme ts strict error
- if (!w.value) {
- // @ts-expect-error fixme ts strict error
- w.value = video.videoWidth || 640
- // @ts-expect-error fixme ts strict error
- h.value = video.videoHeight || 480
+ if (widthWidget && !widthWidget.value) {
+ widthWidget.value = video.videoWidth || 640
+ }
+ if (heightWidget && !heightWidget.value) {
+ heightWidget.value = video.videoHeight || 480
}
btn.disabled = false
btn.label = t('g.capture')
diff --git a/src/extensions/core/widgetInputs.ts b/src/extensions/core/widgetInputs.ts
index c7f9f6eb828..637e256ceb4 100644
--- a/src/extensions/core/widgetInputs.ts
+++ b/src/extensions/core/widgetInputs.ts
@@ -9,9 +9,15 @@ import type {
ISlotType,
LLink
} from '@/lib/litegraph/src/litegraph'
+import type { IWidgetLocator } from '@/lib/litegraph/src/interfaces'
import { NodeSlot } from '@/lib/litegraph/src/node/NodeSlot'
import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events'
-import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
+import type {
+ IBaseWidget,
+ IComboWidget,
+ INumericWidget
+} from '@/lib/litegraph/src/types/widgets'
+import { isPrimitiveNode } from '@/renderer/utils/nodeTypeGuards'
import type { InputSpec } from '@/schemas/nodeDefSchema'
import { app } from '@/scripts/app'
import {
@@ -22,7 +28,16 @@ import {
import { CONFIG, GET_CONFIG } from '@/services/litegraphService'
import { mergeInputSpec } from '@/utils/nodeDefUtil'
import { applyTextReplacements } from '@/utils/searchAndReplace'
-import { isPrimitiveNode } from '@/renderer/utils/nodeTypeGuards'
+
+/**
+ * Widget locator with CONFIG symbol properties for accessing input spec.
+ * Used on input/output slots to retrieve widget configuration.
+ */
+interface IWidgetLocatorWithConfig extends IWidgetLocator {
+ name: string
+ [GET_CONFIG]?: () => InputSpec
+ [CONFIG]?: InputSpec
+}
const replacePropertyName = 'Run widget replace on values'
export class PrimitiveNode extends LGraphNode {
@@ -89,14 +104,21 @@ export class PrimitiveNode extends LGraphNode {
override refreshComboInNode() {
const widget = this.widgets?.[0]
if (widget?.type === 'combo') {
- // @ts-expect-error fixme ts strict error
- widget.options.values = this.outputs[0].widget[GET_CONFIG]()[0]
-
- // @ts-expect-error fixme ts strict error
- if (!widget.options.values.includes(widget.value as string)) {
- // @ts-expect-error fixme ts strict error
- widget.value = widget.options.values[0]
- ;(widget.callback as Function)(widget.value)
+ const comboWidget = widget as IComboWidget
+ const widgetLocator = this.outputs[0].widget as IWidgetLocatorWithConfig
+ const config = widgetLocator?.[GET_CONFIG]?.()
+ const rawValues = config?.[0]
+ if (Array.isArray(rawValues)) {
+ const newValues = rawValues.map(String)
+ comboWidget.options.values = newValues
+ if (!newValues.includes(String(comboWidget.value))) {
+ if (newValues.length > 0) {
+ comboWidget.value = newValues[0]
+ comboWidget.callback?.(comboWidget.value)
+ } else {
+ comboWidget.value = ''
+ }
+ }
}
}
}
@@ -186,15 +208,16 @@ export class PrimitiveNode extends LGraphNode {
const input = theirNode.inputs[link.target_slot]
if (!input) return
- let widget
+ let widget: IWidgetLocatorWithConfig
if (!input.widget) {
- if (!(input.type in ComfyWidgets)) return
- widget = { name: input.name, [GET_CONFIG]: () => [input.type, {}] } //fake widget
+ if (typeof input.type !== 'string' || !(input.type in ComfyWidgets))
+ return
+ const inputType = input.type
+ widget = { name: input.name, [GET_CONFIG]: () => [inputType, {}] }
} else {
- widget = input.widget
+ widget = input.widget as IWidgetLocatorWithConfig
}
- // @ts-expect-error fixme ts strict error
const config = widget[GET_CONFIG]?.()
if (!config) return
@@ -208,8 +231,7 @@ export class PrimitiveNode extends LGraphNode {
widget[CONFIG] ?? config,
theirNode,
widget.name,
- // @ts-expect-error fixme ts strict error
- recreating
+ recreating ?? false
)
}
@@ -227,12 +249,12 @@ export class PrimitiveNode extends LGraphNode {
// Store current size as addWidget resizes the node
const [oldWidth, oldHeight] = this.size
- let widget: IBaseWidget
+ let widget: IBaseWidget | undefined
if (isValidWidgetType(type)) {
widget = (ComfyWidgets[type](this, 'value', inputData, app) || {}).widget
} else {
- // @ts-expect-error InputSpec is not typed correctly
- widget = this.addWidget(type, 'value', null, () => {}, {})
+ // Unknown widget type - use 'custom' as fallback
+ widget = this.addWidget('custom', 'value', type, () => {}, {})
}
if (node?.widgets && widget) {
@@ -474,7 +496,7 @@ export function mergeIfValid(
output: INodeOutputSlot | INodeInputSlot,
config2: InputSpec,
forceUpdate?: boolean,
- recreateWidget?: () => void,
+ recreateWidget?: () => IBaseWidget | undefined,
config1?: InputSpec
): { customConfig: InputSpec[1] } {
if (!config1) {
@@ -484,25 +506,20 @@ export function mergeIfValid(
const customSpec = mergeInputSpec(config1, config2)
if (customSpec || forceUpdate) {
- if (customSpec) {
- // @ts-expect-error fixme ts strict error
- output.widget[CONFIG] = customSpec
+ if (customSpec && output.widget) {
+ const widgetLocator = output.widget as IWidgetLocatorWithConfig
+ widgetLocator[CONFIG] = customSpec
}
- // @ts-expect-error fixme ts strict error
- const widget = recreateWidget?.call(this)
- // When deleting a node this can be null
- if (widget) {
- // @ts-expect-error fixme ts strict error
- const min = widget.options.min
- // @ts-expect-error fixme ts strict error
- const max = widget.options.max
- // @ts-expect-error fixme ts strict error
- if (min != null && widget.value < min) widget.value = min
- // @ts-expect-error fixme ts strict error
- if (max != null && widget.value > max) widget.value = max
- // @ts-expect-error fixme ts strict error
- widget.callback(widget.value)
+ const widget = recreateWidget?.()
+ if (widget?.type === 'number') {
+ const numericWidget = widget as INumericWidget
+ const { min, max } = numericWidget.options
+ let currentValue = numericWidget.value ?? 0
+ if (min != null && currentValue < min) currentValue = min
+ if (max != null && currentValue > max) currentValue = max
+ numericWidget.value = currentValue
+ numericWidget.callback?.(currentValue)
}
}
@@ -512,7 +529,6 @@ export function mergeIfValid(
app.registerExtension({
name: 'Comfy.WidgetInputs',
async beforeRegisterNodeDef(nodeType, _nodeData, app) {
- // @ts-expect-error adding extra property
nodeType.prototype.convertWidgetToInput = function (this: LGraphNode) {
console.warn(
'Please remove call to convertWidgetToInput. Widget to socket conversion is no longer necessary, as they co-exist now.'
diff --git a/src/lib/litegraph/src/CurveEditor.ts b/src/lib/litegraph/src/CurveEditor.ts
index 4a536ce0ace..b1d28d408d1 100644
--- a/src/lib/litegraph/src/CurveEditor.ts
+++ b/src/lib/litegraph/src/CurveEditor.ts
@@ -45,8 +45,7 @@ export class CurveEditor {
draw(
ctx: CanvasRenderingContext2D,
size: Rect,
- // @ts-expect-error - LGraphCanvas parameter type needs fixing
- graphcanvas?: LGraphCanvas,
+ _graphcanvas?: LGraphCanvas,
background_color?: string,
line_color?: string,
inactive = false
diff --git a/src/lib/litegraph/src/LGraph.test.ts b/src/lib/litegraph/src/LGraph.test.ts
index 1c2f38da287..8d622e7d6b3 100644
--- a/src/lib/litegraph/src/LGraph.test.ts
+++ b/src/lib/litegraph/src/LGraph.test.ts
@@ -39,11 +39,12 @@ describe('LGraph', () => {
expect(result1).toEqual(result2)
})
test('can be instantiated', ({ expect }) => {
- // @ts-expect-error Intentional - extra holds any / all consumer data that should be serialised
- const graph = new LGraph({ extra: 'TestGraph' })
+ // extra holds any / all consumer data that should be serialised
+ const graph = new LGraph({
+ extra: 'TestGraph'
+ } as unknown as ConstructorParameters[0])
expect(graph).toBeInstanceOf(LGraph)
expect(graph.extra).toBe('TestGraph')
- expect(graph.extra).toBe('TestGraph')
})
test('is exactly the same type', async ({ expect }) => {
@@ -211,12 +212,13 @@ describe('Graph Clearing and Callbacks', () => {
describe('Legacy LGraph Compatibility Layer', () => {
test('can be extended via prototype', ({ expect, minimalGraph }) => {
- // @ts-expect-error Should always be an error.
- LGraph.prototype.newMethod = function () {
- return 'New method added via prototype'
- }
- // @ts-expect-error Should always be an error.
- expect(minimalGraph.newMethod()).toBe('New method added via prototype')
+ ;(LGraph.prototype as unknown as Record).newMethod =
+ function () {
+ return 'New method added via prototype'
+ }
+ expect(
+ (minimalGraph as unknown as Record string>).newMethod()
+ ).toBe('New method added via prototype')
})
test('is correctly assigned to LiteGraph', ({ expect }) => {
diff --git a/src/lib/litegraph/src/LGraph.ts b/src/lib/litegraph/src/LGraph.ts
index 561836d1eb8..8ca15e58a38 100644
--- a/src/lib/litegraph/src/LGraph.ts
+++ b/src/lib/litegraph/src/LGraph.ts
@@ -15,7 +15,7 @@ import { LGraphGroup } from './LGraphGroup'
import { LGraphNode } from './LGraphNode'
import type { NodeId } from './LGraphNode'
import { LLink } from './LLink'
-import type { LinkId, SerialisedLLinkArray } from './LLink'
+import type { LinkId } from './LLink'
import { MapProxyHandler } from './MapProxyHandler'
import { Reroute } from './Reroute'
import type { RerouteId } from './Reroute'
@@ -63,6 +63,7 @@ import type {
LGraphTriggerHandler,
LGraphTriggerParam
} from './types/graphTriggers'
+import type { GroupNodeWorkflowData } from '@/extensions/core/groupNodeTypes'
import type {
ExportedSubgraph,
ExposedWidget,
@@ -74,6 +75,8 @@ import type {
} from './types/serialisation'
import { getAllNestedItems } from './utils/collections'
+export type { GroupNodeWorkflowData } from '@/extensions/core/groupNodeTypes'
+
export type {
LGraphTriggerAction,
LGraphTriggerParam
@@ -102,18 +105,6 @@ export interface LGraphConfig {
links_ontop?: boolean
}
-export interface GroupNodeWorkflowData {
- external: (number | string)[][]
- links: SerialisedLLinkArray[]
- nodes: {
- index?: number
- type?: string
- inputs?: unknown[]
- outputs?: unknown[]
- }[]
- config?: Record
-}
-
export interface LGraphExtra extends Dictionary {
reroutes?: SerialisableReroute[]
linkExtensions?: { id: number; parentId: number | undefined }[]
@@ -157,7 +148,8 @@ export class LGraph
'widgets',
'inputNode',
'outputNode',
- 'extra'
+ 'extra',
+ 'version'
])
id: UUID = zeroUuid
@@ -206,7 +198,7 @@ export class LGraph
last_update_time: number = 0
starttime: number = 0
catch_errors: boolean = true
- execution_timer_id?: number | null
+ execution_timer_id?: ReturnType | number | null
errors_in_execution?: boolean
/** @deprecated Unused */
execution_time!: number
@@ -215,9 +207,12 @@ export class LGraph
/** Must contain serialisable values, e.g. primitive types */
config: LGraphConfig = {}
vars: Dictionary = {}
- nodes_executing: boolean[] = []
- nodes_actioning: (string | boolean)[] = []
- nodes_executedAction: string[] = []
+ /** @deprecated Use a Map or dedicated state management instead */
+ nodes_executing: Record = {}
+ /** @deprecated Use a Map or dedicated state management instead */
+ nodes_actioning: Record = {}
+ /** @deprecated Use a Map or dedicated state management instead */
+ nodes_executedAction: Record = {}
extra: LGraphExtra = {}
/** @deprecated Deserialising a workflow sets this unused property. */
@@ -296,9 +291,6 @@ export class LGraph
node: LGraphNode
): void
- // @ts-expect-error - Private property type needs fixing
- private _input_nodes?: LGraphNode[]
-
/**
* See {@link LGraph}
* @param o data from previous serialization [optional]
@@ -383,9 +375,9 @@ export class LGraph
this.catch_errors = true
- this.nodes_executing = []
- this.nodes_actioning = []
- this.nodes_executedAction = []
+ this.nodes_executing = {}
+ this.nodes_actioning = {}
+ this.nodes_executedAction = {}
// notify canvas to redraw
this.change()
@@ -474,7 +466,6 @@ export class LGraph
on_frame()
} else {
// execute every 'interval' ms
- // @ts-expect-error - Timer ID type mismatch needs fixing
this.execution_timer_id = setInterval(() => {
// execute
this.onBeforeStep?.()
@@ -574,9 +565,9 @@ export class LGraph
this.iteration += 1
this.elapsed_time = (now - this.last_update_time) * 0.001
this.last_update_time = now
- this.nodes_executing = []
- this.nodes_actioning = []
- this.nodes_executedAction = []
+ this.nodes_executing = {}
+ this.nodes_actioning = {}
+ this.nodes_executedAction = {}
}
/**
@@ -711,13 +702,22 @@ export class LGraph
// sort now by priority
L.sort(function (A, B) {
- // @ts-expect-error ctor props
- const Ap = A.constructor.priority || A.priority || 0
- // @ts-expect-error ctor props
- const Bp = B.constructor.priority || B.priority || 0
+ const ctorA = A.constructor
+ const ctorB = B.constructor
+ const priorityA =
+ 'priority' in ctorA && typeof ctorA.priority === 'number'
+ ? ctorA.priority
+ : 'priority' in A && typeof A.priority === 'number'
+ ? A.priority
+ : 0
+ const priorityB =
+ 'priority' in ctorB && typeof ctorB.priority === 'number'
+ ? ctorB.priority
+ : 'priority' in B && typeof B.priority === 'number'
+ ? B.priority
+ : 0
// if same priority, sort by order
-
- return Ap == Bp ? A.order - B.order : Ap - Bp
+ return priorityA == priorityB ? A.order - B.order : priorityA - priorityB
})
// save order number in the node, again...
@@ -807,18 +807,15 @@ export class LGraph
if (!nodes) return
for (const node of nodes) {
- // @ts-expect-error deprecated
- if (!node[eventname] || node.mode != mode) continue
+ if (!(eventname in node) || node.mode != mode) continue
+ const handler = node[eventname as keyof typeof node]
+ if (typeof handler !== 'function') continue
if (params === undefined) {
- // @ts-expect-error deprecated
- node[eventname]()
+ handler.call(node)
} else if (params && params.constructor === Array) {
- // @ts-expect-error deprecated
- // eslint-disable-next-line prefer-spread
- node[eventname].apply(node, params)
+ handler.apply(node, params)
} else {
- // @ts-expect-error deprecated
- node[eventname](params)
+ handler.call(node, params)
}
}
}
@@ -1233,8 +1230,10 @@ export class LGraph
triggerInput(name: string, value: unknown): void {
const nodes = this.findNodesByTitle(name)
for (const node of nodes) {
- // @ts-expect-error - onTrigger method may not exist on all node types
- node.onTrigger(value)
+ const nodeWithTrigger = node as LGraphNode & {
+ onTrigger?: (value: unknown) => void
+ }
+ nodeWithTrigger.onTrigger?.(value)
}
}
@@ -1242,8 +1241,10 @@ export class LGraph
setCallback(name: string, func?: () => void): void {
const nodes = this.findNodesByTitle(name)
for (const node of nodes) {
- // @ts-expect-error - setTrigger method may not exist on all node types
- node.setTrigger(func)
+ const nodeWithTrigger = node as LGraphNode & {
+ setTrigger?: (func: unknown) => void
+ }
+ nodeWithTrigger.setTrigger?.(func)
}
}
@@ -2145,8 +2146,12 @@ export class LGraph
const nodeList =
!LiteGraph.use_uuids && options?.sortNodes
- ? // @ts-expect-error If LiteGraph.use_uuids is false, ids are numbers.
- [...this._nodes].sort((a, b) => a.id - b.id)
+ ? [...this._nodes].sort((a, b) => {
+ if (typeof a.id === 'number' && typeof b.id === 'number') {
+ return a.id - b.id
+ }
+ return 0
+ })
: this._nodes
const nodes = nodeList.map((node) => node.serialize())
@@ -2167,7 +2172,8 @@ export class LGraph
if (LiteGraph.saveViewportWithGraph) extra.ds = this.#getDragAndScale()
if (!extra.ds) delete extra.ds
- const data: ReturnType = {
+ const data: SerialisableGraph &
+ Required> = {
id,
revision,
version: LGraph.serialisedSchemaVersion,
@@ -2197,7 +2203,7 @@ export class LGraph
}
protected _configureBase(data: ISerialisedGraph | SerialisableGraph): void {
- const { id, extra } = data
+ const { id, extra, version } = data
// Create a new graph ID if none is provided
if (id) {
@@ -2206,6 +2212,11 @@ export class LGraph
this.id = createUuidv4()
}
+ // Store the schema version from loaded data
+ if (typeof version === 'number') {
+ this.version = version
+ }
+
// Extra
this.extra = extra ? structuredClone(extra) : {}
@@ -2298,12 +2309,15 @@ export class LGraph
const nodesData = data.nodes
- // copy all stored fields
- for (const i in data) {
- if (LGraph.ConfigureProperties.has(i)) continue
-
- // @ts-expect-error #574 Legacy property assignment
- this[i] = data[i]
+ // copy all stored fields (legacy property assignment)
+ // Unknown properties are stored in `extra` for backwards compat
+ for (const key in data) {
+ if (LGraph.ConfigureProperties.has(key)) continue
+ if (key in this) continue // Skip known properties
+ const value = data[key as keyof typeof data]
+ if (value !== undefined) {
+ this.extra[key] = value
+ }
}
// Subgraph definitions
diff --git a/src/lib/litegraph/src/LGraphCanvas.ts b/src/lib/litegraph/src/LGraphCanvas.ts
index 3e09ba5fdaa..481597cd7cb 100644
--- a/src/lib/litegraph/src/LGraphCanvas.ts
+++ b/src/lib/litegraph/src/LGraphCanvas.ts
@@ -1,3 +1,4 @@
+import { default as DOMPurify } from 'dompurify'
import { toString } from 'es-toolkit/compat'
import { PREFIX, SEPARATOR } from '@/constants/groupNodeConstants'
@@ -103,11 +104,19 @@ import type { UUID } from './utils/uuid'
import { BaseWidget } from './widgets/BaseWidget'
import { toConcreteWidget } from './widgets/widgetMap'
+function isContentEditable(el: HTMLElement | null): boolean {
+ while (el) {
+ if (el.isContentEditable) return true
+ el = el.parentElement
+ }
+ return false
+}
+
interface IShowSearchOptions {
- node_to?: LGraphNode | null
- node_from?: LGraphNode | null
- slot_from: number | INodeOutputSlot | INodeInputSlot | null | undefined
- type_filter_in?: ISlotType
+ node_to?: SubgraphOutputNode | LGraphNode | null
+ node_from?: SubgraphInputNode | LGraphNode | null
+ slot_from?: number | INodeOutputSlot | INodeInputSlot | SubgraphIO | null
+ type_filter_in?: ISlotType | false
type_filter_out?: ISlotType | false
// TODO check for registered_slot_[in/out]_types not empty // this will be checked for functionality enabled : filter on slot type, in and out
@@ -161,6 +170,15 @@ interface ICloseable {
close(): void
}
+interface IPanel extends Element, ICloseable {
+ node?: LGraphNode
+ graph?: LGraph
+}
+
+function isPanel(el: Element): el is IPanel {
+ return 'close' in el && typeof el.close === 'function'
+}
+
interface IDialogExtensions extends ICloseable {
modified(): void
is_modified: boolean
@@ -702,7 +720,7 @@ export class LGraphCanvas implements CustomEventDispatcher
bg_tint?: string | CanvasGradient | CanvasPattern
// TODO: This looks like another panel thing
prompt_box?: PromptDialog | null
- search_box?: HTMLDivElement
+ search_box?: HTMLDivElement & ICloseable
/** @deprecated Panels */
SELECTED_NODE?: LGraphNode
/** @deprecated Panels */
@@ -742,7 +760,7 @@ export class LGraphCanvas implements CustomEventDispatcher
/** called when rendering a tooltip */
onDrawLinkTooltip?: (
ctx: CanvasRenderingContext2D,
- link: LLink | null,
+ link: LinkSegment | null,
canvas?: LGraphCanvas
) => boolean
@@ -838,10 +856,7 @@ export class LGraphCanvas implements CustomEventDispatcher
if ('shiftKey' in e && e.shiftKey) {
if (this.allow_searchbox) {
- this.showSearchBox(
- e as unknown as MouseEvent,
- linkReleaseContext as IShowSearchOptions
- )
+ this.showSearchBox(e, linkReleaseContext as IShowSearchOptions)
}
} else if (this.linkConnector.state.connectingTo === 'input') {
this.showConnectionMenu({
@@ -1390,7 +1405,7 @@ export class LGraphCanvas implements CustomEventDispatcher
_menu: ContextMenu,
node: LGraphNode
): void {
- const property = item.property || 'title'
+ const property: keyof LGraphNode = item.property || 'title'
const value = node[property]
const title = document.createElement('span')
@@ -1484,8 +1499,13 @@ export class LGraphCanvas implements CustomEventDispatcher
} else if (item.type == 'Boolean') {
value = Boolean(value)
}
- // @ts-expect-error Requires refactor.
- node[property] = value
+ // Set the node property - property is validated as keyof LGraphNode
+ if (property === 'title' && typeof value === 'string') {
+ node.title = value
+ } else if (property in node) {
+ // For other properties, use the properties bag
+ node.properties[property as string] = value
+ }
dialog.remove()
canvas.setDirty(true, true)
}
@@ -1503,10 +1523,9 @@ export class LGraphCanvas implements CustomEventDispatcher
if (typeof values === 'object') {
let desc_value = ''
- for (const k in values) {
- // @ts-expect-error deprecated #578
- if (values[k] != value) continue
-
+ const valuesRecord = values as Record
+ for (const k in valuesRecord) {
+ if (valuesRecord[k] != value) continue
desc_value = k
break
}
@@ -2029,8 +2048,8 @@ export class LGraphCanvas implements CustomEventDispatcher
if (!this.canvas) return window
const doc = this.canvas.ownerDocument
- // @ts-expect-error Check if required
- return doc.defaultView || doc.parentWindow
+ // parentWindow is an IE-specific fallback, no longer relevant
+ return doc.defaultView ?? window
}
/**
@@ -3668,8 +3687,10 @@ export class LGraphCanvas implements CustomEventDispatcher
if (!graph) return
let block_default = false
- // @ts-expect-error EventTarget.localName is not in standard types
- if (e.target.localName == 'input') return
+ const targetEl = e.target
+ if (targetEl instanceof HTMLInputElement) return
+ if (targetEl instanceof HTMLTextAreaElement) return
+ if (targetEl instanceof HTMLElement && isContentEditable(targetEl)) return
if (e.type == 'keydown') {
// TODO: Switch
@@ -3706,16 +3727,13 @@ export class LGraphCanvas implements CustomEventDispatcher
this.pasteFromClipboard({ connectInputs: e.shiftKey })
} else if (e.key === 'Delete' || e.key === 'Backspace') {
// delete or backspace
- // @ts-expect-error EventTarget.localName is not in standard types
- if (e.target.localName != 'input' && e.target.localName != 'textarea') {
- if (this.selectedItems.size === 0) {
- this.#noItemsSelected()
- return
- }
-
- this.deleteSelected()
- block_default = true
+ if (this.selectedItems.size === 0) {
+ this.#noItemsSelected()
+ return
}
+
+ this.deleteSelected()
+ block_default = true
}
// TODO
@@ -4694,9 +4712,12 @@ export class LGraphCanvas implements CustomEventDispatcher
const { ctx, canvas, graph, linkConnector } = this
- // @ts-expect-error start2D method not in standard CanvasRenderingContext2D
- if (ctx.start2D && !this.viewport) {
- // @ts-expect-error start2D method not in standard CanvasRenderingContext2D
+ // start2D is a non-standard method (e.g., GL-backed canvas libraries)
+ if (
+ 'start2D' in ctx &&
+ typeof ctx.start2D === 'function' &&
+ !this.viewport
+ ) {
ctx.start2D()
ctx.restore()
ctx.setTransform(1, 0, 0, 1, 0, 0)
@@ -5369,11 +5390,9 @@ export class LGraphCanvas implements CustomEventDispatcher
}
ctx.fill()
- // @ts-expect-error TODO: Better value typing
const { data } = link
if (data == null) return
- // @ts-expect-error TODO: Better value typing
if (this.onDrawLinkTooltip?.(ctx, link, this) == true) return
let text: string | null = null
@@ -6649,17 +6668,13 @@ export class LGraphCanvas implements CustomEventDispatcher
case 'Search':
if (isFrom) {
opts.showSearchBox(e, {
- // @ts-expect-error - Subgraph types
node_from: opts.nodeFrom,
- // @ts-expect-error - Subgraph types
slot_from: slotX,
type_filter_in: fromSlotType
})
} else {
opts.showSearchBox(e, {
- // @ts-expect-error - Subgraph types
node_to: opts.nodeTo,
- // @ts-expect-error - Subgraph types
slot_from: slotX,
type_filter_out: fromSlotType
})
@@ -6843,9 +6858,7 @@ export class LGraphCanvas implements CustomEventDispatcher
do_type_filter: LiteGraph.search_filter_enabled,
// these are default: pass to set initially set values
- // @ts-expect-error Property missing from interface definition
type_filter_in: false,
-
type_filter_out: false,
show_general_if_none_on_typefilter: true,
show_general_after_typefiltered: true,
@@ -6953,7 +6966,6 @@ export class LGraphCanvas implements CustomEventDispatcher
}
}
- // @ts-expect-error Panel?
that.search_box?.close()
that.search_box = dialog
@@ -7020,7 +7032,6 @@ export class LGraphCanvas implements CustomEventDispatcher
opt.innerHTML = aSlots[iK]
selIn.append(opt)
if (
- // @ts-expect-error Property missing from interface definition
options.type_filter_in !== false &&
String(options.type_filter_in).toLowerCase() ==
String(aSlots[iK]).toLowerCase()
@@ -7065,13 +7076,12 @@ export class LGraphCanvas implements CustomEventDispatcher
// Handles cases where the searchbox is initiated by
// non-click events. e.g. Keyboard shortcuts
+ const defaultY = rect.top + rect.height * 0.5
const safeEvent =
event ??
new MouseEvent('click', {
clientX: rect.left + rect.width * 0.5,
- clientY: rect.top + rect.height * 0.5,
- // @ts-expect-error layerY is a nonstandard property
- layerY: rect.top + rect.height * 0.5
+ clientY: defaultY
})
const left = safeEvent.clientX - 80
@@ -7079,9 +7089,10 @@ export class LGraphCanvas implements CustomEventDispatcher
dialog.style.left = `${left}px`
dialog.style.top = `${top}px`
- // To avoid out of screen problems
- if (safeEvent.layerY > rect.height - 200) {
- helper.style.maxHeight = `${rect.height - safeEvent.layerY - 20}px`
+ // To avoid out of screen problems - derive layerY from clientY
+ const safeLayerY = event?.layerY ?? safeEvent.clientY - rect.top
+ if (safeLayerY > rect.height - 200) {
+ helper.style.maxHeight = `${rect.height - safeLayerY - 20}px`
}
requestAnimationFrame(function () {
input.focus()
@@ -7103,27 +7114,32 @@ export class LGraphCanvas implements CustomEventDispatcher
}
// join node after inserting
- if (options.node_from) {
+ // These code paths only work with LGraphNode instances (not SubgraphIO nodes)
+ if (options.node_from && options.node_from instanceof LGraphNode) {
+ const nodeFrom = options.node_from
// FIXME: any
let iS: any = false
switch (typeof options.slot_from) {
case 'string':
- iS = options.node_from.findOutputSlot(options.slot_from)
+ iS = nodeFrom.findOutputSlot(options.slot_from)
break
- case 'object':
+ case 'object': {
if (options.slot_from == null)
throw new TypeError(
'options.slot_from was null when showing search box'
)
iS = options.slot_from.name
- ? options.node_from.findOutputSlot(options.slot_from.name)
+ ? nodeFrom.findOutputSlot(options.slot_from.name)
: -1
- // @ts-expect-error - slot_index property
- if (iS == -1 && options.slot_from.slot_index !== undefined)
- // @ts-expect-error - slot_index property
+ if (
+ iS == -1 &&
+ 'slot_index' in options.slot_from &&
+ typeof options.slot_from.slot_index === 'number'
+ )
iS = options.slot_from.slot_index
break
+ }
case 'number':
iS = options.slot_from
break
@@ -7131,44 +7147,44 @@ export class LGraphCanvas implements CustomEventDispatcher
// try with first if no name set
iS = 0
}
- if (options.node_from.outputs[iS] !== undefined) {
+ if (nodeFrom.outputs?.[iS] !== undefined) {
if (iS !== false && iS > -1) {
if (node == null)
throw new TypeError(
'options.slot_from was null when showing search box'
)
- options.node_from.connectByType(
- iS,
- node,
- options.node_from.outputs[iS].type
- )
+ nodeFrom.connectByType(iS, node, nodeFrom.outputs[iS].type)
}
} else {
// console.warn("can't find slot " + options.slot_from);
}
}
- if (options.node_to) {
+ if (options.node_to && options.node_to instanceof LGraphNode) {
+ const nodeTo = options.node_to
// FIXME: any
let iS: any = false
switch (typeof options.slot_from) {
case 'string':
- iS = options.node_to.findInputSlot(options.slot_from)
+ iS = nodeTo.findInputSlot(options.slot_from)
break
- case 'object':
+ case 'object': {
if (options.slot_from == null)
throw new TypeError(
'options.slot_from was null when showing search box'
)
iS = options.slot_from.name
- ? options.node_to.findInputSlot(options.slot_from.name)
+ ? nodeTo.findInputSlot(options.slot_from.name)
: -1
- // @ts-expect-error - slot_index property
- if (iS == -1 && options.slot_from.slot_index !== undefined)
- // @ts-expect-error - slot_index property
+ if (
+ iS == -1 &&
+ 'slot_index' in options.slot_from &&
+ typeof options.slot_from.slot_index === 'number'
+ )
iS = options.slot_from.slot_index
break
+ }
case 'number':
iS = options.slot_from
break
@@ -7176,18 +7192,14 @@ export class LGraphCanvas implements CustomEventDispatcher
// try with first if no name set
iS = 0
}
- if (options.node_to.inputs[iS] !== undefined) {
+ if (nodeTo.inputs?.[iS] !== undefined) {
if (iS !== false && iS > -1) {
if (node == null)
throw new TypeError(
'options.slot_from was null when showing search box'
)
// try connection
- options.node_to.connectByTypeOutput(
- iS,
- node,
- options.node_to.inputs[iS].type
- )
+ nodeTo.connectByTypeOutput(iS, node, nodeTo.inputs[iS].type)
}
} else {
// console.warn("can't find slot_nodeTO " + options.slot_from);
@@ -7437,15 +7449,18 @@ export class LGraphCanvas implements CustomEventDispatcher
input = dialog.querySelector('select')
input?.addEventListener('change', function (e) {
dialog.modified()
- setValue((e.target as HTMLSelectElement)?.value)
+ if (e.target instanceof HTMLSelectElement) setValue(e.target.value)
})
} else if (type == 'boolean' || type == 'toggle') {
input = dialog.querySelector('input')
- input?.addEventListener('click', function () {
- dialog.modified()
- // @ts-expect-error setValue function signature not strictly typed
- setValue(!!input.checked)
- })
+ if (input instanceof HTMLInputElement) {
+ const checkbox = input
+ checkbox.addEventListener('click', function () {
+ dialog.modified()
+ // Convert boolean to string for setValue which expects string
+ setValue(checkbox.checked ? 'true' : 'false')
+ })
+ }
} else {
input = dialog.querySelector('input')
if (input) {
@@ -7461,8 +7476,8 @@ export class LGraphCanvas implements CustomEventDispatcher
v = JSON.stringify(v)
}
- // @ts-expect-error HTMLInputElement.value expects string but v can be other types
- input.value = v
+ // Ensure v is converted to string for HTMLInputElement.value
+ input.value = String(v)
input.addEventListener('keydown', function (e) {
if (e.key == 'Escape') {
// ESC
@@ -7494,6 +7509,7 @@ export class LGraphCanvas implements CustomEventDispatcher
function setValue(value: string | number | undefined) {
if (
+ value !== undefined &&
info?.values &&
typeof info.values === 'object' &&
info.values[value] != undefined
@@ -7505,8 +7521,7 @@ export class LGraphCanvas implements CustomEventDispatcher
value = Number(value)
}
if (type == 'array' || type == 'object') {
- // @ts-expect-error JSON.parse doesn't care.
- value = JSON.parse(value)
+ value = JSON.parse(String(value))
}
node.properties[property] = value
if (node.graph) {
@@ -7801,18 +7816,18 @@ export class LGraphCanvas implements CustomEventDispatcher
value_element.addEventListener('click', function (event) {
const values = options.values || []
const propname = this.parentElement?.dataset['property']
- const inner_clicked = (v: string | null) => {
- // node.setProperty(propname,v);
- // graphcanvas.dirty_canvas = true;
- this.textContent = v
- innerChange(propname, v)
- return false
- }
+ const textElement = this
new LiteGraph.ContextMenu(values, {
event,
className: 'dark',
- // @ts-expect-error fixme ts strict error - callback signature mismatch
- callback: inner_clicked
+ callback: (v?: string | IContextMenuValue) => {
+ // node.setProperty(propname,v);
+ // graphcanvas.dirty_canvas = true;
+ const value = typeof v === 'string' ? v : (v?.value ?? null)
+ textElement.textContent = value
+ innerChange(propname, value)
+ return false
+ }
})
})
}
@@ -7829,6 +7844,8 @@ export class LGraphCanvas implements CustomEventDispatcher
if (typeof root.onOpen == 'function') root.onOpen()
+ root.graph = this.graph
+
return root
}
@@ -7864,9 +7881,11 @@ export class LGraphCanvas implements CustomEventDispatcher
const inner_refresh = () => {
// clear
panel.content.innerHTML = ''
+ const ctor = node.constructor
+ const nodeDesc =
+ 'desc' in ctor && typeof ctor.desc === 'string' ? ctor.desc : ''
panel.addHTML(
- // @ts-expect-error - desc property
- `${node.type}${node.constructor.desc || ''}`
+ `${DOMPurify.sanitize(node.type ?? '')}${DOMPurify.sanitize(nodeDesc)}`
)
panel.addHTML('Properties
')
@@ -8023,9 +8042,8 @@ export class LGraphCanvas implements CustomEventDispatcher
throw new TypeError('checkPanels - this.canvas.parentNode was null')
const panels = this.canvas.parentNode.querySelectorAll('.litegraph.dialog')
for (const panel of panels) {
- // @ts-expect-error Panel
+ if (!isPanel(panel)) continue
if (!panel.node) continue
- // @ts-expect-error Panel
if (!panel.node.graph || panel.graph != this.graph) panel.close()
}
}
@@ -8294,8 +8312,9 @@ export class LGraphCanvas implements CustomEventDispatcher
menu_info.push(...node.getExtraSlotMenuOptions(slot))
}
}
- // @ts-expect-error Slot type can be number and has number checks
- options.title = (slot.input ? slot.input.type : slot.output.type) || '*'
+ // Slot type can be ISlotType which includes number, but we convert to string for title
+ const slotType = slot.input ? slot.input.type : slot.output?.type
+ options.title = String(slotType ?? '*')
if (slot.input && slot.input.type == LiteGraph.ACTION)
options.title = 'Action'
diff --git a/src/lib/litegraph/src/LGraphNode.test.ts b/src/lib/litegraph/src/LGraphNode.test.ts
index 98ef533f605..47ce522ec8d 100644
--- a/src/lib/litegraph/src/LGraphNode.test.ts
+++ b/src/lib/litegraph/src/LGraphNode.test.ts
@@ -38,8 +38,8 @@ describe('LGraphNode', () => {
beforeEach(() => {
origLiteGraph = Object.assign({}, LiteGraph)
- // @ts-expect-error Intended: Force remove an otherwise readonly non-optional property
- delete origLiteGraph.Classes
+ // Intended: Force remove an otherwise readonly non-optional property for test isolation
+ delete (origLiteGraph as { Classes?: unknown }).Classes
Object.assign(LiteGraph, {
NODE_TITLE_HEIGHT: 20,
diff --git a/src/lib/litegraph/src/LGraphNode.ts b/src/lib/litegraph/src/LGraphNode.ts
index 0ca41a7320f..287b1526890 100644
--- a/src/lib/litegraph/src/LGraphNode.ts
+++ b/src/lib/litegraph/src/LGraphNode.ts
@@ -34,6 +34,7 @@ import type {
IColorable,
IContextMenuValue,
IFoundSlot,
+ ILinkRouting,
INodeFlags,
INodeInputSlot,
INodeOutputSlot,
@@ -617,7 +618,7 @@ export class LGraphNode
): void
onDeselected?(this: LGraphNode): void
onKeyUp?(this: LGraphNode, e: KeyboardEvent): void
- onKeyDown?(this: LGraphNode, e: KeyboardEvent): void
+ onKeyDown?(this: LGraphNode, e: KeyboardEvent): boolean | void
onSelected?(this: LGraphNode): void
getExtraMenuOptions?(
this: LGraphNode,
@@ -784,7 +785,15 @@ export class LGraphNode
if (this.graph) {
this.graph._version++
}
- for (const j in info) {
+
+ // Use Record types to enable dynamic property access on both info and this
+ const infoRecord = info as unknown as Record
+ const nodeRecord = this as unknown as Record<
+ string,
+ unknown & { configure?(data: unknown): void }
+ >
+
+ for (const j in infoRecord) {
if (j == 'properties') {
// i don't want to clone properties, I want to reuse the old container
for (const k in info.properties) {
@@ -794,23 +803,27 @@ export class LGraphNode
continue
}
- // @ts-expect-error #594
- if (info[j] == null) {
+ const infoValue = infoRecord[j]
+ if (infoValue == null) {
continue
- // @ts-expect-error #594
- } else if (typeof info[j] == 'object') {
- // @ts-expect-error #594
- if (this[j]?.configure) {
- // @ts-expect-error #594
- this[j]?.configure(info[j])
+ } else if (typeof infoValue == 'object') {
+ const nodeValue = nodeRecord[j]
+ if (
+ nodeValue &&
+ typeof nodeValue === 'object' &&
+ 'configure' in nodeValue &&
+ typeof nodeValue.configure === 'function'
+ ) {
+ nodeValue.configure(infoValue)
} else {
- // @ts-expect-error #594
- this[j] = LiteGraph.cloneObject(info[j], this[j])
+ nodeRecord[j] = LiteGraph.cloneObject(
+ infoValue as object,
+ nodeValue as object
+ )
}
} else {
// value
- // @ts-expect-error #594
- this[j] = info[j]
+ nodeRecord[j] = infoValue
}
}
@@ -903,7 +916,6 @@ export class LGraphNode
if (this.inputs)
o.inputs = this.inputs.map((input) => inputAsSerialisable(input))
if (this.outputs)
- // @ts-expect-error - Output serialization type mismatch
o.outputs = this.outputs.map((output) => outputAsSerialisable(output))
if (this.title && this.title != this.constructor.title) o.title = this.title
@@ -915,8 +927,10 @@ export class LGraphNode
o.widgets_values = []
for (const [i, widget] of widgets.entries()) {
if (widget.serialize === false) continue
- // @ts-expect-error #595 No-null
- o.widgets_values[i] = widget ? widget.value : null
+ // Widget value can be any serializable type; null is valid for missing widgets
+ o.widgets_values[i] = widget
+ ? widget.value
+ : (null as unknown as TWidgetValue)
}
}
@@ -958,10 +972,14 @@ export class LGraphNode
}
}
- // @ts-expect-error Exceptional case: id is removed so that the graph can assign a new one on add.
- data.id = undefined
-
- if (LiteGraph.use_uuids) data.id = LiteGraph.uuidv4()
+ // Exceptional case: id is removed so that the graph can assign a new one on add.
+ // The id field is overwritten to -1 which signals the graph to assign a new id.
+ // When using UUIDs, a new UUID is generated immediately.
+ if (LiteGraph.use_uuids) {
+ data.id = LiteGraph.uuidv4()
+ } else {
+ data.id = -1
+ }
node.configure(data)
@@ -1153,10 +1171,11 @@ export class LGraphNode
}
/**
- * Returns the link info in the connection of an input slot
- * @returns object or null
+ * Returns the link info in the connection of an input slot.
+ * Group nodes may override this to return ILinkRouting for internal routing.
+ * @returns LLink, ILinkRouting, or null
*/
- getInputLink(slot: number): LLink | null {
+ getInputLink(slot: number): LLink | ILinkRouting | null {
if (!this.inputs) return null
if (slot < this.inputs.length) {
@@ -1324,10 +1343,6 @@ export class LGraphNode
case LGraphEventMode.ALWAYS:
break
- // @ts-expect-error Not impl.
- case LiteGraph.ON_REQUEST:
- break
-
default:
return false
break
@@ -1346,17 +1361,14 @@ export class LGraphNode
options.action_call ||= `${this.id}_exec_${Math.floor(Math.random() * 9999)}`
if (!this.graph) throw new NullGraphError()
- // @ts-expect-error Technically it works when id is a string. Array gets props.
this.graph.nodes_executing[this.id] = true
this.onExecute(param, options)
- // @ts-expect-error deprecated
this.graph.nodes_executing[this.id] = false
// save execution/action ref
this.exec_version = this.graph.iteration
if (options?.action_call) {
this.action_call = options.action_call
- // @ts-expect-error deprecated
this.graph.nodes_executedAction[this.id] = options.action_call
}
}
@@ -1380,16 +1392,13 @@ export class LGraphNode
options.action_call ||= `${this.id}_${action || 'action'}_${Math.floor(Math.random() * 9999)}`
if (!this.graph) throw new NullGraphError()
- // @ts-expect-error deprecated
this.graph.nodes_actioning[this.id] = action || 'actioning'
this.onAction(action, param, options)
- // @ts-expect-error deprecated
this.graph.nodes_actioning[this.id] = false
// save execution/action ref
if (options?.action_call) {
this.action_call = options.action_call
- // @ts-expect-error deprecated
this.graph.nodes_executedAction[this.id] = options.action_call
}
}
@@ -1838,11 +1847,13 @@ export class LGraphNode
}
}
}
- // litescene mode using the constructor
- // @ts-expect-error deprecated https://github.com/Comfy-Org/litegraph.js/issues/639
- if (this.constructor[`@${property}`])
- // @ts-expect-error deprecated https://github.com/Comfy-Org/litegraph.js/issues/639
- info = this.constructor[`@${property}`]
+ // litescene mode using the constructor (deprecated)
+ const ctor = this.constructor as unknown as Record<
+ string,
+ INodePropertyInfo | undefined
+ >
+ const ctorPropertyInfo = ctor[`@${property}`]
+ if (ctorPropertyInfo) info = ctorPropertyInfo
if (this.constructor.widgets_info?.[property])
info = this.constructor.widgets_info[property]
@@ -1896,8 +1907,7 @@ export class LGraphNode
}
const w: IBaseWidget & { type: Type } = {
- // @ts-expect-error - Type casting for widget type property
- type: type.toLowerCase(),
+ type: type.toLowerCase() as Type,
name: name,
value: value,
callback: typeof callback !== 'function' ? undefined : callback,
@@ -3396,8 +3406,8 @@ export class LGraphNode
trace(msg: string): void {
this.console ||= []
this.console.push(msg)
- // @ts-expect-error deprecated
- if (this.console.length > LGraphNode.MAX_CONSOLE) this.console.shift()
+ const maxConsole = LGraphNode.MAX_CONSOLE ?? 100
+ if (this.console.length > maxConsole) this.console.shift()
}
/* Forces to redraw or the main canvas (LGraphNode) or the bg canvas (links) */
diff --git a/src/lib/litegraph/src/LLink.ts b/src/lib/litegraph/src/LLink.ts
index 6ca8832a175..af7dab2d6ce 100644
--- a/src/lib/litegraph/src/LLink.ts
+++ b/src/lib/litegraph/src/LLink.ts
@@ -11,6 +11,7 @@ import type { LGraphNode, NodeId } from './LGraphNode'
import type { Reroute, RerouteId } from './Reroute'
import type {
CanvasColour,
+ ILinkRouting,
INodeInputSlot,
INodeOutputSlot,
ISlotType,
@@ -89,7 +90,9 @@ type BasicReadonlyNetwork = Pick<
>
// this is the class in charge of storing link information
-export class LLink implements LinkSegment, Serialisable {
+export class LLink
+ implements LinkSegment, Serialisable, ILinkRouting
+{
static _drawDebug = false
/** Link ID */
diff --git a/src/lib/litegraph/src/LiteGraphGlobal.ts b/src/lib/litegraph/src/LiteGraphGlobal.ts
index 6a9cb5db666..eb077b11d3e 100644
--- a/src/lib/litegraph/src/LiteGraphGlobal.ts
+++ b/src/lib/litegraph/src/LiteGraphGlobal.ts
@@ -412,10 +412,11 @@ export class LiteGraphGlobal {
base_class.title ||= classname
- // extend class
- for (const i in LGraphNode.prototype) {
- // @ts-expect-error #576 This functionality is deprecated and should be removed.
- base_class.prototype[i] ||= LGraphNode.prototype[i]
+ // extend class (deprecated - should be using proper class inheritance)
+ const nodeProto = LGraphNode.prototype as unknown as Record
+ const baseProto = base_class.prototype as unknown as Record
+ for (const i in nodeProto) {
+ baseProto[i] ||= nodeProto[i]
}
const prev = this.registered_node_types[type]
@@ -460,20 +461,24 @@ export class LiteGraphGlobal {
* @param slot_type name of the slot type (variable type), eg. string, number, array, boolean, ..
*/
registerNodeAndSlotType(
- type: LGraphNode,
+ type: LGraphNode | string,
slot_type: ISlotType,
out?: boolean
): void {
out ||= false
+ // Handle both string type names and node instances
const base_class =
- typeof type === 'string' &&
- // @ts-expect-error Confirm this function no longer supports string types - base_class should always be an instance not a constructor.
- this.registered_node_types[type] !== 'anonymous'
+ typeof type === 'string' && this.registered_node_types[type]
? this.registered_node_types[type]
: type
- // @ts-expect-error Confirm this function no longer supports string types - base_class should always be an instance not a constructor.
- const class_type = base_class.constructor.type
+ // Get the type from the constructor for node classes
+ const ctor =
+ typeof base_class !== 'string' ? base_class.constructor : undefined
+ const class_type =
+ ctor && 'type' in ctor && typeof ctor.type === 'string'
+ ? ctor.type
+ : undefined
let allTypes = []
if (typeof slot_type === 'string') {
@@ -493,7 +498,8 @@ export class LiteGraphGlobal {
register[slotType] ??= { nodes: [] }
const { nodes } = register[slotType]
- if (!nodes.includes(class_type)) nodes.push(class_type)
+ if (class_type !== undefined && !nodes.includes(class_type))
+ nodes.push(class_type)
// check if is a new type
const types = out ? this.slot_types_out : this.slot_types_in
@@ -559,11 +565,11 @@ export class LiteGraphGlobal {
node.pos ||= [this.DEFAULT_POSITION[0], this.DEFAULT_POSITION[1]]
node.mode ||= LGraphEventMode.ALWAYS
- // extra options
+ // extra options (dynamic property assignment for node configuration)
if (options) {
+ const nodeRecord = node as unknown as Record
for (const i in options) {
- // @ts-expect-error #577 Requires interface
- node[i] = options[i]
+ nodeRecord[i] = (options as Record)[i]
}
}
@@ -655,20 +661,21 @@ export class LiteGraphGlobal {
}
// separated just to improve if it doesn't work
- /** @deprecated Prefer {@link structuredClone} */
+ /**
+ * @deprecated Prefer {@link structuredClone}
+ * Note: JSON.parse returns `unknown`, so type assertions are unavoidable here.
+ * This function is deprecated precisely because it cannot be made type-safe.
+ */
cloneObject(
obj: T,
target?: T
): WhenNullish {
if (obj == null) return null as WhenNullish
- const r = JSON.parse(JSON.stringify(obj))
- if (!target) return r
+ const cloned: unknown = JSON.parse(JSON.stringify(obj))
+ if (!target) return cloned as WhenNullish
- for (const i in r) {
- // @ts-expect-error deprecated
- target[i] = r[i]
- }
+ Object.assign(target, cloned)
return target
}
@@ -788,33 +795,30 @@ export class LiteGraphGlobal {
}
}
- switch (sEvent) {
- // both pointer and move events
- case 'down':
- case 'up':
- case 'move':
- case 'over':
- case 'out':
- // @ts-expect-error - intentional fallthrough
- case 'enter': {
- oDOM.addEventListener(sMethod + sEvent, fCall, capture)
- }
- // only pointerevents
- // falls through
- case 'leave':
- case 'cancel':
- case 'gotpointercapture':
- // @ts-expect-error - intentional fallthrough
- case 'lostpointercapture': {
- if (sMethod != 'mouse') {
- return oDOM.addEventListener(sMethod + sEvent, fCall, capture)
- }
+ // Events that apply to both pointer and mouse methods
+ const pointerAndMouseEvents = ['down', 'up', 'move', 'over', 'out', 'enter']
+ // Events that only apply to pointer method
+ const pointerOnlyEvents = [
+ 'leave',
+ 'cancel',
+ 'gotpointercapture',
+ 'lostpointercapture'
+ ]
+
+ if (pointerAndMouseEvents.includes(sEvent)) {
+ oDOM.addEventListener(sMethod + sEvent, fCall, capture)
+ }
+
+ if (
+ pointerAndMouseEvents.includes(sEvent) ||
+ pointerOnlyEvents.includes(sEvent)
+ ) {
+ if (sMethod != 'mouse') {
+ return oDOM.addEventListener(sMethod + sEvent, fCall, capture)
}
- // not "pointer" || "mouse"
- // falls through
- default:
- return oDOM.addEventListener(sEvent, fCall, capture)
}
+
+ return oDOM.addEventListener(sEvent, fCall, capture)
}
pointerListenerRemove(
@@ -831,45 +835,37 @@ export class LiteGraphGlobal {
)
return
- switch (sEvent) {
- // both pointer and move events
- case 'down':
- case 'up':
- case 'move':
- case 'over':
- case 'out':
- // @ts-expect-error - intentional fallthrough
- case 'enter': {
- if (
- this.pointerevents_method == 'pointer' ||
- this.pointerevents_method == 'mouse'
- ) {
- oDOM.removeEventListener(
- this.pointerevents_method + sEvent,
- fCall,
- capture
- )
- }
+ // Events that apply to both pointer and mouse methods
+ const pointerAndMouseEvents = ['down', 'up', 'move', 'over', 'out', 'enter']
+ // Events that only apply to pointer method
+ const pointerOnlyEvents = [
+ 'leave',
+ 'cancel',
+ 'gotpointercapture',
+ 'lostpointercapture'
+ ]
+
+ if (pointerAndMouseEvents.includes(sEvent)) {
+ if (
+ this.pointerevents_method == 'pointer' ||
+ this.pointerevents_method == 'mouse'
+ ) {
+ oDOM.removeEventListener(
+ this.pointerevents_method + sEvent,
+ fCall,
+ capture
+ )
}
- // only pointerevents
- // falls through
- case 'leave':
- case 'cancel':
- case 'gotpointercapture':
- // @ts-expect-error - intentional fallthrough
- case 'lostpointercapture': {
- if (this.pointerevents_method == 'pointer') {
- return oDOM.removeEventListener(
- this.pointerevents_method + sEvent,
- fCall,
- capture
- )
- }
+ } else if (pointerOnlyEvents.includes(sEvent)) {
+ if (this.pointerevents_method == 'pointer') {
+ oDOM.removeEventListener(
+ this.pointerevents_method + sEvent,
+ fCall,
+ capture
+ )
}
- // not "pointer" || "mouse"
- // falls through
- default:
- return oDOM.removeEventListener(sEvent, fCall, capture)
+ } else {
+ oDOM.removeEventListener(sEvent, fCall, capture)
}
}
diff --git a/src/lib/litegraph/src/__snapshots__/LGraph.test.ts.snap b/src/lib/litegraph/src/__snapshots__/LGraph.test.ts.snap
index 1a92319bb77..bc4625f887e 100644
--- a/src/lib/litegraph/src/__snapshots__/LGraph.test.ts.snap
+++ b/src/lib/litegraph/src/__snapshots__/LGraph.test.ts.snap
@@ -32,7 +32,6 @@ LGraph {
"title": "A group to test with",
},
],
- "_input_nodes": undefined,
"_last_trigger_time": undefined,
"_links": Map {},
"_nodes": [
@@ -281,9 +280,9 @@ LGraph {
"last_update_time": 0,
"links": Map {},
"list_of_graphcanvas": null,
- "nodes_actioning": [],
- "nodes_executedAction": [],
- "nodes_executing": [],
+ "nodes_actioning": {},
+ "nodes_executedAction": {},
+ "nodes_executing": {},
"onTrigger": undefined,
"reroutesInternal": Map {},
"revision": 0,
diff --git a/src/lib/litegraph/src/canvas/FloatingRenderLink.ts b/src/lib/litegraph/src/canvas/FloatingRenderLink.ts
index 9f4fc92f07e..d1ebfb2cbe9 100644
--- a/src/lib/litegraph/src/canvas/FloatingRenderLink.ts
+++ b/src/lib/litegraph/src/canvas/FloatingRenderLink.ts
@@ -196,8 +196,7 @@ export class FloatingRenderLink implements RenderLink {
}
connectToRerouteInput(
- // @ts-expect-error - Reroute type needs fixing
- reroute: Reroute,
+ _reroute: Reroute,
{ node: inputNode, input }: { node: LGraphNode; input: INodeInputSlot },
events: CustomEventTarget
) {
@@ -213,8 +212,7 @@ export class FloatingRenderLink implements RenderLink {
}
connectToRerouteOutput(
- // @ts-expect-error - Reroute type needs fixing
- reroute: Reroute,
+ _reroute: Reroute,
outputNode: LGraphNode,
output: INodeOutputSlot,
events: CustomEventTarget
diff --git a/src/lib/litegraph/src/interfaces.ts b/src/lib/litegraph/src/interfaces.ts
index 0983770a11a..936cf99a1ff 100644
--- a/src/lib/litegraph/src/interfaces.ts
+++ b/src/lib/litegraph/src/interfaces.ts
@@ -184,6 +184,21 @@ export interface ItemLocator {
): SubgraphInputNode | SubgraphOutputNode | undefined
}
+/**
+ * Minimal link routing information used for input resolution.
+ * Both LLink and partial link info objects satisfy this interface.
+ */
+export interface ILinkRouting {
+ /** Output node ID */
+ readonly origin_id: NodeId
+ /** Output slot index */
+ readonly origin_slot: number
+ /** Input node ID */
+ readonly target_id: NodeId
+ /** Input slot index */
+ readonly target_slot: number
+}
+
/** Contains a cached 2D canvas path and a centre point, with an optional forward angle. */
export interface LinkSegment {
/** Link / reroute ID */
@@ -209,6 +224,9 @@ export interface LinkSegment {
readonly origin_id: NodeId | undefined
/** Output slot index */
readonly origin_slot: number | undefined
+
+ /** Optional data attached to the link for tooltip display */
+ data?: number | string | boolean | { toToolTip?(): string }
}
interface IInputOrOutput {
diff --git a/src/lib/litegraph/src/litegraph.ts b/src/lib/litegraph/src/litegraph.ts
index 0b24eb47b80..2e7baca1adb 100644
--- a/src/lib/litegraph/src/litegraph.ts
+++ b/src/lib/litegraph/src/litegraph.ts
@@ -103,10 +103,12 @@ export type {
Size
} from './interfaces'
export {
+ type GroupNodeWorkflowData,
LGraph,
type LGraphTriggerAction,
type LGraphTriggerParam
} from './LGraph'
+
export type { LGraphTriggerEvent } from './types/graphTriggers'
export { BadgePosition, LGraphBadge } from './LGraphBadge'
export { LGraphCanvas } from './LGraphCanvas'
diff --git a/src/lib/litegraph/src/node/NodeSlot.test.ts b/src/lib/litegraph/src/node/NodeSlot.test.ts
index 6971736fc02..49f24986003 100644
--- a/src/lib/litegraph/src/node/NodeSlot.test.ts
+++ b/src/lib/litegraph/src/node/NodeSlot.test.ts
@@ -13,22 +13,22 @@ import type { ReadOnlyRect } from '@/lib/litegraph/src/interfaces'
const boundingRect: ReadOnlyRect = [0, 0, 10, 10]
describe('NodeSlot', () => {
- describe('inputAsSerialisable', () => {
+ describe('outputAsSerialisable', () => {
it('removes _data from serialized slot', () => {
- const slot: INodeOutputSlot = {
+ const slot: INodeOutputSlot & { _data: string } = {
_data: 'test data',
name: 'test-id',
type: 'STRING',
links: [],
boundingRect
}
- // @ts-expect-error Argument type mismatch for test
const serialized = outputAsSerialisable(slot)
expect(serialized).not.toHaveProperty('_data')
})
+ })
+ describe('inputAsSerialisable', () => {
it('removes pos from widget input slots', () => {
- // Minimal slot for serialization test - boundingRect is calculated at runtime, not serialized
const widgetInputSlot: INodeInputSlot = {
name: 'test-id',
pos: [10, 20],
@@ -55,7 +55,6 @@ describe('NodeSlot', () => {
})
it('preserves only widget name during serialization', () => {
- // Extra widget properties simulate real data that should be stripped during serialization
const widgetInputSlot: INodeInputSlot = {
name: 'test-id',
type: 'STRING',
diff --git a/src/lib/litegraph/src/node/NodeSlot.ts b/src/lib/litegraph/src/node/NodeSlot.ts
index 4e17069b28e..6d6196f3def 100644
--- a/src/lib/litegraph/src/node/NodeSlot.ts
+++ b/src/lib/litegraph/src/node/NodeSlot.ts
@@ -74,12 +74,12 @@ export abstract class NodeSlot extends SlotBase implements INodeSlot {
slot: OptionalProps,
node: LGraphNode
) {
- // @ts-expect-error Workaround: Ensure internal properties are not copied to the slot (_listenerController
+ // Workaround: Ensure internal properties are not copied to the slot
// https://github.com/Comfy-Org/litegraph.js/issues/1138
- const maybeSubgraphSlot: OptionalProps<
+ const maybeSubgraphSlot = slot as OptionalProps<
ISubgraphInput,
'link' | 'boundingRect'
- > = slot
+ > & { _listenerController?: unknown }
const { boundingRect, name, type, _listenerController, ...rest } =
maybeSubgraphSlot
const rectangle = boundingRect
diff --git a/src/lib/litegraph/src/node/slotUtils.ts b/src/lib/litegraph/src/node/slotUtils.ts
index 712637040a2..66d08296954 100644
--- a/src/lib/litegraph/src/node/slotUtils.ts
+++ b/src/lib/litegraph/src/node/slotUtils.ts
@@ -5,8 +5,7 @@ import type {
import type {
INodeInputSlot,
INodeOutputSlot,
- INodeSlot,
- IWidget
+ INodeSlot
} from '@/lib/litegraph/src/litegraph'
import type {
ISerialisableNodeInput,
@@ -63,7 +62,7 @@ export function inputAsSerialisable(
}
export function outputAsSerialisable(
- slot: INodeOutputSlot & { widget?: IWidget }
+ slot: INodeOutputSlot & { widget?: { name: string } }
): ISerialisableNodeOutput {
const { pos, slot_index, links, widget } = slot
// Output widgets do not exist in Litegraph; this is a temporary downstream workaround.
diff --git a/src/lib/litegraph/src/polyfills.ts b/src/lib/litegraph/src/polyfills.ts
index 488e2478839..4b4dca8742b 100644
--- a/src/lib/litegraph/src/polyfills.ts
+++ b/src/lib/litegraph/src/polyfills.ts
@@ -1,7 +1,24 @@
-// @ts-expect-error Polyfill
-Symbol.dispose ??= Symbol('Symbol.dispose')
-// @ts-expect-error Polyfill
-Symbol.asyncDispose ??= Symbol('Symbol.asyncDispose')
+// Polyfill Symbol.dispose and Symbol.asyncDispose for environments that don't support them
+// These are well-known symbols added in ES2024 for explicit resource management
+
+// Use a separate reference to Symbol constructor for creating new symbols
+// This avoids TypeScript narrowing issues inside the conditional blocks
+const SymbolCtor: (description?: string) => symbol = Symbol
+
+if (!('dispose' in Symbol)) {
+ Object.defineProperty(Symbol, 'dispose', {
+ value: SymbolCtor('Symbol.dispose'),
+ writable: false,
+ configurable: false
+ })
+}
+if (!('asyncDispose' in Symbol)) {
+ Object.defineProperty(Symbol, 'asyncDispose', {
+ value: SymbolCtor('Symbol.asyncDispose'),
+ writable: false,
+ configurable: false
+ })
+}
// API *************************************************
// like rect but rounded corners
@@ -11,14 +28,15 @@ export function loadPolyfills() {
window.CanvasRenderingContext2D &&
!window.CanvasRenderingContext2D.prototype.roundRect
) {
- // @ts-expect-error Slightly broken polyfill - radius_low not impl. anywhere
- window.CanvasRenderingContext2D.prototype.roundRect = function (
+ // Legacy polyfill for roundRect with additional radius_low parameter (non-standard)
+ const roundRectPolyfill = function (
+ this: CanvasRenderingContext2D,
x: number,
y: number,
w: number,
h: number,
radius: number | number[],
- radius_low: number | number[]
+ radius_low?: number | number[]
) {
let top_left_radius = 0
let top_right_radius = 0
@@ -78,16 +96,23 @@ export function loadPolyfills() {
this.lineTo(x, y + bottom_left_radius)
this.quadraticCurveTo(x, y, x + top_left_radius, y)
}
+
+ // Assign the polyfill, casting to handle the slightly different signature
+ window.CanvasRenderingContext2D.prototype.roundRect =
+ roundRectPolyfill as CanvasRenderingContext2D['roundRect']
}
- if (typeof window != 'undefined' && !window['requestAnimationFrame']) {
+ // Legacy requestAnimationFrame polyfill for older browsers
+ if (typeof window != 'undefined' && !window.requestAnimationFrame) {
+ const win = window as Window & {
+ webkitRequestAnimationFrame?: typeof requestAnimationFrame
+ mozRequestAnimationFrame?: typeof requestAnimationFrame
+ }
window.requestAnimationFrame =
- // @ts-expect-error Legacy code
- window.webkitRequestAnimationFrame ||
- // @ts-expect-error Legacy code
- window.mozRequestAnimationFrame ||
+ win.webkitRequestAnimationFrame ||
+ win.mozRequestAnimationFrame ||
function (callback) {
- window.setTimeout(callback, 1000 / 60)
+ return window.setTimeout(callback, 1000 / 60)
}
}
}
diff --git a/src/lib/litegraph/src/subgraph/ExecutableNodeDTO.ts b/src/lib/litegraph/src/subgraph/ExecutableNodeDTO.ts
index 9e9454a817b..fdb1e6547bd 100644
--- a/src/lib/litegraph/src/subgraph/ExecutableNodeDTO.ts
+++ b/src/lib/litegraph/src/subgraph/ExecutableNodeDTO.ts
@@ -9,7 +9,11 @@ import type {
CallbackReturn,
ISlotType
} from '@/lib/litegraph/src/interfaces'
-import { LGraphEventMode, LiteGraph } from '@/lib/litegraph/src/litegraph'
+import {
+ LGraphEventMode,
+ LiteGraph,
+ LLink
+} from '@/lib/litegraph/src/litegraph'
import type { Subgraph } from './Subgraph'
import type { SubgraphNode } from './SubgraphNode'
@@ -289,7 +293,7 @@ export class ExecutableNodeDTO implements ExecutableLGraphNode {
if (node.isVirtualNode) {
const virtualLink = this.node.getInputLink(slot)
- if (virtualLink) {
+ if (virtualLink instanceof LLink) {
const { inputNode } = virtualLink.resolve(this.graph)
if (!inputNode)
throw new InvalidLinkError(
diff --git a/src/lib/litegraph/src/subgraph/Subgraph.test.ts b/src/lib/litegraph/src/subgraph/Subgraph.test.ts
index ecd939654bb..8e1b9e357d5 100644
--- a/src/lib/litegraph/src/subgraph/Subgraph.test.ts
+++ b/src/lib/litegraph/src/subgraph/Subgraph.test.ts
@@ -46,8 +46,7 @@ describe.skip('Subgraph Construction', () => {
const subgraphData = createTestSubgraphData()
expect(() => {
- // @ts-expect-error Testing invalid null parameter
- new Subgraph(null, subgraphData)
+ new Subgraph(null as never, subgraphData)
}).toThrow('Root graph is required')
})
diff --git a/src/lib/litegraph/src/subgraph/SubgraphNode.titleButton.test.ts b/src/lib/litegraph/src/subgraph/SubgraphNode.titleButton.test.ts
index c76e9d5ee04..4bae0da4d0e 100644
--- a/src/lib/litegraph/src/subgraph/SubgraphNode.titleButton.test.ts
+++ b/src/lib/litegraph/src/subgraph/SubgraphNode.titleButton.test.ts
@@ -136,8 +136,7 @@ describe.skip('SubgraphNode Title Button', () => {
80 - subgraphNode.pos[1] // 80 - 100 = -20
]
- // @ts-expect-error onMouseDown possibly undefined
- const handled = subgraphNode.onMouseDown(
+ const handled = subgraphNode.onMouseDown?.(
event,
clickPosRelativeToNode,
canvas
@@ -173,8 +172,7 @@ describe.skip('SubgraphNode Title Button', () => {
150 - subgraphNode.pos[1] // 150 - 100 = 50
]
- // @ts-expect-error onMouseDown possibly undefined
- const handled = subgraphNode.onMouseDown(
+ const handled = subgraphNode.onMouseDown?.(
event,
clickPosRelativeToNode,
canvas
@@ -220,8 +218,7 @@ describe.skip('SubgraphNode Title Button', () => {
80 - subgraphNode.pos[1] // -20
]
- // @ts-expect-error onMouseDown possibly undefined
- const handled = subgraphNode.onMouseDown(
+ const handled = subgraphNode.onMouseDown?.(
event,
clickPosRelativeToNode,
canvas
diff --git a/src/lib/litegraph/src/subgraph/SubgraphSerialization.test.ts b/src/lib/litegraph/src/subgraph/SubgraphSerialization.test.ts
index f773fe63d54..31e6a2822da 100644
--- a/src/lib/litegraph/src/subgraph/SubgraphSerialization.test.ts
+++ b/src/lib/litegraph/src/subgraph/SubgraphSerialization.test.ts
@@ -8,12 +8,36 @@
import { describe, expect, it } from 'vitest'
import { LGraph, Subgraph } from '@/lib/litegraph/src/litegraph'
+import type { ExportedSubgraph } from '@/lib/litegraph/src/types/serialisation'
import {
createTestSubgraph,
createTestSubgraphNode
} from './__fixtures__/subgraphHelpers'
+function createTestExportedSubgraph(
+ overrides: Partial
+): ExportedSubgraph {
+ return {
+ version: 1,
+ revision: 0,
+ state: { lastGroupId: 0, lastNodeId: 0, lastLinkId: 0, lastRerouteId: 0 },
+ id: 'test-id',
+ name: 'Test Subgraph',
+ nodes: [],
+ links: [],
+ groups: [],
+ config: {},
+ definitions: { subgraphs: [] },
+ inputs: [],
+ outputs: [],
+ inputNode: { id: -10, bounding: [0, 0, 120, 60] },
+ outputNode: { id: -20, bounding: [300, 0, 120, 60] },
+ widgets: [],
+ ...overrides
+ }
+}
+
describe.skip('SubgraphSerialization - Basic Serialization', () => {
it('should save and load simple subgraphs', () => {
const original = createTestSubgraph({
@@ -76,8 +100,6 @@ describe.skip('SubgraphSerialization - Basic Serialization', () => {
// Verify core properties
expect(restored.id).toBe(original.id)
expect(restored.name).toBe(original.name)
- // @ts-expect-error description property not in type definition
- expect(restored.description).toBe(original.description)
// Verify I/O structure
expect(restored.inputs.length).toBe(original.inputs.length)
@@ -229,30 +251,14 @@ describe.skip('SubgraphSerialization - Version Compatibility', () => {
})
it('should load version 1.0+ format', () => {
- const modernFormat = {
- version: 1, // Number as expected by current implementation
+ const modernFormat = createTestExportedSubgraph({
id: 'test-modern-id',
name: 'Modern Subgraph',
- nodes: [],
- links: {},
- groups: [],
- config: {},
- definitions: { subgraphs: [] },
inputs: [{ id: 'input-id', name: 'modern_input', type: 'number' }],
- outputs: [{ id: 'output-id', name: 'modern_output', type: 'string' }],
- inputNode: {
- id: -10,
- bounding: [0, 0, 120, 60]
- },
- outputNode: {
- id: -20,
- bounding: [300, 0, 120, 60]
- },
- widgets: []
- }
+ outputs: [{ id: 'output-id', name: 'modern_output', type: 'string' }]
+ })
expect(() => {
- // @ts-expect-error Type mismatch in ExportedSubgraph format
const subgraph = new Subgraph(new LGraph(), modernFormat)
expect(subgraph.name).toBe('Modern Subgraph')
expect(subgraph.inputs.length).toBe(1)
@@ -261,28 +267,15 @@ describe.skip('SubgraphSerialization - Version Compatibility', () => {
})
it('should handle missing fields gracefully', () => {
- const incompleteFormat = {
- version: 1,
+ const incompleteFormat = createTestExportedSubgraph({
id: 'incomplete-id',
name: 'Incomplete Subgraph',
- nodes: [],
- links: {},
- groups: [],
- config: {},
- definitions: { subgraphs: [] },
- inputNode: {
- id: -10,
- bounding: [0, 0, 120, 60]
- },
- outputNode: {
- id: -20,
- bounding: [300, 0, 120, 60]
- }
- // Missing optional: inputs, outputs, widgets
- }
+ inputs: undefined,
+ outputs: undefined,
+ widgets: undefined
+ })
expect(() => {
- // @ts-expect-error Type mismatch in ExportedSubgraph format
const subgraph = new Subgraph(new LGraph(), incompleteFormat)
expect(subgraph.name).toBe('Incomplete Subgraph')
// Should have default empty arrays
@@ -292,33 +285,23 @@ describe.skip('SubgraphSerialization - Version Compatibility', () => {
})
it('should consider future-proofing', () => {
- const futureFormat = {
- version: 2, // Future version (number)
+ const futureFormat = createTestExportedSubgraph({
id: 'future-id',
- name: 'Future Subgraph',
- nodes: [],
- links: {},
- groups: [],
- config: {},
- definitions: { subgraphs: [] },
- inputs: [],
- outputs: [],
- inputNode: {
- id: -10,
- bounding: [0, 0, 120, 60]
- },
- outputNode: {
- id: -20,
- bounding: [300, 0, 120, 60]
- },
- widgets: [],
- futureFeature: 'unknown_data' // Unknown future field
- }
+ name: 'Future Subgraph'
+ })
+ // Test with unknown future fields - simulating a hypothetical future version.
+ // NOTE: The "as unknown as ExportedSubgraph" assertion below is an intentional
+ // exception to normal type-safety rules. It simulates a future schema/version
+ // to validate deserialization resilience against forward-compatible data.
+ const extendedFormat = {
+ ...futureFormat,
+ version: 2 as const,
+ futureFeature: 'unknown_data'
+ } as unknown as ExportedSubgraph
// Should handle future format gracefully
expect(() => {
- // @ts-expect-error Type mismatch in ExportedSubgraph format
- const subgraph = new Subgraph(new LGraph(), futureFormat)
+ const subgraph = new Subgraph(new LGraph(), extendedFormat)
expect(subgraph.name).toBe('Future Subgraph')
}).not.toThrow()
})
diff --git a/src/lib/litegraph/src/subgraph/SubgraphWidgetPromotion.test.ts b/src/lib/litegraph/src/subgraph/SubgraphWidgetPromotion.test.ts
index d47ca186d9d..909fdd15536 100644
--- a/src/lib/litegraph/src/subgraph/SubgraphWidgetPromotion.test.ts
+++ b/src/lib/litegraph/src/subgraph/SubgraphWidgetPromotion.test.ts
@@ -7,6 +7,14 @@ import type {
TWidgetType
} from '@/lib/litegraph/src/litegraph'
import { BaseWidget, LGraphNode } from '@/lib/litegraph/src/litegraph'
+import type {
+ DrawWidgetOptions,
+ WidgetEventOptions
+} from '@/lib/litegraph/src/widgets/BaseWidget'
+import type {
+ IBaseWidget,
+ TWidgetValue
+} from '@/lib/litegraph/src/types/widgets'
import {
createEventCapture,
@@ -14,11 +22,47 @@ import {
createTestSubgraphNode
} from './__fixtures__/subgraphHelpers'
+/** Concrete test implementation of abstract BaseWidget */
+class TestWidget extends BaseWidget {
+ constructor(options: {
+ name: string
+ type: TWidgetType
+ value: TWidgetValue
+ y: number
+ options: Record
+ node: LGraphNode
+ tooltip?: string
+ }) {
+ super(
+ {
+ name: options.name,
+ type: options.type,
+ value: options.value,
+ y: options.y,
+ options: options.options,
+ tooltip: options.tooltip
+ } as IBaseWidget,
+ options.node
+ )
+ }
+
+ drawWidget(
+ _ctx: CanvasRenderingContext2D,
+ _options: DrawWidgetOptions
+ ): void {
+ // No-op for test
+ }
+
+ onClick(_options: WidgetEventOptions): void {
+ // No-op for test
+ }
+}
+
// Helper to create a node with a widget
function createNodeWithWidget(
title: string,
widgetType: TWidgetType = 'number',
- widgetValue: any = 42,
+ widgetValue: TWidgetValue = 42,
slotType: ISlotType = 'number',
tooltip?: string
) {
@@ -26,8 +70,7 @@ function createNodeWithWidget(
const input = node.addInput('value', slotType)
node.addOutput('out', slotType)
- // @ts-expect-error Abstract class instantiation
- const widget = new BaseWidget({
+ const widget = new TestWidget({
name: 'widget',
type: widgetType,
value: widgetValue,
@@ -181,8 +224,7 @@ describe.skip('SubgraphWidgetPromotion', () => {
const numInput = multiWidgetNode.addInput('num', 'number')
const strInput = multiWidgetNode.addInput('str', 'string')
- // @ts-expect-error Abstract class instantiation
- const widget1 = new BaseWidget({
+ const widget1 = new TestWidget({
name: 'widget1',
type: 'number',
value: 10,
@@ -191,8 +233,7 @@ describe.skip('SubgraphWidgetPromotion', () => {
node: multiWidgetNode
})
- // @ts-expect-error Abstract class instantiation
- const widget2 = new BaseWidget({
+ const widget2 = new TestWidget({
name: 'widget2',
type: 'string',
value: 'hello',
@@ -331,8 +372,7 @@ describe.skip('SubgraphWidgetPromotion', () => {
const numInput = multiWidgetNode.addInput('num', 'number')
const strInput = multiWidgetNode.addInput('str', 'string')
- // @ts-expect-error Abstract class instantiation
- const widget1 = new BaseWidget({
+ const widget1 = new TestWidget({
name: 'widget1',
type: 'number',
value: 10,
@@ -342,8 +382,7 @@ describe.skip('SubgraphWidgetPromotion', () => {
tooltip: 'Number widget tooltip'
})
- // @ts-expect-error Abstract class instantiation
- const widget2 = new BaseWidget({
+ const widget2 = new TestWidget({
name: 'widget2',
type: 'string',
value: 'hello',
diff --git a/src/lib/litegraph/src/widgets/BaseWidget.ts b/src/lib/litegraph/src/widgets/BaseWidget.ts
index a580fc69a61..f96b683f5bf 100644
--- a/src/lib/litegraph/src/widgets/BaseWidget.ts
+++ b/src/lib/litegraph/src/widgets/BaseWidget.ts
@@ -120,29 +120,33 @@ export abstract class BaseWidget<
// `node` has no setter - Object.assign will throw.
// TODO: Resolve this workaround. Ref: https://github.com/Comfy-Org/litegraph.js/issues/1022
+ // Destructure known properties that could conflict with class getters/properties.
+ // These are typed as `unknown` to handle custom widgets that may include them.
const {
node: _,
- // @ts-expect-error Prevent naming conflicts with custom nodes.
- outline_color,
- // @ts-expect-error Prevent naming conflicts with custom nodes.
- background_color,
- // @ts-expect-error Prevent naming conflicts with custom nodes.
- height,
- // @ts-expect-error Prevent naming conflicts with custom nodes.
- text_color,
- // @ts-expect-error Prevent naming conflicts with custom nodes.
- secondary_text_color,
- // @ts-expect-error Prevent naming conflicts with custom nodes.
- disabledTextColor,
- // @ts-expect-error Prevent naming conflicts with custom nodes.
- displayName,
- // @ts-expect-error Prevent naming conflicts with custom nodes.
- displayValue,
- // @ts-expect-error Prevent naming conflicts with custom nodes.
- labelBaseline,
+ outline_color: _outline_color,
+ background_color: _background_color,
+ height: _height,
+ text_color: _text_color,
+ secondary_text_color: _secondary_text_color,
+ disabledTextColor: _disabledTextColor,
+ displayName: _displayName,
+ displayValue: _displayValue,
+ labelBaseline: _labelBaseline,
promoted,
...safeValues
- } = widget
+ } = widget as TWidget & {
+ node: LGraphNode
+ outline_color?: unknown
+ background_color?: unknown
+ height?: unknown
+ text_color?: unknown
+ secondary_text_color?: unknown
+ disabledTextColor?: unknown
+ displayName?: unknown
+ displayValue?: unknown
+ labelBaseline?: unknown
+ }
Object.assign(this, safeValues)
}
@@ -341,8 +345,11 @@ export abstract class BaseWidget<
* Correctly and safely typing this is currently not possible (practical?) in TypeScript 5.8.
*/
createCopyForNode(node: LGraphNode): this {
- // @ts-expect-error - Constructor type casting for widget cloning
- const cloned: this = new (this.constructor as typeof this)(this, node)
+ const WidgetConstructor = this.constructor as new (
+ widget: TWidget,
+ node: LGraphNode
+ ) => this
+ const cloned = new WidgetConstructor(this as unknown as TWidget, node)
cloned.value = this.value
return cloned
}
diff --git a/src/lib/litegraph/src/widgets/ComboWidget.ts b/src/lib/litegraph/src/widgets/ComboWidget.ts
index 2be40749941..6f46828e4dc 100644
--- a/src/lib/litegraph/src/widgets/ComboWidget.ts
+++ b/src/lib/litegraph/src/widgets/ComboWidget.ts
@@ -112,11 +112,12 @@ export class ComboWidget
// avoids double click event
options.canvas.last_mouseclick = 0
+ // Handle both string and non-string values for indexOf lookup
+ const currentValue = this.value
const foundIndex =
typeof values === 'object'
- ? indexedValues.indexOf(String(this.value)) + delta
- : // @ts-expect-error handle non-string values
- indexedValues.indexOf(this.value) + delta
+ ? indexedValues.indexOf(String(currentValue)) + delta
+ : indexedValues.indexOf(currentValue as string) + delta
const index = clamp(foundIndex, 0, indexedValues.length - 1)
diff --git a/src/platform/settings/components/SettingItem.vue b/src/platform/settings/components/SettingItem.vue
index 91f92b2e94f..e569d16736e 100644
--- a/src/platform/settings/components/SettingItem.vue
+++ b/src/platform/settings/components/SettingItem.vue
@@ -40,11 +40,16 @@ const props = defineProps<{
}>()
const { t } = useI18n()
-function translateOptions(options: (SettingOption | string)[]) {
+const settingStore = useSettingStore()
+const settingValue = computed(() => settingStore.get(props.setting.id))
+
+function translateOptions(
+ options:
+ | (SettingOption | string)[]
+ | ((value: unknown) => (SettingOption | string)[])
+) {
if (typeof options === 'function') {
- // @ts-expect-error: Audit and deprecate usage of legacy options type:
- // (value) => [string | {text: string, value: string}]
- return translateOptions(options(props.setting.value ?? ''))
+ return translateOptions(options(settingValue.value ?? ''))
}
return options.map((option) => {
@@ -75,8 +80,6 @@ const formItem = computed(() => {
}
})
-const settingStore = useSettingStore()
-const settingValue = computed(() => settingStore.get(props.setting.id))
const updateSettingValue = async (
newValue: Settings[K]
) => {
diff --git a/src/platform/settings/types.ts b/src/platform/settings/types.ts
index 021fac3750e..e25986d423a 100644
--- a/src/platform/settings/types.ts
+++ b/src/platform/settings/types.ts
@@ -49,6 +49,12 @@ export interface SettingParams extends FormItem {
hideInVueNodes?: boolean
}
+/**
+ * Legacy options function type for dynamic options.
+ * @deprecated Use static options array instead.
+ */
+type LegacyOptionsFunction = (value: unknown) => Array
+
/**
* The base form item for rendering in a form.
*/
@@ -57,7 +63,7 @@ export interface FormItem {
type: SettingInputType | SettingCustomRenderer
tooltip?: string
attrs?: Record
- options?: Array
+ options?: Array | LegacyOptionsFunction
}
export interface ISettingGroup {
diff --git a/src/platform/updates/common/useFrontendVersionMismatchWarning.test.ts b/src/platform/updates/common/useFrontendVersionMismatchWarning.test.ts
index 11a7fcbb07d..f86d6ea5439 100644
--- a/src/platform/updates/common/useFrontendVersionMismatchWarning.test.ts
+++ b/src/platform/updates/common/useFrontendVersionMismatchWarning.test.ts
@@ -6,8 +6,11 @@ import { useToastStore } from '@/platform/updates/common/toastStore'
import { useFrontendVersionMismatchWarning } from '@/platform/updates/common/useFrontendVersionMismatchWarning'
import { useVersionCompatibilityStore } from '@/platform/updates/common/versionCompatibilityStore'
+declare const global: typeof globalThis & {
+ __COMFYUI_FRONTEND_VERSION__: string
+}
+
// Mock globals
-//@ts-expect-error Define global for the test
global.__COMFYUI_FRONTEND_VERSION__ = '1.0.0'
// Mock config first - this needs to be before any imports
diff --git a/src/platform/workflow/validation/schemas/workflowSchema.test.ts b/src/platform/workflow/validation/schemas/workflowSchema.test.ts
index b075e3ebe26..c0e9aabf665 100644
--- a/src/platform/workflow/validation/schemas/workflowSchema.test.ts
+++ b/src/platform/workflow/validation/schemas/workflowSchema.test.ts
@@ -69,13 +69,13 @@ describe('parseComfyWorkflow', () => {
// Should automatically transform the legacy format object to array.
workflow.nodes[0].pos = { '0': 3, '1': 4 }
let validatedWorkflow = await validateComfyWorkflow(workflow)
- // @ts-expect-error fixme ts strict error
- expect(validatedWorkflow.nodes[0].pos).toEqual([3, 4])
+ expect(validatedWorkflow).not.toBeNull()
+ expect(validatedWorkflow!.nodes[0].pos).toEqual([3, 4])
workflow.nodes[0].pos = { 0: 3, 1: 4 }
validatedWorkflow = await validateComfyWorkflow(workflow)
- // @ts-expect-error fixme ts strict error
- expect(validatedWorkflow.nodes[0].pos).toEqual([3, 4])
+ expect(validatedWorkflow).not.toBeNull()
+ expect(validatedWorkflow!.nodes[0].pos).toEqual([3, 4])
// Should accept the legacy bugged format object.
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/710
@@ -92,8 +92,8 @@ describe('parseComfyWorkflow', () => {
'9': 0
}
validatedWorkflow = await validateComfyWorkflow(workflow)
- // @ts-expect-error fixme ts strict error
- expect(validatedWorkflow.nodes[0].pos).toEqual([600, 340])
+ expect(validatedWorkflow).not.toBeNull()
+ expect(validatedWorkflow!.nodes[0].pos).toEqual([600, 340])
})
it('workflow.nodes.widget_values', async () => {
@@ -111,8 +111,8 @@ describe('parseComfyWorkflow', () => {
// dynamic widgets display.
workflow.nodes[0].widgets_values = { foo: 'bar' }
const validatedWorkflow = await validateComfyWorkflow(workflow)
- // @ts-expect-error fixme ts strict error
- expect(validatedWorkflow.nodes[0].widgets_values).toEqual({ foo: 'bar' })
+ expect(validatedWorkflow).not.toBeNull()
+ expect(validatedWorkflow!.nodes[0].widgets_values).toEqual({ foo: 'bar' })
})
it('workflow.links', async () => {
diff --git a/src/renderer/core/layout/store/layoutStore.test.ts b/src/renderer/core/layout/store/layoutStore.test.ts
index ff7ee1fa06b..fdbc8ef77ba 100644
--- a/src/renderer/core/layout/store/layoutStore.test.ts
+++ b/src/renderer/core/layout/store/layoutStore.test.ts
@@ -383,8 +383,8 @@ describe('layoutStore CRDT operations', () => {
})
const originalTitleHeight = LiteGraph.NODE_TITLE_HEIGHT
- // @ts-expect-error – intentionally simulate undefined runtime value
- LiteGraph.NODE_TITLE_HEIGHT = undefined
+ // Intentionally simulate undefined runtime value via Object.assign
+ Object.assign(LiteGraph, { NODE_TITLE_HEIGHT: undefined })
try {
layoutStore.setSource(LayoutSource.DOM)
diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetLegacy.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetLegacy.vue
index d4989a5e359..670a85480f4 100644
--- a/src/renderer/extensions/vueNodes/widgets/components/WidgetLegacy.vue
+++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetLegacy.vue
@@ -64,7 +64,6 @@ function draw() {
}
containerHeight.value = height
// Set node.canvasHeight for legacy widgets that use it (e.g., Impact Pack)
- // @ts-expect-error canvasHeight is a custom property used by some extensions
node.canvasHeight = height
widgetInstance.y = 0
canvasEl.value.height = (height + 2) * scaleFactor
diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetMarkdown.test.ts b/src/renderer/extensions/vueNodes/widgets/components/WidgetMarkdown.test.ts
index ee788cc4db0..4930601c1ed 100644
--- a/src/renderer/extensions/vueNodes/widgets/components/WidgetMarkdown.test.ts
+++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetMarkdown.test.ts
@@ -376,13 +376,10 @@ Another line with more content.`
const vm = wrapper.vm as InstanceType
// Test that the component creates a textarea reference when entering edit mode
- // @ts-expect-error - isEditing is not exposed
expect(vm.isEditing).toBe(false)
- // @ts-expect-error - startEditing is not exposed
await vm.startEditing()
- // @ts-expect-error - isEditing is not exposed
expect(vm.isEditing).toBe(true)
await wrapper.vm.$nextTick()
diff --git a/src/renderer/extensions/vueNodes/widgets/components/WidgetMarkdown.vue b/src/renderer/extensions/vueNodes/widgets/components/WidgetMarkdown.vue
index 28247859ed0..39666ddf411 100644
--- a/src/renderer/extensions/vueNodes/widgets/components/WidgetMarkdown.vue
+++ b/src/renderer/extensions/vueNodes/widgets/components/WidgetMarkdown.vue
@@ -29,6 +29,7 @@
diff --git a/src/renderer/extensions/vueNodes/widgets/composables/useImageUploadWidget.ts b/src/renderer/extensions/vueNodes/widgets/composables/useImageUploadWidget.ts
index 78f9d0c97d7..1a776569bb9 100644
--- a/src/renderer/extensions/vueNodes/widgets/composables/useImageUploadWidget.ts
+++ b/src/renderer/extensions/vueNodes/widgets/composables/useImageUploadWidget.ts
@@ -18,14 +18,16 @@ const ACCEPTED_VIDEO_TYPES = 'video/webm,video/mp4'
type InternalFile = string | ResultItem
type InternalValue = InternalFile | InternalFile[]
type ExposedValue = string | string[]
+type WritableComboWidget = Omit & { value: ExposedValue }
const isImageFile = (file: File) => file.type.startsWith('image/')
const isVideoFile = (file: File) => file.type.startsWith('video/')
-const findFileComboWidget = (node: LGraphNode, inputName: string) =>
- node.widgets!.find((w) => w.name === inputName) as IComboWidget & {
- value: ExposedValue
- }
+const findFileComboWidget = (
+ node: LGraphNode,
+ inputName: string
+): IComboWidget | undefined =>
+ node.widgets?.find((w): w is IComboWidget => w.name === inputName)
export const useImageUploadWidget = () => {
const widgetConstructor: ComfyWidgetConstructor = (
@@ -51,6 +53,9 @@ export const useImageUploadWidget = () => {
const fileFilter = isVideo ? isVideoFile : isImageFile
const fileComboWidget = findFileComboWidget(node, imageInputName)
+ if (!fileComboWidget) {
+ throw new Error(`Widget "${imageInputName}" not found on node`)
+ }
const initialFile = `${fileComboWidget.value}`
const formatPath = (value: InternalFile) =>
createAnnotatedPath(value, { rootFolder: image_folder })
@@ -80,10 +85,9 @@ export const useImageUploadWidget = () => {
output.forEach((path) => addToComboValues(fileComboWidget, path))
// Create a NEW array to ensure Vue reactivity detects the change
- const newValue = allow_batch ? [...output] : output[0]
-
- // @ts-expect-error litegraph combo value type does not support arrays yet
- fileComboWidget.value = newValue
+ // Value property is redefined via Object.defineProperty to support batch uploads
+ const newValue: ExposedValue = allow_batch ? [...output] : output[0]
+ ;(fileComboWidget as unknown as WritableComboWidget).value = newValue
fileComboWidget.callback?.(newValue)
}
})
@@ -103,9 +107,9 @@ export const useImageUploadWidget = () => {
// Add our own callback to the combo widget to render an image when it changes
fileComboWidget.callback = function () {
- nodeOutputStore.setNodeOutputs(node, fileComboWidget.value, {
- isAnimated
- })
+ // Image upload widget value is always a string path, never a number
+ const value = fileComboWidget.value as string | string[]
+ nodeOutputStore.setNodeOutputs(node, value, { isAnimated })
node.graph?.setDirtyCanvas(true)
}
@@ -113,9 +117,8 @@ export const useImageUploadWidget = () => {
// The value isn't set immediately so we need to wait a moment
// No change callbacks seem to be fired on initial setting of the value
requestAnimationFrame(() => {
- nodeOutputStore.setNodeOutputs(node, fileComboWidget.value, {
- isAnimated
- })
+ const value = fileComboWidget.value as string | string[]
+ nodeOutputStore.setNodeOutputs(node, value, { isAnimated })
showPreview({ block: false })
})
diff --git a/src/schemas/nodeDef/migration.test.ts b/src/schemas/nodeDef/migration.test.ts
index 7250918f118..b087ae94687 100644
--- a/src/schemas/nodeDef/migration.test.ts
+++ b/src/schemas/nodeDef/migration.test.ts
@@ -221,9 +221,9 @@ describe('NodeDef Migration', () => {
}
const result = transformNodeDefV1ToV2(nodeDef)
+ const inputWithHidden = plainObject as { hidden?: Record }
- // @ts-expect-error fixme ts strict error
- expect(result.hidden).toEqual(plainObject.hidden)
+ expect(result.hidden).toEqual(inputWithHidden.hidden)
expect(result.hidden?.someHiddenValue).toBe(42)
expect(result.hidden?.anotherHiddenValue).toEqual({ nested: 'object' })
})
diff --git a/src/schemas/nodeDefSchema.validation.test.ts b/src/schemas/nodeDefSchema.validation.test.ts
index 707eabd3e4a..b313650ff68 100644
--- a/src/schemas/nodeDefSchema.validation.test.ts
+++ b/src/schemas/nodeDefSchema.validation.test.ts
@@ -37,15 +37,13 @@ describe('validateNodeDef', () => {
'validateComfyNodeDef with various input spec formats',
(inputSpec, expected) => {
it(`should accept input spec format: ${JSON.stringify(inputSpec)}`, async () => {
- expect(
- // @ts-expect-error fixme ts strict error
- validateComfyNodeDef({
- ...EXAMPLE_NODE_DEF,
- input: {
- required: inputSpec
- }
- }).input.required.ckpt_name
- ).toEqual(expected)
+ const result = validateComfyNodeDef({
+ ...EXAMPLE_NODE_DEF,
+ input: {
+ required: inputSpec
+ }
+ })
+ expect(result?.input?.required?.ckpt_name).toEqual(expected)
})
}
)
diff --git a/src/scripts/app.ts b/src/scripts/app.ts
index edcc64517e0..1a258f6a4a4 100644
--- a/src/scripts/app.ts
+++ b/src/scripts/app.ts
@@ -37,6 +37,7 @@ import type {
NodeExecutionOutput,
ResultItem
} from '@/schemas/apiSchema'
+import type { WorkflowAsGraph } from '@/types/workflowSchemaTypes'
import {
type ComfyNodeDef as ComfyNodeDefV1,
isComboInputSpecV1,
@@ -149,7 +150,7 @@ export class ComfyApp {
static clipspace_invalidate_handler: (() => void) | null = null
static open_maskeditor: (() => void) | null = null
static maskeditor_is_opended: (() => void) | null = null
- static clipspace_return_node = null
+ static clipspace_return_node: LGraphNode | null = null
vueAppReady: boolean
api: ComfyApi
@@ -162,8 +163,8 @@ export class ComfyApp {
// TODO: Migrate internal usage to the
/** @deprecated Use {@link rootGraph} instead */
- get graph(): unknown {
- return this.rootGraphInternal!
+ get graph(): LGraph | undefined {
+ return this.rootGraphInternal
}
get rootGraph(): LGraph {
@@ -1194,8 +1195,8 @@ export class ComfyApp {
}
try {
- // @ts-expect-error Discrepancies between zod and litegraph - in progress
- this.rootGraph.configure(graphData)
+ // Type cast: ComfyWorkflowJSON → WorkflowAsGraph (see WorkflowAsGraph docs)
+ this.rootGraph.configure(graphData as WorkflowAsGraph)
// Save original renderer version before scaling (it gets modified during scaling)
const originalMainGraphRenderer =
diff --git a/src/scripts/domWidget.ts b/src/scripts/domWidget.ts
index a1b57496dc1..0e83ee777f3 100644
--- a/src/scripts/domWidget.ts
+++ b/src/scripts/domWidget.ts
@@ -197,17 +197,24 @@ abstract class BaseDOMWidgetImpl
useDomWidgetStore().unregisterWidget(this.id)
}
- override createCopyForNode(node: LGraphNode): this {
- // @ts-expect-error
- const cloned: this = new (this.constructor as typeof this)({
- node: node,
+ /**
+ * Creates the constructor arguments for cloning this widget.
+ * Subclasses should override to add their specific properties.
+ */
+ protected getCloneArgs(node: LGraphNode): Record {
+ return {
+ node,
name: this.name,
type: this.type,
options: this.options
- })
+ }
+ }
+
+ override createCopyForNode(node: LGraphNode): this {
+ // Safe: subclasses override getCloneArgs to return constructor-compatible args
+ const Ctor = this.constructor as new (args: Record) => this
+ const cloned = new Ctor(this.getCloneArgs(node))
cloned.value = this.value
- // Preserve the Y position from the original widget to maintain proper positioning
- // when widgets are promoted through subgraph nesting
cloned.y = this.y
return cloned
}
@@ -230,20 +237,11 @@ export class DOMWidgetImpl
this.element = obj.element
}
- override createCopyForNode(node: LGraphNode): this {
- // @ts-expect-error
- const cloned: this = new (this.constructor as typeof this)({
- node: node,
- name: this.name,
- type: this.type,
- element: this.element, // Include the element!
- options: this.options
- })
- cloned.value = this.value
- // Preserve the Y position from the original widget to maintain proper positioning
- // when widgets are promoted through subgraph nesting
- cloned.y = this.y
- return cloned
+ protected override getCloneArgs(node: LGraphNode): Record {
+ return {
+ ...super.getCloneArgs(node),
+ element: this.element
+ }
}
/** Extract DOM widget size info */
@@ -318,6 +316,15 @@ export class ComponentWidgetImpl<
this.props = obj.props
}
+ protected override getCloneArgs(node: LGraphNode): Record {
+ return {
+ ...super.getCloneArgs(node),
+ component: this.component,
+ inputSpec: this.inputSpec,
+ props: this.props
+ }
+ }
+
override computeLayoutSize() {
const minHeight = this.options.getMinHeight?.() ?? 50
const maxHeight = this.options.getMaxHeight?.()
diff --git a/src/scripts/metadata/avif.ts b/src/scripts/metadata/avif.ts
index c0d747d9e29..9f9270d5ff3 100644
--- a/src/scripts/metadata/avif.ts
+++ b/src/scripts/metadata/avif.ts
@@ -210,7 +210,7 @@ function findBox(
return null
}
-function parseAvifMetadata(buffer: ArrayBuffer): ComfyMetadata {
+function parseAvifMetadata(buffer: ArrayBuffer): Partial {
const metadata: ComfyMetadata = {}
const dataView = new DataView(buffer)
@@ -318,14 +318,18 @@ function parseAvifMetadata(buffer: ArrayBuffer): ComfyMetadata {
return metadata
}
-// @ts-expect-error fixme ts strict error
-function parseExifData(exifData) {
+function parseExifData(
+ exifData: Uint8Array
+): Record {
// Check for the correct TIFF header (0x4949 for little-endian or 0x4D4D for big-endian)
const isLittleEndian = String.fromCharCode(...exifData.slice(0, 2)) === 'II'
// Function to read 16-bit and 32-bit integers from binary data
- // @ts-expect-error fixme ts strict error
- function readInt(offset, isLittleEndian, length) {
+ function readInt(
+ offset: number,
+ isLittleEndian: boolean,
+ length: number
+ ): number | undefined {
let arr = exifData.slice(offset, offset + length)
if (length === 2) {
return new DataView(arr.buffer, arr.byteOffset, arr.byteLength).getUint16(
@@ -343,12 +347,12 @@ function parseExifData(exifData) {
// Read the offset to the first IFD (Image File Directory)
const ifdOffset = readInt(4, isLittleEndian, 4)
- // @ts-expect-error fixme ts strict error
- function parseIFD(offset) {
+ function parseIFD(offset: number) {
const numEntries = readInt(offset, isLittleEndian, 2)
- const result = {}
+ const result: Record = {}
+
+ if (numEntries === undefined) return result
- // @ts-expect-error fixme ts strict error
for (let i = 0; i < numEntries; i++) {
const entryOffset = offset + 2 + i * 12
const tag = readInt(entryOffset, isLittleEndian, 2)
@@ -358,22 +362,23 @@ function parseExifData(exifData) {
// Read the value(s) based on the data type
let value
- if (type === 2) {
+ if (type === 2 && valueOffset !== undefined && numValues !== undefined) {
// ASCII string
value = new TextDecoder('utf-8').decode(
- // @ts-expect-error fixme ts strict error
exifData.subarray(valueOffset, valueOffset + numValues - 1)
)
}
- // @ts-expect-error fixme ts strict error
- result[tag] = value
+ if (tag !== undefined) {
+ result[tag] = value
+ }
}
return result
}
// Parse the first IFD
+ if (ifdOffset === undefined) return {}
const ifdData = parseIFD(ifdOffset)
return ifdData
}
diff --git a/src/scripts/metadata/flac.ts b/src/scripts/metadata/flac.ts
index 5a3efa6ac69..99aca52e7fc 100644
--- a/src/scripts/metadata/flac.ts
+++ b/src/scripts/metadata/flac.ts
@@ -6,8 +6,7 @@ export function getFromFlacBuffer(buffer: ArrayBuffer): Record {
const signature = String.fromCharCode(...new Uint8Array(buffer, 0, 4))
if (signature !== 'fLaC') {
console.error('Not a valid FLAC file')
- // @ts-expect-error fixme ts strict error
- return
+ return {}
}
// Parse metadata blocks
@@ -30,17 +29,19 @@ export function getFromFlacBuffer(buffer: ArrayBuffer): Record {
if (isLastBlock) break
}
- // @ts-expect-error fixme ts strict error
- return vorbisComment
+ return vorbisComment ?? {}
}
export function getFromFlacFile(file: File): Promise> {
return new Promise((r) => {
const reader = new FileReader()
reader.onload = function (event) {
- // @ts-expect-error fixme ts strict error
- const arrayBuffer = event.target.result as ArrayBuffer
- r(getFromFlacBuffer(arrayBuffer))
+ if (event.target?.result instanceof ArrayBuffer) {
+ r(getFromFlacBuffer(event.target.result))
+ } else {
+ console.error('FileReader returned a non-ArrayBuffer result')
+ r({})
+ }
}
reader.readAsArrayBuffer(file)
})
@@ -50,14 +51,11 @@ export function getFromFlacFile(file: File): Promise> {
function parseVorbisComment(dataView: DataView): Record {
let offset = 0
const vendorLength = dataView.getUint32(offset, true)
- offset += 4
- // @ts-expect-error unused variable
- const vendorString = getString(dataView, offset, vendorLength)
- offset += vendorLength
+ offset += 4 + vendorLength
const userCommentListLength = dataView.getUint32(offset, true)
offset += 4
- const comments = {}
+ const comments: Record = {}
for (let i = 0; i < userCommentListLength; i++) {
const commentLength = dataView.getUint32(offset, true)
offset += 4
@@ -67,7 +65,6 @@ function parseVorbisComment(dataView: DataView): Record {
const ind = comment.indexOf('=')
const key = comment.substring(0, ind)
- // @ts-expect-error fixme ts strict error
comments[key] = comment.substring(ind + 1)
}
diff --git a/src/scripts/metadata/png.ts b/src/scripts/metadata/png.ts
index 2e911796a91..e87f33a9155 100644
--- a/src/scripts/metadata/png.ts
+++ b/src/scripts/metadata/png.ts
@@ -46,8 +46,9 @@ export function getFromPngFile(file: File) {
return new Promise>((r) => {
const reader = new FileReader()
reader.onload = (event) => {
- // @ts-expect-error fixme ts strict error
- r(getFromPngBuffer(event.target.result as ArrayBuffer))
+ if (event.target?.result instanceof ArrayBuffer) {
+ r(getFromPngBuffer(event.target.result))
+ }
}
reader.readAsArrayBuffer(file)
diff --git a/src/scripts/ui.ts b/src/scripts/ui.ts
index 6a428c974ef..07da9462ae3 100644
--- a/src/scripts/ui.ts
+++ b/src/scripts/ui.ts
@@ -94,17 +94,19 @@ export function $el(
return element as ElementType
}
-// @ts-expect-error fixme ts strict error
-function dragElement(dragEl): () => void {
+function dragElement(dragEl: HTMLElement): () => void {
var posDiffX = 0,
posDiffY = 0,
posStartX = 0,
posStartY = 0,
newPosX = 0,
newPosY = 0
- if (dragEl.getElementsByClassName('drag-handle')[0]) {
+ const handle = dragEl.getElementsByClassName('drag-handle')[0] as
+ | HTMLElement
+ | undefined
+ if (handle) {
// if present, the handle is where you move the DIV from:
- dragEl.getElementsByClassName('drag-handle')[0].onmousedown = dragMouseDown
+ handle.onmousedown = dragMouseDown
} else {
// otherwise, move the DIV from anywhere inside the DIV:
dragEl.onmousedown = dragMouseDown
@@ -151,7 +153,6 @@ function dragElement(dragEl): () => void {
dragEl.style.top = newPosY + 'px'
dragEl.style.bottom = 'unset'
- // @ts-expect-error fixme ts strict error
if (savePos) {
localStorage.setItem(
'Comfy.MenuPosition',
@@ -174,13 +175,11 @@ function dragElement(dragEl): () => void {
}
}
- // @ts-expect-error fixme ts strict error
- let savePos = undefined
+ let savePos: boolean | undefined = undefined
restorePos()
savePos = true
- // @ts-expect-error fixme ts strict error
- function dragMouseDown(e) {
+ function dragMouseDown(e: MouseEvent) {
e = e || window.event
e.preventDefault()
// get the mouse cursor position at startup:
@@ -191,8 +190,7 @@ function dragElement(dragEl): () => void {
document.onmousemove = elementDrag
}
- // @ts-expect-error fixme ts strict error
- function elementDrag(e) {
+ function elementDrag(e: MouseEvent) {
e = e || window.event
e.preventDefault()
@@ -230,16 +228,15 @@ function dragElement(dragEl): () => void {
}
class ComfyList {
- #type
- #text
- #reverse
+ #type: 'queue' | 'history'
+ #text: string
+ #reverse: boolean
element: HTMLDivElement
button?: HTMLButtonElement
- // @ts-expect-error fixme ts strict error
- constructor(text, type?, reverse?) {
+ constructor(text: string, type?: 'queue' | 'history', reverse?: boolean) {
this.#text = text
- this.#type = type || text.toLowerCase()
+ this.#type = type || (text.toLowerCase() as 'queue' | 'history')
this.#reverse = reverse || false
this.element = $el('div.comfy-list') as HTMLDivElement
this.element.style.display = 'none'
@@ -257,46 +254,45 @@ class ComfyList {
textContent: section
}),
$el('div.comfy-list-items', [
- // @ts-expect-error fixme ts strict error
- ...(this.#reverse ? items[section].reverse() : items[section]).map(
- (item: TaskItem) => {
- // Allow items to specify a custom remove action (e.g. for interrupt current prompt)
- const removeAction =
- 'remove' in item
- ? item.remove
- : {
- name: 'Delete',
- cb: () => api.deleteItem(this.#type, item.prompt[1])
- }
- return $el('div', { textContent: item.prompt[0] + ': ' }, [
- $el('button', {
- textContent: 'Load',
- onclick: async () => {
- await app.loadGraphData(
- // @ts-expect-error fixme ts strict error
- item.prompt[3].extra_pnginfo.workflow,
- true,
- false
- )
- if ('outputs' in item) {
- app.nodeOutputs = {}
- for (const [key, value] of Object.entries(item.outputs)) {
- const realKey = item['meta']?.[key]?.display_node ?? key
- app.nodeOutputs[realKey] = value
- }
- }
+ ...(this.#reverse
+ ? (items[section as keyof typeof items] as TaskItem[]).reverse()
+ : (items[section as keyof typeof items] as TaskItem[])
+ ).map((item: TaskItem) => {
+ // Allow items to specify a custom remove action (e.g. for interrupt current prompt)
+ const removeAction =
+ 'remove' in item
+ ? item.remove
+ : {
+ name: 'Delete',
+ cb: () => api.deleteItem(this.#type, item.prompt[1])
}
- }),
- $el('button', {
- textContent: removeAction.name,
- onclick: async () => {
- await removeAction.cb()
- await this.update()
+ return $el('div', { textContent: item.prompt[0] + ': ' }, [
+ $el('button', {
+ textContent: 'Load',
+ onclick: async () => {
+ await app.loadGraphData(
+ item.prompt[3].extra_pnginfo?.workflow,
+ true,
+ false
+ )
+ if ('outputs' in item) {
+ app.nodeOutputs = {}
+ for (const [key, value] of Object.entries(item.outputs)) {
+ const realKey = item['meta']?.[key]?.display_node ?? key
+ app.nodeOutputs[realKey] = value
+ }
}
- })
- ])
- }
- )
+ }
+ }),
+ $el('button', {
+ textContent: removeAction.name,
+ onclick: async () => {
+ await removeAction.cb()
+ await this.update()
+ }
+ })
+ ])
+ })
])
]),
$el('div.comfy-list-actions', [
@@ -320,16 +316,14 @@ class ComfyList {
async show() {
this.element.style.display = 'block'
- // @ts-expect-error fixme ts strict error
- this.button.textContent = 'Close'
+ if (this.button) this.button.textContent = 'Close'
await this.load()
}
hide() {
this.element.style.display = 'none'
- // @ts-expect-error fixme ts strict error
- this.button.textContent = 'View ' + this.#text
+ if (this.button) this.button.textContent = 'View ' + this.#text
}
toggle() {
@@ -351,23 +345,15 @@ export class ComfyUI {
lastQueueSize: number
queue: ComfyList
history: ComfyList
- // @ts-expect-error fixme ts strict error
- autoQueueMode: string
- // @ts-expect-error fixme ts strict error
- graphHasChanged: boolean
- // @ts-expect-error fixme ts strict error
- autoQueueEnabled: boolean
- // @ts-expect-error fixme ts strict error
- menuContainer: HTMLDivElement
- // @ts-expect-error fixme ts strict error
- queueSize: Element
- // @ts-expect-error fixme ts strict error
- restoreMenuPosition: () => void
- // @ts-expect-error fixme ts strict error
- loadFile: () => void
-
- // @ts-expect-error fixme ts strict error
- constructor(app) {
+ autoQueueMode!: string
+ graphHasChanged!: boolean
+ autoQueueEnabled: boolean = false
+ menuContainer!: HTMLDivElement
+ queueSize!: Element
+ restoreMenuPosition!: () => void
+ loadFile!: () => void
+
+ constructor(app: ComfyApp) {
this.app = app
this.dialog = new ComfyDialog()
this.settings = new ComfySettingsDialog(app)
@@ -417,9 +403,8 @@ export class ComfyUI {
}
],
{
- // @ts-expect-error fixme ts strict error
onChange: (value) => {
- this.autoQueueMode = value.item.value
+ this.autoQueueMode = value.item.value ?? value.item.text
}
}
)
@@ -486,14 +471,13 @@ export class ComfyUI {
$el('label', { innerHTML: 'Extra options' }, [
$el('input', {
type: 'checkbox',
- // @ts-expect-error fixme ts strict error
- onchange: (i) => {
- // @ts-expect-error fixme ts strict error
- document.getElementById('extraOptions').style.display = i
- .srcElement.checked
- ? 'block'
- : 'none'
- this.batchCount = i.srcElement.checked
+ onchange: (e: Event) => {
+ const extraOptions = document.getElementById('extraOptions')
+ const target = e.target
+ if (!(target instanceof HTMLInputElement) || !extraOptions)
+ return
+ extraOptions.style.display = target.checked ? 'block' : 'none'
+ this.batchCount = target.checked
? Number.parseInt(
(
document.getElementById(
@@ -524,18 +508,15 @@ export class ComfyUI {
value: this.batchCount,
min: '1',
style: { width: '35%', marginLeft: '0.4em' },
- // @ts-expect-error fixme ts strict error
- oninput: (i) => {
- this.batchCount = i.target.value
- /* Even though an element with a type of range logically represents a number (since
- it's used for numeric input), the value it holds is still treated as a string in HTML and
- JavaScript. This behavior is consistent across all elements regardless of their type
- (like text, number, or range), where the .value property is always a string. */
- ;(
- document.getElementById(
- 'batchCountInputRange'
- ) as HTMLInputElement
- ).value = this.batchCount.toString()
+ oninput: (e: Event) => {
+ if (!(e.target instanceof HTMLInputElement)) return
+ this.batchCount = Number.parseInt(e.target.value) || 1
+ const rangeInput = document.getElementById(
+ 'batchCountInputRange'
+ )
+ if (rangeInput instanceof HTMLInputElement) {
+ rangeInput.value = this.batchCount.toString()
+ }
}
}),
$el('input', {
@@ -544,15 +525,15 @@ export class ComfyUI {
min: '1',
max: '100',
value: this.batchCount,
- // @ts-expect-error fixme ts strict error
- oninput: (i) => {
- this.batchCount = i.srcElement.value
- // Note
- ;(
- document.getElementById(
- 'batchCountInputNumber'
- ) as HTMLInputElement
- ).value = i.srcElement.value
+ oninput: (e: Event) => {
+ if (!(e.target instanceof HTMLInputElement)) return
+ this.batchCount = Number.parseInt(e.target.value) || 1
+ const numberInput = document.getElementById(
+ 'batchCountInputNumber'
+ )
+ if (numberInput instanceof HTMLInputElement) {
+ numberInput.value = e.target.value
+ }
}
})
]),
@@ -566,8 +547,8 @@ export class ComfyUI {
type: 'checkbox',
checked: false,
title: 'Automatically queue prompt when the queue size hits 0',
- // @ts-expect-error fixme ts strict error
- onchange: (e) => {
+ onchange: (e: Event) => {
+ if (!(e.target instanceof HTMLInputElement)) return
this.autoQueueEnabled = e.target.checked
autoQueueModeEl.style.display = this.autoQueueEnabled
? ''
@@ -682,8 +663,8 @@ export class ComfyUI {
this.restoreMenuPosition = dragElement(this.menuContainer)
- // @ts-expect-error
- this.setStatus({ exec_info: { queue_remaining: 'X' } })
+ // Initialize with placeholder text before first status update
+ this.queueSize.textContent = 'Queue size: X'
}
setStatus(status: StatusWsMessageStatus | null) {
diff --git a/src/scripts/ui/components/asyncDialog.ts b/src/scripts/ui/components/asyncDialog.ts
index 1da1a5a4efe..457d040ad0f 100644
--- a/src/scripts/ui/components/asyncDialog.ts
+++ b/src/scripts/ui/components/asyncDialog.ts
@@ -1,28 +1,30 @@
import { $el } from '../../ui'
import { ComfyDialog } from '../dialog'
-export class ComfyAsyncDialog extends ComfyDialog {
- // @ts-expect-error fixme ts strict error
- #resolve: (value: any) => void
+type DialogAction = string | { value?: T; text: string }
- constructor(actions?: Array) {
+export class ComfyAsyncDialog<
+ T = string
+> extends ComfyDialog