Browse Source

feat: add PasswordInput component with show/hide toggle

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
feat/registration-page
npmrun 2 weeks ago
parent
commit
decabd2fe9
  1. 42
      app/components/register/PasswordInput.vue
  2. 104
      app/components/register/__tests__/PasswordInput.test.ts

42
app/components/register/PasswordInput.vue

@ -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>

104
app/components/register/__tests__/PasswordInput.test.ts

@ -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…
Cancel
Save