import { describe, expect, it } from 'vitest'
import { defineComponent } from 'vue'
import { mount } from '@vue/test-utils'
import Captcha from '../Captcha.vue'
const CaptchaBaseStub = {
name: 'CaptchaBase',
emits: ['update:modelValue', 'create-captcha:init', 'create-captcha:success', 'create-captcha:error'],
template: `
`,
}
const CaptchaFormStub = {
name: 'CaptchaForm',
emits: ['update:modelValue'],
template: '',
}
const CaptchaFormWithFocusStub = defineComponent({
name: 'CaptchaForm',
props: {
errorMessages: { type: Array, default: () => [] },
},
emits: ['update:modelValue', 'focus', 'blur'],
template: '',
})
describe('Captcha behavior', () => {
it('emits update:type when captcha type changes from CaptchaBase', async () => {
const wrapper = mount(Captcha, {
props: {
urlCreate: '/captcha/captcha.json',
urlGetImage: '/captcha/captcha.png',
urlGetAudio: '/captcha/captcha.mp3',
type: 'image',
},
global: {
stubs: {
CaptchaInformation: true,
CaptchaBase: CaptchaBaseStub,
CaptchaImg: true,
CaptchaAlert: true,
CaptchaBtn: true,
CaptchaHelpdesk: true,
CaptchaForm: CaptchaFormStub,
volumeUp: true,
SyIcon: true,
VBtn: true,
},
},
})
await wrapper.find('.change-type').trigger('click')
expect(wrapper.emitted('update:type')).toBeTruthy()
expect(wrapper.emitted('update:type')?.[0]).toEqual(['audio'])
})
it('clears captcha text when a new captcha is initialized', async () => {
const wrapper = mount(Captcha, {
props: {
urlCreate: '/captcha/captcha.json',
urlGetImage: '/captcha/captcha.png',
urlGetAudio: '/captcha/captcha.mp3',
},
global: {
stubs: {
CaptchaInformation: true,
CaptchaBase: CaptchaBaseStub,
CaptchaImg: true,
CaptchaAlert: true,
CaptchaBtn: true,
CaptchaHelpdesk: true,
CaptchaForm: CaptchaFormStub,
volumeUp: true,
SyIcon: true,
VBtn: true,
},
},
})
// Complete the initial captcha load so subsequent refreshes work
await wrapper.find('.captcha-success').trigger('click')
const form = wrapper.findComponent(CaptchaFormStub)
await form.vm.$emit('update:modelValue', 'captcha-text')
await wrapper.find('.refresh-captcha').trigger('click')
const modelEvents = wrapper.emitted('update:modelValue') || []
expect(modelEvents.length).toBeGreaterThanOrEqual(2)
expect(modelEvents[0]).toEqual(['captcha-text'])
expect(modelEvents[modelEvents.length - 1]).toEqual([''])
})
it('does not clear validation errors when the first captcha loads after a blur', async () => {
const commonStubs = {
CaptchaInformation: true,
CaptchaBase: CaptchaBaseStub,
CaptchaImg: true,
CaptchaAlert: true,
CaptchaBtn: true,
CaptchaHelpdesk: true,
CaptchaForm: CaptchaFormWithFocusStub,
volumeUp: true,
SyIcon: true,
VBtn: true,
}
const wrapper = mount(Captcha, {
props: {
urlCreate: '/captcha/captcha.json',
urlGetImage: '/captcha/captcha.png',
urlGetAudio: '/captcha/captcha.mp3',
customRules: [
{
type: 'custom' as const,
options: {
validate: (v: unknown) => v ? true : 'Le captcha est requis',
},
},
],
},
global: { stubs: commonStubs },
})
// Simulate: user focuses and blurs without entering a value (triggers validation)
const form = wrapper.findComponent(CaptchaFormWithFocusStub)
await form.vm.$emit('focus')
await form.vm.$emit('blur')
// Wait for async validation to complete
await wrapper.vm.$nextTick()
await wrapper.vm.$nextTick()
// Errors should now be set
const errorsBeforeSuccess = form.props('errorMessages') as string[]
expect(errorsBeforeSuccess).toContain('Le captcha est requis')
// Simulate: first captcha load completes (race condition – used to clear validation)
await wrapper.find('.captcha-success').trigger('click')
await wrapper.vm.$nextTick()
// Errors must still be present – first create-captcha:success must NOT clear validation
const errorsAfterFirstSuccess = form.props('errorMessages') as string[]
expect(errorsAfterFirstSuccess).toContain('Le captcha est requis')
})
it('clears validation errors when the captcha is refreshed after a blur', async () => {
const commonStubs = {
CaptchaInformation: true,
CaptchaBase: CaptchaBaseStub,
CaptchaImg: true,
CaptchaAlert: true,
CaptchaBtn: true,
CaptchaHelpdesk: true,
CaptchaForm: CaptchaFormWithFocusStub,
volumeUp: true,
SyIcon: true,
VBtn: true,
}
const wrapper = mount(Captcha, {
props: {
urlCreate: '/captcha/captcha.json',
urlGetImage: '/captcha/captcha.png',
urlGetAudio: '/captcha/captcha.mp3',
customRules: [
{
type: 'custom' as const,
options: {
validate: (v: unknown) => v ? true : 'Le captcha est requis',
},
},
],
},
global: { stubs: commonStubs },
})
// Complete the initial load first
await wrapper.find('.captcha-success').trigger('click')
await wrapper.vm.$nextTick()
// User blurs without a value → error shown
const form = wrapper.findComponent(CaptchaFormWithFocusStub)
await form.vm.$emit('focus')
await form.vm.$emit('blur')
await wrapper.vm.$nextTick()
await wrapper.vm.$nextTick()
expect(form.props('errorMessages') as string[]).toContain('Le captcha est requis')
// User asks for a new captcha (init + success)
await wrapper.find('.refresh-captcha').trigger('click')
await wrapper.find('.captcha-success').trigger('click')
await wrapper.vm.$nextTick()
// Errors should be cleared now (this is the intended behaviour for a refresh)
expect(form.props('errorMessages') as string[]).toEqual([])
})
})