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