test: add e2e tests for properties panel

- Add ComfyPropertiesPanel fixture class with locators and helpers
- Add tests for basic functionality, search, title editing, global settings, and tab navigation
- Tag all tests with @ui for filtering

Amp-Thread-ID: https://ampcode.com/threads/T-019c16eb-8621-7473-9062-a57b0a1e782a
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
bymyself
2026-01-31 17:57:55 -08:00
parent 4a85bffb1f
commit 610165cde9
7 changed files with 642 additions and 12 deletions

View File

@@ -13,6 +13,7 @@ import { ComfyTemplates } from '../helpers/templates'
import { ComfyMouse } from './ComfyMouse'
import { VueNodeHelpers } from './VueNodeHelpers'
import { ComfyNodeSearchBox } from './components/ComfyNodeSearchBox'
import { ComfyPropertiesPanel } from './components/ComfyPropertiesPanel'
import { SettingDialog } from './components/SettingDialog'
import {
NodeLibrarySidebarTab,
@@ -26,18 +27,6 @@ dotenv.config()
type WorkspaceStore = ReturnType<typeof useWorkspaceStore>
class ComfyPropertiesPanel {
readonly root: Locator
readonly panelTitle: Locator
readonly searchBox: Locator
constructor(readonly page: Page) {
this.root = page.getByTestId('properties-panel')
this.panelTitle = this.root.locator('h3')
this.searchBox = this.root.getByPlaceholder('Search...')
}
}
class ComfyMenu {
private _nodeLibraryTab: NodeLibrarySidebarTab | null = null
private _workflowsTab: WorkflowsSidebarTab | null = null
@@ -170,6 +159,7 @@ export class ComfyPage {
public readonly settingDialog: SettingDialog
public readonly confirmDialog: ConfirmDialog
public readonly vueNodes: VueNodeHelpers
public readonly propertiesPanel: ComfyPropertiesPanel
/** Worker index to test user ID */
public readonly userIds: string[] = []
@@ -202,6 +192,7 @@ export class ComfyPage {
this.settingDialog = new SettingDialog(page, this)
this.confirmDialog = new ConfirmDialog(page)
this.vueNodes = new VueNodeHelpers(page)
this.propertiesPanel = new ComfyPropertiesPanel(page)
}
convertLeafToContent(structure: FolderStructure): FolderStructure {

View File

@@ -0,0 +1,84 @@
import type { Locator, Page } from '@playwright/test'
export class ComfyPropertiesPanel {
readonly root: Locator
readonly header: Locator
readonly panelTitle: Locator
readonly nodeTitleInput: Locator
readonly closeButton: Locator
readonly searchBox: Locator
readonly tabList: Locator
constructor(readonly page: Page) {
this.root = page.getByTestId('properties-panel')
this.header = this.root.locator('section').first()
this.panelTitle = this.root.locator('h3')
this.nodeTitleInput = this.root.getByTestId('node-title-input')
this.closeButton = this.root.getByRole('button', { name: 'Close' })
this.searchBox = this.root.getByPlaceholder('Search...')
this.tabList = this.root.locator('[role="tablist"]')
}
getTab(tabName: string): Locator {
return this.tabList.getByRole('tab', { name: tabName })
}
async clickTab(tabName: string) {
await this.getTab(tabName).click()
}
async close() {
await this.closeButton.click()
}
async editTitle(newTitle: string) {
await this.panelTitle.click()
await this.nodeTitleInput.fill(newTitle)
await this.nodeTitleInput.press('Enter')
}
async cancelTitleEdit() {
await this.panelTitle.click()
await this.nodeTitleInput.press('Escape')
}
getNodeSection(nodeTitle: string): Locator {
return this.root.locator(`[data-testid="node-section-${nodeTitle}"]`)
}
getAccordionItem(label: string): Locator {
return this.root.locator('.border-b', { hasText: label })
}
get globalSettingsSection() {
return {
nodes: this.getAccordionItem('Nodes'),
canvas: this.getAccordionItem('Canvas'),
connectionLinks: this.getAccordionItem('Connection Links'),
viewAllSettingsButton: this.root.getByRole('button', {
name: 'View all settings'
})
}
}
async toggleSwitch(switchLabel: string) {
const switchContainer = this.root.locator('label', {
hasText: switchLabel
})
const toggle = switchContainer.locator('button[role="switch"]')
await toggle.click()
}
async getSwitchState(switchLabel: string): Promise<boolean> {
const switchContainer = this.root.locator('label', {
hasText: switchLabel
})
const toggle = switchContainer.locator('button[role="switch"]')
const ariaChecked = await toggle.getAttribute('aria-checked')
return ariaChecked === 'true'
}
get noResultsMessage(): Locator {
return this.root.getByText('No results', { exact: false })
}
}

View File

@@ -0,0 +1,153 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
test.describe('Properties panel basic functionality', { tag: ['@ui'] }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.actionbar.propertiesButton.click()
await expect(comfyPage.propertiesPanel.root).toBeVisible()
})
test.describe('Panel visibility and toggle', () => {
test('opens panel via actionbar button', async ({ comfyPage }) => {
const { propertiesPanel } = comfyPage
await propertiesPanel.close()
await expect(propertiesPanel.root).not.toBeVisible()
await comfyPage.actionbar.propertiesButton.click()
await expect(propertiesPanel.root).toBeVisible()
})
test('closes panel via close button', async ({ comfyPage }) => {
const { propertiesPanel } = comfyPage
await expect(propertiesPanel.root).toBeVisible()
await propertiesPanel.closeButton.click()
await expect(propertiesPanel.root).not.toBeVisible()
})
test('persists open state after page reload', async ({ comfyPage }) => {
const { propertiesPanel } = comfyPage
await expect(propertiesPanel.root).toBeVisible()
await comfyPage.page.reload()
await comfyPage.setup()
await expect(propertiesPanel.root).toBeVisible()
})
})
test.describe('Workflow overview (no selection)', () => {
test('shows "Workflow Overview" title when nothing selected', async ({
comfyPage
}) => {
const { propertiesPanel } = comfyPage
await expect(propertiesPanel.panelTitle).toContainText(
'Workflow Overview'
)
})
test('shows Parameters, Nodes, and Global Settings tabs when nothing selected', async ({
comfyPage
}) => {
const { propertiesPanel } = comfyPage
await expect(propertiesPanel.getTab('Parameters')).toBeVisible()
await expect(propertiesPanel.getTab('Nodes')).toBeVisible()
await expect(propertiesPanel.getTab('Global Settings')).toBeVisible()
})
test('Nodes tab displays workflow nodes', async ({ comfyPage }) => {
const { propertiesPanel } = comfyPage
await propertiesPanel.clickTab('Nodes')
await expect(propertiesPanel.root).toContainText('KSampler')
})
})
test.describe('Single node selection', () => {
test('updates title to node name when single node selected', async ({
comfyPage
}) => {
const { propertiesPanel } = comfyPage
await comfyPage.selectNodes(['KSampler'])
await expect(propertiesPanel.panelTitle).not.toContainText(
'Workflow Overview'
)
})
test('shows Parameters, Info, and Settings tabs for single node', async ({
comfyPage
}) => {
const { propertiesPanel } = comfyPage
await comfyPage.selectNodes(['KSampler'])
await expect(propertiesPanel.getTab('Parameters')).toBeVisible()
await expect(propertiesPanel.getTab('Info')).toBeVisible()
await expect(propertiesPanel.getTab('Settings')).toBeVisible()
await expect(propertiesPanel.getTab('Nodes')).not.toBeVisible()
})
test('Info tab shows node documentation', async ({ comfyPage }) => {
const { propertiesPanel } = comfyPage
await comfyPage.selectNodes(['KSampler'])
await propertiesPanel.clickTab('Info')
await expect(propertiesPanel.root).toContainText('KSampler')
})
test('shows widget inputs in Parameters tab', async ({ comfyPage }) => {
const { propertiesPanel } = comfyPage
await comfyPage.selectNodes(['KSampler'])
await expect(propertiesPanel.root.getByText('seed')).toBeVisible()
await expect(propertiesPanel.root.getByText('steps')).toBeVisible()
await expect(propertiesPanel.root.getByText('cfg')).toBeVisible()
})
})
test.describe('Multiple node selection', () => {
test('shows item count in title for multiple selections', async ({
comfyPage
}) => {
const { propertiesPanel } = comfyPage
await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)'])
await expect(propertiesPanel.panelTitle).toContainText('items selected')
})
test('shows Parameters and Settings tabs for multiple selection', async ({
comfyPage
}) => {
const { propertiesPanel } = comfyPage
await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)'])
await expect(propertiesPanel.getTab('Parameters')).toBeVisible()
await expect(propertiesPanel.getTab('Settings')).toBeVisible()
await expect(propertiesPanel.getTab('Info')).not.toBeVisible()
})
test('lists all selected nodes in Parameters tab', async ({
comfyPage
}) => {
const { propertiesPanel } = comfyPage
await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)'])
await expect(propertiesPanel.root.getByText('KSampler')).toBeVisible()
await expect(
propertiesPanel.root.getByText('CLIP Text Encode (Prompt)')
).toBeVisible()
})
})
})

