apollo-client: MockedProvider causing test to complain about not using `act()`.

Intended outcome:

Actual outcome:

When running test using MockProvider I expect test to not error out with the following error:

 Warning: An update to ResearchCategoryList inside a test was not wrapped in act(...).
 When testing, code that causes React state updates should be wrapped into act(...):

How to reproduce the issue:

The following test will cause test to complain:

it('renders without error', async () => {
  let component
  act(() => {
    component = create(
      <MockedProvider mocks={mocks} addTypename={false}>
        <ResearchCategoryList category="Education" />
      </MockedProvider>
    )
  })
  await wait(0)
  const tree = component.toJSON()
  expect(tree).toMatchSnapshot()

Versions

"@apollo/react-testing": "^3.1.3",

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 30
  • Comments: 26 (2 by maintainers)

Most upvoted comments

@donedgardo I’ve beeing having the same issue, using Jest and @testing-library/react. Right now I’m suppressing those warnings, as described in the in the @testing-library/react documentation.

In my jest global setup file, I’ve added

const originalError = console.error;

beforeAll(() => {
  console.error = (...args: any[]) => {
    if (/Warning.*not wrapped in act/.test(args[0])) {
      return;
    }

    originalError.call(console, ...args);
  };
});

afterAll(() => {
  console.error = originalError;
});

I’m not completely sure about this approach (this might suppress warnings that need to be taken care of), but that does the trick! 😄

@donedgardo Thanks for pointing that issue out. I’ve been using the wait exported from @testing-library/react and that fixes the issue.

import { render, wait } from "@testing-library/react";

it ("renders without errors", () => {
  render(
    <MockedProvider mocks={mocks} addTypename={false}>
      <ResearchCategoryList category="Education" />
    </MockedProvider>
  );

  await wait()

  // run expectations
});

In your case, I think you can try and wrap the await wait() in act as suggested in this comment.

await act(wait);

Both solutions work for me! 🎉

@DylanRJohnston that is exactly the point. It is a hack but it stops log spam in my tests. When I just have await new Promise((resolve) => setTimeout(resolve, 0)) every test that is testing final state receives Warning: An update to MyComponent inside a test was not wrapped in act(...). in the logs.

From my understanding we are waiting for apollo client to update its internal state from loading to the mocked final state. By using the waitFor we are getting a nicer to read version of wrapping it in act. The internal promise is just there to ensure the event log ticks over to the final state.

Another alternative that works is await act(() => new Promise((resolve) => setTimeout(resolve, 0)));

Edit: While I think of it it would be nice if the API had a renderFinalState or waitForFinalState function included.

For anyone else that comes across this I used the following

await waitFor(() => new Promise((resolve) => setTimeout(resolve, 0)));

Hi @Frozen-byte, I suggest you use the render from @testing-library/react.

import { render, wait } from "@testing-library/react";

it("renders loading", async () => {
  const { container } = render(
    <MockedProvider mocks={mocks} addTypename={false}>
      <ResearchCategoryList category="Education" />
    </MockedProvider>
  );

  // take a snapshot of the loading state
  expect(container).toMatchSnapshot();

  // wait for content 
  await wait()

  // take a snapshot of the rendered content (error or data)
  expect(container).toMatchSnapshot();
});

Does that help you?

Found the solution!

The problem is act must scope all changes to hooks, this includes the initial render, and any event handlers that trigger Apollo. So you need the wait after the render, but still inside a call to act.

describe("when loading state", () => {
  it("should render spinner component", async () => {
    let container;

    await act(async () => {
      container = render(...)
      // This is required to give Apollo time to resolve and update the hook and must be inside the act
      await wait(0);
    }) 

    expect(container.getByTestId("spinner")).toBeVisible();
  });
});

It might be a bit of an overkill, but I hate waiting a random number of seconds so I just hooked some deferred promises on the mocks so I can wait for them to finish. I did this mostly because in our project we are using multiple apollo providers so we had random failing tests due to CPU differences between local and CI servers.

I hope this helps, and hopefully the apollo team will include something like this in a future release using the internal renderPromises.

helpers/tests/mockedProvider.jsx

import PropTypes from "prop-types";
import React, { Component, Fragment } from "react";
import { act } from "react-dom/test-utils";
import { useQuery } from "@apollo/react-hooks";
import { MockedProvider as ApolloMockedProvider } from "@apollo/react-testing";


class Deferred {
  constructor() {
    this.isResolved = false;
    this.resolve = null;

    this.deferredPromise = new Promise((resolve) => {
      this.resolve = () => {
        if (!this.isResolved) {
          this.isResolved = true;
          resolve();
        }
      };
    });
  }

  promise = () => this.deferredPromise;

  wait = () => act(this.promise);
}


const Hook = ({ mock }) => {
  const { loading, error, data } = useQuery(mock.request.query);

  if (!mock.promises) {
    mock._deferredPromises = {
      loading: new Deferred(),
      error: new Deferred(),
      data: new Deferred(),
    };

    mock.promises = {
      loading: mock._deferredPromises.loading.wait,
      error: mock._deferredPromises.error.wait,
      data: mock._deferredPromises.data.wait,
    };
  }

  if (loading) {
    mock._deferredPromises.loading.resolve();
  }

  if (error) {
    mock._deferredPromises.error.resolve();
  }

  if (data) {
    mock._deferredPromises.data.resolve();
  }

  return null;
};


export class MockedProvider extends Component {
  static propTypes = {
    children: PropTypes.any.isRequired,
    addTypename: PropTypes.bool,
    mocks: PropTypes.array,
  }

  static defaultProps = {
    addTypename: false,
    mocks: [],
  }

  render() {
    const { addTypename, mocks, children, ...props } = this.props;

    return (
      <ApolloMockedProvider {...props} addTypename={addTypename} mocks={mocks}>
        <Fragment>
          {mocks.map(this.createHook)}
          {children}
        </Fragment>
      </ApolloMockedProvider>
    );
  }

  createHook = (mock, id) => {
    if (!mock.request?.query) {
      return null;
    }

    return <Hook key={id} mock={mock} />;
  }
}

component.test.jsx

import React from "react";
import { mount } from "enzyme";

import { MockedProvider } from "./helpers/tests/mockedProvider";
import MyComponent from "./index";
import query from "./index.gql";


describe("<MyComponent />", () => {
  let mocks = [];

  beforeEach(() => {
    mocks = [{
      request: { query },
      result: {
        // your data
        data: { },
      },
    }];
  });

  it("renders MyComponent with data", async() => {
    const teamPage = mount(
      <MockedProvider mocks={mocks}>
        <MyComponent />
      </MockedProvider>
    );

    expect(teamPage.isEmptyRender()).toEqual(true);

    await mocks[0].promises.data();
    teamPage.update();

    expect(teamPage.isEmptyRender()).toEqual(false);
  });
});

LATER EDIT: I created a gist where I also removed some useless code. You don’t need to wait for the loading state to test it and data and error are final states ( or at least in unit testing ).

With wait() testing-library will complain that wait is depreacted. My solution:

  it("should render without errors", async () => {
    const { container } = render(
      <MockedProvider>
        <MyView />
      </MockedProvider>
    );
    await waitFor(() => {});
    expect(container).toMatchSnapshot();
  });

There is a lot of history in this issue, and the problems listed head in all sorts of different directions. If anyone thinks this is still a problem in @apollo/client@latest, and can provide a small runnable reproduction, we’ll take a closer look. Thanks!

still having that same issue… problem now is, sometimes the tests work without a problem, but sometimes they don’t and they will throw that act error…

I just created a wrapper of render to handle this

import {
  queries,
  RenderOptions,
  RenderResult,
  act,
  render as renderInternal,
} from "@testing-library/react";

export const render = async (
  ui: React.ReactElement,
  options?: Omit<RenderOptions, "queries">
): Promise<RenderResult> => {
  let container: RenderResult;

  await act(async () => {
    container = renderInternal(ui, queries);
    await wait(0);
  });

  return container;
};

Then you can just

describe("when loading state", () => {
  it("should render spinner component", async () => {
    const { getByTestId } = await render(...);

    expect(getByTestId("spinner")).toBeVisible();
  });
});

Be sure to also stick a wait in any events that also trigger responses from apollo, e.g.

  await act(async () => {
    fireEvent.click(component.getInputByTestId("button-save"));

    await wait(0);
  });

I feel as though this may be caused by https://github.com/apollographql/apollo-client/issues/5869

Here’s the code I had to write to alleviate the problem, and the test case shows the problem. Essentially, you need to await to get another tick of the event loop because Apollo under the hood is causing multiple returns from useQuery.

import wait from 'waait';

const Component = ({ spy }) => {
  // this hook does a `useQuery` with `cache-only`
  const { loading, data } = useLocalFilterState();

  spy(loading, data);

  return null;
};

const spy = jest.fn();
await act(async () => {
  mount(
    <ApolloProvider client={client}>
      <Component spy={spy} />
    </ApolloProvider>
  );

  // fixes React warning that "An update to Component inside a test was not wrapped in act(...)."
  // see below comment on why
  await wait(0);
});

// TODO "loading" (first param) should be false, and there should be one render,
//  but there's a bug in Apollo 3.0
// @see https://github.com/apollographql/apollo-client/issues/5869
// assert that an object is returned as "data" (second param) which is all we care about
expect(spy).toHaveBeenCalledTimes(2);
expect(spy).toHaveBeenNthCalledWith(1, true, expect.any(Object));
expect(spy).toHaveBeenNthCalledWith(2, false, expect.any(Object));

@hwillson I used the example from your docs. Go here and run yarn test to see the problem: https://replit.com/@redreceipt/MelodicKosherSubversion

Screen Shot 2022-06-30 at 4 56 59 PM

@ryan-rushton, that’s a misuse of waitFor. waitFor repeatedly calls the function it’s given until it stops rejecting the promise / throwing an error, e.g. using findBy to wait for a component to appear in the dom. Since your function never rejects the waitFor does nothing and is equivalent to await new Promise((resolve) => setTimeout(resolve, 0))

If i use waitfor(), i can’t test the loading state.

how can i test loading state?

describe("when loading state", () => {
  it("should render spinner component", async () => {
    const { getByTestId } = render(
      <MockedProvider mocks={[]}>
        <SomeComponent />
      </MockedProvider>,
    );

    await waitFor(() => {
      /**
       * This test is failed.
       * if use await, in an error state because the data is already fetched.
       */
      expect(getByTestId("spinner")).toBeVisible();
    });
  });
});

Yes that helped a lot! Here is my not simplified working code, without enzyme:

import { render, wait } from "@testing-library/react";

  describe("render correctly", () => {
    it("should resolve loading state", async () => {
      const { container } = render(
        <MockedProvider
          mocks={[
            GetAccountMock,
            GetActivityMock,
            GetSubmittedPriceInquiryProductsMock,
            GetNewPriceInquiryProductsMock,
            UpdateNewPriceInquiryItemsMock
          ]}
          addTypename={false}
        >
          <PriceInquiryForm params={{ inquiryId: "2" }} />
        </MockedProvider>
      );
      // loading
      expect(container.firstChild.classList.contains("loading")).toBe(true);
      expect(container.firstChild).toMatchSnapshot();
      await wait();
      // content
      expect(container.firstChild.classList.contains("loading")).toBe(false);
      expect(container.firstChild).toMatchSnapshot();
    });
  });

// edit: as I tried to split the loading and content part into two assertions I noticed that it is obligatory to await wait() the test (even if it’s the last line), otherwise jest will complain about missing act.