Files
Coltura/frontend/modules/sites/components/__tests__/SiteSelector.spec.ts
tristan 6cf5ef4cfc
All checks were successful
Auto Tag Develop / tag (push) Successful in 6s
Module sites (#8)
| Numéro du ticket | Titre du ticket |
|------------------|-----------------|
|                  |                 |

## Description de la PR

## Modification du .env

## Check list

- [x] Pas de régression
- [x] TU/TI/TF rédigée
- [x] TU/TI/TF OK
- [ ] CHANGELOG modifié

Co-authored-by: Matthieu <mtholot19@gmail.com>
Reviewed-on: #8
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-04-20 15:31:58 +00:00

190 lines
7.0 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import { computed, defineComponent, h, ref, watchEffect } from 'vue'
import type { Site } from '~/shared/types/sites'
import { useCurrentSite } from '~/modules/sites/composables/useCurrentSite'
import SiteSelector from '../SiteSelector.vue'
const mockPatch = vi.hoisted(() => vi.fn())
const mockAuthUser = vi.hoisted(() => ({
value: null as { sites: Site[]; currentSite: Site | null } | null,
}))
// Stubs des auto-imports Nuxt. SiteSelector.vue utilise useCurrentSite,
// useAuthStore, useI18n, watchEffect, computed sans import explicite
// (pattern Nuxt). En Vitest on les expose comme globals.
vi.stubGlobal('useCurrentSite', useCurrentSite)
vi.stubGlobal('useApi', () => ({ patch: mockPatch }))
vi.stubGlobal('useAuthStore', () => ({
get user() {
return mockAuthUser.value
},
setCurrentSite(site: Site | null) {
if (mockAuthUser.value) {
mockAuthUser.value.currentSite = site
}
},
}))
vi.stubGlobal('useI18n', () => ({ t: (key: string) => key }))
vi.stubGlobal('watchEffect', watchEffect)
vi.stubGlobal('computed', computed)
vi.stubGlobal('ref', ref)
// useSidebar et refreshNuxtData sont consommes par useCurrentSite apres
// un switch reussi — stubs minimaux pour eviter ReferenceError au mount.
vi.stubGlobal('useSidebar', () => ({ loadSidebar: vi.fn() }))
vi.stubGlobal('refreshNuxtData', vi.fn())
// Stub de MalioSiteSelector : on se contente de tracker les props recues
// et de re-emettre `change` quand on le simule via `trigger`. Evite de
// monter la vraie lib Malio (qui aurait besoin de tout Tailwind + twMerge).
const MalioSiteSelectorStub = defineComponent({
name: 'MalioSiteSelector',
props: {
sites: { type: Array, required: true },
modelValue: { type: String, default: undefined },
groupClass: { type: String, default: '' },
tileClass: { type: String, default: '' },
labelClass: { type: String, default: '' },
},
emits: ['update:modelValue', 'change'],
setup(props, { emit }) {
return () => h('div', {
'data-testid': 'malio-site-selector',
'data-sites-count': String((props.sites as unknown[]).length),
'data-active-id': String(props.modelValue ?? ''),
'data-label-class': props.labelClass,
}, [
...(props.sites as Array<{ id: string; name: string; color: string }>).map(site =>
h('button', {
'data-testid': `tile-${site.id}`,
// Emet les deux events comme le vrai MalioSiteSelector
// (update:modelValue + change). Le wrapper n'ecoute que
// change aujourd'hui, mais tracker les deux grave la
// signature et prepare un eventuel v-model futur.
onClick: () => {
emit('update:modelValue', site.id)
emit('change', site)
},
}, site.name),
),
])
},
})
const SITE_A: Site = {
id: 1,
name: 'Chatellerault',
street: '14 All.',
complement: null,
postalCode: '86100',
city: 'Châtellerault',
color: '#056CF2',
fullAddress: '14 All.\n86100 Châtellerault',
}
const SITE_B: Site = {
id: 2,
name: 'Saint-Jean',
street: 'Z i',
complement: null,
postalCode: '17400',
city: 'Fontenet',
color: '#F3CB00',
fullAddress: 'Z i\n17400 Fontenet',
}
function mountSelector() {
return mount(SiteSelector, {
global: {
stubs: { MalioSiteSelector: MalioSiteSelectorStub },
},
})
}
describe('SiteSelector', () => {
beforeEach(() => {
mockPatch.mockReset()
mockAuthUser.value = {
sites: [SITE_A, SITE_B],
currentSite: SITE_A,
}
})
it('rend un tile par site autorise', () => {
const wrapper = mountSelector()
const stub = wrapper.find('[data-testid="malio-site-selector"]')
expect(stub.attributes('data-sites-count')).toBe('2')
})
it('marque le site courant via modelValue (string)', () => {
const wrapper = mountSelector()
const stub = wrapper.find('[data-testid="malio-site-selector"]')
// Chatellerault id=1 => '1'
expect(stub.attributes('data-active-id')).toBe('1')
})
it('passe labelClass="text-2xl" pour forcer 24px conforme Figma', () => {
// Decision design : texte blanc par defaut Malio mais taille 24px
// imposee par la maquette. Le reste des attributs text (white, bold,
// uppercase, tracking-wide) provient du default Malio via twMerge.
const wrapper = mountSelector()
const stub = wrapper.find('[data-testid="malio-site-selector"]')
expect(stub.attributes('data-label-class')).toBe('text-2xl')
})
it('clic sur un tile inactif declenche switchSite via PATCH /me/current-site', async () => {
mockPatch.mockResolvedValueOnce({})
const wrapper = mountSelector()
await wrapper.find('[data-testid="tile-2"]').trigger('click')
await flushPromises()
expect(mockPatch).toHaveBeenCalledWith(
'/me/current-site',
{ site: '/api/sites/2' },
expect.anything(),
)
})
it('clic sur le tile deja actif declenche un PATCH (resync cross-tab)', async () => {
// Le court-circuit "si deja actif, ne rien faire" a ete supprime
// pour couvrir le cas ou un autre onglet a modifie le site courant
// cote serveur : un clic sur la tile localement "active" (etat
// potentiellement stale) force une resync via PATCH. Le prix est un
// PATCH superflu quand l'etat local est effectivement a jour.
const wrapper = mountSelector()
await wrapper.find('[data-testid="tile-1"]').trigger('click')
await flushPromises()
expect(mockPatch).toHaveBeenCalledWith(
'/me/current-site',
{ site: '/api/sites/1' },
expect.anything(),
)
})
it('rollback visuel : sur erreur PATCH, data-active-id revient au site initial', async () => {
// Scenario : admin clique sur Saint-Jean alors que Chatellerault est
// actif, mais le serveur rejette (ex : 500). Apres rollback dans
// useCurrentSite, le composant doit re-afficher Chatellerault actif.
mockPatch.mockRejectedValueOnce(new Error('server down'))
const wrapper = mountSelector()
// Avant : Chatellerault (id=1) actif.
expect(wrapper.find('[data-testid="malio-site-selector"]').attributes('data-active-id'))
.toBe('1')
await wrapper.find('[data-testid="tile-2"]').trigger('click')
await flushPromises()
// Apres rollback : Chatellerault (id=1) de nouveau actif.
expect(wrapper.find('[data-testid="malio-site-selector"]').attributes('data-active-id'))
.toBe('1')
// Le store auth ne doit PAS avoir ete laisse avec SITE_B.
expect(mockAuthUser.value?.currentSite).toEqual(SITE_A)
})
})