refactor: split properties panel tests into separate spec files

Move PropertiesPanelHelper into propertiesPanel folder, split monolithic
spec into openClose, workflowOverview, nodeSelection, titleEditing,
searchFiltering, nodeSettings, infoTab, and errorsTab specs. Add
AGENTS.md with test structure documentation.

Fix color tests to use page.evaluate for node property verification
instead of brittle CSS background-color assertions.

Clean up redundant comments from PropertiesPanelHelper.

Addresses review feedback:
https://github.com/Comfy-Org/ComfyUI_frontend/pull/10548#discussion_r3005784036
https://github.com/Comfy-Org/ComfyUI_frontend/pull/10548#discussion_r3005784429
This commit is contained in:
bymyself
2026-03-29 00:32:21 -07:00
parent 3cc4b4f808
commit f1d9a3c87b
11 changed files with 534 additions and 495 deletions

View File

@@ -0,0 +1,34 @@
# Properties Panel E2E Tests
Tests for the right-side properties panel (`RightSidePanel.vue`).
## Structure
| File | Coverage |
|---|---|
| `openClose.spec.ts` | Panel open/close via actionbar and close button |
| `workflowOverview.spec.ts` | No-selection state: tabs, nodes list, global settings |
| `nodeSelection.spec.ts` | Single/multi-node selection, selection changes, tab labels |
| `titleEditing.spec.ts` | Node title editing via pencil icon |
| `searchFiltering.spec.ts` | Widget search and clear |
| `nodeSettings.spec.ts` | Settings tab: node state, color, pinned (requires VueNodes) |
| `infoTab.spec.ts` | Node help content |
| `errorsTab.spec.ts` | Errors tab visibility |
| `propertiesPanelPosition.spec.ts` | Panel position relative to sidebar |
## Shared Helper
`PropertiesPanelHelper.ts` — Encapsulates panel locators and actions. Instantiated in `beforeEach`:
```typescript
let panel: PropertiesPanelHelper
test.beforeEach(async ({ comfyPage }) => {
panel = new PropertiesPanelHelper(comfyPage.page)
})
```
## Conventions
- Tests requiring VueNodes rendering enable it in `beforeEach` via `comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)` and call `comfyPage.vueNodes.waitForNodes()`.
- Verify node state changes via user-facing indicators (text labels like "Bypassed"/"Muted", pin indicator test IDs) rather than internal properties.
- Color changes are verified via `page.evaluate` accessing node properties, per the guidance in `docs/guidance/playwright.md`.

View File

