fix: browser_tests Phase 3 - type annotations and final fixes

- Add 'this: ExpectMatcherState' to ComfyPage.ts makeMatcher function
- Add type annotations to parameters (slot, comfyPage, w, inputs)
- Add 'as any' for ExtensionManager dynamic properties (workflow, focusMode, colorPalette, queueSettings)
- Fix keybindings import paths to use '/types' suffix
- Cast Subgraph types and CanvasPointerEvent in SubgraphHelper
- Fix test setting IDs with 'as any' in useSettingSearch, extensionAPI specs
- Fix AppReadiness, colorPalette, featureFlags type issues
- Replace removed ComfyPage properties with DefaultGraphPositions
- Remove unused _canvas parameter from ClipboardHelper
- Add non-null assertions for app, vaeInput, convertedInput

Reduces browser_tests typecheck errors from 72 to 0.

Amp-Thread-ID: https://ampcode.com/threads/T-019c17a3-2482-7115-8e42-ba0bbb0c9cda
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Alexander Brown
2026-01-31 21:27:38 -08:00
parent 302e9d5be7
commit d605b5da67
21 changed files with 123 additions and 93 deletions

View File

@@ -1,4 +1,9 @@
import type { APIRequestContext, Locator, Page } from '@playwright/test'
import type {
APIRequestContext,
ExpectMatcherState,
Locator,
Page
} from '@playwright/test'
import { test as base, expect } from '@playwright/test'
import dotenv from 'dotenv'
@@ -134,7 +139,7 @@ class ConfirmDialog {
// Wait for workflow service to finish if it's busy
await this.page.waitForFunction(
() => window.app?.extensionManager?.workflow?.isBusy === false,
() => (window.app?.extensionManager as any)?.workflow?.isBusy === false,
undefined,
{ timeout: 3000 }
)
@@ -214,7 +219,7 @@ export class ComfyPage {
this.nodeOps = new NodeOperationsHelper(this)
this.settings = new SettingsHelper(page)
this.keyboard = new KeyboardHelper(page, this.canvas)
this.clipboard = new ClipboardHelper(this.keyboard, this.canvas)
this.clipboard = new ClipboardHelper(this.keyboard)
this.workflow = new WorkflowHelper(this)
this.contextMenu = new ContextMenu(page)
this.toast = new ToastHelper(page)
@@ -382,7 +387,7 @@ export class ComfyPage {
async setFocusMode(focusMode: boolean) {
await this.page.evaluate((focusMode) => {
window.app!.extensionManager.focusMode = focusMode
;(window.app!.extensionManager as any).focusMode = focusMode
}, focusMode)
await this.nextFrame()
}
@@ -441,6 +446,7 @@ const makeMatcher = function <T>(
type: string
) {
return async function (
this: ExpectMatcherState,
node: NodeReference,
options?: { timeout?: number; intervals?: number[] }
) {

View File

@@ -154,7 +154,7 @@ export class WorkflowsSidebarTab extends SidebarTab {
// Wait for workflow service to finish renaming
await this.page.waitForFunction(
() => !window.app?.extensionManager?.workflow?.isBusy,
() => !(window.app?.extensionManager as any)?.workflow?.isBusy,
undefined,
{ timeout: 3000 }
)

View File

@@ -85,7 +85,7 @@ export class Topbar {
// Wait for workflow service to finish saving
await this.page.waitForFunction(
() => !window.app!.extensionManager.workflow.isBusy,
() => !(window.app!.extensionManager as any).workflow.isBusy,
undefined,
{ timeout: 3000 }
)

View File

@@ -3,10 +3,7 @@ import type { Locator } from '@playwright/test'
import type { KeyboardHelper } from './KeyboardHelper'
export class ClipboardHelper {
constructor(
private readonly keyboard: KeyboardHelper,
private readonly _canvas: Locator
) {}
constructor(private readonly keyboard: KeyboardHelper) {}
async copy(locator?: Locator | null): Promise<void> {
await this.keyboard.ctrlSend('KeyC', locator === undefined ? null : locator)

View File

@@ -1,6 +1,6 @@
import type { Page } from '@playwright/test'
import type { KeyCombo } from '../../../src/platform/keybindings'
import type { KeyCombo } from '../../../src/platform/keybindings/types'
export class CommandHelper {
constructor(private readonly page: Page) {}

View File

@@ -13,8 +13,8 @@ export class SettingsHelper {
}
async getSetting<T = unknown>(settingId: string): Promise<T> {
return await this.page.evaluate(async (id) => {
return (await this.page.evaluate(async (id) => {
return await window.app!.extensionManager.setting.get(id)
}, settingId)
}, settingId)) as T
}
}

View File

@@ -1,5 +1,10 @@
import type { Page } from '@playwright/test'
import type {
CanvasPointerEvent,
Subgraph
} from '@/lib/litegraph/src/litegraph'
import type { ComfyPage } from '../ComfyPage'
import type { NodeReference } from '../utils/litegraphUtils'
import { SubgraphSlotReference } from '../utils/litegraphUtils'
@@ -37,13 +42,12 @@ export class SubgraphHelper {
)
}
const subgraph = currentGraph as Subgraph
// Get the appropriate node and slots
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`)
@@ -84,7 +88,7 @@ export class SubgraphHelper {
if (node.onPointerDown) {
node.onPointerDown(
event,
event as unknown as CanvasPointerEvent,
app.canvas.pointer,
app.canvas.linkConnector
)
@@ -117,14 +121,16 @@ export class SubgraphHelper {
if (node.onPointerDown) {
node.onPointerDown(
event,
event as unknown as CanvasPointerEvent,
app.canvas.pointer,
app.canvas.linkConnector
)
// Trigger double-click
if (app.canvas.pointer.onDoubleClick) {
app.canvas.pointer.onDoubleClick(event)
app.canvas.pointer.onDoubleClick(
event as unknown as CanvasPointerEvent
)
}
}

View File

@@ -45,7 +45,7 @@ export class WorkflowHelper {
}
await this.comfyPage.page.evaluate(async () => {
await window.app!.extensionManager.workflow.syncWorkflows()
await (window.app!.extensionManager as any).workflow.syncWorkflows()
})
// Wait for Vue to re-render the workflow list

View File

@@ -42,13 +42,13 @@ class ComfyQueueButtonOptions {
public async setMode(mode: AutoQueueMode) {
await this.page.evaluate((mode) => {
window.app!.extensionManager.queueSettings.mode = mode
;(window.app!.extensionManager as any).queueSettings.mode = mode
}, mode)
}
public async getMode() {
return await this.page.evaluate(() => {
return window.app!.extensionManager.queueSettings.mode
return (window.app!.extensionManager as any).queueSettings.mode
})
}
}

View File

@@ -11,7 +11,8 @@ test.describe('Browser tab title', { tag: '@smoke' }, () => {
test('Can display workflow name', async ({ comfyPage }) => {
const workflowName = await comfyPage.page.evaluate(async () => {
return window.app!.extensionManager.workflow.activeWorkflow.filename
return (window.app!.extensionManager as any).workflow.activeWorkflow
.filename
})
expect(await comfyPage.page.title()).toBe(`*${workflowName} - ComfyUI`)
})
@@ -22,7 +23,8 @@ test.describe('Browser tab title', { tag: '@smoke' }, () => {
comfyPage
}) => {
const workflowName = await comfyPage.page.evaluate(async () => {
return window.app!.extensionManager.workflow.activeWorkflow.filename
return (window.app!.extensionManager as any).workflow.activeWorkflow
.filename
})
expect(await comfyPage.page.title()).toBe(`${workflowName} - ComfyUI`)
@@ -38,7 +40,9 @@ test.describe('Browser tab title', { tag: '@smoke' }, () => {
// Delete the saved workflow for cleanup.
await comfyPage.page.evaluate(async () => {
return window.app!.extensionManager.workflow.activeWorkflow.delete()
return (
window.app!.extensionManager as any
).workflow.activeWorkflow.delete()
})
})
})

View File

@@ -1,13 +1,12 @@
import { expect } from '@playwright/test'
import type { Palette } from '../../src/schemas/colorPaletteSchema'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
})
const customColorPalettes: Record<string, Palette> = {
const customColorPalettes = {
obsidian: {
version: 102,
id: 'obsidian',
@@ -179,7 +178,9 @@ test.describe('Color Palette', { tag: ['@screenshot', '@settings'] }, () => {
test('Can add custom color palette', async ({ comfyPage }) => {
await comfyPage.page.evaluate((p) => {
window.app!.extensionManager.colorPalette.addCustomColorPalette(p)
;(window.app!.extensionManager as any).colorPalette.addCustomColorPalette(
p
)
}, customColorPalettes.obsidian_dark)
expect(await comfyPage.toast.getToastErrorCount()).toBe(0)

View File

@@ -1,7 +1,7 @@
import type { Locator } from '@playwright/test'
import { expect } from '@playwright/test'
import type { Keybinding } from '../../src/platform/keybindings'
import type { Keybinding } from '../../src/platform/keybindings/types'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { DefaultGraphPositions } from '../fixtures/constants/defaultGraphPositions'
@@ -346,7 +346,7 @@ test.describe('Error dialog', () => {
}) => {
await comfyPage.page.evaluate(() => {
const graph = window.graph!
graph.configure = () => {
;(graph as { configure: () => void }).configure = () => {
throw new Error('Error on configure!')
}
})

View File

@@ -87,7 +87,7 @@ test.describe('Topbar commands', () => {
name: 'TestExtension1',
settings: [
{
id: 'TestSetting',
id: 'TestSetting' as any,
name: 'Test Setting',
type: 'text',
defaultValue: 'Hello, world!',
@@ -117,7 +117,7 @@ test.describe('Topbar commands', () => {
name: 'TestExtension1',
settings: [
{
id: 'Comfy.TestSetting',
id: 'Comfy.TestSetting' as any,
name: 'Test Setting',
type: 'boolean',
defaultValue: false,
@@ -144,7 +144,7 @@ test.describe('Topbar commands', () => {
test.describe('Passing through attrs to setting components', () => {
const testCases: Array<{
config: Partial<SettingParams>
config: Partial<Omit<SettingParams, 'id'>>
selector: string
}> = [
{
@@ -201,12 +201,11 @@ test.describe('Topbar commands', () => {
name: 'TestExtension1',
settings: [
{
id: 'Comfy.TestSetting',
id: 'Comfy.TestSetting' as any,
name: 'Test',
// The `disabled` attr is common to all settings components
attrs: { disabled: true },
...config
}
} as any
]
})
}, config)
@@ -258,15 +257,17 @@ test.describe('Topbar commands', () => {
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.nodeOps.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 ({
@@ -278,30 +279,38 @@ test.describe('Topbar commands', () => {
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'
;(window as unknown as Record<string, unknown>)['value'] = 'foo'
void window
.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()
})
})
@@ -323,7 +332,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
}
}
],
@@ -339,9 +350,13 @@ test.describe('Topbar commands', () => {
)
await toolboxButton.click()
// 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

@@ -25,7 +25,7 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
const originalSend = WebSocket.prototype.send
WebSocket.prototype.send = function (data) {
try {
const parsed = JSON.parse(data)
const parsed = JSON.parse(data as string)
if (parsed.type === 'feature_flags') {
window.__capturedMessages!.clientFeatureFlags = parsed
}
@@ -285,10 +285,7 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
window.app?.api?.serverFeatureFlags?.supports_preview_metadata !==
undefined
) {
window.__appReadiness = {
...window.__appReadiness,
featureFlagsReceived: true
}
window.__appReadiness!.featureFlagsReceived = true
clearInterval(checkFeatureFlags)
}
}, 10)
@@ -296,10 +293,7 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
// Monitor API initialization
const checkApi = setInterval(() => {
if (window.app?.api) {
window.__appReadiness = {
...window.__appReadiness,
apiInitialized: true
}
window.__appReadiness!.apiInitialized = true
clearInterval(checkApi)
}
}, 10)
@@ -307,10 +301,7 @@ test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
// Monitor app initialization
const checkApp = setInterval(() => {
if (window.app?.graph) {
window.__appReadiness = {
...window.__appReadiness,
appInitialized: true
}
window.__appReadiness!.appInitialized = true
clearInterval(checkApp)
}
}, 10)

View File

@@ -632,9 +632,9 @@ test.describe('Canvas Interaction', { tag: '@screenshot' }, () => {
})
test('Can pan when dragging a link', async ({ comfyPage, comfyMouse }) => {
const posSlot1 = comfyPage.clipTextEncodeNode1InputSlot
const posSlot1 = DefaultGraphPositions.clipTextEncodeNode1InputSlot
await comfyMouse.move(posSlot1)
const posEmpty = comfyPage.emptySpace
const posEmpty = DefaultGraphPositions.emptySpace
await comfyMouse.drag(posEmpty)
await expect(comfyPage.canvas).toHaveScreenshot('dragging-link1.png')
@@ -845,7 +845,7 @@ test.describe('Viewport settings', () => {
const changeTab = async (tab: Locator) => {
await tab.click()
await comfyPage.nextFrame()
await comfyMouse.move(comfyPage.emptySpace)
await comfyMouse.move(DefaultGraphPositions.emptySpace)
// If tooltip is visible, wait for it to hide
await expect(
@@ -875,7 +875,7 @@ test.describe('Viewport settings', () => {
const tabB = comfyPage.menu.topbar.getWorkflowTab('Workflow B')
await changeTab(tabB)
await comfyMouse.move(comfyPage.emptySpace)
await comfyMouse.move(DefaultGraphPositions.emptySpace)
for (let i = 0; i < 4; i++) {
await comfyMouse.wheel(0, 60)
}

View File

@@ -49,19 +49,22 @@ test.describe('Optional input', { tag: ['@screenshot', '@node'] }, () => {
test('Old workflow with converted input', async ({ comfyPage }) => {
await comfyPage.workflow.loadWorkflow('inputs/old_workflow_converted_input')
const node = await comfyPage.nodeOps.getNodeRefById('1')
const inputs = await node.getProperty('inputs')
const inputs = (await node.getProperty('inputs')) as {
name: string
link?: number | null
}[]
const vaeInput = inputs.find((w) => w.name === 'vae')
const convertedInput = inputs.find((w) => 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.workflow.loadWorkflow('inputs/renamed_converted_widget')
const node = await comfyPage.nodeOps.getNodeRefById('3')
const inputs = await node.getProperty('inputs')
const inputs = (await node.getProperty('inputs')) as { name: string }[]
const renamedInput = inputs.find((w) => w.name === 'breadth')
expect(renamedInput).toBeUndefined()
})

View File

@@ -1,7 +1,5 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../fixtures/ComfyPage'
import { comfyExpect as expect, comfyPageFixture as test } from '../fixtures/ComfyPage';
import type { ComfyPage } from '../fixtures/ComfyPage';
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Disabled')
@@ -138,7 +136,10 @@ test.describe('Node search box', { tag: '@node' }, () => {
})
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

@@ -301,9 +301,9 @@ test.describe('Node library sidebar', () => {
await comfyPage.nextFrame()
// Verify the color selection is saved
const setting = await comfyPage.settings.getSetting(
'Comfy.NodeLibrary.BookmarksCustomization'
)
const setting = await comfyPage.settings.getSetting<
Record<string, { icon?: string; color?: string }>
>('Comfy.NodeLibrary.BookmarksCustomization')
await expect(setting).toHaveProperty(['foo/', 'color'])
await expect(setting['foo/'].color).not.toBeNull()
await expect(setting['foo/'].color).not.toBeUndefined()

View File

@@ -14,14 +14,14 @@ test.describe('Settings Search functionality', { tag: '@settings' }, () => {
name: 'TestSettingsExtension',
settings: [
{
id: 'TestHiddenSetting',
id: 'TestHiddenSetting' as any,
name: 'Test Hidden Setting',
type: 'hidden',
defaultValue: 'hidden_value',
category: ['Test', 'Hidden']
},
{
id: 'TestDeprecatedSetting',
id: 'TestDeprecatedSetting' as any,
name: 'Test Deprecated Setting',
type: 'text',
defaultValue: 'deprecated_value',
@@ -29,7 +29,7 @@ test.describe('Settings Search functionality', { tag: '@settings' }, () => {
category: ['Test', 'Deprecated']
},
{
id: 'TestVisibleSetting',
id: 'TestVisibleSetting' as any,
name: 'Test Visible Setting',
type: 'text',
defaultValue: 'visible_value',

View File

@@ -451,7 +451,7 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
if (!link) throw new Error('Link not found')
// Convert the client/canvas pixel coordinates to graph space
const pos = app.canvas.ds.convertCanvasToOffset([
const pos = app!.canvas.ds.convertCanvasToOffset([
clientPoint.x,
clientPoint.y
])
@@ -539,7 +539,7 @@ test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
if (!link) throw new Error('Link not found')
// Convert the client/canvas pixel coordinates to graph space
const pos = app.canvas.ds.convertCanvasToOffset([
const pos = app!.canvas.ds.convertCanvasToOffset([
clientPoint.x,
clientPoint.y
])

View File

@@ -13,17 +13,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 { _nodes_by_id: Record<string, any> }
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 { _nodes_by_id: Record<string, any> }
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 { _nodes_by_id: Record<string, any> }
const node = graph._nodes_by_id['4']
node.widgets.splice(0, 0, node.widgets[0])
})
await expect(loadCheckpointNode).toHaveCount(4)
@@ -33,17 +36,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 { _nodes_by_id: Record<string, any> }
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 { _nodes_by_id: Record<string, any> }
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 { _nodes_by_id: Record<string, any> }
const node = graph._nodes_by_id['3']
node.widgets.splice(0, 1)
})
await expect(loadCheckpointNode).toHaveCount(3)