All checks were successful
Auto Tag Develop / tag (push) Successful in 9s
- Nouvelles entites ConstructeurCategorie (referentiel M2M) et ConstructeurTelephone (1-N) - Constructeur : retrait colonne phone, ajout collections telephones/categories, groupes de serialisation constructeur:read/write - Migration : cree les 3 tables, migre la colonne phone existante vers constructeur_telephone, drop phone - Commande app:import-fournisseurs (dry-run par defaut, --force) : non destructive, find-or-create par nom, ne touche jamais un ID existant, ajout-seulement pour telephones/categories - MAJ MCP tools / MachineStructureController / audit subscriber / tests - Frontend : page constructeurs avec telephones multiples + categories (tableau, filtre, formulaire), composable useConstructeurCategories, composant ConstructeurCategorieSelect Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
738 lines
25 KiB
TypeScript
738 lines
25 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
|
|
import { mockLinkSKF, mockLinkFAG } from '../fixtures/mockData'
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Import under test (AFTER all vi.mock calls)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
import { useComponentCreate } from '~/composables/useComponentCreate'
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const mockHasAssignments = vi.fn().mockReturnValue(false)
|
|
const mockSerializeStructureAssignments = vi.fn().mockReturnValue(null)
|
|
const mockIsAssignmentNodeComplete = vi.fn().mockReturnValue(true)
|
|
|
|
vi.mock('~/shared/utils/structureAssignmentHelpers', () => ({
|
|
hasAssignments: (...args: any[]) => mockHasAssignments(...args),
|
|
initializeStructureAssignments: () => null,
|
|
isAssignmentNodeComplete: (...args: any[]) => mockIsAssignmentNodeComplete(...args),
|
|
serializeStructureAssignments: (...args: any[]) => mockSerializeStructureAssignments(...args),
|
|
}))
|
|
|
|
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),
|
|
}))
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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')
|
|
})
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Structure serialization in payload
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('submitCreation — structure serialization in payload', () => {
|
|
it('includes structure key with serialized data when assignments exist', async () => {
|
|
const createdComp = { id: 'comp-struct-001', name: 'Composant Structure' }
|
|
mockCreateComposant.mockResolvedValue({ success: true, data: createdComp })
|
|
|
|
const fakeSerializedStructure = {
|
|
path: 'root',
|
|
definition: { typeComposantId: 'tc-moteur' },
|
|
pieces: [{ path: 'root:piece-0', definition: { typePieceId: 'tp-001' }, selectedPieceId: 'piece-abc' }],
|
|
}
|
|
|
|
mockHasAssignments.mockReturnValue(true)
|
|
mockSerializeStructureAssignments.mockReturnValue(fakeSerializedStructure)
|
|
|
|
const composable = useComponentCreate()
|
|
composable.selectedTypeId.value = 'tc-moteur'
|
|
await new Promise(r => setTimeout(r, 0))
|
|
|
|
composable.creationForm.name = 'Composant Structure'
|
|
// Set a non-null structureAssignments so the composable considers it present
|
|
composable.structureAssignments.value = {
|
|
path: 'root',
|
|
definition: {} as any,
|
|
selectedComponentId: '',
|
|
pieces: [{ path: 'root:piece-0', definition: {} as any, selectedPieceId: 'piece-abc' }],
|
|
products: [],
|
|
subcomponents: [],
|
|
}
|
|
|
|
await composable.submitCreation()
|
|
|
|
expect(mockCreateComposant).toHaveBeenCalledTimes(1)
|
|
const payload = mockCreateComposant.mock.calls[0]![0]
|
|
expect(payload.structure).toEqual(fakeSerializedStructure)
|
|
})
|
|
|
|
it('does not include structure key when no assignments exist', async () => {
|
|
const createdComp = { id: 'comp-nostruct-001', name: 'Composant No Structure' }
|
|
mockCreateComposant.mockResolvedValue({ success: true, data: createdComp })
|
|
|
|
// Reset to default: no assignments
|
|
mockHasAssignments.mockReturnValue(false)
|
|
mockSerializeStructureAssignments.mockReturnValue(null)
|
|
|
|
const composable = useComponentCreate()
|
|
composable.selectedTypeId.value = 'tc-moteur'
|
|
await new Promise(r => setTimeout(r, 0))
|
|
|
|
composable.creationForm.name = 'Composant No Structure'
|
|
|
|
await composable.submitCreation()
|
|
|
|
expect(mockCreateComposant).toHaveBeenCalledTimes(1)
|
|
const payload = mockCreateComposant.mock.calls[0]![0]
|
|
expect(payload.structure).toBeUndefined()
|
|
})
|
|
|
|
it('does not include structure key when serializeStructureAssignments returns null', async () => {
|
|
const createdComp = { id: 'comp-sernull-001', name: 'Composant Serialize Null' }
|
|
mockCreateComposant.mockResolvedValue({ success: true, data: createdComp })
|
|
|
|
mockHasAssignments.mockReturnValue(true)
|
|
mockSerializeStructureAssignments.mockReturnValue(null)
|
|
|
|
const composable = useComponentCreate()
|
|
composable.selectedTypeId.value = 'tc-moteur'
|
|
await new Promise(r => setTimeout(r, 0))
|
|
|
|
composable.creationForm.name = 'Composant Serialize Null'
|
|
composable.structureAssignments.value = {
|
|
path: 'root',
|
|
definition: {} as any,
|
|
selectedComponentId: '',
|
|
pieces: [],
|
|
products: [],
|
|
subcomponents: [],
|
|
}
|
|
|
|
await composable.submitCreation()
|
|
|
|
expect(mockCreateComposant).toHaveBeenCalledTimes(1)
|
|
const payload = mockCreateComposant.mock.calls[0]![0]
|
|
expect(payload.structure).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Prix / reference null handling
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('submitCreation — prix and reference null handling', () => {
|
|
it('does not send prix when prix is an empty string', async () => {
|
|
const createdComp = { id: 'comp-noprix-001', name: 'Composant No Prix' }
|
|
mockCreateComposant.mockResolvedValue({ success: true, data: createdComp })
|
|
|
|
// Reset structure mocks to default
|
|
mockHasAssignments.mockReturnValue(false)
|
|
mockSerializeStructureAssignments.mockReturnValue(null)
|
|
|
|
const composable = useComponentCreate()
|
|
composable.selectedTypeId.value = 'tc-moteur'
|
|
await new Promise(r => setTimeout(r, 0))
|
|
|
|
composable.creationForm.name = 'Composant No Prix'
|
|
composable.creationForm.prix = ''
|
|
|
|
await composable.submitCreation()
|
|
|
|
const payload = mockCreateComposant.mock.calls[0]![0]
|
|
expect(payload).not.toHaveProperty('prix')
|
|
})
|
|
|
|
it('does not send prix when prix is non-numeric (avoids NaN)', async () => {
|
|
const createdComp = { id: 'comp-nanprix-001', name: 'Composant NaN Prix' }
|
|
mockCreateComposant.mockResolvedValue({ success: true, data: createdComp })
|
|
|
|
mockHasAssignments.mockReturnValue(false)
|
|
mockSerializeStructureAssignments.mockReturnValue(null)
|
|
|
|
const composable = useComponentCreate()
|
|
composable.selectedTypeId.value = 'tc-moteur'
|
|
await new Promise(r => setTimeout(r, 0))
|
|
|
|
composable.creationForm.name = 'Composant NaN Prix'
|
|
composable.creationForm.prix = 'not-a-number'
|
|
|
|
await composable.submitCreation()
|
|
|
|
const payload = mockCreateComposant.mock.calls[0]![0]
|
|
expect(payload).not.toHaveProperty('prix')
|
|
})
|
|
|
|
it('sends prix as stringified number when valid numeric string', async () => {
|
|
const createdComp = { id: 'comp-validprix-001', name: 'Composant Valid Prix' }
|
|
mockCreateComposant.mockResolvedValue({ success: true, data: createdComp })
|
|
|
|
mockHasAssignments.mockReturnValue(false)
|
|
mockSerializeStructureAssignments.mockReturnValue(null)
|
|
|
|
const composable = useComponentCreate()
|
|
composable.selectedTypeId.value = 'tc-moteur'
|
|
await new Promise(r => setTimeout(r, 0))
|
|
|
|
composable.creationForm.name = 'Composant Valid Prix'
|
|
composable.creationForm.prix = ' 42.5 '
|
|
|
|
await composable.submitCreation()
|
|
|
|
const payload = mockCreateComposant.mock.calls[0]![0]
|
|
expect(payload.prix).toBe('42.5')
|
|
})
|
|
|
|
it('does not send reference when reference is an empty string', async () => {
|
|
const createdComp = { id: 'comp-noref-001', name: 'Composant No Ref' }
|
|
mockCreateComposant.mockResolvedValue({ success: true, data: createdComp })
|
|
|
|
mockHasAssignments.mockReturnValue(false)
|
|
mockSerializeStructureAssignments.mockReturnValue(null)
|
|
|
|
const composable = useComponentCreate()
|
|
composable.selectedTypeId.value = 'tc-moteur'
|
|
await new Promise(r => setTimeout(r, 0))
|
|
|
|
composable.creationForm.name = 'Composant No Ref'
|
|
composable.creationForm.reference = ''
|
|
|
|
await composable.submitCreation()
|
|
|
|
const payload = mockCreateComposant.mock.calls[0]![0]
|
|
expect(payload).not.toHaveProperty('reference')
|
|
})
|
|
|
|
it('does not send reference when reference is whitespace only', async () => {
|
|
const createdComp = { id: 'comp-wsref-001', name: 'Composant WS Ref' }
|
|
mockCreateComposant.mockResolvedValue({ success: true, data: createdComp })
|
|
|
|
mockHasAssignments.mockReturnValue(false)
|
|
mockSerializeStructureAssignments.mockReturnValue(null)
|
|
|
|
const composable = useComponentCreate()
|
|
composable.selectedTypeId.value = 'tc-moteur'
|
|
await new Promise(r => setTimeout(r, 0))
|
|
|
|
composable.creationForm.name = 'Composant WS Ref'
|
|
composable.creationForm.reference = ' '
|
|
|
|
await composable.submitCreation()
|
|
|
|
const payload = mockCreateComposant.mock.calls[0]![0]
|
|
expect(payload).not.toHaveProperty('reference')
|
|
})
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Error paths
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('submitCreation — error paths', () => {
|
|
beforeEach(() => {
|
|
mockHasAssignments.mockReturnValue(false)
|
|
mockSerializeStructureAssignments.mockReturnValue(null)
|
|
})
|
|
|
|
it('does not save custom fields when createComposant returns success: false', async () => {
|
|
mockCreateComposant.mockResolvedValue({ success: false, error: 'Duplicate name' })
|
|
|
|
const composable = useComponentCreate()
|
|
composable.selectedTypeId.value = 'tc-moteur'
|
|
await new Promise(r => setTimeout(r, 0))
|
|
|
|
composable.creationForm.name = 'Composant Fail'
|
|
|
|
await composable.submitCreation()
|
|
|
|
expect(mockCreateComposant).toHaveBeenCalledTimes(1)
|
|
expect(mockSaveAll).not.toHaveBeenCalled()
|
|
expect(mockShowError).toHaveBeenCalledWith('Duplicate name')
|
|
})
|
|
|
|
it('shows toast error when createComposant returns success: false with error message', async () => {
|
|
mockCreateComposant.mockResolvedValue({ success: false, error: 'Server validation failed' })
|
|
|
|
const composable = useComponentCreate()
|
|
composable.selectedTypeId.value = 'tc-moteur'
|
|
await new Promise(r => setTimeout(r, 0))
|
|
|
|
composable.creationForm.name = 'Composant Error'
|
|
|
|
await composable.submitCreation()
|
|
|
|
expect(mockShowError).toHaveBeenCalledWith('Server validation failed')
|
|
expect(mockShowSuccess).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('shows warning for failed custom fields but still navigates (composant exists)', async () => {
|
|
const createdComp = { id: 'comp-cf-warn-001', name: 'Composant CF Warn' }
|
|
mockCreateComposant.mockResolvedValue({ success: true, data: createdComp })
|
|
mockSaveAll.mockResolvedValue(['Tension nominale', 'Certifié CE'])
|
|
|
|
const composable = useComponentCreate()
|
|
composable.selectedTypeId.value = 'tc-moteur'
|
|
await new Promise(r => setTimeout(r, 0))
|
|
|
|
composable.creationForm.name = 'Composant CF Warn'
|
|
|
|
await composable.submitCreation()
|
|
|
|
// Custom field error toast is shown
|
|
expect(mockShowError).toHaveBeenCalledWith(
|
|
'Erreur sur les champs : Tension nominale, Certifié CE',
|
|
)
|
|
// But creation success toast is also shown (composant was created)
|
|
expect(mockShowSuccess).toHaveBeenCalledWith('Composant créé avec succès')
|
|
})
|
|
|
|
it('catches thrown exceptions and shows humanized error', async () => {
|
|
mockCreateComposant.mockRejectedValue(new Error('Network timeout'))
|
|
|
|
const composable = useComponentCreate()
|
|
composable.selectedTypeId.value = 'tc-moteur'
|
|
await new Promise(r => setTimeout(r, 0))
|
|
|
|
composable.creationForm.name = 'Composant Throw'
|
|
|
|
await composable.submitCreation()
|
|
|
|
expect(mockShowError).toHaveBeenCalledWith('Network timeout')
|
|
expect(mockSaveAll).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('resets submitting flag after failure', async () => {
|
|
mockCreateComposant.mockResolvedValue({ success: false, error: 'fail' })
|
|
|
|
const composable = useComponentCreate()
|
|
composable.selectedTypeId.value = 'tc-moteur'
|
|
await new Promise(r => setTimeout(r, 0))
|
|
|
|
composable.creationForm.name = 'Composant Reset Flag'
|
|
|
|
await composable.submitCreation()
|
|
|
|
expect(composable.submitting.value).toBe(false)
|
|
})
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ProductId from structure
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('submitCreation — productId from structure', () => {
|
|
beforeEach(() => {
|
|
mockHasAssignments.mockReturnValue(false)
|
|
mockSerializeStructureAssignments.mockReturnValue(null)
|
|
})
|
|
|
|
it('includes productId in payload when root product selection exists', async () => {
|
|
const createdComp = { id: 'comp-prodid-001', name: 'Composant ProductId' }
|
|
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 ProductId'
|
|
// Set structure assignments with a root product selection
|
|
composable.structureAssignments.value = {
|
|
path: 'root',
|
|
definition: {} as any,
|
|
selectedComponentId: '',
|
|
pieces: [],
|
|
products: [
|
|
{
|
|
path: 'root:product-0',
|
|
definition: { typeProductId: 'tprod-001' } as any,
|
|
selectedProductId: 'prod-selected-123',
|
|
},
|
|
],
|
|
subcomponents: [],
|
|
}
|
|
|
|
await composable.submitCreation()
|
|
|
|
const payload = mockCreateComposant.mock.calls[0]![0]
|
|
expect(payload.productId).toBe('prod-selected-123')
|
|
})
|
|
|
|
it('does not include productId when no root product is selected', async () => {
|
|
const createdComp = { id: 'comp-noprodid-001', name: 'Composant No ProductId' }
|
|
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 No ProductId'
|
|
composable.structureAssignments.value = {
|
|
path: 'root',
|
|
definition: {} as any,
|
|
selectedComponentId: '',
|
|
pieces: [],
|
|
products: [],
|
|
subcomponents: [],
|
|
}
|
|
|
|
await composable.submitCreation()
|
|
|
|
const payload = mockCreateComposant.mock.calls[0]![0]
|
|
expect(payload).not.toHaveProperty('productId')
|
|
})
|
|
|
|
it('does not include productId when product selection is empty string', async () => {
|
|
const createdComp = { id: 'comp-emptyprod-001', name: 'Composant Empty Product' }
|
|
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 Empty Product'
|
|
composable.structureAssignments.value = {
|
|
path: 'root',
|
|
definition: {} as any,
|
|
selectedComponentId: '',
|
|
pieces: [],
|
|
products: [
|
|
{
|
|
path: 'root:product-0',
|
|
definition: { typeProductId: 'tprod-001' } as any,
|
|
selectedProductId: '',
|
|
},
|
|
],
|
|
subcomponents: [],
|
|
}
|
|
|
|
await composable.submitCreation()
|
|
|
|
const payload = mockCreateComposant.mock.calls[0]![0]
|
|
expect(payload).not.toHaveProperty('productId')
|
|
})
|
|
})
|