@@ -1,7 +1,7 @@
import type { Locator, Page } from '@playwright/test'
import { expect } from '@playwright/test'
import { TestIds } from '../fixtures/selectors'
import { TestIds } from '../../fixtures/selectors'
export class PropertiesPanelHelper {
readonly root: Locator
@@ -16,8 +16,7 @@ export class PropertiesPanelHelper {
this.closeButton = this.root.locator('button[aria-pressed]')
}
/** Tab navigation locators */
get tabs() {
get tabs(): Locator {
return this.root.locator('nav button')
}
@@ -25,7 +24,6 @@ export class PropertiesPanelHelper {
return this.root.locator('nav button', { hasText: label })
}
/** Title editing */
get titleEditIcon(): Locator {
return this.panelTitle.locator('i[class*="lucide--pencil"]')
}
@@ -34,42 +32,34 @@ export class PropertiesPanelHelper {
return this.root.getByTestId(TestIds.node.titleInput)
}
/** Settings tab: node state buttons */
getNodeStateButton(state: 'Normal' | 'Bypass' | 'Mute'): Locator {
return this.root.locator('button', { hasText: state })
}
/** Settings tab: color swatch by name */
getColorSwatch(colorName: string): Locator {
return this.root.locator(`[data-testid="${colorName}"]`)
}
/** Settings tab: pinned toggle (PrimeVue ToggleSwitch wrapper div) */
get pinnedSwitch(): Locator {
return this.root.locator('[data-p-checked]').first()
}
/** Subgraph edit button (gear icon in header) */
get subgraphEditButton(): Locator {
return this.root.locator('button:has(i[class*="lucide--settings-2"])')
}
/** Panel content area */
get contentArea(): Locator {
return this.root.locator('.scrollbar-thin')
}
/** Errors tab indicator icon */
get errorsTabIcon(): Locator {
return this.root.locator('nav i[class*="lucide--octagon-alert"]')
}
/** Global settings: "View all settings" button */
get viewAllSettingsButton(): Locator {
return this.root.getByRole('button', { name: /view all settings/i })
}
/** Collapse/expand toggle button */
get collapseToggleButton(): Locator {
return this.root.locator(
'button:has(i[class*="lucide--chevrons-down-up"]), button:has(i[class*="lucide--chevrons-up-down"])'

View File

@@ -0,0 +1,31 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
import { PropertiesPanelHelper } from './PropertiesPanelHelper'
test.describe('Properties panel - Errors tab', () => {
let panel: PropertiesPanelHelper
test.beforeEach(async ({ comfyPage }) => {
panel = new PropertiesPanelHelper(comfyPage.page)
})
test('should show Errors tab when errors exist', async ({ comfyPage }) => {
await comfyPage.settings.setSetting(
'Comfy.RightSidePanel.ShowErrorsTab',
true
)
await comfyPage.workflow.loadWorkflow('missing/missing_nodes')
await comfyPage.actionbar.propertiesButton.click()
await comfyPage.nextFrame()
await expect(panel.errorsTabIcon).toBeVisible()
})
test('should not show Errors tab when errors are disabled', async ({
comfyPage
}) => {
await comfyPage.actionbar.propertiesButton.click()
await expect(panel.errorsTabIcon).not.toBeVisible()
})
})

View File

@@ -0,0 +1,22 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
import { PropertiesPanelHelper } from './PropertiesPanelHelper'
test.describe('Properties panel - Info tab', () => {
let panel: PropertiesPanelHelper
test.beforeEach(async ({ comfyPage }) => {
panel = new PropertiesPanelHelper(comfyPage.page)
await comfyPage.actionbar.propertiesButton.click()
await comfyPage.nodeOps.selectNodes(['KSampler'])
await panel.switchToTab('Info')
})
test('should show node help content', async () => {
await expect(panel.contentArea).toBeVisible()
await expect(
panel.contentArea.getByRole('heading', { name: 'Inputs' })
).toBeVisible()
})
})

View File

@@ -0,0 +1,126 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
import { PropertiesPanelHelper } from './PropertiesPanelHelper'
test.describe('Properties panel - Node selection', () => {
let panel: PropertiesPanelHelper
test.beforeEach(async ({ comfyPage }) => {
panel = new PropertiesPanelHelper(comfyPage.page)
await comfyPage.actionbar.propertiesButton.click()
})
test.describe('Single node', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.nodeOps.selectNodes(['KSampler'])
})
test('should show node title in panel header', async () => {
await expect(panel.panelTitle).toContainText('KSampler')
})
test('should show Parameters, Info, and Settings tabs', async () => {
await expect(panel.getTab('Parameters')).toBeVisible()
await expect(panel.getTab('Info')).toBeVisible()
await expect(panel.getTab('Settings')).toBeVisible()
})
test('should not show Nodes tab for single node', async () => {
await expect(panel.getTab('Nodes')).not.toBeVisible()
})
test('should display node widgets in Parameters tab', async () => {
await expect(panel.contentArea.getByText('seed')).toBeVisible()
await expect(panel.contentArea.getByText('steps')).toBeVisible()
})
})
test.describe('Multi-node', () => {
test('should show item count in title', async ({ comfyPage }) => {
await comfyPage.nodeOps.selectNodes([
'KSampler',
'CLIP Text Encode (Prompt)'
])
await expect(panel.panelTitle).toContainText('3 items selected')
})
test('should list all selected nodes in Parameters tab', async ({
comfyPage
}) => {
await comfyPage.nodeOps.selectNodes([
'KSampler',
'CLIP Text Encode (Prompt)'
])
await expect(panel.root.getByText('KSampler')).toHaveCount(1)
await expect(
panel.root.getByText('CLIP Text Encode (Prompt)')
).toHaveCount(2)
})
test('should not show Info tab for multi-selection', async ({
comfyPage
}) => {
await comfyPage.nodeOps.selectNodes([
'KSampler',
'CLIP Text Encode (Prompt)'
])
await expect(panel.getTab('Info')).not.toBeVisible()
})
})
test.describe('Selection changes', () => {
test('should update from no selection to node selection', async ({
comfyPage
}) => {
await expect(panel.panelTitle).toContainText('Workflow Overview')
await comfyPage.nodeOps.selectNodes(['KSampler'])
await expect(panel.panelTitle).toContainText('KSampler')
})
test('should update from node selection back to no selection', async ({
comfyPage
}) => {
await comfyPage.nodeOps.selectNodes(['KSampler'])
await expect(panel.panelTitle).toContainText('KSampler')
await comfyPage.page.evaluate(() => {
window.app!.canvas.deselectAll()
})
await comfyPage.nextFrame()
await expect(panel.panelTitle).toContainText('Workflow Overview')
})
test('should update between different single node selections', async ({
comfyPage
}) => {
await comfyPage.nodeOps.selectNodes(['KSampler'])
await expect(panel.panelTitle).toContainText('KSampler')
await comfyPage.page.evaluate(() => {
window.app!.canvas.deselectAll()
})
await comfyPage.nextFrame()
await comfyPage.nodeOps.selectNodes(['Empty Latent Image'])
await expect(panel.panelTitle).toContainText('Empty Latent Image')
})
})
test.describe('Tab labels', () => {
test('should show "Parameters" tab for single node', async ({
comfyPage
}) => {
await comfyPage.nodeOps.selectNodes(['KSampler'])
await expect(panel.getTab('Parameters')).toBeVisible()
})
test('should show "Nodes" tab label for multi-selection', async ({
comfyPage
}) => {
await comfyPage.nodeOps.selectNodes([
'KSampler',
'CLIP Text Encode (Prompt)'
])
await expect(panel.getTab('Nodes')).toBeVisible()
})
})
})

View File

@@ -0,0 +1,120 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
import { PropertiesPanelHelper } from './PropertiesPanelHelper'
test.describe('Properties panel - Node settings', () => {
let panel: PropertiesPanelHelper
test.beforeEach(async ({ comfyPage }) => {
panel = new PropertiesPanelHelper(comfyPage.page)
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.vueNodes.waitForNodes()
await comfyPage.actionbar.propertiesButton.click()
await comfyPage.nodeOps.selectNodes(['KSampler'])
await panel.switchToTab('Settings')
})
test.describe('Node state', () => {
test('should show Normal, Bypass, and Mute state buttons', async () => {
await expect(panel.getNodeStateButton('Normal')).toBeVisible()
await expect(panel.getNodeStateButton('Bypass')).toBeVisible()
await expect(panel.getNodeStateButton('Mute')).toBeVisible()
})
test('should set node to Bypass mode', async ({ comfyPage }) => {
await panel.getNodeStateButton('Bypass').click()
const nodeLocator = comfyPage.vueNodes.getNodeByTitle('KSampler')
await expect(nodeLocator.getByText('Bypassed')).toBeVisible()
})
test('should set node to Mute mode', async ({ comfyPage }) => {
await panel.getNodeStateButton('Mute').click()
const nodeLocator = comfyPage.vueNodes.getNodeByTitle('KSampler')
await expect(nodeLocator.getByText('Muted')).toBeVisible()
})
test('should restore node to Normal mode', async ({ comfyPage }) => {
await panel.getNodeStateButton('Bypass').click()
const nodeLocator = comfyPage.vueNodes.getNodeByTitle('KSampler')
await expect(nodeLocator.getByText('Bypassed')).toBeVisible()
await panel.getNodeStateButton('Normal').click()
await expect(nodeLocator.getByText('Bypassed')).not.toBeVisible()
await expect(nodeLocator.getByText('Muted')).not.toBeVisible()
})
})
test.describe('Node color', () => {
test('should display color swatches', async () => {
await expect(panel.getColorSwatch('noColor')).toBeVisible()
await expect(panel.getColorSwatch('red')).toBeVisible()
await expect(panel.getColorSwatch('blue')).toBeVisible()
})
test('should apply color to node', async ({ comfyPage }) => {
await panel.getColorSwatch('red').click()
await expect.poll(() =>
comfyPage.page.evaluate(() => {
const selected = window.app!.canvas.selected_nodes
const node = Object.values(selected)[0]
return node?.color != null
})
).toBe(true)
})
test('should remove color with noColor swatch', async ({ comfyPage }) => {
await panel.getColorSwatch('red').click()
await expect.poll(() =>
comfyPage.page.evaluate(() => {
const selected = window.app!.canvas.selected_nodes
const node = Object.values(selected)[0]
return node?.color != null
})
).toBe(true)
await panel.getColorSwatch('noColor').click()
await expect.poll(() =>
comfyPage.page.evaluate(() => {
const selected = window.app!.canvas.selected_nodes
const node = Object.values(selected)[0]
return node?.color
})
).toBeFalsy()
})
})
test.describe('Pinned state', () => {
test('should display pinned toggle', async () => {
await expect(panel.pinnedSwitch).toBeVisible()
})
test('should toggle pinned state', async ({ comfyPage }) => {
await panel.pinnedSwitch.click()
const nodeLocator = comfyPage.vueNodes.getNodeByTitle('KSampler')
await expect(
nodeLocator.getByTestId('node-pin-indicator')
).toBeVisible()
})
test('should unpin previously pinned node', async ({ comfyPage }) => {
const nodeLocator = comfyPage.vueNodes.getNodeByTitle('KSampler')
await panel.pinnedSwitch.click()
await expect(
nodeLocator.getByTestId('node-pin-indicator')
).toBeVisible()
await panel.pinnedSwitch.click()
await expect(
nodeLocator.getByTestId('node-pin-indicator')
).not.toBeVisible()
})
})
})

View File

@@ -0,0 +1,34 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
import { PropertiesPanelHelper } from './PropertiesPanelHelper'
test.describe('Properties panel - Open and close', () => {
let panel: PropertiesPanelHelper
test.beforeEach(async ({ comfyPage }) => {
panel = new PropertiesPanelHelper(comfyPage.page)
})
test('should open via actionbar toggle button', async ({ comfyPage }) => {
await expect(panel.root).not.toBeVisible()
await comfyPage.actionbar.propertiesButton.click()
await expect(panel.root).toBeVisible()
})
test('should close via panel close button', async ({ comfyPage }) => {
await comfyPage.actionbar.propertiesButton.click()
await expect(panel.root).toBeVisible()
await panel.closeButton.click()
await expect(panel.root).not.toBeVisible()
})
test('should close via close button after opening', async ({
comfyPage
}) => {
await comfyPage.actionbar.propertiesButton.click()
await expect(panel.root).toBeVisible()
await panel.close()
await expect(panel.root).not.toBeVisible()
})
})

View File

@@ -1,483 +0,0 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
import { PropertiesPanelHelper } from '../../helpers/PropertiesPanelHelper'
test.describe('Properties panel', () => {
let panel: PropertiesPanelHelper
test.beforeEach(async ({ comfyPage }) => {
panel = new PropertiesPanelHelper(comfyPage.page)
})
test.describe('Open and close', () => {
test('should open via actionbar toggle button', async ({ comfyPage }) => {
await expect(panel.root).not.toBeVisible()
await comfyPage.actionbar.propertiesButton.click()
await expect(panel.root).toBeVisible()
})
test('should close via panel close button', async ({ comfyPage }) => {
await comfyPage.actionbar.propertiesButton.click()
await expect(panel.root).toBeVisible()
await panel.closeButton.click()
await expect(panel.root).not.toBeVisible()
})
test('should close via close button after opening', async ({
comfyPage
}) => {
await comfyPage.actionbar.propertiesButton.click()
await expect(panel.root).toBeVisible()
await panel.close()
await expect(panel.root).not.toBeVisible()
})
})
test.describe('Workflow Overview (no selection)', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.actionbar.propertiesButton.click()
await expect(panel.root).toBeVisible()
})
test('should show "Workflow Overview" title when nothing is selected', async () => {
await expect(panel.panelTitle).toContainText('Workflow Overview')
})
test('should show Parameters, Nodes, and Settings tabs', async () => {
await expect(panel.getTab('Parameters')).toBeVisible()
await expect(panel.getTab('Nodes')).toBeVisible()
await expect(panel.getTab('Settings')).toBeVisible()
})
test('should not show Info tab when nothing is selected', async () => {
await expect(panel.getTab('Info')).not.toBeVisible()
})
test('should switch to Nodes tab and list all workflow nodes', async ({
comfyPage
}) => {
await panel.switchToTab('Nodes')
const nodeCount = await comfyPage.nodeOps.getNodeCount()
expect(nodeCount).toBeGreaterThan(0)
await expect(panel.contentArea.locator('text=KSampler')).toBeVisible()
})
test('should switch to Settings tab and show global settings', async () => {
await panel.switchToTab('Settings')
await expect(panel.viewAllSettingsButton).toBeVisible()
})
})
test.describe('Single node selection', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.actionbar.propertiesButton.click()
await comfyPage.nodeOps.selectNodes(['KSampler'])
})
test('should show node title in panel header', async () => {
await expect(panel.panelTitle).toContainText('KSampler')
})
test('should show Parameters, Info, and Settings tabs', async () => {
await expect(panel.getTab('Parameters')).toBeVisible()
await expect(panel.getTab('Info')).toBeVisible()
await expect(panel.getTab('Settings')).toBeVisible()
})
test('should not show Nodes tab for single node', async () => {
await expect(panel.getTab('Nodes')).not.toBeVisible()
})
test('should display node widgets in Parameters tab', async () => {
await expect(panel.contentArea.getByText('seed')).toBeVisible()
await expect(panel.contentArea.getByText('steps')).toBeVisible()
})
})
test.describe('Multi-node selection', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.actionbar.propertiesButton.click()
})
test('should show item count in title', async ({ comfyPage }) => {
await comfyPage.nodeOps.selectNodes([
'KSampler',
'CLIP Text Encode (Prompt)'
])
await expect(panel.panelTitle).toContainText('3 items selected')
})
test('should list all selected nodes in Parameters tab', async ({
comfyPage
}) => {
await comfyPage.nodeOps.selectNodes([
'KSampler',
'CLIP Text Encode (Prompt)'
])
await expect(panel.root.getByText('KSampler')).toHaveCount(1)
await expect(
panel.root.getByText('CLIP Text Encode (Prompt)')
).toHaveCount(2)
})
test('should not show Info tab for multi-selection', async ({
comfyPage
}) => {
await comfyPage.nodeOps.selectNodes([
'KSampler',
'CLIP Text Encode (Prompt)'
])
await expect(panel.getTab('Info')).not.toBeVisible()
})
})
test.describe('Title editing', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.actionbar.propertiesButton.click()
await comfyPage.nodeOps.selectNodes(['KSampler'])
})
test('should show pencil icon for editable title', async () => {
await expect(panel.titleEditIcon).toBeVisible()
})
test('should enter edit mode on pencil click', async () => {
await panel.titleEditIcon.click()
await expect(panel.titleInput).toBeVisible()
})
test('should update node title on edit', async () => {
const newTitle = 'My Custom Sampler'
await panel.editTitle(newTitle)
await expect(panel.panelTitle).toContainText(newTitle)
})
test('should not show pencil icon for multi-selection', async ({
comfyPage
}) => {
await comfyPage.nodeOps.selectNodes([
'KSampler',
'CLIP Text Encode (Prompt)'
])
await expect(panel.titleEditIcon).not.toBeVisible()
})
test('should not show pencil icon when nothing is selected', async ({
comfyPage
}) => {
await comfyPage.page.evaluate(() => {
window.app!.canvas.deselectAll()
})
await comfyPage.nextFrame()
await expect(panel.panelTitle).toContainText('Workflow Overview')
await expect(panel.titleEditIcon).not.toBeVisible()
})
})
test.describe('Search filtering', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.actionbar.propertiesButton.click()
await comfyPage.nodeOps.selectNodes([
'KSampler',
'CLIP Text Encode (Prompt)'
])
})
test('should filter nodes by search query', async () => {
await panel.searchWidgets('seed')
await expect(panel.root.getByText('KSampler')).toHaveCount(1)
await expect(
panel.root.getByText('CLIP Text Encode (Prompt)')
).toHaveCount(0)
})
test('should restore all nodes when search is cleared', async () => {
await panel.searchWidgets('seed')
await panel.clearSearch()
await expect(panel.root.getByText('KSampler')).toHaveCount(1)
await expect(
panel.root.getByText('CLIP Text Encode (Prompt)')
).toHaveCount(2)
})
test('should show empty state for no matches', async () => {
await panel.searchWidgets('nonexistent_widget_xyz')
await expect(
panel.contentArea.getByText(/no .* match|no results|no items/i)
).toBeVisible()
})
})
test.describe('Settings tab - Node state', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.vueNodes.waitForNodes()
await comfyPage.actionbar.propertiesButton.click()
await comfyPage.nodeOps.selectNodes(['KSampler'])
await panel.switchToTab('Settings')
})
test('should show Normal, Bypass, and Mute state buttons', async () => {
await expect(panel.getNodeStateButton('Normal')).toBeVisible()
await expect(panel.getNodeStateButton('Bypass')).toBeVisible()
await expect(panel.getNodeStateButton('Mute')).toBeVisible()
})
test('should set node to Bypass mode', async ({ comfyPage }) => {
await panel.getNodeStateButton('Bypass').click()
const nodeLocator = comfyPage.vueNodes.getNodeByTitle('KSampler')
await expect(nodeLocator.getByText('Bypassed')).toBeVisible()
})
test('should set node to Mute mode', async ({ comfyPage }) => {
await panel.getNodeStateButton('Mute').click()
const nodeLocator = comfyPage.vueNodes.getNodeByTitle('KSampler')
await expect(nodeLocator.getByText('Muted')).toBeVisible()
})
test('should restore node to Normal mode', async ({ comfyPage }) => {
await panel.getNodeStateButton('Bypass').click()
const nodeLocator = comfyPage.vueNodes.getNodeByTitle('KSampler')
await expect(nodeLocator.getByText('Bypassed')).toBeVisible()
await panel.getNodeStateButton('Normal').click()
await expect(nodeLocator.getByText('Bypassed')).not.toBeVisible()
await expect(nodeLocator.getByText('Muted')).not.toBeVisible()
})
})
test.describe('Settings tab - Node color', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.vueNodes.waitForNodes()
await comfyPage.actionbar.propertiesButton.click()
await comfyPage.nodeOps.selectNodes(['KSampler'])
await panel.switchToTab('Settings')
})
test('should display color swatches', async () => {
await expect(panel.getColorSwatch('noColor')).toBeVisible()
await expect(panel.getColorSwatch('red')).toBeVisible()
await expect(panel.getColorSwatch('blue')).toBeVisible()
})
test('should apply color to node', async ({ comfyPage }) => {
const nodeLocator = comfyPage.vueNodes.getNodeByTitle('KSampler')
const initialBg = await nodeLocator.evaluate((el) =>
getComputedStyle(el).backgroundColor
)
await panel.getColorSwatch('red').click()
await expect(nodeLocator).not.toHaveCSS('background-color', initialBg)
})
test('should remove color with noColor swatch', async ({ comfyPage }) => {
const nodeLocator = comfyPage.vueNodes.getNodeByTitle('KSampler')
await panel.getColorSwatch('red').click()
await expect(nodeLocator).not.toHaveCSS(
'background-color',
'rgba(0, 0, 0, 0)'
)
await panel.getColorSwatch('noColor').click()
const bgAfterRemove = await nodeLocator.evaluate((el) =>
getComputedStyle(el).backgroundColor
)
expect(bgAfterRemove).not.toContain('red')
})
})
test.describe('Settings tab - Pinned state', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.vueNodes.waitForNodes()
await comfyPage.actionbar.propertiesButton.click()
await comfyPage.nodeOps.selectNodes(['KSampler'])
await panel.switchToTab('Settings')
})
test('should display pinned toggle', async () => {
await expect(panel.pinnedSwitch).toBeVisible()
})
test('should toggle pinned state', async ({ comfyPage }) => {
await panel.pinnedSwitch.click()
const nodeLocator = comfyPage.vueNodes.getNodeByTitle('KSampler')
await expect(
nodeLocator.getByTestId('node-pin-indicator')
).toBeVisible()
})
test('should unpin previously pinned node', async ({ comfyPage }) => {
const nodeLocator = comfyPage.vueNodes.getNodeByTitle('KSampler')
await panel.pinnedSwitch.click()
await expect(
nodeLocator.getByTestId('node-pin-indicator')
).toBeVisible()
await panel.pinnedSwitch.click()
await expect(
nodeLocator.getByTestId('node-pin-indicator')
).not.toBeVisible()
})
})
test.describe('Info tab', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.actionbar.propertiesButton.click()
await comfyPage.nodeOps.selectNodes(['KSampler'])
await panel.switchToTab('Info')
})
test('should show node help content', async () => {
await expect(panel.contentArea).toBeVisible()
await expect(
panel.contentArea.getByRole('heading', { name: 'Inputs' })
).toBeVisible()
})
})
test.describe('Global Settings tab (no selection)', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.actionbar.propertiesButton.click()
await panel.switchToTab('Settings')
})
test('should show "View all settings" button', async () => {
await expect(panel.viewAllSettingsButton).toBeVisible()
})
test('should show Nodes section with toggles', async () => {
await expect(
panel.contentArea.getByRole('button', { name: 'NODES' })
).toBeVisible()
})
test('should show Canvas section with grid settings', async () => {
await expect(panel.contentArea.getByText('Canvas')).toBeVisible()
})
test('should show Connection Links section', async () => {
await expect(
panel.contentArea.getByText('Connection Links')
).toBeVisible()
})
})
test.describe('Selection changes update panel', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.actionbar.propertiesButton.click()
})
test('should update from no selection to node selection', async ({
comfyPage
}) => {
await expect(panel.panelTitle).toContainText('Workflow Overview')
await comfyPage.nodeOps.selectNodes(['KSampler'])
await expect(panel.panelTitle).toContainText('KSampler')
})
test('should update from node selection back to no selection', async ({
comfyPage
}) => {
await comfyPage.nodeOps.selectNodes(['KSampler'])
await expect(panel.panelTitle).toContainText('KSampler')
await comfyPage.page.evaluate(() => {
window.app!.canvas.deselectAll()
})
await comfyPage.nextFrame()
await expect(panel.panelTitle).toContainText('Workflow Overview')
})
test('should update between different single node selections', async ({
comfyPage
}) => {
await comfyPage.nodeOps.selectNodes(['KSampler'])
await expect(panel.panelTitle).toContainText('KSampler')
await comfyPage.page.evaluate(() => {
window.app!.canvas.deselectAll()
})
await comfyPage.nextFrame()
await comfyPage.nodeOps.selectNodes(['Empty Latent Image'])
await expect(panel.panelTitle).toContainText('Empty Latent Image')
})
})
test.describe('Nodes tab (no selection)', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.actionbar.propertiesButton.click()
await panel.switchToTab('Nodes')
})
test('should list all nodes in the workflow', async ({ comfyPage }) => {
const nodeCount = await comfyPage.nodeOps.getNodeCount()
expect(nodeCount).toBeGreaterThan(0)
await expect(
panel.contentArea.getByText('KSampler').first()
).toBeVisible()
})
test('should filter nodes by search in Nodes tab', async () => {
await panel.searchWidgets('KSampler')
await expect(
panel.contentArea.getByText('KSampler').first()
).toBeVisible()
})
})
test.describe('Tab label changes based on selection count', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.actionbar.propertiesButton.click()
})
test('should show "Parameters" tab for single node', async ({
comfyPage
}) => {
await comfyPage.nodeOps.selectNodes(['KSampler'])
await expect(panel.getTab('Parameters')).toBeVisible()
})
test('should show "Nodes" tab label for multi-selection', async ({
comfyPage
}) => {
await comfyPage.nodeOps.selectNodes([
'KSampler',
'CLIP Text Encode (Prompt)'
])
await expect(panel.getTab('Nodes')).toBeVisible()
})
})
test.describe('Errors tab', () => {
test('should show Errors tab when errors exist', async ({ comfyPage }) => {
await comfyPage.settings.setSetting(
'Comfy.RightSidePanel.ShowErrorsTab',
true
)
await comfyPage.workflow.loadWorkflow('missing/missing_nodes')
await comfyPage.actionbar.propertiesButton.click()
await comfyPage.nextFrame()
await expect(panel.errorsTabIcon).toBeVisible()
})
test('should not show Errors tab when errors are disabled', async ({
comfyPage
}) => {
await comfyPage.actionbar.propertiesButton.click()
await expect(panel.errorsTabIcon).not.toBeVisible()
})
})
})

