import { comfyExpect as expect, comfyPageFixture as test } from '@e2e/fixtures/ComfyPage' import { RootCategory } from '@/components/searchbox/v2/rootCategories' test.describe('Node search box V2 extended', { tag: '@node' }, () => { test.beforeEach(async ({ comfyPage }) => { await comfyPage.searchBoxV2.setup() }) test('Double-click on empty canvas opens search', async ({ comfyPage }) => { const { searchBoxV2 } = comfyPage await searchBoxV2.openByDoubleClickCanvas() await expect(searchBoxV2.dialog).toBeVisible() }) test('Escape closes search box without adding 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('Escape') await expect(searchBoxV2.input).toBeHidden() await expect .poll(() => comfyPage.nodeOps.getGraphNodesCount()) .toBe(initialCount) }) for (const closeKey of ['Enter', 'Escape'] as const) { test(`Reopening search after ${closeKey} has no persisted state`, async ({ comfyPage }) => { const { searchBoxV2 } = comfyPage await searchBoxV2.open() await searchBoxV2.input.fill('KSampler') await expect(searchBoxV2.results.first()).toBeVisible() await comfyPage.page.keyboard.press(closeKey) await expect(searchBoxV2.input).toBeHidden() await searchBoxV2.open() await expect(searchBoxV2.input).toHaveValue('') await expect(searchBoxV2.filterChips).toHaveCount(0) }) } test.describe('Category navigation', () => { test('Category navigation updates results', async ({ comfyPage }) => { const { searchBoxV2 } = comfyPage await searchBoxV2.open() await searchBoxV2.categoryButton('sampling').click() await expect(searchBoxV2.results.first()).toBeVisible() const samplingResults = await searchBoxV2.results.allTextContents() await searchBoxV2.categoryButton('loaders').click() await expect(searchBoxV2.results.first()).toBeVisible() await expect .poll(() => searchBoxV2.results.allTextContents()) .not.toEqual(samplingResults) }) }) test.describe('Filter workflow', () => { test('Filter chip removal restores results', async ({ comfyPage }) => { const { searchBoxV2 } = comfyPage await searchBoxV2.open() // Search first to keep the result set under the 64-item cap. await searchBoxV2.input.fill('Load') await expect(searchBoxV2.results.first()).toBeVisible() const unfilteredCount = await searchBoxV2.results.count() await test.step('Apply Input/MODEL filter', async () => { await searchBoxV2.applyTypeFilter('input', 'MODEL') await expect(searchBoxV2.filterChips).toHaveCount(1) await expect .poll(() => searchBoxV2.results.count()) .not.toBe(unfilteredCount) }) await test.step('Remove the filter chip', async () => { await searchBoxV2.removeFilterChip() await expect(searchBoxV2.filterChips).toHaveCount(0) await expect(searchBoxV2.results).toHaveCount(unfilteredCount) }) }) }) test.describe('Link release', () => { test('Link release opens search with pre-applied type filter', async ({ comfyPage }) => { const { searchBoxV2 } = comfyPage await comfyPage.canvasOps.disconnectEdge() await expect(searchBoxV2.input).toBeVisible() // disconnectEdge pulls a CLIP link → expect a single CLIP filter chip. await expect(searchBoxV2.filterChips).toHaveCount(1) await expect(searchBoxV2.filterChips.first()).toContainText('CLIP') }) test('Link release auto-connects added node', async ({ comfyPage }) => { const { searchBoxV2 } = comfyPage const NODE_TYPE = 'CLIPTextEncode' const refsBefore = await comfyPage.nodeOps.getNodeRefsByType(NODE_TYPE) const idsBefore = new Set(refsBefore.map((n) => n.id)) await comfyPage.canvasOps.disconnectEdge() await expect(searchBoxV2.input).toBeVisible() await searchBoxV2.input.fill('CLIP Text Encode') await expect(searchBoxV2.results.first()).toBeVisible() await comfyPage.page.keyboard.press('Enter') await expect(searchBoxV2.input).toBeHidden() // A new CLIPTextEncode node should have been added. await expect .poll(() => comfyPage.nodeOps .getNodeRefsByType(NODE_TYPE) .then((refs) => refs.length) ) .toBe(refsBefore.length + 1) // Verify the auto-connect: the newly-added node's CLIP input must be // connected (proves the release wasn't just dropped). const refsAfter = await comfyPage.nodeOps.getNodeRefsByType(NODE_TYPE) const newNode = refsAfter.find((n) => !idsBefore.has(n.id)) expect(newNode, 'expected a new CLIPTextEncode node').toBeDefined() const clipInput = await newNode!.getInput(0) await expect.poll(() => clipInput.getLinkCount()).toBe(1) }) }) test.describe('Filter combinations', () => { test('Output type filter filters results', async ({ comfyPage }) => { const { searchBoxV2 } = comfyPage await searchBoxV2.open() await searchBoxV2.input.fill('Load') await expect(searchBoxV2.results.first()).toBeVisible() const unfilteredCount = await searchBoxV2.results.count() await searchBoxV2.applyTypeFilter('output', 'IMAGE') await expect(searchBoxV2.filterChips).toHaveCount(1) await expect .poll(() => searchBoxV2.results.count()) .not.toBe(unfilteredCount) }) test('Multiple type filters (Input + Output) narrows results', async ({ comfyPage }) => { const { searchBoxV2 } = comfyPage await searchBoxV2.open() await searchBoxV2.applyTypeFilter('input', 'MODEL') await expect(searchBoxV2.filterChips).toHaveCount(1) await expect(searchBoxV2.results.first()).toBeVisible() const singleFilterCount = await searchBoxV2.results.count() await searchBoxV2.applyTypeFilter('output', 'LATENT') await expect(searchBoxV2.filterChips).toHaveCount(2) await expect .poll(() => searchBoxV2.results.count()) .toBeLessThan(singleFilterCount) }) test('Root filter + search query narrows results', async ({ comfyPage }) => { const { searchBoxV2 } = comfyPage await searchBoxV2.open() await searchBoxV2.input.fill('Sampler') await expect(searchBoxV2.results.first()).toBeVisible() const unfilteredCount = await searchBoxV2.results.count() await searchBoxV2.rootCategoryButton('comfy').click() await expect .poll(() => searchBoxV2.results.count()) .toBeLessThan(unfilteredCount) await expect.poll(() => searchBoxV2.results.count()).toBeGreaterThan(0) }) test('Root filter + category selection', async ({ comfyPage }) => { const { searchBoxV2 } = comfyPage await searchBoxV2.open() await searchBoxV2.rootCategoryButton('comfy').click() await expect(searchBoxV2.results.first()).toBeVisible() const comfyCount = await searchBoxV2.results.count() // Under root filter, categories are prefixed (e.g. comfy/sampling). await searchBoxV2.categoryButton('comfy/sampling').click() await expect .poll(() => searchBoxV2.results.count()) .toBeLessThan(comfyCount) }) }) test.describe('Category sidebar', () => { test('Category tree expand and collapse', async ({ comfyPage }) => { const { searchBoxV2 } = comfyPage await searchBoxV2.open() const samplingBtn = searchBoxV2.categoryButton('sampling') const subcategory = searchBoxV2.categoryButton('sampling/custom_sampling') await test.step('Expanding sampling reveals its subcategories', async () => { await samplingBtn.click() await expect(subcategory).toBeVisible() }) await test.step('Collapsing sampling hides its subcategories', async () => { await samplingBtn.click() await expect(subcategory).toBeHidden() }) }) test('Subcategory narrows results to subset', async ({ comfyPage }) => { const { searchBoxV2 } = comfyPage await searchBoxV2.open() await searchBoxV2.categoryButton('sampling').click() await expect(searchBoxV2.results.first()).toBeVisible() const parentCount = await searchBoxV2.results.count() const subcategory = searchBoxV2.categoryButton('sampling/custom_sampling') await expect(subcategory).toBeVisible() await subcategory.click() await expect .poll(() => searchBoxV2.results.count()) .toBeLessThan(parentCount) }) test('Most relevant resets category filter', async ({ comfyPage }) => { const { searchBoxV2 } = comfyPage await searchBoxV2.open() await expect(searchBoxV2.results.first()).toBeVisible() const defaultCount = await searchBoxV2.results.count() await searchBoxV2.categoryButton('sampling').click() await expect .poll(() => searchBoxV2.results.count()) .not.toBe(defaultCount) await searchBoxV2.categoryButton('most-relevant').click() await expect(searchBoxV2.results).toHaveCount(defaultCount) }) test( 'Blueprint root chip filters to published blueprints', { tag: ['@subgraph'] }, async ({ comfyPage }) => { const blueprintName = `chip-test-${crypto.randomUUID().slice(0, 8)}` const nodeRef = await comfyPage.nodeOps.getNodeRefById('3') await nodeRef.click('title') await comfyPage.command.executeCommand('Comfy.Graph.ConvertToSubgraph') await expect .poll(() => comfyPage.nodeOps .getNodeRefsByTitle('New Subgraph') .then((refs) => refs.length) ) .toBe(1) const subgraphNodes = await comfyPage.nodeOps.getNodeRefsByTitle('New Subgraph') await subgraphNodes[0].click('title') await comfyPage.command.executeCommand('Comfy.PublishSubgraph', { name: blueprintName }) await expect(comfyPage.visibleToasts).toHaveCount(1, { timeout: 5000 }) await comfyPage.toast.closeToasts(1) const { searchBoxV2 } = comfyPage await searchBoxV2.open() const blueprintsChip = searchBoxV2.rootCategoryButton( RootCategory.Blueprint ) await expect(blueprintsChip).toBeVisible() await blueprintsChip.click() // Blueprints persist across tests on the same worker; filter by the // unique name we just published rather than asserting the full list. await expect( searchBoxV2.results.filter({ hasText: blueprintName }) ).toHaveCount(1) } ) }) test.describe('Search behavior', () => { test('Search narrows results progressively', async ({ comfyPage }) => { const { searchBoxV2 } = comfyPage const getCount = () => searchBoxV2.results.count() await searchBoxV2.open() await searchBoxV2.input.fill('S') await expect(searchBoxV2.results.first()).toBeVisible() const count1 = await getCount() await searchBoxV2.input.fill('Sa') await expect.poll(getCount).toBeLessThan(count1) const count2 = await getCount() await searchBoxV2.input.fill('Sampler') await expect.poll(getCount).toBeLessThan(count2) }) test('No results shown for nonsensical query', async ({ comfyPage }) => { const { searchBoxV2 } = comfyPage await searchBoxV2.open() await searchBoxV2.input.fill('zzzxxxyyy_nonexistent_node') await expect(searchBoxV2.noResults).toBeVisible() await expect(searchBoxV2.results).toHaveCount(0) }) }) test.describe('Filter chip interaction', () => { test('Multiple filter chips displayed', async ({ comfyPage }) => { const { searchBoxV2 } = comfyPage await searchBoxV2.open() await searchBoxV2.applyTypeFilter('input', 'MODEL') await searchBoxV2.applyTypeFilter('output', 'LATENT') await expect(searchBoxV2.filterChips).toHaveCount(2) const chipTexts = await searchBoxV2.filterChips.allTextContents() expect(chipTexts.some((t) => t.includes('MODEL'))).toBe(true) expect(chipTexts.some((t) => t.includes('LATENT'))).toBe(true) }) }) test.describe('Settings-driven behavior', () => { test('Node ID name shown when setting enabled', async ({ comfyPage }) => { await comfyPage.settings.setSetting( 'Comfy.NodeSearchBoxImpl.ShowIdName', true ) const { searchBoxV2 } = comfyPage await searchBoxV2.open() await searchBoxV2.input.fill('VAE Decode') await expect(searchBoxV2.results.first()).toBeVisible() await expect(searchBoxV2.nodeIdBadge.first()).toBeVisible() await expect(searchBoxV2.nodeIdBadge.first()).toContainText('VAEDecode') }) test('Follow-cursor disabled places node without ghost mode', async ({ comfyPage }) => { await comfyPage.settings.setSetting( 'Comfy.NodeSearchBoxImpl.FollowCursor', false ) const { searchBoxV2 } = comfyPage const initialCount = await comfyPage.nodeOps.getGraphNodesCount() await searchBoxV2.open() await searchBoxV2.input.fill('KSampler') await expect(searchBoxV2.results.first()).toBeVisible() await searchBoxV2.results.first().click() await expect(searchBoxV2.input).toBeHidden() await expect .poll(() => comfyPage.nodeOps.getGraphNodesCount()) .toBe(initialCount + 1) await expect( comfyPage.page.locator('[data-node-id][data-ghost]') ).toHaveCount(0) }) }) })