import { describe, it, expect, vi, beforeEach } from 'vitest' import { useCategoryEditGuard } from '~/composables/useCategoryEditGuard' // --------------------------------------------------------------------------- // Mocks // --------------------------------------------------------------------------- const mockGet = vi.fn() const mockShowInfo = vi.fn() vi.mock('~/composables/useApi', () => ({ useApi: () => ({ get: mockGet, post: vi.fn(), patch: vi.fn(), put: vi.fn(), delete: vi.fn(), apiCall: vi.fn(), }), })) vi.mock('~/composables/useToast', () => ({ useToast: () => ({ showInfo: mockShowInfo, showSuccess: vi.fn(), showError: vi.fn(), showToast: vi.fn(), toasts: { value: [] }, clearAll: vi.fn(), }), })) const GUARD_CONFIG = { endpoint: '/composants', filterKey: 'typeComposant', labels: { singular: 'composant', plural: 'composants', verifying: 'Vérification des composants liés en cours…', }, } beforeEach(() => { vi.clearAllMocks() }) // --------------------------------------------------------------------------- // Initial state // --------------------------------------------------------------------------- describe('initial state', () => { it('has linkedCount 0 and restrictedMode false', () => { const guard = useCategoryEditGuard(GUARD_CONFIG) expect(guard.linkedCount.value).toBe(0) expect(guard.isRestrictedMode.value).toBe(false) expect(guard.isSubmitBlocked.value).toBe(false) expect(guard.linkedLoading.value).toBe(false) }) it('has empty messages when no linked items', () => { const guard = useCategoryEditGuard(GUARD_CONFIG) expect(guard.restrictedModeMessage.value).toBe('') expect(guard.submitBlockMessage.value).toBe('') }) }) // --------------------------------------------------------------------------- // loadLinkedCount // --------------------------------------------------------------------------- describe('loadLinkedCount', () => { it('sets linkedCount from API totalItems', async () => { mockGet.mockResolvedValue({ success: true, data: { totalItems: 5, member: [] }, }) const guard = useCategoryEditGuard(GUARD_CONFIG) await guard.loadLinkedCount('mt-1') expect(guard.linkedCount.value).toBe(5) expect(guard.isRestrictedMode.value).toBe(true) expect(mockGet).toHaveBeenCalledWith( expect.stringContaining('/composants?'), ) }) it('sets linkedCount 0 when API returns 0 items', async () => { mockGet.mockResolvedValue({ success: true, data: { totalItems: 0, member: [] }, }) const guard = useCategoryEditGuard(GUARD_CONFIG) await guard.loadLinkedCount('mt-1') expect(guard.linkedCount.value).toBe(0) expect(guard.isRestrictedMode.value).toBe(false) }) it('extracts totalItems from hydra:totalItems format', async () => { mockGet.mockResolvedValue({ success: true, data: { 'hydra:totalItems': 3, 'hydra:member': [{}, {}, {}] }, }) const guard = useCategoryEditGuard(GUARD_CONFIG) await guard.loadLinkedCount('mt-1') expect(guard.linkedCount.value).toBe(3) }) it('falls back to member.length when no totalItems', async () => { mockGet.mockResolvedValue({ success: true, data: { member: [{}, {}] }, }) const guard = useCategoryEditGuard(GUARD_CONFIG) await guard.loadLinkedCount('mt-1') expect(guard.linkedCount.value).toBe(2) }) it('falls back to hydra:member.length', async () => { mockGet.mockResolvedValue({ success: true, data: { 'hydra:member': [{}] }, }) const guard = useCategoryEditGuard(GUARD_CONFIG) await guard.loadLinkedCount('mt-1') expect(guard.linkedCount.value).toBe(1) }) it('sets linkedCount 0 on API failure', async () => { mockGet.mockResolvedValue({ success: false }) const guard = useCategoryEditGuard(GUARD_CONFIG) await guard.loadLinkedCount('mt-1') expect(guard.linkedCount.value).toBe(0) expect(guard.isRestrictedMode.value).toBe(false) }) it('sets linkedCount 0 on exception', async () => { mockGet.mockRejectedValue(new Error('Network error')) const guard = useCategoryEditGuard(GUARD_CONFIG) await guard.loadLinkedCount('mt-1') expect(guard.linkedCount.value).toBe(0) }) it('sends correct filter parameters', async () => { mockGet.mockResolvedValue({ success: true, data: { totalItems: 0 } }) const guard = useCategoryEditGuard(GUARD_CONFIG) await guard.loadLinkedCount('abc-123') const callUrl = mockGet.mock.calls[0][0] as string expect(callUrl).toContain('itemsPerPage=1') expect(callUrl).toContain('typeComposant=%2Fapi%2Fmodel_types%2Fabc-123') }) }) // --------------------------------------------------------------------------- // restrictedModeMessage // --------------------------------------------------------------------------- describe('restrictedModeMessage', () => { it('shows singular message for 1 linked item', async () => { mockGet.mockResolvedValue({ success: true, data: { totalItems: 1 }, }) const guard = useCategoryEditGuard(GUARD_CONFIG) await guard.loadLinkedCount('mt-1') expect(guard.restrictedModeMessage.value).toContain('1 composant') expect(guard.restrictedModeMessage.value).toContain('Mode restreint') }) it('shows plural message for multiple linked items', async () => { mockGet.mockResolvedValue({ success: true, data: { totalItems: 5 }, }) const guard = useCategoryEditGuard(GUARD_CONFIG) await guard.loadLinkedCount('mt-1') expect(guard.restrictedModeMessage.value).toContain('5 composants') expect(guard.restrictedModeMessage.value).toContain('renommer les existants') }) it('uses custom labels from config', async () => { mockGet.mockResolvedValue({ success: true, data: { totalItems: 3 }, }) const guard = useCategoryEditGuard({ endpoint: '/pieces', filterKey: 'typePiece', labels: { singular: 'pièce', plural: 'pièces', verifying: 'Vérification...' }, }) await guard.loadLinkedCount('mt-1') expect(guard.restrictedModeMessage.value).toContain('3 pièces') }) }) // --------------------------------------------------------------------------- // isSubmitBlocked & submitBlockMessage // --------------------------------------------------------------------------- describe('submit blocking', () => { it('blocks submit during loading', () => { const guard = useCategoryEditGuard(GUARD_CONFIG) // Simulate loading state by starting a load without awaiting mockGet.mockReturnValue(new Promise(() => {})) // Never resolves guard.loadLinkedCount('mt-1') expect(guard.linkedLoading.value).toBe(true) expect(guard.isSubmitBlocked.value).toBe(true) expect(guard.submitBlockMessage.value).toBe(GUARD_CONFIG.labels.verifying) }) it('unblocks submit after loading completes', async () => { mockGet.mockResolvedValue({ success: true, data: { totalItems: 5 }, }) const guard = useCategoryEditGuard(GUARD_CONFIG) await guard.loadLinkedCount('mt-1') expect(guard.isSubmitBlocked.value).toBe(false) expect(guard.submitBlockMessage.value).toBe('') }) }) // --------------------------------------------------------------------------- // guardSubmitOrNotify // --------------------------------------------------------------------------- describe('guardSubmitOrNotify', () => { it('returns false when not blocked', async () => { mockGet.mockResolvedValue({ success: true, data: { totalItems: 0 }, }) const guard = useCategoryEditGuard(GUARD_CONFIG) await guard.loadLinkedCount('mt-1') expect(guard.guardSubmitOrNotify()).toBe(false) expect(mockShowInfo).not.toHaveBeenCalled() }) it('returns true and shows info when blocked', () => { const guard = useCategoryEditGuard(GUARD_CONFIG) // Simulate loading mockGet.mockReturnValue(new Promise(() => {})) guard.loadLinkedCount('mt-1') expect(guard.guardSubmitOrNotify()).toBe(true) expect(mockShowInfo).toHaveBeenCalled() }) })