fix(browser_tests): remove all @ts-expect-error and type assertions

This commit is contained in:
DrJKL
2026-01-12 11:16:21 -08:00
parent eb2c4e29a3
commit 4ef1fd984b
31 changed files with 727 additions and 277 deletions

View File

@@ -160,7 +160,7 @@ class NodeSlotReference {
const convertedPos = app.canvas.ds.convertOffsetToCanvas(rawPos)
// Debug logging - convert Float64Arrays to regular arrays for visibility
console.log(
`NodeSlotReference debug for ${type} slot ${index} on node ${id}:`,
{

View File

@@ -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'])

View File

@@ -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'])

View File

@@ -42,13 +42,25 @@ 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) {
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
})
}
}

View File

@@ -32,9 +32,11 @@ export class ComfyTemplates {
}
async getAllTemplates(): Promise<TemplateInfo[]> {
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)
}

View File

@@ -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)
}

View File

@@ -10,7 +10,12 @@ 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 app = window['app']
if (!app) throw new Error('App not initialized')
const extMgr = app.extensionManager as {
workflow?: { activeWorkflow?: { filename?: string } }
}
return extMgr.workflow?.activeWorkflow?.filename
})
expect(await comfyPage.page.title()).toBe(`*${workflowName} - ComfyUI`)
})
@@ -21,7 +26,12 @@ test.describe('Browser tab title', () => {
comfyPage
}) => {
const workflowName = await comfyPage.page.evaluate(async () => {
return window['app'].extensionManager.workflow.activeWorkflow.filename
const app = window['app']
if (!app) throw new Error('App not initialized')
const extMgr = app.extensionManager as {
workflow?: { activeWorkflow?: { filename?: string } }
}
return extMgr.workflow?.activeWorkflow?.filename
})
expect(await comfyPage.page.title()).toBe(`${workflowName} - ComfyUI`)
@@ -35,7 +45,12 @@ 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')
const extMgr = app.extensionManager as {
workflow?: { activeWorkflow?: { delete?: () => Promise<void> } }
}
return extMgr.workflow?.activeWorkflow?.delete?.()
})
})
})

View File

@@ -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()

View File

@@ -7,7 +7,15 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
const customColorPalettes: Record<string, Palette> = {
type TestPalette = Omit<Palette, 'colors'> & {
colors: {
node_slot: Record<string, string>
litegraph_base: Partial<Palette['colors']['litegraph_base']>
comfy_base: Partial<Palette['colors']['comfy_base']>
}
}
const customColorPalettes: Record<string, TestPalette> = {
obsidian: {
version: 102,
id: 'obsidian',
@@ -176,7 +184,12 @@ 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 }
}
extMgr.colorPalette?.addCustomColorPalette?.(p)
}, customColorPalettes.obsidian_dark)
expect(await comfyPage.getToastErrorCount()).toBe(0)
@@ -232,7 +245,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')
})

View File

