test(component-create) : add creation flow data integrity tests
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
348
frontend/tests/composables/useComponentCreate.test.ts
Normal file
348
frontend/tests/composables/useComponentCreate.test.ts
Normal file
@@ -0,0 +1,348 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
import { mockLinkSKF, mockLinkFAG } from '../fixtures/mockData'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mocks — API layer
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const mockGet = vi.fn()
|
||||
const mockPost = vi.fn()
|
||||
const mockPatch = vi.fn()
|
||||
const mockDel = vi.fn()
|
||||
const mockPostFormData = vi.fn()
|
||||
|
||||
vi.mock('~/composables/useApi', () => ({
|
||||
useApi: () => ({
|
||||
get: mockGet,
|
||||
post: mockPost,
|
||||
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(),
|
||||
}),
|
||||
}))
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mocks — useComposants (createComposant)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const mockCreateComposant = vi.fn()
|
||||
|
||||
vi.mock('~/composables/useComposants', () => ({
|
||||
useComposants: () => ({
|
||||
createComposant: mockCreateComposant,
|
||||
composants: { value: [] },
|
||||
loading: { value: false },
|
||||
}),
|
||||
}))
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mocks — usePieces, useProducts
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
vi.mock('~/composables/usePieces', () => ({
|
||||
usePieces: () => ({
|
||||
pieces: { value: [] },
|
||||
loading: { value: false },
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('~/composables/useProducts', () => ({
|
||||
useProducts: () => ({
|
||||
products: { value: [] },
|
||||
loading: { value: false },
|
||||
}),
|
||||
}))
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mocks — useComponentTypes, usePieceTypes, useProductTypes
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const mockLoadComponentTypes = vi.fn().mockResolvedValue(undefined)
|
||||
const mockComponentTypes = { value: [] as any[] }
|
||||
|
||||
vi.mock('~/composables/useComponentTypes', () => ({
|
||||
useComponentTypes: () => ({
|
||||
componentTypes: mockComponentTypes,
|
||||
loadComponentTypes: mockLoadComponentTypes,
|
||||
loadingComponentTypes: { value: false },
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('~/composables/usePieceTypes', () => ({
|
||||
usePieceTypes: () => ({
|
||||
pieceTypes: { value: [] },
|
||||
loadPieceTypes: vi.fn().mockResolvedValue(undefined),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('~/composables/useProductTypes', () => ({
|
||||
useProductTypes: () => ({
|
||||
productTypes: { value: [] },
|
||||
loadProductTypes: vi.fn().mockResolvedValue(undefined),
|
||||
}),
|
||||
}))
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mocks — useDocuments (uploadDocuments)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const mockUploadDocuments = vi.fn()
|
||||
|
||||
vi.mock('~/composables/useDocuments', () => ({
|
||||
useDocuments: () => ({
|
||||
uploadDocuments: mockUploadDocuments,
|
||||
documents: { value: [] },
|
||||
loading: { value: false },
|
||||
}),
|
||||
}))
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mocks — useConstructeurLinks (syncLinks)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const mockSyncLinks = vi.fn().mockResolvedValue(undefined)
|
||||
|
||||
vi.mock('~/composables/useConstructeurLinks', () => ({
|
||||
useConstructeurLinks: () => ({
|
||||
fetchLinks: vi.fn().mockResolvedValue([]),
|
||||
syncLinks: mockSyncLinks,
|
||||
}),
|
||||
}))
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mocks — useCustomFieldInputs (saveAll)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const mockSaveAll = vi.fn().mockResolvedValue([])
|
||||
const mockRefreshCF = vi.fn()
|
||||
|
||||
vi.mock('~/composables/useCustomFieldInputs', () => ({
|
||||
useCustomFieldInputs: () => ({
|
||||
fields: { value: [] },
|
||||
requiredFilled: { value: true },
|
||||
saveAll: mockSaveAll,
|
||||
refresh: mockRefreshCF,
|
||||
}),
|
||||
}))
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mocks — usePermissions (auto-imported in Nuxt)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// usePermissions is Nuxt auto-imported (no explicit import in source),
|
||||
// so we stub it as a global function.
|
||||
vi.stubGlobal('usePermissions', () => ({
|
||||
canEdit: { value: true },
|
||||
canManage: { value: true },
|
||||
isAdmin: { value: false },
|
||||
isGranted: () => true,
|
||||
}))
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mocks — useConstructeurs (used by useComposants internally)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
vi.mock('~/composables/useConstructeurs', () => ({
|
||||
useConstructeurs: () => ({
|
||||
ensureConstructeurs: vi.fn().mockResolvedValue([]),
|
||||
}),
|
||||
}))
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mocks — shared utils that touch structure
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
vi.mock('~/shared/utils/structureAssignmentHelpers', () => ({
|
||||
hasAssignments: () => false,
|
||||
initializeStructureAssignments: () => null,
|
||||
isAssignmentNodeComplete: () => true,
|
||||
serializeStructureAssignments: () => null,
|
||||
}))
|
||||
|
||||
vi.mock('~/shared/utils/structureDisplayUtils', () => ({
|
||||
getStructurePieces: () => [],
|
||||
resolvePieceLabel: (p: any) => p?.name ?? '',
|
||||
resolveProductLabel: (p: any) => p?.name ?? '',
|
||||
resolveSubcomponentLabel: (p: any) => p?.name ?? '',
|
||||
fetchModelTypeNames: vi.fn().mockResolvedValue({}),
|
||||
buildTypeLabelMap: () => ({}),
|
||||
}))
|
||||
|
||||
vi.mock('~/shared/modelUtils', () => ({
|
||||
formatStructurePreview: () => '',
|
||||
normalizeStructureForEditor: (s: any) => s,
|
||||
}))
|
||||
|
||||
vi.mock('~/shared/utils/errorMessages', () => ({
|
||||
humanizeError: (msg: string) => msg,
|
||||
}))
|
||||
|
||||
vi.mock('~/shared/constructeurUtils', () => ({
|
||||
uniqueConstructeurIds: (ids: string[]) => [...new Set(ids)],
|
||||
constructeurIdsFromLinks: (links: any[]) => links.map((l: any) => l.constructeurId),
|
||||
}))
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Import under test (AFTER all vi.mock calls)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
import { useComponentCreate } from '~/composables/useComponentCreate'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** A minimal ModelType matching the `COMPONENT` category filter. */
|
||||
const mockModelType = {
|
||||
id: 'tc-moteur',
|
||||
name: 'Moteur électrique',
|
||||
category: 'COMPONENT',
|
||||
structure: null,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
// Provide at least one COMPONENT type so selectedType resolves
|
||||
mockComponentTypes.value = [mockModelType]
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// submitCreation — payload completeness
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('submitCreation — payload completeness', () => {
|
||||
it('includes all form fields in createComposant payload', async () => {
|
||||
const createdComp = { id: 'comp-new-001', name: 'Moteur principal' }
|
||||
mockCreateComposant.mockResolvedValue({ success: true, data: createdComp })
|
||||
|
||||
const composable = useComponentCreate()
|
||||
|
||||
// Select a type
|
||||
composable.selectedTypeId.value = 'tc-moteur'
|
||||
// Wait a tick so watchers fire
|
||||
await new Promise(r => setTimeout(r, 0))
|
||||
|
||||
// Fill form fields
|
||||
composable.creationForm.name = 'Moteur principal'
|
||||
composable.creationForm.description = 'Un moteur triphasé'
|
||||
composable.creationForm.reference = 'MOT-001'
|
||||
composable.creationForm.prix = '1500'
|
||||
|
||||
await composable.submitCreation()
|
||||
|
||||
expect(mockCreateComposant).toHaveBeenCalledTimes(1)
|
||||
const payload = mockCreateComposant.mock.calls[0]![0]
|
||||
expect(payload).toMatchObject({
|
||||
name: 'Moteur principal',
|
||||
description: 'Un moteur triphasé',
|
||||
reference: 'MOT-001',
|
||||
prix: '1500',
|
||||
typeComposantId: 'tc-moteur',
|
||||
})
|
||||
})
|
||||
|
||||
it('saves custom fields after component creation (saveAll is called)', async () => {
|
||||
const createdComp = { id: 'comp-cf-001', name: 'Composant CF' }
|
||||
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 CF'
|
||||
|
||||
await composable.submitCreation()
|
||||
|
||||
expect(mockCreateComposant).toHaveBeenCalledTimes(1)
|
||||
expect(mockSaveAll).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('syncs constructeur links after creation with correct entity type and ID', async () => {
|
||||
const createdComp = { id: 'comp-link-001', name: 'Composant Links' }
|
||||
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 Links'
|
||||
// Add constructeur links
|
||||
composable.constructeurLinks.value = [mockLinkSKF, mockLinkFAG]
|
||||
|
||||
await composable.submitCreation()
|
||||
|
||||
expect(mockSyncLinks).toHaveBeenCalledTimes(1)
|
||||
expect(mockSyncLinks).toHaveBeenCalledWith(
|
||||
'composant',
|
||||
'comp-link-001',
|
||||
[],
|
||||
[mockLinkSKF, mockLinkFAG],
|
||||
)
|
||||
})
|
||||
|
||||
it('uploads documents with correct composantId context', async () => {
|
||||
const createdComp = { id: 'comp-doc-001', name: 'Composant Docs' }
|
||||
mockCreateComposant.mockResolvedValue({ success: true, data: createdComp })
|
||||
mockUploadDocuments.mockResolvedValue({ success: true, data: [] })
|
||||
|
||||
const composable = useComponentCreate()
|
||||
composable.selectedTypeId.value = 'tc-moteur'
|
||||
await new Promise(r => setTimeout(r, 0))
|
||||
|
||||
composable.creationForm.name = 'Composant Docs'
|
||||
|
||||
// Simulate selected documents
|
||||
const fakeFile = new File(['content'], 'schema.pdf', { type: 'application/pdf' })
|
||||
composable.selectedDocuments.value = [fakeFile]
|
||||
|
||||
await composable.submitCreation()
|
||||
|
||||
expect(mockUploadDocuments).toHaveBeenCalledTimes(1)
|
||||
expect(mockUploadDocuments).toHaveBeenCalledWith(
|
||||
{
|
||||
files: [fakeFile],
|
||||
context: { composantId: 'comp-doc-001' },
|
||||
},
|
||||
{ updateStore: false },
|
||||
)
|
||||
})
|
||||
|
||||
it('does not crash with zero constructeurs', async () => {
|
||||
const createdComp = { id: 'comp-no-cstr', name: 'Composant Simple' }
|
||||
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 Simple'
|
||||
// Ensure no constructeur links
|
||||
composable.constructeurLinks.value = []
|
||||
|
||||
await composable.submitCreation()
|
||||
|
||||
expect(mockCreateComposant).toHaveBeenCalledTimes(1)
|
||||
expect(mockSyncLinks).not.toHaveBeenCalled()
|
||||
expect(mockShowSuccess).toHaveBeenCalledWith('Composant créé avec succès')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user