import { describe, it, expect, vi, beforeEach } from 'vitest' import { ref } from 'vue' import { useCustomFieldInputs } from '~/composables/useCustomFieldInputs' import { shouldPersist, formatValueForSave, } from '~/shared/utils/customFields' import { mockCustomFieldDefs, mockCustomFieldValues, mockMachineCustomFieldDefs, mockMachineCustomFieldValues, } from '../fixtures/mockData' // --------------------------------------------------------------------------- // Mocks // --------------------------------------------------------------------------- const mockUpdateCustomFieldValue = vi.fn() const mockUpsertCustomFieldValue = vi.fn() vi.mock('~/composables/useCustomFields', () => ({ useCustomFields: () => ({ updateCustomFieldValue: mockUpdateCustomFieldValue, upsertCustomFieldValue: mockUpsertCustomFieldValue, }), })) vi.mock('~/composables/useToast', () => ({ useToast: () => ({ showSuccess: vi.fn(), showError: vi.fn(), showInfo: vi.fn(), showToast: vi.fn(), toasts: { value: [] }, clearAll: vi.fn(), }), })) beforeEach(() => { vi.clearAllMocks() mockUpdateCustomFieldValue.mockResolvedValue({ success: true }) mockUpsertCustomFieldValue.mockResolvedValue({ success: true, data: { id: 'new-cfv-id', customField: { id: 'new-cf-id' } } }) }) // --------------------------------------------------------------------------- // Field initialization // --------------------------------------------------------------------------- describe('field initialization', () => { it('merges all definitions with their values (6 defs → 6 allFields, 5 standalone fields)', () => { const { fields, allFields } = useCustomFieldInputs({ definitions: ref(mockCustomFieldDefs), values: ref(mockCustomFieldValues), entityType: 'composant', entityId: ref('cl-comp-1'), context: 'standalone', }) expect(allFields.value).toHaveLength(6) expect(fields.value).toHaveLength(5) }) it('preserves value for number type', () => { const { fields } = useCustomFieldInputs({ definitions: ref(mockCustomFieldDefs), values: ref(mockCustomFieldValues), entityType: 'composant', entityId: ref('cl-comp-1'), context: 'standalone', }) const numberField = fields.value.find(f => f.name === 'Tension nominale') expect(numberField?.value).toBe('220') expect(numberField?.type).toBe('number') }) it('preserves value for boolean type', () => { const { fields } = useCustomFieldInputs({ definitions: ref(mockCustomFieldDefs), values: ref(mockCustomFieldValues), entityType: 'composant', entityId: ref('cl-comp-1'), context: 'standalone', }) const boolField = fields.value.find(f => f.name === 'Certifié CE') expect(boolField?.value).toBe('true') expect(boolField?.type).toBe('boolean') }) it('preserves value for select type with options', () => { const { fields } = useCustomFieldInputs({ definitions: ref(mockCustomFieldDefs), values: ref(mockCustomFieldValues), entityType: 'composant', entityId: ref('cl-comp-1'), context: 'standalone', }) const selectField = fields.value.find(f => f.name === 'Indice de protection') expect(selectField?.value).toBe('IP65') expect(selectField?.type).toBe('select') expect(selectField?.options).toEqual(['IP54', 'IP55', 'IP65']) }) it('preserves value for date type', () => { const { fields } = useCustomFieldInputs({ definitions: ref(mockCustomFieldDefs), values: ref(mockCustomFieldValues), entityType: 'composant', entityId: ref('cl-comp-1'), context: 'standalone', }) const dateField = fields.value.find(f => f.name === 'Date de calibration') expect(dateField?.value).toBe('2025-06-15') expect(dateField?.type).toBe('date') }) it('preserves value for text type', () => { const { fields } = useCustomFieldInputs({ definitions: ref(mockCustomFieldDefs), values: ref(mockCustomFieldValues), entityType: 'composant', entityId: ref('cl-comp-1'), context: 'standalone', }) const textField = fields.value.find(f => f.name === 'Remarques techniques') expect(textField?.value).toBe('Roulement renforcé pour environnement humide') expect(textField?.type).toBe('text') }) it('uses defaultValue when no persisted value exists', () => { // Pass empty values array so all fields use defaultValue const { fields } = useCustomFieldInputs({ definitions: ref(mockCustomFieldDefs), values: ref([]), entityType: 'composant', entityId: ref('cl-comp-1'), context: 'standalone', }) const numberField = fields.value.find(f => f.name === 'Tension nominale') expect(numberField?.value).toBe('220') const boolField = fields.value.find(f => f.name === 'Certifié CE') expect(boolField?.value).toBe('false') // No defaultValue → empty string const dateField = fields.value.find(f => f.name === 'Date de calibration') expect(dateField?.value).toBe('') }) it('filters machineContextOnly in standalone context (allFields=6, fields=5)', () => { const { fields, allFields } = useCustomFieldInputs({ definitions: ref(mockCustomFieldDefs), values: ref(mockCustomFieldValues), entityType: 'composant', entityId: ref('cl-comp-1'), context: 'standalone', }) expect(allFields.value).toHaveLength(6) expect(fields.value).toHaveLength(5) expect(fields.value.every(f => !f.machineContextOnly)).toBe(true) }) it('shows only machineContextOnly in machine context (1 field)', () => { const { fields } = useCustomFieldInputs({ definitions: ref(mockCustomFieldDefs), values: ref(mockCustomFieldValues), entityType: 'composant', entityId: ref('cl-comp-1'), context: 'machine', }) expect(fields.value).toHaveLength(1) expect(fields.value[0]?.name).toBe('Position sur machine') expect(fields.value[0]?.machineContextOnly).toBe(true) }) }) // --------------------------------------------------------------------------- // Boolean — the tricky case // --------------------------------------------------------------------------- describe('boolean — the tricky case', () => { it('saves "false" value via update (not ignored)', async () => { const { fields, update } = useCustomFieldInputs({ definitions: ref(mockCustomFieldDefs), values: ref(mockCustomFieldValues), entityType: 'composant', entityId: ref('cl-comp-1'), context: 'standalone', }) const boolField = fields.value.find(f => f.name === 'Certifié CE')! boolField.value = 'false' await update(boolField) expect(mockUpdateCustomFieldValue).toHaveBeenCalledWith('cfv-002', { value: 'false' }) }) it('persists boolean "false" in saveAll (not skipped)', async () => { // Only provide the boolean field def + value const boolDef = mockCustomFieldDefs[1]! const boolVal = { ...mockCustomFieldValues[1]!, value: 'false' } const { fields, saveAll } = useCustomFieldInputs({ definitions: ref([boolDef]), values: ref([boolVal]), entityType: 'composant', entityId: ref('cl-comp-1'), context: 'standalone', }) expect(fields.value[0]?.value).toBe('false') const failed = await saveAll() expect(failed).toEqual([]) expect(mockUpdateCustomFieldValue).toHaveBeenCalledWith('cfv-002', { value: 'false' }) }) }) // --------------------------------------------------------------------------- // Number zero // --------------------------------------------------------------------------- describe('number zero', () => { it('saves "0" value (not ignored)', async () => { const { fields, update } = useCustomFieldInputs({ definitions: ref(mockMachineCustomFieldDefs), values: ref(mockMachineCustomFieldValues), entityType: 'machine', entityId: ref('cl-machine-1'), context: 'standalone', }) const numField = fields.value.find(f => f.name === 'Puissance (kW)')! expect(numField.value).toBe('0') await update(numField) expect(mockUpdateCustomFieldValue).toHaveBeenCalledWith('mcfv-003', { value: '0' }) }) }) // --------------------------------------------------------------------------- // Text empty string // --------------------------------------------------------------------------- describe('text empty string', () => { it('shouldPersist returns false for empty trimmed string', () => { const field = { customFieldId: 'cf-1', customFieldValueId: null, name: 'Notes', type: 'text', required: false, options: [], defaultValue: null, orderIndex: 0, machineContextOnly: false, value: ' ', } expect(shouldPersist(field)).toBe(false) }) it('persists non-empty text value', () => { const field = { customFieldId: 'cf-1', customFieldValueId: null, name: 'Notes', type: 'text', required: false, options: [], defaultValue: null, orderIndex: 0, machineContextOnly: false, value: 'some text', } expect(shouldPersist(field)).toBe(true) expect(formatValueForSave(field)).toBe('some text') }) }) // --------------------------------------------------------------------------- // Select // --------------------------------------------------------------------------- describe('select', () => { it('saves changed option value', async () => { const { fields, update } = useCustomFieldInputs({ definitions: ref(mockCustomFieldDefs), values: ref(mockCustomFieldValues), entityType: 'composant', entityId: ref('cl-comp-1'), context: 'standalone', }) const selectField = fields.value.find(f => f.name === 'Indice de protection')! selectField.value = 'IP55' await update(selectField) expect(mockUpdateCustomFieldValue).toHaveBeenCalledWith('cfv-003', { value: 'IP55' }) }) }) // --------------------------------------------------------------------------- // Date // --------------------------------------------------------------------------- describe('date', () => { it('saves date value in correct format', async () => { const { fields, update } = useCustomFieldInputs({ definitions: ref(mockCustomFieldDefs), values: ref(mockCustomFieldValues), entityType: 'composant', entityId: ref('cl-comp-1'), context: 'standalone', }) const dateField = fields.value.find(f => f.name === 'Date de calibration')! dateField.value = '2026-01-20' await update(dateField) expect(mockUpdateCustomFieldValue).toHaveBeenCalledWith('cfv-004', { value: '2026-01-20' }) }) }) // --------------------------------------------------------------------------- // saveAll isolation // --------------------------------------------------------------------------- describe('saveAll isolation', () => { it('saves all fields independently without losing values', async () => { const { fields, saveAll } = useCustomFieldInputs({ definitions: ref(mockCustomFieldDefs), values: ref(mockCustomFieldValues), entityType: 'composant', entityId: ref('cl-comp-1'), context: 'standalone', }) // Modify one field const numberField = fields.value.find(f => f.name === 'Tension nominale')! numberField.value = '380' const failed = await saveAll() expect(failed).toEqual([]) // All persistable fields should have been saved // 5 fields in standalone context, all have values expect(mockUpdateCustomFieldValue.mock.calls.length).toBeGreaterThanOrEqual(4) // The modified field should have the new value const numberCall = mockUpdateCustomFieldValue.mock.calls.find( (c: any[]) => c[0] === 'cfv-001', ) expect(numberCall?.[1]).toEqual({ value: '380' }) // Another field should still have its original value const boolCall = mockUpdateCustomFieldValue.mock.calls.find( (c: any[]) => c[0] === 'cfv-002', ) expect(boolCall?.[1]).toEqual({ value: 'true' }) }) it('upserts new value when no customFieldValueId exists', async () => { // Use defs without matching values — no customFieldValueId const defs = [mockCustomFieldDefs[0]!] const { saveAll } = useCustomFieldInputs({ definitions: ref(defs), values: ref([]), entityType: 'composant', entityId: ref('cl-comp-1'), context: 'standalone', }) const failed = await saveAll() expect(failed).toEqual([]) // Should use upsert since no customFieldValueId expect(mockUpsertCustomFieldValue).toHaveBeenCalledWith( 'cf-def-001', 'composant', 'cl-comp-1', '220', undefined, ) }) it('returns failed field names on error', async () => { mockUpdateCustomFieldValue.mockResolvedValueOnce({ success: false }) const defs = [mockCustomFieldDefs[0]!] const vals = [mockCustomFieldValues[0]!] const { saveAll } = useCustomFieldInputs({ definitions: ref(defs), values: ref(vals), entityType: 'composant', entityId: ref('cl-comp-1'), context: 'standalone', }) const failed = await saveAll() expect(failed).toEqual(['Tension nominale']) }) }) // --------------------------------------------------------------------------- // requiredFilled validation // --------------------------------------------------------------------------- describe('requiredFilled validation', () => { it('returns true when required fields have values (including defaultValue)', () => { const { requiredFilled } = useCustomFieldInputs({ definitions: ref(mockCustomFieldDefs), values: ref(mockCustomFieldValues), entityType: 'composant', entityId: ref('cl-comp-1'), context: 'standalone', }) // "Tension nominale" is required and has value '220' expect(requiredFilled.value).toBe(true) }) it('returns true when required field uses defaultValue', () => { // No values provided — required field should use defaultValue '220' const { requiredFilled } = useCustomFieldInputs({ definitions: ref(mockCustomFieldDefs), values: ref([]), entityType: 'composant', entityId: ref('cl-comp-1'), context: 'standalone', }) expect(requiredFilled.value).toBe(true) }) it('returns false when required field has no value and no default', () => { // Create a required field with no default and no value const defs = [{ id: 'cf-required-no-default', name: 'Required Field', type: 'text', required: true, options: [], defaultValue: null, orderIndex: 0, machineContextOnly: false, }] const { requiredFilled } = useCustomFieldInputs({ definitions: ref(defs), values: ref([]), entityType: 'composant', entityId: ref('cl-comp-1'), context: 'standalone', }) expect(requiredFilled.value).toBe(false) }) })