test(piece-edit,documents) : add productIds sync, error paths, and document CRUD tests
This commit is contained in:
319
frontend/tests/composables/useDocuments.test.ts
Normal file
319
frontend/tests/composables/useDocuments.test.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
import { wrapCollection } from '../fixtures/mockData'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mocks — API layer
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const mockGet = vi.fn()
|
||||
const mockPatch = vi.fn()
|
||||
const mockPostFormData = vi.fn()
|
||||
const mockDel = vi.fn()
|
||||
|
||||
vi.mock('~/composables/useApi', () => ({
|
||||
useApi: () => ({
|
||||
get: mockGet,
|
||||
post: vi.fn(),
|
||||
patch: mockPatch,
|
||||
put: vi.fn(),
|
||||
delete: mockDel,
|
||||
postFormData: mockPostFormData,
|
||||
}),
|
||||
}))
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mocks — Toast
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const mockShowSuccess = vi.fn()
|
||||
const mockShowError = vi.fn()
|
||||
|
||||
vi.mock('~/composables/useToast', () => ({
|
||||
useToast: () => ({
|
||||
showSuccess: mockShowSuccess,
|
||||
showError: mockShowError,
|
||||
showInfo: vi.fn(),
|
||||
showToast: vi.fn(),
|
||||
toasts: { value: [] },
|
||||
clearAll: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Import under test (AFTER all vi.mock calls)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
import { useDocuments } from '~/composables/useDocuments'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test data
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const mockDocument = {
|
||||
id: 'doc-001',
|
||||
name: 'photo.jpg',
|
||||
filename: 'photo.jpg',
|
||||
mimeType: 'image/jpeg',
|
||||
size: 12345,
|
||||
fileUrl: '/files/photo.jpg',
|
||||
downloadUrl: '/files/photo.jpg/download',
|
||||
createdAt: '2025-01-10T08:00:00+00:00',
|
||||
}
|
||||
|
||||
const mockDocument2 = {
|
||||
id: 'doc-002',
|
||||
name: 'schema.pdf',
|
||||
filename: 'schema.pdf',
|
||||
mimeType: 'application/pdf',
|
||||
size: 54321,
|
||||
fileUrl: '/files/schema.pdf',
|
||||
downloadUrl: '/files/schema.pdf/download',
|
||||
createdAt: '2025-01-11T09:00:00+00:00',
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function createMockFile(name: string, type = 'image/jpeg'): File {
|
||||
return new File(['content'], name, { type })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// beforeEach
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// uploadDocuments — FormData is built correctly
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('uploadDocuments', () => {
|
||||
it('builds FormData with file and context fields', async () => {
|
||||
mockPostFormData.mockResolvedValue({ success: true, data: mockDocument })
|
||||
|
||||
const { uploadDocuments } = useDocuments()
|
||||
const file = createMockFile('photo.jpg')
|
||||
|
||||
await uploadDocuments({
|
||||
files: [file],
|
||||
context: { pieceId: 'piece-001', composantId: 'comp-001' },
|
||||
})
|
||||
|
||||
expect(mockPostFormData).toHaveBeenCalledTimes(1)
|
||||
const [endpoint, formData] = mockPostFormData.mock.calls[0]!
|
||||
expect(endpoint).toBe('/documents')
|
||||
|
||||
expect(formData).toBeInstanceOf(FormData)
|
||||
expect(formData.get('file')).toBe(file)
|
||||
expect(formData.get('name')).toBe('photo.jpg')
|
||||
expect(formData.get('pieceId')).toBe('piece-001')
|
||||
expect(formData.get('composantId')).toBe('comp-001')
|
||||
})
|
||||
|
||||
it('uploads multiple files separately', async () => {
|
||||
mockPostFormData
|
||||
.mockResolvedValueOnce({ success: true, data: mockDocument })
|
||||
.mockResolvedValueOnce({ success: true, data: mockDocument2 })
|
||||
|
||||
const { uploadDocuments } = useDocuments()
|
||||
const file1 = createMockFile('photo.jpg')
|
||||
const file2 = createMockFile('schema.pdf', 'application/pdf')
|
||||
|
||||
const result = await uploadDocuments({
|
||||
files: [file1, file2],
|
||||
context: { machineId: 'machine-001' },
|
||||
})
|
||||
|
||||
expect(mockPostFormData).toHaveBeenCalledTimes(2)
|
||||
|
||||
// First call
|
||||
const [, formData1] = mockPostFormData.mock.calls[0]!
|
||||
expect(formData1.get('name')).toBe('photo.jpg')
|
||||
expect(formData1.get('machineId')).toBe('machine-001')
|
||||
|
||||
// Second call
|
||||
const [, formData2] = mockPostFormData.mock.calls[1]!
|
||||
expect(formData2.get('name')).toBe('schema.pdf')
|
||||
expect(formData2.get('machineId')).toBe('machine-001')
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(Array.isArray(result.data) ? result.data : []).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('appends type to FormData when provided in context', async () => {
|
||||
mockPostFormData.mockResolvedValue({ success: true, data: mockDocument })
|
||||
|
||||
const { uploadDocuments } = useDocuments()
|
||||
const file = createMockFile('facture.pdf', 'application/pdf')
|
||||
|
||||
await uploadDocuments({
|
||||
files: [file],
|
||||
context: { siteId: 'site-001', type: 'facture' },
|
||||
})
|
||||
|
||||
const [, formData] = mockPostFormData.mock.calls[0]!
|
||||
expect(formData.get('type')).toBe('facture')
|
||||
expect(formData.get('siteId')).toBe('site-001')
|
||||
})
|
||||
|
||||
it('returns error when no files provided', async () => {
|
||||
const { uploadDocuments } = useDocuments()
|
||||
|
||||
const result = await uploadDocuments({ files: [], context: {} })
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
expect(mockPostFormData).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// loadDocumentsByComponent
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('loadDocumentsByComponent', () => {
|
||||
it('calls correct endpoint /documents/composant/{id}', async () => {
|
||||
mockGet.mockResolvedValue({ success: true, data: wrapCollection([mockDocument]) })
|
||||
|
||||
const { loadDocumentsByComponent } = useDocuments()
|
||||
const result = await loadDocumentsByComponent('comp-001')
|
||||
|
||||
expect(mockGet).toHaveBeenCalledTimes(1)
|
||||
expect(mockGet).toHaveBeenCalledWith('/documents/composant/comp-001')
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('returns error for empty componentId', async () => {
|
||||
const { loadDocumentsByComponent } = useDocuments()
|
||||
const result = await loadDocumentsByComponent('')
|
||||
|
||||
expect(mockGet).not.toHaveBeenCalled()
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// loadDocumentsByPiece
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('loadDocumentsByPiece', () => {
|
||||
it('calls correct endpoint /documents/piece/{id}', async () => {
|
||||
mockGet.mockResolvedValue({ success: true, data: wrapCollection([mockDocument]) })
|
||||
|
||||
const { loadDocumentsByPiece } = useDocuments()
|
||||
const result = await loadDocumentsByPiece('piece-001')
|
||||
|
||||
expect(mockGet).toHaveBeenCalledTimes(1)
|
||||
expect(mockGet).toHaveBeenCalledWith('/documents/piece/piece-001')
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('returns error for empty pieceId', async () => {
|
||||
const { loadDocumentsByPiece } = useDocuments()
|
||||
const result = await loadDocumentsByPiece('')
|
||||
|
||||
expect(mockGet).not.toHaveBeenCalled()
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// loadDocumentsByMachine
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('loadDocumentsByMachine', () => {
|
||||
it('calls correct endpoint /documents/machine/{id}', async () => {
|
||||
mockGet.mockResolvedValue({ success: true, data: wrapCollection([mockDocument]) })
|
||||
|
||||
const { loadDocumentsByMachine } = useDocuments()
|
||||
const result = await loadDocumentsByMachine('machine-001')
|
||||
|
||||
expect(mockGet).toHaveBeenCalledTimes(1)
|
||||
expect(mockGet).toHaveBeenCalledWith('/documents/machine/machine-001')
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('returns error for empty machineId', async () => {
|
||||
const { loadDocumentsByMachine } = useDocuments()
|
||||
const result = await loadDocumentsByMachine('')
|
||||
|
||||
expect(mockGet).not.toHaveBeenCalled()
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// loadDocumentsByProduct
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('loadDocumentsByProduct', () => {
|
||||
it('calls correct endpoint /documents/product/{id}', async () => {
|
||||
mockGet.mockResolvedValue({ success: true, data: wrapCollection([mockDocument]) })
|
||||
|
||||
const { loadDocumentsByProduct } = useDocuments()
|
||||
const result = await loadDocumentsByProduct('prod-001')
|
||||
|
||||
expect(mockGet).toHaveBeenCalledTimes(1)
|
||||
expect(mockGet).toHaveBeenCalledWith('/documents/product/prod-001')
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('returns error for empty productId', async () => {
|
||||
const { loadDocumentsByProduct } = useDocuments()
|
||||
const result = await loadDocumentsByProduct('')
|
||||
|
||||
expect(mockGet).not.toHaveBeenCalled()
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// deleteDocument
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('deleteDocument', () => {
|
||||
it('calls DELETE on correct endpoint', async () => {
|
||||
mockDel.mockResolvedValue({ success: true })
|
||||
|
||||
const { deleteDocument } = useDocuments()
|
||||
const result = await deleteDocument('doc-001')
|
||||
|
||||
expect(mockDel).toHaveBeenCalledTimes(1)
|
||||
expect(mockDel).toHaveBeenCalledWith('/documents/doc-001')
|
||||
expect(result.success).toBe(true)
|
||||
})
|
||||
|
||||
it('removes from store when updateStore is true', async () => {
|
||||
mockGet.mockResolvedValue({
|
||||
success: true,
|
||||
data: wrapCollection([mockDocument, mockDocument2]),
|
||||
})
|
||||
mockDel.mockResolvedValue({ success: true })
|
||||
|
||||
const { loadDocuments, deleteDocument, documents } = useDocuments()
|
||||
|
||||
// Load documents into store first
|
||||
await loadDocuments({ force: true })
|
||||
|
||||
expect(documents.value).toHaveLength(2)
|
||||
|
||||
// Delete with updateStore: true
|
||||
await deleteDocument('doc-001', { updateStore: true })
|
||||
|
||||
expect(documents.value).toHaveLength(1)
|
||||
expect(documents.value[0]!.id).toBe('doc-002')
|
||||
})
|
||||
|
||||
it('shows success toast on successful delete', async () => {
|
||||
mockDel.mockResolvedValue({ success: true })
|
||||
|
||||
const { deleteDocument } = useDocuments()
|
||||
await deleteDocument('doc-001')
|
||||
|
||||
expect(mockShowSuccess).toHaveBeenCalledWith('Document supprimé')
|
||||
})
|
||||
})
|
||||
@@ -466,4 +466,179 @@ describe('submitEdition — no data loss', () => {
|
||||
expect(formLinks[1].constructeurId).toBe(mockConstructeurFAG.id)
|
||||
expect(formLinks[2].constructeurId).toBe('cstr-new-003')
|
||||
})
|
||||
|
||||
it('sends both productId and productIds in payload', async () => {
|
||||
mockUpdatePiece.mockResolvedValue({ success: true, data: { id: PIECE_ID } })
|
||||
|
||||
const composable = await createAndHydrate()
|
||||
|
||||
composable.setProductSelection(0, 'prod-001')
|
||||
await tick()
|
||||
|
||||
await composable.submitEdition()
|
||||
|
||||
expect(mockUpdatePiece).toHaveBeenCalledTimes(1)
|
||||
const payload = mockUpdatePiece.mock.calls[0]![1]
|
||||
expect(payload.productId).toBe('prod-001')
|
||||
expect(payload.productIds).toEqual(['prod-001'])
|
||||
})
|
||||
|
||||
it('productId is the first product selection when multiple exist', async () => {
|
||||
// Override the piece type to have 2 product requirements
|
||||
const multiProductType = {
|
||||
...mockPieceType,
|
||||
structure: {
|
||||
...mockPieceType.structure,
|
||||
products: [
|
||||
{
|
||||
typeProductId: 'tprod-grease-001',
|
||||
typeProductLabel: 'Graisse SKF',
|
||||
familyCode: 'LUB',
|
||||
role: 'lubrification',
|
||||
},
|
||||
{
|
||||
typeProductId: 'tprod-oil-002',
|
||||
typeProductLabel: 'Huile',
|
||||
familyCode: 'LUB',
|
||||
role: 'lubrification secondaire',
|
||||
},
|
||||
],
|
||||
customFields: [],
|
||||
},
|
||||
}
|
||||
mockPieceTypes.value = [multiProductType]
|
||||
|
||||
mockUpdatePiece.mockResolvedValue({ success: true, data: { id: PIECE_ID } })
|
||||
|
||||
const composable = await createAndHydrate({
|
||||
productIds: ['prod-001', 'prod-002'],
|
||||
})
|
||||
|
||||
composable.setProductSelection(0, 'prod-001')
|
||||
composable.setProductSelection(1, 'prod-002')
|
||||
await tick()
|
||||
|
||||
await composable.submitEdition()
|
||||
|
||||
expect(mockUpdatePiece).toHaveBeenCalledTimes(1)
|
||||
const payload = mockUpdatePiece.mock.calls[0]![1]
|
||||
expect(payload.productId).toBe('prod-001')
|
||||
expect(payload.productIds).toEqual(['prod-001', 'prod-002'])
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// submitEdition — null field handling
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('submitEdition — null field handling', () => {
|
||||
it('empty prix sends null', async () => {
|
||||
mockUpdatePiece.mockResolvedValue({ success: true, data: { id: PIECE_ID } })
|
||||
|
||||
const composable = await createAndHydrate()
|
||||
|
||||
composable.editionForm.prix = ''
|
||||
composable.setProductSelection(0, 'prod-001')
|
||||
await tick()
|
||||
|
||||
await composable.submitEdition()
|
||||
|
||||
const payload = mockUpdatePiece.mock.calls[0]![1]
|
||||
expect(payload.prix).toBeNull()
|
||||
})
|
||||
|
||||
it('whitespace-only prix sends null', async () => {
|
||||
mockUpdatePiece.mockResolvedValue({ success: true, data: { id: PIECE_ID } })
|
||||
|
||||
const composable = await createAndHydrate()
|
||||
|
||||
composable.editionForm.prix = ' '
|
||||
composable.setProductSelection(0, 'prod-001')
|
||||
await tick()
|
||||
|
||||
await composable.submitEdition()
|
||||
|
||||
const payload = mockUpdatePiece.mock.calls[0]![1]
|
||||
expect(payload.prix).toBeNull()
|
||||
})
|
||||
|
||||
it('empty reference sends null', async () => {
|
||||
mockUpdatePiece.mockResolvedValue({ success: true, data: { id: PIECE_ID } })
|
||||
|
||||
const composable = await createAndHydrate()
|
||||
|
||||
composable.editionForm.reference = ''
|
||||
composable.setProductSelection(0, 'prod-001')
|
||||
await tick()
|
||||
|
||||
await composable.submitEdition()
|
||||
|
||||
const payload = mockUpdatePiece.mock.calls[0]![1]
|
||||
expect(payload.reference).toBeNull()
|
||||
})
|
||||
|
||||
it('valid prix is sent as string number', async () => {
|
||||
mockUpdatePiece.mockResolvedValue({ success: true, data: { id: PIECE_ID } })
|
||||
|
||||
const composable = await createAndHydrate()
|
||||
|
||||
composable.editionForm.prix = '99.50'
|
||||
composable.setProductSelection(0, 'prod-001')
|
||||
await tick()
|
||||
|
||||
await composable.submitEdition()
|
||||
|
||||
const payload = mockUpdatePiece.mock.calls[0]![1]
|
||||
expect(payload.prix).toBe('99.5')
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// submitEdition — error paths
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('submitEdition — error paths', () => {
|
||||
it('does not save custom fields when updatePiece fails', async () => {
|
||||
mockUpdatePiece.mockResolvedValue({ success: false, error: 'Server error' })
|
||||
|
||||
const composable = await createAndHydrate()
|
||||
|
||||
composable.setProductSelection(0, 'prod-001')
|
||||
await tick()
|
||||
|
||||
await composable.submitEdition()
|
||||
|
||||
expect(mockUpdatePiece).toHaveBeenCalledTimes(1)
|
||||
expect(mockSaveAll).not.toHaveBeenCalled()
|
||||
expect(mockSyncLinks).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not save custom fields when updatePiece throws', async () => {
|
||||
mockUpdatePiece.mockRejectedValue(new Error('Network failure'))
|
||||
|
||||
const composable = await createAndHydrate()
|
||||
|
||||
composable.setProductSelection(0, 'prod-001')
|
||||
await tick()
|
||||
|
||||
await composable.submitEdition()
|
||||
|
||||
expect(mockUpdatePiece).toHaveBeenCalledTimes(1)
|
||||
expect(mockSaveAll).not.toHaveBeenCalled()
|
||||
expect(mockSyncLinks).not.toHaveBeenCalled()
|
||||
expect(mockShowError).toHaveBeenCalledWith('Network failure')
|
||||
})
|
||||
|
||||
it('shows error toast when product selection is not filled', async () => {
|
||||
const composable = await createAndHydrate()
|
||||
|
||||
// Clear product selection
|
||||
composable.setProductSelection(0, null)
|
||||
await tick()
|
||||
|
||||
await composable.submitEdition()
|
||||
|
||||
expect(mockUpdatePiece).not.toHaveBeenCalled()
|
||||
expect(mockShowError).toHaveBeenCalledWith('Sélectionnez un produit conforme au squelette.')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user