Skip to content

Commit a6ec319

Browse files
authored
Merge pull request #758 from data-driven-forms/manager-field-configs
fix(manager): allow multiple field listener validators
2 parents d307c94 + 2aac294 commit a6ec319

File tree

7 files changed

+360
-79
lines changed

7 files changed

+360
-79
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { isPromise } from '../utils/validate';
2+
import ComposeValidators from '../types/compose-validators';
3+
4+
/**
5+
* Function that allows running mulple validation functions on a single field.
6+
* Pass an array of validators as an argument.
7+
* New validation will be returned, that runs all validation until first one fails, or every validator is sucessfull.
8+
* @param validators Array of Validators. First rejected validator error message will be returned as validation error.
9+
* Synchronous validators are run in synchrounously and sync error is prioritized over async errors.
10+
* @returns {Function} New validation function
11+
*/
12+
const composeValidators: ComposeValidators = (validators = []) => (value, allValues) => {
13+
const promises: Promise<string | undefined>[] = [];
14+
let index = 0;
15+
let error: string | undefined;
16+
while (validators.length > 0 && !error && index < validators.length) {
17+
const result = validators[index](value, allValues);
18+
if (isPromise(result)) {
19+
promises.push(result as Promise<string | undefined>);
20+
} else {
21+
error = result as string | undefined;
22+
}
23+
24+
index = index + 1;
25+
}
26+
27+
if (error) {
28+
return error;
29+
}
30+
31+
if (promises.length > 0) {
32+
return Promise.all(promises)
33+
.catch((asyncError) => {
34+
throw error || asyncError;
35+
})
36+
.then(() => {
37+
if (error) {
38+
throw error;
39+
}
40+
41+
return undefined;
42+
});
43+
}
44+
45+
return undefined;
46+
};
47+
48+
export default composeValidators;
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import composeValidators from '../../files/compose-validators';
2+
import { isPromise } from '../../utils/validate';
3+
4+
describe('composeValidators', () => {
5+
const one = (value) => new Promise((res, rej) => setTimeout(() => (value === 'one' ? rej('error-one') : res()), 200));
6+
const two = (value) => new Promise((res, rej) => setTimeout(() => (value === 'two' ? rej('error-two') : res()), 200));
7+
const three = (value) => (value === 'three' ? 'error-three' : undefined);
8+
const threeCopy = (value) => (value === 'three' ? 'error-three-copy' : undefined);
9+
10+
it('should fail last sync validator', () => {
11+
expect.assertions(2);
12+
const validator = composeValidators([one, two, three]);
13+
const result = validator('three');
14+
expect(isPromise(validator)).toBe(false);
15+
expect(result).toEqual('error-three');
16+
});
17+
18+
it('should fail second async validator', (done) => {
19+
expect.assertions(2);
20+
const validator = composeValidators([one, two, three]);
21+
const result = validator('two');
22+
expect(isPromise(result)).toEqual(true);
23+
return result.catch((result) => {
24+
expect(result).toEqual('error-two');
25+
done();
26+
});
27+
});
28+
29+
it('should fail first async validator', (done) => {
30+
expect.assertions(2);
31+
const validator = composeValidators([one, two, three]);
32+
const result = validator('one');
33+
expect(isPromise(result)).toBe(true);
34+
return result.catch((result) => {
35+
expect(result).toEqual('error-one');
36+
done();
37+
});
38+
});
39+
40+
it('should pass all validators', (done) => {
41+
expect.assertions(2);
42+
const validator = composeValidators([one, two, three]);
43+
const result = validator('ok');
44+
expect(isPromise(result)).toEqual(true);
45+
return result.then((result) => {
46+
expect(result).toBeUndefined();
47+
done();
48+
});
49+
});
50+
51+
it('should pass all async validators', (done) => {
52+
expect.assertions(2);
53+
const validator = composeValidators([one, two]);
54+
const result = validator('ok');
55+
expect(isPromise(result)).toEqual(true);
56+
return result.then((result) => {
57+
expect(result).toBeUndefined();
58+
done();
59+
});
60+
});
61+
62+
it('should return error message of sync validators in order of validators', () => {
63+
expect.assertions(2);
64+
const validator = composeValidators([three, threeCopy]);
65+
const result = validator('three');
66+
expect(isPromise(result)).toEqual(false);
67+
expect(result).toEqual('error-three');
68+
});
69+
70+
it('should pass when no argument is passed to the function', () => {
71+
expect.assertions(1);
72+
const validator = composeValidators();
73+
const result = validator();
74+
expect(result).toBeUndefined();
75+
});
76+
});

