mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-11 16:30:57 +00:00
## Summary Adds setting to disable the node auto-follow cursor behavior when adding nodes from the search, and increased the visibilty of Vue ghost nodes. ## Changes - **What**: - add setting - increase opacity - add test ## Review Focus <!-- Critical design decisions or edge cases that need attention --> <!-- If this PR fixes an issue, uncomment and update the line below --> <!-- Fixes #ISSUE_NUMBER --> ## Screenshots (if applicable) Before <img width="452" height="517" alt="image" src="https://github.com/user-attachments/assets/369c0d90-5352-482b-a1b3-36180bffb3ee" /> After <img width="440" height="536" alt="image" src="https://github.com/user-attachments/assets/2066fdd4-6eb4-4bfb-ac7c-559fc99de57d" /> ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-11365-feat-Search-add-ghost-node-following-setting-and-increase-opacity-3466d73d3650811b9c27ed4cc930816d) by [Unito](https://www.unito.io)
401 lines
14 KiB
TypeScript
401 lines
14 KiB
TypeScript
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)
|
|
})
|
|
})
|
|
})
|