test(component-edit) : add document, 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:13 +02:00
parent eb68336723
commit 8af68c9628

View File

@@ -112,11 +112,15 @@ vi.mock('~/composables/useProductTypes', () => ({
// Mocks — useDocuments
// ---------------------------------------------------------------------------
const mockLoadDocumentsByComponent = vi.fn().mockResolvedValue({ success: true, data: [] })
const mockUploadDocuments = vi.fn().mockResolvedValue({ success: true, data: [] })
const mockDeleteDocument = vi.fn().mockResolvedValue({ success: true })
vi.mock('~/composables/useDocuments', () => ({
useDocuments: () => ({
loadDocumentsByComponent: vi.fn().mockResolvedValue({ success: true, data: [] }),
uploadDocuments: vi.fn().mockResolvedValue({ success: true, data: [] }),
deleteDocument: vi.fn().mockResolvedValue({ success: true }),
loadDocumentsByComponent: mockLoadDocumentsByComponent,
uploadDocuments: mockUploadDocuments,
deleteDocument: mockDeleteDocument,
documents: { value: [] },
loading: { value: false },
}),
@@ -552,3 +556,335 @@ describe('submitEdition — no data loss', () => {
expect(formLinks[0].constructeurId).toBe(mockConstructeurSKF.id)
})
})
// ---------------------------------------------------------------------------
// Document operations
// ---------------------------------------------------------------------------
describe('document operations', () => {
it('populates componentDocuments from fetchComponent response', async () => {
const docFixtures = [
{ id: 'doc-1', name: 'photo.jpg', type: 'photo' },
{ id: 'doc-2', name: 'schema.pdf', type: 'schema' },
]
const composable = await createAndHydrate({ documents: docFixtures } as any)
expect(composable.componentDocuments.value).toHaveLength(2)
expect(composable.componentDocuments.value[0].id).toBe('doc-1')
expect(composable.componentDocuments.value[1].id).toBe('doc-2')
})
it('sets componentDocuments to empty array when response has no documents', async () => {
const composable = await createAndHydrate({ documents: undefined } as any)
expect(composable.componentDocuments.value).toEqual([])
})
it('handleFilesAdded calls uploadDocuments with composantId context', async () => {
mockUploadDocuments.mockResolvedValue({ success: true, data: [] })
mockLoadDocumentsByComponent.mockResolvedValue({ success: true, data: [] })
const composable = await createAndHydrate()
const files = [new File(['content'], 'test.pdf', { type: 'application/pdf' })]
await composable.handleFilesAdded(files)
expect(mockUploadDocuments).toHaveBeenCalledTimes(1)
const callArgs = mockUploadDocuments.mock.calls[0]![0]
expect(callArgs.files).toBe(files)
expect(callArgs.context.composantId).toBe(COMPONENT_ID)
})
it('handleFilesAdded does nothing when files array is empty', async () => {
const composable = await createAndHydrate()
await composable.handleFilesAdded([])
expect(mockUploadDocuments).not.toHaveBeenCalled()
})
it('handleFilesAdded refreshes documents after successful upload', async () => {
const refreshedDocs = [{ id: 'doc-new', name: 'uploaded.pdf' }]
mockUploadDocuments.mockResolvedValue({ success: true, data: [] })
mockLoadDocumentsByComponent.mockResolvedValue({ success: true, data: refreshedDocs })
const composable = await createAndHydrate()
const files = [new File(['data'], 'uploaded.pdf')]
await composable.handleFilesAdded(files)
expect(mockLoadDocumentsByComponent).toHaveBeenCalledWith(COMPONENT_ID, { updateStore: false })
expect(composable.componentDocuments.value).toHaveLength(1)
expect(composable.componentDocuments.value[0].id).toBe('doc-new')
})
it('removeDocument calls deleteDocument and removes from local list', async () => {
mockDeleteDocument.mockResolvedValue({ success: true })
const composable = await createAndHydrate({
documents: [
{ id: 'doc-a', name: 'a.pdf' },
{ id: 'doc-b', name: 'b.pdf' },
],
} as any)
expect(composable.componentDocuments.value).toHaveLength(2)
await composable.removeDocument('doc-a')
expect(mockDeleteDocument).toHaveBeenCalledWith('doc-a', { updateStore: false })
expect(composable.componentDocuments.value).toHaveLength(1)
expect(composable.componentDocuments.value[0].id).toBe('doc-b')
})
it('removeDocument does nothing when documentId is falsy', async () => {
const composable = await createAndHydrate()
await composable.removeDocument(null)
await composable.removeDocument(undefined)
expect(mockDeleteDocument).not.toHaveBeenCalled()
})
})
// ---------------------------------------------------------------------------
// Null field handling in PATCH payload
// ---------------------------------------------------------------------------
describe('null field handling in PATCH payload', () => {
it('empty prix string sends null in payload', async () => {
mockUpdateComposant.mockResolvedValue({ success: true, data: { id: COMPONENT_ID } })
const composable = await createAndHydrate()
composable.editionForm.prix = ''
await composable.submitEdition()
const payload = mockUpdateComposant.mock.calls[0]![1]
expect(payload.prix).toBeNull()
})
it('whitespace-only prix string sends null in payload', async () => {
mockUpdateComposant.mockResolvedValue({ success: true, data: { id: COMPONENT_ID } })
const composable = await createAndHydrate()
composable.editionForm.prix = ' '
await composable.submitEdition()
const payload = mockUpdateComposant.mock.calls[0]![1]
expect(payload.prix).toBeNull()
})
it('valid prix string sends stringified number', async () => {
mockUpdateComposant.mockResolvedValue({ success: true, data: { id: COMPONENT_ID } })
const composable = await createAndHydrate()
composable.editionForm.prix = '42.50'
await composable.submitEdition()
const payload = mockUpdateComposant.mock.calls[0]![1]
expect(payload.prix).toBe('42.5')
})
it('empty reference string sends null in payload', async () => {
mockUpdateComposant.mockResolvedValue({ success: true, data: { id: COMPONENT_ID } })
const composable = await createAndHydrate()
composable.editionForm.reference = ''
await composable.submitEdition()
const payload = mockUpdateComposant.mock.calls[0]![1]
expect(payload.reference).toBeNull()
})
it('empty description string sends null in payload', async () => {
mockUpdateComposant.mockResolvedValue({ success: true, data: { id: COMPONENT_ID } })
const composable = await createAndHydrate()
composable.editionForm.description = ''
await composable.submitEdition()
const payload = mockUpdateComposant.mock.calls[0]![1]
expect(payload.description).toBeNull()
})
it('whitespace-only description sends null in payload', async () => {
mockUpdateComposant.mockResolvedValue({ success: true, data: { id: COMPONENT_ID } })
const composable = await createAndHydrate()
composable.editionForm.description = ' '
await composable.submitEdition()
const payload = mockUpdateComposant.mock.calls[0]![1]
expect(payload.description).toBeNull()
})
it('name is trimmed but never null', async () => {
mockUpdateComposant.mockResolvedValue({ success: true, data: { id: COMPONENT_ID } })
const composable = await createAndHydrate()
composable.editionForm.name = ' Moteur '
await composable.submitEdition()
const payload = mockUpdateComposant.mock.calls[0]![1]
expect(payload.name).toBe('Moteur')
})
})
// ---------------------------------------------------------------------------
// Error paths
// ---------------------------------------------------------------------------
describe('error paths', () => {
it('does not save custom fields when updateComposant returns { success: false }', async () => {
mockUpdateComposant.mockResolvedValue({ success: false })
const composable = await createAndHydrate()
composable.editionForm.name = 'Test'
await composable.submitEdition()
expect(mockUpdateComposant).toHaveBeenCalledTimes(1)
expect(mockSaveAll).not.toHaveBeenCalled()
expect(mockPatch).not.toHaveBeenCalled()
expect(mockSyncLinks).not.toHaveBeenCalled()
})
it('does not patch slots when updateComposant returns { success: false }', async () => {
mockUpdateComposant.mockResolvedValue({ success: false })
const composable = await createAndHydrate()
composable.setPieceSlotSelection('ps-001', 'piece-new')
composable.setProductSlotSelection('prs-001', 'prod-new')
await composable.submitEdition()
expect(mockPatch).not.toHaveBeenCalled()
})
it('does not sync constructeur links when updateComposant fails', async () => {
mockUpdateComposant.mockResolvedValue({ success: false })
const composable = await createAndHydrate()
await composable.submitEdition()
expect(mockSyncLinks).not.toHaveBeenCalled()
})
it('shows error toast when saveAllCustomFields returns failed fields', async () => {
mockUpdateComposant.mockResolvedValue({ success: true, data: { id: COMPONENT_ID } })
mockSaveAll.mockResolvedValue(['Tension nominale', 'Indice de protection'])
const composable = await createAndHydrate()
await composable.submitEdition()
expect(mockShowError).toHaveBeenCalledTimes(1)
expect(mockShowError.mock.calls[0]![0]).toContain('Tension nominale')
expect(mockShowError.mock.calls[0]![0]).toContain('Indice de protection')
})
it('still saves slots and syncs links even when custom fields fail', async () => {
mockUpdateComposant.mockResolvedValue({ success: true, data: { id: COMPONENT_ID } })
mockSaveAll.mockResolvedValue(['Tension nominale'])
mockPatch.mockResolvedValue({ success: true, data: {} })
const composable = await createAndHydrate()
composable.setPieceSlotSelection('ps-001', 'piece-after-cf-fail')
await composable.submitEdition()
// Slots still patched despite custom field failure
expect(mockPatch).toHaveBeenCalledWith('/composant-piece-slots/ps-001', { selectedPieceId: 'piece-after-cf-fail' })
// Links still synced
expect(mockSyncLinks).toHaveBeenCalledTimes(1)
// Success toast still shown (alongside the error toast for CF)
expect(mockShowSuccess).toHaveBeenCalledTimes(1)
})
it('shows error toast when submitEdition throws', async () => {
mockUpdateComposant.mockRejectedValue(new Error('Network failure'))
const composable = await createAndHydrate()
await composable.submitEdition()
expect(mockShowError).toHaveBeenCalledTimes(1)
expect(mockShowError.mock.calls[0]![0]).toContain('Network failure')
expect(composable.saving.value).toBe(false)
})
it('resets saving flag even when updateComposant throws', async () => {
mockUpdateComposant.mockRejectedValue(new Error('Server error'))
const composable = await createAndHydrate()
await composable.submitEdition()
expect(composable.saving.value).toBe(false)
})
})
// ---------------------------------------------------------------------------
// Custom field save verification
// ---------------------------------------------------------------------------
describe('custom field save verification', () => {
it('saveAllCustomFields is called after successful updateComposant', async () => {
mockUpdateComposant.mockResolvedValue({ success: true, data: { id: COMPONENT_ID } })
mockSaveAll.mockResolvedValue([])
const composable = await createAndHydrate()
await composable.submitEdition()
expect(mockSaveAll).toHaveBeenCalledTimes(1)
})
it('does not show error toast when saveAll returns empty array (no failures)', async () => {
mockUpdateComposant.mockResolvedValue({ success: true, data: { id: COMPONENT_ID } })
mockSaveAll.mockResolvedValue([])
const composable = await createAndHydrate()
await composable.submitEdition()
// showError should NOT have been called (only showSuccess)
expect(mockShowError).not.toHaveBeenCalled()
expect(mockShowSuccess).toHaveBeenCalledTimes(1)
})
it('shows error with all failed field names joined', async () => {
mockUpdateComposant.mockResolvedValue({ success: true, data: { id: COMPONENT_ID } })
mockSaveAll.mockResolvedValue(['Champ A', 'Champ B', 'Champ C'])
const composable = await createAndHydrate()
await composable.submitEdition()
expect(mockShowError).toHaveBeenCalledTimes(1)
const errorMsg = mockShowError.mock.calls[0]![0] as string
expect(errorMsg).toContain('Champ A')
expect(errorMsg).toContain('Champ B')
expect(errorMsg).toContain('Champ C')
})
it('submitEdition does nothing when component is null', async () => {
const composable = await createAndHydrate()
// Force component to null
composable.component.value = null
await composable.submitEdition()
expect(mockUpdateComposant).not.toHaveBeenCalled()
expect(mockSaveAll).not.toHaveBeenCalled()
})
})