Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion __tests__/factories/forms/SignupForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ import { mockStrictLayerProps } from '../../helpers';
import ValidationLayer from '../../../src';


const defaultData = { email: null, password: null };
const defaultData = {
email: null,
password: null,
passwordConfirmation: null,
};

type Props = {
id?: string,
Expand Down Expand Up @@ -87,6 +91,24 @@ const SignupForm = ({
</span>
}
</div>
<div
className={classNames(
'password-confirmation-wrapper',
layer.getStatusFor('passwordConfirmation'),
)}
>
<input
type="text"
className="password-confirmation-input"
{...layer.getPropsFor('passwordConfirmation')}
/>
{
layer.getMessageFor('passwordConfirmation') &&
<span className="password-confirmation-message">
{layer.getMessageFor('passwordConfirmation')}
</span>
}
</div>
<div className="submit-button-wrapper">
<button
{...layer.getSubmitButtonProps()}
Expand Down
1 change: 1 addition & 0 deletions __tests__/tests/cases/async-strategies/onBlur.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ describe('asyncStrategy.onBlur()', () => {
validateAsync,
},
password: true,
passwordConfirmation: true,
},
});

Expand Down
1 change: 1 addition & 0 deletions __tests__/tests/cases/async-strategies/onChange.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ describe('asyncStrategy.onChange()', () => {
validateAsync,
},
password: true,
passwordConfirmation: true,
},
});

Expand Down
129 changes: 129 additions & 0 deletions __tests__/tests/cases/validateLinkedFields.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/* @flow */
/* eslint-disable max-len */

import { Record } from 'immutable';

import { mountSignupForm } from '../../factories/forms/SignupForm';

describe('case.validateLinkedFields', () => {
it('validates linked field of JS Object', () => {
const Form = mountSignupForm({
strategy: 'onFirstChange',
data: {
email: null,
password: null,
passwordConfirmation: null,
},
fields: {
email: true,
password: { linkedFields: ['passwordConfirmation'] },
passwordConfirmation: {
validate: (passwordConfirmation, data) => (
passwordConfirmation !== data.password
? { valid: false, message: 'Password does not match' }
: { valid: true, message: 'Nice!' }
),
},
},
});


// No results are shown on mount
expect(Form.find('.password-confirmation-message').length).toBe(0);
expect(Form.find('.password-confirmation-wrapper').hasClass('failure')).toBe(false);
expect(Form.find('.password-confirmation-wrapper').hasClass('success')).toBe(false);

// User types `password`
Form.setProps({ data: { password: '123' } });
Form.find('.password-input').simulate('change');

// User types invalid `passwordConfirmation`
Form.setProps({ data: { password: '123', passwordConfirmation: '12' } });
Form.find('.password-confirmation-input').simulate('change');

// Error message for `passwordConfirmation` is shown
expect(Form.find('.password-confirmation-message').text()).toBe('Password does not match');
expect(Form.find('.password-confirmation-wrapper').hasClass('failure')).toBe(true);
expect(Form.find('.password-confirmation-wrapper').hasClass('success')).toBe(false);

// User types valid `passwordConfirmation`
Form.setProps({ data: { password: '123', passwordConfirmation: '123' } });
Form.find('.password-confirmation-input').simulate('change');

// Success for `passwordConfirmation`
expect(Form.find('.password-confirmation-message').text()).toBe('Nice!');
expect(Form.find('.password-confirmation-wrapper').hasClass('failure')).toBe(false);
expect(Form.find('.password-confirmation-wrapper').hasClass('success')).toBe(true);

// User changes `password`
Form.setProps({ data: { password: '1234', passwordConfirmation: '123' } });
Form.find('.password-input').simulate('change');

// Error message for `passwordConfirmation` is shown
expect(Form.find('.password-confirmation-message').text()).toBe('Password does not match');
expect(Form.find('.password-confirmation-wrapper').hasClass('failure')).toBe(true);
expect(Form.find('.password-confirmation-wrapper').hasClass('success')).toBe(false);
});


it('validates linked field of Immutable Record', () => {
const Data = Record({
email: null,
password: null,
passwordConfirmation: null,
});

const Form = mountSignupForm({
strategy: 'onFirstChange',
data: new Data(),
fields: {
email: true,
password: { linkedFields: ['passwordConfirmation'] },
passwordConfirmation: {
validate: (passwordConfirmation, data) => (
passwordConfirmation !== data.password
? { valid: false, message: 'Password does not match' }
: { valid: true, message: 'Nice!' }
),
},
},
});


// No results are shown on mount
expect(Form.find('.password-confirmation-message').length).toBe(0);
expect(Form.find('.password-confirmation-wrapper').hasClass('failure')).toBe(false);
expect(Form.find('.password-confirmation-wrapper').hasClass('success')).toBe(false);

// User types `password`
Form.setProps({ data: new Data({ password: '123' }) });
Form.find('.password-input').simulate('change');

// User types invalid `passwordConfirmation`
Form.setProps({ data: new Data({ password: '123', passwordConfirmation: '12' }) });
Form.find('.password-confirmation-input').simulate('change');

// Error message for `passwordConfirmation` is shown
expect(Form.find('.password-confirmation-message').text()).toBe('Password does not match');
expect(Form.find('.password-confirmation-wrapper').hasClass('failure')).toBe(true);
expect(Form.find('.password-confirmation-wrapper').hasClass('success')).toBe(false);

// User types valid `passwordConfirmation`
Form.setProps({ data: new Data({ password: '123', passwordConfirmation: '123' }) });
Form.find('.password-confirmation-input').simulate('change');

// Success for `passwordConfirmation`
expect(Form.find('.password-confirmation-message').text()).toBe('Nice!');
expect(Form.find('.password-confirmation-wrapper').hasClass('failure')).toBe(false);
expect(Form.find('.password-confirmation-wrapper').hasClass('success')).toBe(true);

// User changes `password`
Form.setProps({ data: new Data({ password: '1234', passwordConfirmation: '123' }) });
Form.find('.password-input').simulate('change');

// Error message for `passwordConfirmation` is shown
expect(Form.find('.password-confirmation-message').text()).toBe('Password does not match');
expect(Form.find('.password-confirmation-wrapper').hasClass('failure')).toBe(true);
expect(Form.find('.password-confirmation-wrapper').hasClass('success')).toBe(false);
});
});
34 changes: 34 additions & 0 deletions __tests__/tests/modules/utils/getProp.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/* @flow */