packages/form-state-manager/src/tests/files/use-field.test.js

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -70,37 +70,45 @@ describe('useField', () => {
7070
expect(unregisterSpy).toHaveBeenCalledWith(unregisterArguments);
7171
});
7272

73-
it('should set correct value on input type text', () => {
73+
it('should set correct value on input type text', async () => {
7474
const wrapper = mount(<DummyComponent subscriberProps={{ name: 'spy' }} managerApi={managerApi} />);
7575
const input = wrapper.find('input');
76-
input.simulate('change', { target: { value: 'foo' } });
76+
await act(async () => {
77+
input.simulate('change', { target: { value: 'foo' } });
78+
});
7779
wrapper.update();
7880
expect(wrapper.find(SpyComponent).prop('value')).toEqual('foo');
7981
});
8082

81-
it('should set correct value on input type checkbox', () => {
83+
it('should set correct value on input type checkbox', async () => {
8284
const wrapper = mount(<DummyComponent subscriberProps={{ name: 'spy', type: 'checkbox' }} managerApi={managerApi} />);
8385
const input = wrapper.find('input');
84-
input.simulate('change', { target: { checked: true, type: 'checkbox' } });
86+
await act(async () => {
87+
input.simulate('change', { target: { checked: true, type: 'checkbox' } });
88+
});
8589
wrapper.update();
8690
expect(wrapper.find(SpyComponent).prop('checked')).toEqual(true);
8791
});
8892

89-
it('should set correct array value', () => {
93+
it('should set correct array value', async () => {
9094
const wrapper = mount(<DummyComponent subscriberProps={{ fakeComponent: true, name: 'spy', changeValue: [] }} managerApi={managerApi} />);
9195
const input = wrapper.find('button#fake-change');
92-
input.simulate('click');
96+
await act(async () => {
97+
input.simulate('click');
98+
});
9399
wrapper.update();
94100
expect(wrapper.find(NonInputSpyComponent).prop('value')).toEqual([]);
95101
});
96102

97-
it('should set correct on non event object value', () => {
103+
it('should set correct on non event object value', async () => {
98104
const nonEventObject = { value: 1, label: 'bar' };
99105
const wrapper = mount(
100106
<DummyComponent subscriberProps={{ fakeComponent: true, name: 'spy', changeValue: nonEventObject }} managerApi={managerApi} />
101107
);
102108
const input = wrapper.find('button#fake-change');
103-
input.simulate('click');
109+
await act(async () => {
110+
input.simulate('click');
111+
});
104112
wrapper.update();
105113
expect(wrapper.find(NonInputSpyComponent).prop('value')).toEqual(nonEventObject);
106114
});
@@ -431,38 +439,44 @@ describe('useField', () => {
431439
name: 'async-validate',
432440
validate: asyncValidator
433441
};
442+
434443
const wrapper = mount(<DummyComponent managerApi={managerApi} subscriberProps={subscriberProps} />);
435-
const spy = wrapper.find(SpyComponent);
436444
const input = wrapper.find('input');
437-
expect(spy.prop('meta')).toEqual(expect.objectContaining({ validating: true, valid: true }));
445+
expect(wrapper.find(SpyComponent).prop('meta')).toEqual(expect.objectContaining({ validating: true, valid: true }));
438446

439447
await act(async () => {
440448
jest.runAllTimers(); // skip initial validation
441449
});
450+
wrapper.update();
442451

443-
expect(spy.prop('meta')).toEqual(expect.objectContaining({ validating: false, valid: true }));
452+
expect(wrapper.find(SpyComponent).prop('meta')).toEqual(expect.objectContaining({ validating: false, valid: true }));
444453

445-
input.simulate('change', { target: { value: 'foo' } });
454+
await act(() => {
455+
input.simulate('change', { target: { value: 'foo' } });
456+
});
446457
/**
447458
* All validations are pending
448459
*/
449460
await act(async () => {
450461
jest.advanceTimersByTime(10);
451462
});
463+
wrapper.update();
452464
expect(wrapper.find(SpyComponent).prop('meta')).toEqual(expect.objectContaining({ validating: true, valid: true }));
453465
/**
454466
* Second faster async validation has finished
455467
*/
456468
await act(async () => {
457469
jest.advanceTimersByTime(290);
458470
});
471+
wrapper.update();
459472
expect(wrapper.find(SpyComponent).prop('meta')).toEqual(expect.objectContaining({ validating: true, valid: true }));
460473
/**
461474
* First slow async validation has finished
462475
*/
463476
await act(async () => {
464477
jest.advanceTimersByTime(200);
465478
});
479+
wrapper.update();
466480
expect(wrapper.find(SpyComponent).prop('meta')).toEqual(expect.objectContaining({ validating: false, valid: true }));
467481
});
468482
});

0 commit comments

Comments
 (0)