mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 18:52:19 +00:00
## 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>
231 lines
5.5 KiB
Vue
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>
|