Composant MalioSiteSelector : bande horizontale pour choisir un site (usine ou lieu) parmi une liste. Tuiles flex proportionnelles, couleur du site sélectionné partagée par toutes les tuiles (opacité 1 / 0.4). Expose update:modelValue (id) + change (objet site complet) pour faciliter les appels API côté consommateur. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> | Numéro du ticket | Titre du ticket | |------------------|-----------------| | | | ## Description de la PR ## Modification du .env ## Check list - [ ] Pas de régression - [ ] TU/TI/TF rédigée - [ ] TU/TI/TF OK - [ ] CHANGELOG modifié Reviewed-on: #29 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
155 lines
5.4 KiB
TypeScript
155 lines
5.4 KiB
TypeScript
import {describe, expect, it} from 'vitest'
|
|
import {mount} from '@vue/test-utils'
|
|
import type {DefineComponent} from 'vue'
|
|
import SiteSelector from './SiteSelector.vue'
|
|
|
|
type Site = {
|
|
id: string
|
|
name: string
|
|
color: string
|
|
}
|
|
|
|
type SiteSelectorProps = {
|
|
sites: Site[]
|
|
modelValue?: string
|
|
id?: string
|
|
groupClass?: string
|
|
tileClass?: string
|
|
labelClass?: string
|
|
}
|
|
|
|
const SiteSelectorForTest = SiteSelector as DefineComponent<SiteSelectorProps>
|
|
|
|
const sites: Site[] = [
|
|
{id: 'chatellerault', name: 'Châtellerault', color: '#2563eb'},
|
|
{id: 'saint-jean', name: 'Saint-Jean', color: '#16a34a'},
|
|
{id: 'pommevic', name: 'Pommevic', color: '#dc2626'},
|
|
]
|
|
|
|
function mountComponent(props: SiteSelectorProps) {
|
|
return mount(SiteSelectorForTest, {props})
|
|
}
|
|
|
|
describe('MalioSiteSelector', () => {
|
|
it('renders one tile per site with the site name', () => {
|
|
const wrapper = mountComponent({sites})
|
|
const tiles = wrapper.findAll('[role="radio"]')
|
|
expect(tiles).toHaveLength(3)
|
|
expect(tiles[0]!.text()).toBe('Châtellerault')
|
|
expect(tiles[1]!.text()).toBe('Saint-Jean')
|
|
expect(tiles[2]!.text()).toBe('Pommevic')
|
|
})
|
|
|
|
it('has role="radiogroup" on the wrapper', () => {
|
|
const wrapper = mountComponent({sites})
|
|
expect(wrapper.find('[role="radiogroup"]').exists()).toBe(true)
|
|
})
|
|
|
|
it('selects the first site by default in uncontrolled mode', () => {
|
|
const wrapper = mountComponent({sites})
|
|
const tiles = wrapper.findAll('[role="radio"]')
|
|
expect(tiles[0]!.attributes('aria-checked')).toBe('true')
|
|
expect(tiles[1]!.attributes('aria-checked')).toBe('false')
|
|
expect(tiles[2]!.attributes('aria-checked')).toBe('false')
|
|
})
|
|
|
|
it('paints all tiles with the selected site color', () => {
|
|
const wrapper = mountComponent({sites, modelValue: 'saint-jean'})
|
|
const tiles = wrapper.findAll('[role="radio"]')
|
|
for (const tile of tiles) {
|
|
expect(tile.attributes('style')).toContain('background-color: rgb(22, 163, 74)')
|
|
}
|
|
})
|
|
|
|
it('applies opacity 1 on the selected tile and 0.4 on the others', () => {
|
|
const wrapper = mountComponent({sites, modelValue: 'chatellerault'})
|
|
const tiles = wrapper.findAll('[role="radio"]')
|
|
expect(tiles[0]!.attributes('style')).toContain('opacity: 1')
|
|
expect(tiles[1]!.attributes('style')).toContain('opacity: 0.4')
|
|
expect(tiles[2]!.attributes('style')).toContain('opacity: 0.4')
|
|
})
|
|
|
|
it('updates the shared color when the selection changes', async () => {
|
|
const wrapper = mountComponent({sites})
|
|
let tiles = wrapper.findAll('[role="radio"]')
|
|
expect(tiles[0]!.attributes('style')).toContain('background-color: rgb(37, 99, 235)')
|
|
|
|
await tiles[2]!.trigger('click')
|
|
tiles = wrapper.findAll('[role="radio"]')
|
|
for (const tile of tiles) {
|
|
expect(tile.attributes('style')).toContain('background-color: rgb(220, 38, 38)')
|
|
}
|
|
})
|
|
|
|
it('emits update:modelValue with the clicked site id', async () => {
|
|
const wrapper = mountComponent({sites, modelValue: 'chatellerault'})
|
|
const tiles = wrapper.findAll('[role="radio"]')
|
|
|
|
await tiles[1]!.trigger('click')
|
|
|
|
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual(['saint-jean'])
|
|
})
|
|
|
|
it('emits change with the full selected site object', async () => {
|
|
const wrapper = mountComponent({sites, modelValue: 'chatellerault'})
|
|
const tiles = wrapper.findAll('[role="radio"]')
|
|
|
|
await tiles[2]!.trigger('click')
|
|
|
|
expect(wrapper.emitted('change')?.[0]).toEqual([
|
|
{id: 'pommevic', name: 'Pommevic', color: '#dc2626'},
|
|
])
|
|
})
|
|
|
|
it('respects modelValue in controlled mode', () => {
|
|
const wrapper = mountComponent({sites, modelValue: 'pommevic'})
|
|
const tiles = wrapper.findAll('[role="radio"]')
|
|
expect(tiles[0]!.attributes('aria-checked')).toBe('false')
|
|
expect(tiles[1]!.attributes('aria-checked')).toBe('false')
|
|
expect(tiles[2]!.attributes('aria-checked')).toBe('true')
|
|
})
|
|
|
|
it('switches selection on click in uncontrolled mode', async () => {
|
|
const wrapper = mountComponent({sites})
|
|
const tiles = wrapper.findAll('[role="radio"]')
|
|
|
|
await tiles[1]!.trigger('click')
|
|
|
|
expect(tiles[0]!.attributes('aria-checked')).toBe('false')
|
|
expect(tiles[1]!.attributes('aria-checked')).toBe('true')
|
|
expect(tiles[2]!.attributes('aria-checked')).toBe('false')
|
|
})
|
|
|
|
it('sets roving tabindex (active = 0, others = -1)', () => {
|
|
const wrapper = mountComponent({sites, modelValue: 'saint-jean'})
|
|
const tiles = wrapper.findAll('[role="radio"]')
|
|
expect(tiles[0]!.attributes('tabindex')).toBe('-1')
|
|
expect(tiles[1]!.attributes('tabindex')).toBe('0')
|
|
expect(tiles[2]!.attributes('tabindex')).toBe('-1')
|
|
})
|
|
|
|
it('merges groupClass, tileClass and labelClass via twMerge', () => {
|
|
const wrapper = mountComponent({
|
|
sites,
|
|
groupClass: 'rounded-none bg-black',
|
|
tileClass: 'py-10',
|
|
labelClass: 'text-xs',
|
|
})
|
|
const group = wrapper.find('[role="radiogroup"]')
|
|
expect(group.classes()).toContain('rounded-none')
|
|
expect(group.classes()).toContain('bg-black')
|
|
|
|
const tile = wrapper.find('[role="radio"]')
|
|
expect(tile.classes()).toContain('py-10')
|
|
expect(tile.classes()).not.toContain('py-4')
|
|
|
|
const label = tile.find('span')
|
|
expect(label.classes()).toContain('text-xs')
|
|
})
|
|
|
|
it('uses a custom id when provided', () => {
|
|
const wrapper = mountComponent({sites, id: 'my-selector'})
|
|
expect(wrapper.find('[role="radiogroup"]').attributes('id')).toBe('my-selector')
|
|
})
|
|
})
|