2 changed files with 146 additions and 0 deletions
@ -0,0 +1,42 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
const props = withDefaults(defineProps<{ |
||||
|
modelValue: string |
||||
|
label?: string |
||||
|
placeholder?: string |
||||
|
disabled?: boolean |
||||
|
}>(), { |
||||
|
label: '密码', |
||||
|
placeholder: '请输入密码', |
||||
|
disabled: false, |
||||
|
}) |
||||
|
|
||||
|
const emit = defineEmits<{ |
||||
|
'update:modelValue': [value: string] |
||||
|
}>() |
||||
|
|
||||
|
const showPassword = ref(false) |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<UFormField :label="label" required> |
||||
|
<div class="relative"> |
||||
|
<UInput |
||||
|
:type="showPassword ? 'text' : 'password'" |
||||
|
:model-value="modelValue" |
||||
|
:placeholder="placeholder" |
||||
|
:disabled="disabled" |
||||
|
class="w-full" |
||||
|
@update:model-value="emit('update:modelValue', $event)" |
||||
|
/> |
||||
|
<UButton |
||||
|
variant="link" |
||||
|
color="neutral" |
||||
|
size="sm" |
||||
|
:icon="showPassword ? 'i-lucide-eye-off' : 'i-lucide-eye'" |
||||
|
:aria-label="showPassword ? '隐藏密码' : '显示密码'" |
||||
|
class="absolute right-1 top-1/2 -translate-y-1/2" |
||||
|
@click="showPassword = !showPassword" |
||||
|
/> |
||||
|
</div> |
||||
|
</UFormField> |
||||
|
</template> |
||||
@ -0,0 +1,104 @@ |
|||||
|
import { describe, it, expect } from 'vitest' |
||||
|
import { mount } from '@vue/test-utils' |
||||
|
import PasswordInput from '../PasswordInput.vue' |
||||
|
|
||||
|
describe('PasswordInput', () => { |
||||
|
it('renders with type=password by default', () => { |
||||
|
const wrapper = mount(PasswordInput, { |
||||
|
props: { modelValue: '' }, |
||||
|
global: { |
||||
|
stubs: { |
||||
|
UInput: { |
||||
|
template: '<input :type="$attrs.type" :disabled="$attrs.disabled" :value="modelValue" @input="$emit(\'update:modelValue\', $event.target.value)" />', |
||||
|
props: ['type', 'disabled', 'modelValue', 'placeholder'], |
||||
|
}, |
||||
|
UButton: { |
||||
|
template: '<button :disabled="$attrs.disabled" @click="$emit(\'click\')"><slot /></button>', |
||||
|
props: ['disabled'], |
||||
|
emits: ['click'], |
||||
|
}, |
||||
|
UFormField: { |
||||
|
template: '<div><slot /></div>', |
||||
|
props: ['label'], |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
}) |
||||
|
const input = wrapper.find('input') |
||||
|
expect(input.attributes('type')).toBe('password') |
||||
|
}) |
||||
|
|
||||
|
it('toggles to type=text when button clicked', async () => { |
||||
|
const wrapper = mount(PasswordInput, { |
||||
|
props: { modelValue: '' }, |
||||
|
global: { |
||||
|
stubs: { |
||||
|
UInput: { |
||||
|
template: '<input :type="$attrs.type" :disabled="$attrs.disabled" :value="modelValue" @input="$emit(\'update:modelValue\', $event.target.value)" />', |
||||
|
props: ['type', 'disabled', 'modelValue', 'placeholder'], |
||||
|
}, |
||||
|
UButton: { |
||||
|
template: '<button :disabled="$attrs.disabled" @click="$emit(\'click\')"><slot /></button>', |
||||
|
props: ['disabled'], |
||||
|
emits: ['click'], |
||||
|
}, |
||||
|
UFormField: { |
||||
|
template: '<div><slot /></div>', |
||||
|
props: ['label'], |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
}) |
||||
|
const btn = wrapper.find('button') |
||||
|
await btn.trigger('click') |
||||
|
expect(wrapper.find('input').attributes('type')).toBe('text') |
||||
|
}) |
||||
|
|
||||
|
it('renders label when provided', () => { |
||||
|
const wrapper = mount(PasswordInput, { |
||||
|
props: { modelValue: '', label: 'Test Label' }, |
||||
|
global: { |
||||
|
stubs: { |
||||
|
UInput: { |
||||
|
template: '<input :type="$attrs.type" :disabled="$attrs.disabled" :value="modelValue" @input="$emit(\'update:modelValue\', $event.target.value)" />', |
||||
|
props: ['type', 'disabled', 'modelValue', 'placeholder'], |
||||
|
}, |
||||
|
UButton: { |
||||
|
template: '<button :disabled="$attrs.disabled" @click="$emit(\'click\')"><slot /></button>', |
||||
|
props: ['disabled'], |
||||
|
emits: ['click'], |
||||
|
}, |
||||
|
UFormField: { |
||||
|
template: '<div><slot /></div>', |
||||
|
props: ['label'], |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
}) |
||||
|
expect(wrapper.text()).toContain('Test Label') |
||||
|
}) |
||||
|
|
||||
|
it('disables input when disabled prop is true', () => { |
||||
|
const wrapper = mount(PasswordInput, { |
||||
|
props: { modelValue: '', disabled: true }, |
||||
|
global: { |
||||
|
stubs: { |
||||
|
UInput: { |
||||
|
template: '<input :type="$attrs.type" :disabled="$attrs.disabled" :value="modelValue" @input="$emit(\'update:modelValue\', $event.target.value)" />', |
||||
|
props: ['type', 'disabled', 'modelValue', 'placeholder'], |
||||
|
}, |
||||
|
UButton: { |
||||
|
template: '<button :disabled="$attrs.disabled" @click="$emit(\'click\')"><slot /></button>', |
||||
|
props: ['disabled'], |
||||
|
emits: ['click'], |
||||
|
}, |
||||
|
UFormField: { |
||||
|
template: '<div><slot /></div>', |
||||
|
props: ['label'], |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
}) |
||||
|
expect(wrapper.find('input').element.disabled).toBe(true) |
||||
|
}) |
||||
|
}) |
||||
Loading…
Reference in new issue