import { Record } from 'immutable';

import * as utils from '../../../../src/modules/utils';

describe('utils.getProp()', () => {
it('gets prop of flat vanilla JS Object', () => {
const data = { attr: 1 };
const keyPath = 'attr';
const value = utils.getProp(data, keyPath);

expect(value).toBe(1);
});


it('gets prop of nested vanilla JS Object', () => {
const data = { nested: { attr: 1 } };
const keyPath = ['nested', 'attr'];
const value = utils.getProp(data, keyPath);

expect(value).toBe(1);
});


it('gets prop of Immutable Record', () => {
const Data = Record({ attr: null });
const data = new Data({ attr: 1 });
const keyPath = 'attr';
const value = utils.getProp(data, keyPath);

expect(value).toBe(1);
});
});
42 changes: 42 additions & 0 deletions __tests__/tests/modules/utils/setProp.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/* @flow */

import { Record } from 'immutable';

import * as utils from '../../../../src/modules/utils';

describe('utils.setProp()', () => {
it('sets prop on flat vanilla JS Object w/o mutation', () => {
const data = { attr: 1 };
const keyPath = 'attr';
const nextValue = 2;
const nextData = utils.setProp(data, keyPath, nextValue);

expect(data.attr).toBe(1);
expect(nextData.attr).toBe(2);
expect(data).not.toBe(nextData);
});


it('sets prop on nested vanilla JS Object w/o mutation', () => {
const data = { nested: { attr: 1 } };
const keyPath = ['nested', 'attr'];
const nextValue = 2;
const nextData = utils.setProp(data, keyPath, nextValue);

expect(data.nested.attr).toBe(1);
expect(nextData.nested.attr).toBe(2);
expect(data).not.toBe(nextData);
});


it('sets prop on Immutable Record', () => {
const Data = Record({ attr: null });
const data = new Data({ attr: 1 });
const keyPath = 'attr';
const nextValue = 2;
const nextData = utils.setProp(data, keyPath, nextValue);

expect(data.attr).toBe(1);
expect(nextData.attr).toBe(2);
});
});
7 changes: 0 additions & 7 deletions __tests__/tests/modules/utils/x.fetchProp.spec.js

