test(piece-edit,documents) : add productIds sync, error paths, and document CRUD tests

This commit is contained in:
2026-04-06 15:56:07 +02:00
parent 972f30e772
commit 150aceac24
2 changed files with 494 additions and 0 deletions

View 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é')
})
})

View File

@@ -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.')
})
})