mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-04 13:12:10 +00:00
test: migrate 11 interactive component tests from VTU to VTL (Phase 2) (#10490)
## Summary Phase 2 of the VTL migration: migrate 11 interactive component tests from @vue/test-utils to @testing-library/vue (69 tests). Stacked on #10471. ## Changes - **What**: Migrate BatchCountEdit, BypassButton, BuilderFooterToolbar, ComfyActionbar, SidebarIcon, EditableText, UrlInput, SearchInput, TagsInput, TreeExplorerTreeNode, ColorCustomizationSelector from VTU to VTL - **Pattern transforms**: `trigger('click')` → `userEvent.click()`, `setValue()` → `userEvent.type()`, `findComponent().props()` → `getByRole/getByText/getByTestId`, `emitted()` → callback props - **Removed**: 4 `@ts-expect-error` annotations, 1 change-detector test (SearchInput `vm.focus`) - **PrimeVue**: `data-pc-name` selectors + `aria-pressed` for SelectButton, container queries for ColorPicker/InputIcon ## Review Focus - PrimeVue escape hatches in ColorCustomizationSelector (SelectButton/ColorPicker lack standard ARIA roles) - Teleport test in ComfyActionbar uses `container.querySelector` intentionally (scoped to teleport target) - SearchInput debounce tests use `fireEvent.update` instead of `userEvent.type` due to fake timer interaction - EditableText escape-then-blur test simplified: `userEvent.keyboard('{Escape}')` already triggers blur internally ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-10490-test-migrate-11-interactive-component-tests-from-VTU-to-VTL-Phase-2-32e6d73d3650817ca40fd61395499e3f) by [Unito](https://www.unito.io) --------- Co-authored-by: Amp <amp@ampcode.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
@@ -1,64 +1,66 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { fireEvent, render, screen } from '@testing-library/vue'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import PrimeVue from 'primevue/config'
|
||||
import IconField from 'primevue/iconfield'
|
||||
import InputIcon from 'primevue/inputicon'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { createApp, nextTick } from 'vue'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import UrlInput from './UrlInput.vue'
|
||||
import type { ComponentProps } from 'vue-component-type-helpers'
|
||||
|
||||
describe('UrlInput', () => {
|
||||
beforeEach(() => {
|
||||
const app = createApp({})
|
||||
app.use(PrimeVue)
|
||||
})
|
||||
|
||||
const mountComponent = (
|
||||
function renderComponent(
|
||||
props: ComponentProps<typeof UrlInput> & {
|
||||
placeholder?: string
|
||||
disabled?: boolean
|
||||
},
|
||||
options = {}
|
||||
) => {
|
||||
return mount(UrlInput, {
|
||||
'onUpdate:modelValue'?: (value: string) => void
|
||||
}
|
||||
) {
|
||||
const user = userEvent.setup()
|
||||
|
||||
const result = render(UrlInput, {
|
||||
global: {
|
||||
plugins: [PrimeVue],
|
||||
components: { IconField, InputIcon, InputText }
|
||||
},
|
||||
props,
|
||||
...options
|
||||
props
|
||||
})
|
||||
|
||||
return { ...result, user }
|
||||
}
|
||||
|
||||
it('passes through additional attributes to input element', () => {
|
||||
const wrapper = mountComponent({
|
||||
renderComponent({
|
||||
modelValue: '',
|
||||
placeholder: 'Enter URL',
|
||||
disabled: true
|
||||
})
|
||||
|
||||
expect(wrapper.find('input').attributes('disabled')).toBe('')
|
||||
expect(screen.getByRole('textbox')).toBeDisabled()
|
||||
})
|
||||
|
||||
it('emits update:modelValue on blur', async () => {
|
||||
const wrapper = mountComponent({
|
||||
const onUpdate = vi.fn()
|
||||
const { user } = renderComponent({
|
||||
modelValue: '',
|
||||
placeholder: 'Enter URL'
|
||||
placeholder: 'Enter URL',
|
||||
'onUpdate:modelValue': onUpdate
|
||||
})
|
||||
|
||||
const input = wrapper.find('input')
|
||||
await input.setValue('https://test.com/')
|
||||
await input.trigger('blur')
|
||||
const input = screen.getByRole('textbox')
|
||||
await user.type(input, 'https://test.com/')
|
||||
expect(onUpdate).not.toHaveBeenCalled()
|
||||
|
||||
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([
|
||||
'https://test.com/'
|
||||
])
|
||||
await user.tab()
|
||||
|
||||
expect(onUpdate).toHaveBeenCalledTimes(1)
|
||||
expect(onUpdate).toHaveBeenCalledWith('https://test.com/')
|
||||
})
|
||||
|
||||
it('renders spinner when validation is loading', async () => {
|
||||
const wrapper = mountComponent({
|
||||
const { container, rerender } = renderComponent({
|
||||
modelValue: '',
|
||||
placeholder: 'Enter URL',
|
||||
validateUrlFn: () =>
|
||||
@@ -67,43 +69,46 @@ describe('UrlInput', () => {
|
||||
})
|
||||
})
|
||||
|
||||
await wrapper.setProps({ modelValue: 'https://test.com' })
|
||||
await rerender({ modelValue: 'https://test.com' })
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.find('.pi-spinner').exists()).toBe(true)
|
||||
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access -- PrimeVue InputIcon uses pi-spinner class with no ARIA role
|
||||
expect(container.querySelector('.pi-spinner')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('renders check icon when validation is valid', async () => {
|
||||
const wrapper = mountComponent({
|
||||
const { container, rerender } = renderComponent({
|
||||
modelValue: '',
|
||||
placeholder: 'Enter URL',
|
||||
validateUrlFn: () => Promise.resolve(true)
|
||||
})
|
||||
|
||||
await wrapper.setProps({ modelValue: 'https://test.com' })
|
||||
await rerender({ modelValue: 'https://test.com' })
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.find('.pi-check').exists()).toBe(true)
|
||||
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access -- PrimeVue InputIcon uses pi-check class with no ARIA role
|
||||
expect(container.querySelector('.pi-check')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('renders cross icon when validation is invalid', async () => {
|
||||
const wrapper = mountComponent({
|
||||
const { container, rerender } = renderComponent({
|
||||
modelValue: '',
|
||||
placeholder: 'Enter URL',
|
||||
validateUrlFn: () => Promise.resolve(false)
|
||||
})
|
||||
|
||||
await wrapper.setProps({ modelValue: 'https://test.com' })
|
||||
await rerender({ modelValue: 'https://test.com' })
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.find('.pi-times').exists()).toBe(true)
|
||||
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access -- PrimeVue InputIcon uses pi-times class with no ARIA role
|
||||
expect(container.querySelector('.pi-times')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('validates on mount', async () => {
|
||||
const wrapper = mountComponent({
|
||||
const { container } = renderComponent({
|
||||
modelValue: 'https://test.com',
|
||||
validateUrlFn: () => Promise.resolve(true)
|
||||
})
|
||||
@@ -111,12 +116,13 @@ describe('UrlInput', () => {
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
expect(wrapper.find('.pi-check').exists()).toBe(true)
|
||||
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access -- PrimeVue InputIcon uses pi-check class with no ARIA role
|
||||
expect(container.querySelector('.pi-check')).not.toBeNull()
|
||||
})
|
||||
|
||||
it('triggers validation when clicking the validation icon', async () => {
|
||||
let validationCount = 0
|
||||
const wrapper = mountComponent({
|
||||
const { container, user } = renderComponent({
|
||||
modelValue: 'https://test.com',
|
||||
validateUrlFn: () => {
|
||||
validationCount++
|
||||
@@ -129,7 +135,9 @@ describe('UrlInput', () => {
|
||||
await nextTick()
|
||||
|
||||
// Click the validation icon
|
||||
await wrapper.find('.pi-check').trigger('click')
|
||||
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access -- PrimeVue InputIcon uses pi-check class with no ARIA role
|
||||
const icon = container.querySelector('.pi-check')!
|
||||
await user.click(icon)
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
@@ -138,7 +146,7 @@ describe('UrlInput', () => {
|
||||
|
||||
it('prevents multiple simultaneous validations', async () => {
|
||||
let validationCount = 0
|
||||
const wrapper = mountComponent({
|
||||
const { container, rerender, user } = renderComponent({
|
||||
modelValue: '',
|
||||
validateUrlFn: () => {
|
||||
validationCount++
|
||||
@@ -148,14 +156,16 @@ describe('UrlInput', () => {
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.setProps({ modelValue: 'https://test.com' })
|
||||
await rerender({ modelValue: 'https://test.com' })
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
// Trigger multiple validations in quick succession
|
||||
await wrapper.find('.pi-spinner').trigger('click')
|
||||
await wrapper.find('.pi-spinner').trigger('click')
|
||||
await wrapper.find('.pi-spinner').trigger('click')
|
||||
// eslint-disable-next-line testing-library/no-container, testing-library/no-node-access -- PrimeVue InputIcon
|
||||
const spinner = container.querySelector('.pi-spinner')!
|
||||
await user.click(spinner)
|
||||
await user.click(spinner)
|
||||
await user.click(spinner)
|
||||
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
@@ -165,55 +175,49 @@ describe('UrlInput', () => {
|
||||
|
||||
describe('input cleaning functionality', () => {
|
||||
it('trims whitespace when user types', async () => {
|
||||
const wrapper = mountComponent({
|
||||
renderComponent({
|
||||
modelValue: '',
|
||||
placeholder: 'Enter URL'
|
||||
})
|
||||
|
||||
const input = wrapper.find('input')
|
||||
const input = screen.getByRole('textbox')
|
||||
|
||||
// Test leading whitespace
|
||||
await input.setValue(' https://leading-space.com')
|
||||
await input.trigger('input')
|
||||
// The component strips whitespace on input via handleInput
|
||||
// We use fireEvent.input to simulate the input event handler directly
|
||||
await fireEvent.update(input, ' https://leading-space.com')
|
||||
await nextTick()
|
||||
expect(input.element.value).toBe('https://leading-space.com')
|
||||
expect(input).toHaveValue('https://leading-space.com')
|
||||
|
||||
// Test trailing whitespace
|
||||
await input.setValue('https://trailing-space.com ')
|
||||
await input.trigger('input')
|
||||
await fireEvent.update(input, 'https://trailing-space.com ')
|
||||
await nextTick()
|
||||
expect(input.element.value).toBe('https://trailing-space.com')
|
||||
expect(input).toHaveValue('https://trailing-space.com')
|
||||
|
||||
// Test both leading and trailing whitespace
|
||||
await input.setValue(' https://both-spaces.com ')
|
||||
await input.trigger('input')
|
||||
await fireEvent.update(input, ' https://both-spaces.com ')
|
||||
await nextTick()
|
||||
expect(input.element.value).toBe('https://both-spaces.com')
|
||||
expect(input).toHaveValue('https://both-spaces.com')
|
||||
|
||||
// Test whitespace in the middle of the URL
|
||||
await input.setValue('https:// middle-space.com')
|
||||
await input.trigger('input')
|
||||
await fireEvent.update(input, 'https:// middle-space.com')
|
||||
await nextTick()
|
||||
expect(input.element.value).toBe('https://middle-space.com')
|
||||
expect(input).toHaveValue('https://middle-space.com')
|
||||
})
|
||||
|
||||
it('trims whitespace when value set externally', async () => {
|
||||
const wrapper = mountComponent({
|
||||
const { rerender } = renderComponent({
|
||||
modelValue: ' https://initial-value.com ',
|
||||
placeholder: 'Enter URL'
|
||||
})
|
||||
|
||||
const input = wrapper.find('input')
|
||||
const input = screen.getByRole('textbox')
|
||||
|
||||
// Check initial value is trimmed
|
||||
expect(input.element.value).toBe('https://initial-value.com')
|
||||
expect(input).toHaveValue('https://initial-value.com')
|
||||
|
||||
// Update props with whitespace
|
||||
await wrapper.setProps({ modelValue: ' https://updated-value.com ' })
|
||||
await rerender({ modelValue: ' https://updated-value.com ' })
|
||||
await nextTick()
|
||||
|
||||
// Check updated value is trimmed
|
||||
expect(input.element.value).toBe('https://updated-value.com')
|
||||
expect(input).toHaveValue('https://updated-value.com')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user