diff --git a/.playground/pages/composant/inputText.vue b/.playground/pages/composant/inputText.vue
index 0f501a6..e1d478f 100644
--- a/.playground/pages/composant/inputText.vue
+++ b/.playground/pages/composant/inputText.vue
@@ -32,6 +32,10 @@
disabled
label="Champ désactivé"
/>
+
diff --git a/app/components/malio/Input.test.ts b/app/components/malio/Input.test.ts
index aefc9fb..c067744 100644
--- a/app/components/malio/Input.test.ts
+++ b/app/components/malio/Input.test.ts
@@ -1,23 +1,319 @@
-import { describe, expect, it } from 'vitest'
-import { mount } from '@vue/test-utils'
-import Input from './Input.vue'
+import {describe, expect, it} from 'vitest'
+import {config, mount} from '@vue/test-utils'
+import type {DefineComponent} from 'vue'
+import Input from './InputText.vue'
+
+
+type InputProps = {
+ id?: string
+ label?: string
+ name?: string
+ autocomplete?: string
+ modelValue?: string | null
+ textSize?: string
+ labelClass?: string
+ required?: boolean
+ maxLength?: number | string
+ minLength?: number | string
+ disabled?: boolean
+ readonly?: boolean
+ hint?: string
+ error?: string
+ success?: string
+ iconName?: string
+ iconSize?: string | number
+ iconColor?: string
+}
+
+const InputForTest = Input as DefineComponent
+const iconStub = {
+ template: '',
+}
+config.global.stubs = {
+ ...(config.global.stubs ?? {}),
+ Icon: iconStub,
+}
describe('MalioInput', () => {
- it('affiche la valeur initiale', () => {
- const wrapper = mount(Input, {
- props: { modelValue: 'hello' },
+ // Props de base: valeur, label, name, id, autocomplete
+ it('renders the initial input value', () => {
+ const wrapper = mount(InputForTest, {
+ props: {modelValue: 'initialValueTest'},
})
-
- expect(wrapper.get('input').element.value).toBe('hello')
+ expect(wrapper.get('input').element.value).toBe('initialValueTest')
})
- it('emet update:modelValue au changement', async () => {
- const wrapper = mount(Input, {
- props: { modelValue: '' },
+ it('renders the label text', () => {
+ const wrapper = mount(InputForTest, {
+ props: {label: 'labelTest'},
})
+ expect(wrapper.get('label').text()).toBe('labelTest')
+ })
+ it('applies the name attribute', () => {
+ const wrapper = mount(InputForTest, {
+ props: {name: 'nameTest'},
+ })
+ expect(wrapper.get('input').attributes('name')).toBe('nameTest')
+ })
+
+ it('uses provided id on input and label', () => {
+ const wrapper = mount(InputForTest, {
+ props: {id: 'custom-id', label: 'Label'},
+ })
+ expect(wrapper.get('input').attributes('id')).toBe('custom-id')
+ expect(wrapper.get('label').attributes('for')).toBe('custom-id')
+ })
+
+ it('applies a different size of rounded', () => {
+ const wrapper = mount(InputForTest, {
+ props: {rounded: 'rounded-md'},
+ })
+ expect(wrapper.get('input').classes()).toContain('rounded-md')
+ })
+
+ it('generates an id when missing and reuses it on label', () => {
+ const wrapper = mount(InputForTest, {
+ props: {label: 'Label'},
+ })
+ const inputId = wrapper.get('input').attributes('id')
+ expect(inputId).toBeDefined()
+ expect(inputId?.startsWith('malio-input-text-')).toBe(true)
+ expect(wrapper.get('label').attributes('for')).toBe(inputId)
+ })
+
+ it('applies the autocomplete attribute', () => {
+ const wrapper = mount(InputForTest, {
+ props: {autocomplete: 'autocompleteTest'},
+ })
+ expect(wrapper.get('input').attributes('autocomplete')).toBe('autocompleteTest')
+ })
+
+ // États HTML: required, readonly, disabled
+ it('does not set required when false', () => {
+ const wrapper = mount(InputForTest, {
+ props: {required: false},
+ })
+ expect(wrapper.get('input').attributes('required')).toBeUndefined()
+ })
+
+ it('sets required when true', () => {
+ const wrapper = mount(InputForTest, {
+ props: {required: true},
+ })
+ expect(wrapper.get('input').attributes('required')).toBeDefined()
+ })
+
+ it('does not set readonly when false', () => {
+ const wrapper = mount(InputForTest, {
+ props: {readonly: false},
+ })
+ expect(wrapper.get('input').attributes('readonly')).toBeUndefined()
+ })
+
+ it('sets readonly when true', () => {
+ const wrapper = mount(InputForTest, {
+ props: {readonly: true},
+ })
+ expect(wrapper.get('input').attributes('readonly')).toBeDefined()
+ })
+
+ it('does not set disabled and keeps text cursor when false', () => {
+ const wrapper = mount(InputForTest, {
+ props: {disabled: false},
+ })
+ expect(wrapper.get('input').attributes('disabled')).toBeUndefined()
+ expect(wrapper.get('input').classes()).toContain('cursor-text')
+ })
+
+ it('sets disabled styles when true', () => {
+ const wrapper = mount(InputForTest, {
+ props: {disabled: true},
+ })
+ expect(wrapper.get('input').attributes('disabled')).toBeDefined()
+ expect(wrapper.get('input').classes()).toContain('cursor-not-allowed')
+ expect(wrapper.get('input').classes()).toContain('text-black/60')
+ })
+
+ // Émission d'événements
+ it('emits update:modelValue on input change', async () => {
+ const wrapper = mount(InputForTest, {
+ props: {modelValue: ''},
+ })
await wrapper.get('input').setValue('new value')
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['new value'])
})
+
+ // Contraintes et classes de texte
+ it('applies maxLength to input', () => {
+ const wrapper = mount(InputForTest, {
+ props: {maxLength: 25},
+ })
+ expect(wrapper.get('input').attributes('maxlength')).toBe('25')
+ })
+
+ it('applies minLength to input', () => {
+ const wrapper = mount(InputForTest, {
+ props: {minLength: 25},
+ })
+ expect(wrapper.get('input').attributes('minlength')).toBe('25')
+ })
+
+ it('applies textSize class on label', () => {
+ const wrapper = mount(InputForTest, {
+ props: {label: 'Label', textLabel: 'text-sm'},
+ })
+ expect(wrapper.get('label').classes()).toContain('text-sm')
+ })
+
+ // États visuels: erreur et succès
+ it('shows error message without label and icon', () => {
+ const wrapper = mount(InputForTest, {
+ props: {error: 'Error message test'},
+ })
+ expect(wrapper.get('p.text-m-error').text()).toBe('Error message test')
+ expect(wrapper.get('input').classes()).toContain('border-m-error')
+ expect(wrapper.get('p').classes()).toContain('text-m-error')
+ })
+
+ it('shows error message with label and without icon', () => {
+ const wrapper = mount(InputForTest, {
+ props: {error: 'Error message test', label: 'Error message'},
+ })
+ expect(wrapper.get('p.text-m-error').text()).toBe('Error message test')
+ expect(wrapper.get('input').classes()).toContain('border-m-error')
+ expect(wrapper.get('label').classes()).toContain('text-m-error')
+ expect(wrapper.get('label').classes()).toContain('text-m-error')
+ expect(wrapper.get('label').classes()).toContain('text-m-error')
+ expect(wrapper.get('p').classes()).toContain('text-m-error')
+
+ })
+
+ it('shows error message with label and icon', () => {
+ const wrapper = mount(InputForTest, {
+ props: {error: 'Error message test', label: 'Error message', iconName: 'mdi:key-outline'},
+ })
+ expect(wrapper.get('p.text-m-error').text()).toBe('Error message test')
+ expect(wrapper.get('input').classes()).toContain('border-m-error')
+ expect(wrapper.get('label').classes()).toContain('text-m-error')
+ expect(wrapper.get('label').classes()).toContain('text-m-error')
+ expect(wrapper.get('label').classes()).toContain('text-m-error')
+ expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-m-error')
+ expect(wrapper.get('p').classes()).toContain('text-m-error')
+
+ })
+
+ it('shows error message with icon and without label', () => {
+ const wrapper = mount(InputForTest, {
+ props: {error: 'Error message test', iconName: 'mdi:key-outline'},
+ })
+ expect(wrapper.get('p.text-m-error').text()).toBe('Error message test')
+ expect(wrapper.get('input').classes()).toContain('border-m-error')
+ expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-m-error')
+ })
+
+ it('shows success message without label and icon', () => {
+ const wrapper = mount(InputForTest, {
+ props: {success: 'Success message test'},
+ })
+ expect(wrapper.get('p.text-m-success').text()).toBe('Success message test')
+ expect(wrapper.get('input').classes()).toContain('border-m-success')
+ })
+
+ it('shows success message with label and without icon', () => {
+ const wrapper = mount(InputForTest, {
+ props: {success: 'Success message test', label: 'Success message'},
+ })
+ expect(wrapper.get('p.text-m-success').text()).toBe('Success message test')
+ expect(wrapper.get('input').classes()).toContain('border-m-success')
+ expect(wrapper.get('label').classes()).toContain('text-m-success')
+ expect(wrapper.get('label').classes()).toContain('text-m-success')
+ expect(wrapper.get('label').classes()).toContain('text-m-success')
+ })
+
+ it('shows success message with label and icon', () => {
+ const wrapper = mount(InputForTest, {
+ props: {success: 'Success message test', label: 'Success message', iconName: 'mdi:key-outline'},
+ })
+ expect(wrapper.get('p.text-m-success').text()).toBe('Success message test')
+ expect(wrapper.get('input').classes()).toContain('border-m-success')
+ expect(wrapper.get('label').classes()).toContain('text-m-success')
+ expect(wrapper.get('label').classes()).toContain('text-m-success')
+ expect(wrapper.get('label').classes()).toContain('text-m-success')
+ expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-m-success')
+ })
+
+ it('shows success message with icon and without label', () => {
+ const wrapper = mount(InputForTest, {
+ props: {success: 'Success message test', iconName: 'mdi:key-outline'},
+ })
+ expect(wrapper.get('p.text-m-success').text()).toBe('Success message test')
+ expect(wrapper.get('input').classes()).toContain('border-m-success')
+ expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-m-success')
+ })
+
+ it('prioritizes error over success when both are provided', () => {
+ const wrapper = mount(InputForTest, {
+ props: {
+ error: 'Error message test',
+ success: 'Success message test',
+ },
+ })
+ expect(wrapper.find('p.text-m-error').exists()).toBe(true)
+ expect(wrapper.get('p.text-m-error').text()).toBe('Error message test')
+ expect(wrapper.find('p.text-m-success').exists()).toBe(false)
+ expect(wrapper.get('input').classes()).toContain('border-m-error')
+ expect(wrapper.get('input').classes()).not.toContain('border-m-success')
+ })
+
+ // Aide et classes de label
+ it('shows hint message', () => {
+ const wrapper = mount(InputForTest, {
+ props: {hint: 'Hint message test'},
+ })
+ expect(wrapper.get('p.text-m-muted').text()).toBe('Hint message test')
+ })
+
+ it('applies labelClass on label', () => {
+ const wrapper = mount(InputForTest, {
+ props: {label: 'Label', labelClass: 'text-red-500'},
+ })
+ expect(wrapper.get('label').classes()).toContain('text-red-500')
+ })
+
+ it('does not render label when label prop is missing', () => {
+ const wrapper = mount(InputForTest, {
+ props: {labelClass: 'text-red-500'},
+ })
+
+ expect(wrapper.find('label').exists()).toBe(false)
+ })
+
+ // Icône : rendu et options
+ it('renders icon with default positioning and muted color', () => {
+ const wrapper = mount(InputForTest, {
+ props: {iconName: 'mdi:key-outline'},
+ })
+ expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-m-muted')
+ expect(wrapper.get('[data-test="icon"]').classes()).toContain('pointer-events-none')
+ expect(wrapper.get('[data-test="icon"]').classes()).toContain('absolute')
+ expect(wrapper.get('[data-test="icon"]').classes()).toContain('right-2')
+ expect(wrapper.get('[data-test="icon"]').classes()).toContain('top-1/2')
+ expect(wrapper.get('[data-test="icon"]').classes()).toContain('-translate-y-1/2')
+ })
+
+ it('passes icon size prop to icon component', () => {
+ const wrapper = mount(InputForTest, {
+ props: {iconName: 'mdi:key-outline', iconSize: 'text-2xl'},
+ })
+ expect(wrapper.get('[data-test="icon"]').attributes('size')).toBe('text-2xl')
+ })
+
+ it('applies icon color class', () => {
+ const wrapper = mount(InputForTest, {
+ props: {iconName: 'mdi:key-outline', iconColor: 'text-m-primary'},
+ })
+ expect(wrapper.get('[data-test="icon"]').classes()).toContain('text-m-primary')
+ })
})
diff --git a/app/components/malio/InputText.vue b/app/components/malio/InputText.vue
index 3d0616a..cb9374e 100644
--- a/app/components/malio/InputText.vue
+++ b/app/components/malio/InputText.vue
@@ -1,6 +1,6 @@
- {{ label }}
+ {{ label }}
- {{ hint }}
-
-
-
- {{ error }}
-
-
-
- {{ successMessage }}
+ {{ hint || error || successMessage }}
@@ -109,8 +101,8 @@ const props = withDefaults(
modelValue?: string | null | undefined
minWidth?: string
maxWidth?: string
- text?: string
- textSize?: string
+ textInput?: string
+ textLabel?: string
inputClass?: string
labelClass?: string
required?: boolean
@@ -139,12 +131,12 @@ const props = withDefaults(
maxWidth: '',
inputClass: '',
labelClass: '',
- text: 'text-lg',
+ textInput: 'text-lg',
required: false,
maxLength: undefined,
minLength: undefined,
readonly: false,
- textSize: 'text-sm',
+ textLabel: 'text-sml',
disabled: false,
rounded: 'rounded-md',
hint: '',
@@ -199,17 +191,14 @@ const iconInputPaddingClass = computed(() => {
}
.grow-height {
- transition: transform 160ms ease, background 160ms ease, border-color 160ms ease, box-shadow 160ms ease;
- transform-origin: center;
+ transition: border-color 160ms ease, box-shadow 160ms ease, padding-top 160ms ease, padding-bottom 160ms ease;
}
.grow-height:focus {
- transform: scaleY(1.2);
+ padding-top: 0.625rem;
+ padding-bottom: 0.625rem;
}
-
@media (prefers-reduced-motion: reduce) {
- .grow-height {
- transition: none;
- }
+ .grow-height { transition: none; }
}
diff --git a/pre-commit b/pre-commit
index b616324..cf4eeee 100644
--- a/pre-commit
+++ b/pre-commit
@@ -3,23 +3,14 @@ set -e
echo "######### Pre-commit hook start #############"
-if ! command -v npm >/dev/null 2>&1; then
- if [ -f ".nvmrc" ]; then
- NVM_VERSION="$(tr -d '\r\n' < .nvmrc)"
- NVM_VERSION="${NVM_VERSION#v}"
- NPM_BIN="$HOME/.nvm/versions/node/v$NVM_VERSION/bin"
- if [ -x "$NPM_BIN/npm" ]; then
- PATH="$NPM_BIN:$PATH"
- export PATH
- fi
- fi
-fi
-
-if ! command -v npm >/dev/null 2>&1; then
- if [ -s "$HOME/.nvm/nvm.sh" ]; then
- # shellcheck disable=SC1090
- . "$HOME/.nvm/nvm.sh"
- nvm use >/dev/null 2>&1 || true
+# Prefer the exact Node version from .nvmrc for hooks (IDE + CLI consistency).
+if [ -f ".nvmrc" ]; then
+ NVM_VERSION="$(tr -d '\r\n' < .nvmrc)"
+ NVM_VERSION="${NVM_VERSION#v}"
+ NVM_BIN="$HOME/.nvm/versions/node/v$NVM_VERSION/bin"
+ if [ -x "$NVM_BIN/node" ] && [ -x "$NVM_BIN/npm" ]; then
+ PATH="$NVM_BIN:$PATH"
+ export PATH
fi
fi
@@ -28,6 +19,7 @@ if ! command -v npm >/dev/null 2>&1; then
exit 1
fi
+echo "Node $(node -v) / npm $(npm -v)"
echo "--- make pre-commit start ---"
make pre-commit
echo "--- make pre-commit finished ---"