Files
ComfyUI_frontend/src/components/topbar/TopbarBadge.vue
Alexander Brown 08b1199265 test: migrate 13 component tests from VTU to VTL (Phase 1) (#10471)
## Summary

Migrate 13 component test files from @vue/test-utils to
@testing-library/vue as Phase 1 of incremental VTL adoption.

## Changes

- **What**: Rewrite 13 test files (88 tests) to use `render`/`screen`
queries, `userEvent` interactions, and `jest-dom` assertions. Add
`data-testid` attributes to 6 components for lint-clean icon/element
queries. Delete unused `src/utils/test-utils.ts`.
- **Dependencies**: `@testing-library/vue`,
`@testing-library/user-event`, `@testing-library/jest-dom` (installed in
Phase 0)

## Review Focus

- `data-testid` additions to component templates are minimal and
non-behavioral
- PrimeVue passthrough (`pt`) usage in UserAvatar.vue for icon testid
- 2 targeted `eslint-disable` in FormRadioGroup.test.ts where PrimeVue
places `aria-describedby` on wrapper div, not input

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10471-test-migrate-13-component-tests-from-VTU-to-VTL-Phase-1-32d6d73d36508159a33ffa285afb4c38)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-03-26 18:15:11 -07:00

231 lines
5.5 KiB
Vue

<template>
<!-- Icon-only mode with Popover -->
<div
v-if="displayMode === 'icon-only'"
class="relative inline-flex h-full shrink-0 items-center justify-center px-2"
:class="clickableClasses"
:style="menuBackgroundStyle"
@click="togglePopover"
>
<i
v-if="iconClass"
data-testid="badge-icon"
:class="['shrink-0 text-base', iconClass, iconColorClass]"
/>
<div
v-else-if="badge.label"
class="shrink-0 rounded-full px-1.5 py-0.5 text-xxxs font-semibold"
:class="labelClasses"
>
{{ badge.label }}
</div>
<div v-else class="size-2 shrink-0 rounded-full" :class="dotClasses" />
<Popover
ref="popover"
append-to="body"
:auto-z-index="true"
:base-z-index="1000"
:dismissable="true"
:close-on-escape="true"
unstyled
:pt="popoverPt"
>
<div class="flex max-w-xs min-w-40 flex-col gap-2 p-3">
<div
v-if="badge.label"
class="w-fit rounded-full px-1.5 py-0.5 text-xxxs font-semibold"
:class="labelClasses"
>
{{ badge.label }}
</div>
<div class="font-inter text-sm">{{ badge.text }}</div>
<div v-if="badge.tooltip" class="text-xs">
{{ badge.tooltip }}
</div>
</div>
</Popover>
</div>
<!-- Compact mode: Icon + Label only with Popover -->
<div
v-else-if="displayMode === 'compact'"
class="relative inline-flex h-full"
:style="menuBackgroundStyle"
>
<div
class="flex h-full shrink-0 items-center gap-2 whitespace-nowrap"
:class="[
{ 'flex-row-reverse': reverseOrder },
noPadding ? '' : 'px-3',
clickableClasses
]"
@click="togglePopover"
>
<i
v-if="iconClass"
data-testid="badge-icon"
:class="['shrink-0 text-base', iconClass, iconColorClass]"
/>
<div
v-if="badge.label"
class="shrink-0 rounded-full px-1.5 py-0.5 text-xxxs font-semibold"
:class="labelClasses"
>
{{ badge.label }}
</div>
</div>
<Popover
ref="popover"
append-to="body"
:auto-z-index="true"
:base-z-index="1000"
:dismissable="true"
:close-on-escape="true"
unstyled
:pt="popoverPt"
>
<div class="flex max-w-xs min-w-40 flex-col gap-2 p-3">
<div
v-if="badge.label"
class="w-fit rounded-full px-1.5 py-0.5 text-xxxs font-semibold"
:class="labelClasses"
>
{{ badge.label }}
</div>
<div class="font-inter text-sm">{{ badge.text }}</div>
<div v-if="badge.tooltip" class="text-xs">
{{ badge.tooltip }}
</div>
</div>
</Popover>
</div>
<!-- Full mode: Icon + Label + Text -->
<div
v-else
v-tooltip="badge.tooltip"
class="flex h-full shrink-0 items-center gap-2 whitespace-nowrap"
:class="[{ 'flex-row-reverse': reverseOrder }, noPadding ? '' : 'px-3']"
:style="menuBackgroundStyle"
>
<i
v-if="iconClass"
data-testid="badge-icon"
:class="['shrink-0 text-base', iconClass, iconColorClass]"
/>
<div
v-if="badge.label"
class="shrink-0 rounded-full px-1.5 py-0.5 text-xxxs font-semibold"
:class="labelClasses"
>
{{ badge.label }}
</div>
<div class="font-inter text-sm" :class="textClasses">
{{ badge.text }}
</div>
</div>
</template>
<script setup lang="ts">
import Popover from 'primevue/popover'
import { computed, ref } from 'vue'
import type { TopbarBadge } from '@/types/comfy'
import { cn } from '@/utils/tailwindUtil'
const {
badge,
displayMode = 'full',
reverseOrder,
noPadding,
backgroundColor = 'var(--comfy-menu-bg)'
} = defineProps<{
badge: TopbarBadge
displayMode?: 'full' | 'compact' | 'icon-only'
reverseOrder?: boolean
noPadding?: boolean
backgroundColor?: string
}>()
const popover = ref<InstanceType<typeof Popover>>()
const togglePopover = (event: Event) => {
popover.value?.toggle(event)
}
const variant = computed(() => badge.variant ?? 'info')
const menuBackgroundStyle = computed(() => ({
backgroundColor: backgroundColor
}))
const labelClasses = computed(() => {
switch (variant.value) {
case 'error':
return 'bg-danger-100 text-white'
case 'warning':
return 'bg-gold-600 text-black'
case 'info':
default:
return 'bg-white text-black'
}
})
const textClasses = computed(() => {
switch (variant.value) {
case 'error':
return 'text-danger-100'
case 'warning':
return 'text-warning-background'
case 'info':
default:
return 'text-text-primary'
}
})
const iconColorClass = computed(() => textClasses.value)
const iconClass = computed(() => {
if (badge.icon) {
return badge.icon
}
switch (variant.value) {
case 'error':
return 'pi pi-exclamation-circle'
case 'warning':
return 'icon-[lucide--triangle-alert]'
case 'info':
default:
return undefined
}
})
const clickableClasses = 'cursor-pointer transition-opacity hover:opacity-80'
const dotClasses = computed(() => {
switch (variant.value) {
case 'error':
return 'bg-danger-100'
case 'warning':
return 'bg-gold-600'
case 'info':
default:
return 'bg-slate-100'
}
})
const popoverPt = computed(() => ({
root: {
class: cn('absolute z-50')
},
content: {
class: cn(
'mt-1 rounded-lg',
'bg-base-background',
'text-base-foreground',
'shadow-lg',
'border border-border-default'
)
}
}))
</script>