This file was deleted.

4 changes: 2 additions & 2 deletions src/ValidationLayer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import {
} from './modules/validations';
import { buildCompleteAsyncValidationResults } from './modules/validations/utils';
import { parseFieldId, parseFieldStateId, buildFieldValidationStateId } from './modules/ids';
import { fetchProp, isFunction } from './modules/utils';
import { getProp, isFunction } from './modules/utils';
import normalizeFieldsFromProps from './modules/normalizeFieldsFromProps';
import normalizeExternalErrors from './modules/normalizeExternalErrors';
import getFieldsPropsState from './modules/getFieldsPropsState';
Expand Down Expand Up @@ -355,7 +355,7 @@ export default class ValidationLayer extends Component {
const { fieldId } = parseFieldStateId(stateId);
const { keyPath } = parseFieldId(fieldId);

const fieldValue = fetchProp(props.data, keyPath);
const fieldValue = getProp(props.data, keyPath);

// While async call is being processed,
// deferred validation state might become obsolete:
Expand Down
4 changes: 2 additions & 2 deletions src/modules/getFieldsPropsState.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import Constant from '../enums/Constant';

import { getFieldValueHandler } from './getFieldHandler';
import { buildFieldDomId, buildFieldPropsStateId } from './ids';
import { fetchProp, normalizeValueForDom } from './utils';
import { getProp, normalizeValueForDom } from './utils';


/**
Expand All @@ -33,7 +33,7 @@ export default function getFieldsPropsState(
const fieldPropsStateId = buildFieldPropsStateId(field.id);

// $FlowIgnoreMe: We're making sure that value at keyPath is not an object on normalization
const fieldValue: Value = fetchProp(data, field.keyPath);
const fieldValue: Value = getProp(data, field.keyPath);
const fieldNormalizedDomValue = normalizeValueForDom(fieldValue);

const transformBeforeRender = getFieldValueHandler(
Expand Down
4 changes: 2 additions & 2 deletions src/modules/normalizeFieldsFromProps.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Constant from '../enums/Constant';
import AsyncStrategy from '../enums/AsyncStrategy';

import { buildFieldId } from './ids';
import { fetchProp, isPlainObject } from './utils';
import { getProp, isPlainObject } from './utils';
import { debounce } from './validations/utils';


Expand All @@ -37,7 +37,7 @@ export default function normalizeFieldsFromProps(
while (++index < keys.length) {
const key = keys[index];
const fieldsBranch = fields[key];
const dataBranch = fetchProp(data, key);
const dataBranch = getProp(data, key);
const keyPath = parentKeyPath.concat(key);

if (isPlainObject(dataBranch)) {
Expand Down
27 changes: 26 additions & 1 deletion src/modules/utils/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
/* @flow */
/* eslint-disable no-use-before-define */

import * as _ from './lodash';

import type { Value } from '../../types';

/**
* @desc If value is `null` or `undefined`,
* then we should pass empty string as a DOM value.
Expand Down Expand Up @@ -34,7 +38,7 @@ export function normalizeKeyPath(
* as well as Immutable structure (e.g. Map).
*
*/
export function fetchProp(
export function getProp(
container: Object, // eslint-disable-line flowtype/no-weak-types
keyPath: string | Array<string>,
) {
Expand All @@ -48,6 +52,27 @@ export function fetchProp(
}


/**
* @desc Sets value of property at provided key path.
* Data container can be vanilla JS Object,
* as well as Immutable structure (e.g. Map).
*
*/
export function setProp(
container: Object, // eslint-disable-line flowtype/no-weak-types
keyPath: string | Array<string>,
value: Value,
) {
const normalizedKeyPath = normalizeKeyPath(keyPath);

return (
container.setIn && isFunction(container.setIn)
? container.setIn(normalizedKeyPath, value)
: _.set(_.cloneDeep(container), keyPath, value)
);
}


export function isDefined<S>(subject: S): boolean {
return typeof subject !== 'undefined';
}
Expand Down
Loading