Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion browser_tests/fixtures/components/ComfyNodeSearchBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,10 @@ export class ComfyNodeSearchBox {
}

async removeFilter(index: number) {
await this.filterChips.nth(index).locator('.p-chip-remove-icon').click()
await this.filterChips
.nth(index)
.getByRole('button', { name: 'Remove' })
.click()
}

/**
Expand Down
2 changes: 1 addition & 1 deletion browser_tests/tests/nodeSearchBox.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ test.describe('Node search box', { tag: '@node' }, () => {
async ({ comfyPage }) => {
await comfyPage.canvasOps.disconnectEdge()
await expect(comfyPage.searchBox.input).toHaveCount(1)
await comfyPage.page.locator('.p-chip-remove-icon').click()
await comfyPage.searchBox.removeFilter(0)
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler', {
exact: true
})
Expand Down
61 changes: 61 additions & 0 deletions src/components/common/SearchFilterChip.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { render, screen } from '@testing-library/vue'
import userEvent from '@testing-library/user-event'
import { describe, expect, it, vi } from 'vitest'
import { createI18n } from 'vue-i18n'

import SearchFilterChip from './SearchFilterChip.vue'

const i18n = createI18n({
legacy: false,
locale: 'en',
messages: { en: { g: { remove: 'Remove' } } }
})

function renderChip(
props: { text: string; badge: string; badgeClass: string },
onRemove?: (...args: unknown[]) => void
) {
return render(SearchFilterChip, {
props: { ...props, ...(onRemove ? { onRemove } : {}) },
global: { plugins: [i18n] }
})
}

describe('SearchFilterChip', () => {
it('renders badge and text', () => {
renderChip({ text: 'MODEL', badge: 'I', badgeClass: 'i-badge' })
expect(screen.getByText('MODEL')).toBeInTheDocument()
expect(screen.getByText('I')).toBeInTheDocument()
})

it('applies semantic badge class for input type', () => {
renderChip({ text: 'CLIP', badge: 'I', badgeClass: 'i-badge' })
const badge = screen.getByText('I')
expect(badge.className).toContain('bg-green-500')
})
Comment on lines +31 to +35
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid asserting Tailwind utility classes in component tests.

These assertions couple tests to implementation styling (className) instead of user-visible behavior, which makes them brittle during visual refactors.

As per coding guidelines: "**/*.{test,spec}.ts: Do not write tests dependent on non-behavioral features like utility classes or styles."

