Files
Inventory/frontend/tests/composables/useMachineDetailData.test.ts
2026-04-06 16:52:42 +02:00

701 lines
21 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { ref } from 'vue'
// ---------------------------------------------------------------------------
// 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('<html></html>'),
}))
vi.mock('~/utils/documentPreview', () => ({
canPreviewDocument: vi.fn().mockReturnValue(false),
}))
vi.mock('~/shared/utils/documentDisplayUtils', () => ({
downloadDocument: vi.fn(),
}))
// ---------------------------------------------------------------------------
// Import under test (after mocks)
// ---------------------------------------------------------------------------
import { useMachineDetailData } from '~/composables/useMachineDetailData'
// ---------------------------------------------------------------------------
// 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<string, unknown>[]
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<string, unknown>[]
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<string, unknown>[]
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<string, unknown>[])[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<string, unknown>[])[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<string, unknown>[])[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<string, unknown>[])[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<string, unknown>[]
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<string, unknown>[]
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<string, unknown>[])[0]!
const ctxValues = piece.contextCustomFieldValues as Record<string, unknown>[]
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<string, unknown>[])[0]!
const ctxValues = piece.contextCustomFieldValues as Record<string, unknown>[]
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<string, unknown>
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)
})
})