View File

@@ -0,0 +1,41 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
import { PropertiesPanelHelper } from './PropertiesPanelHelper'
test.describe('Properties panel - Search filtering', () => {
let panel: PropertiesPanelHelper
test.beforeEach(async ({ comfyPage }) => {
panel = new PropertiesPanelHelper(comfyPage.page)
await comfyPage.actionbar.propertiesButton.click()
await comfyPage.nodeOps.selectNodes([
'KSampler',
'CLIP Text Encode (Prompt)'
])
})
test('should filter nodes by search query', async () => {
await panel.searchWidgets('seed')
await expect(panel.root.getByText('KSampler')).toHaveCount(1)
await expect(
panel.root.getByText('CLIP Text Encode (Prompt)')
).toHaveCount(0)
})
test('should restore all nodes when search is cleared', async () => {
await panel.searchWidgets('seed')
await panel.clearSearch()
await expect(panel.root.getByText('KSampler')).toHaveCount(1)
await expect(
panel.root.getByText('CLIP Text Encode (Prompt)')
).toHaveCount(2)
})
test('should show empty state for no matches', async () => {
await panel.searchWidgets('nonexistent_widget_xyz')
await expect(
panel.contentArea.getByText(/no .* match|no results|no items/i)
).toBeVisible()
})
})

