// @vitest-environment happy-dom // // Editar un registro con un select que YA tiene valor y guardar sin tocarlo debe // conservar ese valor en el payload. Antes los `values` del DynamicForm se // sembraban en un efecto (primer render con '') y el Select de Radix emitía // `onValueChange('')`, que pisaba el valor real y viajaba en el PATCH (p.ej. un // renglón «Refacción» quedaba en «-»). Cubre también: `readonly` de columna en // `deriveRelationFormFields`, selects vacíos no tocados fuera del PATCH, y el // modal de renglón con cuerpo con scroll y footer fijo. import { afterEach, describe, expect, it, vi } from 'vitest' import { act, cleanup, render, screen, waitFor } from '@testing-library/react' vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (k: string, o?: any) => o?.defaultValue ?? k, i18n: { language: 'es' } }), })) import { DynamicForm, guardEmptySelect } from '../dynamic-form' import { DynamicRelation } from '../dynamic-relation' import { deriveRelationFormFields } from '../dynamic-relation-helpers' import { ApiProvider, type ApiClient } from '../api-context' import type { ActionFieldDef } from '../types' afterEach(cleanup) const kindField: ActionFieldDef = { key: 'kind', label: 'Tipo', type: 'select', options: [ { value: 'service', label: 'Servicio' }, { value: 'part', label: 'Refacción' }, ], } describe('DynamicForm — edición con select', () => { it('guardar sin tocar el select conserva su valor en el payload', async () => { const onSubmit = vi.fn() render( , ) await act(async () => { screen.getByText('Guardar').click() }) expect(onSubmit).toHaveBeenCalledTimes(1) expect(onSubmit.mock.calls[0][0]).toMatchObject({ description: 'Balata', kind: 'part' }) }) it('en edición no manda un select vacío que el usuario no tocó', async () => { const onSubmit = vi.fn() render( , ) await act(async () => { screen.getByText('Guardar').click() }) expect(onSubmit).toHaveBeenCalledTimes(1) expect(onSubmit.mock.calls[0][0]).not.toHaveProperty('kind') }) }) describe('guardEmptySelect', () => { it("ignora '' cuando ya hay valor y '' no es opción", () => { const onChange = vi.fn() guardEmptySelect('part', onChange, ['service', 'part'])('') expect(onChange).not.toHaveBeenCalled() }) it("deja pasar '' si no había valor o si '' es opción válida, y cualquier otro valor", () => { const onChange = vi.fn() guardEmptySelect('', onChange, ['service', 'part'])('') guardEmptySelect('part', onChange, ['', 'part'])('') guardEmptySelect('part', onChange, ['service', 'part'])('service') expect(onChange.mock.calls).toEqual([[''], [''], ['service']]) }) }) const META = { name: 'work_order_item', columns: [ { key: 'id', label: 'ID', type: 'text', sortable: true, filterable: false, hidden: true }, { key: 'work_order_id', label: 'OT', type: 'text', sortable: false, filterable: false }, { key: 'description', label: 'Descripción', type: 'text', sortable: true, filterable: true }, { key: 'kind', label: 'Tipo', type: 'select', sortable: true, filterable: true, options: [ { value: 'service', label: 'Servicio' }, { value: 'part', label: 'Refacción' }, ], }, { key: 'line_total', label: 'Importe', type: 'number', sortable: false, filterable: false, readonly: true }, ], actions: [], hasActions: false, enableCRUDActions: false, } const DATA = [{ id: 'woi_1', work_order_id: 'wo_1', description: 'Balata', kind: 'part', line_total: 450 }] function mockApi(): ApiClient { return { get: vi.fn((url: string) => { if (url.startsWith('/metadata/table/')) { return Promise.resolve({ data: { success: true, data: META } }) } return Promise.resolve({ data: { success: true, data: DATA } }) }), post: vi.fn(() => Promise.resolve({ data: { success: true, data: {} } })), put: vi.fn(() => Promise.resolve({ data: { success: true, data: {} } })), delete: vi.fn(() => Promise.resolve({ data: { success: true, data: {} } })), } } describe('DynamicRelation — editar renglón con select', () => { it('deriveRelationFormFields copia readonly de la columna', () => { const fields = deriveRelationFormFields(META as any, 'work_order_id') expect(fields.find((f) => f.key === 'line_total')?.readonly).toBe(true) expect(fields.find((f) => f.key === 'kind')?.readonly).toBeUndefined() }) it('el PATCH conserva el tipo y no manda columnas readonly; el modal hace scroll con footer fijo', async () => { const api = mockApi() render( , ) await waitFor(() => expect(screen.getByText('Balata')).toBeTruthy()) await act(async () => { screen.getByLabelText('Editar').click() }) const dialog = await screen.findByRole('dialog') expect(dialog.className).toContain('max-h-[90dvh]') expect(dialog.querySelector('.overflow-y-auto')).toBeTruthy() await act(async () => { screen.getByText('Guardar').click() }) await waitFor(() => expect(api.put).toHaveBeenCalledTimes(1)) const [url, payload] = (api.put as any).mock.calls[0] expect(url).toContain('woi_1') expect(payload).toMatchObject({ description: 'Balata', kind: 'part' }) expect(payload).not.toHaveProperty('line_total') }) })