import { comfyExpect as expect, comfyPageFixture as test } from '@e2e/fixtures/ComfyPage' test.describe('Node search box V2', { tag: '@node' }, () => { test.beforeEach(async ({ comfyPage }) => { await comfyPage.searchBoxV2.setup() }) test('Can open search and add node', async ({ comfyPage }) => { const { searchBoxV2 } = comfyPage const initialCount = await comfyPage.nodeOps.getGraphNodesCount() await searchBoxV2.open() await searchBoxV2.input.fill('KSampler') await expect(searchBoxV2.results.first()).toBeVisible() await comfyPage.page.keyboard.press('Enter') await expect(searchBoxV2.input).toBeHidden() await expect .poll(() => comfyPage.nodeOps.getGraphNodesCount()) .toBe(initialCount + 1) }) test('Can add first default result with Enter', async ({ comfyPage }) => { const { searchBoxV2 } = comfyPage const initialCount = await comfyPage.nodeOps.getGraphNodesCount() await searchBoxV2.open() // Default results should be visible without typing. await expect(searchBoxV2.results.first()).toBeVisible() await comfyPage.page.keyboard.press('Enter') await expect(searchBoxV2.input).toBeHidden() await expect .poll(() => comfyPage.nodeOps.getGraphNodesCount()) .toBe(initialCount + 1) }) test.describe('Category navigation', () => { test('Bookmarked filter shows only bookmarked nodes', async ({ comfyPage }) => { const { searchBoxV2 } = comfyPage await comfyPage.settings.setSetting('Comfy.NodeLibrary.Bookmarks.V2', [ 'KSampler' ]) await searchBoxV2.open() await searchBoxV2.rootCategoryButton('favorites').click() await expect(searchBoxV2.results).toHaveCount(1) await expect(searchBoxV2.results.first()).toContainText('KSampler') }) test('Category filters results to matching nodes', async ({ comfyPage }) => { const { searchBoxV2 } = comfyPage await searchBoxV2.open() await searchBoxV2.categoryButton('sampling').click() await expect(searchBoxV2.results.first()).toBeVisible() }) }) test.describe('Filter workflow', () => { test('Can filter by input type via filter bar', async ({ comfyPage }) => { const { searchBoxV2 } = comfyPage await searchBoxV2.open() await test.step('Open Input filter popover', async () => { await searchBoxV2.typeFilterButton('input').click() await expect(searchBoxV2.filterOptions.first()).toBeVisible() }) await test.step('Select MODEL type', async () => { await searchBoxV2.filterSearch.fill('MODEL') await searchBoxV2.filterOptions .filter({ hasText: 'MODEL' }) .first() .click() }) await expect(searchBoxV2.filterChips).toHaveCount(1) await expect(searchBoxV2.filterChips.first()).toContainText('MODEL') await expect(searchBoxV2.results.first()).toBeVisible() }) }) test.describe('Keyboard navigation', () => { test('Can navigate and select with keyboard', async ({ comfyPage }) => { const { searchBoxV2 } = comfyPage const initialCount = await comfyPage.nodeOps.getGraphNodesCount() await searchBoxV2.open() await searchBoxV2.input.fill('KSampler') const results = searchBoxV2.results await expect(results.first()).toBeVisible() await test.step('First result is selected by default', async () => { await expect(results.first()).toHaveAttribute('aria-selected', 'true') }) await test.step('ArrowDown moves selection to next result', async () => { await comfyPage.page.keyboard.press('ArrowDown') await expect(results.nth(1)).toHaveAttribute('aria-selected', 'true') await expect(results.first()).toHaveAttribute('aria-selected', 'false') }) await test.step('ArrowUp moves selection back', async () => { await comfyPage.page.keyboard.press('ArrowUp') await expect(results.first()).toHaveAttribute('aria-selected', 'true') }) await test.step('Enter selects and adds the node', async () => { await comfyPage.page.keyboard.press('Enter') await expect(searchBoxV2.input).toBeHidden() await expect .poll(() => comfyPage.nodeOps.getGraphNodesCount()) .toBe(initialCount + 1) }) }) }) test.describe('Category sidebar', () => { test('Sidebar toggle hides and shows the category sidebar', async ({ comfyPage }) => { const { searchBoxV2 } = comfyPage await searchBoxV2.open() const samplingCategory = searchBoxV2.categoryButton('sampling') await expect(samplingCategory).toBeVisible() await expect(searchBoxV2.sidebarToggle).toHaveAttribute( 'aria-expanded', 'true' ) await searchBoxV2.sidebarToggle.click() await expect(searchBoxV2.sidebarToggle).toHaveAttribute( 'aria-expanded', 'false' ) await expect(samplingCategory).toBeHidden() await searchBoxV2.sidebarToggle.click() await expect(searchBoxV2.sidebarToggle).toHaveAttribute( 'aria-expanded', 'true' ) await expect(samplingCategory).toBeVisible() }) test('Filter bar scrolls horizontally while the sidebar toggle stays pinned', async ({ comfyPage }) => { const { searchBoxV2 } = comfyPage // Narrow viewport so the chips overflow the filter bar await comfyPage.page.setViewportSize({ width: 360, height: 800 }) await searchBoxV2.open() const scrollEl = searchBoxV2.filterChipsScroll const dims = await scrollEl.evaluate((el) => ({ scrollWidth: el.scrollWidth, clientWidth: el.clientWidth })) expect(dims.scrollWidth).toBeGreaterThan(dims.clientWidth) await scrollEl.evaluate((el) => { el.scrollLeft = el.scrollWidth }) // The toggle lives outside the scroll container, so even when the // chips scroll hundreds of px it must remain visible in the viewport. await expect(searchBoxV2.sidebarToggle).toBeInViewport() }) test('@mobile Sidebar is collapsed by default on mobile', async ({ comfyPage }) => { const { searchBoxV2 } = comfyPage await searchBoxV2.open() await expect(searchBoxV2.sidebarToggle).toHaveAttribute( 'aria-expanded', 'false' ) await expect(searchBoxV2.categoryButton('sampling')).toBeHidden() }) test('@mobile Clicking outside the sidebar closes it', async ({ comfyPage }) => { const { searchBoxV2 } = comfyPage await searchBoxV2.open() await searchBoxV2.sidebarToggle.click() await expect(searchBoxV2.sidebarToggle).toHaveAttribute( 'aria-expanded', 'true' ) await expect(searchBoxV2.categoryButton('sampling')).toBeVisible() await expect(searchBoxV2.sidebarBackdrop).toBeVisible() // The backdrop spans the full content area, but the sidebar (z-20) // covers its left ~208px (w-52). Click past that to land on the // backdrop rather than the sidebar. await searchBoxV2.sidebarBackdrop.click({ position: { x: 240, y: 40 } }) await expect(searchBoxV2.sidebarToggle).toHaveAttribute( 'aria-expanded', 'false' ) await expect(searchBoxV2.categoryButton('sampling')).toBeHidden() await expect(searchBoxV2.sidebarBackdrop).toBeHidden() }) test('@mobile Focusing the search input closes the sidebar', async ({ comfyPage }) => { const { searchBoxV2 } = comfyPage await searchBoxV2.open() await searchBoxV2.sidebarToggle.click() await expect(searchBoxV2.sidebarToggle).toHaveAttribute( 'aria-expanded', 'true' ) await searchBoxV2.input.focus() await expect(searchBoxV2.sidebarToggle).toHaveAttribute( 'aria-expanded', 'false' ) }) test('Sidebar state across mobile/desktop resizes', async ({ comfyPage }) => { const { searchBoxV2 } = comfyPage const switchToDesktop = () => comfyPage.page.setViewportSize({ width: 1280, height: 800 }) const switchToMobile = () => comfyPage.page.setViewportSize({ width: 360, height: 800 }) const expectExpanded = (value: 'true' | 'false') => expect(searchBoxV2.sidebarToggle).toHaveAttribute( 'aria-expanded', value ) await switchToDesktop() await searchBoxV2.open() await expectExpanded('true') await switchToMobile() await expectExpanded('false') await searchBoxV2.sidebarToggle.click() await switchToDesktop() await expectExpanded('true') await searchBoxV2.sidebarToggle.click() await switchToMobile() await expectExpanded('false') await switchToDesktop() await expectExpanded('false') }) }) })