View File

@@ -0,0 +1,50 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
import { PropertiesPanelHelper } from './PropertiesPanelHelper'
test.describe('Properties panel - Title editing', () => {
let panel: PropertiesPanelHelper
test.beforeEach(async ({ comfyPage }) => {
panel = new PropertiesPanelHelper(comfyPage.page)
await comfyPage.actionbar.propertiesButton.click()
await comfyPage.nodeOps.selectNodes(['KSampler'])
})
test('should show pencil icon for editable title', async () => {
await expect(panel.titleEditIcon).toBeVisible()
})
test('should enter edit mode on pencil click', async () => {
await panel.titleEditIcon.click()
await expect(panel.titleInput).toBeVisible()
})
test('should update node title on edit', async () => {
const newTitle = 'My Custom Sampler'
await panel.editTitle(newTitle)
await expect(panel.panelTitle).toContainText(newTitle)
})
test('should not show pencil icon for multi-selection', async ({
comfyPage
}) => {
await comfyPage.nodeOps.selectNodes([
'KSampler',
'CLIP Text Encode (Prompt)'
])
await expect(panel.titleEditIcon).not.toBeVisible()
})
test('should not show pencil icon when nothing is selected', async ({
comfyPage
}) => {
await comfyPage.page.evaluate(() => {
window.app!.canvas.deselectAll()
})
await comfyPage.nextFrame()
await expect(panel.panelTitle).toContainText('Workflow Overview')
await expect(panel.titleEditIcon).not.toBeVisible()
})
})

