mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-22 05:19:03 +00:00
Enables eslint/func-style in oxlint with declaration mode to enforce function declarations over function expressions and arrow expressions assigned to variables. Vendored litegraph is excluded via override. Converts existing function expressions and variable-initialized arrow functions to function declarations across src/, browser_tests/, apps/, packages/, and scripts/. Adjusts a handful of let-reassignable callback placeholders, narrowed variable patterns, and typed widget constructors to keep type safety intact. Pre-existing type-aware oxlint errors (no-console, no-floating-promises, no-explicit-any) are unchanged from main.
403 lines
14 KiB
TypeScript
403 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
|
|
function getCount() {
|
|
return 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)
|
|
})
|
|
})
|
|
})
|