@@ -9,25 +9,33 @@ test.beforeEach(async ({ comfyPage }) => {
test.describe('Keybindings', () => {
test('Should execute command', async ({ comfyPage }) => {
await comfyPage.registerCommand('TestCommand', () => {
window['foo'] = true
;(window as unknown as Record<string, unknown>)['foo'] = true
})
await comfyPage.executeCommand('TestCommand')
expect(await comfyPage.page.evaluate(() => window['foo'])).toBe(true)
expect(
await comfyPage.page.evaluate(
() => (window as unknown as Record<string, unknown>)['foo']
)
).toBe(true)
})
test('Should execute async command', async ({ comfyPage }) => {
await comfyPage.registerCommand('TestCommand', async () => {
await new Promise<void>((resolve) =>
setTimeout(() => {
window['foo'] = true
;(window as unknown as Record<string, unknown>)['foo'] = true
resolve()
}, 5)
)
})
await comfyPage.executeCommand('TestCommand')
expect(await comfyPage.page.evaluate(() => window['foo'])).toBe(true)
expect(
await comfyPage.page.evaluate(
() => (window as unknown as Record<string, unknown>)['foo']
)
).toBe(true)
})
test('Should handle command errors', async ({ comfyPage }) => {
@@ -41,7 +49,7 @@ test.describe('Keybindings', () => {
test('Should handle async command errors', async ({ comfyPage }) => {
await comfyPage.registerCommand('TestCommand', async () => {
await new Promise<void>((resolve, reject) =>
await new Promise<void>((_resolve, reject) =>
setTimeout(() => {
reject(new Error('Test error'))
}, 5)

View File

@@ -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')

View File

@@ -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 as unknown as Record<string, unknown>)['foo'] = true
}
}
],
@@ -31,7 +33,11 @@ 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 as unknown as Record<string, unknown>)['foo']
)
).toBe(true)
})
test('Should not allow register command defined in other extension', async ({
@@ -39,7 +45,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 +65,15 @@ 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 as unknown as Record<string, unknown>)['TestCommand'] =
true
}
}
],
@@ -77,15 +87,19 @@ 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 as unknown as Record<string, unknown>)['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 +108,39 @@ test.describe('Topbar commands', () => {
type: 'text',
defaultValue: 'Hello, world!',
onChange: () => {
window['changeCount'] = (window['changeCount'] ?? 0) + 1
const win = window as unknown as Record<string, unknown>
win['changeCount'] = ((win['changeCount'] as number) ?? 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 as unknown as Record<string, unknown>)['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 as unknown as Record<string, unknown>)['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 +149,35 @@ test.describe('Topbar commands', () => {
type: 'boolean',
defaultValue: false,
onChange: () => {
window['changeCount'] = (window['changeCount'] ?? 0) + 1
const win = window as unknown as Record<string, unknown>
win['changeCount'] = ((win['changeCount'] as number) ?? 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 as unknown as Record<string, unknown>)['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 as unknown as Record<string, unknown>)['changeCount']
)
).toBe(2)
})
test.describe('Passing through attrs to setting components', () => {
@@ -191,7 +235,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 +246,7 @@ test.describe('Topbar commands', () => {
// The `disabled` attr is common to all settings components
attrs: { disabled: true },
...config
}
} as SettingParams
]
})
}, config)
@@ -224,7 +270,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,55 +295,71 @@ 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 as unknown as Record<string, unknown>)['value'] = value
})
})
await comfyPage.fillPromptDialog('Hello, world!')
expect(await comfyPage.page.evaluate(() => window['value'])).toBe(
'Hello, world!'
)
expect(
await comfyPage.page.evaluate(
() => (window as unknown as Record<string, unknown>)['value']
)
).toBe('Hello, world!')
})
test('Should allow showing a confirmation 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
.confirm({
title: 'Test Confirm',
message: 'Test Confirm Message'
})
.then((value: boolean) => {
window['value'] = value
.then((value: boolean | null) => {
;(window as unknown as Record<string, unknown>)['value'] = value
})
})
await comfyPage.confirmDialog.click('confirm')
expect(await comfyPage.page.evaluate(() => window['value'])).toBe(true)
expect(
await comfyPage.page.evaluate(
() => (window as unknown as Record<string, unknown>)['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 as unknown as Record<string, unknown>)['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 as unknown as Record<string, unknown>)['value'] = value
})
})
await comfyPage.confirmDialog.click('reject')
expect(await comfyPage.page.evaluate(() => window['value'])).toBeNull()
expect(
await comfyPage.page.evaluate(
() => (window as unknown as Record<string, unknown>)['value']
)
).toBeNull()
})
})
@@ -309,7 +373,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 +383,9 @@ test.describe('Topbar commands', () => {
label: 'Test Command',
icon: 'pi pi-star',
function: () => {
window['selectionCommandExecuted'] = true
;(window as unknown as Record<string, unknown>)[
'selectionCommandExecuted'
] = true
}
}
],
@@ -335,7 +403,12 @@ test.describe('Topbar commands', () => {
// Verify the command was executed
expect(
await comfyPage.page.evaluate(() => window['selectionCommandExecuted'])
await comfyPage.page.evaluate(
() =>
(window as unknown as Record<string, unknown>)[
'selectionCommandExecuted'
]
)
).toBe(true)
})
})

