@object-ui/fields 4.0.1 → 4.0.3

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.
Files changed (26) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/package.json +7 -7
  3. package/dist/packages/fields/src/__tests__/boolean-checkbox.test.d.ts +0 -0
  4. package/dist/packages/fields/src/__tests__/cell-renderers.test.d.ts +0 -0
  5. package/dist/packages/fields/src/__tests__/datetime-cell.test.d.ts +0 -0
  6. package/dist/packages/fields/src/__tests__/datetime-timezone.test.d.ts +0 -0
  7. package/dist/packages/fields/src/__tests__/extreme-inputs.test.d.ts +0 -0
  8. package/dist/packages/fields/src/__tests__/form-variants.test.d.ts +0 -0
  9. package/dist/packages/fields/src/lookup-depends-on.test.d.ts +0 -1
  10. package/dist/packages/fields/src/new-widgets.test.d.ts +0 -8
  11. package/dist/packages/fields/src/record-picker.test.d.ts +0 -1
  12. package/dist/packages/fields/src/remaining-widgets.test.d.ts +0 -1
  13. package/dist/packages/fields/src/text-rich-widgets.test.d.ts +0 -1
  14. package/dist/packages/fields/src/visual-widgets.test.d.ts +0 -1
  15. package/src/__tests__/boolean-checkbox.test.tsx +0 -74
  16. package/src/__tests__/cell-renderers.test.tsx +0 -1193
  17. package/src/__tests__/datetime-cell.test.tsx +0 -46
  18. package/src/__tests__/datetime-timezone.test.tsx +0 -770
  19. package/src/__tests__/extreme-inputs.test.tsx +0 -567
  20. package/src/__tests__/form-variants.test.tsx +0 -306
  21. package/src/lookup-depends-on.test.tsx +0 -130
  22. package/src/new-widgets.test.tsx +0 -302
  23. package/src/record-picker.test.tsx +0 -1703
  24. package/src/remaining-widgets.test.tsx +0 -211
  25. package/src/text-rich-widgets.test.tsx +0 -104
  26. package/src/visual-widgets.test.tsx +0 -97
