Compare commits

...

14 Commits

Author SHA1 Message Date
Dante
674a11d789 Merge branch 'main' into feature/migrate-primevue-chip 2026-05-11 09:02:07 +09:00
dante01yoon
fce534add3 Merge remote-tracking branch 'origin/main' into HEAD 2026-05-04 16:05:31 +09:00
dante01yoon
d4636ff17d test: address tag migration review feedback 2026-05-04 16:01:14 +09:00
Dante
f5a669ff0d Merge branch 'main' into feature/migrate-primevue-chip 2026-03-29 16:32:57 +09:00
dante01yoon
2266827248 test: add unit tests for SearchFilterChip and DownloadItem
SearchFilterChip (5 tests): badge rendering, semantic class
mapping, remove event, fallback class.
DownloadItem (4 tests): cancelled/error tag display, remove
button, in-progress state, file path label.
2026-03-28 18:53:42 +09:00
dante01yoon
614f482c18 refactor: replace PrimeVue Chip with custom Tag component
- SearchFilterChip: PrimeVue Chip+Badge → Tag (removable) + Badge
- NodeSearchItem: PrimeVue Chip → Tag (aliased as ChipTag to avoid
  name collision with PrimeVue Tag, which migrates in a follow-up PR)
- DownloadItem: PrimeVue Chip (removable) → Tag (removable)
- Update E2E selectors from .p-chip-remove-icon to accessible
  getByRole('button', { name: 'Remove' })
2026-03-28 18:45:31 +09:00
dante01yoon
e4286aabf3 test: add golden screenshot for template overlay tags 2026-03-28 16:49:50 +09:00
dante01yoon
ed14722fe2 fix: use TestIds selectors and fix remaining story label mismatch 2026-03-28 16:36:53 +09:00
dante01yoon
69e35c00f3 test: add E2E screenshot test for template card overlay tags 2026-03-28 16:28:50 +09:00
dante01yoon
7721a78087 fix: apply overlay shape to template dialog tags 2026-03-28 16:22:01 +09:00
dante01yoon
ec19b49cc3 fix: stop click propagation on remove button and fix story labels 2026-03-28 16:20:40 +09:00
dante01yoon
0d3f272e6a feat: add overlay shape variant for tags on images
Add overlay shape (bg-zinc-500/40, text-white/90) for tags that
sit on top of image thumbnails. Used in SampleModelSelector.
Pending Figma design system confirmation.
2026-03-28 16:12:54 +09:00
dante01yoon
5319441b24 fix: align Tag variants with Figma design system states
Replace custom overlay variant with Figma-defined states:
- default: full opacity, foreground text
- unselected: opacity-70, muted-foreground text
- selected: full opacity with remove button (via removable prop)
Hover state handled via CSS :hover, not a prop.
2026-03-28 16:06:36 +09:00
dante01yoon
921226f79f feat: add Tag component from design system and rename SquareChip
Add Tag component with CVA variants matching Figma design system:
- square (rounded-sm) and rounded (pill) shapes
- removable state with X close button
- icon slot support

Rename SquareChip to Tag across all consumers and stories.
Includes unit tests (5 tests) covering rendering, removable
behavior, and icon slot.
2026-03-28 15:49:52 +09:00
8 changed files with 217 additions and 22 deletions

View File