View File

@@ -0,0 +1,74 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
import { PropertiesPanelHelper } from './PropertiesPanelHelper'
test.describe('Properties panel - Workflow Overview', () => {
let panel: PropertiesPanelHelper
test.beforeEach(async ({ comfyPage }) => {
panel = new PropertiesPanelHelper(comfyPage.page)
await comfyPage.actionbar.propertiesButton.click()
await expect(panel.root).toBeVisible()
})
test('should show "Workflow Overview" title when nothing is selected', async () => {
await expect(panel.panelTitle).toContainText('Workflow Overview')
})
test('should show Parameters, Nodes, and Settings tabs', async () => {
await expect(panel.getTab('Parameters')).toBeVisible()
await expect(panel.getTab('Nodes')).toBeVisible()
await expect(panel.getTab('Settings')).toBeVisible()
})
test('should not show Info tab when nothing is selected', async () => {
await expect(panel.getTab('Info')).not.toBeVisible()
})
test('should switch to Nodes tab and list all workflow nodes', async ({
comfyPage
}) => {
await panel.switchToTab('Nodes')
const nodeCount = await comfyPage.nodeOps.getNodeCount()
expect(nodeCount).toBeGreaterThan(0)
await expect(panel.contentArea.locator('text=KSampler')).toBeVisible()
})
test('should filter nodes by search in Nodes tab', async () => {
await panel.switchToTab('Nodes')
await panel.searchWidgets('KSampler')
await expect(
panel.contentArea.getByText('KSampler').first()
).toBeVisible()
})
test('should switch to Settings tab and show global settings', async () => {
await panel.switchToTab('Settings')
await expect(panel.viewAllSettingsButton).toBeVisible()
})
test('should show "View all settings" button', async () => {
await panel.switchToTab('Settings')
await expect(panel.viewAllSettingsButton).toBeVisible()
})
test('should show Nodes section with toggles', async () => {
await panel.switchToTab('Settings')
await expect(
panel.contentArea.getByRole('button', { name: 'NODES' })
).toBeVisible()
})
test('should show Canvas section with grid settings', async () => {
await panel.switchToTab('Settings')
await expect(panel.contentArea.getByText('Canvas')).toBeVisible()
})
test('should show Connection Links section', async () => {
await panel.switchToTab('Settings')
await expect(
panel.contentArea.getByText('Connection Links')
).toBeVisible()
})
})