src/__test__/Form.test.tsx

Summary

Maintainability
D
2 days
Test Coverage
import * as React from 'react';
import {act} from 'react-dom/test-utils';
import {render} from '@testing-library/react';

import * as Form from '../';

import {Esimorp} from './Esimorp';

describe('<Form>', () => {
  it('should return children', () => {
    const result = render(
      <Form.Form values={{}}>
        <form />
      </Form.Form>,
    );

    expect(result.container).toMatchInlineSnapshot(`
      <div>
        <form />
      </div>
    `);
  });

  it('should call validate with initial values on first mount', () => {
    const values = {foo: 'bar'};
    const validate = jest.fn<void, [{foo: any}]>();

    render(<Form.Form values={values} validate={validate} />);

    // Expect validate to be called only once per update.
    expect(validate).toHaveBeenCalledTimes(1);

    // Expect validate to have been called with the correct arguments.
    expect(validate).toHaveBeenLastCalledWith(values);
  });

  it('should call validate with current values when validate is updated', () => {
    const values = {foo: 'bar'};

    const TypedForm = Form as Form.TypedModule<typeof values, void, void, void>;

    const {rerender} = render(<TypedForm.Form values={values} />);

    const validate = jest.fn<void, [{foo: any}]>();

    rerender(<TypedForm.Form values={values} validate={validate} />);

    // Expect incoming validate to be called immediately.
    expect(validate).toHaveBeenCalledTimes(1);

    // Expect validate to have been called with the correct arguments.
    expect(validate).toHaveBeenLastCalledWith(values);
  });

  it('should call validate with updated values when value state changes', () => {
    const values = {foo: 'bar'};

    type ModuleType = Form.TypedModule<typeof values, void, void, void>;

    type FormInterface = Form.InterfaceOf<ModuleType>;

    const {Form: FormComponent} = Form as ModuleType;

    const ref = React.createRef<FormInterface>();

    const validate = jest.fn<void, [{foo: any}]>();

    render(<FormComponent ref={ref} values={values} validate={validate} />);

    expect(validate).toHaveBeenLastCalledWith(values);

    act(() => {
      (ref.current as FormInterface).setValue(['foo'], 'updated');
    });

    // Expect validate to be called only once per state update.
    expect(validate).toHaveBeenCalledTimes(2);

    expect(validate).toHaveBeenLastCalledWith(
      expect.objectContaining({foo: 'updated'}),
    );
  });

  it('should not call validate with when other state changes', () => {
    interface Values {
      foo?: any;
    }

    type ModuleType = Form.TypedModule<Values, void, void, void>;

    type FormInterface = Form.InterfaceOf<ModuleType>;

    const TypedForm = Form as ModuleType;

    const ref = React.createRef<FormInterface>();

    const validate = jest.fn<void, [Values]>();

    render(<TypedForm.Form ref={ref} values={{}} validate={validate} />);

    act(() => {
      (ref.current as FormInterface).focus('foo');
      (ref.current as FormInterface).blur('foo');
    });

    // Expect validate to be called only once.
    expect(validate).toHaveBeenCalledTimes(1);
  });

  it('should not call validate with when the values prop changes', () => {
    const validate = jest.fn<void, [{foo: any}]>();

    const {rerender} = render(
      <Form.Form values={{foo: 'bar'}} validate={validate} />,
    );

    rerender(<Form.Form values={{foo: 'pending'}} validate={validate} />);

    // Expect validate to be called only once.
    expect(validate).toHaveBeenCalledTimes(1);
  });

  it('should call warn with initial values on first mount', () => {
    const values = {foo: 'bar'};
    const warn = jest.fn<void, [{foo: any}]>();

    render(<Form.Form values={values} warn={warn} />);

    // Expect warn to be called only once per update.
    expect(warn).toHaveBeenCalledTimes(1);

    // Expect warn to have been called with the correct arguments.
    expect(warn).toHaveBeenLastCalledWith(values);
  });

  it('should call validate with current values when warn is updated', () => {
    const values = {foo: 'bar'};

    const {rerender} = render(<Form.Form values={values} />);

    const warn = jest.fn<void, [{foo: any}]>();

    rerender(<Form.Form values={values} warn={warn} />);

    // Expect incoming warn to be called immediately.
    expect(warn).toHaveBeenCalledTimes(1);

    // Expect warn to have been called with the correct arguments.
    expect(warn).toHaveBeenLastCalledWith(values);
  });

  it('should call warn with updated values when value state changes', () => {
    const values = {foo: 'bar'};

    type ModuleType = Form.TypedModule<typeof values, void, void, void, void>;
    type FormInterface = Form.InterfaceOf<ModuleType>;
    const TypedForm = Form as ModuleType;

    const ref = React.createRef<FormInterface>();

    const warn = jest.fn<void, [{foo: any}]>();

    render(<TypedForm.Form ref={ref} values={values} warn={warn} />);

    expect(warn).toHaveBeenLastCalledWith(values);

    act(() => {
      (ref.current as FormInterface).setValue(['foo'], 'updated');
    });

    // Expect warn to be called only once per state update.
    expect(warn).toHaveBeenCalledTimes(2);

    expect(warn).toHaveBeenLastCalledWith(
      expect.objectContaining({foo: 'updated'}),
    );
  });

  it('should not call warn with when other state changes', () => {
    type ModuleType = Form.TypedModule<{}, void, void, void, void>;
    type FormInterface = Form.InterfaceOf<ModuleType>;
    const TypedForm = Form as ModuleType;

    const ref = React.createRef<FormInterface>();

    const warn = jest.fn<void, [{}]>();

    render(<TypedForm.Form ref={ref} values={{}} warn={warn} />);

    act(() => {
      (ref.current as FormInterface).focus('foo');
      (ref.current as FormInterface).blur('foo');
    });

    // Expect warn to be called only once.
    expect(warn).toHaveBeenCalledTimes(1);
  });

  it('should not call warn with when the values prop changes', () => {
    const values = {foo: 'bar'};

    type ModuleType = Form.TypedModule<typeof values, void, void, void, void>;

    const TypedForm = Form as ModuleType;

    const warn = jest.fn<void, [{foo: any}]>();

    const {rerender} = render(<TypedForm.Form values={values} warn={warn} />);

    rerender(<TypedForm.Form values={{foo: 'pending'}} warn={warn} />);

    // Expect warn to be called only once.
    expect(warn).toHaveBeenCalledTimes(1);
  });

  it('should call onSubmit with current values', async () => {
    const success = new Esimorp();

    type ModuleType = Form.TypedModule<{}, void>;
    type FormInterface = Form.InterfaceOf<ModuleType>;
    const TypedForm = Form as ModuleType;

    const ref = React.createRef<FormInterface>();

    const values = {};

    const onSubmit = jest.fn<void, [typeof values]>();

    render(
      <TypedForm.Form
        ref={ref}
        values={values}
        onSubmit={onSubmit}
        onSubmitSuccess={() => {
          success.resolve();
        }}
      />,
    );

    act(() => {
      (ref.current as FormInterface).submit();
    });

    await success;

    expect(onSubmit).toHaveBeenCalledTimes(1);

    expect(onSubmit).toHaveBeenLastCalledWith(values);
  });

  it('should call correct handlers handler props on successful submit', async () => {
    const submitResult = {};

    type ModuleType = Form.TypedModule<{}, typeof submitResult>;
    type FormInterface = Form.InterfaceOf<ModuleType>;
    const TypedForm = Form as ModuleType;

    const ref = React.createRef<FormInterface>();

    const success = new Esimorp<typeof submitResult>();

    const onSubmitSuccess = jest.fn(success.resolve);
    const onSubmitFail = jest.fn();

    render(
      <TypedForm.Form
        ref={ref}
        values={{}}
        onSubmit={() => Promise.resolve(submitResult)}
        onSubmitSuccess={onSubmitSuccess}
        onSubmitFail={onSubmitFail}
      />,
    );

    await act(async () => {
      (ref.current as FormInterface).submit();
      await success;
    });

    expect(onSubmitSuccess).toHaveBeenCalledTimes(1);
    expect(onSubmitFail).toHaveBeenCalledTimes(0);

    expect(onSubmitSuccess).toHaveBeenLastCalledWith(submitResult);
  });

  it('should complete submit sequence if the component is unmounted', async () => {
    type ModuleType = Form.TypedModule<{}, void>;
    type FormInterface = Form.InterfaceOf<ModuleType>;
    const TypedForm = Form as ModuleType;

    const ref = React.createRef<FormInterface>();

    const success = new Esimorp<void>();

    const onSubmit = jest.fn();
    const onSubmitSuccess = jest.fn(success.resolve);

    await act(async () => {
      const {unmount} = render(
        <TypedForm.Form
          ref={ref}
          values={{}}
          onSubmit={onSubmit}
          onSubmitSuccess={onSubmitSuccess}
        />,
      );

      const {submit} = ref.current as FormInterface;

      unmount();

      submit();

      await success;
    });

    expect(onSubmit).toHaveBeenCalledTimes(1);
    expect(onSubmitSuccess).toHaveBeenCalledTimes(1);
  });

  it('should prevent default behavior of event passed to submit', async () => {
    type ModuleType = Form.TypedModule<{}, void>;
    type FormInterface = Form.InterfaceOf<ModuleType>;
    const TypedForm = Form as ModuleType;

    const ref = React.createRef<FormInterface>();
    const event = {preventDefault: jest.fn()};

    const success = new Esimorp<void>();

    render(
      <TypedForm.Form
        ref={ref}
        values={{}}
        onSubmitSuccess={success.resolve}
      />,
    );

    await act(async () => {
      (ref.current as FormInterface).submit(event as any);

      await success;
    });

    expect(event.preventDefault).toHaveBeenCalledTimes(1);
  });

  it('should call correct handlers on attempted submit with local validation errors', async () => {
    type ModuleType = Form.TypedModule<{}, void, void, string>;
    type FormInterface = Form.InterfaceOf<ModuleType>;
    const TypedForm = Form as ModuleType;

    const ref = React.createRef<FormInterface>();

    const fail = new Esimorp<void>();

    const onSubmitSuccess = jest.fn();
    const onSubmitFail = jest.fn(() => {
      fail.resolve();
    });

    render(
      <TypedForm.Form
        ref={ref}
        values={{}}
        validate={() => 'errors'}
        onSubmitSuccess={onSubmitSuccess}
        onSubmitFail={onSubmitFail}
      />,
    );

    await act(async () => {
      (ref.current as FormInterface).submit();

      await fail;
    });

    expect(onSubmitSuccess).toHaveBeenCalledTimes(0);
    expect(onSubmitFail).toHaveBeenCalledTimes(1);

    expect(onSubmitFail).toHaveBeenLastCalledWith(
      expect.any(Form.SubmitValidationError),
    );
  });

  it('should call correct handlers on attempted submit with remote validation errors', async () => {
    type ModuleType = Form.TypedModule<{}, void>;
    type FormInterface = Form.InterfaceOf<ModuleType>;
    const TypedForm = Form as ModuleType;

    const ref = React.createRef<FormInterface>();

    const validationError = new Form.SubmitValidationError('errors');

    const fail = new Esimorp<Form.SubmitValidationError>();

    const onSubmitSuccess = jest.fn();
    const onSubmitFail = jest.fn(() => {
      fail.resolve();
    });

    render(
      <TypedForm.Form
        ref={ref}
        values={{}}
        onSubmit={() => Promise.reject(validationError)}
        onSubmitSuccess={onSubmitSuccess}
        onSubmitFail={onSubmitFail}
      />,
    );

    await act(async () => {
      (ref.current as FormInterface).submit();

      await fail;
    });

    expect(onSubmitSuccess).toHaveBeenCalledTimes(0);
    expect(onSubmitFail).toHaveBeenCalledTimes(1);

    expect(onSubmitFail).toHaveBeenLastCalledWith(validationError);
  });

  it('should call correct handlers on attempted submit with empty remote validation error', async () => {
    type ModuleType = Form.TypedModule<{}, void>;
    type FormInterface = Form.InterfaceOf<ModuleType>;
    const TypedForm = Form as ModuleType;

    const ref = React.createRef<FormInterface>();

    const validationError = new Form.SubmitValidationError({});

    const fail = new Esimorp<Form.SubmitValidationError>();

    const onSubmitSuccess = jest.fn();
    const onSubmitFail = jest.fn(() => {
      fail.resolve();
    });

    render(
      <TypedForm.Form
        ref={ref}
        values={{}}
        onSubmit={() => Promise.reject(validationError)}
        onSubmitSuccess={onSubmitSuccess}
        onSubmitFail={onSubmitFail}
      />,
    );

    await act(async () => {
      (ref.current as FormInterface).submit();

      await fail;
    });

    expect(onSubmitSuccess).toHaveBeenCalledTimes(0);
    expect(onSubmitFail).toHaveBeenCalledTimes(1);

    expect(onSubmitFail).toHaveBeenLastCalledWith(validationError);
  });

  it('should call correct handlers on attempted submit with unexpected error', async () => {
    type ModuleType = Form.TypedModule<{}, void>;
    type FormInterface = Form.InterfaceOf<ModuleType>;
    const TypedForm = Form as ModuleType;

    const ref = React.createRef<FormInterface>();

    const unexpectedError = new Error();

    const fail = new Esimorp<Error>();

    const onSubmitSuccess = jest.fn();
    const onSubmitFail = jest.fn(() => {
      fail.resolve();
    });

    render(
      <TypedForm.Form
        ref={ref}
        values={{}}
        onSubmit={() => {
          throw unexpectedError;
        }}
        onSubmitSuccess={onSubmitSuccess}
        onSubmitFail={onSubmitFail}
      />,
    );

    act(() => {
      (ref.current as FormInterface).submit();
    });

    await fail;

    expect(onSubmitSuccess).toHaveBeenCalledTimes(0);
    expect(onSubmitFail).toHaveBeenCalledTimes(1);

    expect(onSubmitFail).toHaveBeenLastCalledWith(unexpectedError);
  });

  it('should call correct handlers on attempted submit with unexpected rejection', async () => {
    type ModuleType = Form.TypedModule<{}, void>;
    type FormInterface = Form.InterfaceOf<ModuleType>;
    const TypedForm = Form as ModuleType;

    const ref = React.createRef<FormInterface>();

    const unexpectedError = new Error();

    const fail = new Esimorp<Error>();

    const onSubmitSuccess = jest.fn();
    const onSubmitFail = jest.fn(() => {
      fail.resolve();
    });

    render(
      <TypedForm.Form
        ref={ref}
        values={{}}
        onSubmit={() => Promise.reject(unexpectedError)}
        onSubmitSuccess={onSubmitSuccess}
        onSubmitFail={onSubmitFail}
      />,
    );

    await act(async () => {
      (ref.current as FormInterface).submit();

      await fail;
    });

    expect(onSubmitSuccess).toHaveBeenCalledTimes(0);
    expect(onSubmitFail).toHaveBeenCalledTimes(1);

    expect(onSubmitFail).toHaveBeenLastCalledWith(unexpectedError);
  });

  it('should call correct handlers on attempted submit if a submit is in progress', async () => {
    type ModuleType = Form.TypedModule<{}, void>;
    type FormInterface = Form.InterfaceOf<ModuleType>;
    const TypedForm = Form as ModuleType;

    const ref = React.createRef<FormInterface>();

    const fail = new Esimorp<Error>();

    const onSubmitSuccess = jest.fn();
    const onSubmitFail = jest.fn(() => {
      fail.resolve();
    });

    render(
      <TypedForm.Form
        ref={ref}
        values={{}}
        onSubmit={() => Promise.resolve()}
        onSubmitSuccess={onSubmitSuccess}
        onSubmitFail={onSubmitFail}
      />,
    );

    act(() => {
      (ref.current as FormInterface).submit();
      (ref.current as FormInterface).submit();
    });

    await act(async () => {
      await fail;
    });

    expect(onSubmitSuccess).toHaveBeenCalledTimes(1);
    expect(onSubmitFail).toHaveBeenCalledTimes(1);

    expect(onSubmitFail).toHaveBeenLastCalledWith(
      expect.any(Form.SubmitConcurrencyError),
    );
  });
});