View File

@@ -0,0 +1,124 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
test.describe('Properties panel global settings', { tag: ['@ui'] }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.actionbar.propertiesButton.click()
await expect(comfyPage.propertiesPanel.root).toBeVisible()
await comfyPage.propertiesPanel.clickTab('Global Settings')
})
test.describe('Global settings sections', () => {
test('displays Nodes section', async ({ comfyPage }) => {
const { propertiesPanel } = comfyPage
await expect(propertiesPanel.root.getByText('Nodes')).toBeVisible()
})
test('displays Canvas section', async ({ comfyPage }) => {
const { propertiesPanel } = comfyPage
await expect(propertiesPanel.root.getByText('Canvas')).toBeVisible()
})
test('displays Connection Links section', async ({ comfyPage }) => {
const { propertiesPanel } = comfyPage
await expect(
propertiesPanel.root.getByText('Connection Links')
).toBeVisible()
})
test('has View all settings button', async ({ comfyPage }) => {
const { propertiesPanel } = comfyPage
const viewAllButton = propertiesPanel.root.getByRole('button', {
name: /View all settings/i
})
await expect(viewAllButton).toBeVisible()
})
})
test.describe('Nodes section settings', () => {
test('can toggle Show advanced parameters', async ({ comfyPage }) => {
const { propertiesPanel } = comfyPage
const toggle = propertiesPanel.root
.locator('label', { hasText: 'Show advanced parameters' })
.locator('button[role="switch"]')
await expect(toggle).toBeVisible()
const initialState = await toggle.getAttribute('aria-checked')
await toggle.click()
const newState = await toggle.getAttribute('aria-checked')
expect(newState).not.toBe(initialState)
})
test('can toggle Nodes 2.0 setting', async ({ comfyPage }) => {
const { propertiesPanel } = comfyPage
const toggle = propertiesPanel.root
.locator('label', { hasText: 'Nodes 2.0' })
.locator('button[role="switch"]')
await expect(toggle).toBeVisible()
})
})
test.describe('Canvas section settings', () => {
test('can adjust grid spacing', async ({ comfyPage }) => {
const { propertiesPanel } = comfyPage
const gridSpacingLabel = propertiesPanel.root.getByText('Grid Spacing')
await expect(gridSpacingLabel).toBeVisible()
})
test('can toggle Snap nodes to grid', async ({ comfyPage }) => {
const { propertiesPanel } = comfyPage
const toggle = propertiesPanel.root
.locator('label', { hasText: 'Snap nodes to grid' })
.locator('button[role="switch"]')
await expect(toggle).toBeVisible()
})
})
test.describe('Connection Links section settings', () => {
test('has link shape selector', async ({ comfyPage }) => {
const { propertiesPanel } = comfyPage
const linkShapeLabel = propertiesPanel.root.getByText('Link Shape')
await expect(linkShapeLabel).toBeVisible()
})
test('can toggle Show connected links', async ({ comfyPage }) => {
const { propertiesPanel } = comfyPage
const toggle = propertiesPanel.root
.locator('label', { hasText: 'Show connected links' })
.locator('button[role="switch"]')
await expect(toggle).toBeVisible()
})
})
test.describe('View all settings navigation', () => {
test('opens full settings dialog when clicking View all settings', async ({
comfyPage
}) => {
const { propertiesPanel } = comfyPage
const viewAllButton = propertiesPanel.root.getByRole('button', {
name: /View all settings/i
})
await viewAllButton.click()
await expect(comfyPage.settingDialog.root).toBeVisible()
})
})
})

