mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-21 21:09:00 +00:00
Enables eslint/func-style in oxlint with declaration mode to enforce function declarations over function expressions and arrow expressions assigned to variables. Vendored litegraph is excluded via override. Converts existing function expressions and variable-initialized arrow functions to function declarations across src/, browser_tests/, apps/, packages/, and scripts/. Adjusts a handful of let-reassignable callback placeholders, narrowed variable patterns, and typed widget constructors to keep type safety intact. Pre-existing type-aware oxlint errors (no-console, no-floating-promises, no-explicit-any) are unchanged from main.
299 lines
9.5 KiB
TypeScript
299 lines
9.5 KiB
TypeScript
import { expect } from '@playwright/test'
|
|
|
|
import type { ComfyPage } from '@e2e/fixtures/ComfyPage'
|
|
import { comfyPageFixture as test } from '@e2e/fixtures/ComfyPage'
|
|
|
|
async function pressKeyAndExpectRequest(
|
|
comfyPage: ComfyPage,
|
|
key: string,
|
|
urlPattern: string,
|
|
method: string = 'POST'
|
|
) {
|
|
const requestPromise = comfyPage.page.waitForRequest(
|
|
(req) => req.url().includes(urlPattern) && req.method() === method,
|
|
{ timeout: 5000 }
|
|
)
|
|
await comfyPage.page.keyboard.press(key)
|
|
return requestPromise
|
|
}
|
|
|
|
test.describe('Default Keybindings', { tag: '@keyboard' }, () => {
|
|
test.describe('Sidebar Toggle Shortcuts', () => {
|
|
test.beforeEach(async ({ comfyPage }) => {
|
|
await comfyPage.canvas.click({ position: { x: 400, y: 400 } })
|
|
await comfyPage.nextFrame()
|
|
})
|
|
|
|
const sidebarTabs = [
|
|
{ key: 'KeyW', tabId: 'workflows', label: 'workflows' },
|
|
{ key: 'KeyN', tabId: 'node-library', label: 'node library' },
|
|
{ key: 'KeyM', tabId: 'model-library', label: 'model library' },
|
|
{ key: 'KeyA', tabId: 'assets', label: 'assets' }
|
|
] as const
|
|
|
|
for (const { key, tabId, label } of sidebarTabs) {
|
|
test(`'${key}' toggles ${label} sidebar`, async ({ comfyPage }) => {
|
|
const selectedButton = comfyPage.page.locator(
|
|
`.${tabId}-tab-button.side-bar-button-selected`
|
|
)
|
|
|
|
await expect(selectedButton).toBeHidden()
|
|
|
|
await comfyPage.canvas.press(key)
|
|
await expect(selectedButton).toBeVisible()
|
|
|
|
await comfyPage.canvas.press(key)
|
|
await expect(selectedButton).toBeHidden()
|
|
})
|
|
}
|
|
})
|
|
|
|
test.describe('Canvas View Controls', () => {
|
|
test("'Alt+=' zooms in", async ({ comfyPage }) => {
|
|
const initialScale = await comfyPage.canvasOps.getScale()
|
|
|
|
await comfyPage.keyboard.press('Alt+Equal')
|
|
|
|
await expect
|
|
.poll(() => comfyPage.canvasOps.getScale())
|
|
.toBeGreaterThan(initialScale)
|
|
})
|
|
|
|
test("'Alt+-' zooms out", async ({ comfyPage }) => {
|
|
const initialScale = await comfyPage.canvasOps.getScale()
|
|
|
|
await comfyPage.keyboard.press('Alt+Minus')
|
|
|
|
await expect
|
|
.poll(() => comfyPage.canvasOps.getScale())
|
|
.toBeLessThan(initialScale)
|
|
})
|
|
|
|
test("'.' fits view to nodes", async ({ comfyPage }) => {
|
|
// Set scale very small so fit-view will zoom back to fit nodes
|
|
await comfyPage.canvasOps.setScale(0.1)
|
|
await expect
|
|
.poll(() => comfyPage.canvasOps.getScale())
|
|
.toBeCloseTo(0.1, 1)
|
|
|
|
// Click canvas to ensure focus is within graph-canvas-container
|
|
await comfyPage.canvas.click({ position: { x: 400, y: 400 } })
|
|
await comfyPage.nextFrame()
|
|
|
|
await comfyPage.keyboard.press('Period')
|
|
|
|
await expect
|
|
.poll(() => comfyPage.canvasOps.getScale())
|
|
.toBeGreaterThan(0.1)
|
|
})
|
|
|
|
test("'h' locks canvas", async ({ comfyPage }) => {
|
|
await expect.poll(() => comfyPage.canvasOps.isReadOnly()).toBe(false)
|
|
|
|
await comfyPage.keyboard.press('KeyH')
|
|
|
|
await expect.poll(() => comfyPage.canvasOps.isReadOnly()).toBe(true)
|
|
})
|
|
|
|
test("'v' unlocks canvas", async ({ comfyPage }) => {
|
|
// Lock first
|
|
await comfyPage.command.executeCommand('Comfy.Canvas.Lock')
|
|
await expect.poll(() => comfyPage.canvasOps.isReadOnly()).toBe(true)
|
|
|
|
await comfyPage.keyboard.press('KeyV')
|
|
|
|
await expect.poll(() => comfyPage.canvasOps.isReadOnly()).toBe(false)
|
|
})
|
|
})
|
|
|
|
test.describe('Node State Toggles', () => {
|
|
test("'Alt+c' collapses and expands selected nodes", async ({
|
|
comfyPage
|
|
}) => {
|
|
const nodes = await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
|
|
expect(nodes.length).toBeGreaterThan(0)
|
|
const node = nodes[0]
|
|
|
|
await node.click('title')
|
|
|
|
await expect.poll(() => node.isCollapsed()).toBe(false)
|
|
|
|
await comfyPage.keyboard.press('Alt+KeyC')
|
|
await expect.poll(() => node.isCollapsed()).toBe(true)
|
|
|
|
await comfyPage.keyboard.press('Alt+KeyC')
|
|
await expect.poll(() => node.isCollapsed()).toBe(false)
|
|
})
|
|
|
|
test("'Ctrl+m' mutes and unmutes selected nodes", async ({ comfyPage }) => {
|
|
const nodes = await comfyPage.nodeOps.getNodeRefsByType('CLIPTextEncode')
|
|
expect(nodes.length).toBeGreaterThan(0)
|
|
const node = nodes[0]
|
|
|
|
await node.click('title')
|
|
|
|
// Normal mode is ALWAYS (0)
|
|
function getMode() {
|
|
return comfyPage.page.evaluate((nodeId) => {
|
|
return window.app!.canvas.graph!.getNodeById(nodeId)!.mode
|
|
}, node.id)
|
|
}
|
|
|
|
await expect.poll(() => getMode()).toBe(0)
|
|
|
|
await comfyPage.keyboard.press('Control+KeyM')
|
|
// NEVER (2) = muted
|
|
await expect.poll(() => getMode()).toBe(2)
|
|
|
|
await comfyPage.keyboard.press('Control+KeyM')
|
|
await expect.poll(() => getMode()).toBe(0)
|
|
})
|
|
})
|
|
|
|
test.describe('Mode and Panel Toggles', () => {
|
|
test("'Alt+m' toggles app mode", async ({ comfyPage }) => {
|
|
// Set up linearData so app mode has something to show
|
|
await comfyPage.appMode.enterAppModeWithInputs([])
|
|
await expect(comfyPage.appMode.linearWidgets).toBeVisible()
|
|
|
|
// Toggle off with Alt+m
|
|
await comfyPage.page.keyboard.press('Alt+KeyM')
|
|
await expect(comfyPage.appMode.linearWidgets).toBeHidden()
|
|
|
|
// Toggle on again
|
|
await comfyPage.page.keyboard.press('Alt+KeyM')
|
|
await expect(comfyPage.appMode.linearWidgets).toBeVisible()
|
|
})
|
|
|
|
test("'Alt+Shift+m' toggles minimap", async ({ comfyPage }) => {
|
|
await comfyPage.settings.setSetting('Comfy.Minimap.Visible', true)
|
|
await comfyPage.settings.setSetting('Comfy.Graph.CanvasMenu', true)
|
|
await comfyPage.workflow.loadWorkflow('default')
|
|
|
|
const minimap = comfyPage.page.locator('.litegraph-minimap')
|
|
await expect(minimap).toBeVisible()
|
|
|
|
await comfyPage.page.keyboard.press('Alt+Shift+KeyM')
|
|
await expect(minimap).toBeHidden()
|
|
|
|
await comfyPage.page.keyboard.press('Alt+Shift+KeyM')
|
|
await expect(minimap).toBeVisible()
|
|
})
|
|
|
|
test("'Ctrl+`' toggles terminal/logs panel", async ({ comfyPage }) => {
|
|
await expect(comfyPage.bottomPanel.root).toBeHidden()
|
|
|
|
await comfyPage.page.keyboard.press('Control+Backquote')
|
|
await expect(comfyPage.bottomPanel.root).toBeVisible()
|
|
|
|
await comfyPage.page.keyboard.press('Control+Backquote')
|
|
await expect(comfyPage.bottomPanel.root).toBeHidden()
|
|
})
|
|
})
|
|
|
|
test.describe('Queue and Execution', () => {
|
|
test("'Ctrl+Enter' queues prompt", async ({ comfyPage }) => {
|
|
const request = await pressKeyAndExpectRequest(
|
|
comfyPage,
|
|
'Control+Enter',
|
|
'/prompt',
|
|
'POST'
|
|
)
|
|
expect(request.url()).toContain('/prompt')
|
|
})
|
|
|
|
test("'Ctrl+Shift+Enter' queues prompt to front", async ({ comfyPage }) => {
|
|
const request = await pressKeyAndExpectRequest(
|
|
comfyPage,
|
|
'Control+Shift+Enter',
|
|
'/prompt',
|
|
'POST'
|
|
)
|
|
const body = request.postDataJSON()
|
|
expect(body.front).toBe(true)
|
|
})
|
|
|
|
test("'Ctrl+Alt+Enter' interrupts execution", async ({ comfyPage }) => {
|
|
const request = await pressKeyAndExpectRequest(
|
|
comfyPage,
|
|
'Control+Alt+Enter',
|
|
'/interrupt',
|
|
'POST'
|
|
)
|
|
expect(request.url()).toContain('/interrupt')
|
|
})
|
|
})
|
|
|
|
test.describe('File Operations', () => {
|
|
test("'Ctrl+s' triggers save workflow", async ({ comfyPage }) => {
|
|
// On a new unsaved workflow, Ctrl+s triggers Save As dialog.
|
|
// The dialog appearing proves the keybinding was intercepted by the app.
|
|
await comfyPage.keyboard.press('Control+s')
|
|
|
|
// The Save As dialog should appear
|
|
const saveDialog = comfyPage.page.getByRole('dialog')
|
|
await expect(saveDialog).toBeVisible()
|
|
|
|
// Dismiss the dialog
|
|
await comfyPage.keyboard.press('Escape')
|
|
})
|
|
|
|
test("'Ctrl+o' triggers open workflow", async ({ comfyPage }) => {
|
|
// Ctrl+o calls app.ui.loadFile() which clicks a hidden file input.
|
|
// Detect the file input click via an event listener.
|
|
await comfyPage.page.evaluate(() => {
|
|
window.TestCommand = false
|
|
const fileInputs =
|
|
document.querySelectorAll<HTMLInputElement>('input[type="file"]')
|
|
for (const input of fileInputs) {
|
|
input.addEventListener('click', () => {
|
|
window.TestCommand = true
|
|
})
|
|
}
|
|
})
|
|
|
|
await comfyPage.keyboard.press('Control+o')
|
|
|
|
await expect
|
|
.poll(() => comfyPage.page.evaluate(() => window.TestCommand))
|
|
.toBe(true)
|
|
})
|
|
})
|
|
|
|
test.describe('Graph Operations', () => {
|
|
test("'Ctrl+Shift+e' converts selection to subgraph", async ({
|
|
comfyPage
|
|
}) => {
|
|
await expect
|
|
.poll(
|
|
() => comfyPage.nodeOps.getGraphNodesCount(),
|
|
'Default workflow should have multiple nodes'
|
|
)
|
|
.toBeGreaterThan(1)
|
|
|
|
const initialCount = await comfyPage.nodeOps.getGraphNodesCount()
|
|
|
|
// Select all nodes
|
|
await comfyPage.keyboard.press('Control+a')
|
|
|
|
await comfyPage.keyboard.press('Control+Shift+KeyE')
|
|
|
|
// After conversion, node count should decrease
|
|
// (multiple nodes replaced by single subgraph node)
|
|
await expect
|
|
.poll(() => comfyPage.nodeOps.getGraphNodesCount())
|
|
.toBeLessThan(initialCount)
|
|
})
|
|
|
|
test("'r' refreshes node definitions", async ({ comfyPage }) => {
|
|
const request = await pressKeyAndExpectRequest(
|
|
comfyPage,
|
|
'KeyR',
|
|
'/object_info',
|
|
'GET'
|
|
)
|
|
expect(request.url()).toContain('/object_info')
|
|
})
|
|
})
|
|
})
|