358ba246d7
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
338 lines
11 KiB
TypeScript
338 lines
11 KiB
TypeScript
import {describe, expect, it} from 'vitest'
|
||
import {mount} 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<string | number>
|
||
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
|
||
}
|
||
|
||
const SelectCheckboxForTest = SelectCheckbox as DefineComponent<SelectCheckboxProps>
|
||
|
||
const options: Option[] = [
|
||
{label: 'France', value: 'fr'},
|
||
{label: 'Belgique', value: 'be'},
|
||
{label: 'Canada', value: 'ca'},
|
||
]
|
||
|
||
describe('MalioSelectCheckbox', () => {
|
||
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()
|
||
})
|
||
})
|