import { config, mount, shallowMount, VueWrapper } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import AmeliproSelect from '../AmeliproSelect.vue'
import type { ComponentProps } from 'vue-component-type-helpers'
import type { ExpectedPropOptions } from '@tests/types'
import { defineComponent, type PropType } from 'vue'
import type { SelectItem } from '../types'
import TestHelper from '@tests/helpers/TestHelper'
import type { ValidateOnType } from '../../types'
import type { ValidationRule } from '@/utils/rules/types'
import { isRequired } from '@/utils/rules/isRequired'
vi.mock('@/utils/rules/isRequired', () => ({ isRequired: 'mocked-is-required' }))
const VSelectStub = defineComponent({
name: 'VSelect',
props: {
modelValue: {
type: [String, Number, Object],
default: undefined,
},
items: {
type: Array,
default: () => [],
},
placeholder: {
type: String,
default: undefined,
},
readonly: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
clearable: {
type: Boolean,
default: false,
},
prefix: {
type: [String, Number],
default: undefined,
},
id: {
type: String,
default: undefined,
},
required: {
type: [Boolean, String],
default: false,
},
ariaInvalid: {
type: [Boolean, String],
default: false,
},
ariaDescribedby: {
type: String,
default: undefined,
},
hideDetails: {
type: [Boolean, String],
default: false,
},
style: {
type: [String, Object],
default: undefined,
},
rules: {
type: Array,
default: () => [],
},
validateOn: {
type: String,
default: undefined,
},
},
emits: ['update:modelValue', 'focus', 'blur'],
template: `
`,
})
config.global.stubs = config.global.stubs || {}
config.global.stubs.VSelect = VSelectStub
const expectedPropOptions: ExpectedPropOptions = {
required: {
type: Boolean,
default: false,
},
classes: {
type: String,
default: undefined,
},
clearable: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
fullWidthErrorMsg: {
type: Boolean,
default: false,
},
globalMaxWidth: {
type: String,
default: undefined,
},
globalMinWidth: {
type: String,
default: undefined,
},
globalWidth: {
type: String,
default: undefined,
},
hideErrorMessage: {
type: Boolean,
default: false,
},
horizontal: {
type: Boolean,
default: false,
},
inputMaxWidth: {
type: String,
default: undefined,
},
inputMinWidth: {
type: String,
default: undefined,
},
items: {
type: Array as PropType,
required: true,
},
label: {
type: String,
required: true,
},
labelMaxWidth: {
type: String,
default: undefined,
},
labelMinWidth: {
type: String,
default: undefined,
},
modelValue: {
type: [Object, Number, String] as PropType,
default: undefined,
},
placeholder: {
type: String,
default: undefined,
},
readonly: {
type: Boolean,
default: false,
},
rules: {
type: Array as PropType,
default: () => [],
},
uniqueId: {
type: String,
required: true,
},
validateOn: {
default: 'input',
type: String as PropType,
validator(value: string): boolean {
return ['lazy', 'blur', 'input', 'submit', 'blur lazy', 'input lazy', 'submit lazy', 'lazy blur', 'lazy input', 'lazy submit'].includes(value.toLowerCase())
},
},
}
// Values pour les props "required"
const requiredPropValues = (): ComponentProps => ({
items: [],
label: 'Required label',
uniqueId: 'required-unique-id',
})
// Valeurs pour les props "modified"
const modifiedPropValues = (): ComponentProps => ({
required: true,
classes: 'modified-classes',
clearable: true,
disabled: true,
fullWidthErrorMsg: true,
globalMaxWidth: 'modified-global-max-width',
globalMinWidth: 'modified-global-min-width',
globalWidth: 'modified-global-width',
hideErrorMessage: true,
horizontal: true,
inputMaxWidth: 'modified-input-max-width',
inputMinWidth: 'modified-input-min-width',
items: [
{
disabled: true,
title: 'Modified item title 1',
value: 'modified-item-value-1',
},
{
title: 'Modified item title 2',
value: 'modified-item-value-2',
},
{
title: 'Modified item title 3',
value: 'modified-item-value-3',
},
] as SelectItem[],
label: 'Modified label',
labelMaxWidth: 'modified-label-max-width',
labelMinWidth: 'modified-label-min-width',
modelValue: 'modified-value',
placeholder: 'Modified placeholder',
readonly: true,
rules: [() => 'modified-rule'],
uniqueId: 'modified-unique-id',
})
const testHelper = new TestHelper(AmeliproSelect)
testHelper.setExpectedPropOptions(expectedPropOptions)
.setRequiredPropValues(requiredPropValues)
.setModifiedPropValues(modifiedPropValues)
describe('AmeliproSelect', () => {
let vueWrapper: VueWrapper>
const selectWrapper = () => vueWrapper.findComponent(VSelectStub)
describe('Snapshots', () => {
testHelper.snapshots()
})
describe('Properties', () => {
testHelper.properties()
})
describe('Slots', () => {
describe('label info', () => {
it('renders correctly', () => {
vueWrapper = shallowMount(AmeliproSelect, {
props: modifiedPropValues(),
slots: { labelInfo: 'Custom label info
' },
global: {
stubs: { VSelect: VSelectStub },
},
})
expect(vueWrapper.find('.custom-label-info').text()).toEqual('Custom label info')
})
})
describe('append', () => {
it('renders correctly', () => {
vueWrapper = mount(AmeliproSelect, {
props: modifiedPropValues(),
slots: { append: 'Append outer
' },
global: {
stubs: { VSelect: VSelectStub },
},
})
expect(vueWrapper.find('.append').text()).toEqual('Append outer')
})
})
})
describe('Setting props should update attributes of inner tags', () => {
beforeEach(() => {
vueWrapper = shallowMount(AmeliproSelect, { props: requiredPropValues() })
})
describe('main div', () => {
it('prop required sets display of span', async () => {
expect(vueWrapper.findAll('.amelipro-select__label span').length).toBe(0)
const { required } = modifiedPropValues()
await vueWrapper.setProps({ required })
expect(vueWrapper.findAll('.amelipro-select__label span').length).toBe(3)
})
it('prop horizontal sets attribute class', async () => {
expect(vueWrapper.find('.amelipro-select__wrapper').attributes('class')).toBe('w-100 amelipro-select__wrapper d-flex flex-column')
const { horizontal } = modifiedPropValues()
await vueWrapper.setProps({ horizontal })
expect(vueWrapper.find('.amelipro-select__wrapper').attributes('class')).toBe('w-100 amelipro-select__wrapper d-flex flex-column flex-md-row')
})
it('props globalMaxWidth, globalMinWidth & globalWidth set attribute style', async () => {
if (vueWrapper.attributes('style')) {
expect(vueWrapper.attributes('style')).not.toContain('max-width:')
expect(vueWrapper.attributes('style')).not.toContain('min-width:')
}
const { globalMaxWidth, globalMinWidth, globalWidth } = modifiedPropValues()
await vueWrapper.setProps({ globalMaxWidth, globalMinWidth, globalWidth })
expect(vueWrapper.attributes('style')).toContain('margin-bottom: 12px; max-width: modified-global-max-width; min-width: modified-global-min-width;')
})
})
describe('label wrapper', () => {
it('prop horizontal sets attribute class', async () => {
expect(vueWrapper.find('.amelipro-select__wrapper > div').classes()).not.toContain('mt-md-2')
expect(vueWrapper.find('.amelipro-select__wrapper > div').classes()).not.toContain('mr-md-2')
const { horizontal } = modifiedPropValues()
await vueWrapper.setProps({ horizontal })
expect(vueWrapper.find('.amelipro-select__wrapper > div').classes()).toContain('mt-md-2')
expect(vueWrapper.find('.amelipro-select__wrapper > div').classes()).toContain('mr-md-2')
})
})
describe('label', () => {
it('prop uniqueId sets attributes id & for', async () => {
expect(vueWrapper.find('.amelipro-select__label').attributes('id')).toBe('required-unique-id-label')
expect(vueWrapper.find('.amelipro-select__label').attributes('for')).toBe('required-unique-id')
await vueWrapper.setProps({ uniqueId: 'new-id' })
expect(vueWrapper.find('.amelipro-select__label').attributes('id')).toBe('new-id-label')
expect(vueWrapper.find('.amelipro-select__label').attributes('for')).toBe('new-id')
})
it('props labelMinWidth & labelMaxWidth set attribute style', async () => {
expect(vueWrapper.find('.amelipro-select__label').attributes('style')).toBeUndefined()
const { labelMinWidth, labelMaxWidth } = modifiedPropValues()
await vueWrapper.setProps({ labelMinWidth, labelMaxWidth })
expect(vueWrapper.find('.amelipro-select__label').attributes('style')).toBe('max-width: modified-label-max-width; min-width: modified-label-min-width;')
})
})
})
describe('Setting props should update props or attributes of inner components', () => {
describe('VSelect', () => {
beforeEach(() => {
vueWrapper = shallowMount(AmeliproSelect, {
props: requiredPropValues(),
global: {
stubs: { VSelect: VSelectStub },
},
})
})
it('prop uniqueId sets prop uniqueId', async () => {
expect(vueWrapper.findComponent(VSelectStub).props('id')).toBe(testHelper.default('uniqueId'))
await vueWrapper.setProps({ uniqueId: testHelper.modified('uniqueId') })
expect(vueWrapper.findComponent(VSelectStub).props('id')).toBe(testHelper.modified('uniqueId'))
})
it('prop clearable sets prop clearable', async () => {
expect(vueWrapper.findComponent(VSelectStub).props('clearable')).toBe(testHelper.default('clearable'))
await vueWrapper.setProps({ clearable: testHelper.modified('clearable') })
expect(vueWrapper.findComponent(VSelectStub).props('clearable')).toBe(testHelper.modified('clearable'))
})
it('prop disabled sets prop disabled', async () => {
expect(vueWrapper.findComponent(VSelectStub).props('disabled')).toBe(testHelper.default('disabled'))
await vueWrapper.setProps({ disabled: testHelper.modified('disabled') })
expect(vueWrapper.findComponent(VSelectStub).props('disabled')).toBe(testHelper.modified('disabled'))
})
it('prop hideErrorMessage sets prop hideDetails', async () => {
expect(vueWrapper.findComponent(VSelectStub).props('hideDetails')).toBe(testHelper.default('hideErrorMessage'))
await vueWrapper.setProps({ hideErrorMessage: testHelper.modified('hideErrorMessage') })
expect(vueWrapper.findComponent(VSelectStub).props('hideDetails')).toBe(testHelper.modified('hideErrorMessage'))
})
it('prop items sets prop items', async () => {
expect(vueWrapper.findComponent(VSelectStub).props('items')).toEqual(testHelper.default('items'))
await vueWrapper.setProps({ items: testHelper.modified('items') })
expect(vueWrapper.findComponent(VSelectStub).props('items')).toEqual(testHelper.modified('items'))
})
it('prop placeholder sets prop placeholder', async () => {
expect(vueWrapper.findComponent(VSelectStub).props('placeholder')).toBe(testHelper.default('placeholder'))
await vueWrapper.setProps({ placeholder: testHelper.modified('placeholder') })
expect(vueWrapper.findComponent(VSelectStub).props('placeholder')).toBe(testHelper.modified('placeholder'))
})
it('prop readonly sets prop readonly', async () => {
expect(vueWrapper.findComponent(VSelectStub).props('readonly')).toBe(testHelper.default('readonly'))
await vueWrapper.setProps({ readonly: testHelper.modified('readonly') })
expect(vueWrapper.findComponent(VSelectStub).props('readonly')).toBe(testHelper.modified('readonly'))
})
it('prop ariaRequired sets attribute aria-required', async () => {
expect(vueWrapper.findComponent(VSelectStub).attributes('aria-required')).toBe('false')
await vueWrapper.setProps({ required: testHelper.modified('required') })
expect(vueWrapper.findComponent(VSelectStub).attributes('aria-required')).toBe('true')
})
it('props inputMinWidth & inputMaxWidth set attribute style', async () => {
expect(vueWrapper.findComponent(VSelectStub).attributes('style')).toBeUndefined()
await vueWrapper.setProps({
inputMinWidth: testHelper.modified('inputMinWidth'),
inputMaxWidth: testHelper.modified('inputMaxWidth'),
})
expect(vueWrapper.findComponent(VSelectStub).attributes('style')).toBe(`max-width: ${testHelper.modified('inputMaxWidth')}; min-width: ${testHelper.modified('inputMinWidth')};`)
})
it('props required & rules set prop rules', async () => {
expect(vueWrapper.findComponent(VSelectStub).props('rules')).toEqual([])
await vueWrapper.setProps({ required: testHelper.modified('required') })
expect(vueWrapper.findComponent(VSelectStub).props('rules')).toEqual(['mocked-is-required'])
})
})
})
describe('should react to child component events', () => {
beforeEach(() => {
vueWrapper = mount(AmeliproSelect, { props: modifiedPropValues() })
})
describe('VSelect', () => {
it.skip('should react to blur/focus event', async () => {
// focused est inutilisé
expect(vueWrapper.attributes('???')).toBe('???')
await selectWrapper().vm.$emit('focus')
expect(vueWrapper.attributes('???')).toBe('???')
await selectWrapper().vm.$emit('blur')
expect(vueWrapper.attributes('???')).toBe('???')
})
})
})
describe.skip('Getters', () => {
it('displays error message when input is invalid', async () => {
const uniqueId = 'test-id'
vueWrapper = mount(AmeliproSelect, {
props: {
items: [{ title: 'option 1', value: 'Option1' }],
label: 'Mon Label',
rules: [isRequired],
uniqueId,
},
})
await vueWrapper.vm.$nextTick()
const errorId = `${vueWrapper.props().uniqueId}-error`
expect(errorId).toBe(`${uniqueId}-error`)
})
it('computes bgColor correctly based on props', () => {
vueWrapper = mount(AmeliproSelect, {
props: {
required: true,
bgWhite: true,
disabled: true,
items: [{ title: 'option 1', value: 'Option1' }],
label: 'My Label',
readonly: false,
uniqueId: 'exampleId',
},
})
expect((vueWrapper.vm as unknown as { bgColor: string }).bgColor).toStrictEqual('#EEEEEE')
})
it('renders slot content correctly', () => {
vueWrapper = mount(AmeliproSelect, {
propsData: {
items: [{ title: 'option 1', value: 'Option1' }],
label: 'Mon Label',
uniqueId: 'modified-unique-id',
},
slots: { labelInfo: 'Custom Label Info
' },
})
const labelInfoNameSelector = vueWrapper.find('.custom-label-info')
expect(labelInfoNameSelector.text()).toEqual('Custom Label Info')
})
})
})