@object-ui/fields 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/fields/src/complex-widgets.test.d.ts +1 -0
- package/dist/fields/src/datetime-widgets.test.d.ts +1 -0
- package/dist/fields/src/fields.stories.d.ts +56 -0
- package/dist/fields/src/index.d.ts +48 -0
- package/dist/fields/src/new-widgets.test.d.ts +8 -0
- package/dist/fields/src/remaining-widgets.test.d.ts +1 -0
- package/dist/fields/src/standard-widgets.test.d.ts +0 -0
- package/dist/fields/src/text-rich-widgets.test.d.ts +1 -0
- package/dist/fields/src/visual-widgets.test.d.ts +1 -0
- package/dist/fields/src/widgets/AddressField.d.ts +16 -0
- package/dist/fields/src/widgets/AutoNumberField.d.ts +6 -0
- package/dist/fields/src/widgets/AvatarField.d.ts +6 -0
- package/dist/fields/src/widgets/CodeField.d.ts +7 -0
- package/dist/fields/src/widgets/ColorField.d.ts +6 -0
- package/dist/fields/src/widgets/CurrencyField.d.ts +1 -1
- package/dist/fields/src/widgets/DateTimeField.d.ts +2 -0
- package/dist/fields/src/widgets/FileField.d.ts +2 -0
- package/dist/fields/src/widgets/FormulaField.d.ts +6 -0
- package/dist/fields/src/widgets/GeolocationField.d.ts +14 -0
- package/dist/fields/src/widgets/GridField.d.ts +7 -0
- package/dist/fields/src/widgets/ImageField.d.ts +2 -0
- package/dist/fields/src/widgets/LocationField.d.ts +2 -0
- package/dist/fields/src/widgets/LookupField.d.ts +1 -1
- package/dist/fields/src/widgets/MasterDetailField.d.ts +14 -0
- package/dist/fields/src/widgets/ObjectField.d.ts +6 -0
- package/dist/fields/src/widgets/PasswordField.d.ts +2 -0
- package/dist/fields/src/widgets/PercentField.d.ts +2 -0
- package/dist/fields/src/widgets/QRCodeField.d.ts +6 -0
- package/dist/fields/src/widgets/RatingField.d.ts +6 -0
- package/dist/fields/src/widgets/SignatureField.d.ts +6 -0
- package/dist/fields/src/widgets/SliderField.d.ts +6 -0
- package/dist/fields/src/widgets/SummaryField.d.ts +6 -0
- package/dist/fields/src/widgets/TimeField.d.ts +2 -0
- package/dist/fields/src/widgets/UserField.d.ts +6 -0
- package/dist/fields/src/widgets/VectorField.d.ts +6 -0
- package/dist/fields/src/widgets/types.d.ts +2 -0
- package/dist/index.cjs +4 -2
- package/dist/index.js +2003 -618
- package/package.json +10 -8
- package/src/complex-widgets.test.tsx +146 -0
- package/src/datetime-widgets.test.tsx +93 -0
- package/src/fields.stories.tsx +566 -0
- package/src/index.tsx +303 -68
- package/src/new-widgets.test.tsx +302 -0
- package/src/remaining-widgets.test.tsx +172 -0
- package/src/standard-widgets.test.tsx +278 -0
- package/src/text-rich-widgets.test.tsx +104 -0
- package/src/visual-widgets.test.tsx +97 -0
- package/src/widgets/AddressField.tsx +112 -0
- package/src/widgets/AutoNumberField.tsx +16 -0
- package/src/widgets/AvatarField.tsx +110 -0
- package/src/widgets/BooleanField.tsx +19 -11
- package/src/widgets/CodeField.tsx +34 -0
- package/src/widgets/ColorField.tsx +44 -0
- package/src/widgets/CurrencyField.tsx +8 -7
- package/src/widgets/DateField.tsx +5 -2
- package/src/widgets/DateTimeField.tsx +28 -0
- package/src/widgets/EmailField.tsx +7 -3
- package/src/widgets/FileField.tsx +114 -0
- package/src/widgets/FormulaField.tsx +31 -0
- package/src/widgets/GeolocationField.tsx +151 -0
- package/src/widgets/GridField.tsx +63 -0
- package/src/widgets/ImageField.tsx +108 -0
- package/src/widgets/LocationField.tsx +46 -0
- package/src/widgets/LookupField.tsx +5 -5
- package/src/widgets/MasterDetailField.tsx +131 -0
- package/src/widgets/NumberField.tsx +8 -4
- package/src/widgets/ObjectField.tsx +85 -0
- package/src/widgets/PasswordField.tsx +45 -0
- package/src/widgets/PercentField.tsx +49 -0
- package/src/widgets/PhoneField.tsx +7 -3
- package/src/widgets/QRCodeField.tsx +96 -0
- package/src/widgets/RatingField.tsx +66 -0
- package/src/widgets/RichTextField.tsx +5 -5
- package/src/widgets/SelectField.tsx +7 -5
- package/src/widgets/SignatureField.tsx +140 -0
- package/src/widgets/SliderField.tsx +39 -0
- package/src/widgets/SummaryField.tsx +29 -0
- package/src/widgets/TextAreaField.tsx +9 -6
- package/src/widgets/TextField.tsx +13 -8
- package/src/widgets/TimeField.tsx +22 -0
- package/src/widgets/UrlField.tsx +7 -3
- package/src/widgets/UserField.tsx +88 -0
- package/src/widgets/VectorField.tsx +28 -0
- package/src/widgets/types.ts +2 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
12
|
+
import '@testing-library/jest-dom';
|
|
13
|
+
import { CurrencyField } from './widgets/CurrencyField';
|
|
14
|
+
import { NumberField } from './widgets/NumberField';
|
|
15
|
+
import { TextField } from './widgets/TextField';
|
|
16
|
+
import { SelectField } from './widgets/SelectField';
|
|
17
|
+
import { BooleanField } from './widgets/BooleanField';
|
|
18
|
+
import { DateField } from './widgets/DateField';
|
|
19
|
+
import { EmailField } from './widgets/EmailField';
|
|
20
|
+
import { UrlField } from './widgets/UrlField';
|
|
21
|
+
import { PhoneField } from './widgets/PhoneField';
|
|
22
|
+
import { TextAreaField } from './widgets/TextAreaField';
|
|
23
|
+
|
|
24
|
+
const mockOnChange = vi.fn();
|
|
25
|
+
|
|
26
|
+
describe('Standard Field Widgets', () => {
|
|
27
|
+
describe('CurrencyField', () => {
|
|
28
|
+
const fieldMock = { type: 'currency', currency: 'USD', precision: 2 };
|
|
29
|
+
|
|
30
|
+
it('should render value with currency symbol in input mode', () => {
|
|
31
|
+
render(
|
|
32
|
+
<CurrencyField
|
|
33
|
+
value={123.45}
|
|
34
|
+
onChange={mockOnChange}
|
|
35
|
+
field={fieldMock as any}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
const input = screen.getByRole('spinbutton'); // input type="number"
|
|
39
|
+
expect(input).toHaveValue(123.45);
|
|
40
|
+
expect(screen.getByText('$')).toBeInTheDocument();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should format currency in readonly mode', () => {
|
|
44
|
+
const { container } = render(
|
|
45
|
+
<CurrencyField
|
|
46
|
+
value={1234.56}
|
|
47
|
+
onChange={mockOnChange}
|
|
48
|
+
field={fieldMock as any}
|
|
49
|
+
readonly={true}
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
// Expected output format might depend on locale, matching generic USD format
|
|
53
|
+
expect(container.textContent).toMatch(/\$1,234.56/);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should handle EUR currency', () => {
|
|
57
|
+
render(
|
|
58
|
+
<CurrencyField
|
|
59
|
+
value={100}
|
|
60
|
+
onChange={mockOnChange}
|
|
61
|
+
field={{ ...fieldMock, currency: 'EUR' } as any}
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
expect(screen.getByText('EUR')).toBeInTheDocument();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('NumberField', () => {
|
|
69
|
+
const fieldMock = { type: 'number', precision: 2 };
|
|
70
|
+
|
|
71
|
+
it('should render numeric input', () => {
|
|
72
|
+
render(
|
|
73
|
+
<NumberField
|
|
74
|
+
value={42}
|
|
75
|
+
onChange={mockOnChange}
|
|
76
|
+
field={fieldMock as any}
|
|
77
|
+
/>
|
|
78
|
+
);
|
|
79
|
+
const input = screen.getByRole('spinbutton');
|
|
80
|
+
expect(input).toHaveValue(42);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should call onChange with number', () => {
|
|
84
|
+
render(
|
|
85
|
+
<NumberField
|
|
86
|
+
value={1}
|
|
87
|
+
onChange={mockOnChange}
|
|
88
|
+
field={fieldMock as any}
|
|
89
|
+
/>
|
|
90
|
+
);
|
|
91
|
+
const input = screen.getByRole('spinbutton');
|
|
92
|
+
fireEvent.change(input, { target: { value: '10' } });
|
|
93
|
+
expect(mockOnChange).toHaveBeenCalledWith(10);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should display value in readonly mode', () => {
|
|
97
|
+
const { container } = render(
|
|
98
|
+
<NumberField
|
|
99
|
+
value={99.9}
|
|
100
|
+
onChange={mockOnChange}
|
|
101
|
+
field={fieldMock as any}
|
|
102
|
+
readonly={true}
|
|
103
|
+
/>
|
|
104
|
+
);
|
|
105
|
+
expect(container.textContent).toBe('99.9');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('TextField', () => {
|
|
110
|
+
const fieldMock = { type: 'text' };
|
|
111
|
+
|
|
112
|
+
it('should render text input', () => {
|
|
113
|
+
render(
|
|
114
|
+
<TextField
|
|
115
|
+
value="Hello"
|
|
116
|
+
onChange={mockOnChange}
|
|
117
|
+
field={fieldMock as any}
|
|
118
|
+
/>
|
|
119
|
+
);
|
|
120
|
+
const input = screen.getByRole('textbox');
|
|
121
|
+
expect(input).toHaveValue('Hello');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should render as password when type is password', () => {
|
|
125
|
+
render(
|
|
126
|
+
<TextField
|
|
127
|
+
value="secret"
|
|
128
|
+
onChange={mockOnChange}
|
|
129
|
+
field={{ type: 'password' } as any}
|
|
130
|
+
/>
|
|
131
|
+
);
|
|
132
|
+
// getByRole('textbox') doesn't work for password type usually
|
|
133
|
+
const input = screen.getByDisplayValue('secret');
|
|
134
|
+
expect(input).toHaveAttribute('type', 'password');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should display value in readonly mode', () => {
|
|
138
|
+
const { container } = render(
|
|
139
|
+
<TextField
|
|
140
|
+
value="ReadOnly"
|
|
141
|
+
onChange={mockOnChange}
|
|
142
|
+
field={fieldMock as any}
|
|
143
|
+
readonly={true}
|
|
144
|
+
/>
|
|
145
|
+
);
|
|
146
|
+
expect(container.textContent).toBe('ReadOnly');
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('SelectField', () => {
|
|
151
|
+
const fieldMock = {
|
|
152
|
+
type: 'select',
|
|
153
|
+
options: [
|
|
154
|
+
{ label: 'Option A', value: 'a' },
|
|
155
|
+
{ label: 'Option B', value: 'b' }
|
|
156
|
+
]
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
it('should render select trigger (Combobox/Select)', () => {
|
|
160
|
+
render(
|
|
161
|
+
<SelectField
|
|
162
|
+
value={''}
|
|
163
|
+
onChange={mockOnChange}
|
|
164
|
+
field={fieldMock as any}
|
|
165
|
+
/>
|
|
166
|
+
);
|
|
167
|
+
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should display label in readonly mode', () => {
|
|
171
|
+
const { container } = render(
|
|
172
|
+
<SelectField
|
|
173
|
+
value="a"
|
|
174
|
+
onChange={mockOnChange}
|
|
175
|
+
field={fieldMock as any}
|
|
176
|
+
readonly={true}
|
|
177
|
+
/>
|
|
178
|
+
);
|
|
179
|
+
expect(container.textContent).toBe('Option A');
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('BooleanField', () => {
|
|
184
|
+
const fieldMock = { type: 'boolean' };
|
|
185
|
+
|
|
186
|
+
it('should render checkbox/switch', () => {
|
|
187
|
+
render(
|
|
188
|
+
<BooleanField
|
|
189
|
+
value={true}
|
|
190
|
+
onChange={mockOnChange}
|
|
191
|
+
field={fieldMock as any}
|
|
192
|
+
/>
|
|
193
|
+
);
|
|
194
|
+
// Ensure we can find the switch role (Shadcn switch uses role="switch")
|
|
195
|
+
expect(screen.getByRole('switch')).toBeChecked();
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('should toggle value', () => {
|
|
199
|
+
render(
|
|
200
|
+
<BooleanField
|
|
201
|
+
value={false}
|
|
202
|
+
onChange={mockOnChange}
|
|
203
|
+
field={fieldMock as any}
|
|
204
|
+
/>
|
|
205
|
+
);
|
|
206
|
+
const toggle = screen.getByRole('switch');
|
|
207
|
+
fireEvent.click(toggle);
|
|
208
|
+
expect(mockOnChange).toHaveBeenCalledWith(true);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should display Yes/No in readonly mode', () => {
|
|
212
|
+
const { container } = render(
|
|
213
|
+
<BooleanField
|
|
214
|
+
value={true}
|
|
215
|
+
onChange={mockOnChange}
|
|
216
|
+
field={fieldMock as any}
|
|
217
|
+
readonly={true}
|
|
218
|
+
/>
|
|
219
|
+
);
|
|
220
|
+
expect(container.textContent).toBe('Yes');
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe('EmailField', () => {
|
|
225
|
+
it('should render email input', () => {
|
|
226
|
+
render(
|
|
227
|
+
<EmailField
|
|
228
|
+
value="test@example.com"
|
|
229
|
+
onChange={mockOnChange}
|
|
230
|
+
field={{ type: 'email' } as any}
|
|
231
|
+
/>
|
|
232
|
+
);
|
|
233
|
+
const input = screen.getByDisplayValue('test@example.com');
|
|
234
|
+
expect(input).toHaveAttribute('type', 'email');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should render as link in readonly mode', () => {
|
|
238
|
+
render(
|
|
239
|
+
<EmailField
|
|
240
|
+
value="test@example.com"
|
|
241
|
+
onChange={mockOnChange}
|
|
242
|
+
field={{ type: 'email' } as any}
|
|
243
|
+
readonly={true}
|
|
244
|
+
/>
|
|
245
|
+
);
|
|
246
|
+
const link = screen.getByRole('link');
|
|
247
|
+
expect(link).toHaveAttribute('href', 'mailto:test@example.com');
|
|
248
|
+
expect(link).toHaveTextContent('test@example.com');
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe('UrlField', () => {
|
|
253
|
+
it('should render url input', () => {
|
|
254
|
+
render(
|
|
255
|
+
<UrlField
|
|
256
|
+
value="https://example.com"
|
|
257
|
+
onChange={mockOnChange}
|
|
258
|
+
field={{ type: 'url' } as any}
|
|
259
|
+
/>
|
|
260
|
+
);
|
|
261
|
+
const input = screen.getByDisplayValue('https://example.com');
|
|
262
|
+
expect(input).toHaveAttribute('type', 'url');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('should render clickable link in readonly mode', () => {
|
|
266
|
+
render(
|
|
267
|
+
<UrlField
|
|
268
|
+
value="https://example.com"
|
|
269
|
+
onChange={mockOnChange}
|
|
270
|
+
field={{ type: 'url' } as any}
|
|
271
|
+
readonly={true}
|
|
272
|
+
/>
|
|
273
|
+
);
|
|
274
|
+
const link = screen.getByRole('link');
|
|
275
|
+
expect(link).toHaveAttribute('href', 'https://example.com');
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
2
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
3
|
+
import { TextAreaField } from './widgets/TextAreaField';
|
|
4
|
+
import { RichTextField } from './widgets/RichTextField';
|
|
5
|
+
import { CodeField } from './widgets/CodeField';
|
|
6
|
+
import { PasswordField } from './widgets/PasswordField';
|
|
7
|
+
import type { FieldWidgetProps } from './widgets/types';
|
|
8
|
+
|
|
9
|
+
const mockField = {
|
|
10
|
+
name: 'test_field',
|
|
11
|
+
label: 'Test Field',
|
|
12
|
+
} as any;
|
|
13
|
+
|
|
14
|
+
const baseProps: FieldWidgetProps<string> = {
|
|
15
|
+
field: mockField,
|
|
16
|
+
value: '',
|
|
17
|
+
onChange: vi.fn(),
|
|
18
|
+
readonly: false,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
describe('Text & Rich Content Widgets', () => {
|
|
22
|
+
describe('TextAreaField', () => {
|
|
23
|
+
it('renders textarea in edit mode', () => {
|
|
24
|
+
render(<TextAreaField {...baseProps} />);
|
|
25
|
+
expect(screen.getByRole('textbox')).toBeInTheDocument();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('handles max length indicator', () => {
|
|
29
|
+
const propsWithMax = {
|
|
30
|
+
...baseProps,
|
|
31
|
+
field: { ...mockField, max_length: 100 }
|
|
32
|
+
};
|
|
33
|
+
render(<TextAreaField {...propsWithMax} value="testing" />);
|
|
34
|
+
expect(screen.getByLabelText('Character count: 7 of 100')).toBeInTheDocument();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('renders whitespace-pre-wrap in readonly mode', () => {
|
|
38
|
+
const text = 'Line 1\nLine 2';
|
|
39
|
+
render(<TextAreaField {...baseProps} readonly value={text} />);
|
|
40
|
+
// Check existence logic
|
|
41
|
+
const display = screen.getByText((content) =>
|
|
42
|
+
content.includes('Line 1') && content.includes('Line 2')
|
|
43
|
+
);
|
|
44
|
+
expect(display).toHaveClass('whitespace-pre-wrap');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('RichTextField', () => {
|
|
49
|
+
// Current implementation is a simpler textarea wrapper
|
|
50
|
+
it('renders textarea with format indicator', () => {
|
|
51
|
+
render(<RichTextField {...baseProps} />);
|
|
52
|
+
expect(screen.getByRole('textbox')).toBeInTheDocument();
|
|
53
|
+
expect(screen.getByText((content) => content.includes('Format: markdown'))).toBeInTheDocument();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('renders prose in readonly mode', () => {
|
|
57
|
+
render(<RichTextField {...baseProps} readonly value="# Heading" />);
|
|
58
|
+
const display = screen.getByText('# Heading');
|
|
59
|
+
expect(display).toHaveClass('prose');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('CodeField', () => {
|
|
64
|
+
it('renders textarea with monospace font', () => {
|
|
65
|
+
render(<CodeField {...baseProps} />);
|
|
66
|
+
const input = screen.getByRole('textbox');
|
|
67
|
+
expect(input).toHaveClass('font-mono');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('renders code block in readonly mode', () => {
|
|
71
|
+
render(<CodeField {...baseProps} readonly value="const x = 1;" />);
|
|
72
|
+
// Check for pre > code structure
|
|
73
|
+
const pre = document.querySelector('pre');
|
|
74
|
+
expect(pre).toBeInTheDocument();
|
|
75
|
+
expect(pre).toHaveTextContent('const x = 1;');
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('PasswordField', () => {
|
|
80
|
+
it('renders password input by default', () => {
|
|
81
|
+
const { container } = render(<PasswordField {...baseProps} />);
|
|
82
|
+
const input = container.querySelector('input[type="password"]');
|
|
83
|
+
expect(input).toBeInTheDocument();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('toggles visibility', () => {
|
|
87
|
+
const { container } = render(<PasswordField {...baseProps} />);
|
|
88
|
+
const button = screen.getByRole('button'); // The eye icon button
|
|
89
|
+
const input = container.querySelector('input') as HTMLInputElement;
|
|
90
|
+
|
|
91
|
+
expect(input.type).toBe('password');
|
|
92
|
+
fireEvent.click(button);
|
|
93
|
+
expect(input.type).toBe('text');
|
|
94
|
+
fireEvent.click(button);
|
|
95
|
+
expect(input.type).toBe('password');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('renders masked bullets in readonly mode', () => {
|
|
99
|
+
render(<PasswordField {...baseProps} readonly value="secret" />);
|
|
100
|
+
expect(screen.getByText('••••••••')).toBeInTheDocument();
|
|
101
|
+
expect(screen.queryByText('secret')).not.toBeInTheDocument();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
2
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
3
|
+
import { ColorField } from './widgets/ColorField';
|
|
4
|
+
import { RatingField } from './widgets/RatingField';
|
|
5
|
+
import { SliderField } from './widgets/SliderField';
|
|
6
|
+
import { QRCodeField } from './widgets/QRCodeField'; // Intentionally simpler than Image/Avatar
|
|
7
|
+
import type { FieldWidgetProps } from './widgets/types';
|
|
8
|
+
|
|
9
|
+
const mockField = {
|
|
10
|
+
name: 'test_field',
|
|
11
|
+
label: 'Test Field',
|
|
12
|
+
} as any;
|
|
13
|
+
|
|
14
|
+
const baseProps: FieldWidgetProps<any> = {
|
|
15
|
+
field: mockField,
|
|
16
|
+
value: undefined,
|
|
17
|
+
onChange: vi.fn(),
|
|
18
|
+
readonly: false,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
describe('Visual & Display Widgets', () => {
|
|
22
|
+
describe('ColorField', () => {
|
|
23
|
+
it('renders color picker + text input in edit mode', () => {
|
|
24
|
+
const { container } = render(<ColorField {...baseProps} value="#00ff00" />);
|
|
25
|
+
const colorInput = container.querySelector('input[type="color"]');
|
|
26
|
+
const textInput = screen.getByRole('textbox');
|
|
27
|
+
|
|
28
|
+
expect(colorInput).toBeInTheDocument();
|
|
29
|
+
expect(textInput).toBeInTheDocument();
|
|
30
|
+
expect(textInput).toHaveValue('#00ff00');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('syncs change from color picker', () => {
|
|
34
|
+
const { container } = render(<ColorField {...baseProps} />);
|
|
35
|
+
const colorInput = container.querySelector('input[type="color"]') as HTMLInputElement;
|
|
36
|
+
fireEvent.change(colorInput, { target: { value: '#ff0000' } });
|
|
37
|
+
expect(baseProps.onChange).toHaveBeenCalledWith('#ff0000');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('shows preview box in readonly mode', () => {
|
|
41
|
+
const { container } = render(<ColorField {...baseProps} readonly value="#0000ff" />);
|
|
42
|
+
// Check for the style attribute on the div
|
|
43
|
+
// We search for a div that has the style background-color
|
|
44
|
+
// This is a bit fragile but standard for inline styles
|
|
45
|
+
const preview = container.querySelector('div[style="background-color: #0000ff;"]');
|
|
46
|
+
// Or looser check:
|
|
47
|
+
const divs = container.querySelectorAll('div');
|
|
48
|
+
const found = Array.from(divs).some(d => d.style.backgroundColor === 'rgb(0, 0, 255)' || d.style.backgroundColor === '#0000ff');
|
|
49
|
+
expect(found).toBe(true);
|
|
50
|
+
expect(screen.getByText('#0000ff')).toBeInTheDocument();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('RatingField', () => {
|
|
55
|
+
it('renders stars', () => {
|
|
56
|
+
render(<RatingField {...baseProps} value={3} />);
|
|
57
|
+
// Should see 5 buttons (default max)
|
|
58
|
+
const buttons = screen.getAllByRole('button');
|
|
59
|
+
expect(buttons).toHaveLength(5);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('highlights correct number of stars in readonly', () => {
|
|
63
|
+
const { container } = render(<RatingField {...baseProps} readonly value={3} />);
|
|
64
|
+
// Should have 3 filled stars, 2 empty.
|
|
65
|
+
// Logic: fill-yellow-400
|
|
66
|
+
const filled = container.querySelectorAll('.fill-yellow-400');
|
|
67
|
+
expect(filled).toHaveLength(3);
|
|
68
|
+
expect(screen.getByText('3 / 5')).toBeInTheDocument();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('calls onChange when clicking star', () => {
|
|
72
|
+
render(<RatingField {...baseProps} />);
|
|
73
|
+
const buttons = screen.getAllByRole('button');
|
|
74
|
+
fireEvent.click(buttons[3]); // 4th star (index 3) -> value 4
|
|
75
|
+
expect(baseProps.onChange).toHaveBeenCalledWith(4);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('SliderField', () => {
|
|
80
|
+
it('renders slider', () => {
|
|
81
|
+
const { container } = render(<SliderField {...baseProps} value={50} />);
|
|
82
|
+
// Radix slider usually has role="slider"
|
|
83
|
+
const slider = screen.getByRole('slider');
|
|
84
|
+
expect(slider).toBeInTheDocument();
|
|
85
|
+
// Check text display
|
|
86
|
+
expect(screen.getByText('50')).toBeInTheDocument();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('renders readonly view', () => {
|
|
90
|
+
render(<SliderField {...baseProps} readonly value={75} />);
|
|
91
|
+
expect(screen.getByText('75')).toBeInTheDocument();
|
|
92
|
+
expect(screen.getByText('/ 100')).toBeInTheDocument();
|
|
93
|
+
expect(screen.queryByRole('slider')).not.toBeInTheDocument();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Input, Label } from '@object-ui/components';
|
|
3
|
+
import { FieldWidgetProps } from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Address data structure
|
|
7
|
+
*/
|
|
8
|
+
export interface AddressValue {
|
|
9
|
+
street?: string;
|
|
10
|
+
city?: string;
|
|
11
|
+
state?: string;
|
|
12
|
+
zipCode?: string;
|
|
13
|
+
country?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Address field widget - provides a structured address input
|
|
18
|
+
* Supports street, city, state, zip code, and country
|
|
19
|
+
*/
|
|
20
|
+
export function AddressField({ value, onChange, field, readonly, ...props }: FieldWidgetProps<AddressValue>) {
|
|
21
|
+
const address = value || {};
|
|
22
|
+
|
|
23
|
+
const handleFieldChange = (fieldName: keyof AddressValue, fieldValue: string) => {
|
|
24
|
+
onChange({
|
|
25
|
+
...address,
|
|
26
|
+
[fieldName]: fieldValue,
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const formatAddress = (addr: AddressValue): string => {
|
|
31
|
+
const parts = [
|
|
32
|
+
addr.street,
|
|
33
|
+
addr.city,
|
|
34
|
+
[addr.state, addr.zipCode].filter(Boolean).join(' '),
|
|
35
|
+
addr.country,
|
|
36
|
+
].filter(Boolean);
|
|
37
|
+
return parts.join(', ') || '-';
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
if (readonly) {
|
|
41
|
+
return <span className="text-sm">{formatAddress(address)}</span>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div className="space-y-3">
|
|
46
|
+
<div>
|
|
47
|
+
<Label htmlFor="street" className="text-xs">Street Address</Label>
|
|
48
|
+
<Input
|
|
49
|
+
id="street"
|
|
50
|
+
type="text"
|
|
51
|
+
value={address.street || ''}
|
|
52
|
+
onChange={(e) => handleFieldChange('street', e.target.value)}
|
|
53
|
+
placeholder="123 Main St"
|
|
54
|
+
disabled={readonly || props.disabled}
|
|
55
|
+
className={props.className}
|
|
56
|
+
/>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div className="grid grid-cols-2 gap-3">
|
|
60
|
+
<div>
|
|
61
|
+
<Label htmlFor="city" className="text-xs">City</Label>
|
|
62
|
+
<Input
|
|
63
|
+
id="city"
|
|
64
|
+
type="text"
|
|
65
|
+
value={address.city || ''}
|
|
66
|
+
onChange={(e) => handleFieldChange('city', e.target.value)}
|
|
67
|
+
placeholder="San Francisco"
|
|
68
|
+
disabled={readonly || props.disabled}
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div>
|
|
73
|
+
<Label htmlFor="state" className="text-xs">State / Province</Label>
|
|
74
|
+
<Input
|
|
75
|
+
id="state"
|
|
76
|
+
type="text"
|
|
77
|
+
value={address.state || ''}
|
|
78
|
+
onChange={(e) => handleFieldChange('state', e.target.value)}
|
|
79
|
+
placeholder="CA"
|
|
80
|
+
disabled={readonly || props.disabled}
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div className="grid grid-cols-2 gap-3">
|
|
86
|
+
<div>
|
|
87
|
+
<Label htmlFor="zipCode" className="text-xs">ZIP / Postal Code</Label>
|
|
88
|
+
<Input
|
|
89
|
+
id="zipCode"
|
|
90
|
+
type="text"
|
|
91
|
+
value={address.zipCode || ''}
|
|
92
|
+
onChange={(e) => handleFieldChange('zipCode', e.target.value)}
|
|
93
|
+
placeholder="94102"
|
|
94
|
+
disabled={readonly || props.disabled}
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div>
|
|
99
|
+
<Label htmlFor="country" className="text-xs">Country</Label>
|
|
100
|
+
<Input
|
|
101
|
+
id="country"
|
|
102
|
+
type="text"
|
|
103
|
+
value={address.country || ''}
|
|
104
|
+
onChange={(e) => handleFieldChange('country', e.target.value)}
|
|
105
|
+
placeholder="United States"
|
|
106
|
+
disabled={readonly || props.disabled}
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { FieldWidgetProps } from './types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* AutoNumberField - Read-only auto-generated field
|
|
6
|
+
* Values are automatically generated by the system and cannot be edited
|
|
7
|
+
*/
|
|
8
|
+
export function AutoNumberField({ value, field, ...props }: FieldWidgetProps<string | number>) {
|
|
9
|
+
const displayValue = value != null ? String(value) : '-';
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<span className={`text-sm font-mono text-gray-700 ${props.className || ''}`}>
|
|
13
|
+
{displayValue}
|
|
14
|
+
</span>
|
|
15
|
+
);
|
|
16
|
+
}
|