View File

@@ -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<string, unknown> } }
}
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<string, unknown> }
serverFeatureFlags: Record<string, unknown>
}
}
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
}
})

View File

@@ -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)
})

View File

@@ -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

View File

@@ -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,9 @@ test.describe('Group Node', () => {
await libraryTab.open()
})
test('Is added to node library sidebar', async ({ comfyPage }) => {
test('Is added to node library sidebar', async ({
comfyPage: _comfyPage
}) => {
expect(await libraryTab.getFolder('group nodes').count()).toBe(1)
})
@@ -110,7 +113,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 +152,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<string, { nodes: Array<{ inputs?: unknown[] }> }>
| 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 +239,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 +314,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<typeof app.loadGraphData>[0]
)
}, currentGraphState)
await comfyPage.nextFrame()
await verifyNodeLoaded(comfyPage, 1)
})

View File

@@ -11,23 +11,25 @@ test.describe('Keybindings', () => {
comfyPage
}) => {
await comfyPage.registerKeybinding({ key: 'k' }, () => {
window['TestCommand'] = true
;(window as unknown as Record<string, unknown>)['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(
undefined
)
expect(
await comfyPage.page.evaluate(
() => (window as unknown as Record<string, unknown>)['TestCommand']
)
).toBe(undefined)
})
test('Should not trigger modifier keybinding when typing in input fields', async ({
comfyPage
}) => {
await comfyPage.registerKeybinding({ key: 'k', ctrl: true }, () => {
window['TestCommand'] = true
;(window as unknown as Record<string, unknown>)['TestCommand'] = true
})
const textBox = comfyPage.widgetTextBox
@@ -35,24 +37,28 @@ 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 as unknown as Record<string, unknown>)['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 as unknown as Record<string, unknown>)['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(
undefined
)
expect(
await comfyPage.page.evaluate(
() => (window as unknown as Record<string, unknown>)['TestCommand']
)
).toBe(undefined)
})
})

View File

@@ -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

View File

@@ -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',
@@ -152,7 +154,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: [
{

View File

@@ -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

View File

@@ -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<INodeInputSlot[]>('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<INodeInputSlot[]>('inputs')
const renamedInput = inputs.find(
(w: INodeInputSlot) => w.name === 'breadth'
)
expect(renamedInput).toBeUndefined()
})
test('slider', async ({ comfyPage }) => {

View File

@@ -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])

View File

@@ -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

View File

@@ -25,24 +25,33 @@ test.describe('Remote COMBO Widget', () => {
comfyPage: ComfyPage,
nodeName: string
): Promise<string[] | undefined> => {
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

View File

@@ -265,13 +265,13 @@ 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'
)
)) as Record<string, { icon?: string; color?: string }> | undefined
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('')
await expect(setting?.['foo/'].color).not.toBeNull()
await expect(setting?.['foo/'].color).not.toBeUndefined()
await expect(setting?.['foo/'].color).not.toBe('')
})
test('Can rename customized bookmark folder', async ({ comfyPage }) => {

View File

@@ -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,11 @@ test.describe('Workflows sidebar', () => {
})
// Compare the exported workflow with the original
expect(downloadedContent).toBeDefined()
expect(downloadedContentZh).toBeDefined()
if (!downloadedContent || !downloadedContentZh) return
delete downloadedContent.id
delete downloadedContentZh.id
expect(downloadedContent).toBeDefined()
expect(downloadedContent).toEqual(downloadedContentZh)
})

View File

