// @vitest-environment happy-dom // // Record EDIT dialog polish (mode='edit'), affecting every module that uses the // generic dialog (transfers, orders, customers): // PART 1 — a jsonb line-items field renders READ-ONLY as the inline table // (the detail-view renderer), never an input that stringifies to // "[object Object]". // PART 2 — a FK select seeds its trigger from the backend-injected relation // sibling (`source_warehouse_id` → `source_warehouse: {value,label}`) // so an existing selection shows the related record's NAME, not the // raw uuid. import { afterEach, describe, expect, it, vi } from 'vitest' import { cleanup, render, screen } from '@testing-library/react' // Identity translator (so `defaultValue` surfaces) + Spanish locale. vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (k: string, o?: any) => o?.defaultValue ?? k, i18n: { language: 'es' }, }), })) import { EditField, isLineItemsField, fkSeedOption } from '../dialogs/dynamic-record' import { ApiProvider, type ApiClient } from '../api-context' afterEach(cleanup) const noopApi = { get: vi.fn(async () => ({ data: { data: [] } })) } as unknown as ApiClient describe('isLineItemsField', () => { it('matches a field with a declared item_fields schema', () => { expect( isLineItemsField( { key: 'items', label: 'Items', type: 'json', itemFields: [{ key: 'q', label: 'Q' }] }, null, ), ).toBe(true) // snake_case alias expect( isLineItemsField( { key: 'items', label: 'Items', type: 'json', item_fields: [{ key: 'q', label: 'Q' }] }, null, ), ).toBe(true) }) it('matches any array / plain-object value (would stringify to [object Object])', () => { expect(isLineItemsField({ key: 'items', label: 'Items', type: 'json' }, [{ a: 1 }])).toBe(true) expect(isLineItemsField({ key: 'data', label: 'Data', type: 'json' }, { a: 1 })).toBe(true) }) it('does NOT match scalars or dedicated editable widgets', () => { expect(isLineItemsField({ key: 'name', label: 'Name', type: 'text' }, 'hi')).toBe(false) expect(isLineItemsField({ key: 'n', label: 'N', type: 'number' }, 5)).toBe(false) expect(isLineItemsField({ key: 'photo', label: 'Photo', type: 'image' }, { url: 'x' })).toBe(false) expect(isLineItemsField({ key: 'f', label: 'F', type: 'text', widget: 'upload' }, {})).toBe(false) expect(isLineItemsField({ key: 'when', label: 'When', type: 'date' }, new Date())).toBe(false) }) }) describe('fkSeedOption', () => { const field = { key: 'source_warehouse_id', label: 'Origen', type: 'text', ref: 'warehouses' } it('builds a seed option from the injected {value,label} sibling', () => { const seed = fkSeedOption(field, 'wh-1', { source_warehouse_id: 'wh-1', source_warehouse: { value: 'wh-1', label: 'Test2', image: 'logo.png' }, }) expect(seed).toEqual({ id: 'wh-1', value: 'wh-1', label: 'Test2', name: 'Test2', image: 'logo.png' }) }) it('returns null when there is no usable sibling', () => { expect(fkSeedOption(field, 'wh-1', { source_warehouse_id: 'wh-1' })).toBeNull() expect(fkSeedOption(field, '', {})).toBeNull() expect(fkSeedOption(field, null, { source_warehouse: { label: 'x' } })).toBeNull() }) }) describe('EditField — PART 1: jsonb line-items read-only', () => { it('renders the inline table (read-only), not an input or [object Object]', () => { const onChange = vi.fn() const { container } = render( , ) // Localized headers + resolved ref label from the inline CollectionCell. expect(screen.getByRole('columnheader', { name: 'Producto' })).toBeTruthy() expect(screen.getByRole('cell', { name: 'Test' })).toBeTruthy() expect(screen.getByRole('cell', { name: '10' })).toBeTruthy() // No editable input, no [object Object], no leaked uuid, read-only hint. expect(container.querySelector('input')).toBeNull() expect(container.querySelector('textarea')).toBeNull() expect(container.textContent).not.toContain('[object Object]') expect(container.textContent).not.toContain('550e8400-e29b') expect(screen.getByText('Solo lectura')).toBeTruthy() }) it('renders a bare jsonb object read-only (no schema) instead of [object Object]', () => { const { container } = render( , ) expect(container.querySelector('input')).toBeNull() expect(container.textContent).not.toContain('[object Object]') expect(screen.getByText('Precio:')).toBeTruthy() }) }) describe('EditField — PART 2: FK select shows the resolved label', () => { it('seeds the trigger with the sibling label, not the raw uuid', () => { render( , ) // The combobox trigger shows the resolved label. expect(screen.getByText('Test2')).toBeTruthy() // The raw uuid is not shown. expect(screen.queryByText('550e8400-e29b-41d4-a716-446655440000')).toBeNull() }) it('keeps the raw value as fallback when no sibling was injected', () => { render( , ) // No sibling → existing behaviour: the raw value is shown on the trigger. expect(screen.getByText('wh-xyz')).toBeTruthy() }) })