Browse Source

feat: add CaptchaField component with v-model and loading/error states

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
feat/registration-page
npmrun 2 weeks ago
parent
commit
556ba475ef
  1. 46
      app/components/register/CaptchaField.vue
  2. 62
      app/components/register/__tests__/CaptchaField.test.ts

46
app/components/register/CaptchaField.vue

@ -0,0 +1,46 @@
<script setup lang="ts">
defineProps<{
svg: string
loading: boolean
modelValue: string
}>()
const emit = defineEmits<{
refresh: []
'update:modelValue': [value: string]
}>()
</script>
<template>
<UFormField label="Captcha" required>
<div v-if="svg" class="flex gap-2 items-start">
<!-- eslint-disable-next-line vue/no-v-html -->
<div
class="flex-1 h-10 border rounded-md overflow-hidden bg-[#f8f9fa]"
role="img"
aria-label="CAPTCHA verification code"
v-html="svg"
/>
<UButton
variant="ghost"
color="neutral"
:icon="'i-lucide-refresh-cw'"
:class="{ 'animate-spin': loading }"
:disabled="loading"
square
aria-label="刷新验证码"
@click="emit('refresh')"
/>
</div>
<div v-else class="text-sm text-red-500">
验证码加载失败
<button class="underline" @click="emit('refresh')">点此重试</button>
</div>
<UInput
:model-value="modelValue"
placeholder="Enter the code above"
class="mt-2"
@update:model-value="emit('update:modelValue', $event)"
/>
</UFormField>
</template>

62
app/components/register/__tests__/CaptchaField.test.ts

@ -0,0 +1,62 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import CaptchaField from '../CaptchaField.vue'
const stubs = {
UInput: {
template: '<input :type="type" :disabled="disabled" :value="modelValue" @input="$emit(\'update:modelValue\', $event.target.value)" />',
props: ['type', 'disabled', 'modelValue', 'placeholder'],
},
UButton: {
template: '<button :disabled="disabled" @click="$emit(\'click\')"><slot /></button>',
props: ['disabled', 'icon', 'variant', 'color', 'square'],
emits: ['click'],
},
UFormField: {
template: '<div><span>{{ label }}</span><slot /></div>',
props: ['label', 'required'],
},
}
const defaultProps = { svg: '<svg></svg>', loading: false, modelValue: '' }
function mountComponent(overrides = {}) {
return mount(CaptchaField, {
props: { ...defaultProps, ...overrides },
global: { stubs },
})
}
describe('CaptchaField', () => {
it('renders SVG via v-html', () => {
const svg = '<svg><circle cx="10" cy="10" r="5"/></svg>'
const wrapper = mountComponent({ svg })
expect(wrapper.html()).toContain('<circle')
})
it('emits refresh when button clicked', async () => {
const wrapper = mountComponent()
const btn = wrapper.find('button')
await btn.trigger('click')
expect(wrapper.emitted('refresh')).toBeTruthy()
})
it('disables refresh button when loading', () => {
const wrapper = mountComponent({ loading: true })
const btn = wrapper.find('button')
expect(btn.attributes('disabled')).toBeDefined()
})
it('renders error state when svg is empty', () => {
const wrapper = mountComponent({ svg: '' })
expect(wrapper.text()).toContain('加载失败')
})
it('emits update:modelValue when input changes', async () => {
const wrapper = mountComponent()
const input = wrapper.find('input')
await input.setValue('abc')
expect(wrapper.emitted('update:modelValue')).toBeTruthy()
expect(wrapper.emitted('update:modelValue')![0]).toEqual(['abc'])
})
})
Loading…
Cancel
Save