@@ -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'
@@ -27,12 +26,18 @@ test.describe('Subgraph Slot Rename Dialog', () => {
// 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 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
if (!graph || !('inputs' in graph)) throw new Error('Not in subgraph')
const inputs = graph.inputs as { label?: string; name?: string }[]
return inputs?.[0]?.label || inputs?.[0]?.name || null
})
// First rename
await comfyPage.rightClickSubgraphInputSlot(initialInputLabel)
await comfyPage.rightClickSubgraphInputSlot(initialInputLabel ?? undefined)
await comfyPage.clickLitegraphContextMenuItem('Rename Slot')
await comfyPage.page.waitForSelector(SELECTORS.promptDialog, {
@@ -55,8 +60,18 @@ test.describe('Subgraph Slot Rename Dialog', () => {
// Verify the rename worked
const afterFirstRename = await comfyPage.page.evaluate(() => {
const graph = window['app'].canvas.graph
const slot = graph.inputs?.[0]
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
if (!graph || !('inputs' in graph)) throw new Error('Not in subgraph')
const inputs = graph.inputs as {
label?: string
name?: string
displayName?: string
}[]
const slot = inputs?.[0]
return {
label: slot?.label || null,
name: slot?.name || null,
@@ -97,8 +112,14 @@ test.describe('Subgraph Slot Rename Dialog', () => {
// Verify the second rename worked
const afterSecondRename = 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 available')
const canvas = app.canvas
if (!canvas) throw new Error('Canvas not available')
const graph = canvas.graph
if (!graph || !('inputs' in graph)) throw new Error('Not in subgraph')
const inputs = graph.inputs as { label?: string }[]
return inputs?.[0]?.label || null
})
expect(afterSecondRename).toBe(SECOND_RENAMED_NAME)
})
@@ -113,12 +134,20 @@ test.describe('Subgraph Slot Rename Dialog', () => {
// 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 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
if (!graph || !('outputs' in graph)) throw new Error('Not in subgraph')
const outputs = graph.outputs as { label?: string; name?: string }[]
return outputs?.[0]?.label || outputs?.[0]?.name || null
})
// First rename
await comfyPage.rightClickSubgraphOutputSlot(initialOutputLabel)
await comfyPage.rightClickSubgraphOutputSlot(
initialOutputLabel ?? undefined
)
await comfyPage.clickLitegraphContextMenuItem('Rename Slot')
await comfyPage.page.waitForSelector(SELECTORS.promptDialog, {

View File

@@ -26,8 +26,14 @@ test.describe('Subgraph Operations', () => {
comfyPage: typeof test.prototype.comfyPage,
type: 'inputs' | 'outputs'
): Promise<number> {
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<string, unknown[]>)[slotType]?.length || 0
)
}, type)
}
@@ -36,7 +42,11 @@ test.describe('Subgraph Operations', () => {
comfyPage: typeof test.prototype.comfyPage
): Promise<number> {
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 +55,9 @@ test.describe('Subgraph Operations', () => {
comfyPage: typeof test.prototype.comfyPage
): Promise<boolean> {
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 +142,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 +165,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 +183,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 +205,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 +223,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 +246,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 +266,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 +291,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 +311,30 @@ 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 {
inputs?: { label?: string; labelPos?: [number, number] }[]
inputNode?: {
onPointerDown?: (
e: unknown,
pointer: unknown,
linkConnector: unknown
) => void
}
} | null
if (!graph || !('inputs' in graph)) {
throw new Error('Not in a subgraph')
}
const input = graph.inputs?.[0]
if (!input?.labelPos) {
@@ -302,8 +362,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 +385,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 +400,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 +414,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 +521,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 +754,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)

View File

@@ -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
]
})
})

View File

@@ -3,6 +3,10 @@ import {
comfyPageFixture as test
} from '../../../fixtures/ComfyPage'
interface GraphWithNodes {
_nodes_by_id: Record<string, { widgets: unknown[] }>
}
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)

View File

@@ -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]) return
const widget = app.graph.nodes[0].widgets[0]
widget.callback = (value: number) => {
window['widgetValue'] = value
;(window as unknown as Record<string, unknown>)['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 as unknown as Record<string, unknown>)['widgetValue']
)
).toBeDefined()
})
})
@@ -120,16 +133,20 @@ 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 as unknown as Record<string, unknown>)['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 as unknown as Record<string, unknown>)['widgetValue']
)
).toBeDefined()
})
})
@@ -141,8 +158,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 +234,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 +262,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 +356,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]