Compare commits

..

6 Commits

Author SHA1 Message Date
dante01yoon
116d2c1c94 fix(test): remove unused JobHistorySidebarTab fixture class 2026-04-02 07:53:16 +09:00
dante01yoon
891c6ee8a0 fix(test): address PR review feedback for Node Library V2 E2E tests
Remove redundant tests (tab visibility, sort button, blueprints tab),
strengthen search filtering assertions, use deterministic folder
expansion, and remove unrelated JobHistorySidebarTab fixture wiring.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 22:34:34 +09:00
dante01yoon
0be4188fff fix(test): fix blueprints and sort tests for CI
- Blueprints tab: no blueprint nodes in test env, simplify to tab
  selection assertion only
- Sort: alphabetical doesn't remove folders, assert sort dropdown
  shows multiple options instead
2026-04-01 18:10:42 +09:00
dante01yoon
c3440ad68d test: add 5 E2E scenarios for Node Library V2 sidebar
- Blueprints tab shows section headings (My/Comfy Blueprints)
- Drag node to canvas adds it to the graph
- Right-click node shows context menu with bookmark option
- Search clear restores folder view
- Sort alphabetical changes tree layout
2026-04-01 15:50:34 +09:00
dante01yoon
761d9caf56 fix(test): use role-based selectors for V2 tree nodes
V2 TreeExplorerV2Node does not render data-testid attributes.
Use getByRole('treeitem') with name matching instead.
2026-04-01 15:30:22 +09:00
dante01yoon
4237395196 test: add E2E Playwright tests for Node Library V2 sidebar
Add browser tests for the V2 Node Library sidebar tab gated by
Comfy.NodeLibrary.NewDesign feature flag. Tests cover tab visibility,
tab switching, folder expansion, and search filtering.

Also adds NodeLibrarySidebarTabV2 fixture class and exposes it via
ComfyPage.menu.nodeLibraryTabV2 for V2-specific locators.
2026-04-01 15:13:58 +09:00
5 changed files with 186 additions and 217 deletions

View File

