148 lines
4.2 KiB
TypeScript
148 lines
4.2 KiB
TypeScript
|
|
/**
|
||
|
|
* CustomFieldRenderer tests — renders custom fields based on field_type.
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { describe, it, expect, vi } from 'vitest';
|
||
|
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||
|
|
import { CustomFieldRenderer } from '@/components/contacts/CustomFieldRenderer';
|
||
|
|
import type { CustomFieldDefinition } from '@/api/customFields';
|
||
|
|
|
||
|
|
// Mock i18n
|
||
|
|
vi.mock('react-i18next', () => ({
|
||
|
|
useTranslation: () => ({ t: (key: string) => key }),
|
||
|
|
}));
|
||
|
|
|
||
|
|
// Mock UI components
|
||
|
|
vi.mock('@/components/ui/Input', () => ({
|
||
|
|
Input: ({ value, onChange, type, id, ...props }: any) => (
|
||
|
|
<input
|
||
|
|
data-testid={props['data-testid'] || `input-${id}`}
|
||
|
|
type={type || 'text'}
|
||
|
|
value={value || ''}
|
||
|
|
onChange={onChange}
|
||
|
|
/>
|
||
|
|
),
|
||
|
|
}));
|
||
|
|
|
||
|
|
vi.mock('@/components/ui/Select', () => ({
|
||
|
|
Select: ({ value, onChange, id, options, children, ...props }: any) => (
|
||
|
|
<select
|
||
|
|
data-testid={props['data-testid'] || `select-${id}`}
|
||
|
|
value={value || ''}
|
||
|
|
onChange={onChange}
|
||
|
|
>
|
||
|
|
{options ? options.map((o: any) => <option key={o.value} value={o.value}>{o.label}</option>) : children}
|
||
|
|
</select>
|
||
|
|
),
|
||
|
|
}));
|
||
|
|
|
||
|
|
const mockFields: CustomFieldDefinition[] = [
|
||
|
|
{
|
||
|
|
name: 'lead_source',
|
||
|
|
label: 'Lead Source',
|
||
|
|
label_key: 'custom.leadSource',
|
||
|
|
field_type: 'select',
|
||
|
|
options: ['website', 'referral', 'cold_call'],
|
||
|
|
default_value: null,
|
||
|
|
required: false,
|
||
|
|
entity: 'contact',
|
||
|
|
plugin: 'test_plugin',
|
||
|
|
value: 'website',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: 'score',
|
||
|
|
label: 'Score',
|
||
|
|
label_key: 'custom.score',
|
||
|
|
field_type: 'number',
|
||
|
|
options: [],
|
||
|
|
default_value: 0,
|
||
|
|
required: false,
|
||
|
|
entity: 'contact',
|
||
|
|
plugin: 'test_plugin',
|
||
|
|
value: 42,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: 'active',
|
||
|
|
label: 'Active',
|
||
|
|
label_key: 'custom.active',
|
||
|
|
field_type: 'boolean',
|
||
|
|
options: [],
|
||
|
|
default_value: false,
|
||
|
|
required: false,
|
||
|
|
entity: 'contact',
|
||
|
|
plugin: 'test_plugin',
|
||
|
|
value: true,
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
describe('CustomFieldRenderer', () => {
|
||
|
|
it('renders read mode with field values', () => {
|
||
|
|
render(<CustomFieldRenderer fields={mockFields} mode="read" />);
|
||
|
|
expect(screen.getByTestId('custom-fields-read')).toBeInTheDocument();
|
||
|
|
expect(screen.getByText('website')).toBeInTheDocument();
|
||
|
|
expect(screen.getByText('42')).toBeInTheDocument();
|
||
|
|
expect(screen.getByText('✓')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('renders edit mode with form controls', () => {
|
||
|
|
render(
|
||
|
|
<CustomFieldRenderer
|
||
|
|
fields={mockFields}
|
||
|
|
mode="edit"
|
||
|
|
values={{ lead_source: 'website', score: 42, active: true }}
|
||
|
|
onChange={vi.fn()}
|
||
|
|
/>
|
||
|
|
);
|
||
|
|
expect(screen.getByTestId('custom-fields-edit')).toBeInTheDocument();
|
||
|
|
expect(screen.getByTestId('select-cf-lead_source')).toBeInTheDocument();
|
||
|
|
expect(screen.getByTestId('input-cf-score')).toBeInTheDocument();
|
||
|
|
expect(document.getElementById('cf-active')).toBeInTheDocument();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('calls onChange when select value changes', () => {
|
||
|
|
const onChange = vi.fn();
|
||
|
|
render(
|
||
|
|
<CustomFieldRenderer
|
||
|
|
fields={[mockFields[0]]}
|
||
|
|
mode="edit"
|
||
|
|
values={{ lead_source: 'website' }}
|
||
|
|
onChange={onChange}
|
||
|
|
/>
|
||
|
|
);
|
||
|
|
const select = screen.getByTestId('select-cf-lead_source');
|
||
|
|
fireEvent.change(select, { target: { value: 'referral' } });
|
||
|
|
expect(onChange).toHaveBeenCalledWith('lead_source', 'referral');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('renders nothing when fields array is empty', () => {
|
||
|
|
const { container } = render(<CustomFieldRenderer fields={[]} mode="read" />);
|
||
|
|
expect(container.firstChild).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('renders multiselect field with checkboxes', () => {
|
||
|
|
const multiField: CustomFieldDefinition = {
|
||
|
|
name: 'tags',
|
||
|
|
label: 'Tags',
|
||
|
|
label_key: 'custom.tags',
|
||
|
|
field_type: 'multiselect',
|
||
|
|
options: ['vip', 'customer', 'lead'],
|
||
|
|
default_value: [],
|
||
|
|
required: false,
|
||
|
|
entity: 'contact',
|
||
|
|
plugin: 'test_plugin',
|
||
|
|
value: ['vip'],
|
||
|
|
};
|
||
|
|
render(
|
||
|
|
<CustomFieldRenderer
|
||
|
|
fields={[multiField]}
|
||
|
|
mode="edit"
|
||
|
|
values={{ tags: ['vip'] }}
|
||
|
|
onChange={vi.fn()}
|
||
|
|
/>
|
||
|
|
);
|
||
|
|
const checkboxes = screen.getAllByRole('checkbox');
|
||
|
|
expect(checkboxes).toHaveLength(3);
|
||
|
|
expect(checkboxes[0]).toBeChecked();
|
||
|
|
});
|
||
|
|
});
|