2 changed files with 108 additions and 0 deletions
@ -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> |
|||
@ -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…
Reference in new issue