View File

@@ -0,0 +1,73 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
test.describe('Properties panel search functionality', { tag: ['@ui'] }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.actionbar.propertiesButton.click()
await expect(comfyPage.propertiesPanel.root).toBeVisible()
})
test.describe('Search with multiple nodes selected', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)'])
})
test('filters widgets by search query', async ({ comfyPage }) => {
const { propertiesPanel } = comfyPage
await propertiesPanel.searchBox.fill('seed')
await expect(propertiesPanel.root.getByText('KSampler')).toBeVisible()
await expect(
propertiesPanel.root.getByText('CLIP Text Encode (Prompt)')
).not.toBeVisible()
})
test('shows all nodes when search is cleared', async ({ comfyPage }) => {
const { propertiesPanel } = comfyPage
await propertiesPanel.searchBox.fill('seed')
await propertiesPanel.searchBox.fill('')
await expect(propertiesPanel.root.getByText('KSampler')).toBeVisible()
await expect(
propertiesPanel.root.getByText('CLIP Text Encode (Prompt)')
).toBeVisible()
})
test('shows no results message when search matches nothing', async ({
comfyPage
}) => {
const { propertiesPanel } = comfyPage
await propertiesPanel.searchBox.fill('nonexistent_widget_xyz')
await expect(propertiesPanel.noResultsMessage).toBeVisible()
})
})
test.describe('Search with single node selected', () => {
test('filters widgets within single node', async ({ comfyPage }) => {
const { propertiesPanel } = comfyPage
await comfyPage.selectNodes(['KSampler'])
await propertiesPanel.searchBox.fill('cfg')
await expect(propertiesPanel.root.getByText('cfg')).toBeVisible()
await expect(
propertiesPanel.root.getByText('seed', { exact: true })
).not.toBeVisible()
})
})
test.describe('Search in workflow overview mode', () => {
test('Nodes tab has search functionality', async ({ comfyPage }) => {
const { propertiesPanel } = comfyPage
await propertiesPanel.clickTab('Nodes')
await expect(propertiesPanel.searchBox).toBeVisible()
})
})
})

