Files
malio-layer-ui/app/components/malio/input/InputPassword.test.ts
T
tristan cda0f994ca feat(ui) : prop reserveMessageSpace (défaut true) sur la famille input
Ajoute une prop booléenne reserveMessageSpace (défaut true) aux 10 composants
de la famille input. Par défaut, comportement inchangé (ligne message toujours
rendue avec min-h-[1rem]). À false, la ligne ne prend aucun espace en l'absence
de message, et s'affiche sans min-h quand un message est présent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 16:43:31 +02:00

251 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {describe, expect, it} from 'vitest'
import {mount} from '@vue/test-utils'
import type {DefineComponent} from 'vue'
import { Icon as IconifyIcon } from '@iconify/vue'
import InputPassword from './InputPassword.vue'
type InputPasswordProps = {
id?: string
label?: string
name?: string
autocomplete?: string
modelValue?: string | null
inputClass?: string
labelClass?: string
groupClass?: string
required?: boolean
maxLength?: number | string
minLength?: number | string
disabled?: boolean
readonly?: boolean
hint?: string
error?: string
success?: string
displayIcon?: boolean
reserveMessageSpace?: boolean
}
const InputPasswordForTest = InputPassword as DefineComponent<InputPasswordProps>
const mountComponent = (props: InputPasswordProps = {}) =>
mount(InputPasswordForTest, {
props,
global: {
stubs: {
IconifyIcon: {
template: '<span data-test="icon" v-bind="$attrs" />',
},
},
},
})
describe('MalioInputPassword', () => {
it('renders the initial input value', () => {
const wrapper = mountComponent({modelValue: 'secret123'})
expect(wrapper.get('input').element.value).toBe('secret123')
})
it('renders the label text', () => {
const wrapper = mountComponent({label: 'Mot de passe'})
expect(wrapper.get('label').text()).toBe('Mot de passe')
})
it('affiche l\'astérisque quand required est vrai', () => {
const wrapper = mountComponent({label: 'Champ', required: true})
expect(wrapper.find('[data-test="required-mark"]').exists()).toBe(true)
})
it('n\'affiche pas l\'astérisque par défaut', () => {
const wrapper = mountComponent({label: 'Champ'})
expect(wrapper.find('[data-test="required-mark"]').exists()).toBe(false)
})
it('has type password by default', () => {
const wrapper = mountComponent()
expect(wrapper.get('input').attributes('type')).toBe('password')
})
it('toggles to type text when icon is clicked', async () => {
const wrapper = mountComponent()
await wrapper.get('[data-test="icon"]').trigger('click')
expect(wrapper.get('input').attributes('type')).toBe('text')
})
it('toggles back to password on second click', async () => {
const wrapper = mountComponent()
await wrapper.get('[data-test="icon"]').trigger('click')
await wrapper.get('[data-test="icon"]').trigger('click')
expect(wrapper.get('input').attributes('type')).toBe('password')
})
it('does not render icon when displayIcon is false', () => {
const wrapper = mountComponent({displayIcon: false})
expect(wrapper.find('[data-test="icon"]').exists()).toBe(false)
})
it('renders icon by default', () => {
const wrapper = mountComponent()
expect(wrapper.find('[data-test="icon"]').exists()).toBe(true)
})
it('shows eye-off-outline icon when password is hidden', () => {
const wrapper = mountComponent()
const iconComponent = wrapper.findComponent(IconifyIcon)
expect(iconComponent.props('icon')).toBe('mdi:eye-off-outline')
})
it('shows eye-outline icon when password is visible', async () => {
const wrapper = mountComponent()
await wrapper.get('[data-test="icon"]').trigger('click')
const iconComponent = wrapper.findComponent(IconifyIcon)
expect(iconComponent.props('icon')).toBe('mdi:eye-outline')
})
it('emits update:modelValue on input change', async () => {
const wrapper = mountComponent({modelValue: ''})
await wrapper.get('input').setValue('new password')
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['new password'])
})
it('sets disabled styles when true', () => {
const wrapper = mountComponent({disabled: true})
expect(wrapper.get('input').attributes('disabled')).toBeDefined()
expect(wrapper.get('input').classes()).toContain('cursor-not-allowed')
})
it('sets readonly when true', () => {
const wrapper = mountComponent({readonly: true})
expect(wrapper.get('input').attributes('readonly')).toBeDefined()
})
it('shows error message and styles', () => {
const wrapper = mountComponent({error: 'Mot de passe requis'})
expect(wrapper.get('p.text-m-danger').text()).toBe('Mot de passe requis')
expect(wrapper.get('input').classes()).toContain('border-m-danger')
expect(wrapper.get('input').attributes('aria-invalid')).toBe('true')
})
it('shows error style on icon', () => {
const wrapper = mountComponent({error: 'Error'})
expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-m-danger')
})
it('shows success message and styles', () => {
const wrapper = mountComponent({success: 'Mot de passe valide'})
expect(wrapper.get('p.text-m-success').text()).toBe('Mot de passe valide')
expect(wrapper.get('input').classes()).toContain('border-m-success')
})
it('shows success style on icon', () => {
const wrapper = mountComponent({success: 'Success'})
expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-m-success')
})
it('links label to input via for/id', () => {
const wrapper = mountComponent({id: 'pwd', label: 'Password'})
expect(wrapper.get('input').attributes('id')).toBe('pwd')
expect(wrapper.get('label').attributes('for')).toBe('pwd')
})
it('generates an id when missing and reuses it on label', () => {
const wrapper = mountComponent({label: 'Password'})
const inputId = wrapper.get('input').attributes('id')
expect(inputId?.startsWith('malio-input-password-')).toBe(true)
expect(wrapper.get('label').attributes('for')).toBe(inputId)
})
it('aria-invalid is false when no error', () => {
const wrapper = mountComponent()
expect(wrapper.get('input').attributes('aria-invalid')).toBe('false')
})
it('shows primary icon color on focus', async () => {
const wrapper = mountComponent()
await wrapper.get('input').trigger('focus')
expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-m-primary')
})
it('shows black icon color when filled and unfocused', () => {
const wrapper = mountComponent({modelValue: 'secret'})
expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-black')
})
it('readonly : bordure noire même vide, pas de grow/bleu', () => {
const wrapper = mountComponent({label: 'Champ', readonly: true})
const field = wrapper.get('input')
expect(field.classes()).toContain('border-black')
expect(field.classes()).not.toContain('border-m-muted')
expect(field.classes()).not.toContain('focus:border-m-primary')
expect(field.classes()).not.toContain('grow-height')
})
it('readonly vide : label gris, pas de bleu', () => {
const wrapper = mountComponent({label: 'Champ', readonly: true})
expect(wrapper.get('label').classes()).not.toContain('peer-focus:text-m-primary')
expect(wrapper.get('label').classes()).toContain('text-m-muted')
})
it('readonly vide : icône en text-m-muted', () => {
const wrapper = mountComponent({label: 'Champ', readonly: true})
expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-m-muted')
})
it('readonly rempli : label noir et icône noire', () => {
const wrapper = mountComponent({label: 'Champ', readonly: true, modelValue: 'secret'})
expect(wrapper.get('label').classes()).toContain('text-black')
expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-black')
})
it('readonly : eye toggle reste cliquable', async () => {
const wrapper = mountComponent({label: 'Champ', readonly: true})
await wrapper.get('[data-test="icon"]').trigger('click')
expect(wrapper.get('input').attributes('type')).toBe('text')
})
it('réserve lespace message par défaut même sans message', () => {
const wrapper = mountComponent({label: 'Champ'})
const msg = wrapper.find('[id$="-describedby"]')
expect(msg.exists()).toBe(true)
expect(msg.classes()).toContain('min-h-[1rem]')
})
it('reserveMessageSpace=false sans message : pas de ligne réservée', () => {
const wrapper = mountComponent({label: 'Champ', reserveMessageSpace: false})
expect(wrapper.find('[id$="-describedby"]').exists()).toBe(false)
})
it('reserveMessageSpace=false avec message : ligne rendue sans min-h', () => {
const wrapper = mountComponent({label: 'Champ', reserveMessageSpace: false, error: 'Erreur'})
const msg = wrapper.find('[id$="-describedby"]')
expect(msg.exists()).toBe(true)
expect(msg.classes()).not.toContain('min-h-[1rem]')
})
})