20260819173221
This commit is contained in:
1 parent
1ffc8e1ac9
commit
a0a4273f4c
326 files changed
+301646
-2314
No files matched your search
@@ -0,0 +1,149 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Button from '@/components/ui/button/button.vue'
|
||||
import Tag from '@/components/ui/tag/tag.vue'
|
||||
import Switch from '@/components/ui/switch/switch.vue'
|
||||
import Radio from '@/components/ui/radio/radio.vue'
|
||||
import RadioGroup from '@/components/ui/radio/radio-group.vue'
|
||||
import Checkbox from '@/components/ui/checkbox/checkbox.vue'
|
||||
import Spin from '@/components/ui/spin/spin.vue'
|
||||
|
||||
describe('Button 组件', () => {
|
||||
it('默认渲染 primary 按钮并显示插槽内容', () => {
|
||||
const wrapper = mount(Button, { slots: { default: '提交' } })
|
||||
expect(wrapper.find('button').classes()).toContain('button-primary')
|
||||
expect(wrapper.text()).toContain('提交')
|
||||
})
|
||||
|
||||
it('type/block/loading 映射 class', () => {
|
||||
const wrapper = mount(Button, { props: { type: 'danger', block: true, loading: true } })
|
||||
const btn = wrapper.find('button')
|
||||
expect(btn.classes()).toContain('button-danger')
|
||||
expect(btn.classes()).toContain('button-block')
|
||||
expect(btn.classes()).toContain('button-loading')
|
||||
})
|
||||
|
||||
it('loading 或 disabled 时禁用原生按钮', () => {
|
||||
const loading = mount(Button, { props: { loading: true } })
|
||||
expect(loading.find('button').attributes('disabled')).toBeDefined()
|
||||
const disabled = mount(Button, { props: { disabled: true } })
|
||||
expect(disabled.find('button').attributes('disabled')).toBeDefined()
|
||||
})
|
||||
|
||||
it('htmlType 透传原生 type', () => {
|
||||
const wrapper = mount(Button, { props: { htmlType: 'submit' } })
|
||||
expect(wrapper.find('button').attributes('type')).toBe('submit')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Tag 组件', () => {
|
||||
it('默认渲染 default 变体', () => {
|
||||
const wrapper = mount(Tag, { slots: { default: '标签' } })
|
||||
expect(wrapper.text()).toContain('标签')
|
||||
})
|
||||
|
||||
it('variant 映射 class', () => {
|
||||
const wrapper = mount(Tag, { props: { variant: 'danger' } })
|
||||
expect(wrapper.classes()).toContain('tag-danger')
|
||||
})
|
||||
|
||||
it('closable 点击关闭:emit close 并隐藏', async () => {
|
||||
const wrapper = mount(Tag, { props: { closable: true } })
|
||||
await wrapper.find('[role="button"]').trigger('click')
|
||||
expect(wrapper.emitted('close')).toHaveLength(1)
|
||||
expect(wrapper.classes()).toContain('tag-hidden')
|
||||
})
|
||||
|
||||
it('disabled 时点击不关闭', async () => {
|
||||
const wrapper = mount(Tag, { props: { closable: true, disabled: true } })
|
||||
await wrapper.find('[role="button"]').trigger('click')
|
||||
expect(wrapper.emitted('close')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('href 渲染为链接', () => {
|
||||
const wrapper = mount(Tag, { props: { href: '/x' } })
|
||||
expect(wrapper.find('a').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Switch 组件', () => {
|
||||
it('点击触发 update:modelValue 与 change', async () => {
|
||||
const wrapper = mount(Switch, { props: { modelValue: false } })
|
||||
await wrapper.find('[role="switch"]').trigger('click')
|
||||
expect(wrapper.emitted('update:modelValue')[0]).toEqual([true])
|
||||
expect(wrapper.emitted('change')[0][0]).toBe(true)
|
||||
})
|
||||
|
||||
it('checked 状态映射 class 与 aria-checked', () => {
|
||||
const wrapper = mount(Switch, { props: { modelValue: true } })
|
||||
expect(wrapper.find('[role="switch"]').classes()).toContain('switch-checked')
|
||||
expect(wrapper.find('[role="switch"]').attributes('aria-checked')).toBe('true')
|
||||
})
|
||||
|
||||
it('disabled 时点击不触发', async () => {
|
||||
const wrapper = mount(Switch, { props: { disabled: true } })
|
||||
await wrapper.find('[role="switch"]').trigger('click')
|
||||
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('RadioGroup + Radio 组件', () => {
|
||||
it('选中 radio 触发 v-model 更新', async () => {
|
||||
const real = mount({
|
||||
components: { RadioGroup, Radio },
|
||||
template: '<RadioGroup v-model="v"><Radio value="a" /><Radio value="b" /></RadioGroup>',
|
||||
data: () => ({ v: 'a' }),
|
||||
})
|
||||
const radios = real.findAll('input[type="radio"]')
|
||||
expect(radios[0].element.checked).toBe(true)
|
||||
await radios[1].setValue()
|
||||
expect(real.vm.v).toBe('b')
|
||||
})
|
||||
|
||||
it('独立 Radio 未分组时默认不选中', () => {
|
||||
const wrapper = mount(Radio, { props: { value: 'x' } })
|
||||
expect(wrapper.find('input').element.checked).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Checkbox 组件', () => {
|
||||
it('勾选触发 update:modelValue 与 change', async () => {
|
||||
const wrapper = mount(Checkbox, { props: { modelValue: false } })
|
||||
await wrapper.find('input[type="checkbox"]').setValue(true)
|
||||
expect(wrapper.emitted('update:modelValue')[0]).toEqual([true])
|
||||
expect(wrapper.emitted('change')[0][0]).toBe(true)
|
||||
})
|
||||
|
||||
it('checked/indeterminate/disabled 映射 class', () => {
|
||||
const checked = mount(Checkbox, { props: { modelValue: true } })
|
||||
expect(checked.classes()).toContain('checkbox-checked')
|
||||
const semi = mount(Checkbox, { props: { indeterminate: true, modelValue: false } })
|
||||
expect(semi.classes()).toContain('checkbox-indeterminate')
|
||||
const disabled = mount(Checkbox, { props: { disabled: true } })
|
||||
expect(disabled.classes()).toContain('checkbox-disabled')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Spin 组件', () => {
|
||||
it('默认加载中显示 aria-busy', () => {
|
||||
const wrapper = mount(Spin)
|
||||
expect(wrapper.attributes('aria-busy')).toBe('true')
|
||||
})
|
||||
|
||||
it('spinning=false 不显示', () => {
|
||||
const wrapper = mount(Spin, { props: { spinning: false } })
|
||||
expect(wrapper.attributes('aria-busy')).toBe('false')
|
||||
})
|
||||
|
||||
it('delay 延迟生效:立即不 active,延时后 active', async () => {
|
||||
const wrapper = mount(Spin, { props: { spinning: true, delay: 50 } })
|
||||
expect(wrapper.attributes('aria-busy')).toBe('false')
|
||||
await new Promise((r) => setTimeout(r, 80))
|
||||
expect(wrapper.attributes('aria-busy')).toBe('true')
|
||||
})
|
||||
|
||||
it('description 渲染加载文案', () => {
|
||||
const wrapper = mount(Spin, { props: { description: '加载中...' } })
|
||||
expect(wrapper.text()).toContain('加载中...')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,411 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { nextTick } from 'vue'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Input from '@/components/ui/input/input.vue'
|
||||
|
||||
const inputEl = (wrapper) => wrapper.find('[data-slot="input"]')
|
||||
const textareaEl = (wrapper) => wrapper.find('[data-slot="textarea"]')
|
||||
const clearBtn = (wrapper) => wrapper.find('button[aria-label="清除"]')
|
||||
const resizeHandle = (wrapper) => wrapper.find('[role="slider"]')
|
||||
|
||||
describe('Input 组件 · default 模式', () => {
|
||||
it('渲染 input 并透传 value', () => {
|
||||
const wrapper = mount(Input, { props: { modelValue: 'abc' } })
|
||||
expect(inputEl(wrapper).element.value).toBe('abc')
|
||||
})
|
||||
|
||||
it('Number 类型的 modelValue 正常渲染', () => {
|
||||
const wrapper = mount(Input, { props: { modelValue: 123 } })
|
||||
expect(inputEl(wrapper).element.value).toBe('123')
|
||||
})
|
||||
|
||||
it('输入触发 update:modelValue', async () => {
|
||||
const wrapper = mount(Input, { props: { modelValue: '' } })
|
||||
await inputEl(wrapper).setValue('hello')
|
||||
expect(wrapper.emitted('update:modelValue')[0]).toEqual(['hello'])
|
||||
})
|
||||
|
||||
it('change 事件转发', async () => {
|
||||
const wrapper = mount(Input, { props: { modelValue: '' } })
|
||||
await inputEl(wrapper).setValue('x')
|
||||
await inputEl(wrapper).trigger('change')
|
||||
expect(wrapper.emitted('change')[0]).toEqual(['x'])
|
||||
})
|
||||
|
||||
it('focus/blur 触发事件并切换聚焦态', async () => {
|
||||
const wrapper = mount(Input)
|
||||
await inputEl(wrapper).trigger('focus')
|
||||
expect(wrapper.emitted('focus')).toHaveLength(1)
|
||||
expect(wrapper.classes()).toContain('input-focused')
|
||||
await inputEl(wrapper).trigger('blur')
|
||||
expect(wrapper.emitted('blur')).toHaveLength(1)
|
||||
expect(wrapper.classes()).not.toContain('input-focused')
|
||||
})
|
||||
|
||||
it('maxlength 透传原生属性', () => {
|
||||
const wrapper = mount(Input, { props: { maxlength: 20 } })
|
||||
expect(inputEl(wrapper).attributes('maxlength')).toBe('20')
|
||||
})
|
||||
|
||||
it('disabled/readonly 反映到原生属性与 wrapper class', () => {
|
||||
const d = mount(Input, { props: { disabled: true } })
|
||||
expect(inputEl(d).attributes('disabled')).toBeDefined()
|
||||
expect(d.classes()).toContain('input-disabled')
|
||||
const r = mount(Input, { props: { readonly: true } })
|
||||
expect(inputEl(r).attributes('readonly')).toBeDefined()
|
||||
expect(r.classes()).toContain('input-readonly')
|
||||
})
|
||||
|
||||
it('status=error 映射 aria-invalid 与 class', () => {
|
||||
const wrapper = mount(Input, { props: { status: 'error' } })
|
||||
expect(inputEl(wrapper).attributes('aria-invalid')).toBe('true')
|
||||
expect(wrapper.classes()).toContain('input-error')
|
||||
})
|
||||
|
||||
it('status=warning 映射 class 且不设置 aria-invalid', () => {
|
||||
const wrapper = mount(Input, { props: { status: 'warning' } })
|
||||
expect(wrapper.classes()).toContain('input-warning')
|
||||
expect(inputEl(wrapper).attributes('aria-invalid')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('prefix/suffix 插槽渲染', () => {
|
||||
const wrapper = mount(Input, {
|
||||
slots: { prefix: '<span class="pfx">P</span>', suffix: '<span class="sfx">S</span>' },
|
||||
})
|
||||
expect(wrapper.find('.pfx').exists()).toBe(true)
|
||||
expect(wrapper.find('.sfx').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('allowClear:有值但未聚焦/未悬停时不显示清除按钮', () => {
|
||||
const wrapper = mount(Input, { props: { modelValue: 'v', allowClear: true } })
|
||||
expect(clearBtn(wrapper).exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('allowClear:悬停时显示清除按钮,点击清空', async () => {
|
||||
const wrapper = mount(Input, { props: { modelValue: 'v', allowClear: true } })
|
||||
await wrapper.trigger('mouseenter')
|
||||
expect(clearBtn(wrapper).exists()).toBe(true)
|
||||
await clearBtn(wrapper).trigger('click')
|
||||
expect(wrapper.emitted('update:modelValue')[0]).toEqual([''])
|
||||
expect(wrapper.emitted('clear')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('allowClear:disabled 时即使悬停也不显示清除按钮', async () => {
|
||||
const wrapper = mount(Input, { props: { modelValue: 'v', allowClear: true, disabled: true } })
|
||||
await wrapper.trigger('mouseenter')
|
||||
expect(clearBtn(wrapper).exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('allowClear:readonly 时即使悬停也不显示清除按钮', async () => {
|
||||
const wrapper = mount(Input, { props: { modelValue: 'v', allowClear: true, readonly: true } })
|
||||
await wrapper.trigger('mouseenter')
|
||||
expect(clearBtn(wrapper).exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('allowClear:mouseleave 后清除按钮隐藏', async () => {
|
||||
const wrapper = mount(Input, { props: { modelValue: 'v', allowClear: true } })
|
||||
await wrapper.trigger('mouseenter')
|
||||
expect(clearBtn(wrapper).exists()).toBe(true)
|
||||
await wrapper.trigger('mouseleave')
|
||||
expect(clearBtn(wrapper).exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('showCount 显示字数统计(无上限仅长度)', () => {
|
||||
const wrapper = mount(Input, { props: { modelValue: 'abc', showCount: true } })
|
||||
expect(wrapper.text()).toContain('3')
|
||||
})
|
||||
|
||||
it('showCount 配合 maxlength 显示 当前 / 上限', () => {
|
||||
const wrapper = mount(Input, { props: { modelValue: 'abc', showCount: true, maxlength: 10 } })
|
||||
expect(wrapper.text()).toContain('3 / 10')
|
||||
})
|
||||
|
||||
it('type 校验器拒绝非法模式', () => {
|
||||
const spy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
mount(Input, { props: { type: 'foo' } })
|
||||
expect(spy).toHaveBeenCalled()
|
||||
spy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Input 组件 · password 模式', () => {
|
||||
it('默认 type=password,点击切换明文,再点击切回密文', async () => {
|
||||
const wrapper = mount(Input, { props: { type: 'password', modelValue: 'pw' } })
|
||||
const input = inputEl(wrapper)
|
||||
expect(input.attributes('type')).toBe('password')
|
||||
|
||||
await wrapper.find('button[aria-label="显示密码"]').trigger('click')
|
||||
expect(input.attributes('type')).toBe('text')
|
||||
expect(input.element.value).toBe('pw')
|
||||
|
||||
await wrapper.find('button[aria-label="隐藏密码"]').trigger('click')
|
||||
expect(input.attributes('type')).toBe('password')
|
||||
})
|
||||
|
||||
it('切换按钮 aria-label/aria-pressed 随状态变化', async () => {
|
||||
const wrapper = mount(Input, { props: { type: 'password' } })
|
||||
const toggle = wrapper.find('button[aria-label="显示密码"]')
|
||||
expect(toggle.attributes('aria-pressed')).toBe('false')
|
||||
await toggle.trigger('click')
|
||||
expect(wrapper.find('button[aria-label="隐藏密码"]').attributes('aria-pressed')).toBe('true')
|
||||
})
|
||||
|
||||
it('disabled 时切换按钮禁用', () => {
|
||||
const wrapper = mount(Input, { props: { type: 'password', disabled: true } })
|
||||
expect(wrapper.find('button[aria-label="显示密码"]').attributes('disabled')).toBeDefined()
|
||||
})
|
||||
|
||||
it('readonly 时切换按钮禁用', () => {
|
||||
const wrapper = mount(Input, { props: { type: 'password', readonly: true } })
|
||||
expect(wrapper.find('button[aria-label="显示密码"]').attributes('disabled')).toBeDefined()
|
||||
})
|
||||
|
||||
it('password 模式仍支持字数统计', () => {
|
||||
const wrapper = mount(Input, { props: { type: 'password', modelValue: 'abc', showCount: true } })
|
||||
expect(wrapper.text()).toContain('3')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Input 组件 · number 模式', () => {
|
||||
it('过滤非法字符:字母被剔除,只保留数字与单个小数点', async () => {
|
||||
const wrapper = mount(Input, { props: { type: 'number', modelValue: '' } })
|
||||
await inputEl(wrapper).setValue('12a.b.c')
|
||||
expect(wrapper.emitted('update:modelValue')[0]).toEqual(['12.'])
|
||||
})
|
||||
|
||||
it('过滤多余小数点:12.3.4 归一化为 12.34', async () => {
|
||||
const wrapper = mount(Input, { props: { type: 'number', modelValue: '' } })
|
||||
await inputEl(wrapper).setValue('12.3.4')
|
||||
expect(wrapper.emitted('update:modelValue')[0]).toEqual(['12.34'])
|
||||
})
|
||||
|
||||
it('纯字母输入被清空', async () => {
|
||||
const wrapper = mount(Input, { props: { type: 'number', modelValue: '' } })
|
||||
await inputEl(wrapper).setValue('abc')
|
||||
expect(wrapper.emitted('update:modelValue')[0]).toEqual([''])
|
||||
})
|
||||
|
||||
it('保留小数中间态:输入 "12." 不被截断', async () => {
|
||||
const wrapper = mount(Input, { props: { type: 'number', modelValue: '' } })
|
||||
await inputEl(wrapper).setValue('12.')
|
||||
expect(wrapper.emitted('update:modelValue')[0]).toEqual(['12.'])
|
||||
})
|
||||
|
||||
it('输入 "." 保留中间态,失焦后提交为空串', async () => {
|
||||
const real = mount({
|
||||
components: { Input },
|
||||
template: '<Input v-model="v" type="number" />',
|
||||
data: () => ({ v: '' }),
|
||||
})
|
||||
const input = real.find('[data-slot="input"]')
|
||||
await input.setValue('.')
|
||||
expect(real.vm.v).toBe('.')
|
||||
await input.trigger('blur')
|
||||
expect(real.vm.v).toBe('')
|
||||
})
|
||||
|
||||
it('失焦提交转为 Number:3.50 → 3.5', async () => {
|
||||
const wrapper = mount(Input, { props: { type: 'number', modelValue: '' } })
|
||||
await inputEl(wrapper).setValue('3.50')
|
||||
await inputEl(wrapper).trigger('blur')
|
||||
expect(wrapper.emitted('update:modelValue').at(-1)).toEqual([3.5])
|
||||
})
|
||||
|
||||
it('失焦后原生输入框显示规范化值', async () => {
|
||||
const wrapper = mount(Input, { props: { type: 'number', modelValue: '' } })
|
||||
await inputEl(wrapper).setValue('3.50')
|
||||
await inputEl(wrapper).trigger('blur')
|
||||
expect(inputEl(wrapper).element.value).toBe('3.5')
|
||||
})
|
||||
|
||||
it('change 事件提交 Number 值', async () => {
|
||||
const wrapper = mount(Input, { props: { type: 'number', modelValue: '' } })
|
||||
await inputEl(wrapper).setValue('3.50')
|
||||
await inputEl(wrapper).trigger('change')
|
||||
expect(wrapper.emitted('change')[0]).toEqual([3.5])
|
||||
})
|
||||
|
||||
it('值未变化时失焦不重复触发 update:modelValue', async () => {
|
||||
const wrapper = mount(Input, { props: { type: 'number', modelValue: 3.5 } })
|
||||
await inputEl(wrapper).setValue('3.5')
|
||||
expect(wrapper.emitted('update:modelValue')).toHaveLength(1)
|
||||
await inputEl(wrapper).trigger('blur')
|
||||
expect(wrapper.emitted('update:modelValue')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('空值失焦不重复触发 update:modelValue', async () => {
|
||||
const wrapper = mount(Input, { props: { type: 'number', modelValue: '' } })
|
||||
await inputEl(wrapper).setValue('')
|
||||
await inputEl(wrapper).trigger('blur')
|
||||
expect(wrapper.emitted('update:modelValue')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('透传 inputmode=decimal 与 number 校验 pattern', () => {
|
||||
const wrapper = mount(Input, { props: { type: 'number' } })
|
||||
expect(inputEl(wrapper).attributes('inputmode')).toBe('decimal')
|
||||
expect(inputEl(wrapper).attributes('pattern')).toBe('[0-9]*[.]?[0-9]*')
|
||||
})
|
||||
|
||||
it('Number 类型 modelValue 渲染为字符串', () => {
|
||||
const wrapper = mount(Input, { props: { type: 'number', modelValue: 1.28 } })
|
||||
expect(inputEl(wrapper).element.value).toBe('1.28')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Input 组件 · textarea 模式', () => {
|
||||
it('渲染 textarea 并转发输入', async () => {
|
||||
const wrapper = mount(Input, { props: { type: 'textarea', modelValue: 'multi' } })
|
||||
expect(textareaEl(wrapper).element.value).toBe('multi')
|
||||
await textareaEl(wrapper).setValue('new')
|
||||
expect(wrapper.emitted('update:modelValue')[0]).toEqual(['new'])
|
||||
})
|
||||
|
||||
it('change 事件转发', async () => {
|
||||
const wrapper = mount(Input, { props: { type: 'textarea', modelValue: '' } })
|
||||
await textareaEl(wrapper).setValue('x')
|
||||
await textareaEl(wrapper).trigger('change')
|
||||
expect(wrapper.emitted('change')[0]).toEqual(['x'])
|
||||
})
|
||||
|
||||
it('focus/blur 触发事件', async () => {
|
||||
const wrapper = mount(Input, { props: { type: 'textarea' } })
|
||||
await textareaEl(wrapper).trigger('focus')
|
||||
expect(wrapper.emitted('focus')).toHaveLength(1)
|
||||
await textareaEl(wrapper).trigger('blur')
|
||||
expect(wrapper.emitted('blur')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('rows 透传(默认 3,可自定义)', () => {
|
||||
const d = mount(Input, { props: { type: 'textarea' } })
|
||||
expect(textareaEl(d).attributes('rows')).toBe('3')
|
||||
const custom = mount(Input, { props: { type: 'textarea', rows: 6 } })
|
||||
expect(textareaEl(custom).attributes('rows')).toBe('6')
|
||||
})
|
||||
|
||||
it('maxlength 透传', () => {
|
||||
const wrapper = mount(Input, { props: { type: 'textarea', maxlength: 100 } })
|
||||
expect(textareaEl(wrapper).attributes('maxlength')).toBe('100')
|
||||
})
|
||||
|
||||
it('disabled/readonly 反映到原生属性', () => {
|
||||
const d = mount(Input, { props: { type: 'textarea', disabled: true } })
|
||||
expect(textareaEl(d).attributes('disabled')).toBeDefined()
|
||||
const r = mount(Input, { props: { type: 'textarea', readonly: true } })
|
||||
expect(textareaEl(r).attributes('readonly')).toBeDefined()
|
||||
})
|
||||
|
||||
it('status=error 映射 aria-invalid', () => {
|
||||
const wrapper = mount(Input, { props: { type: 'textarea', status: 'error' } })
|
||||
expect(textareaEl(wrapper).attributes('aria-invalid')).toBe('true')
|
||||
})
|
||||
|
||||
it('textarea 无清除按钮', () => {
|
||||
const wrapper = mount(Input, { props: { type: 'textarea', modelValue: 'x', allowClear: true } })
|
||||
expect(clearBtn(wrapper).exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('showCount 配合 maxlength 显示 当前 / 上限', () => {
|
||||
const wrapper = mount(Input, {
|
||||
props: { type: 'textarea', modelValue: 'abc', showCount: true, maxlength: 100 },
|
||||
})
|
||||
expect(wrapper.text()).toContain('3 / 100')
|
||||
})
|
||||
|
||||
it('可编辑时显示拖拽调整手柄', () => {
|
||||
const wrapper = mount(Input, { props: { type: 'textarea' } })
|
||||
expect(resizeHandle(wrapper).exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('disabled 时不显示拖拽调整手柄', () => {
|
||||
const wrapper = mount(Input, { props: { type: 'textarea', disabled: true } })
|
||||
expect(resizeHandle(wrapper).exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('readonly 时不显示拖拽调整手柄', () => {
|
||||
const wrapper = mount(Input, { props: { type: 'textarea', readonly: true } })
|
||||
expect(resizeHandle(wrapper).exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('键盘调整高度:ArrowDown 增高 / ArrowUp 降低 / Home 还原', async () => {
|
||||
const wrapper = mount(Input, { props: { type: 'textarea' } })
|
||||
const handle = resizeHandle(wrapper)
|
||||
const ta = textareaEl(wrapper)
|
||||
|
||||
await handle.trigger('keydown', { key: 'ArrowDown' })
|
||||
await nextTick()
|
||||
expect(ta.element.style.height).toBe('20px')
|
||||
|
||||
await handle.trigger('keydown', { key: 'ArrowDown' })
|
||||
await nextTick()
|
||||
expect(ta.element.style.height).toBe('40px')
|
||||
|
||||
await handle.trigger('keydown', { key: 'ArrowUp' })
|
||||
await nextTick()
|
||||
expect(ta.element.style.height).toBe('20px')
|
||||
|
||||
await handle.trigger('keydown', { key: 'Home' })
|
||||
await nextTick()
|
||||
expect(ta.element.style.height).toBe('0px')
|
||||
})
|
||||
|
||||
it('键盘调整不会低于默认高度', async () => {
|
||||
const wrapper = mount(Input, { props: { type: 'textarea' } })
|
||||
await resizeHandle(wrapper).trigger('keydown', { key: 'ArrowUp' })
|
||||
await nextTick()
|
||||
expect(textareaEl(wrapper).element.style.height).toBe('0px')
|
||||
})
|
||||
|
||||
it('拖拽手柄忽略非调整键', async () => {
|
||||
const wrapper = mount(Input, { props: { type: 'textarea' } })
|
||||
await resizeHandle(wrapper).trigger('keydown', { key: 'Enter' })
|
||||
await nextTick()
|
||||
expect(textareaEl(wrapper).element.style.height).toBe('')
|
||||
})
|
||||
|
||||
it('textarea 模式切换后高度状态重置', async () => {
|
||||
const wrapper = mount(Input, { props: { type: 'textarea' } })
|
||||
await resizeHandle(wrapper).trigger('keydown', { key: 'ArrowDown' })
|
||||
await nextTick()
|
||||
expect(textareaEl(wrapper).element.style.height).toBe('20px')
|
||||
|
||||
await wrapper.setProps({ type: 'default' })
|
||||
await wrapper.setProps({ type: 'textarea' })
|
||||
await nextTick()
|
||||
expect(textareaEl(wrapper).element.style.height).toBe('')
|
||||
})
|
||||
|
||||
it('拖拽调整高度:pointermove 更新高度', async () => {
|
||||
const wrapper = mount(Input, { props: { type: 'textarea' } })
|
||||
const handleEl = resizeHandle(wrapper).element
|
||||
handleEl.dispatchEvent(new MouseEvent('pointerdown', { clientY: 100, bubbles: true }))
|
||||
document.dispatchEvent(new MouseEvent('pointermove', { clientY: 150, bubbles: true }))
|
||||
await vi.waitFor(() => {
|
||||
expect(textareaEl(wrapper).element.style.height).toBe('50px')
|
||||
})
|
||||
document.dispatchEvent(new MouseEvent('pointerup'))
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('拖拽未渲染帧即结束:立即应用待定高度', async () => {
|
||||
const wrapper = mount(Input, { props: { type: 'textarea' } })
|
||||
const handleEl = resizeHandle(wrapper).element
|
||||
handleEl.dispatchEvent(new MouseEvent('pointerdown', { clientY: 100, bubbles: true }))
|
||||
document.dispatchEvent(new MouseEvent('pointermove', { clientY: 130, bubbles: true }))
|
||||
// 同步结束拖拽:rAF 帧尚未渲染,flushResizeFrame 应取消帧并应用待定高度
|
||||
document.dispatchEvent(new MouseEvent('pointerup', { clientY: 130, bubbles: true }))
|
||||
await nextTick()
|
||||
expect(textareaEl(wrapper).element.style.height).toBe('30px')
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('拖拽移动小于 2px 忽略', async () => {
|
||||
const wrapper = mount(Input, { props: { type: 'textarea' } })
|
||||
const handleEl = resizeHandle(wrapper).element
|
||||
handleEl.dispatchEvent(new MouseEvent('pointerdown', { clientY: 100, bubbles: true }))
|
||||
document.dispatchEvent(new MouseEvent('pointermove', { clientY: 101, bubbles: true }))
|
||||
document.dispatchEvent(new MouseEvent('pointerup', { clientY: 101, bubbles: true }))
|
||||
await nextTick()
|
||||
expect(textareaEl(wrapper).element.style.height).toBe('')
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user