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:
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user