From eb68336723c9735f538be8c6f475c0ffe8898e46 Mon Sep 17 00:00:00 2001 From: r-dev Date: Mon, 6 Apr 2026 15:19:28 +0200 Subject: [PATCH] test(machine-custom-fields) : add checkbox and data integrity tests Co-Authored-By: Claude Opus 4.6 (1M context) --- .../useMachineDetailCustomFields.test.ts | 267 ++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 frontend/tests/composables/useMachineDetailCustomFields.test.ts diff --git a/frontend/tests/composables/useMachineDetailCustomFields.test.ts b/frontend/tests/composables/useMachineDetailCustomFields.test.ts new file mode 100644 index 0000000..35274b7 --- /dev/null +++ b/frontend/tests/composables/useMachineDetailCustomFields.test.ts @@ -0,0 +1,267 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { ref } from 'vue' + +import { useCustomFieldInputs } from '~/composables/useCustomFieldInputs' +import { + 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-mcfv-id', customField: { id: 'new-mcf-id' } }, + }) +}) + +// --------------------------------------------------------------------------- +// Helper — create composable with machine context (no context filter) +// --------------------------------------------------------------------------- + +function createMachineFields( + defs = mockMachineCustomFieldDefs, + vals = mockMachineCustomFieldValues, + entityId = 'cl-machine-1', +) { + return useCustomFieldInputs({ + definitions: ref(defs), + values: ref(vals), + entityType: 'machine', + entityId: ref(entityId), + // No context — machine custom fields don't use machineContextOnly filtering + }) +} + +// --------------------------------------------------------------------------- +// Machine custom field initialization +// --------------------------------------------------------------------------- + +describe('machine custom field initialization', () => { + it('loads all machine custom fields with values (5 fields)', () => { + const { fields } = createMachineFields() + + expect(fields.value).toHaveLength(5) + }) + + it('preserves text value (Numéro de série)', () => { + const { fields } = createMachineFields() + + const textField = fields.value.find(f => f.name === 'Numéro de série') + expect(textField?.value).toBe('SN-2025-001234') + expect(textField?.type).toBe('text') + }) + + it('preserves boolean value (En service = true)', () => { + const { fields } = createMachineFields() + + const boolField = fields.value.find(f => f.name === 'En service') + expect(boolField?.value).toBe('true') + expect(boolField?.type).toBe('boolean') + }) + + it('preserves number zero value (Puissance kW = 0)', () => { + const { fields } = createMachineFields() + + const numField = fields.value.find(f => f.name === 'Puissance (kW)') + expect(numField?.value).toBe('0') + expect(numField?.type).toBe('number') + }) + + it('preserves select value (Catégorie ATEX = Zone 1)', () => { + const { fields } = createMachineFields() + + const selectField = fields.value.find(f => f.name === 'Catégorie ATEX') + expect(selectField?.value).toBe('Zone 1') + expect(selectField?.type).toBe('select') + expect(selectField?.options).toEqual(['Zone 0', 'Zone 1', 'Zone 2', 'Non classé']) + }) + + it('preserves date value (Date mise en service = 2025-01-15)', () => { + const { fields } = createMachineFields() + + const dateField = fields.value.find(f => f.name === 'Date mise en service') + expect(dateField?.value).toBe('2025-01-15') + expect(dateField?.type).toBe('date') + }) +}) + +// --------------------------------------------------------------------------- +// Boolean checkbox — the critical test +// --------------------------------------------------------------------------- + +describe('boolean checkbox — the critical test', () => { + it('toggle true to false sends "false" (not deleted) via update()', async () => { + const { fields, update } = createMachineFields() + + const boolField = fields.value.find(f => f.name === 'En service')! + expect(boolField.value).toBe('true') + + // Toggle to false + boolField.value = 'false' + await update(boolField) + + expect(mockUpdateCustomFieldValue).toHaveBeenCalledWith('mcfv-002', { value: 'false' }) + }) + + it('toggle false to true sends "true"', async () => { + // Start with boolean value = false + const falseVal = { ...mockMachineCustomFieldValues[1]!, value: 'false' } + const vals = mockMachineCustomFieldValues.map((v, i) => (i === 1 ? falseVal : v)) + + const { fields, update } = createMachineFields(mockMachineCustomFieldDefs, vals) + + const boolField = fields.value.find(f => f.name === 'En service')! + expect(boolField.value).toBe('false') + + // Toggle to true + boolField.value = 'true' + await update(boolField) + + expect(mockUpdateCustomFieldValue).toHaveBeenCalledWith('mcfv-002', { value: 'true' }) + }) + + it('boolean false is persisted in saveAll (not skipped)', async () => { + // Only the boolean field with value "false" + const boolDef = mockMachineCustomFieldDefs[1]! + const boolVal = { ...mockMachineCustomFieldValues[1]!, value: 'false' } + + const { fields, saveAll } = createMachineFields([boolDef], [boolVal]) + + expect(fields.value[0]?.value).toBe('false') + + const failed = await saveAll() + expect(failed).toEqual([]) + expect(mockUpdateCustomFieldValue).toHaveBeenCalledWith('mcfv-002', { value: 'false' }) + }) +}) + +// --------------------------------------------------------------------------- +// Number zero — not lost +// --------------------------------------------------------------------------- + +describe('number zero — not lost', () => { + it('preserves zero value after load', () => { + const { fields } = createMachineFields() + + const numField = fields.value.find(f => f.name === 'Puissance (kW)')! + expect(numField.value).toBe('0') + }) + + it('saves zero value (not skipped) in saveAll', async () => { + // Only the number field with value "0" + const numDef = mockMachineCustomFieldDefs[2]! + const numVal = mockMachineCustomFieldValues[2]! + + const { saveAll } = createMachineFields([numDef], [numVal]) + + const failed = await saveAll() + expect(failed).toEqual([]) + expect(mockUpdateCustomFieldValue).toHaveBeenCalledWith('mcfv-003', { value: '0' }) + }) +}) + +// --------------------------------------------------------------------------- +// Select field +// --------------------------------------------------------------------------- + +describe('select field', () => { + it('preserves selected option', () => { + const { fields } = createMachineFields() + + const selectField = fields.value.find(f => f.name === 'Catégorie ATEX')! + expect(selectField.value).toBe('Zone 1') + }) + + it('uses defaultValue when no value exists', () => { + // Use defs with a select that has a defaultValue + const defsWithDefault = mockMachineCustomFieldDefs.map((d, i) => + i === 3 ? { ...d, defaultValue: 'Non classé' } : d, + ) + + // No values for the select field + const valsWithoutSelect = mockMachineCustomFieldValues.filter( + v => v.customField.name !== 'Catégorie ATEX', + ) + + const { fields } = createMachineFields(defsWithDefault, valsWithoutSelect) + + const selectField = fields.value.find(f => f.name === 'Catégorie ATEX')! + expect(selectField.value).toBe('Non classé') + }) +}) + +// --------------------------------------------------------------------------- +// Field isolation +// --------------------------------------------------------------------------- + +describe('field isolation', () => { + it('updating one field does not change other field values', async () => { + const { fields, update } = createMachineFields() + + // Snapshot original values + const originalValues = fields.value.map(f => ({ name: f.name, value: f.value })) + + // Update only the text field + const textField = fields.value.find(f => f.name === 'Numéro de série')! + textField.value = 'SN-UPDATED-999' + await update(textField) + + // All other fields should still have their original values + for (const field of fields.value) { + if (field.name === 'Numéro de série') continue + const original = originalValues.find(o => o.name === field.name) + expect(field.value).toBe(original?.value) + } + }) + + it('saveAll preserves all field values even on partial failure', async () => { + // Make the second call fail (boolean field) + mockUpdateCustomFieldValue + .mockResolvedValueOnce({ success: true }) // text — Numéro de série + .mockResolvedValueOnce({ success: false }) // boolean — En service + .mockResolvedValue({ success: true }) // rest succeed + + const { fields, saveAll } = createMachineFields() + + // Snapshot values before saveAll + const valuesBefore = fields.value.map(f => ({ name: f.name, value: f.value })) + + const failed = await saveAll() + + // Only the boolean field should have failed + expect(failed).toEqual(['En service']) + + // All field values should still be intact (not cleared or corrupted) + for (const field of fields.value) { + const before = valuesBefore.find(v => v.name === field.name) + expect(field.value).toBe(before?.value) + } + }) +})