Files
element-plus/packages/components/input/__tests__/input.test.tsx
btea ea926472df feat(components): [input] textarea supports clearing effects (#23723)
* feat(components): [input] textarea supports clearing effects

* Update packages/components/input/__tests__/input.test.tsx

Co-authored-by: rzzf <cszhjh@gmail.com>

* feat: update

* Apply suggestions from code review

Co-authored-by: Noblet Ouways <91417411+Dsaquel@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Noblet Ouways <91417411+Dsaquel@users.noreply.github.com>

* style: update

* Update packages/components/input/src/input.vue

Co-authored-by: rzzf <cszhjh@gmail.com>

---------

Co-authored-by: rzzf <cszhjh@gmail.com>
Co-authored-by: Noblet Ouways <91417411+Dsaquel@users.noreply.github.com>
2026-03-05 12:08:29 +01:00

843 lines
26 KiB
TypeScript

import { nextTick, ref } from 'vue'
import { mount } from '@vue/test-utils'
import { afterEach, describe, expect, test, vi } from 'vitest'
import defineGetter from '@element-plus/test-utils/define-getter'
import { ElForm, ElFormItem as FormItem } from '@element-plus/components/form'
import Input from '../src/input.vue'
import type { CSSProperties } from 'vue'
import type { InputAutoSize, InputProps } from '../src/input'
import type { InputInstance } from '../src/instance'
describe('Input.vue', () => {
afterEach(() => {
vi.restoreAllMocks()
})
test('create', async () => {
const input = ref('input')
const handleFocus = vi.fn()
const wrapper = mount(() => (
<Input
minlength={3}
maxlength={5}
placeholder="请输入内容"
onFocus={handleFocus}
modelValue={input.value}
/>
))
const inputElm = wrapper.find('input')
const nativeInput = inputElm.element
await inputElm.trigger('focus')
expect(inputElm.exists()).toBe(true)
expect(handleFocus).toHaveBeenCalled()
expect(nativeInput.placeholder).toMatchInlineSnapshot(`"请输入内容"`)
expect(nativeInput.value).toMatchInlineSnapshot(`"input"`)
expect(nativeInput.minLength).toMatchInlineSnapshot(`3`)
input.value = 'text'
await nextTick()
expect(inputElm.element.value).toMatchInlineSnapshot(`"text"`)
})
test('default to empty', () => {
const wrapper = mount(() => <Input />)
const inputElm = wrapper.find('input')
expect(inputElm.element.value).toBe('')
})
test('disabled', async () => {
const wrapper = mount(() => <Input disabled />)
const inputElm = wrapper.find('input')
expect(inputElm.element.disabled).not.toBeNull()
// trigger click should not focus #18012
inputElm.trigger('click')
await nextTick()
expect(inputElm.element.className.includes('is-focus')).toBe(false)
})
describe('test emoji', () => {
test('el-input should minimize value between emoji length and maxLength', async () => {
const inputVal = ref('12🌚')
const wrapper = mount(() => (
<Input
class="test-exceed"
maxlength="4"
showWordLimit
v-model={inputVal.value}
/>
))
const vm = wrapper.vm
const inputElm = wrapper.find('input')
const nativeInput = inputElm.element
expect(nativeInput.value).toMatchInlineSnapshot(`"12🌚"`)
const elCount = wrapper.find('.el-input__count-inner')
expect(elCount.exists()).toBe(true)
expect(elCount.text()).toMatchInlineSnapshot(`"4 / 4"`)
inputVal.value = '1👌3😄'
await nextTick()
expect(nativeInput.value).toMatchInlineSnapshot(`"1👌3😄"`)
expect(elCount.text()).toMatchInlineSnapshot(`"6 / 4"`)
inputVal.value = '哈哈1👌3😄'
await nextTick()
expect(nativeInput.value).toMatchInlineSnapshot(`"哈哈1👌3😄"`)
expect(elCount.text()).toMatchInlineSnapshot(`"8 / 4"`)
expect(Array.from(vm.$el.classList)).toMatchInlineSnapshot(`
[
"el-input",
"is-exceed",
"test-exceed",
]
`)
})
test('textarea should minimize value between emoji length and maxLength', async () => {
const inputVal = ref('啊好😄')
const wrapper = mount(() => (
<Input
type="textarea"
maxlength="4"
showWordLimit
v-model={inputVal.value}
/>
))
const vm = wrapper.vm
const inputElm = wrapper.find('textarea')
const nativeInput = inputElm.element
expect(nativeInput.value).toMatchInlineSnapshot(`"啊好😄"`)
const elCount = wrapper.find('.el-input__count')
expect(elCount.exists()).toBe(true)
expect(elCount.text()).toMatchInlineSnapshot(`"4 / 4"`)
inputVal.value = '哈哈1👌3😄'
await nextTick()
expect(nativeInput.value).toMatchInlineSnapshot(`"哈哈1👌3😄"`)
expect(elCount.text()).toMatchInlineSnapshot(`"8 / 4"`)
expect(Array.from(vm.$el.classList)).toMatchInlineSnapshot(`
[
"el-textarea",
"is-exceed",
]
`)
})
})
test('suffixIcon', () => {
const wrapper = mount(() => <Input suffix-icon="time" />)
const icon = wrapper.find('.el-input__icon')
expect(icon.exists()).toBe(true)
})
test('prefixIcon', () => {
const wrapper = mount(() => <Input prefix-icon="time" />)
const icon = wrapper.find('.el-input__icon')
expect(icon.exists()).toBe(true)
})
test('size', () => {
const wrapper = mount(() => <Input size="large" />)
expect(wrapper.classes('el-input--large')).toBe(true)
})
test('type', () => {
const wrapper = mount(() => <Input type="textarea" />)
expect(wrapper.classes('el-textarea')).toBe(true)
})
test('rows', () => {
const wrapper = mount(() => {
return <Input type="textarea" rows={3} />
})
expect(wrapper.find('textarea').element.rows).toEqual(3)
})
test('resize', async () => {
const resize = ref<InputProps['resize']>('none')
const wrapper = mount(() => <Input type="textarea" resize={resize.value} />)
const textarea = wrapper.find('textarea').element
await nextTick()
expect(textarea.style.resize).toEqual(resize.value)
resize.value = 'horizontal'
await nextTick()
expect(textarea.style.resize).toEqual(resize.value)
})
test('sets value on textarea / input type change', async () => {
const type = ref('text')
const val = ref('123')
const wrapper = mount(() => <Input type={type.value} v-model={val.value} />)
const vm = wrapper.vm
expect(vm.$el.querySelector('input').value).toMatchInlineSnapshot(`"123"`)
type.value = 'textarea'
await nextTick()
await nextTick()
expect(vm.$el.querySelector('textarea').value).toMatchInlineSnapshot(
`"123"`
)
type.value = 'password'
await nextTick()
await nextTick()
expect(vm.$el.querySelector('input').value).toMatchInlineSnapshot(`"123"`)
})
test('limit input and show word count', async () => {
const input1 = ref('')
const input2 = ref('')
const input3 = ref('')
const input4 = ref('exceed')
const show = ref(false)
const wrapper = mount(() => (
<div>
<Input
class="test-text"
type="text"
v-model={input1.value}
maxlength="10"
showWordLimit={show.value}
/>
<Input
class="test-textarea"
type="textarea"
v-model={input2.value}
maxlength="10"
showWordLimit
/>
<Input
class="test-password"
type="password"
v-model={input3.value}
maxlength="10"
showWordLimit
/>
<Input
class="test-initial-exceed"
type="text"
v-model={input4.value}
maxlength="2"
showWordLimit
/>
</div>
))
const inputElm1 = wrapper.vm.$el.querySelector('.test-text')
const inputElm2 = wrapper.vm.$el.querySelector('.test-textarea')
const inputElm3 = wrapper.vm.$el.querySelector('.test-password')
const inputElm4 = wrapper.vm.$el.querySelector('.test-initial-exceed')
expect(inputElm1.querySelectorAll('.el-input__count').length).toEqual(0)
expect(inputElm2.querySelectorAll('.el-input__count').length).toEqual(1)
expect(inputElm3.querySelectorAll('.el-input__count').length).toEqual(0)
expect(Array.from(inputElm4.classList)).toMatchInlineSnapshot(`
[
"el-input",
"is-exceed",
"test-initial-exceed",
]
`)
show.value = true
await nextTick()
expect(inputElm1.querySelectorAll('.el-input__count').length).toEqual(1)
input4.value = '1'
await nextTick()
expect(Array.from(inputElm4.classList)).toMatchInlineSnapshot(`
[
"el-input",
"test-initial-exceed",
]
`)
})
test('use formatter and parser', async () => {
const val = ref('10000')
const formatter = (val: string) => {
return val.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}
const parser = (val: string) => {
return val.replace(/\$\s?|(,*)/g, '')
}
const _val = ref('')
const handleEvent = (val: string) => (_val.value = val)
const wrapper = mount(() => (
<Input
v-model={val.value}
formatter={formatter}
parser={parser}
onInput={handleEvent}
onChange={handleEvent}
/>
))
const vm = wrapper.vm
const event = new Event('input', { bubbles: true })
expect(vm.$el.querySelector('input').value).toEqual('10,000')
expect(vm.$el.querySelector('input').value).not.toEqual('1000')
vm.$el.querySelector('input').value = '1,000,000'
vm.$el.querySelector('input').dispatchEvent(event)
expect(val.value).toEqual('1000000')
expect(_val.value).toEqual('1000000')
vm.$el
.querySelector('input')
.dispatchEvent(new Event('change', { bubbles: true }))
expect(val.value).toEqual('1000000')
expect(_val.value).toEqual('1000000')
})
describe('Input Methods', () => {
test('method:select', async () => {
const testContent = ref('test')
const wrapper = mount(() => <Input v-model={testContent.value} />)
const input = wrapper.find('input').element
// mock selectionRange behaviour, due to jsdom's reason this case cannot run well, may be fixed later using headlesschrome or puppeteer
let selected = false
defineGetter(input, 'selectionStart', function (this: HTMLInputElement) {
return selected ? 0 : this.value.length
})
defineGetter(input, 'selectionEnd', function (this: HTMLInputElement) {
return this.value.length
})
expect(input.selectionStart).toEqual(testContent.value.length)
expect(input.selectionEnd).toEqual(testContent.value.length)
input.select()
selected = true
await nextTick()
expect(input.selectionStart).toEqual(0)
expect(input.selectionEnd).toEqual(testContent.value.length)
})
test('method:resizeTextarea', async () => {
const text = ref('TEXT:resizeTextarea')
const wrapper = mount({
setup: () => () => (
<Input
ref="textarea"
autosize={{ minRows: 1, maxRows: 1 }}
type="textarea"
v-model={text.value}
/>
),
})
const refTextarea = wrapper.vm.$refs.textarea as InputInstance
const originMinHeight = (refTextarea.textareaStyle as CSSProperties)
.minHeight
;(refTextarea.autosize as Exclude<InputAutoSize, boolean>).minRows = 5
refTextarea.resizeTextarea()
// After this textarea min-height (style) will change
const nowMinHeight = (refTextarea.textareaStyle as any)[1].minHeight
expect(originMinHeight).not.toEqual(nowMinHeight)
})
})
describe('Input Events', () => {
const handleFocus = vi.fn()
const handleBlur = vi.fn()
test('event:focus', async () => {
const content = ref('')
const wrapper = mount(() => (
<Input
placeholder="请输入内容"
modelValue={content.value}
onFocus={handleFocus}
/>
))
const input = wrapper.find('input')
await input.trigger('focus')
expect(handleFocus).toHaveBeenCalledOnce()
})
test('event:blur', async () => {
const content = ref('')
const wrapper = mount(() => (
<Input
placeholder="请输入内容"
modelValue={content.value}
onBlur={handleBlur}
/>
))
const input = wrapper.find('input')
await input.trigger('blur')
expect(handleBlur).toHaveBeenCalledOnce()
})
test('textarea & event:focus', async () => {
const content = ref('')
const wrapper = mount(() => (
<Input
type="textarea"
placeholder="请输入内容"
modelValue={content.value}
onFocus={handleFocus}
/>
))
const input = wrapper.find('textarea')
await input.trigger('focus')
expect(handleFocus).toHaveBeenCalledOnce()
})
test('textarea & event:blur', async () => {
const content = ref('')
const wrapper = mount(() => (
<Input
type="textarea"
placeholder="请输入内容"
modelValue={content.value}
onBlur={handleBlur}
/>
))
const input = wrapper.find('textarea')
await input.trigger('blur')
expect(handleBlur).toBeCalled()
})
test('event:change', async () => {
const content = ref('a')
const value = ref('')
const handleChange = (val: string) => {
value.value = val
}
// NOTE: should be same as native's change behavior
const wrapper = mount(() => (
<Input
placeholder="请输入内容"
modelValue={content.value}
onChange={handleChange}
/>
))
const el = wrapper.find('input').element
wrapper.vm
const simulateEvent = (text: string, event: string) => {
el.value = text
el.dispatchEvent(new Event(event))
}
// simplified test, component should emit change when native does
simulateEvent('2', 'change')
await nextTick()
expect(value.value).toBe('2')
simulateEvent('1', 'input')
await nextTick()
expect(value.value).toBe('2')
})
test('event:clear', async () => {
const handleClear = vi.fn()
const handleInput = vi.fn()
const content = ref('a')
const handleTextareaClear = vi.fn()
const handleTextareaInput = vi.fn()
const textareaContent = ref('a')
const wrapper = mount(() => (
<>
<Input
placeholder="请输入内容"
clearable
v-model={content.value}
onClear={handleClear}
onInput={handleInput}
/>
<Input
type="textarea"
placeholder="请输入内容"
clearable
v-model={textareaContent.value}
onClear={handleTextareaClear}
onInput={handleTextareaInput}
/>
</>
))
const input = wrapper.find('input')
const textarea = wrapper.find('textarea')
// focus to show clear button
await input.trigger('focus')
await nextTick()
wrapper.find('.el-input__clear').trigger('click')
await nextTick()
expect(content.value).toEqual('')
expect(handleClear).toBeCalled()
expect(handleClear).toBeCalledWith(expect.any(MouseEvent))
expect(handleInput).toBeCalled()
// textarea
await textarea.trigger('focus')
await nextTick()
wrapper.find('.el-textarea__clear').trigger('click')
await nextTick()
expect(textareaContent.value).toEqual('')
expect(handleTextareaClear).toBeCalled()
expect(handleTextareaInput).toBeCalled()
})
test('event:input', async () => {
const handleInput = vi.fn()
const content = ref('a')
const wrapper = mount(() => (
<Input
placeholder="请输入内容"
clearable
modelValue={content.value}
onInput={handleInput}
/>
))
const inputWrapper = wrapper.find('input')
const nativeInput = inputWrapper.element
nativeInput.value = '1'
await inputWrapper.trigger('compositionstart')
await inputWrapper.trigger('input')
nativeInput.value = '2'
await inputWrapper.trigger('compositionupdate')
await inputWrapper.trigger('input')
await inputWrapper.trigger('compositionend')
expect(handleInput).toBeCalledTimes(1)
// native input value is controlled
expect(content.value).toEqual('a')
expect(nativeInput.value).toEqual('a')
})
})
test('non-emit event such as keyup should work', async () => {
const handleKeyup = vi.fn()
const wrapper = mount(Input, {
attrs: {
onKeyup: handleKeyup,
},
})
await wrapper.find('input').trigger('keyup')
expect(handleKeyup).toBeCalledTimes(1)
})
test('input-style', async () => {
const wrapper = mount(() => (
<>
<Input placeholder="请输入内容" input-style={{ color: 'red' }} />
<Input
placeholder="请输入内容"
input-style={{ color: 'red' }}
type="textarea"
/>
</>
))
const input = wrapper.find('input')
const textarea = wrapper.find('textarea')
await nextTick()
expect(input.element.style.color === 'red').toBeTruthy()
expect(textarea.element.style.color === 'red').toBeTruthy()
})
describe('Textarea Events', () => {
test('event:keydown', async () => {
const handleKeydown = vi.fn()
const content = ref('')
const wrapper = mount(() => (
<Input
type="textarea"
modelValue={content.value}
onKeydown={handleKeydown}
/>
))
await wrapper.find('textarea').trigger('keydown')
expect(handleKeydown).toBeCalledTimes(1)
})
})
test('show-password icon', async () => {
const password = ref('123456')
const wrapper = mount(() => (
<Input type="password" modelValue={password.value} show-password />
))
const icon = wrapper.find('.el-input__icon.el-input__password')
const d = icon.find('path').element.getAttribute('d')
await icon.trigger('click')
const d0 = icon.find('path').element.getAttribute('d')
expect(d !== d0).toBeTruthy()
})
test('show / hide password', async () => {
const password = ref('123456')
const wrapper = mount(() => (
<Input type="password" modelValue={password.value} show-password />
))
const icon = wrapper.find('.el-input__icon.el-input__password')
const input = wrapper.find('input')
expect(input.element.value).toBe('123456')
expect(input.element.selectionStart).toBe(6)
expect(input.element.selectionEnd).toBe(6)
await icon.trigger('click')
expect(input.element.value).toBe('123456')
expect(input.element.selectionStart).toBe(6)
expect(input.element.selectionEnd).toBe(6)
await input.element.setSelectionRange(1, 4)
await icon.trigger('click')
expect(input.element.selectionStart).toBe(1)
expect(input.element.selectionEnd).toBe(4)
})
describe('form item accessibility integration', () => {
test('automatic id attachment', async () => {
const wrapper = mount(() => (
<FormItem label="Foobar" data-test-ref="item">
<Input data-test-ref="input" />
</FormItem>
))
await nextTick()
const formItem = wrapper.find('[data-test-ref="item"]')
const input = wrapper.find('[data-test-ref="input"]')
const formItemLabel = formItem.find('.el-form-item__label')
expect(formItem.attributes().role).toBeFalsy()
expect(formItemLabel.attributes().for).toBe(input.attributes().id)
})
test('specified id attachment', async () => {
const wrapper = mount(() => (
<FormItem label="Foobar" data-test-ref="item">
<Input id="foobar" data-test-ref="input" />
</FormItem>
))
await nextTick()
const formItem = wrapper.find('[data-test-ref="item"]')
const input = wrapper.find('[data-test-ref="input"]')
const formItemLabel = formItem.find('.el-form-item__label')
expect(formItem.attributes().role).toBeFalsy()
expect(input.attributes().id).toBe('foobar')
expect(formItemLabel.attributes().for).toBe(input.attributes().id)
})
test('form item role is group when multiple inputs', async () => {
const wrapper = mount(() => (
<FormItem label="Foobar" data-test-ref="item">
<Input data-test-ref="input1" />
<Input data-test-ref="input2" />
</FormItem>
))
await nextTick()
const formItem = wrapper.find('[data-test-ref="item"]')
expect(formItem.attributes().role).toBe('group')
})
test('The disabled state of a component has higher priority than that of a form', async () => {
const wrapper = mount(() => (
<ElForm disabled>
<Input disabled={false} />
</ElForm>
))
await nextTick()
const input = wrapper.find('.el-input')
expect(input.classes()).not.toContain('is-disabled')
})
})
test('input change event return Event parameter', async () => {
const onChange = vi.fn()
const wrapper = mount(() => <Input onChange={onChange} />)
await wrapper.find('input').trigger('change')
await nextTick()
expect(onChange).toHaveBeenCalledWith('', expect.any(Event))
})
test('modelValue modifiers', async () => {
const number = ref()
const trim = ref()
const lazy = ref()
const trimNumber = ref()
const trimLazy = ref()
const numberLazy = ref()
const trimNumberLazy = ref()
const wrapper = mount(() => (
<>
<Input
id="number"
v-model={number.value}
modelModifiers={{ number: true }}
/>
<Input id="trim" v-model={trim.value} modelModifiers={{ trim: true }} />
<Input id="lazy" v-model={lazy.value} modelModifiers={{ lazy: true }} />
<Input
id="trim-number"
v-model={trimNumber.value}
modelModifiers={{ trim: true, number: true }}
/>
<Input
id="trim-lazy"
v-model={trimLazy.value}
modelModifiers={{ trim: true, lazy: true }}
/>
<Input
id="number-lazy"
v-model={numberLazy.value}
modelModifiers={{ number: true, lazy: true }}
/>
<Input
id="trim-number-lazy"
v-model={trimNumberLazy.value}
modelModifiers={{ trim: true, number: true, lazy: true }}
/>
</>
))
await nextTick()
const triggerEvent = async (type: string, el: Element) => {
const event = new Event(type)
el.dispatchEvent(event)
await nextTick()
}
const mockActiveElement = vi.spyOn(document, 'activeElement', 'get')
const numberEl = wrapper.find('#number').element as HTMLInputElement
const trimEl = wrapper.find('#trim').element as HTMLInputElement
const lazyEl = wrapper.find('#lazy').element as HTMLInputElement
const trimNumberEl = wrapper.find('#trim-number')
.element as HTMLInputElement
const trimLazyEl = wrapper.find('#trim-lazy').element as HTMLInputElement
const numberLazyEl = wrapper.find('#number-lazy')
.element as HTMLInputElement
const trimNumberLazyEl = wrapper.find('#trim-number-lazy')
.element as HTMLInputElement
mockActiveElement.mockReturnValue(numberEl)
numberEl.value = '+01.2'
await triggerEvent('input', numberEl)
expect(number.value).toEqual(1.2)
expect(numberEl.value).toEqual('+01.2')
await triggerEvent('change', numberEl)
expect(numberEl.value).toEqual('1.2')
mockActiveElement.mockReturnValue(trimEl)
trimEl.value = ' hello, world '
await triggerEvent('input', trimEl)
expect(trim.value).toEqual('hello, world')
expect(trimEl.value).toEqual(' hello, world ')
await triggerEvent('change', trimEl)
expect(trimEl.value).toEqual('hello, world')
mockActiveElement.mockReturnValue(lazyEl)
lazyEl.value = 'foo'
await triggerEvent('input', lazyEl)
expect(lazy.value).toBeUndefined()
await triggerEvent('change', lazyEl)
expect(lazy.value).toEqual('foo')
mockActiveElement.mockReturnValue(trimNumberEl)
trimNumberEl.value = ' 1 '
await triggerEvent('input', trimNumberEl)
expect(trimNumber.value).toEqual(1)
expect(trimNumberEl.value).toEqual(' 1 ')
await triggerEvent('change', trimNumberEl)
expect(trimNumberEl.value).toEqual('1')
mockActiveElement.mockReturnValue(trimLazyEl)
trimLazyEl.value = ' hello, world '
await triggerEvent('input', trimLazyEl)
expect(trimLazy.value).toBeUndefined()
expect(trimLazyEl.value).toEqual(' hello, world ')
await triggerEvent('change', trimLazyEl)
expect(trimLazy.value).toEqual('hello, world')
expect(trimLazyEl.value).toEqual('hello, world')
mockActiveElement.mockReturnValue(numberLazyEl)
numberLazyEl.value = '+01.2'
await triggerEvent('input', numberLazyEl)
expect(numberLazy.value).toBeUndefined()
expect(numberLazyEl.value).toEqual('+01.2')
await triggerEvent('change', numberLazyEl)
expect(numberLazy.value).toEqual(1.2)
expect(numberLazyEl.value).toEqual('1.2')
mockActiveElement.mockReturnValue(trimNumberLazyEl)
trimNumberLazyEl.value = ' +01.2 '
await triggerEvent('input', trimNumberLazyEl)
expect(trimNumberLazy.value).toBeUndefined()
expect(trimNumberLazyEl.value).toEqual(' +01.2 ')
await triggerEvent('change', trimNumberLazyEl)
expect(trimNumberLazy.value).toEqual(1.2)
expect(trimNumberLazyEl.value).toEqual('1.2')
mockActiveElement.mockRestore()
})
test('textarea-show-word-limit-outside-position', async () => {
const wrapper = mount(() => (
<Input
placeholder="请输入内容"
showWordLimit
wordLimitPosition="outside"
maxlength={30}
type="textarea"
/>
))
const wordLimit = wrapper.find('.el-input__count')
await nextTick()
expect(wordLimit.exists()).toBe(true)
expect(wordLimit.element.className.includes('is-outside')).toBeTruthy()
})
test('input-show-word-limit-outside-position', async () => {
const wrapper = mount(() => (
<Input
placeholder="请输入内容"
showWordLimit
wordLimitPosition="outside"
maxlength={30}
/>
))
const wordLimit = wrapper.find('.el-input__count')
await nextTick()
expect(wordLimit.exists()).toBe(true)
expect(wordLimit.element.className.includes('is-outside')).toBeTruthy()
})
// TODO: validateEvent & input containes select cases should be added after the rest components finished
// ...
})