View File

@@ -0,0 +1,114 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
test.describe('Properties panel tab navigation', { tag: ['@ui'] }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.actionbar.propertiesButton.click()
await expect(comfyPage.propertiesPanel.root).toBeVisible()
})
test.describe('Tab switching', () => {
test('switches between tabs in workflow overview mode', async ({
comfyPage
}) => {
const { propertiesPanel } = comfyPage
await expect(propertiesPanel.getTab('Parameters')).toBeVisible()
await propertiesPanel.clickTab('Nodes')
await expect(propertiesPanel.root.getByText('KSampler')).toBeVisible()
await propertiesPanel.clickTab('Global Settings')
await expect(propertiesPanel.root.getByText('Nodes')).toBeVisible()
await expect(propertiesPanel.root.getByText('Canvas')).toBeVisible()
})
test('switches between tabs in single node mode', async ({ comfyPage }) => {
const { propertiesPanel } = comfyPage
await comfyPage.selectNodes(['KSampler'])
await expect(propertiesPanel.getTab('Parameters')).toBeVisible()
await expect(propertiesPanel.getTab('Info')).toBeVisible()
await expect(propertiesPanel.getTab('Settings')).toBeVisible()
await propertiesPanel.clickTab('Info')
await expect(propertiesPanel.root).toContainText('KSampler')
await propertiesPanel.clickTab('Settings')
await expect(
propertiesPanel.root.getByText('Color').first()
).toBeVisible()
await propertiesPanel.clickTab('Parameters')
await expect(propertiesPanel.root.getByText('seed')).toBeVisible()
})
})
test.describe('Tab availability based on selection', () => {
test('shows different tabs based on selection state', async ({
comfyPage
}) => {
const { propertiesPanel } = comfyPage
await expect(propertiesPanel.getTab('Parameters')).toBeVisible()
await expect(propertiesPanel.getTab('Nodes')).toBeVisible()
await expect(propertiesPanel.getTab('Global Settings')).toBeVisible()
await expect(propertiesPanel.getTab('Info')).not.toBeVisible()
await comfyPage.selectNodes(['KSampler'])
await expect(propertiesPanel.getTab('Parameters')).toBeVisible()
await expect(propertiesPanel.getTab('Info')).toBeVisible()
await expect(propertiesPanel.getTab('Settings')).toBeVisible()
await expect(propertiesPanel.getTab('Nodes')).not.toBeVisible()
await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)'])
await expect(propertiesPanel.getTab('Parameters')).toBeVisible()
await expect(propertiesPanel.getTab('Settings')).toBeVisible()
await expect(propertiesPanel.getTab('Info')).not.toBeVisible()
await expect(propertiesPanel.getTab('Nodes')).not.toBeVisible()
})
test('first tab updates for multiple selection', async ({ comfyPage }) => {
const { propertiesPanel } = comfyPage
await comfyPage.selectNodes(['KSampler'])
await expect(propertiesPanel.getTab('Parameters')).toBeVisible()
await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)'])
const firstTab = propertiesPanel.tabList.locator('[role="tab"]').first()
await expect(firstTab).toBeVisible()
})
})
test.describe('Tab persistence', () => {
test('remembers active tab when selection changes', async ({
comfyPage
}) => {
const { propertiesPanel } = comfyPage
await comfyPage.selectNodes(['KSampler'])
await propertiesPanel.clickTab('Settings')
await comfyPage.selectNodes(['CLIP Text Encode (Prompt)'])
await expect(propertiesPanel.getTab('Settings')).toBeVisible()
})
test('falls back to default tab when current tab not available', async ({
comfyPage
}) => {
const { propertiesPanel } = comfyPage
await comfyPage.selectNodes(['KSampler'])
await propertiesPanel.clickTab('Info')
await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)'])
await expect(propertiesPanel.getTab('Info')).not.toBeVisible()
const firstTab = propertiesPanel.tabList.locator('[role="tab"]').first()
await expect(firstTab).toHaveAttribute('aria-selected', 'true')
})
})
})