@@ -81,7 +81,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()
}
/**

View File

@@ -127,7 +127,7 @@ test.describe('Node search box', { tag: '@node' }, () => {
const initialNodeCount = await comfyPage.nodeOps.getGraphNodesCount()
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
})

View File

@@ -0,0 +1,63 @@
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.each([
['input type', 'I', 'i-badge', 'bg-green-500'],
['output type', 'O', 'o-badge', 'bg-red-500'],
['combo type', 'C', 'c-badge', 'bg-blue-500'],
['seed type', 'S', 's-badge', 'bg-yellow-500']
])(
'applies semantic badge class for %s',
(_, badgeText, badgeClass, color) => {
renderChip({ text: 'CLIP', badge: badgeText, badgeClass })
const badge = screen.getByText(badgeText)
expect(badge.className).toContain(color)
}
)
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')
})
})

View File

@@ -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

View File

@@ -37,21 +37,20 @@
: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 Tag from 'primevue/tag'
import { computed } from 'vue'
import ChipTag from '@/components/chip/Tag.vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
import type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'

View File

@@ -0,0 +1,118 @@
import { render, screen } from '@testing-library/vue'
import userEvent from '@testing-library/user-event'
import { createTestingPinia } from '@pinia/testing'
import { describe, expect, it } from 'vitest'
import { createI18n } from 'vue-i18n'
import { DownloadStatus } from '@comfyorg/comfyui-electron-types'
import { useElectronDownloadStore } from '@/stores/electronDownloadStore'
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',
error: 'Error'
}
}
}
})
function createDownload(
status: DownloadStatus,
url = 'http://example.com/model.bin'
): ElectronDownload {
return {
url,
filename: 'model.bin',
savePath: '/models/checkpoints/model.bin',
status
}
}
function renderDownloadItem(
download: ElectronDownload,
initialDownloads: ElectronDownload[] = []
) {
const pinia = createTestingPinia({
initialState: {
downloads: { downloads: initialDownloads }
}
})
const view = render(DownloadItem, {
props: { download },
global: {
plugins: [pinia, i18n],
stubs: {
ProgressBar: true
}
}
})
return {
...view,
electronDownloadStore: useElectronDownloadStore()
}
}
describe('DownloadItem', () => {
it('shows cancelled tag with remove button for cancelled downloads', () => {
renderDownloadItem(createDownload(DownloadStatus.CANCELLED))
expect(screen.getByText('Cancelled')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Remove' })).toBeInTheDocument()
})
it('removes cancelled downloads from the store', async () => {
const user = userEvent.setup()
const cancelledDownload = createDownload(DownloadStatus.CANCELLED)
const pausedDownload = createDownload(
DownloadStatus.PAUSED,
'http://example.com/other-model.bin'
)
const { electronDownloadStore } = renderDownloadItem(cancelledDownload, [
cancelledDownload,
pausedDownload
])
await user.click(screen.getByRole('button', { name: 'Remove' }))
expect(electronDownloadStore.downloads).toEqual([pausedDownload])
})
it('shows error tag for error downloads', () => {
renderDownloadItem(createDownload(DownloadStatus.ERROR))
expect(screen.getByText('Error')).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()
expect(screen.queryByText('Error')).not.toBeInTheDocument()
})
it('displays file path label', () => {
renderDownloadItem(createDownload(DownloadStatus.CANCELLED))
expect(screen.getByText('checkpoints/model.bin')).toBeInTheDocument()
})
})

View File

@@ -4,13 +4,18 @@
{{ 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(
download.status === 'error'
? 'electronFileDownload.error'
: 'electronFileDownload.cancelled'
)
"
class="mt-2 bg-red-700 text-sm font-light"
removable
@remove="handleRemoveDownload"
>
{{ t('electronFileDownload.cancelled') }}
</Chip>
/>
</div>
<div
v-if="
@@ -67,10 +72,10 @@
</template>
<script setup lang="ts">
import Chip from 'primevue/chip'
import ProgressBar from 'primevue/progressbar'
import { useI18n } from 'vue-i18n'
import Tag from '@/components/chip/Tag.vue'
import Button from '@/components/ui/button/Button.vue'
import { useElectronDownloadStore } from '@/stores/electronDownloadStore'
import type { ElectronDownload } from '@/stores/electronDownloadStore'

View File

@@ -1200,7 +1200,8 @@
"paused": "Paused",
"resume": "Resume Download",
"cancel": "Cancel Download",
"cancelled": "Cancelled"
"cancelled": "Cancelled",
"error": "Error"
},
"maskEditor": {
"title": "Mask Editor",