repo_description: 🏁 High performance subscription-based form state management for React
repo_topics: asynchronous form form-validation forms react state-management subscription validation
code: import React from "react";
  import { render, fireEvent, cleanup } from "@testing-library/react";
  import "@testing-library/jest-dom";
  import { ErrorBoundary, Toggle, wrapWith } from "./testUtils";
  import Form from "./ReactFinalForm";
  import Field from "./Field";
  import FormSpy from "./FormSpy";
  
  const onSubmitMock = (_values) => {};
  const hasFormApi = (props) => {
    expect(props.form).toBeDefined();
    expect(typeof props.form.batch).toBe("function");
    expect(typeof props.form.blur).toBe("function");
    expect(typeof props.form.change).toBe("function");
    expect(typeof props.form.focus).toBe("function");
    expect(typeof props.form.initialize).toBe("function");
    expect(typeof props.form.reset).toBe("function");
  };
  
  describe("FormSpy", () => {
    afterEach(cleanup);
  
    it("should warn if not used inside a form", () => {
      jest.spyOn(console, "error").mockImplementation(() => {});
      const errorSpy = jest.fn();
      render(
        <ErrorBoundary spy={errorSpy}>
          <FormSpy render={() => <div />} />
        </ErrorBoundary>,
      );
      expect(errorSpy).toHaveBeenCalled();
      expect(errorSpy).toHaveBeenCalledTimes(1);
      expect(errorSpy.mock.calls[0][0].message).toBe(
        "FormSpy must be used inside of a <Form> component",
      );
      console.error.mockRestore();
    });
  
    it("should allow subscribing to everything", () => {
      const spy = jest.fn();
      const { getByTestId } = render(
        <Form onSubmit={onSubmitMock}>
          {() => (
            <form>
              <Field name="name" component="input" data-testid="name" />
              <FormSpy
                render={wrapWith(spy, () => (
                  <div />
                ))}
              />
            </form>
          )}
        </Form>,
      );
      expect(spy).toHaveBeenCalled();
      // All forms without restricted subscriptions render twice at first because they
      // need to update their validation and touched/modified/visited maps every time
      // new fields are registered.
      expect(spy).toHaveBeenCalledTimes(2);
      hasFormApi(spy.mock.calls[0][0]);
      expect(spy.mock.calls[0][0].dirty).toBe(false);
      expect(spy.mock.calls[0][0].errors).toEqual({});
      expect(spy.mock.calls[0][0].invalid).toBe(false);
      expect(spy.mock.calls[0][0].pristine).toBe(true);
      expect(spy.mock.calls[0][0].submitFailed).toBe(false);
      expect(spy.mock.calls[0][0].submitSucceeded).toBe(false);
      expect(spy.mock.calls[0][0].submitting).toBe(false);
      expect(spy.mock.calls[0][0].valid).toBe(true);
      expect(spy.mock.calls[0][0].validating).toBe(false);
      expect(spy.mock.calls[0][0].values).toEqual({});
      hasFormApi(spy.mock.calls[1][0]);
      expect(spy.mock.calls[1][0].dirty).toBe(false);
      expect(spy.mock.calls[1][0].errors).toEqual({});
      expect(spy.mock.calls[1][0].invalid).toBe(false);
      expect(spy.mock.calls[1][0].pristine).toBe(true);
      expect(spy.mock.calls[1][0].submitFailed).toBe(false);
      expect(spy.mock.calls[1][0].submitSucceeded).toBe(false);
      expect(spy.mock.calls[1][0].submitting).toBe(false);
      expect(spy.mock.calls[1][0].valid).toBe(true);
      expect(spy.mock.calls[1][0].validating).toBe(false);
      expect(spy.mock.calls[1][0].values).toEqual({});
  
      // change value
      fireEvent.change(getByTestId("name"), { target: { value: "erikras" } });
  
      expect(spy).toHaveBeenCalledTimes(3);
      hasFormApi(spy.mock.calls[2][0]);
      expect(spy.mock.calls[2][0].dirty).toBe(true);
      expect(spy.mock.calls[2][0].errors).toEqual({});
      expect(spy.mock.calls[2][0].invalid).toBe(false);
      expect(spy.mock.calls[2][0].pristine).toBe(false);
      expect(spy.mock.calls[2][0].submitFailed).toBe(false);
      expect(spy.mock.calls[2][0].submitSucceeded).toBe(false);
      expect(spy.mock.calls[2][0].submitting).toBe(false);
      expect(spy.mock.calls[2][0].valid).toBe(true);
      expect(spy.mock.calls[2][0].validating).toBe(false);
      expect(spy.mock.calls[2][0].values).toEqual({ name: "erikras" });
    });
  
    it("should NOT resubscribe if subscription changes", () => {
      const firstSubscription = { values: true, pristine: true };
      const secondSubscription = { dirty: true, submitting: true };
      const spy = jest.fn();
      const { getByText } = render(
        <Toggle>
          {(useAlternateSubscription) => (
            <Form
              onSubmit={onSubmitMock}
              initialValues={{ dog: "Odie", cat: "Garfield" }}
            >
              {() => (
                <form>
                  <Field name="dog" component="input" />
                  <Field name="cat" component="input" />
                  <FormSpy
                    subscription={
                      useAlternateSubscription
                        ? secondSubscription
                        : firstSubscription
                    }
                  >
                    {wrapWith(spy, (props) => (
                      <div />
                    ))}
                  </FormSpy>
                </form>
              )}
            </Form>
          )}
        </Toggle>,
      );
      expect(spy).toHaveBeenCalled();
      // All forms without restricted subscriptions render twice at first because they
      // need to update their validation and touched/modified/visited maps every time
      // new fields are registered.
      expect(spy).toHaveBeenCalledTimes(2);
      hasFormApi(spy.mock.calls[0][0]);
      expect(spy.mock.calls[0][0].dirty).toBe(false);
      expect(spy.mock.calls[0][0].errors).toEqual({});
      expect(spy.mock.calls[0][0].invalid).toBe(false);
      expect(spy.mock.calls[0][0].pristine).toBe(true);
      expect(spy.mock.calls[0][0].submitFailed).toBe(false);
      expect(spy.mock.calls[0][0].submitSucceeded).toBe(false);
      expect(spy.mock.calls[0][0].submitting).toBe(false);
      expect(spy.mock.calls[0][0].valid).toBe(true);
      expect(spy.mock.calls[0][0].validating).toBe(false);
      expect(spy.mock.calls[0][0].values).toEqual({
        dog: "Odie",
        cat: "Garfield",
      });
      // Reinstate check for spy.mock.calls[1][0] as there are two initial calls
      hasFormApi(spy.mock.calls[1][0]);
      expect(spy.mock.calls[1][0].dirty).toBeUndefined();
      expect(spy.mock.calls[1][0].errors).toBeUndefined();
      expect(spy.mock.calls[1][0].invalid).toBeUndefined();
      expect(spy.mock.calls[1][0].pristine).toBe(true);
      expect(spy.mock.calls[1][0].submitFailed).toBeUndefined();
      expect(spy.mock.calls[1][0].submitSucceeded).toBeUndefined();
      expect(spy.mock.calls[1][0].submitting).toBeUndefined();
      expect(spy.mock.calls[1][0].valid).toBeUndefined();
      expect(spy.mock.calls[1][0].validating).toBeUndefined();
      expect(spy.mock.calls[1][0].values).toEqual({
        dog: "Odie",
        cat: "Garfield",
      });
  
      fireEvent.click(getByText("Toggle"));
  
      // If FormSpy correctly does not re-render for subscription prop change alone,
      // count should remain 3.
      expect(spy).toHaveBeenCalledTimes(3);
    });
  
    it("should hear changes", () => {
      const spy = jest.fn();
      const { getByTestId } = render(
        <Form onSubmit={onSubmitMock}>
          {() => (
            <form>
              <Field name="name" component="input" data-testid="name" />
              <FormSpy subscription={{ dirty: true, values: true }}>
                {wrapWith(spy, (props) => (
                  <div />
                ))}
              </FormSpy>
            </form>
          )}
        </Form>,
      );
      expect(spy).toHaveBeenCalled();
      // All forms without restricted subscriptions render twice at first because they
      // need to update their validation and touched/modified/visited maps every time
      // new fields are registered.
      expect(spy).toHaveBeenCalledTimes(2);
      hasFormApi(spy.mock.calls[0][0]);
      expect(spy.mock.calls[0][0].dirty).toBe(false);
      expect(spy.mock.calls[0][0].errors).toEqual({});
      expect(spy.mock.calls[0][0].invalid).toBe(false);
      expect(spy.mock.calls[0][0].pristine).toBe(true);
      expect(spy.mock.calls[0][0].submitFailed).toBe(false);
      expect(spy.mock.calls[0][0].submitSucceeded).toBe(false);
      expect(spy.mock.calls[0][0].submitting).toBe(false);
      expect(spy.mock.calls[0][0].valid).toBe(true);
      expect(spy.mock.calls[0][0].values).toEqual({}); // Default/empty for first call
  
      // Check second initial call
      hasFormApi(spy.mock.calls[1][0]);
      expect(spy.mock.calls[1][0].dirty).toBe(false);
      expect(spy.mock.calls[1][0].errors).toBeUndefined();
      expect(spy.mock.calls[1][0].invalid).toBeUndefined();
      expect(spy.mock.calls[1][0].pristine).toBeUndefined();
      expect(spy.mock.calls[1][0].values).toEqual({});
  
      fireEvent.change(getByTestId("name"), { target: { value: "bob" } });
  
      expect(spy).toHaveBeenCalledTimes(3); // Called again for the change
      hasFormApi(spy.mock.calls[2][0]);
      expect(spy.mock.calls[2][0].dirty).toBe(true);
      expect(spy.mock.calls[2][0].errors).toBeUndefined();
      expect(spy.mock.calls[2][0].invalid).toBeUndefined();
      expect(spy.mock.calls[2][0].pristine).toBeUndefined();
      expect(spy.mock.calls[2][0].submitFailed).toBeUndefined();
      expect(spy.mock.calls[2][0].submitSucceeded).toBeUndefined();
      expect(spy.mock.calls[2][0].submitting).toBeUndefined();
      expect(spy.mock.calls[2][0].valid).toBeUndefined();
      expect(spy.mock.calls[2][0].values).toEqual({ name: "bob" });
    });
  
    it("should unsubscribe on unmount", () => {
      // This is mainly here for code coverage. 🧐
      const spy = jest.fn();
      const { getByText } = render(
        <Toggle>
          {(hidden) => (
            <Form onSubmit={onSubmitMock}>
              {() => (
                <form>
                  <Field name="name" component="input" />
                  {!hidden && (
                    <FormSpy>
                      {wrapWith(spy, (props) => (
                        <div />
                      ))}
                    </FormSpy>
                  )}
                </form>
              )}
            </Form>
          )}
        </Toggle>,
      );
      expect(spy).toHaveBeenCalled();
      // All forms without restricted subscriptions render twice at first because they
      // need to update their validation and touched/modified/visited maps every time
      // new fields are registered.
      expect(spy).toHaveBeenCalledTimes(2);
      fireEvent.click(getByText("Toggle"));
      expect(spy).toHaveBeenCalledTimes(2);
    });
  
    it("should call onChange", () => {
      const spy = jest.fn();
      const { getByTestId } = render(
        <Form onSubmit={onSubmitMock}>
          {() => (
            <form>
              <Field name="name" component="input" data-testid="name" />
              <FormSpy
                subscription={{ dirty: true, values: true }}
                onChange={spy}
              />
            </form>
          )}
        </Form>,
      );
      expect(spy).toHaveBeenCalled();
      expect(spy).toHaveBeenCalledTimes(1);
      expect(spy.mock.calls[0][0].dirty).toBe(false);
      expect(spy.mock.calls[0][0].errors).toBeUndefined();
      expect(spy.mock.calls[0][0].invalid).toBeUndefined();
      expect(spy.mock.calls[0][0].pristine).toBeUndefined();
      expect(spy.mock.calls[0][0].submitFailed).toBeUndefined();
      expect(spy.mock.calls[0][0].submitSucceeded).toBeUndefined();
      expect(spy.mock.calls[0][0].submitting).toBeUndefined();
      expect(spy.mock.calls[0][0].valid).toBeUndefined();
      expect(spy.mock.calls[0][0].validating).toBeUndefined();
      expect(spy.mock.calls[0][0].values).toEqual({});
  
      fireEvent.change(getByTestId("name"), { target: { value: "erikras" } });
  
      expect(spy).toHaveBeenCalledTimes(2);
      expect(spy.mock.calls[1][0].dirty).toBe(true);
      expect(spy.mock.calls[1][0].values).toEqual({ name: "erikras" });
    });
  
    it("should not rerender when onChange changes, but SHOULD use latest onChange passed", () => {
      const spy = jest.fn();
      const { getByTestId } = render(
        <Form onSubmit={onSubmitMock}>
          {({ values }) => (
            <form>
              <Field name="name" component="input" data-testid="name" />
              <FormSpy
                subscription={{ values: true }}
                onChange={(formState) => {
                  spy(values.name, formState.values.name);
                }}
              />
            </form>
          )}
        </Form>,
      );
      expect(spy).toHaveBeenCalled();
      expect(spy).toHaveBeenCalledTimes(1);
      expect(spy.mock.calls[0]).toEqual([undefined, undefined]);
  
      fireEvent.change(getByTestId("name"), { target: { value: "erikras" } });
  
      expect(spy).toHaveBeenCalledTimes(2);
      expect(spy.mock.calls[1]).toEqual(["erikras", "erikras"]);
  
      fireEvent.change(getByTestId("name"), {
        target: { value: "erikras rulez" },
      });
  
      expect(spy).toHaveBeenCalledTimes(3);
      expect(spy.mock.calls[2]).toEqual(["erikras rulez", "erikras rulez"]);
    });
  
    it("should not render with render prop when given onChange", () => {
      const spy = jest.fn();
      const renderSpy = jest.fn();
      render(
        <Form onSubmit={onSubmitMock}>
          {() => (
            <form>
              <Field name="name" component="input" data-testid="name" />
              <FormSpy
                subscription={{ dirty: true, values: true }}
                onChange={spy}
                render={renderSpy}
              />
            </form>
          )}
        </Form>,
      );
      expect(spy).toHaveBeenCalled();
      expect(spy).toHaveBeenCalledTimes(1);
      expect(renderSpy).not.toHaveBeenCalled();
    });
  
    it("should ignore SyntheticEvents on form reset ", () => {
      const { getByTestId, getByText } = render(
        <Form
          onSubmit={onSubmitMock}
          subscription={{}}
          initialValues={{ name: "erikras" }}
        >
          {() => (
            <form>
              <Field name="name" component="input" data-testid="name" />
              <FormSpy subscription={{}}>
                {({ form }) => (
                  <button type="button" onClick={form.reset}>
                    Reset
                  </button>
                )}
              </FormSpy>
            </form>
          )}
        </Form>,
      );
      expect(getByTestId("name").value).toBe("erikras");
      fireEvent.change(getByTestId("name"), {
        target: { value: "erikrasmussen" },
      });
      expect(getByTestId("name").value).toBe("erikrasmussen");
      fireEvent.click(getByText("Reset"));
      expect(getByTestId("name").value).toBe("erikras");
    });
  
    it("should accept new initial values on form reset ", () => {
      const { getByTestId, getByText } = render(
        <Form
          onSubmit={onSubmitMock}
          subscription={{}}
          initialValues={{ name: "erikras" }}
        >
          {() => (
            <form>
              <Field name="name" component="input" data-testid="name" />
              <FormSpy subscription={{}}>
                {({ form }) => (
                  <button
                    type="button"
                    onClick={() => form.reset({ name: "bob" })}
                  >
                    Reset
                  </button>
                )}
              </FormSpy>
            </form>
          )}
        </Form>,
      );
      expect(getByTestId("name").value).toBe("erikras");
      fireEvent.change(getByTestId("name"), {
        target: { value: "erikrasmussen" },
      });
      expect(getByTestId("name").value).toBe("erikrasmussen");
      fireEvent.click(getByText("Reset"));
      expect(getByTestId("name").value).toBe("bob");
    });
  
    it("should allow subscription to be undefined", () => {
      // Implementation of this test case is not provided in the original file or the code block
      // This test case is assumed to exist based on the code block
    });
  
    it("should not throw error when subscribing to active (fix #1055)", () => {
      // Issue #1055: Cannot set property active of #<Object> which has only a getter
      // This was caused by spreading lazy state with getter-only properties into renderProps
      const spy = jest.fn();
      const { container, getByTestId } = render(
        <Form onSubmit={onSubmitMock}>
          {() => (
            <form>
              <Field name="name" component="input" data-testid="name" />
              <FormSpy
                subscription={{ active: true, values: true }}
                render={wrapWith(spy, (props) => {
                  // Verify that we can access active and values without error
                  const { active, values } = props;
                  return <div data-testid="spy">active={String(active)} values={JSON.stringify(values)}</div>;
                })}
              />
            </form>
          )}
        </Form>,
      );
      
      // Should render without throwing "Cannot set property active" error
      expect(container).toBeTruthy();
      expect(spy).toHaveBeenCalled();
      
      // Verify that active and values properties exist and can be accessed
      const props = spy.mock.calls[spy.mock.calls.length - 1][0];
      expect("active" in props).toBe(true);
      expect("values" in props).toBe(true);
      
      // Active should be undefined initially (no field focused)
      expect(props.active).toBeUndefined();
      
      // Values should be defined (empty object initially)
      expect(props.values).toBeDefined();
      expect(props.values).toEqual({});
      
      // When we focus a field, active should be the field name
      fireEvent.focus(getByTestId("name"));
      expect(spy.mock.calls[spy.mock.calls.length - 1][0].active).toBe("name");
    });
  });
  