@@ -1,306 +0,0 @@
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
- /**
10
- * P3.2 Field Widget Polish - Form Variants
11
- *
12
- * Tests that field widgets work correctly in different form contexts:
13
- * standalone, multiple fields together, value changes, and controlled mode.
14
- */
15
-
16
- import { describe, it, expect, vi } from 'vitest';
17
- import React, { useState } from 'react';
18
- import { render, screen, fireEvent } from '@testing-library/react';
19
- import '@testing-library/jest-dom';
20
-
21
- import { TextField } from '../widgets/TextField';
22
- import { NumberField } from '../widgets/NumberField';
23
- import { EmailField } from '../widgets/EmailField';
24
- import { BooleanField } from '../widgets/BooleanField';
25
- import { TextAreaField } from '../widgets/TextAreaField';
26
- import { CurrencyField } from '../widgets/CurrencyField';
27
- import { UrlField } from '../widgets/UrlField';
28
-
29
- const noop = vi.fn();
30
-
31
- describe('P3.2 Form Variants', () => {
32
- // ---------------------------------------------------------------
33
- // Multiple fields rendered together
34
- // ---------------------------------------------------------------
35
- describe('multiple fields in a form', () => {
36
- it('renders multiple different field types side by side', () => {
37
- const { container } = render(
38
- <div>
39
- <TextField value="John" onChange={noop} field={{ type: 'text' } as any} />
40
- <EmailField value="john@test.com" onChange={noop} field={{ type: 'email' } as any} />
41
- <NumberField value={42} onChange={noop} field={{ type: 'number' } as any} />
42
- <BooleanField value={true} onChange={noop} field={{ type: 'boolean' } as any} />
43
- </div>
44
- );
45
- expect(screen.getAllByRole('textbox')).toHaveLength(2); // text + email
46
- expect(screen.getByRole('spinbutton')).toHaveValue(42);
47
- expect(screen.getByRole('switch')).toBeChecked();
48
- });
49
-
50
- it('each field maintains independent state via onChange', () => {
51
- const textChange = vi.fn();
52
- const numberChange = vi.fn();
53
-
54
- render(
55
- <div>
56
- <TextField value="" onChange={textChange} field={{ type: 'text' } as any} />
57
- <NumberField value={0} onChange={numberChange} field={{ type: 'number' } as any} />
58
- </div>
59
- );
60
-
61
- const inputs = screen.getAllByRole('textbox');
62
- fireEvent.change(inputs[0], { target: { value: 'hello' } });
63
- expect(textChange).toHaveBeenCalledWith('hello');
64
- expect(numberChange).not.toHaveBeenCalled();
65
- });
66
- });
67
-
68
- // ---------------------------------------------------------------
69
- // Controlled component behavior
70
- // ---------------------------------------------------------------
71
- describe('controlled component behavior', () => {
72
- function ControlledTextField() {
73
- const [val, setVal] = useState('initial');
74
- return (
75
- <div>
76
- <TextField value={val} onChange={setVal} field={{ type: 'text' } as any} />
77
- <span data-testid="output">{val}</span>
78
- </div>
79
- );
80
- }
81
-
82
- it('TextField works as controlled component', () => {
83
- render(<ControlledTextField />);
84
- const input = screen.getByRole('textbox');
85
- expect(input).toHaveValue('initial');
86
-
87
- fireEvent.change(input, { target: { value: 'updated' } });
88
- expect(screen.getByTestId('output').textContent).toBe('updated');
89
- });
90
-
91
- function ControlledNumberField() {
92
- const [val, setVal] = useState(10);
93
- return (
94
- <div>
95
- <NumberField value={val} onChange={setVal} field={{ type: 'number' } as any} />
96
- <span data-testid="output">{String(val)}</span>
97
- </div>
98
- );
99
- }
100
-
101
- it('NumberField works as controlled component', () => {
102
- render(<ControlledNumberField />);
103
- const input = screen.getByRole('spinbutton');
104
- expect(input).toHaveValue(10);
105
-
106
- fireEvent.change(input, { target: { value: '25' } });
107
- expect(screen.getByTestId('output').textContent).toBe('25');
108
- });
109
-
110
- function ControlledBooleanField() {
111
- const [val, setVal] = useState(false);
112
- return (
113
- <div>
114
- <BooleanField value={val} onChange={setVal} field={{ type: 'boolean' } as any} />
115
- <span data-testid="output">{String(val)}</span>
116
- </div>
117
- );
118
- }
119
-
120
- it('BooleanField works as controlled component', () => {
121
- render(<ControlledBooleanField />);
122
- const toggle = screen.getByRole('switch');
123
- expect(toggle).not.toBeChecked();
124
-
125
- fireEvent.click(toggle);
126
- expect(screen.getByTestId('output').textContent).toBe('true');
127
- });
128
- });
129
-
130
- // ---------------------------------------------------------------
131
- // Switching between readonly and edit mode
132
- // ---------------------------------------------------------------
133
- describe('readonly/edit mode switching', () => {
134
- function ModeToggle() {
135
- const [ro, setRo] = useState(false);
136
- return (
137
- <div>
138
- <button data-testid="toggle" onClick={() => setRo(!ro)}>Toggle</button>
139
- <TextField value="hello" onChange={noop} field={{ type: 'text' } as any} readonly={ro} />
140
- </div>
141
- );
142
- }
143
-
144
- it('TextField switches from edit to readonly', () => {
145
- render(<ModeToggle />);
146
- // Start in edit mode
147
- expect(screen.getByRole('textbox')).toBeInTheDocument();
148
-
149
- // Toggle to readonly
150
- fireEvent.click(screen.getByTestId('toggle'));
151
- expect(screen.queryByRole('textbox')).toBeNull();
152
- expect(screen.getByText('hello')).toBeInTheDocument();
153
-
154
- // Toggle back to edit
155
- fireEvent.click(screen.getByTestId('toggle'));
156
- expect(screen.getByRole('textbox')).toBeInTheDocument();
157
- });
158
- });
159
-
160
- // ---------------------------------------------------------------
161
- // TextField with rows (textarea variant)
162
- // ---------------------------------------------------------------
163
- describe('TextField textarea variant', () => {
164
- it('renders as textarea when rows > 1', () => {
165
- render(
166
- <TextField
167
- value="multi-line"
168
- onChange={noop}
169
- field={{ type: 'text', rows: 5 } as any}
170
- />
171
- );
172
- const el = screen.getByRole('textbox');
173
- expect(el.tagName).toBe('TEXTAREA');
174
- });
175
-
176
- it('renders as input when rows is not set', () => {
177
- render(
178
- <TextField
179
- value="single-line"
180
- onChange={noop}
181
- field={{ type: 'text' } as any}
182
- />
183
- );
184
- const el = screen.getByRole('textbox');
185
- expect(el.tagName).toBe('INPUT');
186
- });
187
- });
188
-
189
- // ---------------------------------------------------------------
190
- // TextAreaField with custom rows
191
- // ---------------------------------------------------------------
192
- describe('TextAreaField with custom rows', () => {
193
- it('uses custom rows from field metadata', () => {
194
- render(
195
- <TextAreaField
196
- value=""
197
- onChange={noop}
198
- field={{ type: 'textarea', rows: 10 } as any}
199
- />
200
- );
201
- const textarea = screen.getByRole('textbox');
202
- expect(textarea).toHaveAttribute('rows', '10');
203
- });
204
-
205
- it('defaults to 4 rows', () => {
206
- render(
207
- <TextAreaField
208
- value=""
209
- onChange={noop}
210
- field={{ type: 'textarea' } as any}
211
- />
212
- );
213
- const textarea = screen.getByRole('textbox');
214
- expect(textarea).toHaveAttribute('rows', '4');
215
- });
216
- });
217
-
218
- // ---------------------------------------------------------------
219
- // CurrencyField interaction
220
- // ---------------------------------------------------------------
221
- describe('CurrencyField interaction', () => {
222
- it('calls onChange with parsed number on input', () => {
223
- const onChange = vi.fn();
224
- render(
225
- <CurrencyField
226
- value={100}
227
- onChange={onChange}
228
- field={{ type: 'currency', currency: 'USD' } as any}
229
- />
230
- );
231
- fireEvent.change(screen.getByRole('spinbutton'), { target: { value: '250.50' } });
232
- expect(onChange).toHaveBeenCalledWith(250.50);
233
- });
234
-
235
- it('calls onChange with null on empty input', () => {
236
- const onChange = vi.fn();
237
- render(
238
- <CurrencyField
239
- value={100}
240
- onChange={onChange}
241
- field={{ type: 'currency', currency: 'USD' } as any}
242
- />
243
- );
244
- fireEvent.change(screen.getByRole('spinbutton'), { target: { value: '' } });
245
- expect(onChange).toHaveBeenCalledWith(null);
246
- });
247
-
248
- it('readonly mode formats currency value', () => {
249
- const { container } = render(
250
- <CurrencyField
251
- value={1234.56}
252
- onChange={noop}
253
- field={{ type: 'currency', currency: 'USD' } as any}
254
- readonly={true}
255
- />
256
- );
257
- expect(container.textContent).toMatch(/\$1,234\.56/);
258
- });
259
- });
260
-
261
- // ---------------------------------------------------------------
262
- // Email and URL readonly links
263
- // ---------------------------------------------------------------
264
- describe('readonly link generation', () => {
265
- it('EmailField generates mailto link in readonly', () => {
266
- render(
267
- <EmailField
268
- value="user@example.com"
269
- onChange={noop}
270
- field={{ type: 'email' } as any}
271
- readonly={true}
272
- />
273
- );
274
- const link = screen.getByRole('link');
275
- expect(link).toHaveAttribute('href', 'mailto:user@example.com');
276
- });
277
-
278
- it('UrlField generates external link in readonly', () => {
279
- render(
280
- <UrlField
281
- value="https://example.com"
282
- onChange={noop}
283
- field={{ type: 'url' } as any}
284
- readonly={true}
285
- />
286
- );
287
- const link = screen.getByRole('link');
288
- expect(link).toHaveAttribute('href', 'https://example.com');
289
- expect(link).toHaveAttribute('target', '_blank');
290
- expect(link).toHaveAttribute('rel', 'noopener noreferrer');
291
- });
292
-
293
- it('UrlField does not generate link for non-http URLs', () => {
294
- const { container } = render(
295
- <UrlField
296
- value="javascript:alert(1)"
297
- onChange={noop}
298
- field={{ type: 'url' } as any}
299
- readonly={true}
300
- />
301
- );
302
- expect(screen.queryByRole('link')).toBeNull();
303
- expect(container.textContent).toBe('javascript:alert(1)');
304
- });
305
- });
306
- });
@@ -1,130 +0,0 @@
1
- /**
2
- * Tests for the dependent-lookups feature: `field.depends_on` should add
3
- * an `$filter` chain when querying, gate the trigger when dependencies are
4
- * empty, and re-fetch when the dependency value changes.
5
- */
6
- import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
7
- import { describe, it, expect, vi, beforeEach } from 'vitest';
8
- import React, { useState } from 'react';
9
- import { LookupField } from './widgets/LookupField';
10
-
11
- const mockDataSource = {
12
- find: vi.fn(),
13
- findOne: vi.fn(),
14
- create: vi.fn(),
15
- update: vi.fn(),
16
- delete: vi.fn(),
17
- };
18
-
19
- beforeEach(() => {
20
- vi.clearAllMocks();
21
- mockDataSource.find.mockResolvedValue({ data: [], total: 0 });
22
- });
23
-
24
- describe('LookupField — depends_on', () => {
25
- const baseField: any = {
26
- name: 'city',
27
- type: 'lookup',
28
- reference: 'cities',
29
- display_field: 'name',
30
- id_field: 'id',
31
- depends_on: ['country'],
32
- };
33
-
34
- it('disables the trigger when a dependency value is missing', () => {
35
- render(
36
- <LookupField
37
- field={baseField}
38
- value={null}
39
- onChange={vi.fn()}
40
- readonly={false}
41
- dataSource={mockDataSource as any}
42
- dependentValues={{}}
43
- />,
44
- );
45
- const trigger = screen.getByTestId('lookup-trigger-gated');
46
- expect(trigger).toBeDisabled();
47
- expect(trigger).toHaveTextContent(/Select country first/i);
48
- });
49
-
50
- it('enables the trigger and includes a filter chain once the dependency is set', async () => {
51
- render(
52
- <LookupField
53
- field={baseField}
54
- value={null}
55
- onChange={vi.fn()}
56
- readonly={false}
57
- dataSource={mockDataSource as any}
58
- dependentValues={{ country: 'US' }}
59
- />,
60
- );
61
- // Trigger is enabled (no gated test id).
62
- expect(screen.queryByTestId('lookup-trigger-gated')).not.toBeInTheDocument();
63
-
64
- // Open the popover to fire fetch.
65
- await act(async () => {
66
- fireEvent.click(screen.getByRole('button', { name: /select|search/i }));
67
- });
68
-
69
- await waitFor(() => expect(mockDataSource.find).toHaveBeenCalled());
70
- const [, params] = mockDataSource.find.mock.calls[0];
71
- expect(params.$filter).toEqual({ country: 'US' });
72
- });
73
-
74
- it('supports the explicit { field, param } shape', async () => {
75
- const explicitField: any = {
76
- ...baseField,
77
- depends_on: [{ field: 'country', param: 'country_id' }],
78
- };
79
- render(
80
- <LookupField
81
- field={explicitField}
82
- value={null}
83
- onChange={vi.fn()}
84
- readonly={false}
85
- dataSource={mockDataSource as any}
86
- dependentValues={{ country: 42 }}
87
- />,
88
- );
89
- await act(async () => {
90
- fireEvent.click(screen.getByRole('button', { name: /select|search/i }));
91
- });
92
- await waitFor(() => expect(mockDataSource.find).toHaveBeenCalled());
93
- const [, params] = mockDataSource.find.mock.calls[0];
94
- // numeric value, no quoting
95
- expect(params.$filter).toEqual({ country_id: 42 });
96
- });
97
-
98
- it('re-fetches when the dependency value changes while the picker is open', async () => {
99
- function Harness() {
100
- const [country, setCountry] = useState('US');
101
- return (
102
- <div>
103
- <button data-testid="set-uk" onClick={() => setCountry('UK')}>UK</button>
104
- <LookupField
105
- field={baseField}
106
- value={null}
107
- onChange={vi.fn()}
108
- readonly={false}
109
- dataSource={mockDataSource as any}
110
- dependentValues={{ country }}
111
- />
112
- </div>
113
- );
114
- }
115
- render(<Harness />);
116
- // Open the popover.
117
- await act(async () => {
118
- fireEvent.click(screen.getByRole('button', { name: /select|search/i }));
119
- });
120
- await waitFor(() => expect(mockDataSource.find).toHaveBeenCalledTimes(1));
121
-
122
- // Change the dependency — should re-fetch.
123
- await act(async () => {
124
- fireEvent.click(screen.getByTestId('set-uk'));
125
- });
126
- await waitFor(() => expect(mockDataSource.find).toHaveBeenCalledTimes(2));
127
- const [, paramsAfter] = mockDataSource.find.mock.calls[1];
128
- expect(paramsAfter.$filter).toEqual({ country: 'UK' });
129
- });
130
- });