Repository: jaredpalmer/formik (ts)

Issue:
[Docs] Add testing recipes / guide
Reference: #68, 

### Story:
As user of Formik, I want to know how to test my Formik forms and inputs with popular test frameworks. 

### Rough Outline
- Using Jest snapshots
  - This is a quick way to test `mapPropsToValues`
- Using Enzyme
  - Explain when/how to use `mount` and `shallow`
  - Explain ways to simulate and test `change`, `blur`, and `submit` events
  - Explain how to use `submitForm()` to test async `handleSubmit`'s
  - Add tips about using `data-test-id=".."` attributes
- Using Karma / Selenium / Nightmare / Headless Chrome idk??
  - (may need help with this)


Pull request description:
Add testing recipes to docs
Closes #65 
Related #50 

This is work in progress. ... Right now it's basically a recipe, but ideally this section should go into detail explaining the big gotcha: why promises inside of Enzyme's sync `simulate()`  will be swallowed (see due to Enzyme's  https://github.com/airbnb/enzyme/issues/823). 

This is going to result in confusion to newcomers when testing updates to inner form ui because the results of an async `validate` or `validationSchema` will neither resolve nor seem to fully update their UI intuitively. More specifically... 

- Using `simulate('submit', {..})` on an inner form component with an async `validate` function or `validationSchema` Formik option can only test updates as far [as this `setState` call](https://github.com/jaredpalmer/formik/blob/master/src/formik.tsx#L392). 

- Depending on a form's `validateOnChange` or `validateOnBlur` options (defaults are `false` and `true` respectively), either the `simulate('change')` or `simulate('blur')` will have a similar issue with testing UI updates related to `errors`. Synchronous calls to `setState()` avoid this issue entirely and so testing changes to `values` and `touched` via `simulate()` do not have this problem.

Note: The above does not impact form's that have synchronous `validate` functions.

The suggested approach expose helpers that allow various events to be tested outside an event callback.

From Jordan Harband of Airbnb:

> No, there's no way to await the outcome of an event listener. Separately, `this.setState` is async and takes a callback, so for your promise to work properly it'd need to pass a callback and resolve the wrapping new Promise when `setState` is done....**What I'd recommend is having your event listener call into an instance method; spy on the instance method to test the event listener, and then unit-test the instance method.**
>  Source: https://github.com/airbnb/enzyme/issues/823#issuecomment-282213559 (emphasis added)


- [x] Recipe for `submitForm()`
- [ ] Recipe for testing `validationSchema`

Want to open this up for comment about the best approach to documenting all of this...

Patch:
diff --git a/README.md b/README.md
index 2d3439c..e259930 100644
--- a/README.md
+++ b/README.md
@@ -46,6 +46,7 @@ You can also try before you buy with this **[demo of Formik on CodeSandbox.io](h
       - [`displayName?: string`](#displayname-string)
       - [`handleSubmit: (values: Values, formikBag: FormikBag) => void`](#handlesubmit-values-values-formikbag-formikbag--void)
         - [The "FormikBag":](#the-formikbag)
+      - [`isInitialValid?: boolean | (props: Props) => boolean`](#isinitialvalid-boolean--props-props--boolean)
       - [`mapPropsToValues?: (props: Props) => Values`](#mappropstovalues-props-props--values)
       - [`validate?: (values: Values, props: Props) => FormikError<Values> | Promise<any>`](#validate-values-values-props-props--formikerrorvalues--promiseany)
       - [`validateOnBlur?: boolean`](#validateonblur-boolean)
@@ -59,6 +60,7 @@ You can also try before you buy with this **[demo of Formik on CodeSandbox.io](h
       - [`handleReset: () => void`](#handlereset---void)
       - [`handleSubmit: (e: React.FormEvent<HTMLFormEvent>) => void`](#handlesubmit-e-reactformeventhtmlformevent--void)
       - [`isSubmitting: boolean`](#issubmitting-boolean)
+      - [`isValid: boolean`](#isvalid-boolean)
       - [`resetForm: (nextProps?: Props) => void`](#resetform-nextprops-props--void)
       - [`setErrors: (fields: { [field: string]: string }) => void`](#seterrors-fields--field-string-string---void)
       - [`setFieldError: (field: string, errorMsg: string) => void`](#setfielderror-field-string-errormsg-string--void)
@@ -78,6 +80,10 @@ You can also try before you buy with this **[demo of Formik on CodeSandbox.io](h
   - [React Native](#react-native)
     - [Why use `setFieldValue` instead of `handleChange`?](#why-use-setfieldvalue-instead-of-handlechange)
     - [Avoiding a Render Callback](#avoiding-a-render-callback)
+  - [Testing Formik](#testing-formik)
+    - [Dummy Form](#dummy-form)
+    - [Simulating input](#simulating-input)
+    - [Simulating form submission](#simulating-form-submission)
 - [Authors](#authors)
 
 <!-- END doctoc generated TOC please keep comment here to allow auto update -->
@@ -255,6 +261,10 @@ Your form submission handler. It is passed your forms [`values`] and the "Formik
 
 Note: [`errors`], [`touched`], [`status`] and all event handlers are NOT included in the `FormikBag`.
 
+##### `isInitialValid?: boolean | (props: Props) => boolean`
+
+Default is `false`. Control the initial value of [`isValid`] prop prior to mount. You can also pass a function. Useful for situations when you want to enable/disable a submit and reset buttons on initial mount.
+
 ##### `mapPropsToValues?: (props: Props) => Values`
 
 If this option is specified, then Formik will transfer its results into updatable form state and make these values available to the new component as [`props.values`][`values`]. If `mapPropsToValues` is not specified, then Formik will map all props that are not functions to the inner component's [`props.values`][`values`]. That is, if you omit it, Formik will only pass `props` where `typeof props[k] !== 'function'`, where `k` is some key. 
@@ -353,6 +363,10 @@ Submit handler. This should be passed to `<form onSubmit={handleSubmit}>...</for
 ##### `isSubmitting: boolean`
 Submitting state. Either `true` or `false`. Formik will set this to `true` on your behalf before calling [`handleSubmit`] to reduce boilerplate.
 
+##### `isValid: boolean` 
+
+Returns `true` if the there are no [`errors`], or the result of [`isInitialValid`] the form if is in "pristine" condition (i.e. not [`dirty`])).
+
 ##### `resetForm: (nextProps?: Props) => void`
 Imperatively reset the form. This will clear [`errors`] and [`touched`], set [`isSubmitting`] to `false` and rerun `mapPropsToValues` with the current `WrappedComponent`'s `props` or what's passed as an argument. That latter is useful for calling `resetForm` within `componentWillReceiveProps`.
 
@@ -656,7 +670,170 @@ export const MyReactNativeForm: React.SFC<InjectedFormikProps<Props, Values>> =
 export default Formik<Props, Values>({ ... })(MyReactNativeForm)
 ```
 
+### Testing Formik
+
+_This section is a work in progress._
+
+The suggested approach to testing Formik forms is with Airbnb's [Enzyme](https://github.com/airbnb/enzyme) test utility library.
+
+The documentation and examples in this guide use Facebook's [Jest](https://facebook.github.io/jest) test runner. However, feel free to use [mocha](https://mochajs.org/) and [chai](http://chaijs.com/) if you prefer that.
+
+To get started with Enzyme, you can simply install it with npm:
+
+```bash
+npm i  enzyme --save-dev
+```
+
+If you are using React >=15.5, in addition to enzyme, you will have to ensure that you also have the following npm modules installed if they were not already:
+
+```bash
+npm i react-test-renderer react-dom  --save-dev
+```
+
+####  Dummy Form
+Imagine we have a basic form with one field `name`.
+
+```js
+// MyForm.js
+import { Formik, yupToFormError } from 'formik'
+import Yup from 'yup'
+
+export const validationSchema = Yup.object().shape({
+	name: Yup
+		.string()
+		.min(2, 'Must be longer than 2 characters')
+		.max(30, 'No one\'s name is that long')
+		.required('Required')
+})
+
+export const handleSubmit(values, { setSubmitting }) => {
+	 setTimeout(() => {
+      setSubmitting(false)
+    }, 1000)
+}
+
+export const mapPropsToValues =  props => ({ name: '' })
+
+
+export const MyFormInner = ({
+  values,
+  handleSubmit,
+  handleChange,
+  handleBlur,
+  setStatus,
+  status,
+  errors,
+  isSubmitting,
+}) => {
+  return (
+    <form onSubmit={handleSubmit}>
+      <input
+        type="text"
+        onChange={handleChange}
+        onBlur={handleBlur}
+        value={values.name}
+        name="name"
+      />
+      {errors.name &&
+        <div id="feedback">
+          {errors.name}
+        </div>}
+      {isSubmitting && <div id="submitting">Submitting</div>}
+      {status &&
+        !!status.myStatusMessage &&
+        <div id="status">
+          {status.myStatusMessage}
+        </div>}
+      <button type="submit">Submit</button>
+    </form>
+  );
+};
+
+export default Formik({
+  mapPropsToValues,
+  validationSchema,
+  handleSubmit
+})(MyFormInner)
+```
+
+#### Simulating input
+
+We can test that our UI is updating properly by using Enzyme's `shallow` renderer in addition to its `dive()` and `simulate()` methods. This lets us render the Formik-enhanced form, but then jump down and run simulations and assertions from the perspective of your inner form. 
+
+```js
+// MyForm.test.js
+import MyForm, { MyInnerForm } from './MyForm'
+
+describe('MyForm', () => {
+  test('should update an input when it is changed', () => {
+    const tree = shallow(<MyForm />);
+
+    tree.find(MyInnerForm).dive().find('input').simulate('change', {
+      // you must add this next line as (Formik calls e.persist() internally)
+      persist: () => {},
+      // simulate changing e.target.name and e.target.value
+      target: {
+        name: 'name',
+        value: 'ian',
+      },
+    });
+
+    const newValue = tree.find(MyInnerForm).dive().find('input').props().value;
+
+    expect(newValue).toEqual('ian');
+  });
+});
+
+```
+
+#### Simulating form submission
 
+```js
+// MyForm.test.js
+import MyForm, { MyInnerForm, validationSchema } from './MyForm'
+
+describe('MyForm', () => {
+	
+	test('submits the form', () => {
+	   const tree = shallow(<MyForm />)
+	   expect(tree.find(MyInnerForm).dive().find('#submitting')).toHaveLength(0);      
+	    
+		// simulate submit event. this is always sync! async calls to setState are swallowed. 
+		// be careful of false positives
+		tree.find(MyInnerForm).dive().find('form').simulate('submit', {
+		  preventDefault: () => {} // no op 
+		});
+		
+    // Because the simulated event is 100% sync, we can use it to test the synchronous changes
+    // here. Any async stuff you do inside handleSubmit will be swallowed. Thus our UI
+    // will see the following changes:
+    // - isSubmitting -> true (even if you set it to false asynchronously in your handleSubmit)
+    // - touched: all fields
+		expect(tree.find(Form).dive().find('#submitting')).toHaveLength(1);
+		expect(tree.find(Form).dive().find('button[type="submit"]').props().disabled).toBe(true)
+	})
+	
+	test('what happens when the form is submitted', async () => {
+	   const tree = shallow(<MyForm />)
+	   
+	   expect(tree.find(MyInnerForm).dive().find('#submitting')).toHaveLength(0);      
+	   
+	   await mockCallsToMyApi()
+	   await tree.find(MyInnerForm).props().submitForm()
+		
+		// check that ui has completely updated
+		expect(tree.find(MyInnerForm).update().dive().find('#submitting')).toHaveLength(0);
+		expect(tree.find(MyInnerForm).update().dive().find('#status').text).toEqual('Success!')
+		expect(tree.find(MyInnerForm).update().dive().find('button[type="submit"]').props().disabled).toBe(false)
+		
+		// check that props have updated
+		expect(tree.find(MyInnerForm).props().status).toEqual({ myStatusMessage: 'Success!' })
+		expect(tree.find(MyInnerForm).props().errors).toEqual({})
+		expect(tree.find(MyInnerForm).props().touched).toEqual({ name: true }) // submit will touch all fields
+	})
+
+})
+```
 
 ## Authors
 
@@ -669,6 +846,7 @@ MIT License.
 [`displayName`]: #displayname-string
 [`handleSubmit`]: #handlesubmit-payload-formikbag--void
 [`FormikBag`]: #the-formikbag
+[`isInitialValid`]: #isinitialvalid-boolean--props-props--boolean
 [`mapPropsToValues`]: #mappropstovalues-props--props
 [`validate`]: #validate-values-values-props-props--formikerrorvalues--promiseany
 [`validateOnBlur`]: #validateonblur-boolean
@@ -684,6 +862,7 @@ MIT License.
 [`handleReset`]: #handlereset---void
 [`handleSubmit`]: #handlesubmit-e-reactformeventhtmlformevent--void
 [`isSubmitting`]: #issubmitting-boolean
+[`isValid`]: #isvalid-boolean
 [`resetForm`]: #resetform-nextprops-props--void
 [`setErrors`]: #seterrors-fields--field-string-string---void
 [`setFieldError`]: #setfielderror-field-string-errormsg-string--void
diff --git a/package.json b/package.json
index 7fe0cdb..c08d9d1 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "formik",
   "description": "Forms in React, without tears",
-  "version": "0.8.2",
+  "version": "0.8.6",
   "license": "MIT",
   "author": "Jared Palmer <jared@palmer.net>",
   "repository": "jaredpalmer/formik",
diff --git a/src/formik.tsx b/src/formik.tsx
index 5ca2e51..5be202c 100644
--- a/src/formik.tsx
+++ b/src/formik.tsx
@@ -1,6 +1,6 @@
 import * as React from 'react';
 
-import { isPromise, isReactNative } from './utils';
+import { isFunction, isPromise, isReactNative, values } from './utils';
 
 import { hoistNonReactStatics } from './hoistStatics';
 
@@ -61,6 +61,8 @@ export interface FormikConfig<
   validateOnChange?: boolean;
   /** Tells Formik to validate the form on each input's onBlur event */
   validateOnBlur?: boolean;
+  /** Tell Formik if initial form values are valid or not on first render */
+  isInitialValid?: boolean | ((props: Props) => boolean | undefined);
 }
 
 /**
@@ -90,6 +92,8 @@ export interface FormikState<Values> {
 export interface FormikComputedProps {
   /** True if any input has been touched. False otherwise. */
   readonly dirty: boolean;
+  /** Result of isInitiallyValid on mount, then whether true values pass validation. */
+  readonly isValid: boolean;
 }
 
 /**
@@ -187,6 +191,7 @@ export function Formik<Props, Values extends FormikValues, Payload = Values>({
   handleSubmit,
   validateOnChange = false,
   validateOnBlur = true,
+  isInitialValid = false,
 }: FormikConfig<Props, Values, Payload>): ComponentDecorator<
   Props,
   InjectedFormikProps<Props, Values>
@@ -273,10 +278,14 @@ export function Formik<Props, Values extends FormikValues, Payload = Values>({
        * Run validations and update state accordingly
        */
       runValidations = (values: Values) => {
+        if (validationSchema) {
+          this.runValidationSchema(values);
+        }
+
         if (validate) {
           const maybePromisedErrors = validate(values, this.props);
           if (isPromise(maybePromisedErrors)) {
-            (validate(values, this.props) as Promise<any>).then(
+            (maybePromisedErrors as Promise<any>).then(
               () => {
                 this.setState({ errors: {} });
               },
@@ -286,10 +295,6 @@ export function Formik<Props, Values extends FormikValues, Payload = Values>({
             this.setErrors(maybePromisedErrors as FormikErrors<Values>);
           }
         }
-
-        if (validationSchema) {
-          this.runValidationSchema(values);
-        }
       };
 
       handleChange = (e: React.ChangeEvent<any>) => {
@@ -398,7 +403,7 @@ Formik cannot determine which value to update. For more info see https://github.
           const maybePromisedErrors =
             validate(this.state.values, this.props) || {};
           if (isPromise(maybePromisedErrors)) {
-            (validate(this.state.values, this.props) as Promise<any>).then(
+            (maybePromisedErrors as Promise<any>).then(
               () => {
                 this.setState({ errors: {} });
                 this.executeSubmit();
@@ -407,13 +412,14 @@ Formik cannot determine which value to update. For more info see https://github.
             );
             return;
           } else {
+            const isValid = Object.keys(maybePromisedErrors).length === 0;
             this.setState({
               errors: maybePromisedErrors as FormikErrors<Values>,
-              isSubmitting: Object.keys(maybePromisedErrors).length > 0,
+              isSubmitting: isValid,
             });
 
             // only submit if there are no errors
-            if (Object.keys(maybePromisedErrors).length === 0) {
+            if (isValid) {
               this.executeSubmit();
             }
           }
@@ -495,6 +501,7 @@ Formik cannot determine which value to update. For more info see https://github.
           errors: {},
           touched: {},
           error: undefined,
+          status: undefined,
           values: nextProps
             ? mapPropsToValues(nextProps)
             : mapPropsToValues(this.props),
@@ -507,16 +514,26 @@ Formik cannot determine which value to update. For more info see https://github.
           errors: {},
           touched: {},
           error: undefined,
+          status: undefined,
           values: mapPropsToValues(this.props),
         });
       };
 
       render() {
+        const dirty =
+          values<boolean>(this.state.touched).filter(Boolean).length > 0;
         return (
           <WrappedComponent
             {...this.props}
             {...this.state}
-            dirty={Object.values(this.state.touched).filter(Boolean).length > 0}
+            dirty={dirty}
+            isValid={
+              dirty
+                ? this.state.errors && Object.keys(this.state.errors).length > 0
+                : isInitialValid !== false && isFunction(isInitialValid)
+                  ? (isInitialValid as (props: Props) => boolean)(this.props)
+                  : isInitialValid as boolean
+            }
             setStatus={this.setStatus}
             setError={this.setError}
             setFieldError={this.setFieldError}
diff --git a/src/utils.ts b/src/utils.ts
index 5740840..db8b5e0 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -1,9 +1,4 @@
-/**
- * Returns whether something is a Promise or not.
- * @param value anything
- * 
- * @see https://github.com/pburtchaell/redux-promise-middleware/blob/master/src/isPromise.js
- */
+/** @private is the given object/value a promise? */
 export function isPromise(value: any): boolean {
   if (value !== null && typeof value === 'object') {
     return value && typeof value.then === 'function';
@@ -12,11 +7,23 @@ export function isPromise(value: any): boolean {
   return false;
 }
 
-/**
- * True if running in React Native. 
- */
+/** @private is running React Native?  */
 export const isReactNative =
   typeof window !== 'undefined' &&
   window.navigator &&
   window.navigator.product &&
   window.navigator.product === 'ReactNative';
+
+/** @private Returns values of an object in a new array */
+export function values<T>(obj: any): T[] {
+  const vals = [];
+  for (var key in obj) {
+    if (obj.hasOwnProperty(key)) {
+      vals.push(obj[key]);
+    }
+  }
+  return vals;
+}
+
+/** @private is the given object a Function? */
+export const isFunction = (obj: any) => 'function' === typeof obj;
diff --git a/tsconfig.json b/tsconfig.json
index c463794..2e47183 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -7,7 +7,7 @@
     "experimentalDecorators": false,
     "importHelpers": true,
     "jsx": "react",
-    "lib": ["es2015", "es2017.object", "dom"],
+    "lib": ["es2015", "dom"],
     "module": "es2015",
     "moduleResolution": "node",
     "noFallthroughCasesInSwitch": true,


Test patch:
diff --git a/test/formik.test.tsx b/test/formik.test.tsx
index 43484c9..014f29e 100644
--- a/test/formik.test.tsx
+++ b/test/formik.test.tsx
@@ -79,6 +79,35 @@ describe('Formik', () => {
     expect(tree.find(Form).props().values).toEqual({ name: 'jared' });
     expect(tree.find(Form).props().errors).toEqual({});
     expect(tree.find(Form).props().dirty).toBe(false);
+    expect(tree.find(Form).props().isValid).toBe(false);
+  });
+
+  it('should compute isValid if isInitialValid is present and returns true', () => {
+    const InvalidForm = FormFactory({ isInitialValid: props => true });
+    const tree = shallow(<InvalidForm user={{ name: 'jared' }} />);
+    expect(tree.find(Form).props().dirty).toBe(false);
+    expect(tree.find(Form).props().isValid).toBe(true);
+  });
+
+  it('should compute isValid if isInitialValid is present and returns false', () => {
+    const InvalidForm = FormFactory({ isInitialValid: props => false });
+    const tree = shallow(<InvalidForm user={{ name: 'jared' }} />);
+    expect(tree.find(Form).props().dirty).toBe(false);
+    expect(tree.find(Form).props().isValid).toBe(false);
+  });
+
+  it('should compute isValid if isInitialValid boolean is present and set to true', () => {
+    const InvalidForm = FormFactory({ isInitialValid: true });
+    const tree = shallow(<InvalidForm user={{ name: 'jared' }} />);
+    expect(tree.find(Form).props().dirty).toBe(false);
+    expect(tree.find(Form).props().isValid).toBe(true);
+  });
+
+  it('should compute isValid if isInitialValid is present and set to false', () => {
+    const InvalidForm = FormFactory({ isInitialValid: false });
+    const tree = shallow(<InvalidForm user={{ name: 'jared' }} />);
+    expect(tree.find(Form).props().dirty).toBe(false);
+    expect(tree.find(Form).props().isValid).toBe(false);
   });
 
   describe('FormikHandlers', () => {