Compare commits

...

2 Commits

Author SHA1 Message Date
Matt Miller
b0df1a5456 test: cover parameter tab search headers
Add render tests for TabNodes, TabNormalInputs, and TabGlobalParameters
verifying each composes the extracted PanelSearchHeader (and slots its
collapse toggle where present), mirroring TabSubgraphInputs. Restores
patch coverage on the newly-touched template lines.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 13:29:15 -07:00
Matt Miller
49d4f252b3 refactor: extract PanelSearchHeader for parameter tabs 2026-07-02 12:57:29 -07:00
9 changed files with 235 additions and 42 deletions

View File

@@ -0,0 +1,50 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/vue'
import { h } from 'vue'
import { createI18n } from 'vue-i18n'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import PanelSearchHeader from './PanelSearchHeader.vue'
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: { en: { g: { searchPlaceholder: 'Search', clear: 'Clear' } } }
})
function renderHeader(searcher: (query: string) => Promise<void>) {
return render(PanelSearchHeader, {
props: { searcher, updateKey: [], modelValue: '' },
slots: { default: () => h('button', { 'data-testid': 'slot' }, 'toggle') },
global: { plugins: [i18n] }
})
}
describe('PanelSearchHeader', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('forwards the searcher to the embedded search input', async () => {
const searcher = vi.fn().mockResolvedValue(undefined)
renderHeader(searcher)
await waitFor(() =>
expect(searcher).toHaveBeenCalledWith('', expect.any(Function))
)
})
it('renders default slot content alongside the search input', () => {
renderHeader(vi.fn().mockResolvedValue(undefined))
expect(screen.getByTestId('slot')).toBeTruthy()
expect(screen.getByRole('textbox')).toBeTruthy()
})
it('emits query updates from the search input through v-model', async () => {
const { emitted } = renderHeader(vi.fn().mockResolvedValue(undefined))
await fireEvent.update(screen.getByRole('textbox'), 'seed')
expect(emitted()['update:modelValue']).toContainEqual(['seed'])
})
})

View File

@@ -0,0 +1,28 @@
<script setup lang="ts">
import type { ComponentProps } from 'vue-component-type-helpers'
import AsyncSearchInput from '@/components/ui/search-input/AsyncSearchInput.vue'
type AsyncSearchInputProps = ComponentProps<typeof AsyncSearchInput>
const { searcher, updateKey } = defineProps<{
searcher?: AsyncSearchInputProps['searcher']
updateKey?: AsyncSearchInputProps['updateKey']
}>()
const query = defineModel<string>({ default: '' })
</script>
<template>
<div
class="flex items-center border-b border-interface-stroke px-4 pt-1 pb-4"
>
<AsyncSearchInput
v-model="query"
:searcher
:update-key="updateKey"
class="flex-1"
/>
<slot />
</div>
</template>

View File

@@ -0,0 +1,39 @@
import { render, screen } from '@testing-library/vue'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it } from 'vitest'
import { createI18n } from 'vue-i18n'
import TabGlobalParameters from './TabGlobalParameters.vue'
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: {
en: {
g: { searchPlaceholder: 'Search', clear: 'Clear' },
rightSidePanel: { favorites: 'Favorites', favoritesNone: 'No favorites' }
}
}
})
function renderTab() {
return render(TabGlobalParameters, {
global: {
plugins: [i18n],
stubs: { SectionWidgets: true }
}
})
}
describe('TabGlobalParameters', () => {
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }))
})
it('renders the shared search header', () => {
renderTab()
expect(screen.getByRole('textbox')).toBeTruthy()
})
})

View File

@@ -3,12 +3,12 @@ import { storeToRefs } from 'pinia'
import { computed, ref, shallowRef } from 'vue'
import { useI18n } from 'vue-i18n'
import AsyncSearchInput from '@/components/ui/search-input/AsyncSearchInput.vue'
import { useFavoritedWidgetsStore } from '@/stores/workspace/favoritedWidgetsStore'
import type { ValidFavoritedWidget } from '@/stores/workspace/favoritedWidgetsStore'
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
import { searchWidgets } from '../shared'
import PanelSearchHeader from './PanelSearchHeader.vue'
import SectionWidgets from './SectionWidgets.vue'
const favoritedWidgetsStore = useFavoritedWidgetsStore()
@@ -55,16 +55,11 @@ function handleReorder({
</script>
<template>
<div
class="flex items-center border-b border-interface-stroke px-4 pt-1 pb-4"
>
<AsyncSearchInput
v-model="searchQuery"
:searcher
:update-key="favoritedWidgets"
class="flex-1"
/>
</div>
<PanelSearchHeader
v-model="searchQuery"
:searcher
:update-key="favoritedWidgets"
/>
<SectionWidgets
:label
:widgets="searchedFavoritedWidgets"

View File

@@ -0,0 +1,48 @@
import { render, screen } from '@testing-library/vue'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it } from 'vitest'
import { createI18n } from 'vue-i18n'
import TabNodes from './TabNodes.vue'
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: {
en: {
g: { searchPlaceholder: 'Search', clear: 'Clear' },
rightSidePanel: { noneSearchDesc: 'None', inputsNoneTooltip: '' }
}
}
})
const CollapseToggleButtonStub = {
props: ['modelValue', 'show'],
template: '<button data-testid="collapse-toggle" />'
}
function renderTab() {
return render(TabNodes, {
global: {
plugins: [i18n],
stubs: {
SectionWidgets: true,
CollapseToggleButton: CollapseToggleButtonStub
}
}
})
}
describe('TabNodes', () => {
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }))
})
it('composes the shared search header with the collapse toggle in its slot', () => {
renderTab()
expect(screen.getByRole('textbox')).toBeTruthy()
expect(screen.getByTestId('collapse-toggle')).toBeTruthy()
})
})

