@jasperoosthoek/react-toolbox 0.8.0 → 0.9.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/change-log.md +330 -309
- package/dist/components/buttons/ConfirmButton.d.ts +2 -2
- package/dist/components/buttons/DeleteConfirmButton.d.ts +2 -2
- package/dist/components/buttons/IconButtons.d.ts +40 -41
- package/dist/components/errors/Errors.d.ts +1 -2
- package/dist/components/forms/FormField.d.ts +22 -0
- package/dist/components/forms/FormFields.d.ts +1 -56
- package/dist/components/forms/FormModal.d.ts +7 -34
- package/dist/components/forms/FormModalProvider.d.ts +19 -26
- package/dist/components/forms/FormProvider.d.ts +66 -0
- package/dist/components/forms/fields/FormBadgesSelection.d.ts +26 -0
- package/dist/components/forms/fields/FormCheckbox.d.ts +7 -0
- package/dist/components/forms/fields/FormDropdown.d.ts +19 -0
- package/dist/components/forms/fields/FormInput.d.ts +17 -0
- package/dist/components/forms/fields/FormSelect.d.ts +12 -0
- package/dist/components/forms/fields/index.d.ts +5 -0
- package/dist/components/indicators/CheckIndicator.d.ts +1 -2
- package/dist/components/indicators/LoadingIndicator.d.ts +4 -4
- package/dist/components/login/LoginPage.d.ts +1 -1
- package/dist/components/tables/DataTable.d.ts +2 -2
- package/dist/components/tables/DragAndDropList.d.ts +2 -2
- package/dist/components/tables/SearchBox.d.ts +2 -2
- package/dist/index.d.ts +4 -1
- package/dist/index.js +2 -2
- package/dist/index.js.LICENSE.txt +0 -4
- package/dist/localization/LocalizationContext.d.ts +1 -1
- package/dist/utils/hooks.d.ts +1 -1
- package/dist/utils/timeAndDate.d.ts +5 -2
- package/dist/utils/utils.d.ts +3 -3
- package/package.json +10 -11
- package/src/__tests__/buttons.test.tsx +545 -0
- package/src/__tests__/errors.test.tsx +339 -0
- package/src/__tests__/forms.test.tsx +3021 -0
- package/src/__tests__/hooks.test.tsx +413 -0
- package/src/__tests__/indicators.test.tsx +284 -0
- package/src/__tests__/localization.test.tsx +462 -0
- package/src/__tests__/login.test.tsx +417 -0
- package/src/__tests__/setupTests.ts +328 -0
- package/src/__tests__/tables.test.tsx +609 -0
- package/src/__tests__/timeAndDate.test.tsx +308 -0
- package/src/__tests__/utils.test.tsx +422 -0
- package/src/components/forms/FormField.tsx +92 -0
- package/src/components/forms/FormFields.tsx +3 -423
- package/src/components/forms/FormModal.tsx +168 -243
- package/src/components/forms/FormModalProvider.tsx +141 -95
- package/src/components/forms/FormProvider.tsx +218 -0
- package/src/components/forms/fields/FormBadgesSelection.tsx +108 -0
- package/src/components/forms/fields/FormCheckbox.tsx +76 -0
- package/src/components/forms/fields/FormDropdown.tsx +123 -0
- package/src/components/forms/fields/FormInput.tsx +114 -0
- package/src/components/forms/fields/FormSelect.tsx +47 -0
- package/src/components/forms/fields/index.ts +6 -0
- package/src/index.ts +32 -28
- package/src/localization/LocalizationContext.tsx +156 -131
- package/src/localization/localization.ts +131 -131
- package/src/utils/hooks.ts +108 -94
- package/src/utils/timeAndDate.ts +33 -4
- package/src/utils/utils.ts +74 -66
- package/dist/components/forms/CreateEditModal.d.ts +0 -41
- package/dist/components/forms/CreateEditModalProvider.d.ts +0 -41
- package/dist/components/forms/FormFields.test.d.ts +0 -4
- package/dist/login/Login.d.ts +0 -70
- package/src/components/forms/FormFields.test.tsx +0 -107
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isEmpty,
|
|
3
|
+
snakeToCamelCase,
|
|
4
|
+
camelToSnakeCase,
|
|
5
|
+
pluralToSingle,
|
|
6
|
+
arrayToObject,
|
|
7
|
+
roundFixed,
|
|
8
|
+
round,
|
|
9
|
+
downloadFile,
|
|
10
|
+
} from '../utils/utils';
|
|
11
|
+
|
|
12
|
+
// Mock fetch
|
|
13
|
+
const mockFetch = jest.fn();
|
|
14
|
+
global.fetch = mockFetch;
|
|
15
|
+
|
|
16
|
+
// Mock DOM APIs
|
|
17
|
+
Object.defineProperty(window, 'URL', {
|
|
18
|
+
value: {
|
|
19
|
+
createObjectURL: jest.fn(() => 'mock-blob-url'),
|
|
20
|
+
revokeObjectURL: jest.fn(),
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('Utils - General Utilities Tests', () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
jest.clearAllMocks();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('isEmpty', () => {
|
|
30
|
+
it('should return true for undefined', () => {
|
|
31
|
+
expect(isEmpty(undefined)).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should return true for null', () => {
|
|
35
|
+
expect(isEmpty(null)).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should return true for false', () => {
|
|
39
|
+
expect(isEmpty(false)).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should return true for empty object', () => {
|
|
43
|
+
expect(isEmpty({})).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should return true for empty string', () => {
|
|
47
|
+
expect(isEmpty('')).toBe(true);
|
|
48
|
+
expect(isEmpty(' ')).toBe(true); // whitespace only
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should return false for non-empty values', () => {
|
|
52
|
+
expect(isEmpty(0)).toBe(false);
|
|
53
|
+
expect(isEmpty(true)).toBe(false);
|
|
54
|
+
expect(isEmpty('hello')).toBe(false);
|
|
55
|
+
expect(isEmpty('0')).toBe(false);
|
|
56
|
+
expect(isEmpty({ key: 'value' })).toBe(false);
|
|
57
|
+
expect(isEmpty([1, 2, 3])).toBe(false);
|
|
58
|
+
expect(isEmpty([])).toBe(true); // empty array should be considered empty
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should handle edge cases', () => {
|
|
62
|
+
expect(isEmpty(0)).toBe(false);
|
|
63
|
+
expect(isEmpty(-1)).toBe(false);
|
|
64
|
+
expect(isEmpty(NaN)).toBe(false);
|
|
65
|
+
expect(isEmpty(Infinity)).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('snakeToCamelCase', () => {
|
|
70
|
+
it('should convert snake_case to camelCase', () => {
|
|
71
|
+
expect(snakeToCamelCase('snake_case')).toBe('snakeCase');
|
|
72
|
+
expect(snakeToCamelCase('multiple_word_example')).toBe('multipleWordExample');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should convert kebab-case to camelCase', () => {
|
|
76
|
+
expect(snakeToCamelCase('kebab-case')).toBe('kebabCase');
|
|
77
|
+
expect(snakeToCamelCase('multiple-word-example')).toBe('multipleWordExample');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should handle mixed cases', () => {
|
|
81
|
+
expect(snakeToCamelCase('mixed_case-example')).toBe('mixedCaseExample');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should handle single words', () => {
|
|
85
|
+
expect(snakeToCamelCase('word')).toBe('word');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should handle empty string', () => {
|
|
89
|
+
expect(snakeToCamelCase('')).toBe('');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should preserve already camelCase strings', () => {
|
|
93
|
+
expect(snakeToCamelCase('alreadyCamelCase')).toBe('alreadyCamelCase');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('camelToSnakeCase', () => {
|
|
98
|
+
it('should convert camelCase to SNAKE_CASE', () => {
|
|
99
|
+
expect(camelToSnakeCase('camelCase')).toBe('CAMEL_CASE');
|
|
100
|
+
expect(camelToSnakeCase('multipleWordExample')).toBe('MULTIPLE_WORD_EXAMPLE');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should handle single words', () => {
|
|
104
|
+
expect(camelToSnakeCase('word')).toBe('WORD');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should handle empty string', () => {
|
|
108
|
+
expect(camelToSnakeCase('')).toBe('');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should handle already uppercase strings', () => {
|
|
112
|
+
expect(camelToSnakeCase('ALREADY_UPPER')).toBe('ALREADY_UPPER');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('should handle strings with numbers', () => {
|
|
116
|
+
expect(camelToSnakeCase('test123')).toBe('TEST_123');
|
|
117
|
+
expect(camelToSnakeCase('test123Word')).toBe('TEST_123_WORD');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('pluralToSingle', () => {
|
|
122
|
+
it('should convert standard plurals to singular', () => {
|
|
123
|
+
expect(pluralToSingle('cats')).toBe('cat');
|
|
124
|
+
expect(pluralToSingle('dogs')).toBe('dog');
|
|
125
|
+
expect(pluralToSingle('items')).toBe('item');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should handle words ending in "ies"', () => {
|
|
129
|
+
expect(pluralToSingle('categories')).toBe('category');
|
|
130
|
+
expect(pluralToSingle('stories')).toBe('story');
|
|
131
|
+
expect(pluralToSingle('companies')).toBe('company');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should handle uppercase "IES"', () => {
|
|
135
|
+
expect(pluralToSingle('CATEGORIES')).toBe('CATEGORY');
|
|
136
|
+
expect(pluralToSingle('STORIES')).toBe('STORY');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should leave non-plural words unchanged', () => {
|
|
140
|
+
expect(pluralToSingle('cat')).toBe('cat');
|
|
141
|
+
expect(pluralToSingle('item')).toBe('item');
|
|
142
|
+
expect(pluralToSingle('mouse')).toBe('mouse');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should handle edge cases', () => {
|
|
146
|
+
expect(pluralToSingle('')).toBe('');
|
|
147
|
+
expect(pluralToSingle('s')).toBe('');
|
|
148
|
+
expect(pluralToSingle('ies')).toBe('y');
|
|
149
|
+
expect(pluralToSingle('IES')).toBe('Y'); // now works correctly
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('arrayToObject', () => {
|
|
154
|
+
it('should convert array to object using specified key', () => {
|
|
155
|
+
const array = [
|
|
156
|
+
{ id: 1, name: 'Alice' },
|
|
157
|
+
{ id: 2, name: 'Bob' },
|
|
158
|
+
{ id: 3, name: 'Charlie' },
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
const result = arrayToObject(array, 'id');
|
|
162
|
+
|
|
163
|
+
expect(result).toEqual({
|
|
164
|
+
1: { id: 1, name: 'Alice' },
|
|
165
|
+
2: { id: 2, name: 'Bob' },
|
|
166
|
+
3: { id: 3, name: 'Charlie' },
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should work with string keys', () => {
|
|
171
|
+
const array = [
|
|
172
|
+
{ code: 'US', name: 'United States' },
|
|
173
|
+
{ code: 'CA', name: 'Canada' },
|
|
174
|
+
{ code: 'MX', name: 'Mexico' },
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
const result = arrayToObject(array, 'code');
|
|
178
|
+
|
|
179
|
+
expect(result).toEqual({
|
|
180
|
+
US: { code: 'US', name: 'United States' },
|
|
181
|
+
CA: { code: 'CA', name: 'Canada' },
|
|
182
|
+
MX: { code: 'MX', name: 'Mexico' },
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should handle empty array', () => {
|
|
187
|
+
const result = arrayToObject([], 'id');
|
|
188
|
+
expect(result).toEqual({});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('should handle duplicate keys (last one wins)', () => {
|
|
192
|
+
const array = [
|
|
193
|
+
{ id: 1, name: 'First' },
|
|
194
|
+
{ id: 1, name: 'Second' },
|
|
195
|
+
];
|
|
196
|
+
|
|
197
|
+
const result = arrayToObject(array, 'id');
|
|
198
|
+
|
|
199
|
+
expect(result).toEqual({
|
|
200
|
+
1: { id: 1, name: 'Second' },
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe('roundFixed', () => {
|
|
206
|
+
it('should round numbers to specified decimal places as string', () => {
|
|
207
|
+
expect(roundFixed(3.14159, 2)).toBe('3.14');
|
|
208
|
+
expect(roundFixed(3.14159, 0)).toBe('3');
|
|
209
|
+
expect(roundFixed(3.14159, 4)).toBe('3.1416');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should handle string input', () => {
|
|
213
|
+
expect(roundFixed('3.14159', 2)).toBe('3.14');
|
|
214
|
+
expect(roundFixed('10', 2)).toBe('10.00');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should use 0 decimals as default', () => {
|
|
218
|
+
expect(roundFixed(3.14159)).toBe('3');
|
|
219
|
+
expect(roundFixed(3.9)).toBe('4');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should handle negative numbers', () => {
|
|
223
|
+
expect(roundFixed(-3.14159, 2)).toBe('-3.14');
|
|
224
|
+
expect(roundFixed(-3.9)).toBe('-4');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should handle zero', () => {
|
|
228
|
+
expect(roundFixed(0, 2)).toBe('0.00');
|
|
229
|
+
expect(roundFixed('0', 3)).toBe('0.000');
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe('round', () => {
|
|
234
|
+
it('should round numbers to specified decimal places as number', () => {
|
|
235
|
+
expect(round(3.14159, 2)).toBe(3.14);
|
|
236
|
+
expect(round(3.14159, 0)).toBe(3);
|
|
237
|
+
expect(round(3.14159, 4)).toBe(3.1416);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should handle string input', () => {
|
|
241
|
+
expect(round('3.14159', 2)).toBe(3.14);
|
|
242
|
+
expect(round('10', 2)).toBe(10);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should use 0 decimals as default', () => {
|
|
246
|
+
expect(round(3.14159)).toBe(3);
|
|
247
|
+
expect(round(3.9)).toBe(4);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should handle negative numbers', () => {
|
|
251
|
+
expect(round(-3.14159, 2)).toBe(-3.14);
|
|
252
|
+
expect(round(-3.9)).toBe(-4);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should handle zero', () => {
|
|
256
|
+
expect(round(0, 2)).toBe(0);
|
|
257
|
+
expect(round('0', 3)).toBe(0);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should return number type not string', () => {
|
|
261
|
+
const result = round(3.14159, 2);
|
|
262
|
+
expect(typeof result).toBe('number');
|
|
263
|
+
expect(result).toBe(3.14);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe('downloadFile', () => {
|
|
268
|
+
let mockLink: any;
|
|
269
|
+
let createElementSpy: jest.SpyInstance;
|
|
270
|
+
let appendChildSpy: jest.SpyInstance;
|
|
271
|
+
let removeChildSpy: jest.SpyInstance;
|
|
272
|
+
|
|
273
|
+
beforeEach(() => {
|
|
274
|
+
mockLink = {
|
|
275
|
+
href: '',
|
|
276
|
+
setAttribute: jest.fn(),
|
|
277
|
+
click: jest.fn(),
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
createElementSpy = jest.spyOn(document, 'createElement').mockReturnValue(mockLink);
|
|
281
|
+
appendChildSpy = jest.spyOn(document.body, 'appendChild').mockImplementation();
|
|
282
|
+
removeChildSpy = jest.spyOn(document.body, 'removeChild').mockImplementation();
|
|
283
|
+
|
|
284
|
+
// Mock successful fetch response
|
|
285
|
+
mockFetch.mockResolvedValue({
|
|
286
|
+
ok: true,
|
|
287
|
+
status: 200,
|
|
288
|
+
blob: () => Promise.resolve(new Blob(['test content'], { type: 'text/plain' })),
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
afterEach(() => {
|
|
293
|
+
createElementSpy.mockRestore();
|
|
294
|
+
appendChildSpy.mockRestore();
|
|
295
|
+
removeChildSpy.mockRestore();
|
|
296
|
+
mockFetch.mockClear();
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('should download file using default fetch', async () => {
|
|
300
|
+
await downloadFile('http://example.com/file.txt', 'download.txt');
|
|
301
|
+
|
|
302
|
+
expect(mockFetch).toHaveBeenCalledWith('http://example.com/file.txt', {
|
|
303
|
+
method: 'GET',
|
|
304
|
+
headers: {},
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
expect(window.URL.createObjectURL).toHaveBeenCalled();
|
|
308
|
+
expect(createElementSpy).toHaveBeenCalledWith('a');
|
|
309
|
+
expect(mockLink.setAttribute).toHaveBeenCalledWith('download', 'download.txt');
|
|
310
|
+
expect(appendChildSpy).toHaveBeenCalledWith(mockLink);
|
|
311
|
+
expect(mockLink.click).toHaveBeenCalled();
|
|
312
|
+
expect(removeChildSpy).toHaveBeenCalledWith(mockLink);
|
|
313
|
+
expect(window.URL.revokeObjectURL).toHaveBeenCalledWith('mock-blob-url');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it('should download file using custom fetch function', async () => {
|
|
317
|
+
const customFetch = jest.fn().mockResolvedValue({
|
|
318
|
+
ok: true,
|
|
319
|
+
status: 200,
|
|
320
|
+
blob: () => Promise.resolve(new Blob(['custom content'], { type: 'text/plain' })),
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
await downloadFile('http://example.com/file.txt', 'download.txt', {
|
|
324
|
+
fetchFn: customFetch
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
expect(customFetch).toHaveBeenCalledWith('http://example.com/file.txt', {
|
|
328
|
+
method: 'GET',
|
|
329
|
+
headers: {},
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('should set correct href and download attributes', async () => {
|
|
334
|
+
await downloadFile('http://example.com/test.pdf', 'my-file.pdf');
|
|
335
|
+
|
|
336
|
+
expect(mockLink.href).toBe('mock-blob-url');
|
|
337
|
+
expect(mockLink.setAttribute).toHaveBeenCalledWith('download', 'my-file.pdf');
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('should handle fetch errors', async () => {
|
|
341
|
+
const error = new Error('Network error');
|
|
342
|
+
mockFetch.mockRejectedValue(error);
|
|
343
|
+
|
|
344
|
+
await expect(
|
|
345
|
+
downloadFile('http://example.com/file.txt', 'download.txt')
|
|
346
|
+
).rejects.toThrow('Network error');
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('should create blob URL and trigger download', async () => {
|
|
350
|
+
const mockBlob = new Blob(['test content'], { type: 'application/pdf' });
|
|
351
|
+
mockFetch.mockResolvedValue({
|
|
352
|
+
ok: true,
|
|
353
|
+
status: 200,
|
|
354
|
+
blob: () => Promise.resolve(mockBlob),
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
await downloadFile('http://example.com/document.pdf', 'document.pdf');
|
|
358
|
+
|
|
359
|
+
expect(window.URL.createObjectURL).toHaveBeenCalledWith(mockBlob);
|
|
360
|
+
expect(mockLink.href).toBe('mock-blob-url');
|
|
361
|
+
expect(mockLink.click).toHaveBeenCalled();
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('should handle HTTP errors', async () => {
|
|
365
|
+
mockFetch.mockResolvedValue({
|
|
366
|
+
ok: false,
|
|
367
|
+
status: 404,
|
|
368
|
+
blob: () => Promise.resolve(new Blob()),
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
await expect(
|
|
372
|
+
downloadFile('http://example.com/file.txt', 'download.txt')
|
|
373
|
+
).rejects.toThrow('HTTP error! status: 404');
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('should use custom headers when provided', async () => {
|
|
377
|
+
await downloadFile('http://example.com/file.txt', 'download.txt', {
|
|
378
|
+
headers: { 'Authorization': 'Bearer token123' }
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
expect(mockFetch).toHaveBeenCalledWith('http://example.com/file.txt', {
|
|
382
|
+
method: 'GET',
|
|
383
|
+
headers: { 'Authorization': 'Bearer token123' },
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
describe('Edge Cases and Error Handling', () => {
|
|
389
|
+
it('should handle null and undefined inputs gracefully', () => {
|
|
390
|
+
expect(isEmpty(null)).toBe(true);
|
|
391
|
+
expect(isEmpty(undefined)).toBe(true);
|
|
392
|
+
|
|
393
|
+
expect(snakeToCamelCase('')).toBe('');
|
|
394
|
+
expect(camelToSnakeCase('')).toBe('');
|
|
395
|
+
expect(pluralToSingle('')).toBe('');
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('should handle special characters in string conversions', () => {
|
|
399
|
+
expect(snakeToCamelCase('test_with_123')).toBe('testWith123');
|
|
400
|
+
expect(snakeToCamelCase('test-with-special')).toBe('testWithSpecial');
|
|
401
|
+
expect(camelToSnakeCase('testWith123')).toBe('TEST_WITH_123');
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('should handle floating point precision', () => {
|
|
405
|
+
expect(round(0.1 + 0.2, 1)).toBe(0.3);
|
|
406
|
+
expect(roundFixed(0.1 + 0.2, 1)).toBe('0.3');
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
describe('Function Export Verification', () => {
|
|
411
|
+
it('should export all utility functions', () => {
|
|
412
|
+
expect(typeof isEmpty).toBe('function');
|
|
413
|
+
expect(typeof snakeToCamelCase).toBe('function');
|
|
414
|
+
expect(typeof camelToSnakeCase).toBe('function');
|
|
415
|
+
expect(typeof pluralToSingle).toBe('function');
|
|
416
|
+
expect(typeof arrayToObject).toBe('function');
|
|
417
|
+
expect(typeof roundFixed).toBe('function');
|
|
418
|
+
expect(typeof round).toBe('function');
|
|
419
|
+
expect(typeof downloadFile).toBe('function');
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useForm } from './FormProvider';
|
|
3
|
+
import { FormValue } from './FormFields';
|
|
4
|
+
import { FormInput, FormInputProps } from './fields/FormInput';
|
|
5
|
+
|
|
6
|
+
// FormField is now just a convenience wrapper around FormInput
|
|
7
|
+
export interface FormFieldProps extends FormInputProps {
|
|
8
|
+
children?: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const FormField = ({ children, ...props }: FormFieldProps) => {
|
|
12
|
+
const { formFields, hasProvider } = useForm();
|
|
13
|
+
|
|
14
|
+
if (!hasProvider || !formFields) {
|
|
15
|
+
console.error('FormField must be used within a FormProvider');
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const fieldConfig = formFields[props.name];
|
|
20
|
+
if (!fieldConfig) {
|
|
21
|
+
console.error(`FormField: No field configuration found for "${props.name}"`);
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// If children are provided, render a custom wrapper
|
|
26
|
+
if (children) {
|
|
27
|
+
return (
|
|
28
|
+
<div className="form-group">
|
|
29
|
+
{children}
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Otherwise, render as FormInput
|
|
35
|
+
return <FormInput {...props} />;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Hook to get form field value and setter for a specific field
|
|
39
|
+
export const useFormField = (componentProps: { name: string; label?: any; required?: boolean; [key: string]: any }) => {
|
|
40
|
+
const { name, label: propLabel, required: propRequired, ...htmlProps } = componentProps;
|
|
41
|
+
|
|
42
|
+
const {
|
|
43
|
+
getValue,
|
|
44
|
+
setValue,
|
|
45
|
+
formFields,
|
|
46
|
+
validationErrors,
|
|
47
|
+
pristine,
|
|
48
|
+
validated,
|
|
49
|
+
submit,
|
|
50
|
+
hasProvider
|
|
51
|
+
} = useForm();
|
|
52
|
+
|
|
53
|
+
if (!hasProvider) {
|
|
54
|
+
console.error('useFormField must be used within a FormProvider');
|
|
55
|
+
return {
|
|
56
|
+
value: '',
|
|
57
|
+
onChange: () => {},
|
|
58
|
+
isInvalid: false,
|
|
59
|
+
error: null,
|
|
60
|
+
label: undefined,
|
|
61
|
+
required: false,
|
|
62
|
+
mergedProps: {},
|
|
63
|
+
submit: () => {},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const fieldConfig = formFields[name];
|
|
68
|
+
if (!fieldConfig) {
|
|
69
|
+
console.error(`useFormField: No field configuration found for "${name}"`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const isInvalid = !pristine && !validated && !!validationErrors[name];
|
|
73
|
+
const error = isInvalid ? validationErrors[name] : null;
|
|
74
|
+
|
|
75
|
+
// Priority order: component props > config props > defaults
|
|
76
|
+
const label = propLabel !== undefined ? propLabel : fieldConfig?.label;
|
|
77
|
+
const required = propRequired !== undefined ? propRequired : fieldConfig?.required;
|
|
78
|
+
|
|
79
|
+
// Merge props: htmlProps override config.formProps
|
|
80
|
+
const mergedProps = { ...fieldConfig?.formProps, ...htmlProps };
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
value: getValue(name),
|
|
84
|
+
onChange: (value: FormValue) => setValue(name, value),
|
|
85
|
+
isInvalid,
|
|
86
|
+
error,
|
|
87
|
+
label,
|
|
88
|
+
required,
|
|
89
|
+
mergedProps,
|
|
90
|
+
submit,
|
|
91
|
+
};
|
|
92
|
+
};
|