Compare commits

..

1 Commits

Author SHA1 Message Date
Benjamin Lu
dc9c9e3242 fix: add media asset addToWorkflow locale 2026-01-29 14:07:47 -08:00
230 changed files with 3055 additions and 7237 deletions

View File

@@ -21,7 +21,6 @@ jobs:
uses: actions/checkout@v6
with:
ref: ${{ !github.event.pull_request.head.repo.fork && github.head_ref || github.ref }}
token: ${{ secrets.PR_GH_TOKEN }}
- name: Setup frontend
uses: ./.github/actions/setup-frontend

View File

@@ -17,8 +17,6 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
token: ${{ secrets.PR_GH_TOKEN }}
# Setup playwright environment
- name: Setup ComfyUI Frontend

View File

@@ -173,7 +173,6 @@ jobs:
uses: actions/checkout@v6
with:
ref: ${{ needs.setup.outputs.branch }}
token: ${{ secrets.PR_GH_TOKEN }}
# Download all changed snapshot files from shards
- name: Download snapshot artifacts

View File

@@ -50,7 +50,6 @@ jobs:
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
ENABLE_MINIFY: 'true'
USE_PROD_CONFIG: 'true'
IS_NIGHTLY: ${{ case(github.ref == 'refs/heads/main', 'true', 'false') }}
run: |
pnpm install --frozen-lockfile
pnpm build

View File

@@ -5,7 +5,7 @@ import * as fs from 'fs'
import type { LGraphNode, LGraph } from '../../src/lib/litegraph/src/litegraph'
import type { NodeId } from '../../src/platform/workflow/validation/schemas/workflowSchema'
import type { KeyCombo } from '../../src/platform/keybindings'
import type { KeyCombo } from '../../src/schemas/keyBindingSchema'
import type { useWorkspaceStore } from '../../src/stores/workspaceStore'
import { NodeBadgeMode } from '../../src/types/nodeSource'
import { ComfyActionbar } from '../helpers/actionbar'

View File

@@ -7,7 +7,7 @@ import { webSocketFixture } from '../fixtures/ws.ts'
const test = mergeTests(comfyPageFixture, webSocketFixture)
test.describe('Actionbar', { tag: '@ui' }, () => {
test.describe('Actionbar', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
})

View File

@@ -2,7 +2,7 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Bottom Panel Shortcuts', { tag: '@ui' }, () => {
test.describe('Bottom Panel Shortcuts', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
})

View File

@@ -2,7 +2,7 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Browser tab title', { tag: '@smoke' }, () => {
test.describe('Browser tab title', () => {
test.describe('Beta Menu', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')

View File

@@ -19,7 +19,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Change Tracker', { tag: '@workflow' }, () => {
test.describe('Change Tracker', () => {
test.describe('Undo/Redo', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')

View File

@@ -151,7 +151,7 @@ const customColorPalettes: Record<string, Palette> = {
}
}
test.describe('Color Palette', { tag: ['@screenshot', '@settings'] }, () => {
test.describe('Color Palette', () => {
test('Can show custom color palette', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.CustomColorPalettes', customColorPalettes)
// Reload to apply the new setting. Setting Comfy.CustomColorPalettes directly
@@ -194,110 +194,104 @@ test.describe('Color Palette', { tag: ['@screenshot', '@settings'] }, () => {
})
})
test.describe(
'Node Color Adjustments',
{ tag: ['@screenshot', '@settings'] },
() => {
test.describe('Node Color Adjustments', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.loadWorkflow('nodes/every_node_color')
})
test('should adjust opacity via node opacity setting', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Node.Opacity', 0.5)
// Drag mouse to force canvas to redraw
await comfyPage.page.mouse.move(0, 0)
await expect(comfyPage.canvas).toHaveScreenshot('node-opacity-0.5.png')
await comfyPage.setSetting('Comfy.Node.Opacity', 1.0)
await comfyPage.page.mouse.move(8, 8)
await expect(comfyPage.canvas).toHaveScreenshot('node-opacity-1.png')
})
test('should persist color adjustments when changing themes', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Node.Opacity', 0.2)
await comfyPage.setSetting('Comfy.ColorPalette', 'arc')
await comfyPage.nextFrame()
await comfyPage.page.mouse.move(0, 0)
await expect(comfyPage.canvas).toHaveScreenshot(
'node-opacity-0.2-arc-theme.png'
)
})
test('should not serialize color adjustments in workflow', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Node.Opacity', 0.5)
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
await comfyPage.nextFrame()
const parsed = await (
await comfyPage.page.waitForFunction(
() => {
const workflow = localStorage.getItem('workflow')
if (!workflow) return null
try {
const data = JSON.parse(workflow)
return Array.isArray(data?.nodes) ? data : null
} catch {
return null
}
},
{ timeout: 3000 }
)
).jsonValue()
expect(parsed.nodes).toBeDefined()
expect(Array.isArray(parsed.nodes)).toBe(true)
for (const node of parsed.nodes) {
if (node.bgcolor) expect(node.bgcolor).not.toMatch(/hsla/)
if (node.color) expect(node.color).not.toMatch(/hsla/)
}
})
test('should lighten node colors when switching to light theme', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('node-lightened-colors.png')
})
test.describe('Context menu color adjustments', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.loadWorkflow('nodes/every_node_color')
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
await comfyPage.setSetting('Comfy.Node.Opacity', 0.3)
const node = await comfyPage.getFirstNodeRef()
await node?.clickContextMenuOption('Colors')
})
test('should adjust opacity via node opacity setting', async ({
test('should persist color adjustments when changing custom node colors', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Node.Opacity', 0.5)
// Drag mouse to force canvas to redraw
await comfyPage.page.mouse.move(0, 0)
await expect(comfyPage.canvas).toHaveScreenshot('node-opacity-0.5.png')
await comfyPage.setSetting('Comfy.Node.Opacity', 1.0)
await comfyPage.page.mouse.move(8, 8)
await expect(comfyPage.canvas).toHaveScreenshot('node-opacity-1.png')
})
test('should persist color adjustments when changing themes', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Node.Opacity', 0.2)
await comfyPage.setSetting('Comfy.ColorPalette', 'arc')
await comfyPage.nextFrame()
await comfyPage.page.mouse.move(0, 0)
await comfyPage.page
.locator('.litemenu-entry.submenu span:has-text("red")')
.click()
await expect(comfyPage.canvas).toHaveScreenshot(
'node-opacity-0.2-arc-theme.png'
'node-opacity-0.3-color-changed.png'
)
})
test('should not serialize color adjustments in workflow', async ({
test('should persist color adjustments when removing custom node color', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Node.Opacity', 0.5)
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
await comfyPage.nextFrame()
const parsed = await (
await comfyPage.page.waitForFunction(
() => {
const workflow = localStorage.getItem('workflow')
if (!workflow) return null
try {
const data = JSON.parse(workflow)
return Array.isArray(data?.nodes) ? data : null
} catch {
return null
}
},
{ timeout: 3000 }
)
).jsonValue()
expect(parsed.nodes).toBeDefined()
expect(Array.isArray(parsed.nodes)).toBe(true)
for (const node of parsed.nodes) {
if (node.bgcolor) expect(node.bgcolor).not.toMatch(/hsla/)
if (node.color) expect(node.color).not.toMatch(/hsla/)
}
})
test('should lighten node colors when switching to light theme', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
await comfyPage.nextFrame()
await comfyPage.page
.locator('.litemenu-entry.submenu span:has-text("No color")')
.click()
await expect(comfyPage.canvas).toHaveScreenshot(
'node-lightened-colors.png'
'node-opacity-0.3-color-removed.png'
)
})
test.describe('Context menu color adjustments', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
await comfyPage.setSetting('Comfy.Node.Opacity', 0.3)
const node = await comfyPage.getFirstNodeRef()
await node?.clickContextMenuOption('Colors')
})
test('should persist color adjustments when changing custom node colors', async ({
comfyPage
}) => {
await comfyPage.page
.locator('.litemenu-entry.submenu span:has-text("red")')
.click()
await expect(comfyPage.canvas).toHaveScreenshot(
'node-opacity-0.3-color-changed.png'
)
})
test('should persist color adjustments when removing custom node color', async ({
comfyPage
}) => {
await comfyPage.page
.locator('.litemenu-entry.submenu span:has-text("No color")')
.click()
await expect(comfyPage.canvas).toHaveScreenshot(
'node-opacity-0.3-color-removed.png'
)
})
})
}
)
})
})

View File