View File

@@ -7,12 +7,12 @@ import CollapseToggleButton from '@/components/rightSidePanel/layout/CollapseTog
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import AsyncSearchInput from '@/components/ui/search-input/AsyncSearchInput.vue'
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
import type { NodeId } from '@/types/nodeId'
import { computedSectionDataList, searchWidgetsAndNodes } from '../shared'
import type { NodeWidgetsListList } from '../shared'
import PanelSearchHeader from './PanelSearchHeader.vue'
import SectionWidgets from './SectionWidgets.vue'
const canvasStore = useCanvasStore()
@@ -76,20 +76,16 @@ async function searcher(query: string) {
</script>
<template>
<div
class="flex items-center border-b border-interface-stroke px-4 pt-1 pb-4"
<PanelSearchHeader
v-model="searchQuery"
:searcher
:update-key="widgetsSectionDataList"
>
<AsyncSearchInput
v-model="searchQuery"
:searcher
:update-key="widgetsSectionDataList"
class="flex-1"
/>
<CollapseToggleButton
v-model="isAllCollapsed"
:show="!isSearching && widgetsSectionDataList.length > 1"
/>
</div>
</PanelSearchHeader>
<TransitionGroup tag="div" name="list-scale" class="relative">
<div
v-if="isSearching && searchedWidgetsSectionDataList.length === 0"

View File

@@ -0,0 +1,49 @@
import { render, screen } from '@testing-library/vue'
import { createTestingPinia } from '@pinia/testing'
import { setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it } from 'vitest'
import { createI18n } from 'vue-i18n'
import TabNormalInputs from './TabNormalInputs.vue'
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: {
en: {
g: { searchPlaceholder: 'Search', clear: 'Clear' },
rightSidePanel: { advanced: 'Advanced', inputsNone: 'None' }
}
}
})
const CollapseToggleButtonStub = {
props: ['modelValue', 'show'],
template: '<button data-testid="collapse-toggle" />'
}
function renderTab() {
return render(TabNormalInputs, {
props: { nodes: [] },
global: {
plugins: [i18n],
stubs: {
SectionWidgets: true,
CollapseToggleButton: CollapseToggleButtonStub
}
}
})
}
describe('TabNormalInputs', () => {
beforeEach(() => {
setActivePinia(createTestingPinia({ stubActions: false }))
})
it('composes the shared search header with the collapse toggle in its slot', () => {
renderTab()
expect(screen.getByRole('textbox')).toBeTruthy()
expect(screen.getByTestId('collapse-toggle')).toBeTruthy()
})
})

View File

@@ -5,13 +5,13 @@ import { useI18n } from 'vue-i18n'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import CollapseToggleButton from '@/components/rightSidePanel/layout/CollapseToggleButton.vue'
import AsyncSearchInput from '@/components/ui/search-input/AsyncSearchInput.vue'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
import type { NodeId } from '@/types/nodeId'
import { computedSectionDataList, searchWidgetsAndNodes } from '../shared'
import type { NodeWidgetsListList } from '../shared'
import PanelSearchHeader from './PanelSearchHeader.vue'
import SectionWidgets from './SectionWidgets.vue'
const { nodes, mustShowNodeTitle } = defineProps<{
@@ -124,15 +124,11 @@ const advancedLabel = computed(() => {
</script>
<template>
<div
class="flex items-center border-b border-interface-stroke px-4 pt-1 pb-4"
<PanelSearchHeader
v-model="searchQuery"
:searcher
:update-key="widgetsSectionDataList"
>
<AsyncSearchInput
v-model="searchQuery"
:searcher
:update-key="widgetsSectionDataList"
class="flex-1"
/>
<CollapseToggleButton
v-model="isAllCollapsed"
:show="
@@ -141,7 +137,7 @@ const advancedLabel = computed(() => {
1
"
/>
</div>
</PanelSearchHeader>
<TransitionGroup tag="div" name="list-scale" class="relative">
<div
v-if="searchedWidgetsSectionDataList.length === 0"

View File

@@ -11,12 +11,12 @@ import {
} from '@/core/graph/subgraph/promotionUtils'
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import AsyncSearchInput from '@/components/ui/search-input/AsyncSearchInput.vue'
import CollapseToggleButton from '@/components/rightSidePanel/layout/CollapseToggleButton.vue'
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
import { searchWidgets } from '../shared'
import type { NodeWidgetsList } from '../shared'
import PanelSearchHeader from './PanelSearchHeader.vue'
import SectionWidgets from './SectionWidgets.vue'
const { node } = defineProps<{
@@ -122,20 +122,12 @@ const label = computed(() => {
</script>
<template>
<div
class="flex items-center border-b border-interface-stroke px-4 pt-1 pb-4"
>
<AsyncSearchInput
v-model="searchQuery"
:searcher
:update-key="widgetsList"
class="flex-1"
/>
<PanelSearchHeader v-model="searchQuery" :searcher :update-key="widgetsList">
<CollapseToggleButton
v-model="isAllCollapsed"
:show="!isSearching && advancedInputsWidgets.length > 0"
/>
</div>
</PanelSearchHeader>
<SectionWidgets
:collapse="firstSectionCollapsed && !isSearching"
:node