import {describe, expect, it} from 'vitest' import {mount, renderToString} from '@vue/test-utils' import type {DefineComponent} from 'vue' import SelectCheckbox from './SelectCheckbox.vue' type Option = { label: string value: string | number } type SelectCheckboxProps = { modelValue?: Array options?: Option[] emptyOptionLabel?: string label?: string hint?: string error?: string success?: string textField?: string textValue?: string textLabel?: string rounded?: string displayTag?: boolean displaySelectAll?: boolean selectAllLabel?: string disabled?: boolean readonly?: boolean groupClass?: string required?: boolean reserveMessageSpace?: boolean } const SelectCheckboxForTest = SelectCheckbox as DefineComponent const options: Option[] = [ {label: 'France', value: 'fr'}, {label: 'Belgique', value: 'be'}, {label: 'Canada', value: 'ca'}, ] describe('MalioSelectCheckbox', () => { it('rend sans planter quand modelValue n’est pas fourni (non contrôlé)', () => { expect(() => mount(SelectCheckboxForTest, {props: {label: 'Catégories', options}}), ).not.toThrow() }) it('rend en SSR sans planter quand modelValue est absent (cause du crash playground)', async () => { await expect( renderToString(SelectCheckboxForTest, {props: {label: 'Catégories', readonly: true, options}}), ).resolves.toBeTruthy() }) it('renders checkbox inputs for options', async () => { const wrapper = mount(SelectCheckboxForTest, { props: {modelValue: [], options}, }) await wrapper.get('button').trigger('click') const checkboxes = wrapper.findAll('input[type="checkbox"]') expect(checkboxes).toHaveLength(options.length) }) it('emits an array with the toggled option value', async () => { const wrapper = mount(SelectCheckboxForTest, { props: {modelValue: ['fr'], options}, }) await wrapper.get('button').trigger('click') const checkboxInputs = wrapper.findAll('input[type="checkbox"]') await checkboxInputs[1].setValue(true) expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([['fr', 'be']]) }) it('shows the selected count over the total count in the trigger', () => { const wrapper = mount(SelectCheckboxForTest, { props: {modelValue: ['fr', 'ca'], options}, }) expect(wrapper.text()).toContain('2/3') }) it('shows 0 over the total count when nothing is selected', () => { const wrapper = mount(SelectCheckboxForTest, { props: {modelValue: [], options}, }) expect(wrapper.text()).toContain('0/3') }) it('hides the summary when displayTag is enabled and options are selected', () => { const wrapper = mount(SelectCheckboxForTest, { props: {modelValue: ['fr', 'ca'], options, displayTag: true}, }) expect(wrapper.text()).not.toContain('2/3') expect(wrapper.text()).toContain('France') expect(wrapper.text()).toContain('Canada') }) it('hides the summary when displayTag is enabled and nothing is selected', () => { const wrapper = mount(SelectCheckboxForTest, { props: {modelValue: [], options, displayTag: true, emptyOptionLabel: 'Aucune selection'}, }) expect(wrapper.text()).not.toContain('0/3') expect(wrapper.text()).toContain('Aucune selection') }) it('does not show select all checkbox by default', async () => { const wrapper = mount(SelectCheckboxForTest, { props: {modelValue: [], options}, }) await wrapper.get('button').trigger('click') const checkboxes = wrapper.findAll('input[type="checkbox"]') expect(checkboxes).toHaveLength(options.length) }) it('shows select all checkbox when displaySelectAll is true', async () => { const wrapper = mount(SelectCheckboxForTest, { props: {modelValue: [], options, displaySelectAll: true}, }) await wrapper.get('button').trigger('click') const checkboxes = wrapper.findAll('input[type="checkbox"]') expect(checkboxes).toHaveLength(options.length + 1) expect(wrapper.text()).toContain('Tout sélectionner') }) it('shows custom select all label', async () => { const wrapper = mount(SelectCheckboxForTest, { props: {modelValue: [], options, displaySelectAll: true, selectAllLabel: 'Sélectionner tout'}, }) await wrapper.get('button').trigger('click') expect(wrapper.text()).toContain('Sélectionner tout') }) it('emits all values when select all is clicked and none selected', async () => { const wrapper = mount(SelectCheckboxForTest, { props: {modelValue: [], options, displaySelectAll: true}, }) await wrapper.get('button').trigger('click') const checkboxes = wrapper.findAll('input[type="checkbox"]') await checkboxes[0].setValue(true) expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([['fr', 'be', 'ca']]) }) it('emits empty array when select all is clicked and all selected', async () => { const wrapper = mount(SelectCheckboxForTest, { props: {modelValue: ['fr', 'be', 'ca'], options, displaySelectAll: true}, }) await wrapper.get('button').trigger('click') const checkboxes = wrapper.findAll('input[type="checkbox"]') await checkboxes[0].setValue(false) expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([[]]) }) it('select all checkbox is checked when all options are selected', async () => { const wrapper = mount(SelectCheckboxForTest, { props: {modelValue: ['fr', 'be', 'ca'], options, displaySelectAll: true}, }) await wrapper.get('button').trigger('click') const checkboxes = wrapper.findAll('input[type="checkbox"]') expect((checkboxes[0].element as HTMLInputElement).checked).toBe(true) }) it('select all checkbox is unchecked when not all options are selected', async () => { const wrapper = mount(SelectCheckboxForTest, { props: {modelValue: ['fr'], options, displaySelectAll: true}, }) await wrapper.get('button').trigger('click') const checkboxes = wrapper.findAll('input[type="checkbox"]') expect((checkboxes[0].element as HTMLInputElement).checked).toBe(false) }) it('applies groupClass via twMerge', () => { const wrapper = mount(SelectCheckboxForTest, { props: {modelValue: [], options: [], groupClass: 'mt-4'}, }) const root = wrapper.find('button').element.parentElement expect(root?.className).toContain('mt-4') }) it('shows muted chevron color when nothing is selected and closed', () => { const wrapper = mount(SelectCheckboxForTest, { props: {modelValue: [], options}, }) expect(wrapper.get('[data-test="chevron"]').classes()).toContain('text-m-muted') }) it('shows primary chevron color when open', async () => { const wrapper = mount(SelectCheckboxForTest, { props: {modelValue: [], options}, }) await wrapper.get('button').trigger('click') expect(wrapper.get('[data-test="chevron"]').classes()).toContain('text-m-primary') }) it('shows black chevron color when options are selected and closed', () => { const wrapper = mount(SelectCheckboxForTest, { props: {modelValue: ['fr'], options}, }) expect(wrapper.get('[data-test="chevron"]').classes()).toContain('text-black') }) it('shows muted chevron color when disabled', () => { const wrapper = mount(SelectCheckboxForTest, { props: {modelValue: ['fr'], options, disabled: true}, }) expect(wrapper.get('[data-test="chevron"]').classes()).toContain('text-m-muted') }) it('shows danger chevron color on error even when open', async () => { const wrapper = mount(SelectCheckboxForTest, { props: {modelValue: [], options, error: 'Selection error'}, }) await wrapper.get('button').trigger('click') expect(wrapper.get('[data-test="chevron"]').classes()).toContain('text-m-danger') }) it('shows success chevron color on success', () => { const wrapper = mount(SelectCheckboxForTest, { props: {modelValue: [], options, success: 'OK'}, }) expect(wrapper.get('[data-test="chevron"]').classes()).toContain('text-m-success') }) it('affiche l\'astérisque quand required est vrai', () => { const wrapper = mount(SelectCheckboxForTest, { props: {modelValue: [], 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 = mount(SelectCheckboxForTest, { props: {modelValue: [], label: 'Champ'}, }) expect(wrapper.find('[data-test="required-mark"]').exists()).toBe(false) }) it('expose aria-required quand required est vrai', () => { const wrapper = mount(SelectCheckboxForTest, { props: {modelValue: [], options, required: true}, }) expect(wrapper.find('[aria-required="true"]').exists()).toBe(true) }) it('n\'expose pas aria-required par défaut', () => { const wrapper = mount(SelectCheckboxForTest, { props: {modelValue: [], options}, }) expect(wrapper.find('[aria-required="true"]').exists()).toBe(false) }) it('keeps the bottom border allocation when open downward (transparent, not zero)', async () => { const wrapper = mount(SelectCheckboxForTest, { props: {modelValue: [], options}, }) await wrapper.get('button').trigger('click') const buttonClasses = wrapper.get('button').classes() // !border-b-0 would shrink the bottom border to 0px and grow content area by 1px; // !border-b-transparent keeps the 1px allocation but hides the line expect(buttonClasses).not.toContain('!border-b-0') expect(buttonClasses).toContain('!border-b-transparent') }) it('readonly : bordure noire même sans sélection, pas de grow/bleu', () => { const wrapper = mount(SelectCheckboxForTest, { props: {label: 'Champ', readonly: true, modelValue: [], options: [{label: 'A', value: 'a'}]}, }) const trigger = wrapper.get('button') expect(trigger.classes()).toContain('border-black') expect(trigger.classes()).not.toContain('border-m-muted') expect(trigger.classes()).not.toContain('grow-height') expect(trigger.classes()).not.toContain('focus-visible:border-m-primary') }) it('readonly vide : label gris, pas de bleu', () => { const wrapper = mount(SelectCheckboxForTest, { props: {label: 'Champ', readonly: true, modelValue: [], options: [{label: 'A', value: 'a'}]}, }) const label = wrapper.get('label') expect(label.classes()).not.toContain('text-m-primary') expect(label.classes()).toContain('text-m-muted') }) it('readonly sélectionné : label noir + chevron noir', () => { const wrapper = mount(SelectCheckboxForTest, { props: {label: 'Champ', readonly: true, modelValue: ['a'], options: [{label: 'A', value: 'a'}]}, }) expect(wrapper.get('label').classes()).toContain('text-black') expect(wrapper.get('[data-test="chevron"]').classes()).toContain('text-black') }) it('readonly empêche l’ouverture du dropdown', async () => { const wrapper = mount(SelectCheckboxForTest, { props: {label: 'Champ', readonly: true, modelValue: [], options: [{label: 'A', value: 'a'}]}, }) await wrapper.get('button').trigger('click') expect(wrapper.find('[role="listbox"]').exists()).toBe(false) }) it('readonly expose aria-readonly et reste focusable (pas disabled)', () => { const wrapper = mount(SelectCheckboxForTest, { props: {label: 'Champ', readonly: true, modelValue: [], options}, }) const trigger = wrapper.get('button') expect(trigger.attributes('aria-readonly')).toBe('true') expect(trigger.attributes('disabled')).toBeUndefined() }) it('disabled + readonly : pas d’aria-readonly (disabled prime)', () => { const wrapper = mount(SelectCheckboxForTest, {props: {modelValue: [], label: 'Champ', disabled: true, readonly: true, options: [{label: 'A', value: 'a'}]}}) const trigger = wrapper.get('button') expect(trigger.attributes('aria-readonly')).toBeUndefined() expect(trigger.attributes('disabled')).toBeDefined() }) it('réserve l’espace message par défaut même sans message', () => { const wrapper = mount(SelectCheckboxForTest, {props: {label: 'Champ', options}}) 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 = mount(SelectCheckboxForTest, {props: {label: 'Champ', options, reserveMessageSpace: false}}) expect(wrapper.find('[id$="-describedby"]').exists()).toBe(false) }) it('reserveMessageSpace=false avec message : ligne rendue sans min-h', () => { const wrapper = mount(SelectCheckboxForTest, {props: {label: 'Champ', options, reserveMessageSpace: false, error: 'Erreur'}}) const msg = wrapper.find('[id$="-describedby"]') expect(msg.exists()).toBe(true) expect(msg.classes()).not.toContain('min-h-[1rem]') }) })