oxform-core 0.1.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.
@@ -0,0 +1,254 @@
1
+ import { FormApi } from '#form-api';
2
+ import type { FormIssue } from '#form-api.types';
3
+ import { describe, expect, it } from 'vitest';
4
+ import z from 'zod';
5
+
6
+ const schema = z.object({
7
+ name: z.string(),
8
+ email: z.string(),
9
+ nested: z.object({
10
+ value: z.string(),
11
+ }),
12
+ });
13
+
14
+ const defaultValues = {
15
+ name: 'John',
16
+ email: 'john@example.com',
17
+ nested: {
18
+ value: 'test',
19
+ },
20
+ };
21
+
22
+ const setup = () => {
23
+ const form = new FormApi({
24
+ schema,
25
+ defaultValues,
26
+ });
27
+
28
+ form['~mount']();
29
+ return { form };
30
+ };
31
+
32
+ const mockError: FormIssue = {
33
+ code: 'custom',
34
+ message: 'Custom error message',
35
+ path: ['name'],
36
+ } as any;
37
+
38
+ const mockError2: FormIssue = {
39
+ code: 'custom_2',
40
+ message: 'Second custom error',
41
+ path: ['name'],
42
+ } as any;
43
+
44
+ describe('replace mode (default)', () => {
45
+ it('should set errors for a field with no existing errors', () => {
46
+ const { form } = setup();
47
+
48
+ form.field.setErrors('name', [mockError]);
49
+
50
+ expect(form.field.errors('name')).toEqual([mockError]);
51
+ });
52
+
53
+ it('should replace existing errors by default', () => {
54
+ const { form } = setup();
55
+
56
+ form.field.setErrors('name', [mockError]);
57
+ expect(form.field.errors('name')).toEqual([mockError]);
58
+
59
+ form.field.setErrors('name', [mockError2]);
60
+ expect(form.field.errors('name')).toEqual([mockError2]);
61
+ });
62
+
63
+ it('should replace existing errors when mode is explicitly set to replace', () => {
64
+ const { form } = setup();
65
+
66
+ form.field.setErrors('name', [mockError]);
67
+ form.field.setErrors('name', [mockError2], { mode: 'replace' });
68
+
69
+ expect(form.field.errors('name')).toEqual([mockError2]);
70
+ });
71
+
72
+ it('should clear errors when setting empty array', () => {
73
+ const { form } = setup();
74
+
75
+ form.field.setErrors('name', [mockError]);
76
+ expect(form.field.errors('name')).toEqual([mockError]);
77
+
78
+ form.field.setErrors('name', []);
79
+ expect(form.field.errors('name')).toEqual([]);
80
+ });
81
+ });
82
+
83
+ describe('append mode', () => {
84
+ it('should append new errors to existing ones', () => {
85
+ const { form } = setup();
86
+
87
+ form.field.setErrors('name', [mockError]);
88
+ form.field.setErrors('name', [mockError2], { mode: 'append' });
89
+
90
+ expect(form.field.errors('name')).toEqual([mockError, mockError2]);
91
+ });
92
+
93
+ it('should append to empty error list', () => {
94
+ const { form } = setup();
95
+
96
+ form.field.setErrors('name', [mockError], { mode: 'append' });
97
+
98
+ expect(form.field.errors('name')).toEqual([mockError]);
99
+ });
100
+
101
+ it('should append multiple errors at once', () => {
102
+ const { form } = setup();
103
+
104
+ const additionalError: FormIssue = {
105
+ code: 'custom_3',
106
+ message: 'Third custom error',
107
+ path: ['name'],
108
+ } as any;
109
+
110
+ form.field.setErrors('name', [mockError]);
111
+ form.field.setErrors('name', [mockError2, additionalError], { mode: 'append' });
112
+
113
+ expect(form.field.errors('name')).toEqual([mockError, mockError2, additionalError]);
114
+ });
115
+ });
116
+
117
+ describe('keep mode', () => {
118
+ it('should keep existing errors when they exist', () => {
119
+ const { form } = setup();
120
+
121
+ form.field.setErrors('name', [mockError]);
122
+ form.field.setErrors('name', [mockError2], { mode: 'keep' });
123
+
124
+ expect(form.field.errors('name')).toEqual([mockError]);
125
+ });
126
+
127
+ it('should set new errors when no existing errors', () => {
128
+ const { form } = setup();
129
+
130
+ form.field.setErrors('name', [mockError], { mode: 'keep' });
131
+
132
+ expect(form.field.errors('name')).toEqual([mockError]);
133
+ });
134
+
135
+ it('should not change errors when trying to set empty array with existing errors', () => {
136
+ const { form } = setup();
137
+
138
+ form.field.setErrors('name', [mockError]);
139
+ form.field.setErrors('name', [], { mode: 'keep' });
140
+
141
+ expect(form.field.errors('name')).toEqual([mockError]);
142
+ });
143
+ });
144
+
145
+ describe('nested fields', () => {
146
+ it('should set errors for nested fields', () => {
147
+ const { form } = setup();
148
+
149
+ const nestedError: FormIssue = {
150
+ code: 'custom',
151
+ message: 'Nested error',
152
+ path: ['nested', 'value'],
153
+ } as any;
154
+
155
+ form.field.setErrors('nested.value', [nestedError]);
156
+
157
+ expect(form.field.errors('nested.value')).toEqual([nestedError]);
158
+ });
159
+
160
+ it('should work with all modes for nested fields', () => {
161
+ const { form } = setup();
162
+
163
+ const nestedError1: FormIssue = {
164
+ code: 'custom_1',
165
+ message: 'First nested error',
166
+ path: ['nested', 'value'],
167
+ } as any;
168
+
169
+ const nestedError2: FormIssue = {
170
+ code: 'custom_2',
171
+ message: 'Second nested error',
172
+ path: ['nested', 'value'],
173
+ } as any;
174
+
175
+ form.field.setErrors('nested.value', [nestedError1]);
176
+ form.field.setErrors('nested.value', [nestedError2], { mode: 'append' });
177
+ expect(form.field.errors('nested.value')).toEqual([nestedError1, nestedError2]);
178
+
179
+ form.field.setErrors('nested.value', [{ ...nestedError1, code: 'should_not_appear' } as any], { mode: 'keep' });
180
+ expect(form.field.errors('nested.value')).toEqual([nestedError1, nestedError2]);
181
+
182
+ form.field.setErrors('nested.value', [nestedError1], { mode: 'replace' });
183
+ expect(form.field.errors('nested.value')).toEqual([nestedError1]);
184
+ });
185
+ });
186
+
187
+ describe('multiple fields', () => {
188
+ it('should not affect other fields when setting errors', () => {
189
+ const { form } = setup();
190
+
191
+ const nameError: FormIssue = {
192
+ code: 'custom',
193
+ message: 'Name error',
194
+ path: ['name'],
195
+ } as any;
196
+
197
+ const emailError: FormIssue = {
198
+ code: 'custom',
199
+ message: 'Email error',
200
+ path: ['email'],
201
+ } as any;
202
+
203
+ form.field.setErrors('name', [nameError]);
204
+ form.field.setErrors('email', [emailError]);
205
+
206
+ expect(form.field.errors('name')).toEqual([nameError]);
207
+ expect(form.field.errors('email')).toEqual([emailError]);
208
+
209
+ form.field.setErrors('name', []);
210
+ expect(form.field.errors('name')).toEqual([]);
211
+ expect(form.field.errors('email')).toEqual([emailError]);
212
+ });
213
+ });
214
+
215
+ describe('form status updates', () => {
216
+ it('should update form validity when errors are set', () => {
217
+ const { form } = setup();
218
+
219
+ expect(form.store.state.status.valid).toBe(true);
220
+
221
+ form.field.setErrors('name', [mockError]);
222
+
223
+ expect(form.store.state.status.valid).toBe(false);
224
+ });
225
+
226
+ it('should update form validity when errors are cleared', () => {
227
+ const { form } = setup();
228
+
229
+ form.field.setErrors('name', [mockError]);
230
+ expect(form.store.state.status.valid).toBe(false);
231
+
232
+ form.field.setErrors('name', []);
233
+ expect(form.store.state.status.valid).toBe(true);
234
+ });
235
+
236
+ it('should maintain field meta state when setting errors manually', () => {
237
+ const { form } = setup();
238
+
239
+ form.field.change('name', 'Jane');
240
+ form.field.focus('name');
241
+ form.field.blur('name');
242
+
243
+ const metaBefore = form.field.meta('name');
244
+
245
+ form.field.setErrors('name', [mockError]);
246
+
247
+ const metaAfter = form.field.meta('name');
248
+
249
+ expect(metaAfter.dirty).toBe(metaBefore.dirty);
250
+ expect(metaAfter.touched).toBe(metaBefore.touched);
251
+ expect(metaAfter.blurred).toBe(metaBefore.blurred);
252
+ expect(metaAfter.valid).toBe(false); // This should change due to error
253
+ });
254
+ });
@@ -0,0 +1,341 @@
1
+ import { FieldApi } from '#field-api';
2
+ import { FormApi } from '#form-api';
3
+ import type { FormIssue } from '#form-api.types';
4
+ import { describe, expect, it } from 'vitest';
5
+ import z from 'zod';
6
+
7
+ const schema = z.object({
8
+ name: z.string().min(1, 'Name is required'),
9
+ email: z.string(),
10
+ nested: z.object({
11
+ value: z.string().min(1, 'Value is required'),
12
+ }),
13
+ });
14
+
15
+ const defaultValues = {
16
+ name: 'John',
17
+ email: 'john@example.com',
18
+ nested: {
19
+ value: 'test',
20
+ },
21
+ };
22
+
23
+ const setup = () => {
24
+ const form = new FormApi({
25
+ schema,
26
+ defaultValues,
27
+ });
28
+ form['~mount']();
29
+
30
+ const nameField = new FieldApi({ form, name: 'name' });
31
+ nameField['~mount']();
32
+
33
+ const emailField = new FieldApi({ form, name: 'email' });
34
+ emailField['~mount']();
35
+
36
+ const nestedField = new FieldApi({ form, name: 'nested.value' });
37
+ nestedField['~mount']();
38
+
39
+ return { form, nameField, emailField, nestedField };
40
+ };
41
+
42
+ describe('FieldApi methods', () => {
43
+ describe('validate method', () => {
44
+ it('should validate the specific field only', async () => {
45
+ const { nameField, form } = setup();
46
+
47
+ // Make the name field invalid
48
+ nameField.change('');
49
+
50
+ // Validate only the name field
51
+ const issues = await nameField.validate({ type: 'submit' });
52
+
53
+ expect(issues).toHaveLength(1);
54
+ expect(issues[0].path).toEqual(['name']);
55
+ expect(form.field.errors('name')).toHaveLength(1);
56
+ expect(form.field.errors('email')).toHaveLength(0);
57
+ });
58
+
59
+ it('should validate field with different validation types', async () => {
60
+ const { nameField } = setup();
61
+
62
+ // Test different validation types
63
+ await nameField.validate({ type: 'blur' });
64
+ await nameField.validate({ type: 'focus' });
65
+ await nameField.validate({ type: 'submit' });
66
+ await nameField.validate({ type: 'change' });
67
+
68
+ // Should not throw and should work with all types
69
+ expect(nameField.errors).toEqual([]);
70
+ });
71
+
72
+ it('should validate field without options (using default schema)', async () => {
73
+ const { nameField } = setup();
74
+
75
+ nameField.change(''); // Make invalid
76
+ const issues = await nameField.validate();
77
+
78
+ expect(issues).toHaveLength(1);
79
+ expect(issues[0].path).toEqual(['name']);
80
+ });
81
+ });
82
+
83
+ describe('setErrors method', () => {
84
+ const mockError: FormIssue = {
85
+ code: 'custom',
86
+ message: 'Custom field error',
87
+ path: ['name'],
88
+ } as any;
89
+
90
+ const mockError2: FormIssue = {
91
+ code: 'custom_2',
92
+ message: 'Second field error',
93
+ path: ['name'],
94
+ } as any;
95
+
96
+ it('should set errors in replace mode by default', () => {
97
+ const { nameField } = setup();
98
+
99
+ nameField.setErrors([mockError]);
100
+ expect(nameField.errors).toEqual([mockError]);
101
+
102
+ // Replace with new error
103
+ nameField.setErrors([mockError2]);
104
+ expect(nameField.errors).toEqual([mockError2]);
105
+ });
106
+
107
+ it('should set errors in append mode', () => {
108
+ const { nameField } = setup();
109
+
110
+ nameField.setErrors([mockError]);
111
+ nameField.setErrors([mockError2], { mode: 'append' });
112
+
113
+ expect(nameField.errors).toEqual([mockError, mockError2]);
114
+ });
115
+
116
+ it('should set errors in keep mode', () => {
117
+ const { nameField } = setup();
118
+
119
+ // Set initial error
120
+ nameField.setErrors([mockError]);
121
+
122
+ // Try to set new error with keep mode - should keep existing
123
+ nameField.setErrors([mockError2], { mode: 'keep' });
124
+ expect(nameField.errors).toEqual([mockError]);
125
+ });
126
+
127
+ it('should set errors in keep mode when no existing errors', () => {
128
+ const { nameField } = setup();
129
+
130
+ nameField.setErrors([mockError], { mode: 'keep' });
131
+ expect(nameField.errors).toEqual([mockError]);
132
+ });
133
+
134
+ it('should clear errors when setting empty array', () => {
135
+ const { nameField } = setup();
136
+
137
+ nameField.setErrors([mockError]);
138
+ expect(nameField.errors).toEqual([mockError]);
139
+
140
+ nameField.setErrors([]);
141
+ expect(nameField.errors).toEqual([]);
142
+ });
143
+
144
+ it('should work with nested fields', () => {
145
+ const { nestedField } = setup();
146
+
147
+ const nestedError: FormIssue = {
148
+ code: 'custom',
149
+ message: 'Nested field error',
150
+ path: ['nested', 'value'],
151
+ } as any;
152
+
153
+ nestedField.setErrors([nestedError]);
154
+ expect(nestedField.errors).toEqual([nestedError]);
155
+ });
156
+ });
157
+
158
+ describe('reset method', () => {
159
+ it('should reset field to default value', () => {
160
+ const { nameField } = setup();
161
+
162
+ // Change the field value
163
+ nameField.change('Jane');
164
+ expect(nameField.value).toBe('Jane');
165
+
166
+ // Reset the field
167
+ nameField.reset();
168
+ expect(nameField.value).toBe('John'); // back to default
169
+ });
170
+
171
+ it('should reset field to specific value', () => {
172
+ const { nameField } = setup();
173
+
174
+ nameField.change('Jane');
175
+ nameField.reset({ value: 'Bob' });
176
+
177
+ expect(nameField.value).toBe('Bob');
178
+ });
179
+
180
+ it('should reset field meta by default', () => {
181
+ const { nameField } = setup();
182
+
183
+ // Make the field dirty and touched
184
+ nameField.change('Jane');
185
+ nameField.focus();
186
+ nameField.blur();
187
+
188
+ expect(nameField.meta.dirty).toBe(true);
189
+ expect(nameField.meta.touched).toBe(true);
190
+ expect(nameField.meta.blurred).toBe(true);
191
+
192
+ // Reset the field
193
+ nameField.reset();
194
+
195
+ expect(nameField.meta.dirty).toBe(false);
196
+ expect(nameField.meta.touched).toBe(false);
197
+ expect(nameField.meta.blurred).toBe(false);
198
+ });
199
+
200
+ it('should keep field meta when specified', () => {
201
+ const { nameField } = setup();
202
+
203
+ // Make the field dirty and touched
204
+ nameField.change('Jane');
205
+ nameField.focus();
206
+ nameField.blur();
207
+
208
+ const metaBefore = { ...nameField.meta };
209
+
210
+ // Reset but keep meta
211
+ nameField.reset({ keep: { meta: true } });
212
+
213
+ expect(nameField.value).toBe('John'); // value should reset
214
+ expect(nameField.meta.dirty).toBe(metaBefore.dirty);
215
+ expect(nameField.meta.touched).toBe(metaBefore.touched);
216
+ expect(nameField.meta.blurred).toBe(metaBefore.blurred);
217
+ });
218
+
219
+ it('should reset field errors by default', () => {
220
+ const { nameField } = setup();
221
+
222
+ const mockError: FormIssue = {
223
+ code: 'custom',
224
+ message: 'Test error',
225
+ path: ['name'],
226
+ } as any;
227
+
228
+ nameField.setErrors([mockError]);
229
+ expect(nameField.errors).toEqual([mockError]);
230
+
231
+ nameField.reset();
232
+ expect(nameField.errors).toEqual([]);
233
+ });
234
+
235
+ it('should keep field errors when specified', () => {
236
+ const { nameField } = setup();
237
+
238
+ const mockError: FormIssue = {
239
+ code: 'custom',
240
+ message: 'Test error',
241
+ path: ['name'],
242
+ } as any;
243
+
244
+ nameField.setErrors([mockError]);
245
+ nameField.change('Jane');
246
+
247
+ nameField.reset({ keep: { errors: true } });
248
+
249
+ expect(nameField.value).toBe('John'); // value should reset
250
+ expect(nameField.errors).toEqual([mockError]); // errors should be kept
251
+ });
252
+
253
+ it('should work with nested fields', () => {
254
+ const { nestedField } = setup();
255
+
256
+ nestedField.change('changed');
257
+ expect(nestedField.value).toBe('changed');
258
+
259
+ nestedField.reset();
260
+ expect(nestedField.value).toBe('test'); // back to default
261
+ });
262
+ });
263
+
264
+ describe('integration with existing methods', () => {
265
+ it('should work with change and validation together', async () => {
266
+ const { nameField } = setup();
267
+
268
+ // Change to invalid value
269
+ nameField.change('');
270
+
271
+ // Validate the field
272
+ const issues = await nameField.validate({ type: 'submit' });
273
+ expect(issues).toHaveLength(1);
274
+ expect(nameField.errors).toHaveLength(1);
275
+
276
+ // Change to valid value
277
+ nameField.change('Jane');
278
+
279
+ // Validate again
280
+ const validIssues = await nameField.validate({ type: 'submit' });
281
+ expect(validIssues).toHaveLength(0);
282
+ expect(nameField.errors).toHaveLength(0);
283
+ });
284
+
285
+ it('should work with focus, blur, and validation', async () => {
286
+ const { nameField } = setup();
287
+
288
+ nameField.focus();
289
+ expect(nameField.meta.touched).toBe(true);
290
+
291
+ nameField.blur();
292
+ expect(nameField.meta.blurred).toBe(true);
293
+
294
+ await nameField.validate({ type: 'blur' });
295
+ // Should not throw and should work properly
296
+ });
297
+
298
+ it('should maintain consistency with form state', () => {
299
+ const { nameField, form } = setup();
300
+
301
+ const mockError: FormIssue = {
302
+ code: 'custom',
303
+ message: 'Test error',
304
+ path: ['name'],
305
+ } as any;
306
+
307
+ // Set error through field
308
+ nameField.setErrors([mockError]);
309
+
310
+ // Should be reflected in form
311
+ expect(form.field.errors('name')).toEqual([mockError]);
312
+ expect(form.store.state.status.valid).toBe(false);
313
+
314
+ // Clear error through field
315
+ nameField.setErrors([]);
316
+
317
+ // Should be reflected in form
318
+ expect(form.field.errors('name')).toEqual([]);
319
+ expect(form.store.state.status.valid).toBe(true);
320
+ });
321
+
322
+ it('should work with form-level operations', async () => {
323
+ const { nameField, form } = setup();
324
+
325
+ // Change field value
326
+ nameField.change('Jane');
327
+ expect(nameField.value).toBe('Jane');
328
+ expect(form.field.get('name')).toBe('Jane');
329
+
330
+ // Reset through form
331
+ form.field.reset('name');
332
+ expect(nameField.value).toBe('John');
333
+
334
+ // Validate through form
335
+ nameField.change('');
336
+ const issues = await form.validate('name', { type: 'submit' });
337
+ expect(issues).toHaveLength(1);
338
+ expect(nameField.errors).toHaveLength(1);
339
+ });
340
+ });
341
+ });