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>
645 lines
20 KiB
TypeScript
645 lines
20 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
|
|
import {
|
|
mockPieceFromApi,
|
|
mockLinkSKF,
|
|
mockLinkFAG,
|
|
mockConstructeurSKF,
|
|
mockConstructeurFAG,
|
|
wrapCollection,
|
|
} from '../fixtures/mockData'
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Import under test (AFTER all vi.mock calls)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
import { usePieceEdit } from '~/composables/usePieceEdit'
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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 — usePieces (updatePiece)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const mockUpdatePiece = vi.fn()
|
|
|
|
vi.mock('~/composables/usePieces', () => ({
|
|
usePieces: () => ({
|
|
updatePiece: mockUpdatePiece,
|
|
pieces: { value: [] },
|
|
loading: { value: false },
|
|
}),
|
|
}))
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Mocks — usePieceTypes
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const mockPieceTypes = { value: [] as any[] }
|
|
const mockLoadPieceTypes = vi.fn().mockResolvedValue(undefined)
|
|
|
|
vi.mock('~/composables/usePieceTypes', () => ({
|
|
usePieceTypes: () => ({
|
|
pieceTypes: mockPieceTypes,
|
|
loadPieceTypes: mockLoadPieceTypes,
|
|
}),
|
|
}))
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Mocks — useDocuments
|
|
// ---------------------------------------------------------------------------
|
|
|
|
vi.mock('~/composables/useDocuments', () => ({
|
|
useDocuments: () => ({
|
|
loadDocumentsByPiece: vi.fn().mockResolvedValue({ success: true, data: [] }),
|
|
uploadDocuments: vi.fn().mockResolvedValue({ success: true, data: [] }),
|
|
deleteDocument: vi.fn().mockResolvedValue({ success: true }),
|
|
documents: { value: [] },
|
|
loading: { value: false },
|
|
}),
|
|
}))
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Mocks — useConstructeurLinks
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const mockFetchLinks = vi.fn().mockResolvedValue([])
|
|
const mockSyncLinks = vi.fn().mockResolvedValue(undefined)
|
|
|
|
vi.mock('~/composables/useConstructeurLinks', () => ({
|
|
useConstructeurLinks: () => ({
|
|
fetchLinks: mockFetchLinks,
|
|
syncLinks: mockSyncLinks,
|
|
}),
|
|
}))
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Mocks — useCustomFieldInputs
|
|
// ---------------------------------------------------------------------------
|
|
|
|
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)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
vi.stubGlobal('usePermissions', () => ({
|
|
canEdit: { value: true },
|
|
canManage: { value: true },
|
|
isAdmin: { value: false },
|
|
isGranted: () => true,
|
|
}))
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Mocks — useConstructeurs
|
|
// ---------------------------------------------------------------------------
|
|
|
|
vi.mock('~/composables/useConstructeurs', () => ({
|
|
useConstructeurs: () => ({
|
|
ensureConstructeurs: vi.fn().mockResolvedValue([]),
|
|
}),
|
|
}))
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Mocks — useEntityHistory
|
|
// ---------------------------------------------------------------------------
|
|
|
|
vi.mock('~/composables/useEntityHistory', () => ({
|
|
useEntityHistory: () => ({
|
|
history: { value: [] },
|
|
loading: { value: false },
|
|
error: { value: null },
|
|
loadHistory: vi.fn().mockResolvedValue([]),
|
|
}),
|
|
}))
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Mocks — shared utils
|
|
// ---------------------------------------------------------------------------
|
|
|
|
vi.mock('~/shared/modelUtils', () => ({
|
|
formatPieceStructurePreview: () => '',
|
|
}))
|
|
|
|
vi.mock('~/shared/constructeurUtils', () => ({
|
|
uniqueConstructeurIds: (ids: string[]) => [...new Set(ids)],
|
|
constructeurIdsFromLinks: (links: any[]) => links.map((l: any) => l.constructeurId),
|
|
}))
|
|
|
|
vi.mock('~/utils/documentPreview', () => ({
|
|
canPreviewDocument: () => false,
|
|
}))
|
|
|
|
vi.mock('~/services/modelTypes', () => ({
|
|
getModelType: vi.fn().mockResolvedValue(null),
|
|
}))
|
|
|
|
vi.mock('~/shared/apiRelations', () => ({
|
|
extractRelationId: (rel: any) => {
|
|
if (typeof rel === 'string') return rel
|
|
if (rel && typeof rel === 'object' && 'id' in rel) return rel.id
|
|
return null
|
|
},
|
|
}))
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Test data
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const PIECE_ID = 'piece-001'
|
|
|
|
const mockPieceType = {
|
|
id: 'tp-bearing-001',
|
|
name: 'Roulement',
|
|
code: 'ROUL',
|
|
category: 'PIECE',
|
|
structure: {
|
|
products: [
|
|
{
|
|
typeProductId: 'tprod-grease-001',
|
|
typeProductLabel: 'Graisse SKF',
|
|
familyCode: 'LUB',
|
|
role: 'lubrification',
|
|
},
|
|
],
|
|
customFields: [],
|
|
},
|
|
}
|
|
|
|
function buildPieceWithProducts() {
|
|
return {
|
|
...mockPieceFromApi,
|
|
id: PIECE_ID,
|
|
'@id': `/api/pieces/${PIECE_ID}`,
|
|
description: 'Roulement haute performance',
|
|
prix: '42.50',
|
|
typePieceId: 'tp-bearing-001',
|
|
productIds: ['prod-001'],
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const tick = () => new Promise(r => setTimeout(r, 0))
|
|
|
|
async function createAndHydrate(overrides?: Record<string, any>) {
|
|
const pieceData = { ...buildPieceWithProducts(), ...overrides }
|
|
|
|
mockGet.mockImplementation((url: string) => {
|
|
if (url.includes(`/pieces/${PIECE_ID}`)) {
|
|
return Promise.resolve({ success: true, data: structuredClone(pieceData) })
|
|
}
|
|
return Promise.resolve({ success: true, data: wrapCollection([]) })
|
|
})
|
|
|
|
mockFetchLinks.mockResolvedValue([
|
|
{ ...mockLinkSKF },
|
|
{ ...mockLinkFAG },
|
|
])
|
|
|
|
const composable = usePieceEdit(PIECE_ID)
|
|
|
|
await composable.fetchPiece()
|
|
await tick()
|
|
|
|
return composable
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// beforeEach
|
|
// ---------------------------------------------------------------------------
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mockPieceTypes.value = [mockPieceType]
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// fetchPiece — hydration
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('fetchPiece — hydration', () => {
|
|
it('loads all simple fields (name, reference, description, prix)', async () => {
|
|
const composable = await createAndHydrate()
|
|
|
|
expect(composable.editionForm.name).toBe('Roulement 6205')
|
|
expect(composable.editionForm.reference).toBe('ROUL-6205')
|
|
expect(composable.editionForm.description).toBe('Roulement haute performance')
|
|
expect(composable.editionForm.prix).toBe('42.50')
|
|
})
|
|
|
|
it('loads piece with product slots', async () => {
|
|
const composable = await createAndHydrate()
|
|
|
|
expect(composable.piece.value).not.toBeNull()
|
|
expect(composable.piece.value.productSlots).toHaveLength(1)
|
|
expect(composable.piece.value.productSlots[0].product.id).toBe('prod-001')
|
|
})
|
|
|
|
it('loads constructeur links via fetchLinks', async () => {
|
|
const composable = await createAndHydrate()
|
|
|
|
expect(mockFetchLinks).toHaveBeenCalledWith('piece', PIECE_ID)
|
|
expect(composable.constructeurLinks.value).toHaveLength(2)
|
|
expect(composable.constructeurLinks.value[0].constructeurId).toBe(mockConstructeurSKF.id)
|
|
expect(composable.constructeurLinks.value[1].constructeurId).toBe(mockConstructeurFAG.id)
|
|
})
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Product selections
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('product selections', () => {
|
|
it('setProductSelection updates the correct index', async () => {
|
|
const composable = await createAndHydrate()
|
|
|
|
// The structure has 1 product requirement, so productSelections should have 1 entry
|
|
composable.setProductSelection(0, 'prod-new-001')
|
|
await tick()
|
|
|
|
expect(composable.productSelections.value[0]).toBe('prod-new-001')
|
|
})
|
|
|
|
it('setProductSelection to null does not crash', async () => {
|
|
const composable = await createAndHydrate()
|
|
|
|
// Set then clear
|
|
composable.setProductSelection(0, 'prod-001')
|
|
await tick()
|
|
|
|
composable.setProductSelection(0, null)
|
|
await tick()
|
|
|
|
expect(composable.productSelections.value[0]).toBeNull()
|
|
})
|
|
})
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// submitEdition — no data loss
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('submitEdition — no data loss', () => {
|
|
it('sends all form fields in update payload', async () => {
|
|
mockUpdatePiece.mockResolvedValue({ success: true, data: { id: PIECE_ID } })
|
|
|
|
const composable = await createAndHydrate()
|
|
|
|
composable.editionForm.name = 'Roulement modifie'
|
|
composable.editionForm.description = 'Nouvelle description'
|
|
composable.editionForm.reference = 'REF-MOD-001'
|
|
composable.editionForm.prix = '99.99'
|
|
|
|
// Ensure product selection is filled so submit proceeds
|
|
composable.setProductSelection(0, 'prod-001')
|
|
await tick()
|
|
|
|
await composable.submitEdition()
|
|
|
|
expect(mockUpdatePiece).toHaveBeenCalledTimes(1)
|
|
const payload = mockUpdatePiece.mock.calls[0]![1]
|
|
expect(payload).toMatchObject({
|
|
name: 'Roulement modifie',
|
|
description: 'Nouvelle description',
|
|
reference: 'REF-MOD-001',
|
|
prix: '99.99',
|
|
})
|
|
})
|
|
|
|
it('saves custom fields after piece update', 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)
|
|
expect(mockSaveAll).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('syncs constructeur links', async () => {
|
|
mockUpdatePiece.mockResolvedValue({ success: true, data: { id: PIECE_ID } })
|
|
|
|
const composable = await createAndHydrate()
|
|
composable.setProductSelection(0, 'prod-001')
|
|
await tick()
|
|
|
|
await composable.submitEdition()
|
|
|
|
expect(mockSyncLinks).toHaveBeenCalledTimes(1)
|
|
const [entityType, entityId, origLinks, formLinks] = mockSyncLinks.mock.calls[0]!
|
|
expect(entityType).toBe('piece')
|
|
expect(entityId).toBe(PIECE_ID)
|
|
expect(origLinks).toHaveLength(2)
|
|
expect(formLinks).toHaveLength(2)
|
|
})
|
|
|
|
it('editing name does not lose constructeur links', async () => {
|
|
mockUpdatePiece.mockResolvedValue({ success: true, data: { id: PIECE_ID } })
|
|
|
|
const composable = await createAndHydrate()
|
|
|
|
// Only edit name
|
|
composable.editionForm.name = 'Nouveau nom piece'
|
|
composable.setProductSelection(0, 'prod-001')
|
|
await tick()
|
|
|
|
await composable.submitEdition()
|
|
|
|
expect(mockUpdatePiece).toHaveBeenCalledTimes(1)
|
|
const payload = mockUpdatePiece.mock.calls[0]![1]
|
|
expect(payload.name).toBe('Nouveau nom piece')
|
|
|
|
// syncLinks still called with constructeur links preserved
|
|
expect(mockSyncLinks).toHaveBeenCalledTimes(1)
|
|
const [, , origLinks, formLinks] = mockSyncLinks.mock.calls[0]!
|
|
expect(origLinks).toHaveLength(2)
|
|
expect(formLinks).toHaveLength(2)
|
|
expect(formLinks[0].constructeurId).toBe(mockConstructeurSKF.id)
|
|
expect(formLinks[1].constructeurId).toBe(mockConstructeurFAG.id)
|
|
})
|
|
|
|
it('editing name does not lose product slots', async () => {
|
|
mockUpdatePiece.mockResolvedValue({ success: true, data: { id: PIECE_ID } })
|
|
|
|
const composable = await createAndHydrate()
|
|
|
|
// Set product selection
|
|
composable.setProductSelection(0, 'prod-001')
|
|
await tick()
|
|
|
|
// Now edit only name
|
|
composable.editionForm.name = 'Autre nom'
|
|
await tick()
|
|
|
|
await composable.submitEdition()
|
|
|
|
const payload = mockUpdatePiece.mock.calls[0]![1]
|
|
expect(payload.name).toBe('Autre nom')
|
|
// productIds should still contain the selection
|
|
expect(payload.productIds).toContain('prod-001')
|
|
})
|
|
|
|
it('adding a constructeur preserves existing ones', async () => {
|
|
mockUpdatePiece.mockResolvedValue({ success: true, data: { id: PIECE_ID } })
|
|
|
|
const composable = await createAndHydrate()
|
|
composable.setProductSelection(0, 'prod-001')
|
|
await tick()
|
|
|
|
// Initially has SKF + FAG from fetchLinks
|
|
expect(composable.constructeurLinks.value).toHaveLength(2)
|
|
|
|
// Add a third constructeur
|
|
const newLink = {
|
|
linkId: null as string | null,
|
|
constructeurId: 'cstr-new-003',
|
|
constructeur: { id: 'cstr-new-003', name: 'NEW Corp', email: null, phone: null },
|
|
supplierReference: 'NEW-REF-001',
|
|
}
|
|
composable.constructeurLinks.value = [
|
|
...composable.constructeurLinks.value,
|
|
newLink,
|
|
]
|
|
await tick()
|
|
|
|
await composable.submitEdition()
|
|
|
|
expect(mockSyncLinks).toHaveBeenCalledTimes(1)
|
|
const [, , origLinks, formLinks] = mockSyncLinks.mock.calls[0]!
|
|
// Original had 2 (SKF + FAG)
|
|
expect(origLinks).toHaveLength(2)
|
|
// Form now has 3 (SKF + FAG + NEW)
|
|
expect(formLinks).toHaveLength(3)
|
|
expect(formLinks[0].constructeurId).toBe(mockConstructeurSKF.id)
|
|
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.')
|
|
})
|
|
})
|