import { describe, it, expect, vi, beforeEach } from 'vitest' import { mockLinkSKF, mockLinkFAG } from '../fixtures/mockData' // --------------------------------------------------------------------------- // Mocks — API layer // --------------------------------------------------------------------------- const mockGet = vi.fn() const mockPost = vi.fn() const mockPatch = vi.fn() const mockDel = vi.fn() const mockPostFormData = vi.fn() vi.mock('~/composables/useApi', () => ({ useApi: () => ({ get: mockGet, post: mockPost, patch: mockPatch, put: vi.fn(), delete: mockDel, postFormData: mockPostFormData, }), })) // --------------------------------------------------------------------------- // Mocks — Toast // --------------------------------------------------------------------------- const mockShowSuccess = vi.fn() const mockShowError = vi.fn() vi.mock('~/composables/useToast', () => ({ useToast: () => ({ showSuccess: mockShowSuccess, showError: mockShowError, showInfo: vi.fn(), showToast: vi.fn(), toasts: { value: [] }, clearAll: vi.fn(), }), })) // --------------------------------------------------------------------------- // Mocks — useComposants (createComposant) // --------------------------------------------------------------------------- const mockCreateComposant = vi.fn() vi.mock('~/composables/useComposants', () => ({ useComposants: () => ({ createComposant: mockCreateComposant, composants: { value: [] }, loading: { value: false }, }), })) // --------------------------------------------------------------------------- // Mocks — usePieces, useProducts // --------------------------------------------------------------------------- vi.mock('~/composables/usePieces', () => ({ usePieces: () => ({ pieces: { value: [] }, loading: { value: false }, }), })) vi.mock('~/composables/useProducts', () => ({ useProducts: () => ({ products: { value: [] }, loading: { value: false }, }), })) // --------------------------------------------------------------------------- // Mocks — useComponentTypes, usePieceTypes, useProductTypes // --------------------------------------------------------------------------- const mockLoadComponentTypes = vi.fn().mockResolvedValue(undefined) const mockComponentTypes = { value: [] as any[] } vi.mock('~/composables/useComponentTypes', () => ({ useComponentTypes: () => ({ componentTypes: mockComponentTypes, loadComponentTypes: mockLoadComponentTypes, loadingComponentTypes: { value: false }, }), })) vi.mock('~/composables/usePieceTypes', () => ({ usePieceTypes: () => ({ pieceTypes: { value: [] }, loadPieceTypes: vi.fn().mockResolvedValue(undefined), }), })) vi.mock('~/composables/useProductTypes', () => ({ useProductTypes: () => ({ productTypes: { value: [] }, loadProductTypes: vi.fn().mockResolvedValue(undefined), }), })) // --------------------------------------------------------------------------- // Mocks — useDocuments (uploadDocuments) // --------------------------------------------------------------------------- const mockUploadDocuments = vi.fn() vi.mock('~/composables/useDocuments', () => ({ useDocuments: () => ({ uploadDocuments: mockUploadDocuments, documents: { value: [] }, loading: { value: false }, }), })) // --------------------------------------------------------------------------- // Mocks — useConstructeurLinks (syncLinks) // --------------------------------------------------------------------------- const mockSyncLinks = vi.fn().mockResolvedValue(undefined) vi.mock('~/composables/useConstructeurLinks', () => ({ useConstructeurLinks: () => ({ fetchLinks: vi.fn().mockResolvedValue([]), syncLinks: mockSyncLinks, }), })) // --------------------------------------------------------------------------- // Mocks — useCustomFieldInputs (saveAll) // --------------------------------------------------------------------------- const mockSaveAll = vi.fn().mockResolvedValue([]) const mockRefreshCF = vi.fn() vi.mock('~/composables/useCustomFieldInputs', () => ({ useCustomFieldInputs: () => ({ fields: { value: [] }, requiredFilled: { value: true }, saveAll: mockSaveAll, refresh: mockRefreshCF, }), })) // --------------------------------------------------------------------------- // Mocks — usePermissions (auto-imported in Nuxt) // --------------------------------------------------------------------------- // usePermissions is Nuxt auto-imported (no explicit import in source), // so we stub it as a global function. vi.stubGlobal('usePermissions', () => ({ canEdit: { value: true }, canManage: { value: true }, isAdmin: { value: false }, isGranted: () => true, })) // --------------------------------------------------------------------------- // Mocks — useConstructeurs (used by useComposants internally) // --------------------------------------------------------------------------- vi.mock('~/composables/useConstructeurs', () => ({ useConstructeurs: () => ({ ensureConstructeurs: vi.fn().mockResolvedValue([]), }), })) // --------------------------------------------------------------------------- // Mocks — shared utils that touch structure // --------------------------------------------------------------------------- const mockHasAssignments = vi.fn().mockReturnValue(false) const mockSerializeStructureAssignments = vi.fn().mockReturnValue(null) const mockIsAssignmentNodeComplete = vi.fn().mockReturnValue(true) vi.mock('~/shared/utils/structureAssignmentHelpers', () => ({ hasAssignments: (...args: any[]) => mockHasAssignments(...args), initializeStructureAssignments: () => null, isAssignmentNodeComplete: (...args: any[]) => mockIsAssignmentNodeComplete(...args), serializeStructureAssignments: (...args: any[]) => mockSerializeStructureAssignments(...args), })) vi.mock('~/shared/utils/structureDisplayUtils', () => ({ getStructurePieces: () => [], resolvePieceLabel: (p: any) => p?.name ?? '', resolveProductLabel: (p: any) => p?.name ?? '', resolveSubcomponentLabel: (p: any) => p?.name ?? '', fetchModelTypeNames: vi.fn().mockResolvedValue({}), buildTypeLabelMap: () => ({}), })) vi.mock('~/shared/modelUtils', () => ({ formatStructurePreview: () => '', normalizeStructureForEditor: (s: any) => s, })) vi.mock('~/shared/utils/errorMessages', () => ({ humanizeError: (msg: string) => msg, })) vi.mock('~/shared/constructeurUtils', () => ({ uniqueConstructeurIds: (ids: string[]) => [...new Set(ids)], constructeurIdsFromLinks: (links: any[]) => links.map((l: any) => l.constructeurId), })) // --------------------------------------------------------------------------- // Import under test (AFTER all vi.mock calls) // --------------------------------------------------------------------------- import { useComponentCreate } from '~/composables/useComponentCreate' // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- /** A minimal ModelType matching the `COMPONENT` category filter. */ const mockModelType = { id: 'tc-moteur', name: 'Moteur électrique', category: 'COMPONENT', structure: null, } beforeEach(() => { vi.clearAllMocks() // Provide at least one COMPONENT type so selectedType resolves mockComponentTypes.value = [mockModelType] }) // --------------------------------------------------------------------------- // submitCreation — payload completeness // --------------------------------------------------------------------------- describe('submitCreation — payload completeness', () => { it('includes all form fields in createComposant payload', async () => { const createdComp = { id: 'comp-new-001', name: 'Moteur principal' } mockCreateComposant.mockResolvedValue({ success: true, data: createdComp }) const composable = useComponentCreate() // Select a type composable.selectedTypeId.value = 'tc-moteur' // Wait a tick so watchers fire await new Promise(r => setTimeout(r, 0)) // Fill form fields composable.creationForm.name = 'Moteur principal' composable.creationForm.description = 'Un moteur triphasé' composable.creationForm.reference = 'MOT-001' composable.creationForm.prix = '1500' await composable.submitCreation() expect(mockCreateComposant).toHaveBeenCalledTimes(1) const payload = mockCreateComposant.mock.calls[0]![0] expect(payload).toMatchObject({ name: 'Moteur principal', description: 'Un moteur triphasé', reference: 'MOT-001', prix: '1500', typeComposantId: 'tc-moteur', }) }) it('saves custom fields after component creation (saveAll is called)', async () => { const createdComp = { id: 'comp-cf-001', name: 'Composant CF' } mockCreateComposant.mockResolvedValue({ success: true, data: createdComp }) const composable = useComponentCreate() composable.selectedTypeId.value = 'tc-moteur' await new Promise(r => setTimeout(r, 0)) composable.creationForm.name = 'Composant CF' await composable.submitCreation() expect(mockCreateComposant).toHaveBeenCalledTimes(1) expect(mockSaveAll).toHaveBeenCalledTimes(1) }) it('syncs constructeur links after creation with correct entity type and ID', async () => { const createdComp = { id: 'comp-link-001', name: 'Composant Links' } mockCreateComposant.mockResolvedValue({ success: true, data: createdComp }) const composable = useComponentCreate() composable.selectedTypeId.value = 'tc-moteur' await new Promise(r => setTimeout(r, 0)) composable.creationForm.name = 'Composant Links' // Add constructeur links composable.constructeurLinks.value = [mockLinkSKF, mockLinkFAG] await composable.submitCreation() expect(mockSyncLinks).toHaveBeenCalledTimes(1) expect(mockSyncLinks).toHaveBeenCalledWith( 'composant', 'comp-link-001', [], [mockLinkSKF, mockLinkFAG], ) }) it('uploads documents with correct composantId context', async () => { const createdComp = { id: 'comp-doc-001', name: 'Composant Docs' } mockCreateComposant.mockResolvedValue({ success: true, data: createdComp }) mockUploadDocuments.mockResolvedValue({ success: true, data: [] }) const composable = useComponentCreate() composable.selectedTypeId.value = 'tc-moteur' await new Promise(r => setTimeout(r, 0)) composable.creationForm.name = 'Composant Docs' // Simulate selected documents const fakeFile = new File(['content'], 'schema.pdf', { type: 'application/pdf' }) composable.selectedDocuments.value = [fakeFile] await composable.submitCreation() expect(mockUploadDocuments).toHaveBeenCalledTimes(1) expect(mockUploadDocuments).toHaveBeenCalledWith( { files: [fakeFile], context: { composantId: 'comp-doc-001' }, }, { updateStore: false }, ) }) it('does not crash with zero constructeurs', async () => { const createdComp = { id: 'comp-no-cstr', name: 'Composant Simple' } mockCreateComposant.mockResolvedValue({ success: true, data: createdComp }) const composable = useComponentCreate() composable.selectedTypeId.value = 'tc-moteur' await new Promise(r => setTimeout(r, 0)) composable.creationForm.name = 'Composant Simple' // Ensure no constructeur links composable.constructeurLinks.value = [] await composable.submitCreation() expect(mockCreateComposant).toHaveBeenCalledTimes(1) expect(mockSyncLinks).not.toHaveBeenCalled() expect(mockShowSuccess).toHaveBeenCalledWith('Composant créé avec succès') }) }) // --------------------------------------------------------------------------- // Structure serialization in payload // --------------------------------------------------------------------------- describe('submitCreation — structure serialization in payload', () => { it('includes structure key with serialized data when assignments exist', async () => { const createdComp = { id: 'comp-struct-001', name: 'Composant Structure' } mockCreateComposant.mockResolvedValue({ success: true, data: createdComp }) const fakeSerializedStructure = { path: 'root', definition: { typeComposantId: 'tc-moteur' }, pieces: [{ path: 'root:piece-0', definition: { typePieceId: 'tp-001' }, selectedPieceId: 'piece-abc' }], } mockHasAssignments.mockReturnValue(true) mockSerializeStructureAssignments.mockReturnValue(fakeSerializedStructure) const composable = useComponentCreate() composable.selectedTypeId.value = 'tc-moteur' await new Promise(r => setTimeout(r, 0)) composable.creationForm.name = 'Composant Structure' // Set a non-null structureAssignments so the composable considers it present composable.structureAssignments.value = { path: 'root', definition: {} as any, selectedComponentId: '', pieces: [{ path: 'root:piece-0', definition: {} as any, selectedPieceId: 'piece-abc' }], products: [], subcomponents: [], } await composable.submitCreation() expect(mockCreateComposant).toHaveBeenCalledTimes(1) const payload = mockCreateComposant.mock.calls[0]![0] expect(payload.structure).toEqual(fakeSerializedStructure) }) it('does not include structure key when no assignments exist', async () => { const createdComp = { id: 'comp-nostruct-001', name: 'Composant No Structure' } mockCreateComposant.mockResolvedValue({ success: true, data: createdComp }) // Reset to default: no assignments mockHasAssignments.mockReturnValue(false) mockSerializeStructureAssignments.mockReturnValue(null) const composable = useComponentCreate() composable.selectedTypeId.value = 'tc-moteur' await new Promise(r => setTimeout(r, 0)) composable.creationForm.name = 'Composant No Structure' await composable.submitCreation() expect(mockCreateComposant).toHaveBeenCalledTimes(1) const payload = mockCreateComposant.mock.calls[0]![0] expect(payload.structure).toBeUndefined() }) it('does not include structure key when serializeStructureAssignments returns null', async () => { const createdComp = { id: 'comp-sernull-001', name: 'Composant Serialize Null' } mockCreateComposant.mockResolvedValue({ success: true, data: createdComp }) mockHasAssignments.mockReturnValue(true) mockSerializeStructureAssignments.mockReturnValue(null) const composable = useComponentCreate() composable.selectedTypeId.value = 'tc-moteur' await new Promise(r => setTimeout(r, 0)) composable.creationForm.name = 'Composant Serialize Null' composable.structureAssignments.value = { path: 'root', definition: {} as any, selectedComponentId: '', pieces: [], products: [], subcomponents: [], } await composable.submitCreation() expect(mockCreateComposant).toHaveBeenCalledTimes(1) const payload = mockCreateComposant.mock.calls[0]![0] expect(payload.structure).toBeUndefined() }) }) // --------------------------------------------------------------------------- // Prix / reference null handling // --------------------------------------------------------------------------- describe('submitCreation — prix and reference null handling', () => { it('does not send prix when prix is an empty string', async () => { const createdComp = { id: 'comp-noprix-001', name: 'Composant No Prix' } mockCreateComposant.mockResolvedValue({ success: true, data: createdComp }) // Reset structure mocks to default mockHasAssignments.mockReturnValue(false) mockSerializeStructureAssignments.mockReturnValue(null) const composable = useComponentCreate() composable.selectedTypeId.value = 'tc-moteur' await new Promise(r => setTimeout(r, 0)) composable.creationForm.name = 'Composant No Prix' composable.creationForm.prix = '' await composable.submitCreation() const payload = mockCreateComposant.mock.calls[0]![0] expect(payload).not.toHaveProperty('prix') }) it('does not send prix when prix is non-numeric (avoids NaN)', async () => { const createdComp = { id: 'comp-nanprix-001', name: 'Composant NaN Prix' } mockCreateComposant.mockResolvedValue({ success: true, data: createdComp }) mockHasAssignments.mockReturnValue(false) mockSerializeStructureAssignments.mockReturnValue(null) const composable = useComponentCreate() composable.selectedTypeId.value = 'tc-moteur' await new Promise(r => setTimeout(r, 0)) composable.creationForm.name = 'Composant NaN Prix' composable.creationForm.prix = 'not-a-number' await composable.submitCreation() const payload = mockCreateComposant.mock.calls[0]![0] expect(payload).not.toHaveProperty('prix') }) it('sends prix as stringified number when valid numeric string', async () => { const createdComp = { id: 'comp-validprix-001', name: 'Composant Valid Prix' } mockCreateComposant.mockResolvedValue({ success: true, data: createdComp }) mockHasAssignments.mockReturnValue(false) mockSerializeStructureAssignments.mockReturnValue(null) const composable = useComponentCreate() composable.selectedTypeId.value = 'tc-moteur' await new Promise(r => setTimeout(r, 0)) composable.creationForm.name = 'Composant Valid Prix' composable.creationForm.prix = ' 42.5 ' await composable.submitCreation() const payload = mockCreateComposant.mock.calls[0]![0] expect(payload.prix).toBe('42.5') }) it('does not send reference when reference is an empty string', async () => { const createdComp = { id: 'comp-noref-001', name: 'Composant No Ref' } mockCreateComposant.mockResolvedValue({ success: true, data: createdComp }) mockHasAssignments.mockReturnValue(false) mockSerializeStructureAssignments.mockReturnValue(null) const composable = useComponentCreate() composable.selectedTypeId.value = 'tc-moteur' await new Promise(r => setTimeout(r, 0)) composable.creationForm.name = 'Composant No Ref' composable.creationForm.reference = '' await composable.submitCreation() const payload = mockCreateComposant.mock.calls[0]![0] expect(payload).not.toHaveProperty('reference') }) it('does not send reference when reference is whitespace only', async () => { const createdComp = { id: 'comp-wsref-001', name: 'Composant WS Ref' } mockCreateComposant.mockResolvedValue({ success: true, data: createdComp }) mockHasAssignments.mockReturnValue(false) mockSerializeStructureAssignments.mockReturnValue(null) const composable = useComponentCreate() composable.selectedTypeId.value = 'tc-moteur' await new Promise(r => setTimeout(r, 0)) composable.creationForm.name = 'Composant WS Ref' composable.creationForm.reference = ' ' await composable.submitCreation() const payload = mockCreateComposant.mock.calls[0]![0] expect(payload).not.toHaveProperty('reference') }) }) // --------------------------------------------------------------------------- // Error paths // --------------------------------------------------------------------------- describe('submitCreation — error paths', () => { beforeEach(() => { mockHasAssignments.mockReturnValue(false) mockSerializeStructureAssignments.mockReturnValue(null) }) it('does not save custom fields when createComposant returns success: false', async () => { mockCreateComposant.mockResolvedValue({ success: false, error: 'Duplicate name' }) const composable = useComponentCreate() composable.selectedTypeId.value = 'tc-moteur' await new Promise(r => setTimeout(r, 0)) composable.creationForm.name = 'Composant Fail' await composable.submitCreation() expect(mockCreateComposant).toHaveBeenCalledTimes(1) expect(mockSaveAll).not.toHaveBeenCalled() expect(mockShowError).toHaveBeenCalledWith('Duplicate name') }) it('shows toast error when createComposant returns success: false with error message', async () => { mockCreateComposant.mockResolvedValue({ success: false, error: 'Server validation failed' }) const composable = useComponentCreate() composable.selectedTypeId.value = 'tc-moteur' await new Promise(r => setTimeout(r, 0)) composable.creationForm.name = 'Composant Error' await composable.submitCreation() expect(mockShowError).toHaveBeenCalledWith('Server validation failed') expect(mockShowSuccess).not.toHaveBeenCalled() }) it('shows warning for failed custom fields but still navigates (composant exists)', async () => { const createdComp = { id: 'comp-cf-warn-001', name: 'Composant CF Warn' } mockCreateComposant.mockResolvedValue({ success: true, data: createdComp }) mockSaveAll.mockResolvedValue(['Tension nominale', 'Certifié CE']) const composable = useComponentCreate() composable.selectedTypeId.value = 'tc-moteur' await new Promise(r => setTimeout(r, 0)) composable.creationForm.name = 'Composant CF Warn' await composable.submitCreation() // Custom field error toast is shown expect(mockShowError).toHaveBeenCalledWith( 'Erreur sur les champs : Tension nominale, Certifié CE', ) // But creation success toast is also shown (composant was created) expect(mockShowSuccess).toHaveBeenCalledWith('Composant créé avec succès') }) it('catches thrown exceptions and shows humanized error', async () => { mockCreateComposant.mockRejectedValue(new Error('Network timeout')) const composable = useComponentCreate() composable.selectedTypeId.value = 'tc-moteur' await new Promise(r => setTimeout(r, 0)) composable.creationForm.name = 'Composant Throw' await composable.submitCreation() expect(mockShowError).toHaveBeenCalledWith('Network timeout') expect(mockSaveAll).not.toHaveBeenCalled() }) it('resets submitting flag after failure', async () => { mockCreateComposant.mockResolvedValue({ success: false, error: 'fail' }) const composable = useComponentCreate() composable.selectedTypeId.value = 'tc-moteur' await new Promise(r => setTimeout(r, 0)) composable.creationForm.name = 'Composant Reset Flag' await composable.submitCreation() expect(composable.submitting.value).toBe(false) }) }) // --------------------------------------------------------------------------- // ProductId from structure // --------------------------------------------------------------------------- describe('submitCreation — productId from structure', () => { beforeEach(() => { mockHasAssignments.mockReturnValue(false) mockSerializeStructureAssignments.mockReturnValue(null) }) it('includes productId in payload when root product selection exists', async () => { const createdComp = { id: 'comp-prodid-001', name: 'Composant ProductId' } mockCreateComposant.mockResolvedValue({ success: true, data: createdComp }) const composable = useComponentCreate() composable.selectedTypeId.value = 'tc-moteur' await new Promise(r => setTimeout(r, 0)) composable.creationForm.name = 'Composant ProductId' // Set structure assignments with a root product selection composable.structureAssignments.value = { path: 'root', definition: {} as any, selectedComponentId: '', pieces: [], products: [ { path: 'root:product-0', definition: { typeProductId: 'tprod-001' } as any, selectedProductId: 'prod-selected-123', }, ], subcomponents: [], } await composable.submitCreation() const payload = mockCreateComposant.mock.calls[0]![0] expect(payload.productId).toBe('prod-selected-123') }) it('does not include productId when no root product is selected', async () => { const createdComp = { id: 'comp-noprodid-001', name: 'Composant No ProductId' } mockCreateComposant.mockResolvedValue({ success: true, data: createdComp }) const composable = useComponentCreate() composable.selectedTypeId.value = 'tc-moteur' await new Promise(r => setTimeout(r, 0)) composable.creationForm.name = 'Composant No ProductId' composable.structureAssignments.value = { path: 'root', definition: {} as any, selectedComponentId: '', pieces: [], products: [], subcomponents: [], } await composable.submitCreation() const payload = mockCreateComposant.mock.calls[0]![0] expect(payload).not.toHaveProperty('productId') }) it('does not include productId when product selection is empty string', async () => { const createdComp = { id: 'comp-emptyprod-001', name: 'Composant Empty Product' } mockCreateComposant.mockResolvedValue({ success: true, data: createdComp }) const composable = useComponentCreate() composable.selectedTypeId.value = 'tc-moteur' await new Promise(r => setTimeout(r, 0)) composable.creationForm.name = 'Composant Empty Product' composable.structureAssignments.value = { path: 'root', definition: {} as any, selectedComponentId: '', pieces: [], products: [ { path: 'root:product-0', definition: { typeProductId: 'tprod-001' } as any, selectedProductId: '', }, ], subcomponents: [], } await composable.submitCreation() const payload = mockCreateComposant.mock.calls[0]![0] expect(payload).not.toHaveProperty('productId') }) })