refactor: migrate consumers from PrimeVue Badge and StatusBadge to Badge

Replace PrimeVue Badge in TreeExplorerTreeNode, UsageLogsTable, and
SearchFilterChip. Replace StatusBadge with Badge in all consumer
components. Replace PrimeVue Chip with native HTML in SearchFilterChip.
Update customerEventsService severity values to match Badge variants.
Add E2E visual regression tests for badge rendering.
This commit is contained in:
dante01yoon
2026-03-27 08:47:59 +09:00
parent a4773708fe
commit 8d6a1cb251
19 changed files with 150 additions and 108 deletions

View File

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

View File

@@ -0,0 +1,86 @@
import { expect } from '@playwright/test'
import type { Page } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe(
'Badge visual regression',
{ tag: ['@screenshot', '@ui'] },
() => {
async function dismissToasts(comfyPage: { page: Page }) {
const toastCloseButtons = comfyPage.page.locator('.p-toast-close-button')
const count = await toastCloseButtons.count()
for (let i = 0; i < count; i++) {
await toastCloseButtons
.nth(i)
.click()
.catch(() => {})
}
if (count > 0) {
await comfyPage.page
.locator('.p-toast-message')
.first()
.waitFor({ state: 'hidden', timeout: 3000 })
.catch(() => {})
}
}
test.describe('SearchFilterChip badge', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting(
'Comfy.NodeSearchBoxImpl',
'v1 (legacy)'
)
await dismissToasts(comfyPage)
})
test('Single filter chip renders correctly', async ({ comfyPage }) => {
await comfyPage.canvasOps.doubleClick()
await comfyPage.searchBox.addFilter('CONDITIONING', 'Input Type')
const searchContainer = comfyPage.page.locator(
'.comfy-vue-node-search-container'
)
await expect(searchContainer).toHaveScreenshot(
'filter-chip-conditioning.png'
)
})
test('Multiple filter chips render correctly', async ({ comfyPage }) => {
await comfyPage.canvasOps.doubleClick()
await comfyPage.searchBox.addFilter('MODEL', 'Input Type')
await comfyPage.searchBox.addFilter('CLIP', 'Output Type')
const searchContainer = comfyPage.page.locator(
'.comfy-vue-node-search-container'
)
await expect(searchContainer).toHaveScreenshot(
'filter-chips-multiple.png'
)
})
})
test.describe('Node library tree badge', () => {
test('Folder node count badge renders correctly', async ({
comfyPage
}) => {
await dismissToasts(comfyPage)
const sidebarButton = comfyPage.page.getByRole('button', {
name: 'Node Library'
})
await sidebarButton.click()
const sidebar = comfyPage.page.getByRole('complementary', {
name: 'Sidebar'
})
await sidebar
.getByRole('treeitem')
.first()
.waitFor({ state: 'visible', timeout: 10000 })
await expect(sidebar).toHaveScreenshot('node-library-tree-badges.png')
})
})
}
)

View File

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

View File

