From 972f30e7720c957b0dc1f47d72e2762a6f3de391 Mon Sep 17 00:00:00 2001 From: r-dev Date: Mon, 6 Apr 2026 15:55:37 +0200 Subject: [PATCH] test(component-create) : add structure, error path, and null handling tests Co-Authored-By: Claude Opus 4.6 (1M context) --- .../composables/useComponentCreate.test.ts | 395 +++++++++++++++++- 1 file changed, 392 insertions(+), 3 deletions(-) diff --git a/frontend/tests/composables/useComponentCreate.test.ts b/frontend/tests/composables/useComponentCreate.test.ts index 2edd034..082051b 100644 --- a/frontend/tests/composables/useComponentCreate.test.ts +++ b/frontend/tests/composables/useComponentCreate.test.ts @@ -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') + }) +})