From 1e40334e1133afad44b889990dd6a25db112a9e1 Mon Sep 17 00:00:00 2001 From: r-dev Date: Mon, 6 Apr 2026 15:12:13 +0200 Subject: [PATCH] test(component-create) : add creation flow data integrity tests Co-Authored-By: Claude Opus 4.6 (1M context) --- .../composables/useComponentCreate.test.ts | 348 ++++++++++++++++++ 1 file changed, 348 insertions(+) create mode 100644 frontend/tests/composables/useComponentCreate.test.ts diff --git a/frontend/tests/composables/useComponentCreate.test.ts b/frontend/tests/composables/useComponentCreate.test.ts new file mode 100644 index 0000000..2edd034 --- /dev/null +++ b/frontend/tests/composables/useComponentCreate.test.ts @@ -0,0 +1,348 @@ +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 +// --------------------------------------------------------------------------- + +vi.mock('~/shared/utils/structureAssignmentHelpers', () => ({ + hasAssignments: () => false, + initializeStructureAssignments: () => null, + isAssignmentNodeComplete: () => true, + serializeStructureAssignments: () => null, +})) + +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') + }) +})