import { describe, it, expect, vi, beforeEach } from 'vitest' import { ref } from 'vue' // --------------------------------------------------------------------------- // Import under test (after mocks) // --------------------------------------------------------------------------- import { useMachineDetailData } from '~/composables/useMachineDetailData' // --------------------------------------------------------------------------- // Mock data — realistic /machines/{id}/structure response // --------------------------------------------------------------------------- const MACHINE_ID = 'cl-machine-abc123' const SITE_ID = 'cl-site-nord-001' const COMPONENT_LINK_ID = 'cl-mcl-001' const PIECE_LINK_ID = 'cl-mpl-001' const PRODUCT_LINK_ID = 'cl-mprl-001' const COMPOSANT_ID = 'cl-comp-moteur-001' const PIECE_ID = 'cl-piece-roul-001' const PRODUCT_ID = 'cl-prod-graisse-001' const CONSTRUCTEUR_ID = 'cstr-skf-001' const mockConstructeurSKF = { id: CONSTRUCTEUR_ID, name: 'SKF', email: 'contact@skf.com', phone: '+33 1 23 45 67 89', } const mockStructureResponse = { success: true, data: { machine: { id: MACHINE_ID, name: 'Presse hydraulique PH-200', reference: 'MACH-PH-200', prix: 150000, siteId: SITE_ID, site: { id: SITE_ID, name: 'Usine Nord' }, documents: [{ id: 'doc-001', name: 'Manuel PH-200.pdf', type: 'manual' }], customFieldValues: [ { id: 'mcfv-001', value: 'SN-2025-PH200', customField: { id: 'mcf-001', name: 'Serial Number', type: 'text', required: true, options: [], defaultValue: null, orderIndex: 0, machineContextOnly: false, }, }, ], customFields: [ { id: 'mcf-001', name: 'Serial Number', type: 'text', required: true, options: [], defaultValue: null, orderIndex: 0, machineContextOnly: false, }, ], constructeurs: [ { id: 'cl-mconst-001', constructeur: mockConstructeurSKF, supplierReference: 'SKF-PH200', }, ], }, componentLinks: [ { id: COMPONENT_LINK_ID, composant: { id: COMPOSANT_ID, name: 'Moteur principal', reference: 'COMP-MOT-001', prix: 12500, typeComposant: { id: 'tc-moteur', name: 'Moteur electrique' }, constructeurs: [mockConstructeurSKF], constructeurIds: [CONSTRUCTEUR_ID], documents: [], customFields: [ { definitionId: 'cf-comp-001', name: 'Tension nominale', type: 'number', value: '380', }, ], customFieldValues: [], }, overrides: { name: 'Moteur principal PH-200', reference: 'COMP-MOT-PH200', prix: 13000, }, contextCustomFields: [ { id: 'ctx-cf-001', name: 'Position sur machine', type: 'text', machineContextOnly: true, }, ], contextCustomFieldValues: [ { id: 'ctx-cfv-001', value: 'Bloc moteur gauche', customField: { id: 'ctx-cf-001', name: 'Position sur machine', type: 'text', machineContextOnly: true, }, }, ], pieceLinks: [ { id: PIECE_LINK_ID, piece: { id: PIECE_ID, name: 'Roulement 6205', reference: 'ROUL-6205', prix: 45.90, typePiece: { id: 'tp-bearing', name: 'Roulement' }, constructeurs: [mockConstructeurSKF], documents: [], customFields: [], }, overrides: { name: 'Roulement 6205-RS', }, quantity: 2, parentComponentLinkId: COMPONENT_LINK_ID, contextCustomFields: [], contextCustomFieldValues: [ { id: 'ctx-cfv-piece-001', value: 'Cote entrainement', customField: { id: 'ctx-cf-piece-001', name: 'Emplacement', type: 'text', machineContextOnly: true, }, }, ], }, ], childLinks: [], }, ], pieceLinks: [], productLinks: [ { id: PRODUCT_LINK_ID, product: { id: PRODUCT_ID, name: 'Graisse LGMT2', reference: 'LUB-LGMT2', prix: 45.90, }, overrides: null, }, ], }, } // Response with NO overrides — for fallback testing const mockStructureNoOverrides = { success: true, data: { machine: { ...mockStructureResponse.data.machine, }, componentLinks: [ { id: COMPONENT_LINK_ID, composant: { id: COMPOSANT_ID, name: 'Moteur principal', reference: 'COMP-MOT-001', prix: 12500, typeComposant: { id: 'tc-moteur', name: 'Moteur electrique' }, constructeurs: [], documents: [], customFields: [], customFieldValues: [], }, overrides: null, contextCustomFields: [], contextCustomFieldValues: [], pieceLinks: [ { id: PIECE_LINK_ID, piece: { id: PIECE_ID, name: 'Roulement 6205', reference: 'ROUL-6205', prix: 45.90, typePiece: { id: 'tp-bearing', name: 'Roulement' }, constructeurs: [], documents: [], customFields: [], }, overrides: null, quantity: 1, parentComponentLinkId: COMPONENT_LINK_ID, contextCustomFields: [], contextCustomFieldValues: [], }, ], childLinks: [], }, ], pieceLinks: [], productLinks: [], }, } // --------------------------------------------------------------------------- // Mocks — all composables used by useMachineDetailData // --------------------------------------------------------------------------- const mockGet = vi.fn() const mockPatch = vi.fn() const mockPost = vi.fn() const mockDel = vi.fn() vi.mock('~/composables/useApi', () => ({ useApi: () => ({ get: mockGet, patch: mockPatch, post: mockPost, delete: mockDel, }), })) vi.mock('~/composables/useToast', () => ({ useToast: () => ({ showSuccess: vi.fn(), showError: vi.fn(), showInfo: vi.fn(), showToast: vi.fn(), toasts: { value: [] }, clearAll: vi.fn(), }), })) vi.mock('~/composables/useMachines', () => ({ useMachines: () => ({ updateMachine: vi.fn().mockResolvedValue({ success: true }), updateStructure: vi.fn().mockResolvedValue({ success: true }), }), })) vi.mock('~/composables/useComposants', () => ({ useComposants: () => ({ updateComposant: vi.fn().mockResolvedValue({ success: true }), }), })) vi.mock('~/composables/usePieces', () => ({ usePieces: () => ({ updatePiece: vi.fn().mockResolvedValue({ success: true }), }), })) vi.mock('~/composables/useComponentTypes', () => ({ useComponentTypes: () => ({ componentTypes: ref([ { id: 'tc-moteur', name: 'Moteur electrique' }, ]), loadComponentTypes: vi.fn().mockResolvedValue(undefined), }), })) vi.mock('~/composables/usePieceTypes', () => ({ usePieceTypes: () => ({ pieceTypes: ref([ { id: 'tp-bearing', name: 'Roulement' }, ]), loadPieceTypes: vi.fn().mockResolvedValue(undefined), }), })) vi.mock('~/composables/useCustomFields', () => ({ useCustomFields: () => ({ upsertCustomFieldValue: vi.fn().mockResolvedValue({ success: true }), updateCustomFieldValue: vi.fn().mockResolvedValue({ success: true }), }), })) vi.mock('~/composables/useConstructeurs', () => ({ useConstructeurs: () => ({ constructeurs: ref([mockConstructeurSKF]), loadConstructeurs: vi.fn().mockResolvedValue(undefined), }), })) vi.mock('~/composables/useSites', () => ({ useSites: () => ({ sites: ref([{ id: SITE_ID, name: 'Usine Nord' }]), loadSites: vi.fn().mockResolvedValue(undefined), }), })) vi.mock('~/composables/useProducts', () => ({ useProducts: () => ({ products: ref([ { id: PRODUCT_ID, name: 'Graisse LGMT2', reference: 'LUB-LGMT2', prix: 45.90 }, ]), loadProducts: vi.fn().mockResolvedValue(undefined), }), })) vi.mock('~/composables/useDocuments', () => ({ useDocuments: () => ({ uploadDocuments: vi.fn().mockResolvedValue({ success: true }), deleteDocument: vi.fn().mockResolvedValue({ success: true }), loadDocumentsByMachine: vi.fn().mockResolvedValue({ success: true, data: [] }), loadDocumentsByProduct: vi.fn().mockResolvedValue({ success: true, data: [] }), }), })) vi.mock('~/composables/useConstructeurLinks', () => ({ useConstructeurLinks: () => ({ fetchLinks: vi.fn().mockResolvedValue([]), syncLinks: vi.fn().mockResolvedValue({ success: true }), }), })) vi.mock('~/utils/printTemplates/machineReport', () => ({ buildMachinePrintContext: vi.fn(), buildMachinePrintHtml: vi.fn().mockReturnValue(''), })) vi.mock('~/utils/documentPreview', () => ({ canPreviewDocument: vi.fn().mockReturnValue(false), })) vi.mock('~/shared/utils/documentDisplayUtils', () => ({ downloadDocument: vi.fn(), })) // --------------------------------------------------------------------------- // Setup // --------------------------------------------------------------------------- beforeEach(() => { vi.clearAllMocks() mockGet.mockResolvedValue(mockStructureResponse) }) // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- async function loadAndReturn(responseOverride?: unknown) { if (responseOverride) { mockGet.mockResolvedValue(responseOverride) } const result = useMachineDetailData(MACHINE_ID) await result.loadMachineData() return result } // =========================================================================== // 1. Hierarchy loading // =========================================================================== describe('hierarchy loading', () => { it('loads machine with all core fields', async () => { const { machine, machineName, machineReference, machineSiteId } = await loadAndReturn() expect(machine.value).not.toBeNull() expect(machine.value!.id).toBe(MACHINE_ID) expect(machine.value!.name).toBe('Presse hydraulique PH-200') expect(machine.value!.reference).toBe('MACH-PH-200') expect(machine.value!.prix).toBe(150000) expect(machineName.value).toBe('Presse hydraulique PH-200') expect(machineReference.value).toBe('MACH-PH-200') expect(machineSiteId.value).toBe(SITE_ID) }) it('calls GET /machines/{id}/structure', async () => { await loadAndReturn() expect(mockGet).toHaveBeenCalledWith(`/machines/${MACHINE_ID}/structure`) }) it('loads componentLinks from structure response', async () => { const { machineComponentLinks } = await loadAndReturn() expect(machineComponentLinks.value).toHaveLength(1) expect(machineComponentLinks.value[0]!.id).toBe(COMPONENT_LINK_ID) }) it('builds component hierarchy with composant data', async () => { const { components } = await loadAndReturn() expect(components.value.length).toBeGreaterThanOrEqual(1) const comp = components.value[0]! expect(comp.composantId).toBe(COMPOSANT_ID) }) it('loads piece links nested under their parent componentLink', async () => { const { components } = await loadAndReturn() const comp = components.value[0]! const pieces = comp.pieces as Record[] expect(pieces).toBeDefined() expect(pieces.length).toBeGreaterThanOrEqual(1) const piece = pieces[0]! expect(piece.pieceId).toBe(PIECE_ID) expect(piece.parentComponentLinkId).toBe(COMPONENT_LINK_ID) }) it('preserves piece quantity', async () => { const { components } = await loadAndReturn() const comp = components.value[0]! const pieces = comp.pieces as Record[] const piece = pieces[0]! expect(piece.quantity).toBe(2) }) it('loads product links at machine level', async () => { const { machineProductLinks } = await loadAndReturn() expect(machineProductLinks.value).toHaveLength(1) expect(machineProductLinks.value[0]!.id).toBe(PRODUCT_LINK_ID) }) it('preserves machine documents', async () => { const { machine } = await loadAndReturn() const docs = machine.value!.documents as unknown[] expect(docs).toHaveLength(1) }) it('preserves machine customFieldValues', async () => { const { machine } = await loadAndReturn() const cfv = machine.value!.customFieldValues as Record[] expect(cfv).toHaveLength(1) expect((cfv[0] as any).value).toBe('SN-2025-PH200') }) it('sets loading to false after data load', async () => { const { loading } = await loadAndReturn() expect(loading.value).toBe(false) }) it('handles failed API response gracefully', async () => { const { machine, components, pieces } = await loadAndReturn({ success: false, error: 'Not found', }) expect(machine.value).toBeNull() expect(components.value).toEqual([]) expect(pieces.value).toEqual([]) }) it('handles invalid machine payload gracefully', async () => { const { machine } = await loadAndReturn({ success: true, data: null, }) expect(machine.value).toBeNull() }) }) // =========================================================================== // 2. Overrides // =========================================================================== describe('overrides on component links', () => { it('uses nameOverride when present', async () => { const { components } = await loadAndReturn() const comp = components.value[0]! expect(comp.name).toBe('Moteur principal PH-200') }) it('falls back to composant.name when nameOverride is null', async () => { const { components } = await loadAndReturn(mockStructureNoOverrides) const comp = components.value[0]! expect(comp.name).toBe('Moteur principal') }) it('uses referenceOverride when present', async () => { const { components } = await loadAndReturn() const comp = components.value[0]! expect(comp.reference).toBe('COMP-MOT-PH200') }) it('falls back to composant.reference when referenceOverride is null', async () => { const { components } = await loadAndReturn(mockStructureNoOverrides) const comp = components.value[0]! expect(comp.reference).toBe('COMP-MOT-001') }) it('uses prixOverride when present', async () => { const { components } = await loadAndReturn() const comp = components.value[0]! expect(comp.prix).toBe(13000) }) it('falls back to composant.prix when prixOverride is null', async () => { const { components } = await loadAndReturn(mockStructureNoOverrides) const comp = components.value[0]! expect(comp.prix).toBe(12500) }) }) describe('overrides on piece links', () => { it('uses piece nameOverride when present', async () => { const { components } = await loadAndReturn() const piece = (components.value[0]!.pieces as Record[])[0]! expect(piece.name).toBe('Roulement 6205-RS') }) it('falls back to piece.name when nameOverride is null', async () => { const { components } = await loadAndReturn(mockStructureNoOverrides) const piece = (components.value[0]!.pieces as Record[])[0]! expect(piece.name).toBe('Roulement 6205') }) it('preserves piece reference from underlying entity when no override', async () => { const { components } = await loadAndReturn() const piece = (components.value[0]!.pieces as Record[])[0]! // The override only has name, so reference comes from the piece entity expect(piece.reference).toBe('ROUL-6205') }) it('preserves piece prix from underlying entity when no override', async () => { const { components } = await loadAndReturn() const piece = (components.value[0]!.pieces as Record[])[0]! expect(piece.prix).toBe(45.90) }) }) // =========================================================================== // 3. Custom field values on links (context fields) // =========================================================================== describe('contextCustomFieldValues on component links', () => { it('loads contextCustomFieldValues on component hierarchy nodes', async () => { const { components } = await loadAndReturn() const comp = components.value[0]! const ctxValues = comp.contextCustomFieldValues as Record[] expect(ctxValues).toBeDefined() expect(ctxValues).toHaveLength(1) expect((ctxValues[0] as any).value).toBe('Bloc moteur gauche') expect((ctxValues[0] as any).customField.name).toBe('Position sur machine') }) it('loads contextCustomFields definitions on component hierarchy nodes', async () => { const { components } = await loadAndReturn() const comp = components.value[0]! const ctxFields = comp.contextCustomFields as Record[] expect(ctxFields).toBeDefined() expect(ctxFields).toHaveLength(1) expect((ctxFields[0] as any).name).toBe('Position sur machine') expect((ctxFields[0] as any).machineContextOnly).toBe(true) }) }) describe('contextCustomFieldValues on piece links', () => { it('loads contextCustomFieldValues on piece hierarchy nodes', async () => { const { components } = await loadAndReturn() const piece = (components.value[0]!.pieces as Record[])[0]! const ctxValues = piece.contextCustomFieldValues as Record[] expect(ctxValues).toBeDefined() expect(ctxValues).toHaveLength(1) expect((ctxValues[0] as any).value).toBe('Cote entrainement') }) it('has empty contextCustomFieldValues when none provided', async () => { const { components } = await loadAndReturn(mockStructureNoOverrides) const piece = (components.value[0]!.pieces as Record[])[0]! const ctxValues = piece.contextCustomFieldValues as Record[] expect(ctxValues).toEqual([]) }) }) // =========================================================================== // 4. Constructeur links on machine // =========================================================================== describe('constructeur links on machine', () => { it('parses constructeur links from machine data', async () => { const { constructeurLinks } = await loadAndReturn() expect(constructeurLinks.value).toHaveLength(1) expect(constructeurLinks.value[0]!.constructeurId).toBe(CONSTRUCTEUR_ID) expect(constructeurLinks.value[0]!.supplierReference).toBe('SKF-PH200') }) it('populates machineConstructeurIds from links', async () => { const { machineConstructeurIds } = await loadAndReturn() expect(machineConstructeurIds.value).toContain(CONSTRUCTEUR_ID) }) it('stores original constructeur links for cancel rollback', async () => { const { originalConstructeurLinks } = await loadAndReturn() expect(originalConstructeurLinks.value).toHaveLength(1) expect(originalConstructeurLinks.value[0]!.constructeurId).toBe(CONSTRUCTEUR_ID) }) it('hasMachineConstructeur is true when constructeur present', async () => { const { hasMachineConstructeur } = await loadAndReturn() expect(hasMachineConstructeur.value).toBe(true) }) it('resolves constructeur display objects', async () => { const { machineConstructeursDisplay } = await loadAndReturn() expect(machineConstructeursDisplay.value.length).toBeGreaterThanOrEqual(1) const display = machineConstructeursDisplay.value[0] as any expect(display.name).toBe('SKF') }) }) // =========================================================================== // 5. Site (required) // =========================================================================== describe('site loaded with machine data', () => { it('machineSiteId is populated from machine payload', async () => { const { machineSiteId } = await loadAndReturn() expect(machineSiteId.value).toBe(SITE_ID) }) it('sites ref is available for dropdowns', async () => { const { sites } = await loadAndReturn() expect(sites.value).toHaveLength(1) expect((sites.value[0] as any).name).toBe('Usine Nord') }) it('machine.site object is preserved in machine ref', async () => { const { machine } = await loadAndReturn() const site = machine.value!.site as Record expect(site.id).toBe(SITE_ID) expect(site.name).toBe('Usine Nord') }) }) // =========================================================================== // 6. UI state defaults // =========================================================================== describe('UI state defaults', () => { it('isEditMode starts as false', () => { const { isEditMode } = useMachineDetailData(MACHINE_ID) expect(isEditMode.value).toBe(false) }) it('saving starts as false', () => { const { saving } = useMachineDetailData(MACHINE_ID) expect(saving.value).toBe(false) }) it('loading starts as true', () => { const { loading } = useMachineDetailData(MACHINE_ID) expect(loading.value).toBe(true) }) })