@@ -44,7 +44,7 @@ const renderActionbar = (showRunProgressBar: boolean) => {
name: 'Panel',
template: '<div><slot /></div>'
},
StatusBadge: true,
Badge: true,
ComfyRunButton: {
name: 'ComfyRunButton',
template: '<button type="button">Run</button>'

View File

@@ -59,7 +59,7 @@
<span class="text-sm font-normal tabular-nums">
{{ activeJobsLabel }}
</span>
<StatusBadge
<Badge
v-if="activeJobsCount > 0"
data-testid="active-jobs-indicator"
variant="dot"
@@ -104,7 +104,7 @@ import { computed, nextTick, ref, watch } from 'vue'
import type { ComponentPublicInstance } from 'vue'
import { useI18n } from 'vue-i18n'
import StatusBadge from '@/components/common/StatusBadge.vue'
import Badge from '@/components/common/Badge.vue'
import QueueInlineProgress from '@/components/queue/QueueInlineProgress.vue'
import Button from '@/components/ui/button/Button.vue'
import { useQueueFeatureFlags } from '@/composables/queue/useQueueFeatureFlags'

View File

@@ -1,17 +1,28 @@
<template>
<Chip removable @remove="emit('remove', $event)">
<Badge size="small" :class="semanticBadgeClass">
{{ badge }}
</Badge>
<span
class="inline-flex items-center gap-1 rounded-2xl bg-surface-700 py-0.5 pr-1 pl-2 text-xs"
>
<Badge :label="badge" :class="semanticBadgeClass" />
{{ text }}
</Chip>
<button
type="button"
:aria-label="$t('g.remove')"
class="inline-flex cursor-pointer items-center justify-center rounded-full p-0.5 hover:bg-surface-600"
@click="emit('remove', $event)"
>
<i
class="icon-[lucide--x] size-3 text-muted-foreground"
aria-hidden="true"
/>
</button>
</span>
</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'
export interface SearchFilter {
text: string
badge: string

View File

@@ -1,36 +0,0 @@
<script setup lang="ts">
import { computed } from 'vue'
import { cn } from '@/utils/tailwindUtil'
import { statusBadgeVariants } from './statusBadge.variants'
import type { StatusBadgeVariants } from './statusBadge.variants'
const {
label,
severity = 'default',
variant,
class: className
} = defineProps<{
label?: string | number
severity?: StatusBadgeVariants['severity']
variant?: StatusBadgeVariants['variant']
class?: string
}>()
const badgeClass = computed(() =>
cn(
statusBadgeVariants({
severity,
variant: variant ?? (label == null ? 'dot' : 'label')
}),
className
)
)
</script>
<template>
<span :class="badgeClass">
{{ label }}
</span>
</template>

View File

@@ -1,6 +1,6 @@
import { createTestingPinia } from '@pinia/testing'
import { fireEvent, render, screen } from '@testing-library/vue'
import Badge from 'primevue/badge'
import Badge from '@/components/common/Badge.vue'
import PrimeVue from 'primevue/config'
import InputText from 'primevue/inputtext'
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'

View File

@@ -23,9 +23,10 @@
</span>
<Badge
v-if="showNodeBadgeText"
:value="nodeBadgeText"
:label="nodeBadgeText"
severity="secondary"
class="leaf-count-badge"
:variant="nodeBadgeText.length > 1 ? 'label' : 'circle'"
class="ml-2"
/>
</div>
<div
@@ -38,7 +39,7 @@
<script setup lang="ts" generic="T">
import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview'
import Badge from 'primevue/badge'
import Badge from '@/components/common/Badge.vue'
import { computed, inject, ref } from 'vue'
import EditableText from '@/components/common/EditableText.vue'
@@ -146,9 +147,6 @@ if (props.node.droppable) {
align-items: center;
justify-content: space-between;
}
.leaf-count-badge {
margin-left: 0.5rem;
}
.node-content {
display: flex;
align-items: center;

View File

@@ -1,26 +0,0 @@
import type { VariantProps } from 'cva'
import { cva } from 'cva'
export const statusBadgeVariants = cva({
base: 'inline-flex items-center justify-center rounded-full',
variants: {
severity: {
default: 'bg-primary-background text-base-foreground',
secondary: 'bg-secondary-background text-base-foreground',
warn: 'bg-warning-background text-base-background',
danger: 'bg-destructive-background text-white',
contrast: 'bg-base-foreground text-base-background'
},
variant: {
label: 'h-3.5 px-1 text-3xs font-semibold uppercase',
dot: 'size-2',
circle: 'size-3.5 text-3xs font-semibold'
}
},
defaultVariants: {
severity: 'default',
variant: 'label'
}
})
export type StatusBadgeVariants = VariantProps<typeof statusBadgeVariants>

View File

@@ -1,6 +1,6 @@
import { createTestingPinia } from '@pinia/testing'
import { mount } from '@vue/test-utils'
import Badge from 'primevue/badge'
import Badge from '@/components/common/Badge.vue'
import Button from '@/components/ui/button/Button.vue'
import Column from 'primevue/column'
import PrimeVue from 'primevue/config'
@@ -125,13 +125,13 @@ describe('UsageLogsTable', () => {
mockCustomerEventsService.getEventSeverity.mockImplementation((type) => {
switch (type) {
case EventType.CREDIT_ADDED:
return 'success'
return 'default'
case EventType.ACCOUNT_CREATED:
return 'info'
return 'secondary'
case EventType.API_USAGE_COMPLETED:
return 'warning'
return 'warn'
default:
return 'info'
return 'secondary'
}
})
mockCustomerEventsService.formatAmount.mockImplementation((amount) => {

View File

@@ -20,7 +20,7 @@
<Column field="event_type" :header="$t('credits.eventType')">
<template #body="{ data }">
<Badge
:value="customerEventService.formatEventType(data.event_type)"
:label="customerEventService.formatEventType(data.event_type)"
:severity="customerEventService.getEventSeverity(data.event_type)"
/>
</template>
@@ -91,7 +91,7 @@
</template>
<script setup lang="ts">
import Badge from 'primevue/badge'
import Badge from '@/components/common/Badge.vue'
import Column from 'primevue/column'
import DataTable from 'primevue/datatable'
import Message from 'primevue/message'

View File

@@ -3,7 +3,7 @@ import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import Loader from '@/components/loader/Loader.vue'
import StatusBadge from '@/components/common/StatusBadge.vue'
import Badge from '@/components/common/Badge.vue'
import type { AssetDownload } from '@/stores/assetDownloadStore'
import { cn } from '@/utils/tailwindUtil'
@@ -40,11 +40,11 @@ const isPending = computed(() => job.status === 'created')
<i
class="icon-[lucide--circle-alert] size-4 text-destructive-background"
/>
<StatusBadge :label="t('progressToast.failed')" severity="danger" />
<Badge :label="t('progressToast.failed')" severity="danger" />
</template>
<template v-else-if="isCompleted">
<StatusBadge :label="t('progressToast.finished')" severity="contrast" />
<Badge :label="t('progressToast.finished')" severity="contrast" />
</template>
<template v-else-if="isRunning">

View File

@@ -20,7 +20,7 @@
<span ref="textRef" class="min-w-0 truncate">
<slot />
</span>
<StatusBadge
<Badge
v-if="badge !== undefined"
:label="String(badge)"
severity="contrast"
@@ -33,7 +33,7 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import StatusBadge from '@/components/common/StatusBadge.vue'
import Badge from '@/components/common/Badge.vue'
import type { NavItemData } from '@/types/navTypes'
import NavIcon from './NavIcon.vue'

View File

@@ -119,9 +119,10 @@
@click.stop="handleSelect"
>
{{ $t('g.use') }}
<StatusBadge
<Badge
v-if="isNewlyImported"
severity="contrast"
variant="dot"
class="absolute -top-0.5 -right-0.5"
/>
</Button>
@@ -137,7 +138,7 @@ import { useI18n } from 'vue-i18n'
import IconGroup from '@/components/button/IconGroup.vue'
import MoreButton from '@/components/button/MoreButton.vue'
import StatusBadge from '@/components/common/StatusBadge.vue'
import Badge from '@/components/common/Badge.vue'
import { showConfirmDialog } from '@/components/dialog/confirm/confirmDialog'
import Button from '@/components/ui/button/Button.vue'
import AssetBadgeGroup from '@/platform/assets/components/AssetBadgeGroup.vue'

View File

@@ -28,7 +28,7 @@
"
@click="$emit('stepClick', step.name)"
>
<StatusBadge
<Badge
:label="step.number"
variant="circle"
severity="contrast"
@@ -67,7 +67,7 @@ import { computed } from 'vue'
import { vAutoAnimate } from '@formkit/auto-animate/vue'
import StatusBadge from '@/components/common/StatusBadge.vue'
import Badge from '@/components/common/Badge.vue'
import Button from '@/components/ui/button/Button.vue'
import type { ComfyHubPublishStep } from '@/platform/workflow/sharing/composables/useComfyHubPublishWizard'
import { cn } from '@/utils/tailwindUtil'

View File

@@ -80,7 +80,7 @@
<span class="text-sm font-bold text-text-primary">
{{ subscriptionTierName }}
</span>
<StatusBadge
<Badge
v-if="isCancelled"
:label="$t('subscription.canceled')"
severity="warn"
@@ -365,7 +365,7 @@ import { useI18n } from 'vue-i18n'
import { useToast } from 'primevue/usetoast'
import StatusBadge from '@/components/common/StatusBadge.vue'
import Badge from '@/components/common/Badge.vue'
import Button from '@/components/ui/button/Button.vue'
import { useBillingContext } from '@/composables/billing/useBillingContext'
import { useBillingOperationStore } from '@/platform/workspace/stores/billingOperationStore'

View File

@@ -216,15 +216,17 @@ describe('useCustomerEventsService', () => {
describe('getEventSeverity', () => {
it('should return correct severity for known event types', () => {
expect(service.getEventSeverity(EventType.CREDIT_ADDED)).toBe('success')
expect(service.getEventSeverity(EventType.ACCOUNT_CREATED)).toBe('info')
expect(service.getEventSeverity(EventType.CREDIT_ADDED)).toBe('default')
expect(service.getEventSeverity(EventType.ACCOUNT_CREATED)).toBe(
'secondary'
)
expect(service.getEventSeverity(EventType.API_USAGE_COMPLETED)).toBe(
'warning'
'warn'
)
})
it('should return default severity for unknown event types', () => {
expect(service.getEventSeverity('unknown_event')).toBe('info')
expect(service.getEventSeverity('unknown_event')).toBe('secondary')
})
})

View File

@@ -2,6 +2,7 @@ import type { AxiosError, AxiosResponse } from 'axios'
import axios from 'axios'
import { ref, watch } from 'vue'
import type { BadgeVariants } from '@/components/common/badge.variants'
import { getComfyApiBaseUrl } from '@/config/comfyApi'
import { d } from '@/i18n'
import { useAuthStore } from '@/stores/authStore'
@@ -134,16 +135,18 @@ export const useCustomerEventsService = () => {
return value
}
function getEventSeverity(eventType: string) {
function getEventSeverity(
eventType: string
): NonNullable<BadgeVariants['severity']> {
switch (eventType) {
case 'credit_added':
return 'success'
return 'default'
case 'account_created':
return 'info'
return 'secondary'
case 'api_usage_completed':
return 'warning'
return 'warn'
default:
return 'info'
return 'secondary'
}
}