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