test(component-create) : add structure, error path, and null handling tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-06 15:55:37 +02:00
parent 8af68c9628
commit 972f30e772

View File

@@ -172,11 +172,15 @@ vi.mock('~/composables/useConstructeurs', () => ({
// 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: () => false,
hasAssignments: (...args: any[]) => mockHasAssignments(...args),
initializeStructureAssignments: () => null,
isAssignmentNodeComplete: () => true,
serializeStructureAssignments: () => null,
isAssignmentNodeComplete: (...args: any[]) => mockIsAssignmentNodeComplete(...args),
serializeStructureAssignments: (...args: any[]) => mockSerializeStructureAssignments(...args),
}))
vi.mock('~/shared/utils/structureDisplayUtils', () => ({
@@ -346,3 +350,388 @@ describe('submitCreation — payload completeness', () => {
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')
})
})