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 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') }) })