206 lines
8.2 KiB
TypeScript
206 lines
8.2 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import { mount, flushPromises } from '@vue/test-utils'
|
|
import { defineComponent, h, ref } from 'vue'
|
|
|
|
// ── Auto-imports Nuxt stubbes globalement ───────────────────────────────────
|
|
// La page ne les importe pas (auto-import) : on les expose en globals pour le
|
|
// runtime de test (happy-dom). Meme philosophie que les autres specs commercial.
|
|
const mockPush = vi.hoisted(() => vi.fn())
|
|
const mockApiGet = vi.hoisted(() => vi.fn())
|
|
const mockCan = vi.hoisted(() => vi.fn())
|
|
const mockSetFilters = vi.hoisted(() => vi.fn())
|
|
const mockFetch = vi.hoisted(() => vi.fn())
|
|
const mockToastError = vi.hoisted(() => vi.fn())
|
|
|
|
vi.stubGlobal('useI18n', () => ({ t: (key: string) => key }))
|
|
vi.stubGlobal('useHead', () => undefined)
|
|
vi.stubGlobal('useApi', () => ({ get: mockApiGet }))
|
|
vi.stubGlobal('useRouter', () => ({ push: mockPush }))
|
|
vi.stubGlobal('useToast', () => ({ error: mockToastError, success: vi.fn() }))
|
|
vi.stubGlobal('usePermissions', () => ({ can: mockCan }))
|
|
|
|
// Le repository est lui aussi un auto-import : on controle items + setFilters.
|
|
vi.stubGlobal('useSuppliersRepository', () => ({
|
|
items: ref([
|
|
{
|
|
id: 7,
|
|
companyName: 'ACME',
|
|
categories: [{ code: 'NEG', name: 'Négociant' }],
|
|
sites: [{ id: 86, name: '86', color: '#123456' }],
|
|
updatedAt: '2026-01-15T10:00:00+00:00',
|
|
},
|
|
]),
|
|
totalItems: ref(1),
|
|
currentPage: ref(1),
|
|
itemsPerPage: ref(10),
|
|
itemsPerPageOptions: ref([10, 25, 50]),
|
|
fetch: mockFetch,
|
|
goToPage: vi.fn(),
|
|
setItemsPerPage: vi.fn(),
|
|
setFilters: mockSetFilters,
|
|
}))
|
|
|
|
// happy-dom n'implemente pas createObjectURL : on ajoute les methodes statiques
|
|
// sur la classe URL existante (sans la remplacer — sinon `new URL()` casse).
|
|
globalThis.URL.createObjectURL = vi.fn(() => 'blob:fake')
|
|
globalThis.URL.revokeObjectURL = vi.fn()
|
|
|
|
// Import APRES les stubs (la page resout les auto-imports au top-level du module).
|
|
const SuppliersIndex = (await import('../suppliers/index.vue')).default
|
|
|
|
// ── Stubs de composants ──────────────────────────────────────────────────────
|
|
const ButtonStub = defineComponent({
|
|
props: { label: { type: String, default: '' }, disabled: { type: Boolean, default: false } },
|
|
emits: ['click'],
|
|
setup(props, { emit }) {
|
|
return () => h('button', { 'data-label': props.label, onClick: () => emit('click') }, props.label)
|
|
},
|
|
})
|
|
|
|
const DataTableStub = defineComponent({
|
|
props: { items: { type: Array, default: () => [] } },
|
|
emits: ['row-click', 'update:page', 'update:per-page'],
|
|
setup(props, { emit }) {
|
|
return () => h('div', { 'data-testid': 'datatable' },
|
|
(props.items as Array<{ id: number }>).map(it =>
|
|
h('tr', { 'data-row-id': it.id, onClick: () => emit('row-click', it) }),
|
|
),
|
|
)
|
|
},
|
|
})
|
|
|
|
const DrawerStub = defineComponent({
|
|
props: { modelValue: { type: Boolean, default: false } },
|
|
setup(_, { slots }) {
|
|
return () => h('div', {}, [slots.header?.(), slots.default?.(), slots.footer?.()])
|
|
},
|
|
})
|
|
|
|
const SlotStub = defineComponent({ setup(_, { slots }) { return () => h('div', {}, slots.default?.()) } })
|
|
|
|
const PageHeaderStub = defineComponent({
|
|
setup(_, { slots }) { return () => h('div', {}, [slots.default?.(), slots.actions?.()]) },
|
|
})
|
|
|
|
const CheckboxStub = defineComponent({
|
|
props: { id: { type: String, default: '' }, modelValue: { type: Boolean, default: false } },
|
|
emits: ['update:model-value'],
|
|
setup(props, { emit }) {
|
|
return () => h('input', {
|
|
'type': 'checkbox',
|
|
'data-id': props.id,
|
|
'onChange': (e: Event) => emit('update:model-value', (e.target as HTMLInputElement).checked),
|
|
})
|
|
},
|
|
})
|
|
|
|
const InputTextStub = defineComponent({ setup() { return () => h('input') } })
|
|
|
|
function mountPage() {
|
|
return mount(SuppliersIndex, {
|
|
global: {
|
|
stubs: {
|
|
PageHeader: PageHeaderStub,
|
|
MalioButton: ButtonStub,
|
|
MalioDataTable: DataTableStub,
|
|
MalioDrawer: DrawerStub,
|
|
MalioAccordion: SlotStub,
|
|
MalioAccordionItem: SlotStub,
|
|
MalioInputText: InputTextStub,
|
|
MalioCheckbox: CheckboxStub,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
describe('Répertoire fournisseurs (page /suppliers)', () => {
|
|
beforeEach(() => {
|
|
mockPush.mockReset()
|
|
mockApiGet.mockReset().mockResolvedValue({ member: [] })
|
|
mockCan.mockReset().mockReturnValue(true)
|
|
mockSetFilters.mockReset()
|
|
mockFetch.mockReset()
|
|
mockToastError.mockReset()
|
|
})
|
|
|
|
it('charge la liste au montage', async () => {
|
|
mountPage()
|
|
await flushPromises()
|
|
expect(mockFetch).toHaveBeenCalled()
|
|
})
|
|
|
|
it('affiche « + Ajouter » uniquement avec la permission manage', async () => {
|
|
mockCan.mockImplementation((perm: string) => perm === 'commercial.suppliers.manage')
|
|
const wrapper = mountPage()
|
|
await flushPromises()
|
|
expect(wrapper.find('[data-label="commercial.suppliers.add"]').exists()).toBe(true)
|
|
})
|
|
|
|
it('masque « + Ajouter » sans la permission manage (view seul)', async () => {
|
|
mockCan.mockImplementation((perm: string) => perm === 'commercial.suppliers.view')
|
|
const wrapper = mountPage()
|
|
await flushPromises()
|
|
expect(wrapper.find('[data-label="commercial.suppliers.add"]').exists()).toBe(false)
|
|
})
|
|
|
|
it('navigue vers la consultation au clic sur une ligne', async () => {
|
|
const wrapper = mountPage()
|
|
await flushPromises()
|
|
await wrapper.find('tr[data-row-id="7"]').trigger('click')
|
|
expect(mockPush).toHaveBeenCalledWith('/suppliers/7')
|
|
})
|
|
|
|
it('charge les categories de type FOURNISSEUR pour le filtre', async () => {
|
|
mountPage()
|
|
await flushPromises()
|
|
expect(mockApiGet).toHaveBeenCalledWith(
|
|
'/categories',
|
|
expect.objectContaining({ pagination: 'false', typeCode: 'FOURNISSEUR' }),
|
|
expect.objectContaining({ toast: false }),
|
|
)
|
|
})
|
|
|
|
it('appelle l\'export XLSX sur /suppliers/export.xlsx en blob', async () => {
|
|
const wrapper = mountPage()
|
|
await flushPromises()
|
|
await wrapper.find('[data-label="commercial.suppliers.export"]').trigger('click')
|
|
await flushPromises()
|
|
expect(mockApiGet).toHaveBeenCalledWith(
|
|
'/suppliers/export.xlsx',
|
|
expect.any(Object),
|
|
expect.objectContaining({ responseType: 'blob', toast: false }),
|
|
)
|
|
})
|
|
|
|
it('repercute le filtre « Inclure les archivés » dans setFilters sans toucher l\'URL', async () => {
|
|
const wrapper = mountPage()
|
|
await flushPromises()
|
|
|
|
// Coche « Inclure les archivés » puis applique les filtres.
|
|
await wrapper.find('input[data-id="filter-include-archived"]').setValue(true)
|
|
await wrapper.find('[data-label="commercial.suppliers.filters.apply"]').trigger('click')
|
|
|
|
expect(mockSetFilters).toHaveBeenLastCalledWith(
|
|
{ includeArchived: true },
|
|
{ replace: true },
|
|
)
|
|
// Etat 100 % local (regle n°6) : aucune navigation/query string declenchee.
|
|
expect(mockPush).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('badge filtres actifs + Réinitialiser vide l\'etat applique', async () => {
|
|
const wrapper = mountPage()
|
|
await flushPromises()
|
|
|
|
await wrapper.find('input[data-id="filter-include-archived"]').setValue(true)
|
|
await wrapper.find('[data-label="commercial.suppliers.filters.apply"]').trigger('click')
|
|
|
|
// Le libelle du bouton Filtrer porte le compteur (1 filtre actif).
|
|
expect(wrapper.find('[data-label="commercial.suppliers.filters.title (1)"]').exists()).toBe(true)
|
|
|
|
// Réinitialiser → query propre (setFilters avec objet vide).
|
|
await wrapper.find('[data-label="commercial.suppliers.filters.reset"]').trigger('click')
|
|
expect(mockSetFilters).toHaveBeenLastCalledWith({}, { replace: true })
|
|
})
|
|
})
|