Also applies to: 37-41, 52-60

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/common/SearchFilterChip.test.ts` around lines 31 - 35, The
test currently asserts Tailwind utility classes
(expect(badge.className).toContain('bg-green-500')) which is brittle; update the
tests that use renderChip (the "applies semantic badge class for input type"
test and the others flagged) to assert semantic behavior instead — e.g., check
the badge element exists, its text content/accessible name is 'I' and that it
has the semantic prop/class passed in (badgeClass like 'i-badge') or use
toHaveAttribute/toHaveClass with only non-utility class names, rather than
checking for Tailwind utility classes; locate the checks in the tests
referencing renderChip and replace utility-class assertions with these semantic
assertions.


it('applies semantic badge class for output type', () => {
renderChip({ text: 'IMAGE', badge: 'O', badgeClass: 'o-badge' })
const badge = screen.getByText('O')
expect(badge.className).toContain('bg-red-500')
})

it('shows remove button and emits remove on click', async () => {
const user = userEvent.setup()
const onRemove = vi.fn()
renderChip({ text: 'MODEL', badge: 'I', badgeClass: 'i-badge' }, onRemove)

await user.click(screen.getByRole('button', { name: 'Remove' }))
expect(onRemove).toHaveBeenCalledOnce()
})

it('falls back to raw badgeClass when no semantic mapping', () => {
renderChip({
text: 'CUSTOM',
badge: 'X',
badgeClass: 'custom-class'
})
const badge = screen.getByText('X')
expect(badge.className).toContain('custom-class')
})
})
22 changes: 14 additions & 8 deletions src/components/common/SearchFilterChip.vue
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
<template>
<Chip removable @remove="emit('remove', $event)">
<Badge size="small" :class="semanticBadgeClass">
{{ badge }}
</Badge>
{{ text }}
</Chip>
<Tag
:label="text"
shape="rounded"
removable
class="bg-surface-700"
@remove="emit('remove', $event)"
>
<template #icon>
<Badge :label="badge" :class="semanticBadgeClass" />
</template>
</Tag>
</template>

<script setup lang="ts">
import Badge from 'primevue/badge'
import Chip from 'primevue/chip'
import { computed } from 'vue'

import Badge from '@/components/common/Badge.vue'
import Tag from '@/components/chip/Tag.vue'

export interface SearchFilter {
text: string
badge: string
Expand Down
9 changes: 4 additions & 5 deletions src/components/searchbox/NodeSearchItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,17 @@
:value="formatNumberWithSuffix(nodeFrequency, { roundToInt: true })"
severity="secondary"
/>
<Chip
<ChipTag
v-if="nodeDef.nodeSource.type !== NodeSourceType.Unknown"
:label="nodeDef.nodeSource.displayText"
class="text-sm font-light"
>
{{ nodeDef.nodeSource.displayText }}
</Chip>
/>
</div>
</div>
</template>

<script setup lang="ts">
import Chip from 'primevue/chip'
import ChipTag from '@/components/chip/Tag.vue'
import Tag from 'primevue/tag'
import { computed } from 'vue'

Expand Down
86 changes: 86 additions & 0 deletions src/components/sidebar/tabs/modelLibrary/DownloadItem.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { render, screen } from '@testing-library/vue'
import { createTestingPinia } from '@pinia/testing'
import { describe, expect, it } from 'vitest'
import { createI18n } from 'vue-i18n'

import { DownloadStatus } from '@comfyorg/comfyui-electron-types'

import type { ElectronDownload } from '@/stores/electronDownloadStore'

import DownloadItem from './DownloadItem.vue'

const i18n = createI18n({
legacy: false,
locale: 'en',
messages: {
en: {
g: { remove: 'Remove' },
electronFileDownload: {
cancelled: 'Cancelled',
pause: 'Pause',
resume: 'Resume',
cancel: 'Cancel'
}
}
}
})

function renderDownloadItem(download: ElectronDownload) {
return render(DownloadItem, {
props: { download },
global: {
plugins: [createTestingPinia(), i18n],
stubs: {
ProgressBar: true
}
}
})
}

describe('DownloadItem', () => {
it('shows cancelled tag with remove button for cancelled downloads', () => {
renderDownloadItem({
url: 'http://example.com/model.bin',
filename: 'model.bin',
savePath: '/models/checkpoints/model.bin',
status: DownloadStatus.CANCELLED
})

expect(screen.getByText('Cancelled')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Remove' })).toBeInTheDocument()
})

it('shows cancelled tag for error downloads', () => {
renderDownloadItem({
url: 'http://example.com/model.bin',
filename: 'model.bin',
savePath: '/models/checkpoints/model.bin',
status: DownloadStatus.ERROR
})

expect(screen.getByText('Cancelled')).toBeInTheDocument()
})

it('does not show cancelled tag for in-progress downloads', () => {
renderDownloadItem({
url: 'http://example.com/model.bin',
filename: 'model.bin',
savePath: '/models/checkpoints/model.bin',
status: DownloadStatus.IN_PROGRESS,
progress: 0.5
})

expect(screen.queryByText('Cancelled')).not.toBeInTheDocument()
})

it('displays file path label', () => {
renderDownloadItem({
url: 'http://example.com/model.bin',
filename: 'model.bin',
savePath: '/models/checkpoints/model.bin',
status: DownloadStatus.CANCELLED
})

expect(screen.getByText('checkpoints/model.bin')).toBeInTheDocument()
})
})
12 changes: 6 additions & 6 deletions src/components/sidebar/tabs/modelLibrary/DownloadItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@
{{ getDownloadLabel(download.savePath ?? '') }}
</div>
<div v-if="['cancelled', 'error'].includes(download.status ?? '')">
<Chip
class="mt-2 h-6 bg-red-700 text-sm font-light"
<Tag
:label="t('electronFileDownload.cancelled')"
class="mt-2 bg-red-700 text-sm font-light"
Comment on lines 6 to +9
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use a status-specific label for error state.

Line 6 includes error, but Line 8 always renders the cancelled label. That can mislead users when a download failed for a non-cancel reason.

💡 Proposed fix
-      <Tag
-        :label="t('electronFileDownload.cancelled')"
+      <Tag
+        :label="
+          t(
+            download.status === 'error'
+              ? 'electronFileDownload.error'
+              : 'electronFileDownload.cancelled'
+          )
+        "
         class="mt-2 bg-red-700 text-sm font-light"
         removable
         `@remove`="handleRemoveDownload"
       />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/sidebar/tabs/modelLibrary/DownloadItem.vue` around lines 6 -
9, The template currently treats both 'cancelled' and 'error' statuses the same
and always renders the cancelled label; update the conditional rendering around
the Tag (where download.status is checked) to use a status-specific label:
render t('electronFileDownload.cancelled') when download.status === 'cancelled'
and render t('electronFileDownload.error') (or the appropriate error translation
key) when download.status === 'error', keeping the same Tag component and
styling logic so users see the correct label for each status.

removable
@remove="handleRemoveDownload"
>
{{ t('electronFileDownload.cancelled') }}
</Chip>
/>
</div>
<div
v-if="
Expand Down Expand Up @@ -67,8 +66,9 @@
</template>

<script setup lang="ts">
import Chip from 'primevue/chip'
import ProgressBar from 'primevue/progressbar'

import Tag from '@/components/chip/Tag.vue'
import { useI18n } from 'vue-i18n'

import Button from '@/components/ui/button/Button.vue'
Expand Down
Loading