test(custom-fields) : add data integrity tests for all field types

This commit is contained in:
2026-04-06 13:17:37 +02:00
parent 82cbeb91a5
commit b54739f6de

View File

@@ -0,0 +1,475 @@
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)
})
})