@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Keybindings', { tag: '@keyboard' }, () => {
test.describe('Keybindings', () => {
test('Should execute command', async ({ comfyPage }) => {
await comfyPage.registerCommand('TestCommand', () => {
window['foo'] = true

View File

@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Copy Paste', { tag: ['@screenshot', '@workflow'] }, () => {
test.describe('Copy Paste', () => {
test('Can copy and paste node', async ({ comfyPage }) => {
await comfyPage.clickEmptyLatentNode()
await comfyPage.page.mouse.move(10, 10)

View File

@@ -22,7 +22,7 @@ async function verifyCustomIconSvg(iconElement: Locator) {
expect(decodedSvg).toContain("<svg xmlns='http://www.w3.org/2000/svg'")
}
test.describe('Custom Icons', { tag: '@settings' }, () => {
test.describe('Custom Icons', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
})

View File

@@ -1,14 +1,14 @@
import type { Locator } from '@playwright/test'
import { expect } from '@playwright/test'
import type { Keybinding } from '../../src/platform/keybindings'
import type { Keybinding } from '../../src/schemas/keyBindingSchema'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Load workflow warning', { tag: '@ui' }, () => {
test.describe('Load workflow warning', () => {
test('Should display a warning when loading a workflow with missing nodes', async ({
comfyPage
}) => {

View File

@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('DOM Widget', { tag: '@widget' }, () => {
test.describe('DOM Widget', () => {
test('Collapsed multiline textarea is not visible', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/collapsed_multiline')
const textareaWidget = comfyPage.page.locator('.comfy-multiline-input')
@@ -29,16 +29,12 @@ test.describe('DOM Widget', { tag: '@widget' }, () => {
await expect(lastMultiline).not.toBeVisible()
})
test(
'Position update when entering focus mode',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.executeCommand('Workspace.ToggleFocusMode')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('focus-mode-on.png')
}
)
test('Position update when entering focus mode', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.executeCommand('Workspace.ToggleFocusMode')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('focus-mode-on.png')
})
// No DOM widget should be created by creation of interim LGraphNode objects.
test('Copy node with DOM widget by dragging + alt', async ({ comfyPage }) => {

View File

@@ -6,48 +6,40 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Execution', { tag: ['@smoke', '@workflow'] }, () => {
test(
'Report error on unconnected slot',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.disconnectEdge()
await comfyPage.clickEmptySpace()
test.describe('Execution', () => {
test('Report error on unconnected slot', async ({ comfyPage }) => {
await comfyPage.disconnectEdge()
await comfyPage.clickEmptySpace()
await comfyPage.executeCommand('Comfy.QueuePrompt')
await expect(comfyPage.page.locator('.comfy-error-report')).toBeVisible()
await comfyPage.page.locator('.p-dialog-close-button').click()
await comfyPage.page.locator('.comfy-error-report').waitFor({
state: 'hidden'
})
await expect(comfyPage.canvas).toHaveScreenshot(
'execution-error-unconnected-slot.png'
)
}
)
await comfyPage.executeCommand('Comfy.QueuePrompt')
await expect(comfyPage.page.locator('.comfy-error-report')).toBeVisible()
await comfyPage.page.locator('.p-dialog-close-button').click()
await comfyPage.page.locator('.comfy-error-report').waitFor({
state: 'hidden'
})
await expect(comfyPage.canvas).toHaveScreenshot(
'execution-error-unconnected-slot.png'
)
})
})
test.describe(
'Execute to selected output nodes',
{ tag: ['@smoke', '@workflow'] },
() => {
test('Execute to selected output nodes', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('execution/partial_execution')
const input = await comfyPage.getNodeRefById(3)
const output1 = await comfyPage.getNodeRefById(1)
const output2 = await comfyPage.getNodeRefById(4)
test.describe('Execute to selected output nodes', () => {
test('Execute to selected output nodes', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('execution/partial_execution')
const input = await comfyPage.getNodeRefById(3)
const output1 = await comfyPage.getNodeRefById(1)
const output2 = await comfyPage.getNodeRefById(4)
expect(await (await input.getWidget(0)).getValue()).toBe('foo')
expect(await (await output1.getWidget(0)).getValue()).toBe('')
expect(await (await output2.getWidget(0)).getValue()).toBe('')
await output1.click('title')
await comfyPage.executeCommand('Comfy.QueueSelectedOutputNodes')
await expect(async () => {
expect(await (await input.getWidget(0)).getValue()).toBe('foo')
expect(await (await output1.getWidget(0)).getValue()).toBe('')
expect(await (await output1.getWidget(0)).getValue()).toBe('foo')
expect(await (await output2.getWidget(0)).getValue()).toBe('')
await output1.click('title')
await comfyPage.executeCommand('Comfy.QueueSelectedOutputNodes')
await expect(async () => {
expect(await (await input.getWidget(0)).getValue()).toBe('foo')
expect(await (await output1.getWidget(0)).getValue()).toBe('foo')
expect(await (await output2.getWidget(0)).getValue()).toBe('')
}).toPass({ timeout: 2_000 })
})
}
)
}).toPass({ timeout: 2_000 })
})
})

View File

@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Feature Flags', { tag: ['@slow', '@settings'] }, () => {
test.describe('Feature Flags', () => {
test('Client and server exchange feature flags on connection', async ({
comfyPage
}) => {

View File

@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Graph', { tag: ['@smoke', '@canvas'] }, () => {
test.describe('Graph', () => {
// Should be able to fix link input slot index after swap the input order
// Ref: https://github.com/Comfy-Org/ComfyUI_frontend/issues/3348
test('Fix link input slots', async ({ comfyPage }) => {

View File

@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Graph Canvas Menu', { tag: ['@screenshot', '@canvas'] }, () => {
test.describe('Graph Canvas Menu', () => {
test.beforeEach(async ({ comfyPage }) => {
// Set link render mode to spline to make sure it's not affected by other tests'
// side effects.
@@ -15,33 +15,29 @@ test.describe('Graph Canvas Menu', { tag: ['@screenshot', '@canvas'] }, () => {
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true)
})
test(
'Can toggle link visibility',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
const button = comfyPage.page.getByTestId('toggle-link-visibility-button')
await button.click()
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'canvas-with-hidden-links.png'
)
const hiddenLinkRenderMode = await comfyPage.page.evaluate(() => {
return window['LiteGraph'].HIDDEN_LINK
})
expect(await comfyPage.getSetting('Comfy.LinkRenderMode')).toBe(
hiddenLinkRenderMode
)
test('Can toggle link visibility', async ({ comfyPage }) => {
const button = comfyPage.page.getByTestId('toggle-link-visibility-button')
await button.click()
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'canvas-with-hidden-links.png'
)
const hiddenLinkRenderMode = await comfyPage.page.evaluate(() => {
return window['LiteGraph'].HIDDEN_LINK
})
expect(await comfyPage.getSetting('Comfy.LinkRenderMode')).toBe(
hiddenLinkRenderMode
)
await button.click()
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'canvas-with-visible-links.png'
)
expect(await comfyPage.getSetting('Comfy.LinkRenderMode')).not.toBe(
hiddenLinkRenderMode
)
}
)
await button.click()
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'canvas-with-visible-links.png'
)
expect(await comfyPage.getSetting('Comfy.LinkRenderMode')).not.toBe(
hiddenLinkRenderMode
)
})
test('Toggle minimap button is clickable and has correct test id', async ({
comfyPage

View File

@@ -8,7 +8,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Group Node', { tag: '@node' }, () => {
test.describe('Group Node', () => {
test.describe('Node library sidebar', () => {
const groupNodeName = 'DefautWorkflowGroupNode'
const groupNodeCategory = 'group nodes>workflow'
@@ -89,20 +89,16 @@ test.describe('Group Node', { tag: '@node' }, () => {
// does not have a v-model on the query, so we cannot observe the raw
// query update, and thus cannot set the spinning state between the raw query
// update and the debounced search update.
test.skip(
'Can be added to canvas using search',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
const groupNodeName = 'DefautWorkflowGroupNode'
await comfyPage.convertAllNodesToGroupNode(groupNodeName)
await comfyPage.doubleClickCanvas()
await comfyPage.nextFrame()
await comfyPage.searchBox.fillAndSelectFirstNode(groupNodeName)
await expect(comfyPage.canvas).toHaveScreenshot(
'group-node-copy-added-from-search.png'
)
}
)
test.skip('Can be added to canvas using search', async ({ comfyPage }) => {
const groupNodeName = 'DefautWorkflowGroupNode'
await comfyPage.convertAllNodesToGroupNode(groupNodeName)
await comfyPage.doubleClickCanvas()
await comfyPage.nextFrame()
await comfyPage.searchBox.fillAndSelectFirstNode(groupNodeName)
await expect(comfyPage.canvas).toHaveScreenshot(
'group-node-copy-added-from-search.png'
)
})
test('Displays tooltip on title hover', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.EnableTooltips', true)

View File

@@ -13,7 +13,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Item Interaction', { tag: ['@screenshot', '@node'] }, () => {
test.describe('Item Interaction', () => {
test('Can select/delete all items', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('groups/mixed_graph_items')
await comfyPage.canvas.press('Control+a')
@@ -60,17 +60,13 @@ test.describe('Node Interaction', () => {
})
})
test(
'@2x Can highlight selected',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await expect(comfyPage.canvas).toHaveScreenshot('default.png')
await comfyPage.clickTextEncodeNode1()
await expect(comfyPage.canvas).toHaveScreenshot('selected-node1.png')
await comfyPage.clickTextEncodeNode2()
await expect(comfyPage.canvas).toHaveScreenshot('selected-node2.png')
}
)
test('@2x Can highlight selected', async ({ comfyPage }) => {
await expect(comfyPage.canvas).toHaveScreenshot('default.png')
await comfyPage.clickTextEncodeNode1()
await expect(comfyPage.canvas).toHaveScreenshot('selected-node1.png')
await comfyPage.clickTextEncodeNode2()
await expect(comfyPage.canvas).toHaveScreenshot('selected-node2.png')
})
const dragSelectNodes = async (
comfyPage: ComfyPage,
@@ -154,12 +150,12 @@ test.describe('Node Interaction', () => {
})
})
test('Can drag node', { tag: '@screenshot' }, async ({ comfyPage }) => {
test('Can drag node', async ({ comfyPage }) => {
await comfyPage.dragNode2()
await expect(comfyPage.canvas).toHaveScreenshot('dragged-node1.png')
})
test.describe('Edge Interaction', { tag: '@screenshot' }, () => {
test.describe('Edge Interaction', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.LinkRelease.Action', 'no action')
await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'no action')
@@ -226,18 +222,12 @@ test.describe('Node Interaction', () => {
})
})
test(
'Can adjust widget value',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.adjustWidgetValue()
await expect(comfyPage.canvas).toHaveScreenshot(
'adjusted-widget-value.png'
)
}
)
test('Can adjust widget value', async ({ comfyPage }) => {
await comfyPage.adjustWidgetValue()
await expect(comfyPage.canvas).toHaveScreenshot('adjusted-widget-value.png')
})
test('Link snap to slot', { tag: '@screenshot' }, async ({ comfyPage }) => {
test('Link snap to slot', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('links/snap_to_slot')
await expect(comfyPage.canvas).toHaveScreenshot('snap_to_slot.png')
@@ -254,67 +244,57 @@ test.describe('Node Interaction', () => {
await expect(comfyPage.canvas).toHaveScreenshot('snap_to_slot_linked.png')
})
test(
'Can batch move links by drag with shift',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.loadWorkflow('links/batch_move_links')
await expect(comfyPage.canvas).toHaveScreenshot('batch_move_links.png')
test('Can batch move links by drag with shift', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('links/batch_move_links')
await expect(comfyPage.canvas).toHaveScreenshot('batch_move_links.png')
const outputSlot1Pos = {
x: 304,
y: 127
}
const outputSlot2Pos = {
x: 307,
y: 310
}
await comfyPage.page.keyboard.down('Shift')
await comfyPage.dragAndDrop(outputSlot1Pos, outputSlot2Pos)
await comfyPage.page.keyboard.up('Shift')
await expect(comfyPage.canvas).toHaveScreenshot(
'batch_move_links_moved.png'
)
const outputSlot1Pos = {
x: 304,
y: 127
}
)
test(
'Can batch disconnect links with ctrl+alt+click',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
const loadCheckpointClipSlotPos = {
x: 332,
y: 508
}
await comfyPage.canvas.click({
modifiers: ['Control', 'Alt'],
position: loadCheckpointClipSlotPos
})
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'batch-disconnect-links-disconnected.png'
)
const outputSlot2Pos = {
x: 307,
y: 310
}
)
test(
'Can toggle dom widget node open/closed',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await expect(comfyPage.canvas).toHaveScreenshot('default.png')
await comfyPage.clickTextEncodeNodeToggler()
await expect(comfyPage.canvas).toHaveScreenshot(
'text-encode-toggled-off.png'
)
await comfyPage.delay(1000)
await comfyPage.clickTextEncodeNodeToggler()
await expect(comfyPage.canvas).toHaveScreenshot(
'text-encode-toggled-back-open.png'
)
await comfyPage.page.keyboard.down('Shift')
await comfyPage.dragAndDrop(outputSlot1Pos, outputSlot2Pos)
await comfyPage.page.keyboard.up('Shift')
await expect(comfyPage.canvas).toHaveScreenshot(
'batch_move_links_moved.png'
)
})
test('Can batch disconnect links with ctrl+alt+click', async ({
comfyPage
}) => {
const loadCheckpointClipSlotPos = {
x: 332,
y: 508
}
)
await comfyPage.canvas.click({
modifiers: ['Control', 'Alt'],
position: loadCheckpointClipSlotPos
})
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'batch-disconnect-links-disconnected.png'
)
})
test('Can toggle dom widget node open/closed', async ({ comfyPage }) => {
await expect(comfyPage.canvas).toHaveScreenshot('default.png')
await comfyPage.clickTextEncodeNodeToggler()
await expect(comfyPage.canvas).toHaveScreenshot(
'text-encode-toggled-off.png'
)
await comfyPage.delay(1000)
await comfyPage.clickTextEncodeNodeToggler()
await expect(comfyPage.canvas).toHaveScreenshot(
'text-encode-toggled-back-open.png'
)
})
test('Can close prompt dialog with canvas click (number widget)', async ({
comfyPage
@@ -361,23 +341,19 @@ test.describe('Node Interaction', () => {
await expect(legacyPrompt).toBeHidden()
})
test(
'Can double click node title to edit',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.loadWorkflow('nodes/single_ksampler')
await comfyPage.canvas.dblclick({
position: {
x: 50,
y: 10
},
delay: 5
})
await comfyPage.page.keyboard.type('Hello World')
await comfyPage.page.keyboard.press('Enter')
await expect(comfyPage.canvas).toHaveScreenshot('node-title-edited.png')
}
)
test('Can double click node title to edit', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('nodes/single_ksampler')
await comfyPage.canvas.dblclick({
position: {
x: 50,
y: 10
},
delay: 5
})
await comfyPage.page.keyboard.type('Hello World')
await comfyPage.page.keyboard.press('Enter')
await expect(comfyPage.canvas).toHaveScreenshot('node-title-edited.png')
})
test('Double click node body does not trigger edit', async ({
comfyPage
@@ -393,41 +369,29 @@ test.describe('Node Interaction', () => {
expect(await comfyPage.page.locator('.node-title-editor').count()).toBe(0)
})
test(
'Can group selected nodes',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.GroupSelectedNodes.Padding', 10)
await comfyPage.select2Nodes()
await comfyPage.page.keyboard.down('Control')
await comfyPage.page.keyboard.press('KeyG')
await comfyPage.page.keyboard.up('Control')
await comfyPage.nextFrame()
// Confirm group title
await comfyPage.page.keyboard.press('Enter')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'group-selected-nodes.png'
)
}
)
test('Can group selected nodes', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.GroupSelectedNodes.Padding', 10)
await comfyPage.select2Nodes()
await comfyPage.page.keyboard.down('Control')
await comfyPage.page.keyboard.press('KeyG')
await comfyPage.page.keyboard.up('Control')
await comfyPage.nextFrame()
// Confirm group title
await comfyPage.page.keyboard.press('Enter')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('group-selected-nodes.png')
})
test(
'Can fit group to contents',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.loadWorkflow('groups/oversized_group')
await comfyPage.ctrlA()
await comfyPage.nextFrame()
await comfyPage.executeCommand('Comfy.Graph.FitGroupToContents')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'group-fit-to-contents.png'
)
}
)
test('Can fit group to contents', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('groups/oversized_group')
await comfyPage.ctrlA()
await comfyPage.nextFrame()
await comfyPage.executeCommand('Comfy.Graph.FitGroupToContents')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('group-fit-to-contents.png')
})
test('Can pin/unpin nodes', { tag: '@screenshot' }, async ({ comfyPage }) => {
test('Can pin/unpin nodes', async ({ comfyPage }) => {
await comfyPage.select2Nodes()
await comfyPage.executeCommand('Comfy.Canvas.ToggleSelectedNodes.Pin')
await comfyPage.nextFrame()
@@ -437,22 +401,20 @@ test.describe('Node Interaction', () => {
await expect(comfyPage.canvas).toHaveScreenshot('nodes-unpinned.png')
})
test(
'Can bypass/unbypass nodes with keyboard shortcut',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.select2Nodes()
await comfyPage.canvas.press('Control+b')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('nodes-bypassed.png')
await comfyPage.canvas.press('Control+b')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('nodes-unbypassed.png')
}
)
test('Can bypass/unbypass nodes with keyboard shortcut', async ({
comfyPage
}) => {
await comfyPage.select2Nodes()
await comfyPage.canvas.press('Control+b')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('nodes-bypassed.png')
await comfyPage.canvas.press('Control+b')
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('nodes-unbypassed.png')
})
})
test.describe('Group Interaction', { tag: '@screenshot' }, () => {
test.describe('Group Interaction', () => {
test('Can double click group title to edit', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('groups/single_group')
await comfyPage.canvas.dblclick({
@@ -468,7 +430,7 @@ test.describe('Group Interaction', { tag: '@screenshot' }, () => {
})
})
test.describe('Canvas Interaction', { tag: '@screenshot' }, () => {
test.describe('Canvas Interaction', () => {
test('Can zoom in/out', async ({ comfyPage }) => {
await comfyPage.zoom(-100)
await expect(comfyPage.canvas).toHaveScreenshot('zoomed-in.png')
@@ -670,7 +632,7 @@ test.describe('Widget Interaction', () => {
})
})
test.describe('Load workflow', { tag: '@screenshot' }, () => {
test.describe('Load workflow', () => {
test('Can load workflow with string node id', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('nodes/string_node_id')
await expect(comfyPage.canvas).toHaveScreenshot('string_node_id.png')
@@ -862,7 +824,7 @@ test.describe('Viewport settings', () => {
})
})
test.describe('Canvas Navigation', { tag: '@screenshot' }, () => {
test.describe('Canvas Navigation', () => {
test.describe('Legacy Mode', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Canvas.NavigationMode', 'legacy')

View File

@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Keybindings', { tag: '@keyboard' }, () => {
test.describe('Keybindings', () => {
test('Should not trigger non-modifier keybinding when typing in input fields', async ({
comfyPage
}) => {

View File

@@ -14,7 +14,7 @@ function listenForEvent(): Promise<Event> {
})
}
test.describe('Canvas Event', { tag: '@canvas' }, () => {
test.describe('Canvas Event', () => {
test('Emit litegraph:canvas empty-release', async ({ comfyPage }) => {
const eventPromise = comfyPage.page.evaluate(listenForEvent)
const disconnectPromise = comfyPage.disconnectEdge()

View File

@@ -6,50 +6,46 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe(
'Load Workflow in Media',
{ tag: ['@screenshot', '@workflow'] },
() => {
const fileNames = [
'workflow.webp',
'edited_workflow.webp',
'no_workflow.webp',
'large_workflow.webp',
'workflow_prompt_parameters.png',
'workflow.webm',
// Skipped due to 3d widget unstable visual result.
// 3d widget shows grid after fully loaded.
// 'workflow.glb',
'workflow.mp4',
'workflow.mov',
'workflow.m4v',
'workflow.svg'
// TODO: Re-enable after fixing test asset to use core nodes only
// Currently opens missing nodes dialog which is outside scope of AVIF loading functionality
// 'workflow.avif'
]
fileNames.forEach(async (fileName) => {
test(`Load workflow in ${fileName} (drop from filesystem)`, async ({
comfyPage
}) => {
await comfyPage.dragAndDropFile(`workflowInMedia/${fileName}`)
await expect(comfyPage.canvas).toHaveScreenshot(`${fileName}.png`)
})
test.describe('Load Workflow in Media', () => {
const fileNames = [
'workflow.webp',
'edited_workflow.webp',
'no_workflow.webp',
'large_workflow.webp',
'workflow_prompt_parameters.png',
'workflow.webm',
// Skipped due to 3d widget unstable visual result.
// 3d widget shows grid after fully loaded.
// 'workflow.glb',
'workflow.mp4',
'workflow.mov',
'workflow.m4v',
'workflow.svg'
// TODO: Re-enable after fixing test asset to use core nodes only
// Currently opens missing nodes dialog which is outside scope of AVIF loading functionality
// 'workflow.avif'
]
fileNames.forEach(async (fileName) => {
test(`Load workflow in ${fileName} (drop from filesystem)`, async ({
comfyPage
}) => {
await comfyPage.dragAndDropFile(`workflowInMedia/${fileName}`)
await expect(comfyPage.canvas).toHaveScreenshot(`${fileName}.png`)
})
})
const urls = [
'https://comfyanonymous.github.io/ComfyUI_examples/hidream/hidream_dev_example.png'
]
urls.forEach(async (url) => {
test(`Load workflow from URL ${url} (drop from different browser tabs)`, async ({
comfyPage
}) => {
await comfyPage.dragAndDropURL(url)
const readableName = url.split('/').pop()
await expect(comfyPage.canvas).toHaveScreenshot(
`dropped_workflow_url_${readableName}.png`
)
})
const urls = [
'https://comfyanonymous.github.io/ComfyUI_examples/hidream/hidream_dev_example.png'
]
urls.forEach(async (url) => {
test(`Load workflow from URL ${url} (drop from different browser tabs)`, async ({
comfyPage
}) => {
await comfyPage.dragAndDropURL(url)
const readableName = url.split('/').pop()
await expect(comfyPage.canvas).toHaveScreenshot(
`dropped_workflow_url_${readableName}.png`
)
})
}
)
})
})

View File

@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => {
test.describe('LOD Threshold', () => {
test('Should switch to low quality mode at correct zoom threshold', async ({
comfyPage
}) => {
@@ -149,55 +149,53 @@ test.describe('LOD Threshold', { tag: ['@screenshot', '@canvas'] }, () => {
expect(state.scale).toBeLessThan(0.2) // Very zoomed out
})
test(
'Should show visual difference between LOD on and off',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
// Load a workflow with text-heavy nodes for clear visual difference
await comfyPage.loadWorkflow('default')
test('Should show visual difference between LOD on and off', async ({
comfyPage
}) => {
// Load a workflow with text-heavy nodes for clear visual difference
await comfyPage.loadWorkflow('default')
// Set zoom level clearly below the threshold to ensure LOD activates
const targetZoom = 0.4 // Well below default threshold of ~0.571
// Set zoom level clearly below the threshold to ensure LOD activates
const targetZoom = 0.4 // Well below default threshold of ~0.571
// Zoom to target level
await comfyPage.page.evaluate((zoom) => {
window['app'].canvas.ds.scale = zoom
window['app'].canvas.setDirty(true, true)
}, targetZoom)
await comfyPage.nextFrame()
// Zoom to target level
await comfyPage.page.evaluate((zoom) => {
window['app'].canvas.ds.scale = zoom
window['app'].canvas.setDirty(true, true)
}, targetZoom)
await comfyPage.nextFrame()
// Take snapshot with LOD active (default 8px setting)
await expect(comfyPage.canvas).toHaveScreenshot(
'lod-comparison-low-quality.png'
)
// Take snapshot with LOD active (default 8px setting)
await expect(comfyPage.canvas).toHaveScreenshot(
'lod-comparison-low-quality.png'
)
const lowQualityState = await comfyPage.page.evaluate(() => {
const canvas = window['app'].canvas
return {
lowQuality: canvas.low_quality,
scale: canvas.ds.scale
}
})
expect(lowQualityState.lowQuality).toBe(true)
const lowQualityState = await comfyPage.page.evaluate(() => {
const canvas = window['app'].canvas
return {
lowQuality: canvas.low_quality,
scale: canvas.ds.scale
}
})
expect(lowQualityState.lowQuality).toBe(true)
// Disable LOD to see high quality at same zoom
await comfyPage.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 0)
await comfyPage.nextFrame()
// Disable LOD to see high quality at same zoom
await comfyPage.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 0)
await comfyPage.nextFrame()
// Take snapshot with LOD disabled (full quality at same zoom)
await expect(comfyPage.canvas).toHaveScreenshot(
'lod-comparison-high-quality.png'
)
// Take snapshot with LOD disabled (full quality at same zoom)
await expect(comfyPage.canvas).toHaveScreenshot(
'lod-comparison-high-quality.png'
)
const highQualityState = await comfyPage.page.evaluate(() => {
const canvas = window['app'].canvas
return {
lowQuality: canvas.low_quality,
scale: canvas.ds.scale
}
})
expect(highQualityState.lowQuality).toBe(false)
expect(highQualityState.scale).toBeCloseTo(targetZoom, 2)
}
)
const highQualityState = await comfyPage.page.evaluate(() => {
const canvas = window['app'].canvas
return {
lowQuality: canvas.low_quality,
scale: canvas.ds.scale
}
})
expect(highQualityState.lowQuality).toBe(false)
expect(highQualityState.scale).toBeCloseTo(targetZoom, 2)
})
})

View File

@@ -2,7 +2,7 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Menu', { tag: '@ui' }, () => {
test.describe('Menu', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
})

View File

@@ -2,7 +2,7 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Minimap', { tag: '@canvas' }, () => {
test.describe('Minimap', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting('Comfy.Minimap.Visible', true)

View File

@@ -1,39 +1,35 @@
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { expect } from '@playwright/test'
test.describe(
'Mobile Baseline Snapshots',
{ tag: ['@mobile', '@screenshot'] },
() => {
test('@mobile empty canvas', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.ConfirmClear', false)
await comfyPage.executeCommand('Comfy.ClearWorkflow')
await expect(async () => {
expect(await comfyPage.getGraphNodesCount()).toBe(0)
}).toPass({ timeout: 256 })
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('mobile-empty-canvas.png')
})
test.describe('Mobile Baseline Snapshots', () => {
test('@mobile empty canvas', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.ConfirmClear', false)
await comfyPage.executeCommand('Comfy.ClearWorkflow')
await expect(async () => {
expect(await comfyPage.getGraphNodesCount()).toBe(0)
}).toPass({ timeout: 256 })
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('mobile-empty-canvas.png')
})
test('@mobile default workflow', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('default')
await expect(comfyPage.canvas).toHaveScreenshot(
'mobile-default-workflow.png'
)
})
test('@mobile default workflow', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('default')
await expect(comfyPage.canvas).toHaveScreenshot(
'mobile-default-workflow.png'
)
})
test('@mobile settings dialog', async ({ comfyPage }) => {
await comfyPage.settingDialog.open()
await comfyPage.nextFrame()
test('@mobile settings dialog', async ({ comfyPage }) => {
await comfyPage.settingDialog.open()
await comfyPage.nextFrame()
await expect(comfyPage.settingDialog.root).toHaveScreenshot(
'mobile-settings-dialog.png',
{
mask: [
comfyPage.settingDialog.root.getByTestId('current-user-indicator')
]
}
)
})
}
)
await expect(comfyPage.settingDialog.root).toHaveScreenshot(
'mobile-settings-dialog.png',
{
mask: [
comfyPage.settingDialog.root.getByTestId('current-user-indicator')
]
}
)
})
})

View File

@@ -8,7 +8,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Node Badge', { tag: ['@screenshot', '@smoke', '@node'] }, () => {
test.describe('Node Badge', () => {
test('Can add badge', async ({ comfyPage }) => {
await comfyPage.page.evaluate(() => {
const LGraphBadge = window['LGraphBadge']
@@ -66,60 +66,50 @@ test.describe('Node Badge', { tag: ['@screenshot', '@smoke', '@node'] }, () => {
})
})
test.describe(
'Node source badge',
{ tag: ['@screenshot', '@smoke', '@node'] },
() => {
Object.values(NodeBadgeMode).forEach(async (mode) => {
test(`Shows node badges (${mode})`, async ({ comfyPage }) => {
// Execution error workflow has both custom node and core node.
await comfyPage.loadWorkflow('nodes/execution_error')
await comfyPage.setSetting('Comfy.NodeBadge.NodeSourceBadgeMode', mode)
await comfyPage.setSetting('Comfy.NodeBadge.NodeIdBadgeMode', mode)
await comfyPage.nextFrame()
await comfyPage.resetView()
await expect(comfyPage.canvas).toHaveScreenshot(
`node-badge-${mode}.png`
)
})
})
}
)
test.describe(
'Node badge color',
{ tag: ['@screenshot', '@smoke', '@node'] },
() => {
test('Can show node badge with unknown color palette', async ({
comfyPage
}) => {
await comfyPage.setSetting(
'Comfy.NodeBadge.NodeIdBadgeMode',
NodeBadgeMode.ShowAll
)
await comfyPage.setSetting('Comfy.ColorPalette', 'unknown')
test.describe('Node source badge', () => {
Object.values(NodeBadgeMode).forEach(async (mode) => {
test(`Shows node badges (${mode})`, async ({ comfyPage }) => {
// Execution error workflow has both custom node and core node.
await comfyPage.loadWorkflow('nodes/execution_error')
await comfyPage.setSetting('Comfy.NodeBadge.NodeSourceBadgeMode', mode)
await comfyPage.setSetting('Comfy.NodeBadge.NodeIdBadgeMode', mode)
await comfyPage.nextFrame()
// Click empty space to trigger canvas re-render.
await comfyPage.clickEmptySpace()
await expect(comfyPage.canvas).toHaveScreenshot(
'node-badge-unknown-color-palette.png'
)
await comfyPage.resetView()
await expect(comfyPage.canvas).toHaveScreenshot(`node-badge-${mode}.png`)
})
})
})
test('Can show node badge with light color palette', async ({
comfyPage
}) => {
await comfyPage.setSetting(
'Comfy.NodeBadge.NodeIdBadgeMode',
NodeBadgeMode.ShowAll
)
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
await comfyPage.nextFrame()
// Click empty space to trigger canvas re-render.
await comfyPage.clickEmptySpace()
await expect(comfyPage.canvas).toHaveScreenshot(
'node-badge-light-color-palette.png'
)
})
}
)
test.describe('Node badge color', () => {
test('Can show node badge with unknown color palette', async ({
comfyPage
}) => {
await comfyPage.setSetting(
'Comfy.NodeBadge.NodeIdBadgeMode',
NodeBadgeMode.ShowAll
)
await comfyPage.setSetting('Comfy.ColorPalette', 'unknown')
await comfyPage.nextFrame()
// Click empty space to trigger canvas re-render.
await comfyPage.clickEmptySpace()
await expect(comfyPage.canvas).toHaveScreenshot(
'node-badge-unknown-color-palette.png'
)
})
test('Can show node badge with light color palette', async ({
comfyPage
}) => {
await comfyPage.setSetting(
'Comfy.NodeBadge.NodeIdBadgeMode',
NodeBadgeMode.ShowAll
)
await comfyPage.setSetting('Comfy.ColorPalette', 'light')
await comfyPage.nextFrame()
// Click empty space to trigger canvas re-render.
await comfyPage.clickEmptySpace()
await expect(comfyPage.canvas).toHaveScreenshot(
'node-badge-light-color-palette.png'
)
})
})

View File

@@ -8,7 +8,7 @@ test.beforeEach(async ({ comfyPage }) => {
// If an input is optional by node definition, it should be shown as
// a hollow circle no matter what shape it was defined in the workflow JSON.
test.describe('Optional input', { tag: ['@screenshot', '@node'] }, () => {
test.describe('Optional input', () => {
test('No shape specified', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/optional_input_no_shape')
await expect(comfyPage.canvas).toHaveScreenshot('optional_input.png')

View File

@@ -23,7 +23,7 @@ async function selectNodeWithPan(comfyPage: ComfyPage, nodeRef: NodeReference) {
await nodeRef.click('title')
}
test.describe('Node Help', { tag: ['@slow', '@ui'] }, () => {
test.describe('Node Help', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setup()
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')

View File

@@ -7,7 +7,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Node search box', { tag: '@node' }, () => {
test.describe('Node search box', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.LinkRelease.Action', 'search box')
await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'search box')
@@ -46,14 +46,14 @@ test.describe('Node search box', { tag: '@node' }, () => {
await expect(comfyPage.searchBox.input).toBeVisible()
})
test('Can add node', { tag: '@screenshot' }, async ({ comfyPage }) => {
test('Can add node', async ({ comfyPage }) => {
await comfyPage.doubleClickCanvas()
await expect(comfyPage.searchBox.input).toHaveCount(1)
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler')
await expect(comfyPage.canvas).toHaveScreenshot('added-node.png')
})
test('Can auto link node', { tag: '@screenshot' }, async ({ comfyPage }) => {
test('Can auto link node', async ({ comfyPage }) => {
await comfyPage.disconnectEdge()
// Select the second item as the first item is always reroute
await comfyPage.searchBox.fillAndSelectFirstNode('CLIPTextEncode', {
@@ -62,47 +62,41 @@ test.describe('Node search box', { tag: '@node' }, () => {
await expect(comfyPage.canvas).toHaveScreenshot('auto-linked-node.png')
})
test(
'Can auto link batch moved node',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.loadWorkflow('links/batch_move_links')
test('Can auto link batch moved node', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('links/batch_move_links')
const outputSlot1Pos = {
x: 304,
y: 127
}
const emptySpacePos = {
x: 5,
y: 5
}
await comfyPage.page.keyboard.down('Shift')
await comfyPage.dragAndDrop(outputSlot1Pos, emptySpacePos)
await comfyPage.page.keyboard.up('Shift')
// Select the second item as the first item is always reroute
await comfyPage.searchBox.fillAndSelectFirstNode('Load Checkpoint', {
suggestionIndex: 0
})
await expect(comfyPage.canvas).toHaveScreenshot(
'auto-linked-node-batch.png'
)
const outputSlot1Pos = {
x: 304,
y: 127
}
)
test(
'Link release connecting to node with no slots',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.disconnectEdge()
await expect(comfyPage.searchBox.input).toHaveCount(1)
await comfyPage.page.locator('.p-chip-remove-icon').click()
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler')
await expect(comfyPage.canvas).toHaveScreenshot(
'added-node-no-connection.png'
)
const emptySpacePos = {
x: 5,
y: 5
}
)
await comfyPage.page.keyboard.down('Shift')
await comfyPage.dragAndDrop(outputSlot1Pos, emptySpacePos)
await comfyPage.page.keyboard.up('Shift')
// Select the second item as the first item is always reroute
await comfyPage.searchBox.fillAndSelectFirstNode('Load Checkpoint', {
suggestionIndex: 0
})
await expect(comfyPage.canvas).toHaveScreenshot(
'auto-linked-node-batch.png'
)
})
test('Link release connecting to node with no slots', async ({
comfyPage
}) => {
await comfyPage.disconnectEdge()
await expect(comfyPage.searchBox.input).toHaveCount(1)
await comfyPage.page.locator('.p-chip-remove-icon').click()
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler')
await expect(comfyPage.canvas).toHaveScreenshot(
'added-node-no-connection.png'
)
})
test('Has correct aria-labels on search results', async ({ comfyPage }) => {
const node = 'Load Checkpoint'
@@ -257,45 +251,40 @@ test.describe('Node search box', { tag: '@node' }, () => {
})
})
test.describe('Release context menu', { tag: '@node' }, () => {
test.describe('Release context menu', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.LinkRelease.Action', 'context menu')
await comfyPage.setSetting('Comfy.LinkRelease.ActionShift', 'search box')
await comfyPage.setSetting('Comfy.NodeSearchBoxImpl', 'default')
})
test(
'Can trigger on link release',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.disconnectEdge()
const contextMenu = comfyPage.page.locator('.litecontextmenu')
// Wait for context menu with correct title (slot name | slot type)
// The title shows the output slot name and type from the disconnected link
await expect(contextMenu.locator('.litemenu-title')).toContainText(
'CLIP | CLIP'
)
await comfyPage.page.mouse.move(10, 10)
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'link-release-context-menu.png'
)
}
)
test('Can trigger on link release', async ({ comfyPage }) => {
await comfyPage.disconnectEdge()
const contextMenu = comfyPage.page.locator('.litecontextmenu')
// Wait for context menu with correct title (slot name | slot type)
// The title shows the output slot name and type from the disconnected link
await expect(contextMenu.locator('.litemenu-title')).toContainText(
'CLIP | CLIP'
)
await comfyPage.page.mouse.move(10, 10)
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'link-release-context-menu.png'
)
})
test(
'Can search and add node from context menu',
{ tag: '@screenshot' },
async ({ comfyPage, comfyMouse }) => {
await comfyPage.disconnectEdge()
await comfyMouse.move({ x: 10, y: 10 })
await comfyPage.clickContextMenuItem('Search')
await comfyPage.searchBox.fillAndSelectFirstNode('CLIP Prompt')
await expect(comfyPage.canvas).toHaveScreenshot(
'link-context-menu-search.png'
)
}
)
test('Can search and add node from context menu', async ({
comfyPage,
comfyMouse
}) => {
await comfyPage.disconnectEdge()
await comfyMouse.move({ x: 10, y: 10 })
await comfyPage.clickContextMenuItem('Search')
await comfyPage.searchBox.fillAndSelectFirstNode('CLIP Prompt')
await expect(comfyPage.canvas).toHaveScreenshot(
'link-context-menu-search.png'
)
})
test('Existing user (pre-1.24.1) gets context menu by default on link release', async ({
comfyPage

View File

@@ -6,8 +6,8 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Note Node', { tag: '@node' }, () => {
test('Can load node nodes', { tag: '@screenshot' }, async ({ comfyPage }) => {
test.describe('Note Node', () => {
test('Can load node nodes', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('nodes/note_nodes')
await expect(comfyPage.canvas).toHaveScreenshot('note_nodes.png')
})

View File

@@ -7,7 +7,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Primitive Node', { tag: ['@screenshot', '@node'] }, () => {
test.describe('Primitive Node', () => {
test('Can load with correct size', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('primitive/primitive_node')
await expect(comfyPage.canvas).toHaveScreenshot('primitive_node.png')

View File

@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Record Audio Node', { tag: '@screenshot' }, () => {
test.describe('Record Audio Node', () => {
test('should add a record audio node and take a screenshot', async ({
comfyPage
}) => {

View File

@@ -3,7 +3,7 @@ import { expect } from '@playwright/test'
import type { ComfyPage } from '../fixtures/ComfyPage'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Remote COMBO Widget', { tag: '@widget' }, () => {
test.describe('Remote COMBO Widget', () => {
const mockOptions = ['d', 'c', 'b', 'a']
const addRemoteWidgetNode = async (

View File

@@ -3,7 +3,7 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { getMiddlePoint } from '../fixtures/utils/litegraphUtils'
test.describe('Reroute Node', { tag: ['@screenshot', '@node'] }, () => {
test.describe('Reroute Node', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
})
@@ -38,96 +38,92 @@ test.describe('Reroute Node', { tag: ['@screenshot', '@node'] }, () => {
})
})
test.describe(
'LiteGraph Native Reroute Node',
{ tag: ['@screenshot', '@node'] },
() => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.setSetting('LiteGraph.Reroute.SplineOffset', 80)
})
test.describe('LiteGraph Native Reroute Node', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.setSetting('LiteGraph.Reroute.SplineOffset', 80)
})
test('loads from workflow', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('reroute/native_reroute')
await expect(comfyPage.canvas).toHaveScreenshot('native_reroute.png')
})
test('loads from workflow', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('reroute/native_reroute')
await expect(comfyPage.canvas).toHaveScreenshot('native_reroute.png')
})
test('@2x @0.5x Can add reroute by alt clicking on link', async ({
comfyPage
}) => {
const loadCheckpointNode = (
await comfyPage.getNodeRefsByTitle('Load Checkpoint')
)[0]
const clipEncodeNode = (
await comfyPage.getNodeRefsByTitle('CLIP Text Encode (Prompt)')
)[0]
test('@2x @0.5x Can add reroute by alt clicking on link', async ({
comfyPage
}) => {
const loadCheckpointNode = (
await comfyPage.getNodeRefsByTitle('Load Checkpoint')
)[0]
const clipEncodeNode = (
await comfyPage.getNodeRefsByTitle('CLIP Text Encode (Prompt)')
)[0]
const slot1 = await loadCheckpointNode.getOutput(1)
const slot2 = await clipEncodeNode.getInput(0)
const middlePoint = getMiddlePoint(
await slot1.getPosition(),
await slot2.getPosition()
)
const slot1 = await loadCheckpointNode.getOutput(1)
const slot2 = await clipEncodeNode.getInput(0)
const middlePoint = getMiddlePoint(
await slot1.getPosition(),
await slot2.getPosition()
)
await comfyPage.page.keyboard.down('Alt')
await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y)
await comfyPage.page.keyboard.up('Alt')
await comfyPage.page.keyboard.down('Alt')
await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y)
await comfyPage.page.keyboard.up('Alt')
await expect(comfyPage.canvas).toHaveScreenshot(
'native_reroute_alt_click.png'
)
})
await expect(comfyPage.canvas).toHaveScreenshot(
'native_reroute_alt_click.png'
)
})
test('Can add reroute by clicking middle of link context menu', async ({
comfyPage
}) => {
const loadCheckpointNode = (
await comfyPage.getNodeRefsByTitle('Load Checkpoint')
)[0]
const clipEncodeNode = (
await comfyPage.getNodeRefsByTitle('CLIP Text Encode (Prompt)')
)[0]
test('Can add reroute by clicking middle of link context menu', async ({
comfyPage
}) => {
const loadCheckpointNode = (
await comfyPage.getNodeRefsByTitle('Load Checkpoint')
)[0]
const clipEncodeNode = (
await comfyPage.getNodeRefsByTitle('CLIP Text Encode (Prompt)')
)[0]
const slot1 = await loadCheckpointNode.getOutput(1)
const slot2 = await clipEncodeNode.getInput(0)
const middlePoint = getMiddlePoint(
await slot1.getPosition(),
await slot2.getPosition()
)
const slot1 = await loadCheckpointNode.getOutput(1)
const slot2 = await clipEncodeNode.getInput(0)
const middlePoint = getMiddlePoint(
await slot1.getPosition(),
await slot2.getPosition()
)
await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y)
await comfyPage.page
.locator('.litecontextmenu .litemenu-entry', { hasText: 'Add Reroute' })
.click()
await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y)
await comfyPage.page
.locator('.litecontextmenu .litemenu-entry', { hasText: 'Add Reroute' })
.click()
await expect(comfyPage.canvas).toHaveScreenshot(
'native_reroute_context_menu.png'
)
})
await expect(comfyPage.canvas).toHaveScreenshot(
'native_reroute_context_menu.png'
)
})
test('Can delete link that is connected to two reroutes', async ({
comfyPage
}) => {
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/4695
await comfyPage.loadWorkflow(
'reroute/single-native-reroute-default-workflow'
)
test('Can delete link that is connected to two reroutes', async ({
comfyPage
}) => {
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/4695
await comfyPage.loadWorkflow(
'reroute/single-native-reroute-default-workflow'
)
// To find the clickable midpoint button, we use the hardcoded value from the browser logs
// since the link is a bezier curve and not a straight line.
const middlePoint = { x: 359.4188232421875, y: 468.7716979980469 }
// To find the clickable midpoint button, we use the hardcoded value from the browser logs
// since the link is a bezier curve and not a straight line.
const middlePoint = { x: 359.4188232421875, y: 468.7716979980469 }
// Click the middle point of the link to open the context menu.
await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y)
// Click the middle point of the link to open the context menu.
await comfyPage.page.mouse.click(middlePoint.x, middlePoint.y)
// Click the "Delete" context menu option.
await comfyPage.page
.locator('.litecontextmenu .litemenu-entry', { hasText: 'Delete' })
.click()
// Click the "Delete" context menu option.
await comfyPage.page
.locator('.litecontextmenu .litemenu-entry', { hasText: 'Delete' })
.click()
await expect(comfyPage.canvas).toHaveScreenshot(
'native_reroute_delete_from_midpoint_context_menu.png'
)
})
}
)
await expect(comfyPage.canvas).toHaveScreenshot(
'native_reroute_delete_from_midpoint_context_menu.png'
)
})
})

View File

@@ -7,49 +7,43 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe(
'Canvas Right Click Menu',
{ tag: ['@screenshot', '@ui'] },
() => {
test('Can add node', async ({ comfyPage }) => {
await comfyPage.rightClickCanvas()
await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png')
await comfyPage.page.getByText('Add Node').click()
await comfyPage.nextFrame()
await comfyPage.page.getByText('loaders').click()
await comfyPage.nextFrame()
await comfyPage.page.getByText('Load VAE').click()
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('add-node-node-added.png')
})
test.describe('Canvas Right Click Menu', () => {
test('Can add node', async ({ comfyPage }) => {
await comfyPage.rightClickCanvas()
await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png')
await comfyPage.page.getByText('Add Node').click()
await comfyPage.nextFrame()
await comfyPage.page.getByText('loaders').click()
await comfyPage.nextFrame()
await comfyPage.page.getByText('Load VAE').click()
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('add-node-node-added.png')
})
test('Can add group', async ({ comfyPage }) => {
await comfyPage.rightClickCanvas()
await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png')
await comfyPage.page.getByText('Add Group', { exact: true }).click()
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'add-group-group-added.png'
)
})
test('Can add group', async ({ comfyPage }) => {
await comfyPage.rightClickCanvas()
await expect(comfyPage.canvas).toHaveScreenshot('right-click-menu.png')
await comfyPage.page.getByText('Add Group', { exact: true }).click()
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('add-group-group-added.png')
})
test('Can convert to group node', async ({ comfyPage }) => {
await comfyPage.select2Nodes()
await expect(comfyPage.canvas).toHaveScreenshot('selected-2-nodes.png')
await comfyPage.rightClickCanvas()
await comfyPage.clickContextMenuItem('Convert to Group Node (Deprecated)')
await comfyPage.promptDialogInput.fill('GroupNode2CLIP')
await comfyPage.page.keyboard.press('Enter')
await comfyPage.promptDialogInput.waitFor({ state: 'hidden' })
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'right-click-node-group-node.png'
)
})
}
)
test('Can convert to group node', async ({ comfyPage }) => {
await comfyPage.select2Nodes()
await expect(comfyPage.canvas).toHaveScreenshot('selected-2-nodes.png')
await comfyPage.rightClickCanvas()
await comfyPage.clickContextMenuItem('Convert to Group Node (Deprecated)')
await comfyPage.promptDialogInput.fill('GroupNode2CLIP')
await comfyPage.page.keyboard.press('Enter')
await comfyPage.promptDialogInput.waitFor({ state: 'hidden' })
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'right-click-node-group-node.png'
)
})
})
test.describe('Node Right Click Menu', { tag: ['@screenshot', '@ui'] }, () => {
test.describe('Node Right Click Menu', () => {
test('Can open properties panel', async ({ comfyPage }) => {
await comfyPage.rightClickEmptyLatentNode()
await expect(comfyPage.canvas).toHaveScreenshot('right-click-node.png')

View File

@@ -10,7 +10,7 @@ test.beforeEach(async ({ comfyPage }) => {
const BLUE_COLOR = 'rgb(51, 51, 85)'
const RED_COLOR = 'rgb(85, 51, 51)'
test.describe('Selection Toolbox', { tag: ['@screenshot', '@ui'] }, () => {
test.describe('Selection Toolbox', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
})

View File

@@ -7,190 +7,178 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe(
'Selection Toolbox - More Options Submenus',
{ tag: '@ui' },
() => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
await comfyPage.loadWorkflow('nodes/single_ksampler')
await comfyPage.nextFrame()
await comfyPage.selectNodes(['KSampler'])
await comfyPage.nextFrame()
})
test.describe('Selection Toolbox - More Options Submenus', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)
await comfyPage.loadWorkflow('nodes/single_ksampler')
await comfyPage.nextFrame()
await comfyPage.selectNodes(['KSampler'])
await comfyPage.nextFrame()
})
const openMoreOptions = async (comfyPage: ComfyPage) => {
const ksamplerNodes = await comfyPage.getNodeRefsByTitle('KSampler')
if (ksamplerNodes.length === 0) {
throw new Error('No KSampler nodes found')
}
// Drag the KSampler to the center of the screen
const nodePos = await ksamplerNodes[0].getPosition()
const viewportSize = comfyPage.page.viewportSize()
const centerX = viewportSize.width / 3
const centerY = viewportSize.height / 2
await comfyPage.dragAndDrop(
{ x: nodePos.x, y: nodePos.y },
{ x: centerX, y: centerY }
)
await comfyPage.nextFrame()
await ksamplerNodes[0].click('title')
await comfyPage.nextFrame()
await expect(comfyPage.page.locator('.selection-toolbox')).toBeVisible({
timeout: 5000
})
const moreOptionsBtn = comfyPage.page.locator(
'[data-testid="more-options-button"]'
)
await expect(moreOptionsBtn).toBeVisible({ timeout: 3000 })
await comfyPage.page.click('[data-testid="more-options-button"]')
await comfyPage.nextFrame()
const menuOptionsVisible = await comfyPage.page
.getByText('Rename')
.isVisible({ timeout: 2000 })
.catch(() => false)
if (menuOptionsVisible) {
return
}
await moreOptionsBtn.click({ force: true })
await comfyPage.nextFrame()
const menuOptionsVisibleAfterClick = await comfyPage.page
.getByText('Rename')
.isVisible({ timeout: 2000 })
.catch(() => false)
if (menuOptionsVisibleAfterClick) {
return
}
throw new Error('Could not open More Options menu - popover not showing')
const openMoreOptions = async (comfyPage: ComfyPage) => {
const ksamplerNodes = await comfyPage.getNodeRefsByTitle('KSampler')
if (ksamplerNodes.length === 0) {
throw new Error('No KSampler nodes found')
}
test('opens Node Info from More Options menu', async ({ comfyPage }) => {
await openMoreOptions(comfyPage)
const nodeInfoButton = comfyPage.page.getByText('Node Info', {
exact: true
})
await expect(nodeInfoButton).toBeVisible()
await nodeInfoButton.click()
await comfyPage.nextFrame()
// Drag the KSampler to the center of the screen
const nodePos = await ksamplerNodes[0].getPosition()
const viewportSize = comfyPage.page.viewportSize()
const centerX = viewportSize.width / 3
const centerY = viewportSize.height / 2
await comfyPage.dragAndDrop(
{ x: nodePos.x, y: nodePos.y },
{ x: centerX, y: centerY }
)
await comfyPage.nextFrame()
await ksamplerNodes[0].click('title')
await comfyPage.nextFrame()
await expect(comfyPage.page.locator('.selection-toolbox')).toBeVisible({
timeout: 5000
})
test('changes node shape via Shape submenu', async ({ comfyPage }) => {
const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
const initialShape = await nodeRef.getProperty<number>('shape')
const moreOptionsBtn = comfyPage.page.locator(
'[data-testid="more-options-button"]'
)
await expect(moreOptionsBtn).toBeVisible({ timeout: 3000 })
await openMoreOptions(comfyPage)
await comfyPage.page.getByText('Shape', { exact: true }).hover()
await expect(
comfyPage.page.getByText('Box', { exact: true })
).toBeVisible({
timeout: 5000
})
await comfyPage.page.getByText('Box', { exact: true }).click()
await comfyPage.nextFrame()
await comfyPage.page.click('[data-testid="more-options-button"]')
const newShape = await nodeRef.getProperty<number>('shape')
expect(newShape).not.toBe(initialShape)
expect(newShape).toBe(1)
})
await comfyPage.nextFrame()
test('changes node color via Color submenu swatch', async ({
comfyPage
}) => {
const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
const initialColor = await nodeRef.getProperty<string | undefined>(
'color'
)
const menuOptionsVisible = await comfyPage.page
.getByText('Rename')
.isVisible({ timeout: 2000 })
.catch(() => false)
if (menuOptionsVisible) {
return
}
await openMoreOptions(comfyPage)
await comfyPage.page.getByText('Color', { exact: true }).click()
const blueSwatch = comfyPage.page.locator('[title="Blue"]')
await expect(blueSwatch.first()).toBeVisible({ timeout: 5000 })
await blueSwatch.first().click()
await comfyPage.nextFrame()
await moreOptionsBtn.click({ force: true })
await comfyPage.nextFrame()
const newColor = await nodeRef.getProperty<string | undefined>('color')
expect(newColor).toBe('#223')
if (initialColor) {
expect(newColor).not.toBe(initialColor)
}
})
const menuOptionsVisibleAfterClick = await comfyPage.page
.getByText('Rename')
.isVisible({ timeout: 2000 })
.catch(() => false)
if (menuOptionsVisibleAfterClick) {
return
}
test('renames a node using Rename action', async ({ comfyPage }) => {
const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
await openMoreOptions(comfyPage)
await comfyPage.page
.getByText('Rename', { exact: true })
.click({ force: true })
const input = comfyPage.page.locator(
'.group-title-editor.node-title-editor .editable-text input'
)
await expect(input).toBeVisible()
await input.fill('RenamedNode')
await input.press('Enter')
await comfyPage.nextFrame()
const newTitle = await nodeRef.getProperty<string>('title')
expect(newTitle).toBe('RenamedNode')
})
test('closes More Options menu when clicking outside', async ({
comfyPage
}) => {
await openMoreOptions(comfyPage)
const renameItem = comfyPage.page.getByText('Rename', { exact: true })
await expect(renameItem).toBeVisible({ timeout: 5000 })
// Wait for multiple frames to allow PrimeVue's outside click handler to initialize
for (let i = 0; i < 30; i++) {
await comfyPage.nextFrame()
}
await comfyPage.page
.locator('#graph-canvas')
.click({ position: { x: 0, y: 50 }, force: true })
await comfyPage.nextFrame()
await expect(
comfyPage.page.getByText('Rename', { exact: true })
).not.toBeVisible()
})
test('closes More Options menu when clicking the button again (toggle)', async ({
comfyPage
}) => {
await openMoreOptions(comfyPage)
await expect(
comfyPage.page.getByText('Rename', { exact: true })
).toBeVisible({ timeout: 5000 })
await comfyPage.page.evaluate(() => {
const btn = document.querySelector(
'[data-testid="more-options-button"]'
)
if (btn) {
const event = new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window,
detail: 1
})
btn.dispatchEvent(event)
}
})
await comfyPage.nextFrame()
await expect(
comfyPage.page.getByText('Rename', { exact: true })
).not.toBeVisible()
})
throw new Error('Could not open More Options menu - popover not showing')
}
)
test('opens Node Info from More Options menu', async ({ comfyPage }) => {
await openMoreOptions(comfyPage)
const nodeInfoButton = comfyPage.page.getByText('Node Info', {
exact: true
})
await expect(nodeInfoButton).toBeVisible()
await nodeInfoButton.click()
await comfyPage.nextFrame()
})
test('changes node shape via Shape submenu', async ({ comfyPage }) => {
const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
const initialShape = await nodeRef.getProperty<number>('shape')
await openMoreOptions(comfyPage)
await comfyPage.page.getByText('Shape', { exact: true }).hover()
await expect(comfyPage.page.getByText('Box', { exact: true })).toBeVisible({
timeout: 5000
})
await comfyPage.page.getByText('Box', { exact: true }).click()
await comfyPage.nextFrame()
const newShape = await nodeRef.getProperty<number>('shape')
expect(newShape).not.toBe(initialShape)
expect(newShape).toBe(1)
})
test('changes node color via Color submenu swatch', async ({ comfyPage }) => {
const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
const initialColor = await nodeRef.getProperty<string | undefined>('color')
await openMoreOptions(comfyPage)
await comfyPage.page.getByText('Color', { exact: true }).click()
const blueSwatch = comfyPage.page.locator('[title="Blue"]')
await expect(blueSwatch.first()).toBeVisible({ timeout: 5000 })
await blueSwatch.first().click()
await comfyPage.nextFrame()
const newColor = await nodeRef.getProperty<string | undefined>('color')
expect(newColor).toBe('#223')
if (initialColor) {
expect(newColor).not.toBe(initialColor)
}
})
test('renames a node using Rename action', async ({ comfyPage }) => {
const nodeRef = (await comfyPage.getNodeRefsByTitle('KSampler'))[0]
await openMoreOptions(comfyPage)
await comfyPage.page
.getByText('Rename', { exact: true })
.click({ force: true })
const input = comfyPage.page.locator(
'.group-title-editor.node-title-editor .editable-text input'
)
await expect(input).toBeVisible()
await input.fill('RenamedNode')
await input.press('Enter')
await comfyPage.nextFrame()
const newTitle = await nodeRef.getProperty<string>('title')
expect(newTitle).toBe('RenamedNode')
})
test('closes More Options menu when clicking outside', async ({
comfyPage
}) => {
await openMoreOptions(comfyPage)
const renameItem = comfyPage.page.getByText('Rename', { exact: true })
await expect(renameItem).toBeVisible({ timeout: 5000 })
// Wait for multiple frames to allow PrimeVue's outside click handler to initialize
for (let i = 0; i < 30; i++) {
await comfyPage.nextFrame()
}
await comfyPage.page
.locator('#graph-canvas')
.click({ position: { x: 0, y: 50 }, force: true })
await comfyPage.nextFrame()
await expect(
comfyPage.page.getByText('Rename', { exact: true })
).not.toBeVisible()
})
test('closes More Options menu when clicking the button again (toggle)', async ({
comfyPage
}) => {
await openMoreOptions(comfyPage)
await expect(
comfyPage.page.getByText('Rename', { exact: true })
).toBeVisible({ timeout: 5000 })
await comfyPage.page.evaluate(() => {
const btn = document.querySelector('[data-testid="more-options-button"]')
if (btn) {
const event = new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window,
detail: 1
})
btn.dispatchEvent(event)
}
})
await comfyPage.nextFrame()
await expect(
comfyPage.page.getByText('Rename', { exact: true })
).not.toBeVisible()
})
})

View File

@@ -12,7 +12,7 @@ const SELECTORS = {
promptDialog: '.graphdialog input'
} as const
test.describe('Subgraph Slot Rename Dialog', { tag: '@subgraph' }, () => {
test.describe('Subgraph Slot Rename Dialog', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})

View File

@@ -16,7 +16,7 @@ const SELECTORS = {
domWidget: '.comfy-multiline-input'
} as const
test.describe('Subgraph Operations', { tag: ['@slow', '@subgraph'] }, () => {
test.describe('Subgraph Operations', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})

View File

@@ -13,7 +13,7 @@ async function checkTemplateFileExists(
return response.ok()
}
test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => {
test.describe('Templates', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting('Comfy.Workflow.ShowMissingModelsWarning', false)
@@ -207,114 +207,109 @@ test.describe('Templates', { tag: ['@slow', '@workflow'] }, () => {
await expect(nav).toBeVisible() // Nav should be visible at tablet size
})
test(
'template cards descriptions adjust height dynamically',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
// Setup test by intercepting templates response to inject cards with varying description lengths
await comfyPage.page.route(
'**/templates/index.json',
async (route, _) => {
const response = [
test('template cards descriptions adjust height dynamically', async ({
comfyPage
}) => {
// Setup test by intercepting templates response to inject cards with varying description lengths
await comfyPage.page.route('**/templates/index.json', async (route, _) => {
const response = [
{
moduleName: 'default',
title: 'Test Templates',
type: 'image',
templates: [
{
moduleName: 'default',
title: 'Test Templates',
type: 'image',
templates: [
{
name: 'short-description',
title: 'Short Description',
mediaType: 'image',
mediaSubtype: 'webp',
description: 'This is a short description.'
},
{
name: 'medium-description',
title: 'Medium Description',
mediaType: 'image',
mediaSubtype: 'webp',
description:
'This is a medium length description that should take up two lines on most displays.'
},
{
name: 'long-description',
title: 'Long Description',
mediaType: 'image',
mediaSubtype: 'webp',
description:
'This is a much longer description that should definitely wrap to multiple lines. It contains enough text to demonstrate how the cards handle varying amounts of content while maintaining a consistent layout grid.'
}
]
name: 'short-description',
title: 'Short Description',
mediaType: 'image',
mediaSubtype: 'webp',
description: 'This is a short description.'
},
{
name: 'medium-description',
title: 'Medium Description',
mediaType: 'image',
mediaSubtype: 'webp',
description:
'This is a medium length description that should take up two lines on most displays.'
},
{
name: 'long-description',
title: 'Long Description',
mediaType: 'image',
mediaSubtype: 'webp',
description:
'This is a much longer description that should definitely wrap to multiple lines. It contains enough text to demonstrate how the cards handle varying amounts of content while maintaining a consistent layout grid.'
}
]
await route.fulfill({
status: 200,
body: JSON.stringify(response),
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-store'
}
})
}
)
// Mock the thumbnail images to avoid 404s
await comfyPage.page.route('**/templates/**.webp', async (route) => {
const headers = {
'Content-Type': 'image/webp',
]
await route.fulfill({
status: 200,
body: JSON.stringify(response),
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-store'
}
await route.fulfill({
status: 200,
path: 'browser_tests/assets/example.webp',
headers
})
})
})
// Open templates dialog
await comfyPage.executeCommand('Comfy.BrowseTemplates')
await expect(comfyPage.templates.content).toBeVisible()
// Mock the thumbnail images to avoid 404s
await comfyPage.page.route('**/templates/**.webp', async (route) => {
const headers = {
'Content-Type': 'image/webp',
'Cache-Control': 'no-store'
}
await route.fulfill({
status: 200,
path: 'browser_tests/assets/example.webp',
headers
})
})
// Wait for cards to load
await expect(
comfyPage.page.locator(
'[data-testid="template-workflow-short-description"]'
)
).toBeVisible({ timeout: 5000 })
// Open templates dialog
await comfyPage.executeCommand('Comfy.BrowseTemplates')
await expect(comfyPage.templates.content).toBeVisible()
// Verify all three cards with different descriptions are visible
const shortDescCard = comfyPage.page.locator(
// Wait for cards to load
await expect(
comfyPage.page.locator(
'[data-testid="template-workflow-short-description"]'
)
const mediumDescCard = comfyPage.page.locator(
'[data-testid="template-workflow-medium-description"]'
)
const longDescCard = comfyPage.page.locator(
'[data-testid="template-workflow-long-description"]'
)
).toBeVisible({ timeout: 5000 })
await expect(shortDescCard).toBeVisible()
await expect(mediumDescCard).toBeVisible()
await expect(longDescCard).toBeVisible()
// Verify all three cards with different descriptions are visible
const shortDescCard = comfyPage.page.locator(
'[data-testid="template-workflow-short-description"]'
)
const mediumDescCard = comfyPage.page.locator(
'[data-testid="template-workflow-medium-description"]'
)
const longDescCard = comfyPage.page.locator(
'[data-testid="template-workflow-long-description"]'
)
// Verify descriptions are visible and have line-clamp class
// The description is in a p tag with text-muted class
const shortDesc = shortDescCard.locator('p.text-muted.line-clamp-2')
const mediumDesc = mediumDescCard.locator('p.text-muted.line-clamp-2')
const longDesc = longDescCard.locator('p.text-muted.line-clamp-2')
await expect(shortDescCard).toBeVisible()
await expect(mediumDescCard).toBeVisible()
await expect(longDescCard).toBeVisible()
await expect(shortDesc).toContainText('short description')
await expect(mediumDesc).toContainText('medium length description')
await expect(longDesc).toContainText('much longer description')
// Verify descriptions are visible and have line-clamp class
// The description is in a p tag with text-muted class
const shortDesc = shortDescCard.locator('p.text-muted.line-clamp-2')
const mediumDesc = mediumDescCard.locator('p.text-muted.line-clamp-2')
const longDesc = longDescCard.locator('p.text-muted.line-clamp-2')
// Verify grid layout maintains consistency
const templateGrid = comfyPage.page.locator(
'[data-testid="template-workflows-content"]'
)
await expect(templateGrid).toBeVisible()
await expect(templateGrid).toHaveScreenshot(
'template-grid-varying-content.png'
)
}
)
await expect(shortDesc).toContainText('short description')
await expect(mediumDesc).toContainText('medium length description')
await expect(longDesc).toContainText('much longer description')
// Verify grid layout maintains consistency
const templateGrid = comfyPage.page.locator(
'[data-testid="template-workflows-content"]'
)
await expect(templateGrid).toBeVisible()
await expect(templateGrid).toHaveScreenshot(
'template-grid-varying-content.png'
)
})
})

View File

@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Settings Search functionality', { tag: '@settings' }, () => {
test.describe('Settings Search functionality', () => {
test.beforeEach(async ({ comfyPage }) => {
// Register test settings to verify hidden/deprecated filtering
await comfyPage.page.evaluate(() => {

View File

@@ -5,7 +5,7 @@ import { userSelectPageFixture as test } from '../fixtures/UserSelectPage'
/**
* Expects ComfyUI backend to be launched with `--multi-user` flag.
*/
test.describe('User Select View', { tag: '@settings' }, () => {
test.describe('User Select View', () => {
test.beforeEach(async ({ userSelectPage, page }) => {
await page.goto(userSelectPage.url)
await page.evaluate(() => {

View File

@@ -3,7 +3,7 @@ import { expect } from '@playwright/test'
import type { SystemStats } from '../../src/schemas/apiSchema'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Version Mismatch Warnings', { tag: '@slow' }, () => {
test.describe('Version Mismatch Warnings', () => {
const ALWAYS_AHEAD_OF_INSTALLED_VERSION = '100.100.100'
const ALWAYS_BEHIND_INSTALLED_VERSION = '0.0.0'

View File

@@ -2,7 +2,7 @@ import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Viewport', { tag: ['@screenshot', '@smoke', '@canvas'] }, () => {
test.describe('Viewport', () => {
test('Fits view to nodes when saved viewport position is offscreen', async ({
comfyPage
}) => {

View File

@@ -5,7 +5,7 @@ import {
const CREATE_GROUP_HOTKEY = 'Control+g'
test.describe('Vue Node Groups', { tag: '@screenshot' }, () => {
test.describe('Vue Node Groups', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.setSetting('Comfy.Minimap.ShowGroups', true)

View File

@@ -9,14 +9,10 @@ test.describe('Vue Nodes Canvas Pan', () => {
await comfyPage.vueNodes.waitForNodes()
})
test(
'@mobile Can pan with touch',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.panWithTouch({ x: 64, y: 64 }, { x: 256, y: 256 })
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-nodes-paned-with-touch.png'
)
}
)
test('@mobile Can pan with touch', async ({ comfyPage }) => {
await comfyPage.panWithTouch({ x: 64, y: 64 }, { x: 256, y: 256 })
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-nodes-paned-with-touch.png'
)
})
})

View File

@@ -10,30 +10,25 @@ test.describe('Vue Nodes Zoom', () => {
await comfyPage.vueNodes.waitForNodes()
})
test(
'should not capture drag while zooming with ctrl+shift+drag',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
const checkpointNode =
comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
const nodeBoundingBox = await checkpointNode.boundingBox()
if (!nodeBoundingBox) throw new Error('Node bounding box not available')
test('should not capture drag while zooming with ctrl+shift+drag', async ({
comfyPage
}) => {
const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
const nodeBoundingBox = await checkpointNode.boundingBox()
if (!nodeBoundingBox) throw new Error('Node bounding box not available')
const nodeMidpointX = nodeBoundingBox.x + nodeBoundingBox.width / 2
const nodeMidpointY = nodeBoundingBox.y + nodeBoundingBox.height / 2
const nodeMidpointX = nodeBoundingBox.x + nodeBoundingBox.width / 2
const nodeMidpointY = nodeBoundingBox.y + nodeBoundingBox.height / 2
// Start the Ctrl+Shift drag-to-zoom on the canvas and continue dragging over
// the node. The node should not capture the drag while drag-zooming.
await comfyPage.page.keyboard.down('Control')
await comfyPage.page.keyboard.down('Shift')
await comfyPage.dragAndDrop(
{ x: 200, y: 300 },
{ x: nodeMidpointX, y: nodeMidpointY }
)
// Start the Ctrl+Shift drag-to-zoom on the canvas and continue dragging over
// the node. The node should not capture the drag while drag-zooming.
await comfyPage.page.keyboard.down('Control')
await comfyPage.page.keyboard.down('Shift')
await comfyPage.dragAndDrop(
{ x: 200, y: 300 },
{ x: nodeMidpointX, y: nodeMidpointY }
)
await expect(comfyPage.canvas).toHaveScreenshot(
'zoomed-in-ctrl-shift.png'
)
}
)
await expect(comfyPage.canvas).toHaveScreenshot('zoomed-in-ctrl-shift.png')
})
})

View File

@@ -98,7 +98,7 @@ async function connectSlots(
await nextFrame()
}
test.describe('Vue Node Link Interaction', { tag: '@screenshot' }, () => {
test.describe('Vue Node Link Interaction', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)

View File

@@ -5,7 +5,7 @@ import {
import type { ComfyPage } from '../../../../fixtures/ComfyPage'
import { fitToViewInstant } from '../../../../helpers/fitToView'
test.describe('Vue Node Bring to Front', { tag: '@screenshot' }, () => {
test.describe('Vue Node Bring to Front', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)

View File

@@ -1,8 +1,8 @@
import {
type ComfyPage,
comfyExpect as expect,
comfyPageFixture as test
} from '../../../../fixtures/ComfyPage'
import type { ComfyPage } from '../../../../fixtures/ComfyPage'
import type { Position } from '../../../../fixtures/types'
test.describe('Vue Node Moving', () => {
@@ -29,47 +29,39 @@ test.describe('Vue Node Moving', () => {
expect(diffY).toBeGreaterThan(0)
}
test(
'should allow moving nodes by dragging',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
const loadCheckpointHeaderPos =
await getLoadCheckpointHeaderPos(comfyPage)
await comfyPage.dragAndDrop(loadCheckpointHeaderPos, {
x: 256,
y: 256
})
test('should allow moving nodes by dragging', async ({ comfyPage }) => {
const loadCheckpointHeaderPos = await getLoadCheckpointHeaderPos(comfyPage)
await comfyPage.dragAndDrop(loadCheckpointHeaderPos, {
x: 256,
y: 256
})
const newHeaderPos = await getLoadCheckpointHeaderPos(comfyPage)
await expectPosChanged(loadCheckpointHeaderPos, newHeaderPos)
const newHeaderPos = await getLoadCheckpointHeaderPos(comfyPage)
await expectPosChanged(loadCheckpointHeaderPos, newHeaderPos)
await expect(comfyPage.canvas).toHaveScreenshot('vue-node-moved-node.png')
}
)
await expect(comfyPage.canvas).toHaveScreenshot('vue-node-moved-node.png')
})
test(
'@mobile should allow moving nodes by dragging on touch devices',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
// Disable minimap (gets in way of the node on small screens)
await comfyPage.setSetting('Comfy.Minimap.Visible', false)
test('@mobile should allow moving nodes by dragging on touch devices', async ({
comfyPage
}) => {
// Disable minimap (gets in way of the node on small screens)
await comfyPage.setSetting('Comfy.Minimap.Visible', false)
const loadCheckpointHeaderPos =
await getLoadCheckpointHeaderPos(comfyPage)
await comfyPage.panWithTouch(
{
x: 64,
y: 64
},
loadCheckpointHeaderPos
)
const loadCheckpointHeaderPos = await getLoadCheckpointHeaderPos(comfyPage)
await comfyPage.panWithTouch(
{
x: 64,
y: 64
},
loadCheckpointHeaderPos
)
const newHeaderPos = await getLoadCheckpointHeaderPos(comfyPage)
await expectPosChanged(loadCheckpointHeaderPos, newHeaderPos)
const newHeaderPos = await getLoadCheckpointHeaderPos(comfyPage)
await expectPosChanged(loadCheckpointHeaderPos, newHeaderPos)
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-node-moved-node-touch.png'
)
}
)
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-node-moved-node-touch.png'
)
})
})

View File

@@ -15,25 +15,22 @@ test.describe('Vue Node Bypass', () => {
await comfyPage.vueNodes.waitForNodes()
})
test(
'should allow toggling bypass on a selected node with hotkey',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.page.getByText('Load Checkpoint').click()
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
test('should allow toggling bypass on a selected node with hotkey', async ({
comfyPage
}) => {
await comfyPage.page.getByText('Load Checkpoint').click()
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
const checkpointNode =
comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-node-bypassed-state.png'
)
const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-node-bypassed-state.png'
)
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS)
}
)
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS)
})
test('should allow toggling bypass on multiple selected nodes with hotkey', async ({
comfyPage

View File

@@ -3,7 +3,7 @@ import {
comfyPageFixture as test
} from '../../../fixtures/ComfyPage'
test.describe('Vue Node Custom Colors', { tag: '@screenshot' }, () => {
test.describe('Vue Node Custom Colors', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting('Comfy.Canvas.SelectionToolbox', true)

View File

@@ -12,24 +12,19 @@ test.describe('Vue Node Mute', () => {
await comfyPage.vueNodes.waitForNodes()
})
test(
'should allow toggling mute on a selected node with hotkey',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.page.getByText('Load Checkpoint').click()
await comfyPage.page.keyboard.press(MUTE_HOTKEY)
test('should allow toggling mute on a selected node with hotkey', async ({
comfyPage
}) => {
await comfyPage.page.getByText('Load Checkpoint').click()
await comfyPage.page.keyboard.press(MUTE_HOTKEY)
const checkpointNode =
comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
await expect(checkpointNode).toHaveCSS('opacity', MUTE_OPACITY)
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-node-muted-state.png'
)
const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
await expect(checkpointNode).toHaveCSS('opacity', MUTE_OPACITY)
await expect(comfyPage.canvas).toHaveScreenshot('vue-node-muted-state.png')
await comfyPage.page.keyboard.press(MUTE_HOTKEY)
await expect(checkpointNode).not.toHaveCSS('opacity', MUTE_OPACITY)
}
)
await comfyPage.page.keyboard.press(MUTE_HOTKEY)
await expect(checkpointNode).not.toHaveCSS('opacity', MUTE_OPACITY)
})
test('should allow toggling mute on multiple selected nodes with hotkey', async ({
comfyPage

View File

@@ -9,17 +9,13 @@ test.describe('Vue Upload Widgets', () => {
await comfyPage.vueNodes.waitForNodes()
})
test(
'should hide canvas-only upload buttons',
{ tag: '@screenshot' },
async ({ comfyPage }) => {
await comfyPage.setup()
await comfyPage.loadWorkflow('widgets/all_load_widgets')
await comfyPage.vueNodes.waitForNodes()
test('should hide canvas-only upload buttons', async ({ comfyPage }) => {
await comfyPage.setup()
await comfyPage.loadWorkflow('widgets/all_load_widgets')
await comfyPage.vueNodes.waitForNodes()
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-nodes-upload-widgets.png'
)
}
)
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-nodes-upload-widgets.png'
)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 81 KiB

View File

@@ -6,7 +6,7 @@ test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Combo text widget', { tag: ['@screenshot', '@widget'] }, () => {
test.describe('Combo text widget', () => {
test('Truncates text when resized', async ({ comfyPage }) => {
await comfyPage.resizeLoadCheckpointNode(0.2, 1)
await expect(comfyPage.canvas).toHaveScreenshot(
@@ -79,7 +79,7 @@ test.describe('Combo text widget', { tag: ['@screenshot', '@widget'] }, () => {
})
})
test.describe('Boolean widget', { tag: ['@screenshot', '@widget'] }, () => {
test.describe('Boolean widget', () => {
test('Can toggle', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/boolean_widget')
await expect(comfyPage.canvas).toHaveScreenshot('boolean_widget.png')
@@ -92,7 +92,7 @@ test.describe('Boolean widget', { tag: ['@screenshot', '@widget'] }, () => {
})
})
test.describe('Slider widget', { tag: ['@screenshot', '@widget'] }, () => {
test.describe('Slider widget', () => {
test('Can drag adjust value', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('inputs/simple_slider')
const node = (await comfyPage.getFirstNodeRef())!
@@ -113,7 +113,7 @@ test.describe('Slider widget', { tag: ['@screenshot', '@widget'] }, () => {
})
})
test.describe('Number widget', { tag: ['@screenshot', '@widget'] }, () => {
test.describe('Number widget', () => {
test('Can drag adjust value', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/seed_widget')
@@ -134,28 +134,22 @@ test.describe('Number widget', { tag: ['@screenshot', '@widget'] }, () => {
})
})
test.describe(
'Dynamic widget manipulation',
{ tag: ['@screenshot', '@widget'] },
() => {
test('Auto expand node when widget is added dynamically', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('nodes/single_ksampler')
test.describe('Dynamic widget manipulation', () => {
test('Auto expand node when widget is added dynamically', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('nodes/single_ksampler')
await comfyPage.page.evaluate(() => {
window['graph'].nodes[0].addWidget('number', 'new_widget', 10)
window['graph'].setDirtyCanvas(true, true)
})
await expect(comfyPage.canvas).toHaveScreenshot(
'ksampler_widget_added.png'
)
await comfyPage.page.evaluate(() => {
window['graph'].nodes[0].addWidget('number', 'new_widget', 10)
window['graph'].setDirtyCanvas(true, true)
})
}
)
test.describe('Image widget', { tag: ['@screenshot', '@widget'] }, () => {
await expect(comfyPage.canvas).toHaveScreenshot('ksampler_widget_added.png')
})
})
test.describe('Image widget', () => {
test('Can load image', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/load_image_widget')
await expect(comfyPage.canvas).toHaveScreenshot('load_image_widget.png')
@@ -242,103 +236,99 @@ test.describe('Image widget', { tag: ['@screenshot', '@widget'] }, () => {
})
})
test.describe(
'Animated image widget',
{ tag: ['@screenshot', '@widget'] },
() => {
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/3718
test.skip('Shows preview of uploaded animated image', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('widgets/load_animated_webp')
test.describe('Animated image widget', () => {
// https://github.com/Comfy-Org/ComfyUI_frontend/issues/3718
test.skip('Shows preview of uploaded animated image', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('widgets/load_animated_webp')
// Get position of the load animated webp node
const nodes = await comfyPage.getNodeRefsByType(
'DevToolsLoadAnimatedImageTest'
)
const loadAnimatedWebpNode = nodes[0]
const { x, y } = await loadAnimatedWebpNode.getPosition()
// Get position of the load animated webp node
const nodes = await comfyPage.getNodeRefsByType(
'DevToolsLoadAnimatedImageTest'
)
const loadAnimatedWebpNode = nodes[0]
const { x, y } = await loadAnimatedWebpNode.getPosition()
// Drag and drop image file onto the load animated webp node
await comfyPage.dragAndDropFile('animated_webp.webp', {
dropPosition: { x, y }
})
// Expect the image preview to change automatically
await expect(comfyPage.canvas).toHaveScreenshot(
'animated_image_preview_drag_and_dropped.png'
)
// Move mouse and click on canvas to trigger render
await comfyPage.page.mouse.click(64, 64)
// Expect the image preview to change to the next frame of the animation
await expect(comfyPage.canvas).toHaveScreenshot(
'animated_image_preview_drag_and_dropped_next_frame.png'
)
// Drag and drop image file onto the load animated webp node
await comfyPage.dragAndDropFile('animated_webp.webp', {
dropPosition: { x, y }
})
test('Can drag-and-drop animated webp image', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/load_animated_webp')
// Expect the image preview to change automatically
await expect(comfyPage.canvas).toHaveScreenshot(
'animated_image_preview_drag_and_dropped.png'
)
// Get position of the load animated webp node
const nodes = await comfyPage.getNodeRefsByType(
'DevToolsLoadAnimatedImageTest'
)
const loadAnimatedWebpNode = nodes[0]
const { x, y } = await loadAnimatedWebpNode.getPosition()
// Move mouse and click on canvas to trigger render
await comfyPage.page.mouse.click(64, 64)
// Drag and drop image file onto the load animated webp node
await comfyPage.dragAndDropFile('animated_webp.webp', {
dropPosition: { x, y },
waitForUpload: true
})
// Expect the image preview to change to the next frame of the animation
await expect(comfyPage.canvas).toHaveScreenshot(
'animated_image_preview_drag_and_dropped_next_frame.png'
)
})
// Expect the filename combo value to be updated
const fileComboWidget = await loadAnimatedWebpNode.getWidget(0)
const filename = await fileComboWidget.getValue()
expect(filename).toContain('animated_webp.webp')
test('Can drag-and-drop animated webp image', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/load_animated_webp')
// Get position of the load animated webp node
const nodes = await comfyPage.getNodeRefsByType(
'DevToolsLoadAnimatedImageTest'
)
const loadAnimatedWebpNode = nodes[0]
const { x, y } = await loadAnimatedWebpNode.getPosition()
// Drag and drop image file onto the load animated webp node
await comfyPage.dragAndDropFile('animated_webp.webp', {
dropPosition: { x, y },
waitForUpload: true
})
test('Can preview saved animated webp image', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/save_animated_webp')
// Expect the filename combo value to be updated
const fileComboWidget = await loadAnimatedWebpNode.getWidget(0)
const filename = await fileComboWidget.getValue()
expect(filename).toContain('animated_webp.webp')
})
// Get position of the load animated webp node
const loadNodes = await comfyPage.getNodeRefsByType(
'DevToolsLoadAnimatedImageTest'
)
const loadAnimatedWebpNode = loadNodes[0]
const { x, y } = await loadAnimatedWebpNode.getPosition()
test('Can preview saved animated webp image', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/save_animated_webp')
// Drag and drop image file onto the load animated webp node
await comfyPage.dragAndDropFile('animated_webp.webp', {
dropPosition: { x, y }
})
await comfyPage.nextFrame()
// Get position of the load animated webp node
const loadNodes = await comfyPage.getNodeRefsByType(
'DevToolsLoadAnimatedImageTest'
)
const loadAnimatedWebpNode = loadNodes[0]
const { x, y } = await loadAnimatedWebpNode.getPosition()
// Get the SaveAnimatedWEBP node
const saveNodes = await comfyPage.getNodeRefsByType('SaveAnimatedWEBP')
const saveAnimatedWebpNode = saveNodes[0]
if (!saveAnimatedWebpNode)
throw new Error('SaveAnimatedWEBP node not found')
// Simulate the graph executing
await comfyPage.page.evaluate(
([loadId, saveId]) => {
// Set the output of the SaveAnimatedWEBP node to equal the loader node's image
window['app'].nodeOutputs[saveId] = window['app'].nodeOutputs[loadId]
app.canvas.setDirty(true)
},
[loadAnimatedWebpNode.id, saveAnimatedWebpNode.id]
)
await expect(
comfyPage.page.locator('.dom-widget').locator('img')
).toHaveCount(2)
// Drag and drop image file onto the load animated webp node
await comfyPage.dragAndDropFile('animated_webp.webp', {
dropPosition: { x, y }
})
}
)
await comfyPage.nextFrame()
test.describe('Load audio widget', { tag: ['@screenshot', '@widget'] }, () => {
// Get the SaveAnimatedWEBP node
const saveNodes = await comfyPage.getNodeRefsByType('SaveAnimatedWEBP')
const saveAnimatedWebpNode = saveNodes[0]
if (!saveAnimatedWebpNode)
throw new Error('SaveAnimatedWEBP node not found')
// Simulate the graph executing
await comfyPage.page.evaluate(
([loadId, saveId]) => {
// Set the output of the SaveAnimatedWEBP node to equal the loader node's image
window['app'].nodeOutputs[saveId] = window['app'].nodeOutputs[loadId]
app.canvas.setDirty(true)
},
[loadAnimatedWebpNode.id, saveAnimatedWebpNode.id]
)
await expect(
comfyPage.page.locator('.dom-widget').locator('img')
).toHaveCount(2)
})
})
test.describe('Load audio widget', () => {
test('Can load audio', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/load_audio_widget')
// Wait for the audio widget to be rendered in the DOM
@@ -348,7 +338,7 @@ test.describe('Load audio widget', { tag: ['@screenshot', '@widget'] }, () => {
})
})
test.describe('Unserialized widgets', { tag: '@widget' }, () => {
test.describe('Unserialized widgets', () => {
test('Unserialized widgets values do not mark graph as modified', async ({
comfyPage
}) => {

View File

@@ -1,9 +1,8 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import type { ComfyPage } from '../fixtures/ComfyPage'
import { type ComfyPage, comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Workflow Tab Thumbnails', { tag: '@workflow' }, () => {
test.describe('Workflow Tab Thumbnails', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.setSetting('Comfy.Workflow.WorkflowTabsPosition', 'Topbar')

View File

@@ -1,6 +1,6 @@
{
"name": "@comfyorg/comfyui-frontend",
"version": "1.39.4",
"version": "1.39.1",
"private": true,
"description": "Official front-end implementation of ComfyUI",
"homepage": "https://comfy.org",

View File

@@ -46,8 +46,6 @@ onMounted(() => {
document.addEventListener('contextmenu', showContextMenu)
}
// Handle preload errors that occur during dynamic imports (e.g., stale chunks after deployment)
// See: https://vite.dev/guide/build#load-error-handling
window.addEventListener('vite:preloadError', (event) => {
event.preventDefault()
// eslint-disable-next-line no-undef

View File

@@ -256,11 +256,6 @@
"
/>
</template>
<LogoOverlay
v-if="template.logos?.length"
:logos="template.logos"
:get-logo-url="workflowTemplatesStore.getLogoUrl"
/>
<ProgressSpinner
v-if="loadingTemplate === template.name"
class="absolute inset-0 z-10 m-auto h-12 w-12"
@@ -402,7 +397,6 @@ import AudioThumbnail from '@/components/templates/thumbnails/AudioThumbnail.vue
import CompareSliderThumbnail from '@/components/templates/thumbnails/CompareSliderThumbnail.vue'
import DefaultThumbnail from '@/components/templates/thumbnails/DefaultThumbnail.vue'
import HoverDissolveThumbnail from '@/components/templates/thumbnails/HoverDissolveThumbnail.vue'
import LogoOverlay from '@/components/templates/thumbnails/LogoOverlay.vue'
import Button from '@/components/ui/button/Button.vue'
import BaseModalLayout from '@/components/widget/layout/BaseModalLayout.vue'
import LeftSidePanel from '@/components/widget/panel/LeftSidePanel.vue'
@@ -778,7 +772,7 @@ useIntersectionObserver(loadTrigger, () => {
// Reset pagination when filters change
watch(
[
filteredTemplates,
searchQuery,
selectedNavItem,
sortBy,
selectedModels,

View File

@@ -150,11 +150,13 @@ import { useI18n } from 'vue-i18n'
import SearchBox from '@/components/common/SearchBox.vue'
import Button from '@/components/ui/button/Button.vue'
import { KeyComboImpl } from '@/platform/keybindings/keyCombo'
import { KeybindingImpl } from '@/platform/keybindings/keybinding'
import { useKeybindingService } from '@/platform/keybindings/keybindingService'
import { useKeybindingStore } from '@/platform/keybindings/keybindingStore'
import { useKeybindingService } from '@/services/keybindingService'
import { useCommandStore } from '@/stores/commandStore'
import {
KeyComboImpl,
KeybindingImpl,
useKeybindingStore
} from '@/stores/keybindingStore'
import { normalizeI18nKey } from '@/utils/formatUtil'
import PanelTemplate from './PanelTemplate.vue'
@@ -263,15 +265,18 @@ function cancelEdit() {
}
async function saveKeybinding() {
const commandId = currentEditingCommand.value?.id
const combo = newBindingKeyCombo.value
if (currentEditingCommand.value && newBindingKeyCombo.value) {
const updated = keybindingStore.updateKeybindingOnCommand(
new KeybindingImpl({
commandId: currentEditingCommand.value.id,
combo: newBindingKeyCombo.value
})
)
if (updated) {
await keybindingService.persistUserKeybindings()
}
}
cancelEdit()
if (!combo || commandId == undefined) return
const updated = keybindingStore.updateKeybindingOnCommand(
new KeybindingImpl({ commandId, combo })
)
if (updated) await keybindingService.persistUserKeybindings()
}
async function resetKeybinding(commandData: ICommandData) {

View File

@@ -63,18 +63,14 @@
<i class="pi pi-sign-out" />
{{ $t('auth.signOut.signOut') }}
</Button>
<i18n-t
<Button
v-if="!isApiKeyLogin"
keypath="auth.deleteAccount.contactSupport"
tag="p"
class="text-muted text-sm"
class="w-fit"
variant="destructive-textonly"
@click="handleDeleteAccount"
>
<template #email>
<a href="mailto:support@comfy.org" class="underline"
>support@comfy.org</a
>
</template>
</i18n-t>
{{ $t('auth.deleteAccount.deleteAccount') }}
</Button>
</div>
</div>
@@ -120,6 +116,7 @@ const {
providerName,
providerIcon,
handleSignOut,
handleSignIn
handleSignIn,
handleDeleteAccount
} = useCurrentUser()
</script>

View File

@@ -13,7 +13,7 @@
import Tag from 'primevue/tag'
import { computed } from 'vue'
import type { KeyComboImpl } from '@/platform/keybindings/keyCombo'
import type { KeyComboImpl } from '@/stores/keybindingStore'
const { keyCombo, isModified = false } = defineProps<{
keyCombo: KeyComboImpl

View File

@@ -1,6 +1,6 @@
<template>
<!-- Help Center Popup positioned within canvas area -->
<Teleport to="body">
<Teleport to="#graph-canvas-container">
<div
v-if="isHelpCenterVisible"
class="help-center-popup"

View File

@@ -76,6 +76,14 @@ describe('NodePreview', () => {
expect(wrapper.find('._sb_preview_badge').text()).toBe('Preview')
})
it('applies text-ellipsis class to node header for text truncation', () => {
const wrapper = mountComponent()
const nodeHeader = wrapper.find('.node_header')
expect(nodeHeader.classes()).toContain('text-ellipsis')
expect(nodeHeader.classes()).toContain('mr-4')
})
it('sets title attribute on node header with full display name', () => {
const wrapper = mountComponent()
const nodeHeader = wrapper.find('.node_header')

View File

@@ -10,7 +10,7 @@ https://github.com/Nuked88/ComfyUI-N-Sidebar/blob/7ae7da4a9761009fb6629bc04c6830
<div v-else class="_sb_node_preview bg-component-node-background">
<div class="_sb_table">
<div
class="node_header text-ellipsis"
class="node_header mr-4 text-ellipsis"
:title="nodeDef.display_name"
:style="{
backgroundColor: litegraphColors.NODE_DEFAULT_COLOR,

View File

@@ -70,7 +70,7 @@ import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useCommandStore } from '@/stores/commandStore'
import { useKeybindingStore } from '@/platform/keybindings/keybindingStore'
import { useKeybindingStore } from '@/stores/keybindingStore'
import { useMenuItemStore } from '@/stores/menuItemStore'
import { useUserStore } from '@/stores/userStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'

View File

@@ -2,7 +2,7 @@
<div class="flex h-full flex-col">
<!-- Active Jobs Grid -->
<div
v-if="isQueuePanelV2Enabled && activeJobItems.length"
v-if="activeJobItems.length"
class="grid max-h-[50%] scrollbar-custom overflow-y-auto"
:style="gridStyle"
>
@@ -65,7 +65,6 @@ import MediaAssetCard from '@/platform/assets/components/MediaAssetCard.vue'
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
import { isActiveJobState } from '@/utils/queueUtil'
import { cn } from '@/utils/tailwindUtil'
import { useSettingStore } from '@/platform/settings/settingStore'
const {
assets,
@@ -91,11 +90,6 @@ const emit = defineEmits<{
const { t } = useI18n()
const { jobItems } = useJobList()
const settingStore = useSettingStore()
const isQueuePanelV2Enabled = computed(() =>
settingStore.get('Comfy.Queue.QPOV2')
)
type AssetGridItem = { key: string; asset: AssetItem }

View File

@@ -1,7 +1,7 @@
<template>
<div class="flex h-full flex-col">
<div
v-if="isQueuePanelV2Enabled && activeJobItems.length"
v-if="activeJobItems.length"
class="flex max-h-[50%] scrollbar-custom flex-col gap-2 overflow-y-auto px-2"
>
<AssetsListItem
@@ -133,7 +133,6 @@ import {
} from '@/utils/formatUtil'
import { iconForJobState } from '@/utils/queueDisplay'
import { cn } from '@/utils/tailwindUtil'
import { useSettingStore } from '@/platform/settings/settingStore'
const {
assets,
@@ -155,11 +154,6 @@ const emit = defineEmits<{
const { t } = useI18n()
const { jobItems } = useJobList()
const settingStore = useSettingStore()
const isQueuePanelV2Enabled = computed(() =>
settingStore.get('Comfy.Queue.QPOV2')
)
const hoveredJobId = ref<string | null>(null)
const hoveredAssetId = ref<string | null>(null)

View File

@@ -1,157 +0,0 @@
import { mount } from '@vue/test-utils'
import type { ComponentProps } from 'vue-component-type-helpers'
import { nextTick, ref } from 'vue'
import { describe, expect, it, vi } from 'vitest'
import LogoOverlay from '@/components/templates/thumbnails/LogoOverlay.vue'
import type { LogoInfo } from '@/platform/workflow/templates/types/template'
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: (key: string) =>
key === 'templates.logoProviderSeparator' ? ' & ' : key,
locale: ref('en')
})
}))
type LogoOverlayProps = ComponentProps<typeof LogoOverlay>
describe('LogoOverlay', () => {
function mockGetLogoUrl(provider: string) {
return `/logos/${provider}.png`
}
function mountOverlay(
logos: LogoInfo[],
props: Partial<LogoOverlayProps> = {}
) {
return mount(LogoOverlay, {
props: {
logos,
getLogoUrl: mockGetLogoUrl,
...props
}
})
}
it('renders nothing when logos array is empty', () => {
const wrapper = mountOverlay([])
expect(wrapper.findAll('img')).toHaveLength(0)
})
it('renders a single logo with correct src and alt', () => {
const wrapper = mountOverlay([{ provider: 'Google' }])
const img = wrapper.find('img')
expect(img.attributes('src')).toBe('/logos/Google.png')
expect(img.attributes('alt')).toBe('Google')
})
it('renders multiple separate logo entries', () => {
const wrapper = mountOverlay([
{ provider: 'Google' },
{ provider: 'OpenAI' },
{ provider: 'Stability' }
])
expect(wrapper.findAll('img')).toHaveLength(3)
})
it('displays provider name as label for single provider', () => {
const wrapper = mountOverlay([{ provider: 'Google' }])
const span = wrapper.find('span')
expect(span.text()).toBe('Google')
})
it('images are not draggable', () => {
const wrapper = mountOverlay([{ provider: 'Google' }])
const img = wrapper.find('img')
expect(img.attributes('draggable')).toBe('false')
})
it('filters out logos with empty URLs', () => {
function getLogoUrl(provider: string) {
return provider === 'Google' ? '/logos/Google.png' : ''
}
const wrapper = mount(LogoOverlay, {
props: {
logos: [{ provider: 'Google' }, { provider: 'Unknown' }],
getLogoUrl
}
})
expect(wrapper.findAll('img')).toHaveLength(1)
})
it('renders one logo per unique provider', () => {
const wrapper = mountOverlay([
{ provider: 'Google' },
{ provider: 'OpenAI' }
])
expect(wrapper.findAll('img')).toHaveLength(2)
})
describe('stacked logos', () => {
it('renders multiple providers as stacked overlapping logos', () => {
const wrapper = mountOverlay([{ provider: ['WaveSpeed', 'Hunyuan'] }])
const images = wrapper.findAll('img')
expect(images).toHaveLength(2)
expect(images[0].attributes('alt')).toBe('WaveSpeed')
expect(images[1].attributes('alt')).toBe('Hunyuan')
})
it('joins provider names with locale-aware conjunction for default label', () => {
const wrapper = mountOverlay([{ provider: ['WaveSpeed', 'Hunyuan'] }])
const span = wrapper.find('span')
expect(span.text()).toBe('WaveSpeed and Hunyuan')
})
it('uses custom label when provided', () => {
const wrapper = mountOverlay([
{ provider: ['WaveSpeed', 'Hunyuan'], label: 'Custom Label' }
])
const span = wrapper.find('span')
expect(span.text()).toBe('Custom Label')
})
it('applies negative gap for overlap effect', () => {
const wrapper = mountOverlay([
{ provider: ['WaveSpeed', 'Hunyuan'], gap: -8 }
])
const images = wrapper.findAll('img')
expect(images[1].attributes('style')).toContain('margin-left: -8px')
})
it('applies default gap when not specified', () => {
const wrapper = mountOverlay([{ provider: ['WaveSpeed', 'Hunyuan'] }])
const images = wrapper.findAll('img')
expect(images[1].attributes('style')).toContain('margin-left: -6px')
})
it('filters out invalid providers from stacked logos', () => {
function getLogoUrl(provider: string) {
return provider === 'WaveSpeed' ? '/logos/WaveSpeed.png' : ''
}
const wrapper = mount(LogoOverlay, {
props: {
logos: [{ provider: ['WaveSpeed', 'Unknown'] }],
getLogoUrl
}
})
expect(wrapper.findAll('img')).toHaveLength(1)
expect(wrapper.find('span').text()).toBe('WaveSpeed')
})
})
describe('error handling', () => {
it('keeps showing remaining providers when one image fails in stacked logos', async () => {
const wrapper = mountOverlay([{ provider: ['Google', 'OpenAI'] }])
const images = wrapper.findAll('[data-testid="logo-img"]')
expect(images).toHaveLength(2)
await images[0].trigger('error')
await nextTick()
const remainingImages = wrapper.findAll('[data-testid="logo-img"]')
expect(remainingImages).toHaveLength(2)
expect(remainingImages[1].attributes('alt')).toBe('OpenAI')
})
})
})

View File

@@ -1,117 +0,0 @@
<template>
<div
v-for="logo in validLogos"
:key="logo.key"
:class="
cn('pointer-events-none absolute z-10', logo.position ?? defaultPosition)
"
>
<div
v-show="!hasAllFailed(logo.providers)"
data-testid="logo-pill"
class="flex items-center gap-1.5 rounded-full bg-black/20 py-1 pr-2"
:style="{ opacity: logo.opacity ?? 0.85 }"
>
<div class="ml-0.5 flex items-center">
<img
v-for="(provider, providerIndex) in logo.providers"
:key="provider"
data-testid="logo-img"
:src="logo.urls[providerIndex]"
:alt="provider"
class="h-6 w-6 rounded-full border-2 border-white object-cover"
:class="{ relative: providerIndex > 0 }"
:style="
providerIndex > 0 ? { marginLeft: `${logo.gap ?? -6}px` } : {}
"
draggable="false"
@error="onImageError(provider)"
/>
</div>
<span class="text-sm font-medium text-white">
{{ logo.label }}
</span>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import type { LogoInfo } from '@/platform/workflow/templates/types/template'
import { cn } from '@/utils/tailwindUtil'
const { t, locale } = useI18n()
function formatProviderList(providers: string[]): string {
const localeValue = String(locale.value)
try {
return new Intl.ListFormat(localeValue, {
style: 'long',
type: 'conjunction'
}).format(providers)
} catch {
return providers.join(t('templates.logoProviderSeparator'))
}
}
const {
logos,
getLogoUrl,
defaultPosition = 'top-2 left-2'
} = defineProps<{
logos: LogoInfo[]
getLogoUrl: (provider: string) => string
defaultPosition?: string
}>()
const failedLogos = ref(new Set<string>())
function onImageError(provider: string) {
failedLogos.value = new Set([...failedLogos.value, provider])
}
function hasAllFailed(providers: string[]): boolean {
return providers.every((p) => failedLogos.value.has(p))
}
interface ValidatedLogo {
key: string
providers: string[]
urls: string[]
label: string
position: string | undefined
opacity: number | undefined
gap: number | undefined
}
const validLogos = computed<ValidatedLogo[]>(() => {
const result: ValidatedLogo[] = []
logos.forEach((logo, index) => {
const providers = Array.isArray(logo.provider)
? logo.provider
: [logo.provider]
const urls = providers.map((p) => getLogoUrl(p))
const validProviders = providers.filter((_, i) => urls[i])
const validUrls = urls.filter((url) => url)
if (validProviders.length === 0) return
const providerKey = validProviders.join('-')
const layoutKey = `${logo.position ?? ''}-${logo.opacity ?? ''}-${logo.gap ?? ''}`
result.push({
key: providerKey ? `${providerKey}-${layoutKey}` : `logo-${index}`,
providers: validProviders,
urls: validUrls,
label: logo.label ?? formatProviderList(validProviders),
position: logo.position,
opacity: logo.opacity,
gap: logo.gap
})
})
return result
})
</script>

View File

@@ -1,6 +1,9 @@
import { whenever } from '@vueuse/core'
import { computed, watch } from 'vue'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { t } from '@/i18n'
import { useDialogService } from '@/services/dialogService'
import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore'
import { useCommandStore } from '@/stores/commandStore'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
@@ -10,6 +13,8 @@ export const useCurrentUser = () => {
const authStore = useFirebaseAuthStore()
const commandStore = useCommandStore()
const apiKeyStore = useApiKeyAuthStore()
const dialogService = useDialogService()
const { deleteAccount } = useFirebaseAuthActions()
const firebaseUser = computed(() => authStore.currentUser)
const isApiKeyLogin = computed(() => apiKeyStore.isAuthenticated)
@@ -111,6 +116,18 @@ export const useCurrentUser = () => {
await commandStore.execute('Comfy.User.OpenSignInDialog')
}
const handleDeleteAccount = async () => {
const confirmed = await dialogService.confirm({
title: t('auth.deleteAccount.confirmTitle'),
message: t('auth.deleteAccount.confirmMessage'),
type: 'delete'
})
if (confirmed) {
await deleteAccount()
}
}
return {
loading: authStore.loading,
isLoggedIn,
@@ -124,6 +141,7 @@ export const useCurrentUser = () => {
resolvedUserInfo,
handleSignOut,
handleSignIn,
handleDeleteAccount,
onUserResolved,
onTokenRefreshed,
onUserLogout

View File

@@ -206,6 +206,21 @@ export const useFirebaseAuthActions = () => {
[createReauthenticationRecovery<[string], void>()]
)
const deleteAccount = wrapWithErrorHandlingAsync(
async () => {
await authStore.deleteAccount()
toastStore.add({
severity: 'success',
summary: t('auth.deleteAccount.success'),
detail: t('auth.deleteAccount.successDetail'),
life: 5000
})
},
reportError,
undefined,
[createReauthenticationRecovery<[], void>()]
)
return {
logout,
sendPasswordReset,
@@ -217,6 +232,7 @@ export const useFirebaseAuthActions = () => {
signInWithEmail,
signUpWithEmail,
updatePassword,
deleteAccount,
accessError,
reportError
}

View File

@@ -276,7 +276,7 @@ describe('useSelectedLiteGraphItems', () => {
expect(selectedNodes).toContainEqual(subNode2)
})
it('toggleSelectedNodesMode should not apply state to subgraph children', () => {
it('toggleSelectedNodesMode should apply unified state to subgraph children', () => {
const { toggleSelectedNodesMode } = useSelectedLiteGraphItems()
const subNode1 = { id: 11, mode: LGraphEventMode.ALWAYS } as LGraphNode
const subNode2 = { id: 12, mode: LGraphEventMode.NEVER } as LGraphNode
@@ -294,8 +294,9 @@ describe('useSelectedLiteGraphItems', () => {
// regularNode: BYPASS -> NEVER (since BYPASS != NEVER)
expect(regularNode.mode).toBe(LGraphEventMode.NEVER)
// Subgraph children do not change state
expect(subNode1.mode).toBe(LGraphEventMode.ALWAYS) // was ALWAYS, stays ALWAYS
// Subgraph children get unified state (same as their parent):
// Both children should now be NEVER, regardless of their previous states
expect(subNode1.mode).toBe(LGraphEventMode.NEVER) // was ALWAYS, now NEVER
expect(subNode2.mode).toBe(LGraphEventMode.NEVER) // was NEVER, stays NEVER
})
@@ -316,9 +317,9 @@ describe('useSelectedLiteGraphItems', () => {
// Selected subgraph should toggle to ALWAYS (since it was already NEVER)
expect(subgraphNode.mode).toBe(LGraphEventMode.ALWAYS)
// All children should be unchanged
// All children should also get ALWAYS (unified with parent's new state)
expect(subNode1.mode).toBe(LGraphEventMode.ALWAYS)
expect(subNode2.mode).toBe(LGraphEventMode.BYPASS)
expect(subNode2.mode).toBe(LGraphEventMode.ALWAYS)
})
})

View File

@@ -2,7 +2,10 @@ import type { LGraphNode, Positionable } from '@/lib/litegraph/src/litegraph'
import { LGraphEventMode, Reroute } from '@/lib/litegraph/src/litegraph'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { app } from '@/scripts/app'
import { collectFromNodes } from '@/utils/graphTraversalUtil'
import {
collectFromNodes,
traverseNodesDepthFirst
} from '@/utils/graphTraversalUtil'
/**
* Composable for handling selected LiteGraph items filtering and operations.
@@ -94,10 +97,16 @@ export function useSelectedLiteGraphItems() {
}
/**
* Toggle the execution mode of all selected nodes
* Toggle the execution mode of all selected nodes with unified subgraph behavior.
*
* - If any nodes are not already the specified node mode → all are set to specified mode
* - Otherwise → set all nodes to ALWAYS
* Top-level behavior (selected nodes): Standard toggle logic
* - If the selected node is already in the specified mode → set to ALWAYS
* - Otherwise → set to the specified mode
*
* Subgraph behavior (children of selected subgraph nodes): Unified state application
* - All children inherit the same mode that their parent subgraph node was set to
* - This creates predictable behavior: if you toggle a subgraph to "mute",
* ALL nodes inside become muted, regardless of their previous individual states
*
* @param mode - The LGraphEventMode to toggle to (e.g., NEVER for mute, BYPASS for bypass)
*/
@@ -115,8 +124,27 @@ export function useSelectedLiteGraphItems() {
)
const newModeForSelectedNode = allNodesMatch ? LGraphEventMode.ALWAYS : mode
for (const selectedNode of selectedNodeArray)
// Process each selected node independently to determine its target state and apply to children
selectedNodeArray.forEach((selectedNode) => {
// Apply standard toggle logic to the selected node itself
selectedNode.mode = newModeForSelectedNode
// If this selected node is a subgraph, apply the same mode uniformly to all its children
// This ensures predictable behavior: all children get the same state as their parent
if (selectedNode.isSubgraphNode?.() && selectedNode.subgraph) {
traverseNodesDepthFirst([selectedNode], {
visitor: (node) => {
// Skip the parent node since we already handled it above
if (node === selectedNode) return undefined
// Apply the parent's new mode to all children uniformly
node.mode = newModeForSelectedNode
return undefined
}
})
}
})
}
return {

View File

@@ -59,7 +59,10 @@ function mkFileUrl(props: { ref: ImageRef; preview?: boolean }): string {
}
const pathPlusQueryParams = api.apiURL(
'/view?' + params.toString() + app.getPreviewFormatParam()
'/view?' +
params.toString() +
app.getPreviewFormatParam() +
app.getRandParam()
)
const imageElement = new Image()
imageElement.crossOrigin = 'anonymous'

View File

@@ -10,7 +10,7 @@ type MockTask = {
executionEndTimestamp?: number
previewOutput?: {
isImage: boolean
url: string
urlWithTimestamp: string
}
}
@@ -72,7 +72,7 @@ describe('useCompletionSummary', () => {
if (previewUrl) {
task.previewOutput = {
isImage,
url: previewUrl
urlWithTimestamp: previewUrl
}
}

View File

@@ -78,7 +78,7 @@ export const useCompletionSummary = () => {
completedCount++
const preview = task.previewOutput
if (preview?.isImage) {
imagePreviews.push(preview.url)
imagePreviews.push(preview.urlWithTimestamp)
}
} else if (state === 'failed') {
failedCount++

View File

@@ -862,7 +862,7 @@ export function useCoreCommands(): ComfyCommand[] {
userEmail: userEmail.value,
userId: resolvedUserInfo.value?.id
})
window.open(supportUrl, '_blank', 'noopener,noreferrer')
window.open(supportUrl, '_blank')
}
},
{

View File

@@ -1,4 +1,4 @@
import { refDebounced, watchDebounced } from '@vueuse/core'
import { refThrottled, watchDebounced } from '@vueuse/core'
import Fuse from 'fuse.js'
import type { IFuseOptions } from 'fuse.js'
import { computed, ref, watch } from 'vue'
@@ -119,7 +119,7 @@ export function useTemplateFiltering(
)
})
const debouncedSearchQuery = refDebounced(searchQuery, 150)
const debouncedSearchQuery = refThrottled(searchQuery, 50)
const filteredBySearch = computed(() => {
if (!debouncedSearchQuery.value.trim()) {

View File

@@ -1,4 +1,4 @@
import type { Keybinding } from './types'
import type { Keybinding } from '@/schemas/keyBindingSchema'
export const CORE_KEYBINDINGS: Keybinding[] = [
{
@@ -76,6 +76,7 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
},
commandId: 'Comfy.ShowSettingsDialog'
},
// For '=' both holding shift and not holding shift
{
combo: {
key: '=',
@@ -93,6 +94,7 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
commandId: 'Comfy.Canvas.ZoomIn',
targetElementId: 'graph-canvas'
},
// For number pad '+'
{
combo: {
key: '+',

View File

@@ -557,7 +557,7 @@ function withComfyAutogrow(node: LGraphNode): asserts node is AutogrowNode {
if (!autogrowGroup) return
if (app.configuringGraph && input.widget)
ensureWidgetForInput(node, input)
if (iscon) {
if (iscon && linf) {
if (swappingConnection || !linf) return
autogrowInputConnected(slot, this)
} else {

View File

@@ -292,10 +292,10 @@ export class GroupNodeConfig {
this.processNode(node, seenInputs, seenOutputs)
}
for (const p of this._convertedToProcess) {
for (const p of this.#convertedToProcess) {
p()
}
this._convertedToProcess = []
this.#convertedToProcess = []
if (!this.nodeDef) return
await app.registerNodeDef(`${PREFIX}${SEPARATOR}` + this.name, this.nodeDef)
useNodeDefStore().addNodeDef(this.nodeDef)
@@ -773,7 +773,7 @@ export class GroupNodeConfig {
}
}
private _convertedToProcess: (() => void)[] = []
#convertedToProcess: (() => void)[] = []
processNodeInputs(
node: GroupNodeData,
seenInputs: Record<string, number>,
@@ -804,7 +804,7 @@ export class GroupNodeConfig {
)
// Converted inputs have to be processed after all other nodes as they'll be at the end of the list
this._convertedToProcess.push(() =>
this.#convertedToProcess.push(() =>
this.processConvertedWidgets(
inputs,
node,

View File

@@ -1,5 +1,6 @@
import type { NodeOutputWith } from '@/schemas/apiSchema'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
import { useExtensionService } from '@/services/extensionService'
type ImageCompareOutput = NodeOutputWith<{
@@ -22,14 +23,15 @@ useExtensionService().registerExtension({
onExecuted?.call(this, output)
const { a_images: aImages, b_images: bImages } = output
const rand = app.getRandParam()
const beforeUrl =
aImages && aImages.length > 0
? api.apiURL(`/view?${new URLSearchParams(aImages[0])}`)
? api.apiURL(`/view?${new URLSearchParams(aImages[0])}${rand}`)
: ''
const afterUrl =
bImages && bImages.length > 0
? api.apiURL(`/view?${new URLSearchParams(bImages[0])}`)
? api.apiURL(`/view?${new URLSearchParams(bImages[0])}${rand}`)
: ''
const widget = node.widgets?.find((w) => w.type === 'imagecompare')

View File

@@ -13,9 +13,7 @@ import './imageCompare'
import './imageCrop'
import './load3d'
import './maskeditor'
if (!isCloud) {
await import('./nodeTemplates')
}
import './nodeTemplates'
import './noteNode'
import './previewAny'
import './rerouteNode'

View File

@@ -2,6 +2,7 @@ import type Load3d from '@/extensions/core/load3d/Load3d'
import { t } from '@/i18n'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { api } from '@/scripts/api'
import { app } from '@/scripts/app'
class Load3dUtils {
static async generateThumbnailIfNeeded(
@@ -132,7 +133,8 @@ class Load3dUtils {
const params = [
'filename=' + encodeURIComponent(filename),
'type=' + type,
'subfolder=' + subfolder
'subfolder=' + subfolder,
app.getRandParam().substring(1)
].join('&')
return `/view?${params}`

View File

@@ -103,7 +103,7 @@ export class PrimitiveNode extends LGraphNode {
override onAfterGraphConfigured() {
if (this.outputs[0].links?.length && !this.widgets?.length) {
this._onFirstConnection()
this.#onFirstConnection()
// Populate widget values from config data
if (this.widgets && this.widgets_values) {
@@ -116,7 +116,7 @@ export class PrimitiveNode extends LGraphNode {
}
// Merge values if required
this._mergeWidgetConfig()
this.#mergeWidgetConfig()
}
}
@@ -133,11 +133,11 @@ export class PrimitiveNode extends LGraphNode {
const links = this.outputs[0].links
if (connected) {
if (links?.length && !this.widgets?.length) {
this._onFirstConnection()
this.#onFirstConnection()
}
} else {
// We may have removed a link that caused the constraints to change
this._mergeWidgetConfig()
this.#mergeWidgetConfig()
if (!links?.length) {
this.onLastDisconnect()
@@ -159,7 +159,7 @@ export class PrimitiveNode extends LGraphNode {
}
if (this.outputs[slot].links?.length) {
const valid = this._isValidConnection(input)
const valid = this.#isValidConnection(input)
if (valid) {
// On connect of additional outputs, copy our value to their widget
this.applyToGraph([{ target_id: target_node.id, target_slot } as LLink])
@@ -170,7 +170,7 @@ export class PrimitiveNode extends LGraphNode {
return true
}
private _onFirstConnection(recreating?: boolean) {
#onFirstConnection(recreating?: boolean) {
// First connection can fire before the graph is ready on initial load so random things can be missing
if (!this.outputs[0].links || !this.graph) {
this.onLastDisconnect()
@@ -204,7 +204,7 @@ export class PrimitiveNode extends LGraphNode {
this.outputs[0].name = type
this.outputs[0].widget = widget
this._createWidget(
this.#createWidget(
widget[CONFIG] ?? config,
theirNode,
widget.name,
@@ -213,7 +213,7 @@ export class PrimitiveNode extends LGraphNode {
)
}
private _createWidget(
#createWidget(
inputData: InputSpec,
node: LGraphNode,
widgetName: string,
@@ -307,8 +307,8 @@ export class PrimitiveNode extends LGraphNode {
recreateWidget() {
const values = this.widgets?.map((w) => w.value)
this._removeWidgets()
this._onFirstConnection(true)
this.#removeWidgets()
this.#onFirstConnection(true)
if (values?.length && this.widgets) {
for (let i = 0; i < this.widgets.length; i++)
this.widgets[i].value = values[i]
@@ -316,7 +316,7 @@ export class PrimitiveNode extends LGraphNode {
return this.widgets?.[0]
}
private _mergeWidgetConfig() {
#mergeWidgetConfig() {
// Merge widget configs if the node has multiple outputs
const output = this.outputs[0]
const links = output.links ?? []
@@ -348,11 +348,11 @@ export class PrimitiveNode extends LGraphNode {
const theirInput = theirNode.inputs[link.target_slot]
// Call is valid connection so it can merge the configs when validating
this._isValidConnection(theirInput, hasConfig)
this.#isValidConnection(theirInput, hasConfig)
}
}
private _isValidConnection(input: INodeInputSlot, forceUpdate?: boolean) {
#isValidConnection(input: INodeInputSlot, forceUpdate?: boolean) {
// Only allow connections where the configs match
const output = this.outputs?.[0]
const config2 = (input.widget?.[GET_CONFIG] as () => InputSpec)?.()
@@ -367,7 +367,7 @@ export class PrimitiveNode extends LGraphNode {
)
}
private _removeWidgets() {
#removeWidgets() {
if (this.widgets) {
// Allow widgets to cleanup
for (const w of this.widgets) {
@@ -398,7 +398,7 @@ export class PrimitiveNode extends LGraphNode {
this.outputs[0].name = 'connect to widget input'
delete this.outputs[0].widget
this._removeWidgets()
this.#removeWidgets()
}
}

View File

@@ -31,17 +31,17 @@ export class CanvasPointer {
/** Maximum offset from click location */
static get maxClickDrift() {
return this._maxClickDrift
return this.#maxClickDrift
}
static set maxClickDrift(value) {
this._maxClickDrift = value
this._maxClickDrift2 = value * value
this.#maxClickDrift = value
this.#maxClickDrift2 = value * value
}
private static _maxClickDrift = 6
static #maxClickDrift = 6
/** {@link maxClickDrift} squared. Used to calculate click drift without `sqrt`. */
private static _maxClickDrift2 = this._maxClickDrift ** 2
static #maxClickDrift2 = this.#maxClickDrift ** 2
/** Assume that "wheel" events with both deltaX and deltaY less than this value are trackpad gestures. */
static trackpadThreshold = 60
@@ -153,18 +153,18 @@ export class CanvasPointer {
* Therefore, simply setting this value twice will execute the first callback.
*/
get finally() {
return this._finally
return this.#finally
}
set finally(value) {
try {
this._finally?.()
this.#finally?.()
} finally {
this._finally = value
this.#finally = value
}
}
private _finally?: () => unknown
#finally?: () => unknown
constructor(element: Element) {
this.element = element
@@ -197,7 +197,7 @@ export class CanvasPointer {
// Primary button released - treat as pointerup.
if (!(e.buttons & eDown.buttons)) {
this._completeClick(e)
this.#completeClick(e)
this.reset()
return
}
@@ -209,8 +209,8 @@ export class CanvasPointer {
const longerThanBufferTime =
e.timeStamp - eDown.timeStamp > CanvasPointer.bufferTime
if (longerThanBufferTime || !this._hasSamePosition(e, eDown)) {
this._setDragStarted(e)
if (longerThanBufferTime || !this.#hasSamePosition(e, eDown)) {
this.#setDragStarted(e)
}
}
@@ -221,13 +221,13 @@ export class CanvasPointer {
up(e: CanvasPointerEvent): boolean {
if (e.button !== this.eDown?.button) return false
this._completeClick(e)
this.#completeClick(e)
const { dragStarted } = this
this.reset()
return !dragStarted
}
private _completeClick(e: CanvasPointerEvent): void {
#completeClick(e: CanvasPointerEvent): void {
const { eDown } = this
if (!eDown) return
@@ -236,11 +236,11 @@ export class CanvasPointer {
if (this.dragStarted) {
// A move event already started drag
this.onDragEnd?.(e)
} else if (!this._hasSamePosition(e, eDown)) {
} else if (!this.#hasSamePosition(e, eDown)) {
// Teleport without a move event (e.g. tab out, move, tab back)
this._setDragStarted()
this.#setDragStarted()
this.onDragEnd?.(e)
} else if (this.onDoubleClick && this._isDoubleClick()) {
} else if (this.onDoubleClick && this.#isDoubleClick()) {
// Double-click event
this.onDoubleClick(e)
this.eLastDown = undefined
@@ -258,10 +258,10 @@ export class CanvasPointer {
* @param tolerance2 The maximum distance (squared) before the positions are considered different
* @returns `true` if the two events were no more than {@link maxClickDrift} apart, otherwise `false`
*/
private _hasSamePosition(
#hasSamePosition(
a: PointerEvent,
b: PointerEvent,
tolerance2 = CanvasPointer._maxClickDrift2
tolerance2 = CanvasPointer.#maxClickDrift2
): boolean {
const drift = dist2(a.clientX, a.clientY, b.clientX, b.clientY)
return drift <= tolerance2
@@ -271,21 +271,21 @@ export class CanvasPointer {
* Checks whether the pointer is currently past the max click drift threshold.
* @returns `true` if the latest pointer event is past the the click drift threshold
*/
private _isDoubleClick(): boolean {
#isDoubleClick(): boolean {
const { eDown, eLastDown } = this
if (!eDown || !eLastDown) return false
// Use thrice the drift distance for double-click gap
const tolerance2 = (3 * CanvasPointer._maxClickDrift) ** 2
const tolerance2 = (3 * CanvasPointer.#maxClickDrift) ** 2
const diff = eDown.timeStamp - eLastDown.timeStamp
return (
diff > 0 &&
diff < CanvasPointer.doubleClickTime &&
this._hasSamePosition(eDown, eLastDown, tolerance2)
this.#hasSamePosition(eDown, eLastDown, tolerance2)
)
}
private _setDragStarted(eMove?: CanvasPointerEvent): void {
#setDragStarted(eMove?: CanvasPointerEvent): void {
this.dragStarted = true
this.onDragStart?.(this, eMove)
delete this.onDragStart
@@ -303,14 +303,14 @@ export class CanvasPointer {
const timeSinceLastEvent = Math.max(0, now - this.lastWheelEventTime)
this.lastWheelEventTime = now
if (this._isHighResWheelEvent(e, now)) {
if (this.#isHighResWheelEvent(e, now)) {
this.detectedDevice = 'mouse'
} else if (this._isWithinCooldown(timeSinceLastEvent)) {
if (this._shouldBufferLinuxEvent(e)) {
this._bufferLinuxEvent(e, now)
} else if (this.#isWithinCooldown(timeSinceLastEvent)) {
if (this.#shouldBufferLinuxEvent(e)) {
this.#bufferLinuxEvent(e, now)
}
} else {
this._updateDeviceMode(e, now)
this.#updateDeviceMode(e, now)
this.hasReceivedWheelEvent = true
}
@@ -321,7 +321,7 @@ export class CanvasPointer {
* Validates buffered high res wheel events and switches to mouse mode if pattern matches.
* @returns `true` if switched to mouse mode
*/
private _isHighResWheelEvent(event: WheelEvent, now: number): boolean {
#isHighResWheelEvent(event: WheelEvent, now: number): boolean {
if (!this.bufferedLinuxEvent || this.bufferedLinuxEventTime <= 0) {
return false
}
@@ -329,15 +329,15 @@ export class CanvasPointer {
const timeSinceBuffer = now - this.bufferedLinuxEventTime
if (timeSinceBuffer > CanvasPointer.maxHighResBufferTime) {
this._clearLinuxBuffer()
this.#clearLinuxBuffer()
return false
}
if (
event.deltaX === 0 &&
this._isLinuxWheelPattern(this.bufferedLinuxEvent.deltaY, event.deltaY)
this.#isLinuxWheelPattern(this.bufferedLinuxEvent.deltaY, event.deltaY)
) {
this._clearLinuxBuffer()
this.#clearLinuxBuffer()
return true
}
@@ -347,7 +347,7 @@ export class CanvasPointer {
/**
* Checks if we're within the cooldown period where mode switching is disabled.
*/
private _isWithinCooldown(timeSinceLastEvent: number): boolean {
#isWithinCooldown(timeSinceLastEvent: number): boolean {
const isFirstEvent = !this.hasReceivedWheelEvent
const cooldownExpired = timeSinceLastEvent >= CanvasPointer.trackpadMaxGap
return !isFirstEvent && !cooldownExpired
@@ -356,23 +356,23 @@ export class CanvasPointer {
/**
* Updates the device mode based on event patterns.
*/
private _updateDeviceMode(event: WheelEvent, now: number): void {
if (this._isTrackpadPattern(event)) {
#updateDeviceMode(event: WheelEvent, now: number): void {
if (this.#isTrackpadPattern(event)) {
this.detectedDevice = 'trackpad'
} else if (this._isMousePattern(event)) {
} else if (this.#isMousePattern(event)) {
this.detectedDevice = 'mouse'
} else if (
this.detectedDevice === 'trackpad' &&
this._shouldBufferLinuxEvent(event)
this.#shouldBufferLinuxEvent(event)
) {
this._bufferLinuxEvent(event, now)
this.#bufferLinuxEvent(event, now)
}
}
/**
* Clears the buffered Linux wheel event and associated timer.
*/
private _clearLinuxBuffer(): void {
#clearLinuxBuffer(): void {
this.bufferedLinuxEvent = undefined
this.bufferedLinuxEventTime = 0
if (this.linuxBufferTimeoutId !== undefined) {
@@ -385,7 +385,7 @@ export class CanvasPointer {
* Checks if the event matches trackpad input patterns.
* @param event The wheel event to check
*/
private _isTrackpadPattern(event: WheelEvent): boolean {
#isTrackpadPattern(event: WheelEvent): boolean {
// Two-finger panning: non-zero deltaX AND deltaY
if (event.deltaX !== 0 && event.deltaY !== 0) return true
@@ -399,7 +399,7 @@ export class CanvasPointer {
* Checks if the event matches mouse wheel input patterns.
* @param event The wheel event to check
*/
private _isMousePattern(event: WheelEvent): boolean {
#isMousePattern(event: WheelEvent): boolean {
const absoluteDeltaY = Math.abs(event.deltaY)
// Primary threshold for switching from trackpad to mouse
@@ -417,7 +417,7 @@ export class CanvasPointer {
* Checks if the event should be buffered as a potential Linux wheel event.
* @param event The wheel event to check
*/
private _shouldBufferLinuxEvent(event: WheelEvent): boolean {
#shouldBufferLinuxEvent(event: WheelEvent): boolean {
const absoluteDeltaY = Math.abs(event.deltaY)
const isInLinuxRange = absoluteDeltaY >= 10 && absoluteDeltaY < 60
const isVerticalOnly = event.deltaX === 0
@@ -436,7 +436,7 @@ export class CanvasPointer {
* @param event The event to buffer
* @param now The current timestamp
*/
private _bufferLinuxEvent(event: WheelEvent, now: number): void {
#bufferLinuxEvent(event: WheelEvent, now: number): void {
if (this.linuxBufferTimeoutId !== undefined) {
clearTimeout(this.linuxBufferTimeoutId)
}
@@ -446,7 +446,7 @@ export class CanvasPointer {
// Set timeout to clear buffer after 10ms
this.linuxBufferTimeoutId = setTimeout(() => {
this._clearLinuxBuffer()
this.#clearLinuxBuffer()
}, CanvasPointer.maxHighResBufferTime)
}
@@ -455,7 +455,7 @@ export class CanvasPointer {
* @param deltaY1 The first deltaY value
* @param deltaY2 The second deltaY value
*/
private _isLinuxWheelPattern(deltaY1: number, deltaY2: number): boolean {
#isLinuxWheelPattern(deltaY1: number, deltaY2: number): boolean {
const absolute1 = Math.abs(deltaY1)
const absolute2 = Math.abs(deltaY2)

View File

@@ -81,7 +81,7 @@ export class DragAndScale {
* Returns `true` if the current state has changed from the previous state.
* @returns `true` if the current state has changed from the previous state, otherwise `false`.
*/
private _stateHasChanged(): boolean {
#stateHasChanged(): boolean {
const current = this.state
const previous = this.lastState
@@ -95,7 +95,7 @@ export class DragAndScale {
computeVisibleArea(viewport: Rect | undefined): void {
const { scale, offset, visible_area } = this
if (this._stateHasChanged()) {
if (this.#stateHasChanged()) {
this.onChanged?.(scale, offset)
copyState(this.state, this.lastState)
}

View File

@@ -1,10 +1,6 @@
import { describe, expect, it } from 'vitest'
import { LGraph, LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph'
import {
createTestSubgraphData,
createTestSubgraphNode
} from './subgraph/__fixtures__/subgraphHelpers'
import { test } from './__fixtures__/testExtensions'
@@ -210,70 +206,6 @@ describe('Graph Clearing and Callbacks', () => {
})
})
describe('Subgraph Definition Garbage Collection', () => {
function createSubgraphWithNodes(rootGraph: LGraph, nodeCount: number) {
const subgraph = rootGraph.createSubgraph(createTestSubgraphData())
const innerNodes: LGraphNode[] = []
for (let i = 0; i < nodeCount; i++) {
const node = new LGraphNode(`Inner Node ${i}`)
subgraph.add(node)
innerNodes.push(node)
}
return { subgraph, innerNodes }
}
it('removing SubgraphNode fires onRemoved for inner nodes', () => {
const rootGraph = new LGraph()
const { subgraph, innerNodes } = createSubgraphWithNodes(rootGraph, 2)
const removedNodeIds = new Set<string>()
for (const node of innerNodes) {
node.onRemoved = () => removedNodeIds.add(String(node.id))
}
const subgraphNode = createTestSubgraphNode(subgraph, { pos: [100, 100] })
rootGraph.add(subgraphNode)
expect(subgraph.nodes.length).toBe(2)
rootGraph.remove(subgraphNode)
expect(removedNodeIds.size).toBe(2)
})
it('removing SubgraphNode fires onNodeRemoved callback', () => {
const rootGraph = new LGraph()
const { subgraph } = createSubgraphWithNodes(rootGraph, 2)
const graphRemovedNodeIds = new Set<string>()
subgraph.onNodeRemoved = (node) => graphRemovedNodeIds.add(String(node.id))
const subgraphNode = createTestSubgraphNode(subgraph, { pos: [100, 100] })
rootGraph.add(subgraphNode)
rootGraph.remove(subgraphNode)
expect(graphRemovedNodeIds.size).toBe(2)
})
it('subgraph definition is removed when SubgraphNode is removed', () => {
const rootGraph = new LGraph()
const { subgraph } = createSubgraphWithNodes(rootGraph, 1)
const subgraphId = subgraph.id
const subgraphNode = createTestSubgraphNode(subgraph, { pos: [100, 100] })
rootGraph.add(subgraphNode)
expect(rootGraph.subgraphs.has(subgraphId)).toBe(true)
rootGraph.remove(subgraphNode)
expect(rootGraph.subgraphs.has(subgraphId)).toBe(false)
})
})
describe('Legacy LGraph Compatibility Layer', () => {
test('can be extended via prototype', ({ expect, minimalGraph }) => {
// @ts-expect-error Should always be an error.

View File

@@ -244,7 +244,7 @@ export class LGraph
}
/** Internal only. Not required for serialisation; calculated on deserialise. */
private _lastFloatingLinkId: number = 0
#lastFloatingLinkId: number = 0
private readonly floatingLinksInternal: Map<LinkId, LLink> = new Map()
get floatingLinks(): ReadonlyMap<LinkId, LLink> {
@@ -365,7 +365,7 @@ export class LGraph
this.reroutes.clear()
this.floatingLinksInternal.clear()
this._lastFloatingLinkId = 0
this.#lastFloatingLinkId = 0
// other scene stuff
this._groups = []
@@ -992,16 +992,6 @@ export class LGraph
}
}
// Subgraph cleanup (use local const to avoid type narrowing affecting node.graph assignment)
const subgraphNode = node.isSubgraphNode() ? node : null
if (subgraphNode) {
for (const innerNode of subgraphNode.subgraph.nodes) {
innerNode.onRemoved?.()
subgraphNode.subgraph.onNodeRemoved?.(innerNode)
}
this.rootGraph.subgraphs.delete(subgraphNode.subgraph.id)
}
// callback
node.onRemoved?.()
@@ -1304,7 +1294,7 @@ export class LGraph
addFloatingLink(link: LLink): LLink {
if (link.id === -1) {
link.id = ++this._lastFloatingLinkId
link.id = ++this.#lastFloatingLinkId
}
this.floatingLinksInternal.set(link.id, link)
@@ -2175,16 +2165,8 @@ export class LGraph
}
}
/**
* Custom JSON serialization to prevent circular reference errors.
* Called automatically by JSON.stringify().
*/
toJSON(): ISerialisedGraph {
return this.serialize()
}
/** @returns The drag and scale state of the first attached canvas, otherwise `undefined`. */
private _getDragAndScale(): DragAndScaleState | undefined {
#getDragAndScale(): DragAndScaleState | undefined {
const ds = this.list_of_graphcanvas?.at(0)?.ds
if (ds) return { scale: ds.scale, offset: ds.offset }
}
@@ -2224,7 +2206,7 @@ export class LGraph
// Save scale and offset
const extra = { ...this.extra }
if (LiteGraph.saveViewportWithGraph) extra.ds = this._getDragAndScale()
if (LiteGraph.saveViewportWithGraph) extra.ds = this.#getDragAndScale()
if (!extra.ds) delete extra.ds
const data: ReturnType<typeof this.asSerialisable> = {
@@ -2414,8 +2396,8 @@ export class LGraph
const floatingLink = LLink.create(linkData)
this.addFloatingLink(floatingLink)
if (floatingLink.id > this._lastFloatingLinkId)
this._lastFloatingLinkId = floatingLink.id
if (floatingLink.id > this.#lastFloatingLinkId)
this.#lastFloatingLinkId = floatingLink.id
}
}
@@ -2466,13 +2448,13 @@ export class LGraph
}
}
private _canvas?: LGraphCanvas
#canvas?: LGraphCanvas
get primaryCanvas(): LGraphCanvas | undefined {
return this.rootGraph._canvas
return this.rootGraph.#canvas
}
set primaryCanvas(canvas: LGraphCanvas) {
this.rootGraph._canvas = canvas
this.rootGraph.#canvas = canvas
}
load(url: string | Blob | URL | File, callback: () => void) {
@@ -2541,9 +2523,9 @@ export class Subgraph
/** A list of node widgets displayed in the parent graph, on the subgraph object. */
readonly widgets: ExposedWidget[] = []
private _rootGraph: LGraph
#rootGraph: LGraph
override get rootGraph(): LGraph {
return this._rootGraph
return this.#rootGraph
}
constructor(rootGraph: LGraph, data: ExportedSubgraph) {
@@ -2551,11 +2533,11 @@ export class Subgraph
super()
this._rootGraph = rootGraph
this.#rootGraph = rootGraph
const cloned = structuredClone(data)
this._configureBase(cloned)
this._configureSubgraph(cloned)
this.#configureSubgraph(cloned)
}
getIoNodeOnPos(
@@ -2567,7 +2549,7 @@ export class Subgraph
if (outputNode.containsPoint([x, y])) return outputNode
}
private _configureSubgraph(
#configureSubgraph(
data:
| (ISerialisedGraph & ExportedSubgraph)
| (SerialisableGraph & ExportedSubgraph)
@@ -2611,7 +2593,7 @@ export class Subgraph
): boolean | undefined {
const r = super.configure(data, keep_old)
this._configureSubgraph(data)
this.#configureSubgraph(data)
return r
}

View File

@@ -316,17 +316,17 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
selectionChanged: false
}
private _subgraph?: Subgraph
#subgraph?: Subgraph
get subgraph(): Subgraph | undefined {
return this._subgraph
return this.#subgraph
}
set subgraph(value: Subgraph | undefined) {
if (value !== this._subgraph) {
this._subgraph = value
if (value !== this.#subgraph) {
this.#subgraph = value
if (value)
this.dispatch('litegraph:set-graph', {
oldGraph: this._subgraph,
oldGraph: this.#subgraph,
newGraph: value
})
}
@@ -361,7 +361,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
this.canvas.dispatchEvent(new CustomEvent(type, { detail }))
}
private _updateCursorStyle() {
#updateCursorStyle() {
if (!this.state.shouldSetCursor) return
const crosshairItems =
@@ -398,7 +398,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
set read_only(value: boolean) {
this.state.readOnly = value
this._updateCursorStyle()
this.#updateCursorStyle()
}
get isDragging(): boolean {
@@ -415,7 +415,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
set hoveringOver(value: CanvasItem) {
this.state.hoveringOver = value
this._updateCursorStyle()
this.#updateCursorStyle()
}
/** @deprecated Replace all references with {@link pointer}.{@link CanvasPointer.isDown isDown}. */
@@ -435,7 +435,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
set dragging_canvas(value: boolean) {
this.state.draggingCanvas = value
this._updateCursorStyle()
this.#updateCursorStyle()
}
/**
@@ -450,16 +450,16 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
return `normal ${LiteGraph.NODE_SUBTEXT_SIZE}px ${LiteGraph.NODE_FONT}`
}
private _maximumFrameGap = 0
#maximumFrameGap = 0
/** Maximum frames per second to render. 0: unlimited. Default: 0 */
public get maximumFps() {
return this._maximumFrameGap > Number.EPSILON
? this._maximumFrameGap / 1000
return this.#maximumFrameGap > Number.EPSILON
? this.#maximumFrameGap / 1000
: 0
}
public set maximumFps(value) {
this._maximumFrameGap = value > Number.EPSILON ? 1000 / value : 0
this.#maximumFrameGap = value > Number.EPSILON ? 1000 / value : 0
}
/**
@@ -660,12 +660,12 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
* The IDs of the nodes that are currently visible on the canvas. More
* performant than {@link visible_nodes} for visibility checks.
*/
private _visible_node_ids: Set<NodeId> = new Set()
#visible_node_ids: Set<NodeId> = new Set()
node_over?: LGraphNode
node_capturing_input?: LGraphNode | null
highlighted_links: Dictionary<boolean> = {}
private _visibleReroutes: Set<Reroute> = new Set()
#visibleReroutes: Set<Reroute> = new Set()
dirty_canvas: boolean = true
dirty_bgcanvas: boolean = true
@@ -725,9 +725,9 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
NODEPANEL_IS_OPEN?: boolean
/** Once per frame check of snap to grid value. @todo Update on change. */
private _snapToGrid?: number
#snapToGrid?: number
/** Set on keydown, keyup. @todo */
private _shiftDown: boolean = false
#shiftDown: boolean = false
/** Link rendering adapter for litegraph-to-canvas integration */
linkRenderer: LitegraphLinkAdapter | null = null
@@ -735,11 +735,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
/** If true, enable drag zoom. Ctrl+Shift+Drag Up/Down: zoom canvas. */
dragZoomEnabled: boolean = false
/** The start position of the drag zoom and original read-only state. */
private _dragZoomStart: {
pos: Point
scale: number
readOnly: boolean
} | null = null
#dragZoomStart: { pos: Point; scale: number; readOnly: boolean } | null = null
/** If true, enable live selection during drag. Nodes are selected/deselected in real-time. */
liveSelection: boolean = false
@@ -814,7 +810,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
}
this.linkConnector.events.addEventListener('link-created', () =>
this._dirty()
this.#dirty()
)
// @deprecated Workaround: Keep until connecting_links is removed.
@@ -1812,7 +1808,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
this.dragging_canvas = false
this._dirty()
this.#dirty()
this.dirty_area = null
this.node_in_panel = null
@@ -1840,7 +1836,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
this.linkRenderer = new LitegraphLinkAdapter(false)
this.dispatch('litegraph:set-graph', { newGraph, oldGraph: graph })
this._dirty()
this.#dirty()
}
openSubgraph(subgraph: Subgraph, fromNode: SubgraphNode): void {
@@ -1877,7 +1873,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
* @returns The canvas element
* @throws If {@link canvas} is an element ID that does not belong to a valid HTML canvas element
*/
private _validateCanvas(
#validateCanvas(
canvas: string | HTMLCanvasElement
): HTMLCanvasElement & { data?: LGraphCanvas } {
if (typeof canvas === 'string') {
@@ -1896,7 +1892,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
* @param skip_events If true, events on the previous canvas will not be removed. Has no effect on the first invocation.
*/
setCanvas(canvas: string | HTMLCanvasElement, skip_events?: boolean) {
const element = this._validateCanvas(canvas)
const element = this.#validateCanvas(canvas)
if (element === this.canvas) return
// maybe detach events from old_canvas
if (!element && this.canvas && !skip_events) this.unbindEvents()
@@ -1909,12 +1905,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
// TODO: classList.add
element.className += ' lgraphcanvas'
Object.defineProperty(element, 'data', {
value: this,
writable: true,
configurable: true,
enumerable: false
})
element.data = this
// Background canvas: To render objects behind nodes (background, links, groups)
this.bgcanvas = document.createElement('canvas')
@@ -2035,12 +2026,12 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
}
/** Marks the entire canvas as dirty. */
private _dirty(): void {
#dirty(): void {
this.dirty_canvas = true
this.dirty_bgcanvas = true
}
private _linkConnectorDrop(): void {
#linkConnectorDrop(): void {
const { graph, linkConnector, pointer } = this
if (!graph) throw new NullGraphError()
@@ -2079,10 +2070,10 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
const window = this.getCanvasWindow()
if (this.is_rendering) {
if (this._maximumFrameGap > 0) {
if (this.#maximumFrameGap > 0) {
// Manual FPS limit
const gap =
this._maximumFrameGap - (LiteGraph.getTime() - this.last_draw_time)
this.#maximumFrameGap - (LiteGraph.getTime() - this.last_draw_time)
setTimeout(renderFrame.bind(this), Math.max(1, gap))
} else {
// FPS limited by refresh rate
@@ -2170,7 +2161,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
!e.altKey &&
e.buttons
) {
this._dragZoomStart = {
this.#dragZoomStart = {
pos: [e.x, e.y],
scale: this.ds.scale,
readOnly: this.read_only
@@ -2217,9 +2208,9 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
// left button mouse / single finger
if (e.button === 0 && !pointer.isDouble) {
this._processPrimaryButton(e, node)
this.#processPrimaryButton(e, node)
} else if (e.button === 1) {
this._processMiddleButton(e, node)
this.#processMiddleButton(e, node)
} else if (
(e.button === 2 || pointer.isDouble) &&
this.allow_interaction &&
@@ -2255,7 +2246,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
reroute = graph.getRerouteOnPos(
e.canvasX,
e.canvasY,
this._visibleReroutes
this.#visibleReroutes
)
}
if (reroute) {
@@ -2311,24 +2302,18 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
* @param y The y coordinate in canvas space
* @returns The positionable item or undefined
*/
private _getPositionableOnPos(
x: number,
y: number
): Positionable | undefined {
#getPositionableOnPos(x: number, y: number): Positionable | undefined {
const ioNode = this.subgraph?.getIoNodeOnPos(x, y)
if (ioNode) return ioNode
for (const reroute of this._visibleReroutes) {
for (const reroute of this.#visibleReroutes) {
if (reroute.containsPoint([x, y])) return reroute
}
return this.graph?.getGroupTitlebarOnPos(x, y)
}
private _processPrimaryButton(
e: CanvasPointerEvent,
node: LGraphNode | undefined
) {
#processPrimaryButton(e: CanvasPointerEvent, node: LGraphNode | undefined) {
const { pointer, graph, linkConnector, subgraph } = this
if (!graph) throw new NullGraphError()
@@ -2344,7 +2329,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
!e.altKey &&
LiteGraph.leftMouseClickBehavior === 'panning'
) {
this._setupNodeSelectionDrag(e, pointer, node)
this.#setupNodeSelectionDrag(e, pointer, node)
return
}
@@ -2375,16 +2360,16 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
if (this.allow_dragnodes) {
pointer.onDragStart = (pointer) => {
this._startDraggingItems(cloned, pointer)
this.#startDraggingItems(cloned, pointer)
}
pointer.onDragEnd = (e) => this._processDraggedItems(e)
pointer.onDragEnd = (e) => this.#processDraggedItems(e)
}
return
}
// Node clicked
if (node && (this.allow_interaction || node.flags.allow_interaction)) {
this._processNodeClick(e, ctrlOrMeta, node)
this.#processNodeClick(e, ctrlOrMeta, node)
} else {
// Subgraph IO nodes
if (subgraph) {
@@ -2402,8 +2387,8 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
ioNode.onPointerDown(e, pointer, linkConnector)
pointer.onClick ??= () => canvas.processSelect(ioNode, e)
pointer.onDragStart ??= () =>
canvas._startDraggingItems(ioNode, pointer, true)
pointer.onDragEnd ??= (eUp) => canvas._processDraggedItems(eUp)
canvas.#startDraggingItems(ioNode, pointer, true)
pointer.onDragEnd ??= (eUp) => canvas.#processDraggedItems(eUp)
return true
}
}
@@ -2419,7 +2404,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
}
// Fallback to checking visible reroutes directly
for (const reroute of this._visibleReroutes) {
for (const reroute of this.#visibleReroutes) {
const overReroute =
foundReroute === reroute || reroute.containsPoint([x, y])
if (!reroute.isSlotHovered && !overReroute) continue
@@ -2428,19 +2413,19 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
pointer.onClick = () => this.processSelect(reroute, e)
if (!e.shiftKey) {
pointer.onDragStart = (pointer) =>
this._startDraggingItems(reroute, pointer, true)
pointer.onDragEnd = (e) => this._processDraggedItems(e)
this.#startDraggingItems(reroute, pointer, true)
pointer.onDragEnd = (e) => this.#processDraggedItems(e)
}
}
if (reroute.isOutputHovered || (overReroute && e.shiftKey)) {
linkConnector.dragFromReroute(graph, reroute)
this._linkConnectorDrop()
this.#linkConnectorDrop()
}
if (reroute.isInputHovered) {
linkConnector.dragFromRerouteToOutput(graph, reroute)
this._linkConnectorDrop()
this.#linkConnectorDrop()
}
reroute.hideSlots()
@@ -2485,14 +2470,14 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
if (e.shiftKey && !e.altKey) {
linkConnector.dragFromLinkSegment(graph, linkSegment)
this._linkConnectorDrop()
this.#linkConnectorDrop()
return
} else if (e.altKey && !e.shiftKey) {
const newReroute = graph.createReroute([x, y], linkSegment)
pointer.onDragStart = (pointer) =>
this._startDraggingItems(newReroute, pointer)
pointer.onDragEnd = (e) => this._processDraggedItems(e)
this.#startDraggingItems(newReroute, pointer)
pointer.onDragEnd = (e) => this.#processDraggedItems(e)
return
}
} else if (
@@ -2534,7 +2519,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
eMove.canvasY - group.pos[1] - offsetY
]
// Unless snapping.
if (this._snapToGrid) snapPoint(pos, this._snapToGrid)
if (this.#snapToGrid) snapPoint(pos, this.#snapToGrid)
const resized = group.resize(pos[0], pos[1])
if (resized) this.dirty_bgcanvas = true
@@ -2557,9 +2542,9 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
pointer.onClick = () => this.processSelect(group, e)
pointer.onDragStart = (pointer) => {
group.recomputeInsideNodes()
this._startDraggingItems(group, pointer, true)
this.#startDraggingItems(group, pointer, true)
}
pointer.onDragEnd = (e) => this._processDraggedItems(e)
pointer.onDragEnd = (e) => this.#processDraggedItems(e)
}
}
@@ -2597,12 +2582,12 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
pointer.finally = () => (this.dragging_canvas = false)
this.dragging_canvas = true
} else {
this._setupNodeSelectionDrag(e, pointer)
this.#setupNodeSelectionDrag(e, pointer)
}
}
}
private _setupNodeSelectionDrag(
#setupNodeSelectionDrag(
e: CanvasPointerEvent,
pointer: CanvasPointer,
node?: LGraphNode | undefined
@@ -2617,7 +2602,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
pointer.onClick = (eUp) => {
// Click, not drag
const clickedItem =
node ?? this._getPositionableOnPos(eUp.canvasX, eUp.canvasY)
node ?? this.#getPositionableOnPos(eUp.canvasX, eUp.canvasY)
this.processSelect(clickedItem, eUp)
}
pointer.onDragStart = () => (this.dragging_rectangle = dragRect)
@@ -2632,7 +2617,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
} else {
// Classic mode: select only when drag ends
pointer.onDragEnd = (upEvent) =>
this._handleMultiSelect(upEvent, dragRect)
this.#handleMultiSelect(upEvent, dragRect)
}
pointer.finally = () => (this.dragging_rectangle = null)
@@ -2644,7 +2629,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
* @param ctrlOrMeta Ctrl or meta key is pressed
* @param node The node to process a click event for
*/
private _processNodeClick(
#processNodeClick(
e: CanvasPointerEvent,
ctrlOrMeta: boolean,
node: LGraphNode
@@ -2700,13 +2685,13 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
// Drag multiple output links
if (e.shiftKey && hasRelevantOutputLinks(output, graph)) {
linkConnector.moveOutputLink(graph, output)
this._linkConnectorDrop()
this.#linkConnectorDrop()
return
}
// New output link
linkConnector.dragNewFromOutput(graph, node, output)
this._linkConnectorDrop()
this.#linkConnectorDrop()
if (LiteGraph.shift_click_do_break_link_from) {
if (e.shiftKey) {
@@ -2759,7 +2744,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
linkConnector.dragNewFromInput(graph, node, input)
}
this._linkConnectorDrop()
this.#linkConnectorDrop()
this.dirty_bgcanvas = true
return
@@ -2889,7 +2874,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
}
// Apply snapping to position changes
if (this._snapToGrid) {
if (this.#snapToGrid) {
if (
resizeDirection.includes('N') ||
resizeDirection.includes('W')
@@ -2897,7 +2882,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
const originalX = newBounds.x
const originalY = newBounds.y
snapPoint(newBounds.pos, this._snapToGrid)
snapPoint(newBounds.pos, this.#snapToGrid)
// Adjust size to compensate for snapped position
if (resizeDirection.includes('N')) {
@@ -2908,7 +2893,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
}
}
snapPoint(newBounds.size, this._snapToGrid)
snapPoint(newBounds.size, this.#snapToGrid)
}
// Apply snapping to size changes
@@ -2933,11 +2918,11 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
node.pos = newBounds.pos
node.setSize(newBounds.size)
this._dirty()
this.#dirty()
}
pointer.onDragEnd = () => {
this._dirty()
this.#dirty()
graph.afterChange(node)
}
pointer.finally = () => {
@@ -2953,8 +2938,8 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
// Drag node
pointer.onDragStart = (pointer) =>
this._startDraggingItems(node, pointer, true)
pointer.onDragEnd = (e) => this._processDraggedItems(e)
this.#startDraggingItems(node, pointer, true)
pointer.onDragEnd = (e) => this.#processDraggedItems(e)
}
this.dirty_canvas = true
@@ -3023,10 +3008,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
* @param e The pointerdown event
* @param node The node to process a click event for
*/
private _processMiddleButton(
e: CanvasPointerEvent,
node: LGraphNode | undefined
) {
#processMiddleButton(e: CanvasPointerEvent, node: LGraphNode | undefined) {
const { pointer } = this
if (
@@ -3123,14 +3105,14 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
}
}
private _processDragZoom(e: PointerEvent): void {
#processDragZoom(e: PointerEvent): void {
// stop canvas zoom action
if (!e.buttons) {
this._finishDragZoom()
this.#finishDragZoom()
return
}
const start = this._dragZoomStart
const start = this.#dragZoomStart
if (!start) throw new TypeError('Drag-zoom state object was null')
if (!this.graph) throw new NullGraphError()
@@ -3144,10 +3126,10 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
this.graph.change()
}
private _finishDragZoom(): void {
const start = this._dragZoomStart
#finishDragZoom(): void {
const start = this.#dragZoomStart
if (!start) return
this._dragZoomStart = null
this.#dragZoomStart = null
this.read_only = start.readOnly
}
@@ -3159,9 +3141,9 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
this.dragZoomEnabled &&
e.ctrlKey &&
e.shiftKey &&
this._dragZoomStart
this.#dragZoomStart
) {
this._processDragZoom(e)
this.#processDragZoom(e)
return
}
@@ -3228,7 +3210,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
} else if (this.dragging_canvas) {
this.ds.offset[0] += delta[0] / this.ds.scale
this.ds.offset[1] += delta[1] / this.ds.scale
this._dirty()
this.#dirty()
} else if (
(this.allow_interaction || node?.flags.allow_interaction) &&
!this.read_only
@@ -3276,7 +3258,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
this.node_over = node
this.dirty_canvas = true
for (const reroute of this._visibleReroutes) {
for (const reroute of this.#visibleReroutes) {
reroute.hideSlots()
this.dirty_bgcanvas = true
}
@@ -3400,10 +3382,10 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
}
} else {
// Reroutes
underPointer = this._updateReroutes(underPointer)
underPointer = this.#updateReroutes(underPointer)
// Not over a node
const segment = this._getLinkCentreOnPos(e)
const segment = this.#getLinkCentreOnPos(e)
if (this.over_link_center !== segment) {
underPointer |= CanvasItem.Link
this.over_link_center = segment
@@ -3453,7 +3435,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
}
}
this._dirty()
this.#dirty()
}
}
@@ -3467,14 +3449,14 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
* Updates the hover / snap state of all visible reroutes.
* @returns The original value of {@link underPointer}, with any found reroute items added.
*/
private _updateReroutes(underPointer: CanvasItem): CanvasItem {
#updateReroutes(underPointer: CanvasItem): CanvasItem {
const { graph, pointer, linkConnector } = this
if (!graph) throw new NullGraphError()
// Update reroute hover state
if (!pointer.isDown) {
let anyChanges = false
for (const reroute of this._visibleReroutes) {
for (const reroute of this.#visibleReroutes) {
anyChanges ||= reroute.updateVisibility(this.graph_mouse)
if (reroute.isSlotHovered) underPointer |= CanvasItem.RerouteSlot
@@ -3482,7 +3464,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
if (anyChanges) this.dirty_bgcanvas = true
} else if (linkConnector.isConnecting) {
// Highlight the reroute that the mouse is over
for (const reroute of this._visibleReroutes) {
for (const reroute of this.#visibleReroutes) {
if (reroute.containsPoint(this.graph_mouse)) {
if (linkConnector.isRerouteValidDrop(reroute)) {
linkConnector.overReroute = reroute
@@ -3507,7 +3489,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
* @param pointer The pointer event that initiated the drag, e.g. pointerdown
* @param sticky If `true`, the item is added to the selection - see {@link processSelect}
*/
private _startDraggingItems(
#startDraggingItems(
item: Positionable,
pointer: CanvasPointer,
sticky = false
@@ -3529,7 +3511,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
* Handles shared clean up and placement after items have been dragged.
* @param e The event that completed the drag, e.g. pointerup, pointermove
*/
private _processDraggedItems(e: CanvasPointerEvent): void {
#processDraggedItems(e: CanvasPointerEvent): void {
const { graph } = this
if (e.shiftKey || LiteGraph.alwaysSnapToGrid)
graph?.snapToGrid(this.selectedItems)
@@ -3551,7 +3533,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
const { graph, pointer } = this
if (!graph) return
this._finishDragZoom()
this.#finishDragZoom()
LGraphCanvas.active_canvas = this
@@ -3695,7 +3677,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
return
}
private _noItemsSelected(): void {
#noItemsSelected(): void {
const event = new CustomEvent('litegraph:no-items-selected', {
bubbles: true
})
@@ -3706,7 +3688,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
* process a key event
*/
processKey(e: KeyboardEvent): void {
this._shiftDown = e.shiftKey
this.#shiftDown = e.shiftKey
const { graph } = this
if (!graph) return
@@ -3753,7 +3735,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
// @ts-expect-error EventTarget.localName is not in standard types
if (e.target.localName != 'input' && e.target.localName != 'textarea') {
if (this.selectedItems.size === 0) {
this._noItemsSelected()
this.#noItemsSelected()
return
}
@@ -4115,7 +4097,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
* @param dragRect The drag rectangle to normalize (modified in place)
* @returns The normalized rectangle
*/
private _normalizeDragRect(dragRect: Rect): Rect {
#normalizeDragRect(dragRect: Rect): Rect {
const w = Math.abs(dragRect[2])
const h = Math.abs(dragRect[3])
if (dragRect[2] < 0) dragRect[0] -= w
@@ -4130,7 +4112,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
* @param rect The rectangle to check against
* @returns Set of positionable items that overlap with the rectangle
*/
private _getItemsInRect(rect: Rect): Set<Positionable> {
#getItemsInRect(rect: Rect): Set<Positionable> {
const { graph, subgraph } = this
if (!graph) throw new NullGraphError()
@@ -4184,9 +4166,9 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
dragRect[2],
dragRect[3]
]
this._normalizeDragRect(normalizedRect)
this.#normalizeDragRect(normalizedRect)
const itemsInRect = this._getItemsInRect(normalizedRect)
const itemsInRect = this.#getItemsInRect(normalizedRect)
const desired = new Set<Positionable>()
if (e.shiftKey && !e.altKey) {
@@ -4233,16 +4215,16 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
* @param e The pointer up event
* @param dragRect The drag rectangle
*/
private _handleMultiSelect(e: CanvasPointerEvent, dragRect: Rect): void {
#handleMultiSelect(e: CanvasPointerEvent, dragRect: Rect): void {
const normalizedRect: Rect = [
dragRect[0],
dragRect[1],
dragRect[2],
dragRect[3]
]
this._normalizeDragRect(normalizedRect)
this.#normalizeDragRect(normalizedRect)
const itemsInRect = this._getItemsInRect(normalizedRect)
const itemsInRect = this.#getItemsInRect(normalizedRect)
const { selectedItems } = this
if (e.shiftKey) {
@@ -4606,7 +4588,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
*/
setZoom(value: number, zooming_center: Point) {
this.ds.changeScale(value, zooming_center)
this._dirty()
this.#dirty()
}
/**
@@ -4689,7 +4671,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
* @returns `true` if the node is visible, otherwise `false`
*/
isNodeVisible(node: LGraphNode): boolean {
return this._visible_node_ids.has(node.id)
return this.#visible_node_ids.has(node.id)
}
/**
@@ -4710,7 +4692,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
if (this.dirty_canvas || force_canvas) {
this.computeVisibleNodes(undefined, this.visible_nodes)
// Update visible node IDs
this._visible_node_ids = new Set(
this.#visible_node_ids = new Set(
this.visible_nodes.map((node) => node.id)
)
@@ -4764,8 +4746,8 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
}
// TODO: Set snapping value when changed instead of once per frame
this._snapToGrid =
this._shiftDown || LiteGraph.alwaysSnapToGrid
this.#snapToGrid =
this.#shiftDown || LiteGraph.alwaysSnapToGrid
? this.graph?.getSnapToGridSize()
: undefined
@@ -4807,7 +4789,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
// draw nodes
const { visible_nodes } = this
const drawSnapGuides =
this._snapToGrid &&
this.#snapToGrid &&
(this.isDragging || layoutStore.isDraggingVueNodes.value)
for (const node of visible_nodes) {
@@ -4847,7 +4829,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
if (linkConnector.isConnecting) {
// current connection (the one being dragged by the mouse)
const { renderLinks } = linkConnector
const highlightPos = this._getHighlightPosition()
const highlightPos = this.#getHighlightPosition()
ctx.lineWidth = this.connections_width
for (const renderLink of renderLinks) {
@@ -4901,7 +4883,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
}
// Gradient half-border over target node
this._renderSnapHighlight(ctx, highlightPos)
this.#renderSnapHighlight(ctx, highlightPos)
}
// on top of link center
@@ -4927,7 +4909,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
}
/** @returns If the pointer is over a link centre marker, the link segment it belongs to. Otherwise, `undefined`. */
private _getLinkCentreOnPos(e: CanvasPointerEvent): LinkSegment | undefined {
#getLinkCentreOnPos(e: CanvasPointerEvent): LinkSegment | undefined {
// Skip hit detection if center markers are disabled
if (this.linkMarkerShape === LinkMarkerShape.None) {
return undefined
@@ -4946,7 +4928,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
}
/** Get the target snap / highlight point in graph space */
private _getHighlightPosition(): Readonly<Point> {
#getHighlightPosition(): Readonly<Point> {
return LiteGraph.snaps_for_comfy
? (this.linkConnector.state.snapLinksPos ??
this._highlight_pos ??
@@ -4959,7 +4941,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
* Partial border over target node and a highlight over the slot itself.
* @param ctx Canvas 2D context
*/
private _renderSnapHighlight(
#renderSnapHighlight(
ctx: CanvasRenderingContext2D,
highlightPos: Readonly<Point>
): void {
@@ -5610,7 +5592,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
// Normalise boundingRect to pos to snap
snapGuide[0] += offsetX
snapGuide[1] += offsetY
if (this._snapToGrid) snapPoint(snapGuide, this._snapToGrid)
if (this.#snapToGrid) snapPoint(snapGuide, this.#snapToGrid)
snapGuide[0] -= offsetX
snapGuide[1] -= offsetY
@@ -5690,7 +5672,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
const output = start_node.outputs[outputId]
if (!output) continue
this._renderAllLinkSegments(
this.#renderAllLinkSegments(
ctx,
link,
startPos,
@@ -5719,7 +5701,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
? getSlotPosition(inputNode, link.target_slot, true)
: inputNode.getInputPos(link.target_slot)
this._renderAllLinkSegments(
this.#renderAllLinkSegments(
ctx,
link,
output.pos,
@@ -5746,7 +5728,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
? getSlotPosition(outputNode, link.origin_slot, false)
: outputNode.getOutputPos(link.origin_slot)
this._renderAllLinkSegments(
this.#renderAllLinkSegments(
ctx,
link,
startPos,
@@ -5760,10 +5742,10 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
}
if (graph.floatingLinks.size > 0) {
this._renderFloatingLinks(ctx, graph, visibleReroutes, now)
this.#renderFloatingLinks(ctx, graph, visibleReroutes, now)
}
const rerouteSet = this._visibleReroutes
const rerouteSet = this.#visibleReroutes
rerouteSet.clear()
// Render reroutes, ordered by number of non-floating links
@@ -5772,7 +5754,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
rerouteSet.add(reroute)
if (
this._snapToGrid &&
this.#snapToGrid &&
this.isDragging &&
this.selectedItems.has(reroute)
) {
@@ -5794,7 +5776,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
: this.editor_alpha
}
private _renderFloatingLinks(
#renderFloatingLinks(
ctx: CanvasRenderingContext2D,
graph: LGraph,
visibleReroutes: Reroute[],
@@ -5823,7 +5805,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
const endDirection = node.inputs[link.target_slot]?.dir
firstReroute._dragging = true
this._renderAllLinkSegments(
this.#renderAllLinkSegments(
ctx,
link,
startPos,
@@ -5845,7 +5827,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
const startDirection = node.outputs[link.origin_slot]?.dir
link._dragging = true
this._renderAllLinkSegments(
this.#renderAllLinkSegments(
ctx,
link,
startPos,
@@ -5861,7 +5843,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
ctx.globalAlpha = globalAlpha
}
private _renderAllLinkSegments(
#renderAllLinkSegments(
ctx: CanvasRenderingContext2D,
link: LLink,
startPos: Point,
@@ -6164,7 +6146,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
ctx.save()
ctx.globalAlpha = 0.5 * this.editor_alpha
const drawSnapGuides =
this._snapToGrid &&
this.#snapToGrid &&
(this.isDragging || layoutStore.isDraggingVueNodes.value)
for (const group of groups) {
@@ -6531,7 +6513,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
},
optPass || {}
)
const dirty = () => this._dirty()
const dirty = () => this.#dirty()
const that = this
const { graph } = this
@@ -7540,7 +7522,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
function inner() {
setValue(input?.value)
}
const dirty = () => this._dirty()
const dirty = () => this.#dirty()
function setValue(value: string | number | undefined) {
if (
@@ -8374,7 +8356,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
reroute = this.graph.getRerouteOnPos(
event.canvasX,
event.canvasY,
this._visibleReroutes
this.#visibleReroutes
)
}
if (reroute) {
@@ -8664,17 +8646,4 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
const mutations = this.initLayoutMutations()
this.applyNodePositionUpdates(nodesToReposition, mutations)
}
/**
* Custom JSON serialization to prevent circular reference errors.
* LGraphCanvas should not be serialized directly - serialize the graph instead.
*/
toJSON(): { ds: { scale: number; offset: [number, number] } } {
return {
ds: {
scale: this.ds.scale,
offset: [...this.ds.offset] as [number, number]
}
}
}
}

View File

@@ -273,8 +273,8 @@ export class LGraphNode
inputs: INodeInputSlot[] = []
outputs: INodeOutputSlot[] = []
private _concreteInputs: NodeInputSlot[] = []
private _concreteOutputs: NodeOutputSlot[] = []
#concreteInputs: NodeInputSlot[] = []
#concreteOutputs: NodeOutputSlot[] = []
properties: Dictionary<NodeProperty | undefined> = {}
properties_info: INodePropertyInfo[] = []
@@ -438,24 +438,24 @@ export class LGraphNode
}
/** @inheritdoc {@link renderArea} */
private _renderArea = new Rectangle()
#renderArea = new Rectangle()
/**
* Rect describing the node area, including shadows and any protrusions.
* Determines if the node is visible. Calculated once at the start of every frame.
*/
get renderArea(): ReadOnlyRect {
return this._renderArea
return this.#renderArea
}
/** @inheritdoc {@link boundingRect} */
private _boundingRect: Rectangle = new Rectangle()
#boundingRect: Rectangle = new Rectangle()
/**
* Cached node position & area as `x, y, width, height`. Includes changes made by {@link onBounding}, if present.
*
* Determines the node hitbox and other rendering effects. Calculated once at the start of every frame.
*/
get boundingRect(): ReadOnlyRectangle {
return this._boundingRect
return this.#boundingRect
}
/** The offset from {@link pos} to the top-left of {@link boundingRect}. */
@@ -753,9 +753,7 @@ export class LGraphNode
onPropertyChange?(this: LGraphNode): void
updateOutputData?(this: LGraphNode, origin_slot: number): void
private _getErrorStrokeStyle(
this: LGraphNode
): IDrawBoundingOptions | undefined {
#getErrorStrokeStyle(this: LGraphNode): IDrawBoundingOptions | undefined {
if (this.has_errors) {
return {
padding: 12,
@@ -765,9 +763,7 @@ export class LGraphNode
}
}
private _getSelectedStrokeStyle(
this: LGraphNode
): IDrawBoundingOptions | undefined {
#getSelectedStrokeStyle(this: LGraphNode): IDrawBoundingOptions | undefined {
if (this.selected) {
return {
padding: this.has_errors ? 20 : undefined
@@ -782,8 +778,8 @@ export class LGraphNode
this.size = [LiteGraph.NODE_WIDTH, 60]
this.pos = [10, 10]
this.strokeStyles = {
error: this._getErrorStrokeStyle,
selected: this._getSelectedStrokeStyle
error: this.#getErrorStrokeStyle,
selected: this.#getSelectedStrokeStyle
}
// Initialize property manager with tracked properties
this.changeTracker = new LGraphNodeProperties(this)
@@ -2071,11 +2067,11 @@ export class LGraphNode
* Called automatically at the start of every frame.
*/
updateArea(ctx?: CanvasRenderingContext2D): void {
const bounds = this._boundingRect
const bounds = this.#boundingRect
this.measure(bounds, ctx)
this.onBounding?.(bounds)
const renderArea = this._renderArea
const renderArea = this.#renderArea
renderArea.set(bounds)
// 4 offset for collapsed node connection points
renderArea[0] -= 4
@@ -2297,7 +2293,7 @@ export class LGraphNode
optsIn?: FindFreeSlotOptions & { returnObj?: TReturn }
): INodeInputSlot | -1
findInputSlotFree(optsIn?: FindFreeSlotOptions) {
return this._findFreeSlot(this.inputs, optsIn)
return this.#findFreeSlot(this.inputs, optsIn)
}
/**
@@ -2312,14 +2308,14 @@ export class LGraphNode
optsIn?: FindFreeSlotOptions & { returnObj?: TReturn }
): INodeOutputSlot | -1
findOutputSlotFree(optsIn?: FindFreeSlotOptions) {
return this._findFreeSlot(this.outputs, optsIn)
return this.#findFreeSlot(this.outputs, optsIn)
}
/**
* Finds the next free slot
* @param slots The slots to search, i.e. this.inputs or this.outputs
*/
private _findFreeSlot<TSlot extends INodeInputSlot | INodeOutputSlot>(
#findFreeSlot<TSlot extends INodeInputSlot | INodeOutputSlot>(
slots: TSlot[],
options?: FindFreeSlotOptions
): TSlot | number {
@@ -2361,7 +2357,7 @@ export class LGraphNode
preferFreeSlot?: boolean,
doNotUseOccupied?: boolean
) {
return this._findSlotByType(
return this.#findSlotByType(
this.inputs,
type,
returnObj,
@@ -2391,7 +2387,7 @@ export class LGraphNode
preferFreeSlot?: boolean,
doNotUseOccupied?: boolean
) {
return this._findSlotByType(
return this.#findSlotByType(
this.outputs,
type,
returnObj,
@@ -2437,14 +2433,14 @@ export class LGraphNode
doNotUseOccupied?: boolean
): number | INodeOutputSlot | INodeInputSlot {
return input
? this._findSlotByType(
? this.#findSlotByType(
this.inputs,
type,
returnObj,
preferFreeSlot,
doNotUseOccupied
)
: this._findSlotByType(
: this.#findSlotByType(
this.outputs,
type,
returnObj,
@@ -2465,7 +2461,7 @@ export class LGraphNode
* @see {findInputSlotByType}
* @returns If a match is found, the slot if returnObj is true, otherwise the index. If no matches are found, -1
*/
private _findSlotByType<TSlot extends INodeInputSlot | INodeOutputSlot>(
#findSlotByType<TSlot extends INodeInputSlot | INodeOutputSlot>(
slots: TSlot[],
type: ISlotType,
returnObj?: boolean,
@@ -3314,8 +3310,8 @@ export class LGraphNode
// default vertical slots
const offset = LiteGraph.NODE_SLOT_HEIGHT * 0.5
const slotIndex = is_input
? this._defaultVerticalInputs.indexOf(this.inputs[slot_number])
: this._defaultVerticalOutputs.indexOf(this.outputs[slot_number])
? this.#defaultVerticalInputs.indexOf(this.inputs[slot_number])
: this.#defaultVerticalOutputs.indexOf(this.outputs[slot_number])
out[0] = is_input ? nodeX + offset : nodeX + this.size[0] + 1 - offset
out[1] =
@@ -3328,7 +3324,7 @@ export class LGraphNode
/**
* @internal The inputs that are not positioned with absolute coordinates.
*/
private get _defaultVerticalInputs() {
get #defaultVerticalInputs() {
return this.inputs.filter(
(slot) => !slot.pos && !(this.widgets?.length && isWidgetInputSlot(slot))
)
@@ -3337,7 +3333,7 @@ export class LGraphNode
/**
* @internal The outputs that are not positioned with absolute coordinates.
*/
private get _defaultVerticalOutputs() {
get #defaultVerticalOutputs() {
return this.outputs.filter((slot: INodeOutputSlot) => !slot.pos)
}
@@ -3345,7 +3341,7 @@ export class LGraphNode
* Get the context needed for slot position calculations
* @internal
*/
private _getSlotPositionContext(): SlotPositionContext {
#getSlotPositionContext(): SlotPositionContext {
return {
nodeX: this.pos[0],
nodeY: this.pos[1],
@@ -3377,7 +3373,7 @@ export class LGraphNode
* @returns Position of the centre of the input slot in graph co-ordinates.
*/
getInputSlotPos(input: INodeInputSlot): Point {
return calculateInputSlotPosFromSlot(this._getSlotPositionContext(), input)
return calculateInputSlotPosFromSlot(this.#getSlotPositionContext(), input)
}
/**
@@ -3920,13 +3916,13 @@ export class LGraphNode
*/
drawCollapsedSlots(ctx: CanvasRenderingContext2D): void {
// Render the first connected slot only.
for (const slot of this._concreteInputs) {
for (const slot of this.#concreteInputs) {
if (slot.link != null) {
slot.drawCollapsed(ctx)
break
}
}
for (const slot of this._concreteOutputs) {
for (const slot of this.#concreteOutputs) {
if (slot.links?.length) {
slot.drawCollapsed(ctx)
break
@@ -3938,7 +3934,7 @@ export class LGraphNode
return [...this.inputs, ...this.outputs]
}
private _measureSlot(
#measureSlot(
slot: NodeInputSlot | NodeOutputSlot,
slotIndex: number,
isInput: boolean
@@ -3955,27 +3951,27 @@ export class LGraphNode
slot.boundingRect[3] = LiteGraph.NODE_SLOT_HEIGHT
}
private _measureSlots(): ReadOnlyRect | null {
#measureSlots(): ReadOnlyRect | null {
const slots: (NodeInputSlot | NodeOutputSlot)[] = []
for (const [slotIndex, slot] of this._concreteInputs.entries()) {
for (const [slotIndex, slot] of this.#concreteInputs.entries()) {
// Unrecognized nodes (Nodes with error) has inputs but no widgets. Treat
// converted inputs as normal inputs.
/** Widget input slots are handled in {@link layoutWidgetInputSlots} */
if (this.widgets?.length && isWidgetInputSlot(slot)) continue
this._measureSlot(slot, slotIndex, true)
this.#measureSlot(slot, slotIndex, true)
slots.push(slot)
}
for (const [slotIndex, slot] of this._concreteOutputs.entries()) {
this._measureSlot(slot, slotIndex, false)
for (const [slotIndex, slot] of this.#concreteOutputs.entries()) {
this.#measureSlot(slot, slotIndex, false)
slots.push(slot)
}
return slots.length ? createBounds(slots, 0) : null
}
private _getMouseOverSlot(slot: INodeSlot): INodeSlot | null {
#getMouseOverSlot(slot: INodeSlot): INodeSlot | null {
const isInput = isINodeInputSlot(slot)
const mouseOverId = this.mouseOver?.[isInput ? 'inputId' : 'outputId'] ?? -1
if (mouseOverId === -1) {
@@ -3984,11 +3980,11 @@ export class LGraphNode
return isInput ? this.inputs[mouseOverId] : this.outputs[mouseOverId]
}
private _isMouseOverSlot(slot: INodeSlot): boolean {
return this._getMouseOverSlot(slot) === slot
#isMouseOverSlot(slot: INodeSlot): boolean {
return this.#getMouseOverSlot(slot) === slot
}
private _isMouseOverWidget(widget: IBaseWidget | undefined): boolean {
#isMouseOverWidget(widget: IBaseWidget | undefined): boolean {
if (!widget) return false
return this.mouseOver?.overWidget === widget
}
@@ -4020,9 +4016,9 @@ export class LGraphNode
ctx: CanvasRenderingContext2D,
{ fromSlot, colorContext, editorAlpha, lowQuality }: DrawSlotsOptions
) {
for (const slot of [...this._concreteInputs, ...this._concreteOutputs]) {
for (const slot of [...this.#concreteInputs, ...this.#concreteOutputs]) {
const isValidTarget = fromSlot && slot.isValidTarget(fromSlot)
const isMouseOverSlot = this._isMouseOverSlot(slot)
const isMouseOverSlot = this.#isMouseOverSlot(slot)
// change opacity of incompatible slots when dragging a connection
const isValid = !fromSlot || isValidTarget
@@ -4037,7 +4033,7 @@ export class LGraphNode
isMouseOverSlot ||
isValidTarget ||
!slot.isWidgetInputSlot ||
this._isMouseOverWidget(this.getWidgetFromSlot(slot)) ||
this.#isMouseOverWidget(this.getWidgetFromSlot(slot)) ||
slot.isConnected ||
slot.alwaysVisible
) {
@@ -4058,7 +4054,7 @@ export class LGraphNode
* - {@link IBaseWidget.y}
* @param widgetStartY The y-coordinate of the first widget
*/
private _arrangeWidgets(widgetStartY: number): void {
#arrangeWidgets(widgetStartY: number): void {
if (!this.widgets || !this.widgets.length) return
const bodyHeight = this.bodyHeight
@@ -4136,7 +4132,7 @@ export class LGraphNode
/**
* Arranges the layout of the node's widget input slots.
*/
private _arrangeWidgetInputSlots(): void {
#arrangeWidgetInputSlots(): void {
if (!this.widgets) return
const slotByWidgetName = new Map<
@@ -4158,10 +4154,10 @@ export class LGraphNode
const slot = slotByWidgetName.get(widget.name)
if (!slot) continue
const actualSlot = this._concreteInputs[slot.index]
const actualSlot = this.#concreteInputs[slot.index]
const offset = LiteGraph.NODE_SLOT_HEIGHT * 0.5
actualSlot.pos = [offset, widget.y + offset]
this._measureSlot(actualSlot, slot.index, true)
this.#measureSlot(actualSlot, slot.index, true)
}
} else {
// For Vue positioning, just measure the slots without setting pos
@@ -4169,7 +4165,7 @@ export class LGraphNode
const slot = slotByWidgetName.get(widget.name)
if (!slot) continue
this._measureSlot(this._concreteInputs[slot.index], slot.index, true)
this.#measureSlot(this.#concreteInputs[slot.index], slot.index, true)
}
}
}
@@ -4182,10 +4178,10 @@ export class LGraphNode
* have been removed from the ecosystem.
*/
_setConcreteSlots(): void {
this._concreteInputs = this.inputs.map((slot) =>
this.#concreteInputs = this.inputs.map((slot) =>
toClass(NodeInputSlot, slot, this)
)
this._concreteOutputs = this.outputs.map((slot) =>
this.#concreteOutputs = this.outputs.map((slot) =>
toClass(NodeOutputSlot, slot, this)
)
}
@@ -4194,12 +4190,12 @@ export class LGraphNode
* Arranges node elements in preparation for rendering (slots & widgets).
*/
arrange(): void {
const slotsBounds = this._measureSlots()
const slotsBounds = this.#measureSlots()
const widgetStartY = slotsBounds
? slotsBounds[1] + slotsBounds[3] - this.pos[1]
: 0
this._arrangeWidgets(widgetStartY)
this._arrangeWidgetInputSlots()
this.#arrangeWidgets(widgetStartY)
this.#arrangeWidgetInputSlots()
}
/**

View File

@@ -25,24 +25,24 @@ export class LGraphNodeProperties {
node: LGraphNode
/** Set of property paths that have been instrumented */
private _instrumentedPaths = new Set<string>()
#instrumentedPaths = new Set<string>()
constructor(node: LGraphNode) {
this.node = node
this._setupInstrumentation()
this.#setupInstrumentation()
}
/**
* Sets up property instrumentation for all tracked properties
*/
private _setupInstrumentation(): void {
#setupInstrumentation(): void {
for (const path of DEFAULT_TRACKED_PROPERTIES) {
this._instrumentProperty(path)
this.#instrumentProperty(path)
}
}
private _resolveTargetObject(parts: string[]): {
#resolveTargetObject(parts: string[]): {
targetObject: Record<string, unknown>
propertyName: string
} {
@@ -73,14 +73,14 @@ export class LGraphNodeProperties {
/**
* Instruments a single property to track changes
*/
private _instrumentProperty(path: string): void {
#instrumentProperty(path: string): void {
const parts = path.split('.')
if (parts.length > 1) {
this._ensureNestedPath(path)
this.#ensureNestedPath(path)
}
const { targetObject, propertyName } = this._resolveTargetObject(parts)
const { targetObject, propertyName } = this.#resolveTargetObject(parts)
const hasProperty = Object.prototype.hasOwnProperty.call(
targetObject,
@@ -96,7 +96,7 @@ export class LGraphNodeProperties {
set: (newValue: unknown) => {
const oldValue = value
value = newValue
this._emitPropertyChange(path, oldValue, newValue)
this.#emitPropertyChange(path, oldValue, newValue)
// Update enumerable: true for non-undefined values, false for undefined
const shouldBeEnumerable = newValue !== undefined
@@ -121,24 +121,24 @@ export class LGraphNodeProperties {
Object.defineProperty(
targetObject,
propertyName,
this._createInstrumentedDescriptor(path, currentValue)
this.#createInstrumentedDescriptor(path, currentValue)
)
}
this._instrumentedPaths.add(path)
this.#instrumentedPaths.add(path)
}
/**
* Creates a property descriptor that emits change events
*/
private _createInstrumentedDescriptor(
#createInstrumentedDescriptor(
propertyPath: string,
initialValue: unknown
): PropertyDescriptor {
return this._createInstrumentedDescriptorTyped(propertyPath, initialValue)
return this.#createInstrumentedDescriptorTyped(propertyPath, initialValue)
}
private _createInstrumentedDescriptorTyped<TValue>(
#createInstrumentedDescriptorTyped<TValue>(
propertyPath: string,
initialValue: TValue
): PropertyDescriptor {
@@ -149,7 +149,7 @@ export class LGraphNodeProperties {
set: (newValue: TValue) => {
const oldValue = value
value = newValue
this._emitPropertyChange(propertyPath, oldValue, newValue)
this.#emitPropertyChange(propertyPath, oldValue, newValue)
},
enumerable: true,
configurable: true
@@ -159,15 +159,15 @@ export class LGraphNodeProperties {
/**
* Emits a property change event if the node is connected to a graph
*/
private _emitPropertyChange(
#emitPropertyChange(
propertyPath: string,
oldValue: unknown,
newValue: unknown
): void {
this._emitPropertyChangeTyped(propertyPath, oldValue, newValue)
this.#emitPropertyChangeTyped(propertyPath, oldValue, newValue)
}
private _emitPropertyChangeTyped<TValue>(
#emitPropertyChangeTyped<TValue>(
propertyPath: string,
oldValue: TValue,
newValue: TValue
@@ -183,7 +183,7 @@ export class LGraphNodeProperties {
/**
* Ensures parent objects exist for nested properties
*/
private _ensureNestedPath(path: string): void {
#ensureNestedPath(path: string): void {
const parts = path.split('.')
// LGraphNode supports dynamic property access at runtime
let current: Record<string, unknown> = this.node as unknown as Record<
@@ -208,7 +208,7 @@ export class LGraphNodeProperties {
* Checks if a property is being tracked
*/
isTracked(path: string): boolean {
return this._instrumentedPaths.has(path)
return this.#instrumentedPaths.has(path)
}
/**

View File

@@ -119,14 +119,14 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
/** @inheritdoc */
_dragging?: boolean
private _color?: CanvasColour | null
#color?: CanvasColour | null
/** Custom colour for this link only */
public get color(): CanvasColour | null | undefined {
return this._color
return this.#color
}
public set color(value: CanvasColour) {
this._color = value === '' ? null : value
this.#color = value === '' ? null : value
}
public get isFloatingOutput(): boolean {

View File

@@ -1,43 +1,301 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`LGraph > supports schema v0.4 graphs > oldSchemaGraph 1`] = `
{
"config": {},
"definitions": undefined,
"extra": {
"reroutes": undefined,
},
"floatingLinks": undefined,
"groups": [
{
"bounding": [
LGraph {
"_groups": [
LGraphGroup {
"_bounding": Rectangle [
20,
20,
1,
3,
],
"_children": Set {},
"_nodes": [],
"_pos": Float64Array [
20,
20,
],
"_size": Float64Array [
1,
3,
],
"color": "#6029aa",
"flags": {},
"font": undefined,
"font_size": 14,
"graph": [Circular],
"id": 123,
"selected": undefined,
"setDirtyCanvas": [Function],
"title": "A group to test with",
},
],
"id": "b4e984f1-b421-4d24-b8b4-ff895793af13",
"last_link_id": 0,
"last_node_id": 1,
"links": [],
"nodes": [
{
"id": 1,
"mode": 0,
"pos": [
"_input_nodes": undefined,
"_last_trigger_time": undefined,
"_links": Map {},
"_nodes": [
LGraphNode {
"_collapsed_width": undefined,
"_level": undefined,
"_pos": Float64Array [
10,
10,
],
"_posSize": Rectangle [
10,
10,
140,
60,
],
"_relative_id": undefined,
"_shape": undefined,
"_size": Float64Array [
140,
60,
],
"action_call": undefined,
"action_triggered": undefined,
"badgePosition": "top-left",
"badges": [],
"bgcolor": undefined,
"block_delete": undefined,
"boxcolor": undefined,
"changeTracker": undefined,
"clip_area": undefined,
"clonable": undefined,
"color": undefined,
"console": undefined,
"exec_version": undefined,
"execute_triggered": undefined,
"flags": {},
"freeWidgetSpace": undefined,
"gotFocusAt": undefined,
"graph": [Circular],
"has_errors": true,
"id": 1,
"ignore_remove": undefined,
"inputs": [],
"last_serialization": {
"id": 1,
},
"locked": undefined,
"lostFocusAt": undefined,
"mode": 0,
"mouseOver": undefined,
"order": 0,
"outputs": [],
"progress": undefined,
"properties": {},
"properties_info": [],
"redraw_on_mouse": undefined,
"removable": undefined,
"resizable": undefined,
"selected": undefined,
"serialize_widgets": undefined,
"showAdvanced": undefined,
"strokeStyles": {
"error": [Function],
"selected": [Function],
},
"title": undefined,
"title_buttons": [],
"type": "",
"widgets": undefined,
"widgets_start_y": undefined,
"widgets_up": undefined,
},
],
"_nodes_by_id": {
"1": LGraphNode {
"_collapsed_width": undefined,
"_level": undefined,
"_pos": Float64Array [
10,
10,
],
"_posSize": Rectangle [
10,
10,
140,
60,
],
"_relative_id": undefined,
"_shape": undefined,
"_size": Float64Array [
140,
60,
],
"action_call": undefined,
"action_triggered": undefined,
"badgePosition": "top-left",
"badges": [],
"bgcolor": undefined,
"block_delete": undefined,
"boxcolor": undefined,
"changeTracker": undefined,
"clip_area": undefined,
"clonable": undefined,
"color": undefined,
"console": undefined,
"exec_version": undefined,
"execute_triggered": undefined,
"flags": {},
"freeWidgetSpace": undefined,
"gotFocusAt": undefined,
"graph": [Circular],
"has_errors": true,
"id": 1,
"ignore_remove": undefined,
"inputs": [],
"last_serialization": {
"id": 1,
},
"locked": undefined,
"lostFocusAt": undefined,
"mode": 0,
"mouseOver": undefined,
"order": 0,
"outputs": [],
"progress": undefined,
"properties": {},
"properties_info": [],
"redraw_on_mouse": undefined,
"removable": undefined,
"resizable": undefined,
"selected": undefined,
"serialize_widgets": undefined,
"showAdvanced": undefined,
"strokeStyles": {
"error": [Function],
"selected": [Function],
},
"title": undefined,
"title_buttons": [],
"type": "",
"widgets": undefined,
"widgets_start_y": undefined,
"widgets_up": undefined,
},
},
"_nodes_executable": [],
"_nodes_in_order": [
LGraphNode {
"_collapsed_width": undefined,
"_level": undefined,
"_pos": Float64Array [
10,
10,
],
"_posSize": Rectangle [
10,
10,
140,
60,
],
"_relative_id": undefined,
"_shape": undefined,
"_size": Float64Array [
140,
60,
],
"action_call": undefined,
"action_triggered": undefined,
"badgePosition": "top-left",
"badges": [],
"bgcolor": undefined,
"block_delete": undefined,
"boxcolor": undefined,
"changeTracker": undefined,
"clip_area": undefined,
"clonable": undefined,
"color": undefined,
"console": undefined,
"exec_version": undefined,
"execute_triggered": undefined,
"flags": {},
"freeWidgetSpace": undefined,
"gotFocusAt": undefined,
"graph": [Circular],
"has_errors": true,
"id": 1,
"ignore_remove": undefined,
"inputs": [],
"last_serialization": {
"id": 1,
},
"locked": undefined,
"lostFocusAt": undefined,
"mode": 0,
"mouseOver": undefined,
"order": 0,
"outputs": [],
"progress": undefined,
"properties": {},
"properties_info": [],
"redraw_on_mouse": undefined,
"removable": undefined,
"resizable": undefined,
"selected": undefined,
"serialize_widgets": undefined,
"showAdvanced": undefined,
"strokeStyles": {
"error": [Function],
"selected": [Function],
},
"title": undefined,
"title_buttons": [],
"type": "",
"widgets": undefined,
"widgets_start_y": undefined,
"widgets_up": undefined,
},
],
"_subgraphs": Map {},
"_version": 3,
"catch_errors": true,
"config": {},
"elapsed_time": 0.01,
"errors_in_execution": undefined,
"events": CustomEventTarget {
Symbol(listeners): {
"bubbling": Map {},
"capturing": Map {},
},
Symbol(listenerOptions): {
"bubbling": Map {},
"capturing": Map {},
},
},
"execution_time": undefined,
"execution_timer_id": undefined,
"extra": {},
"filter": undefined,
"fixedtime": 0,
"fixedtime_lapse": 0.01,
"floatingLinksInternal": Map {},
"globaltime": 0,
"id": "b4e984f1-b421-4d24-b8b4-ff895793af13",
"iteration": 0,
"last_update_time": 0,
"links": Map {},
"list_of_graphcanvas": null,
"nodes_actioning": [],
"nodes_executedAction": [],
"nodes_executing": [],
"onTrigger": undefined,
"reroutesInternal": Map {},
"revision": 0,
"runningtime": 0,
"starttime": 0,
"state": {
"lastGroupId": 123,
"lastLinkId": 0,
"lastNodeId": 1,
"lastRerouteId": 0,
},
"status": 1,
"vars": {},
"version": 0.4,
}
`;

Some files were not shown because too many files have changed in this diff Show More