import type { Meta, StoryObj } from '@storybook/vue3' import { fn, userEvent, within } from '@storybook/test' import { ref, watch } from 'vue' import Captcha from '../Captcha.vue' import { VBtn, VCard, VForm } from 'vuetify/components' import SyForm from '../../Customs/SyForm/SyForm.vue' import { getValidationDocumentation } from '@/composables/unifyValidation/documentationValidationProps' const meta: Meta = { title: 'Composants/Formulaires/Captcha/Validation', component: Captcha, argTypes: { ...getValidationDocumentation('string'), }, parameters: { layout: 'centered', docs: { description: { component: 'Exemples de validation pour le composant Captcha.', }, }, controls: { exclude: ['onUpdate:modelValue', 'onUpdate:type', 'onImageError', 'onAudioError', 'onCreationError'] }, }, args: { 'onUpdate:modelValue': fn(), 'onUpdate:type': fn(), 'onImageError': fn(), 'onAudioError': fn(), 'onCreationError': fn(), 'readonly': false, 'disabled': false, 'required': false, 'isValidateOnBlur': true, 'showSuccessMessages': true, 'disableErrorHandling': false, 'customRules': [], 'customWarningRules': [], 'customSuccessRules': [], 'errorMessages': null, 'warningMessages': null, 'successMessages': null, 'hasError': false, 'hasWarning': false, 'hasSuccess': false, 'useVuetifyValidation': false, 'maxErrors': 1, 'urlCreate': 'https://free.mockerapi.com/mock/0adac32b-e832-4553-aa7f-0011b7f35f0c', 'urlGetImage': '/captcha/captcha.png', 'urlGetAudio': '/captcha/captcha.mp3', 'type': 'image', 'modelValue': 'abc', }, } export default meta type StoryArgs = { modelValue?: string } & Record type Story = StoryObj export const WithError: Story = { parameters: { a11y: { disable: true, }, docs: { description: { story: 'Validation bloquante via `customRules` (message d\'erreur personnalisé).', }, }, sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` import { ref } from 'vue' import { Captcha } from '@cnamts/synapse' const modelValue = ref('ytq') `, }, ], }, render: (args) => { return { components: { Captcha, VCard }, setup() { const captchaValue = ref(args.modelValue) watch(() => args.modelValue, () => { captchaValue.value = args.modelValue }) return { args, captchaValue } }, template: ` `, } }, args: { modelValue: 'ytq', // Validation à la saisie pour que l'état (erreur/succès) se mette à jour // immédiatement quand on modifie le champ, sans attendre le blur. isValidateOnBlur: false, customRules: [ { type: 'custom', options: { validate: (v: unknown) => { return (String(v || '')).length >= 6 }, message: 'Le captcha doit contenir au moins 6 caractères.', successMessage: 'Le captcha contient au moins 6 caractères.', }, }, ], }, play: async ({ canvasElement }) => { const input = within(canvasElement).getByRole('textbox') await userEvent.clear(input) await userEvent.type(input, 'ytq') input.blur() }, } export const WithWarning: Story = { ...WithError, parameters: { a11y: { disable: true, }, docs: { description: { story: 'Validation non bloquante via `customWarningRules` (message d\'avertissement personnalisé).', }, }, sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` import { ref } from 'vue' import { Captcha } from '@cnamts/synapse' const modelValue = ref('ytq') `, }, ], }, args: { ...WithError.args, customRules: [], customWarningRules: [ { type: 'custom', options: { validate: (v: unknown) => { return (String(v || '')).length >= 6 }, warningMessage: 'Le captcha devrait idéalement contenir au moins 6 caractères.', }, }, ], }, } export const WithSuccess: Story = { ...WithError, parameters: { a11y: { disable: true, }, docs: { description: { story: 'Validation de succès via `customSuccessRules` (message de succès personnalisé).', }, }, sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` import { ref } from 'vue' import { Captcha } from '@cnamts/synapse' const modelValue = ref('ytqZNq') `, }, ], }, args: { ...WithError.args, customRules: [ { type: 'custom', options: { validate: (v: unknown) => (String(v || '')).length >= 6, message: 'Le captcha doit contenir au moins 6 caractères.', }, }, ], customSuccessRules: [ { type: 'custom', options: { validate: (v: unknown) => (String(v || '')).length >= 6, successMessage: 'Le captcha est correctement renseigné.', }, }, ], }, play: async ({ canvasElement }) => { const input = within(canvasElement).getByRole('textbox') await userEvent.clear(input) await userEvent.type(input, 'ytqZNq') input.blur() }, } export const ValidateOnInput: Story = { parameters: { a11y: { disable: true, }, docs: { description: { story: 'Validation déclenchée à chaque frappe via `isValidateOnBlur=false`.', }, }, sourceCode: [ { name: 'Template', code: ` `, }, { name: 'Script', code: ` import { ref } from 'vue' import { Captcha } from '@cnamts/synapse' const value = ref('abc') const verifyCaptcha = () => { // call the API to verify the captcha and return the response } `, }, ], }, render: (args) => { return { components: { Captcha, VCard }, setup() { const captchaValue = ref(args.modelValue) watch(() => args.modelValue, () => { captchaValue.value = args.modelValue }) const verifyCaptcha = () => { if (captchaValue.value === 'ytqZNq' || captchaValue.value === '941335') { return Promise.resolve({ response: { data: { message: 'Success' } } }) } return Promise.reject({ response: { data: { message: 'Le captcha est incorrect' } } }) } return { args, captchaValue, verifyCaptcha } }, template: `

La validation se déclenche à chaque modification de la valeur.

`, } }, play: async ({ canvasElement }) => { const input = within(canvasElement).getByRole('textbox') await userEvent.clear(input) // Saisie de la bonne réponse : la validation se met à jour à chaque frappe // (erreur tant que < 6 caractères, puis succès une fois le bon captcha saisi). await userEvent.type(input, 'ytqZNq') }, } export const ExternalMessages: Story = { parameters: { a11y: { disable: true, }, docs: { description: { story: 'Messages injectés par le parent via `errorMessages`, `warningMessages` et `successMessages`.', }, }, sourceCode: [ { name: 'Template', code: `
Simuler une erreur Simuler un avertissement Simuler un succès Réinitialiser
`, }, ], }, render: args => ({ components: { Captcha, VBtn, VCard }, setup() { const captchaValue = ref(args.modelValue) watch(() => args.modelValue, (newValue) => { captchaValue.value = newValue }) const errorMessages = ref(null) const warningMessages = ref(null) const successMessages = ref(null) const verifyCaptcha = () => { if (captchaValue.value === 'ytqZNq' || captchaValue.value === '941335') { return Promise.resolve({ response: { data: { message: 'Success' } } }) } return Promise.reject({ response: { data: { message: 'Le captcha est incorrect' } } }) } function setError() { errorMessages.value = ['Ce captcha est déjà utilisé'] warningMessages.value = null successMessages.value = null } function setWarning() { errorMessages.value = null warningMessages.value = ['Ce captcha pourrait être plus lisible'] successMessages.value = null } function setSuccess() { errorMessages.value = null warningMessages.value = null successMessages.value = ['Captcha accepté'] } function reset() { errorMessages.value = null warningMessages.value = null successMessages.value = null } return { args, captchaValue, errorMessages, warningMessages, successMessages, verifyCaptcha, setError, setWarning, setSuccess, reset } }, template: `

Les messages sont fournis directement par le parent sans déclencher de règle de validation.

Simuler une erreur Simuler un avertissement Simuler un succès Réinitialiser
`, }), } export const DisableErrorHandling: Story = { parameters: { a11y: { disable: true, }, docs: { description: { story: 'Désactivation de la gestion des erreurs via `disableErrorHandling`.', }, }, sourceCode: [ { name: 'Template', code: ``, }, { name: 'Script', code: ``, }, ], }, render: args => ({ components: { Captcha, VCard }, setup() { const value1 = ref('') const value2 = ref('') const verifyCaptcha = (captchaValue: string | null) => { if (captchaValue === 'ytqZNq' || captchaValue === '941335') { return Promise.resolve({ response: { data: { message: 'Success' } } }) } return Promise.reject({ response: { data: { message: 'Le captcha est incorrect' } } }) } const customRules = [ { type: 'custom', options: { validate: (value: string) => { if (!value || value.trim().length === 0) { return false } return true }, message: 'Ce champ est requis.', fieldIdentifier: 'captcha', }, }, { type: 'custom', options: { validate: async (value: string) => { try { const r = await verifyCaptcha(value) return r.response.data.message === 'Success' } catch { return false } }, message: 'Le captcha est incorrect.', }, }, ] return { args, value1, value2, customRules } }, template: `

Cette démonstration compare un Captcha standard et un avec disableErrorHandling=true.

Validation normale

Sans gestion d'erreurs

Instructions :

  1. Cliquez dans un champ puis en dehors pour déclencher la validation
  2. Le champ de gauche affichera une erreur requise, mais pas celui de droite
`, }), } export const SyFormValidation: Story = { parameters: { a11y: { disable: true, }, docs: { description: { story: 'Intégration avec `SyForm` pour soumettre un Captcha validé par règles personnalisées.', }, }, sourceCode: [ { name: 'Template', code: ``, }, { name: 'Script', code: ``, }, ], }, render: args => ({ components: { Captcha, VBtn, SyForm }, setup() { const captchaValue = ref(args.modelValue) watch(() => args.modelValue, (newValue) => { captchaValue.value = newValue }) const verifyCaptcha = () => { if (captchaValue.value === 'ytqZNq' || captchaValue.value === '941335') { return Promise.resolve({ response: { data: { message: 'Success' } } }) } return Promise.reject({ response: { data: { message: 'Le captcha est incorrect' } } }) } const customRules = [ { type: 'custom', options: { validate: (value: string) => String(value || '').length >= 6, message: 'Le captcha doit contenir au moins 6 caractères.', fieldIdentifier: 'captcha', }, }, { type: 'custom', options: { validate: async () => { try { const r = await verifyCaptcha() return r.response.data.message === 'Success' } catch { return false } }, message: 'Le captcha est incorrect', fieldIdentifier: 'captcha', successMessage: 'Le captcha est correct', }, }, ] function handleSubmit(e: { isValid: boolean }) { alert(e.isValid ? 'Valeur valide !' : 'Veuillez corriger les erreurs.') } return { args, captchaValue, customRules, handleSubmit } }, template: `

Il faut privilégier l'utilisation de SyForm pour bénéficier de l'intégration de validation.

Valider
`, }), } export const VFormValidation: Story = { parameters: { a11y: { disable: true, }, docs: { description: { story: 'Validation déclenchée à la soumission du formulaire via l\'API exposée `validate()` du Captcha.', }, }, sourceCode: [ { name: 'Template', code: ``, }, { name: 'Script', code: ``, }, ], }, render: args => ({ components: { Captcha, VBtn, VForm }, setup() { const value = ref(args.modelValue) watch(() => args.modelValue, (newValue) => { value.value = newValue }) const captchaRef = ref() const verifyCaptcha = () => { if (value.value === 'ytqZNq' || value.value === '941335') { return Promise.resolve({ response: { data: { message: 'Success' } } }) } return Promise.reject({ response: { data: { message: 'Le captcha est incorrect' } } }) } const customRules = [ { type: 'custom', options: { validate: (captchaValue: string) => String(captchaValue || '').length >= 6, message: 'Le captcha doit contenir au moins 6 caractères.', fieldIdentifier: 'captcha', }, }, { type: 'custom', options: { validate: async () => { try { const r = await verifyCaptcha() return r.response.data.message === 'Success' } catch { return false } }, message: 'Le captcha est incorrect', successMessage: 'Le captcha est correct', fieldIdentifier: 'captcha', }, }, ] async function handleSubmit() { if (captchaRef.value) { const result = await captchaRef.value.validateOnSubmit() alert(result ? 'Valeur valide !' : 'Veuillez corriger les erreurs.') } } return { args, value, captchaRef, customRules, handleSubmit } }, template: `

Il faut privilégier l'utilisation du composant SyForm pour déclencher la validation à la soumission.

Valider
`, }), } export const SyFormVuetifyValidation: Story = { parameters: { a11y: { disable: true, }, docs: { description: { story: 'Validation native Vuetify (`useVuetifyValidation=true` + `rules`) intégrée automatiquement à un `SyForm`.', }, }, sourceCode: [ { name: 'Template', code: ``, }, { name: 'Script', code: ``, }, ], }, render: args => ({ components: { Captcha, VBtn, SyForm }, setup() { const captchaValue = ref(args.modelValue) watch(() => args.modelValue, (newValue) => { captchaValue.value = newValue }) const verifyCaptcha = () => { if (captchaValue.value === 'ytqZNq' || captchaValue.value === '941335') { return Promise.resolve({ response: { data: { message: 'Success' } } }) } return Promise.reject({ response: { data: { message: 'Le captcha est incorrect' } } }) } const rules = [ (value: string) => !!value || 'Ce champ est requis', (value: string) => String(value || '').length >= 6 || 'Le captcha doit contenir au moins 6 caractères', async () => { try { const r = await verifyCaptcha() return r.response.data.message === 'Success' || 'Le captcha est incorrect' } catch { return 'Le captcha est incorrect' } }, ] function handleSubmit(e: { isValid: boolean }) { alert(e.isValid ? 'Valeur valide !' : 'Veuillez corriger les erreurs.') } return { args, captchaValue, rules, handleSubmit } }, template: `

Validation Vuetify native intégrée au SyForm via useVuetifyValidation.

Valider
`, }), } export const VFormVuetifyValidation: Story = { parameters: { a11y: { disable: true, }, docs: { description: { story: 'Validation de style Vuetify avec `useVuetifyValidation=true` et des règles natives.', }, }, sourceCode: [ { name: 'Template', code: ``, }, { name: 'Script', code: ``, }, ], }, render: args => ({ components: { Captcha, VBtn, VForm }, setup() { const captchaValue = ref(args.modelValue) watch(() => args.modelValue, (newValue) => { captchaValue.value = newValue }) const rules = [ (value: string) => !!value || 'Ce champ est requis', (value: string) => String(value || '').length >= 6 || 'Le captcha doit contenir au moins 6 caractères', async () => { try { const r = await verifyCaptcha() return r.response.data.message === 'Success' || 'Le captcha est incorrect' } catch { return 'Le captcha est incorrect' } }, ] const verifyCaptcha = () => { if (captchaValue.value === 'ytqZNq' || captchaValue.value === '941335') { return Promise.resolve({ response: { data: { message: 'Success' } } }) } return Promise.reject({ response: { data: { message: 'Le captcha est incorrect' } } }) } async function handleSubmit(e: Promise<{ valid: boolean }>) { const result = await e alert(result.valid ? 'Valeur valide !' : 'Veuillez corriger les erreurs.') } return { args, captchaValue, rules, handleSubmit } }, template: `

Les règles suivent le contrat Vuetify natif: (value) => true | 'message'.

Valider
`, }), }