View File

@@ -0,0 +1,91 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
test.describe('Properties panel title editing', { tag: ['@ui'] }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.actionbar.propertiesButton.click()
await expect(comfyPage.propertiesPanel.root).toBeVisible()
})
test.describe('Single node title editing', () => {
test('shows editable title for single node selection', async ({
comfyPage
}) => {
const { propertiesPanel } = comfyPage
await comfyPage.selectNodes(['KSampler'])
await propertiesPanel.panelTitle.click()
await expect(propertiesPanel.nodeTitleInput).toBeVisible()
})
test('edits node title successfully', async ({ comfyPage }) => {
const { propertiesPanel } = comfyPage
await comfyPage.selectNodes(['KSampler'])
const newTitle = 'My Custom KSampler'
await propertiesPanel.panelTitle.click()
await propertiesPanel.nodeTitleInput.fill(newTitle)
await propertiesPanel.nodeTitleInput.press('Enter')
await expect(propertiesPanel.panelTitle).toContainText(newTitle)
const renamedNodes = await comfyPage.getNodeRefsByTitle(newTitle)
expect(renamedNodes.length).toBeGreaterThan(0)
})
test('cancels title edit with Escape', async ({ comfyPage }) => {
const { propertiesPanel } = comfyPage
await comfyPage.selectNodes(['KSampler'])
const originalTitle = await propertiesPanel.panelTitle.innerText()
await propertiesPanel.panelTitle.click()
await propertiesPanel.nodeTitleInput.fill('Should Not Be Saved')
await propertiesPanel.nodeTitleInput.press('Escape')
await expect(propertiesPanel.panelTitle).toContainText(originalTitle)
})
test('does not save empty title', async ({ comfyPage }) => {
const { propertiesPanel } = comfyPage
await comfyPage.selectNodes(['KSampler'])
const originalTitle = await propertiesPanel.panelTitle.innerText()
await propertiesPanel.panelTitle.click()
await propertiesPanel.nodeTitleInput.fill('')
await propertiesPanel.nodeTitleInput.press('Enter')
await expect(propertiesPanel.panelTitle).toContainText(originalTitle)
})
})
test.describe('Title editing restrictions', () => {
test('does not allow title editing for multiple selection', async ({
comfyPage
}) => {
const { propertiesPanel } = comfyPage
await comfyPage.selectNodes(['KSampler', 'CLIP Text Encode (Prompt)'])
await propertiesPanel.panelTitle.click()
await expect(propertiesPanel.nodeTitleInput).not.toBeVisible()
})
test('does not allow title editing for workflow overview', async ({
comfyPage
}) => {
const { propertiesPanel } = comfyPage
await propertiesPanel.panelTitle.click()
await expect(propertiesPanel.nodeTitleInput).not.toBeVisible()
})
})
})