@@ -20,6 +20,7 @@ import { SettingDialog } from '@e2e/fixtures/components/SettingDialog'
import {
AssetsSidebarTab,
NodeLibrarySidebarTab,
NodeLibrarySidebarTabV2,
WorkflowsSidebarTab
} from '@e2e/fixtures/components/SidebarTab'
import { Topbar } from '@e2e/fixtures/components/Topbar'
@@ -56,6 +57,7 @@ class ComfyPropertiesPanel {
class ComfyMenu {
private _assetsTab: AssetsSidebarTab | null = null
private _nodeLibraryTab: NodeLibrarySidebarTab | null = null
private _nodeLibraryTabV2: NodeLibrarySidebarTabV2 | null = null
private _workflowsTab: WorkflowsSidebarTab | null = null
private _topbar: Topbar | null = null
@@ -78,6 +80,11 @@ class ComfyMenu {
return this._nodeLibraryTab
}
get nodeLibraryTabV2() {
this._nodeLibraryTabV2 ??= new NodeLibrarySidebarTabV2(this.page)
return this._nodeLibraryTabV2
}
get assetsTab() {
this._assetsTab ??= new AssetsSidebarTab(this.page)
return this._assetsTab

View File

@@ -20,8 +20,6 @@ export class BottomPanel {
readonly root: Locator
readonly keyboardShortcutsButton: Locator
readonly toggleButton: Locator
readonly closeButton: Locator
readonly resizeGutter: Locator
readonly shortcuts: ShortcutsTab
constructor(readonly page: Page) {
@@ -32,10 +30,6 @@ export class BottomPanel {
this.toggleButton = page.getByRole('button', {
name: /Toggle Bottom Panel/i
})
this.closeButton = this.root.getByRole('button', { name: /Close/i })
this.resizeGutter = page.locator(
'.splitter-overlay-bottom > .p-splitter-gutter'
)
this.shortcuts = new ShortcutsTab(page)
}
}

View File

@@ -100,6 +100,59 @@ export class NodeLibrarySidebarTab extends SidebarTab {
}
}
export class NodeLibrarySidebarTabV2 extends SidebarTab {
constructor(public override readonly page: Page) {
super(page, 'node-library')
}
get searchInput() {
return this.page.getByPlaceholder('Search...')
}
get sidebarContent() {
return this.page.locator('.sidebar-content-container')
}
getTab(name: string) {
return this.sidebarContent.getByRole('tab', { name, exact: true })
}
get allTab() {
return this.getTab('All')
}
get blueprintsTab() {
return this.getTab('Blueprints')
}
get sortButton() {
return this.sidebarContent.getByRole('button', { name: 'Sort' })
}
getFolder(folderName: string) {
return this.sidebarContent
.getByRole('treeitem', { name: folderName })
.first()
}
getNode(nodeName: string) {
return this.sidebarContent.getByRole('treeitem', { name: nodeName }).first()
}
async expandFolder(folderName: string) {
const folder = this.getFolder(folderName)
const isExpanded = await folder.getAttribute('aria-expanded')
if (isExpanded !== 'true') {
await folder.click()
}
}
override async open() {
await super.open()
await this.searchInput.waitFor({ state: 'visible' })
}
}
export class WorkflowsSidebarTab extends SidebarTab {
constructor(public override readonly page: Page) {
super(page, 'workflows')

View File

@@ -1,211 +0,0 @@
import {
comfyPageFixture as test,
comfyExpect as expect
} from '../fixtures/ComfyPage'
test.describe('Bottom Panel', { tag: '@ui' }, () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
})
test('should close panel via close button inside the panel', async ({
comfyPage
}) => {
const { bottomPanel } = comfyPage
await bottomPanel.toggleButton.click()
await expect(bottomPanel.root).toBeVisible()
await bottomPanel.closeButton.click()
await expect(bottomPanel.root).not.toBeVisible()
})
test('should remember last active tab when re-opening terminal panel', async ({
comfyPage
}) => {
const { bottomPanel } = comfyPage
await bottomPanel.toggleButton.click()
await expect(bottomPanel.root).toBeVisible()
const logsTab = comfyPage.page.getByRole('tab', { name: /Logs/i })
const hasLogsTab = await logsTab.isVisible().catch(() => false)
if (!hasLogsTab) {
test.skip()
return
}
// Logs should be active by default
await expect(logsTab).toHaveAttribute('aria-selected', 'true')
// Close then reopen
await bottomPanel.closeButton.click()
await expect(bottomPanel.root).not.toBeVisible()
await bottomPanel.toggleButton.click()
await expect(bottomPanel.root).toBeVisible()
// Logs tab should still be the active tab
await expect(logsTab).toHaveAttribute('aria-selected', 'true')
})
test('should display resize gutter when panel is open', async ({
comfyPage
}) => {
const { bottomPanel } = comfyPage
await bottomPanel.toggleButton.click()
await expect(bottomPanel.root).toBeVisible()
await expect(bottomPanel.resizeGutter).toBeVisible()
})
test('should hide resize gutter when panel is closed', async ({
comfyPage
}) => {
const { bottomPanel } = comfyPage
await expect(bottomPanel.root).not.toBeVisible()
await expect(bottomPanel.resizeGutter).toBeHidden()
})
test('should resize panel by dragging the gutter', async ({ comfyPage }) => {
const { bottomPanel } = comfyPage
await bottomPanel.toggleButton.click()
await expect(bottomPanel.root).toBeVisible()
const initialHeight = await bottomPanel.root.evaluate(
(el) => el.getBoundingClientRect().height
)
const gutterBox = await bottomPanel.resizeGutter.boundingBox()
if (!gutterBox) {
test.skip()
return
}
const gutterCenterX = gutterBox.x + gutterBox.width / 2
const gutterCenterY = gutterBox.y + gutterBox.height / 2
// Drag gutter upward to enlarge the bottom panel
await comfyPage.page.mouse.move(gutterCenterX, gutterCenterY)
await comfyPage.page.mouse.down()
await comfyPage.page.mouse.move(gutterCenterX, gutterCenterY - 100, {
steps: 5
})
await comfyPage.page.mouse.up()
const newHeight = await bottomPanel.root.evaluate(
(el) => el.getBoundingClientRect().height
)
expect(newHeight).toBeGreaterThan(initialHeight)
})
test('should not block canvas interactions when panel is closed', async ({
comfyPage
}) => {
const { bottomPanel } = comfyPage
// Ensure panel is closed
await expect(bottomPanel.root).not.toBeVisible()
// Click on the canvas to interact with it (e.g. deselect all)
const canvasCenter = await comfyPage.canvas.boundingBox()
if (!canvasCenter) {
test.skip()
return
}
// Double-click canvas to open search box -- proves canvas receives events
await comfyPage.canvas.dblclick({
position: { x: canvasCenter.width / 2, y: canvasCenter.height / 2 }
})
await expect(comfyPage.searchBox.input).toBeVisible()
})
test('should switch from shortcuts to terminal panel', async ({
comfyPage
}) => {
const { bottomPanel } = comfyPage
// Open shortcuts panel first
await bottomPanel.keyboardShortcutsButton.click()
await expect(bottomPanel.root).toBeVisible()
await expect(bottomPanel.shortcuts.essentialsTab).toBeVisible()
// Click toggle button to switch to terminal panel
await bottomPanel.toggleButton.click()
const logsTab = comfyPage.page.getByRole('tab', { name: /Logs/i })
const hasTerminalTabs = await logsTab.isVisible().catch(() => false)
if (hasTerminalTabs) {
// Terminal panel is showing -- shortcuts tabs should not be visible
await expect(bottomPanel.shortcuts.essentialsTab).not.toBeVisible()
await expect(logsTab).toBeVisible()
}
})
test('should switch from terminal to shortcuts panel', async ({
comfyPage
}) => {
const { bottomPanel } = comfyPage
// Open terminal panel first
await bottomPanel.toggleButton.click()
await expect(bottomPanel.root).toBeVisible()
const logsTab = comfyPage.page.getByRole('tab', { name: /Logs/i })
const hasTerminalTabs = await logsTab.isVisible().catch(() => false)
if (!hasTerminalTabs) {
test.skip()
return
}
// Switch to shortcuts
await bottomPanel.keyboardShortcutsButton.click()
await expect(bottomPanel.shortcuts.essentialsTab).toBeVisible()
await expect(logsTab).not.toBeVisible()
})
test('should close panel via close button from shortcuts view', async ({
comfyPage
}) => {
const { bottomPanel } = comfyPage
await bottomPanel.keyboardShortcutsButton.click()
await expect(bottomPanel.root).toBeVisible()
await bottomPanel.closeButton.click()
await expect(bottomPanel.root).not.toBeVisible()
})
test('should display all registered terminal tabs', async ({ comfyPage }) => {
const { bottomPanel } = comfyPage
await bottomPanel.toggleButton.click()
await expect(bottomPanel.root).toBeVisible()
const logsTab = comfyPage.page.getByRole('tab', { name: /Logs/i })
const hasTerminalTabs = await logsTab.isVisible().catch(() => false)
if (!hasTerminalTabs) {
test.skip()
return
}
// At least the Logs tab should be present
await expect(logsTab).toBeVisible()
// The active tab should have aria-selected
const tabs = bottomPanel.root.getByRole('tab')
const tabCount = await tabs.count()
expect(tabCount).toBeGreaterThanOrEqual(1)
// Exactly one tab should be selected
const selectedTabs = bottomPanel.root.locator(
'[role="tab"][aria-selected="true"]'
)
await expect(selectedTabs).toHaveCount(1)
})
})

View File

@@ -0,0 +1,126 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
test.describe('Node library sidebar V2', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting('Comfy.UseNewMenu', 'Top')
await comfyPage.settings.setSetting('Comfy.NodeLibrary.NewDesign', true)
const tab = comfyPage.menu.nodeLibraryTabV2
await tab.open()
})
test('Can switch between tabs', async ({ comfyPage }) => {
const tab = comfyPage.menu.nodeLibraryTabV2
await expect(tab.allTab).toHaveAttribute('aria-selected', 'true')
await tab.blueprintsTab.click()
await expect(tab.blueprintsTab).toHaveAttribute('aria-selected', 'true')
await expect(tab.allTab).toHaveAttribute('aria-selected', 'false')
await tab.allTab.click()
await expect(tab.allTab).toHaveAttribute('aria-selected', 'true')
await expect(tab.blueprintsTab).toHaveAttribute('aria-selected', 'false')
})
test('All tab displays node tree with folders', async ({ comfyPage }) => {
const tab = comfyPage.menu.nodeLibraryTabV2
await expect(tab.allTab).toHaveAttribute('aria-selected', 'true')
await expect(tab.getFolder('sampling')).toBeVisible()
})
test('Can expand folder and see nodes in All tab', async ({ comfyPage }) => {
const tab = comfyPage.menu.nodeLibraryTabV2
await tab.expandFolder('sampling')
await expect(tab.getNode('KSampler (Advanced)')).toBeVisible()
})
test('Search filters nodes in All tab', async ({ comfyPage }) => {
const tab = comfyPage.menu.nodeLibraryTabV2
await expect(tab.getNode('KSampler (Advanced)')).not.toBeVisible()
await tab.searchInput.fill('KSampler')
await expect(tab.getNode('KSampler (Advanced)')).toBeVisible({
timeout: 5000
})
await expect(tab.getNode('CLIPLoader')).not.toBeVisible()
})
test('Drag node to canvas adds it', async ({ comfyPage }) => {
const tab = comfyPage.menu.nodeLibraryTabV2
await tab.expandFolder('sampling')
await expect(tab.getNode('KSampler (Advanced)')).toBeVisible()
const initialCount = await comfyPage.nodeOps.getGraphNodesCount()
const canvasBoundingBox = await comfyPage.page
.locator('#graph-canvas')
.boundingBox()
expect(canvasBoundingBox).not.toBeNull()
const targetPosition = {
x: canvasBoundingBox!.x + canvasBoundingBox!.width / 2,
y: canvasBoundingBox!.y + canvasBoundingBox!.height / 2
}
const nodeLocator = tab.getNode('KSampler (Advanced)')
await nodeLocator.dragTo(comfyPage.page.locator('#graph-canvas'), {
targetPosition
})
await expect
.poll(() => comfyPage.nodeOps.getGraphNodesCount(), { timeout: 5000 })
.toBe(initialCount + 1)
})
test('Right-click node shows context menu with bookmark option', async ({
comfyPage
}) => {
const tab = comfyPage.menu.nodeLibraryTabV2
await tab.expandFolder('sampling')
const node = tab.getNode('KSampler (Advanced)')
await expect(node).toBeVisible()
await node.click({ button: 'right' })
const contextMenu = comfyPage.page.getByRole('menuitem', {
name: /Bookmark Node/
})
await expect(contextMenu).toBeVisible({ timeout: 3000 })
})
test('Search clear restores folder view', async ({ comfyPage }) => {
const tab = comfyPage.menu.nodeLibraryTabV2
await expect(tab.getFolder('sampling')).toBeVisible()
await tab.searchInput.fill('KSampler')
await expect(tab.getNode('KSampler (Advanced)')).toBeVisible({
timeout: 5000
})
await tab.searchInput.clear()
await tab.searchInput.press('Enter')
await expect(tab.getFolder('sampling')).toBeVisible({ timeout: 5000 })
})
test('Sort dropdown shows sorting options', async ({ comfyPage }) => {
const tab = comfyPage.menu.nodeLibraryTabV2
await tab.sortButton.click()
// Reka UI DropdownMenuRadioItem renders with role="menuitemradio"
const options = comfyPage.page.getByRole('menuitemradio')
await expect(options.first()).toBeVisible({ timeout: 3000 })
await expect
.poll(() => options.count(), { timeout: 3000 })
.toBeGreaterThanOrEqual(2)
})
})