Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MockedProvider causing test to complain about not using act(). #5920

Closed
donedgardo opened this issue Feb 6, 2020 · 26 comments
Closed

MockedProvider causing test to complain about not using act(). #5920

donedgardo opened this issue Feb 6, 2020 · 26 comments

Comments

@donedgardo
Copy link

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",

@CosmaTrix
Copy link

@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
Copy link
Author

@CosmaTrix
Copy link

@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! 🎉

@Frozen-byte
Copy link

Frozen-byte commented Feb 19, 2020

Does anyone know how to test "loading..." States, then?
As Apollos documentation

import { mount } from "enzyme";

it('renders loading', () => {
  let component = mount(
      <MockedProvider mocks={mocks} addTypename={false}>
        <ResearchCategoryList category="Education" />
      </MockedProvider>
    )
  // promise not resolved yet
  expect(component.render()).toMatchSnapshot()
});

Leads into error a test was not wrapped in act(...)

This test is fine, but will not test the loading state as the promise is already resolved

import { mount } from "enzyme";
import { wait } from "@testing-library/react";

it('renders loading', async () => {
  let component = mount(
      <MockedProvider mocks={mocks} addTypename={false}>
        <ResearchCategoryList category="Education" />
      </MockedProvider>
    )
  await wait(); // promise resolves
  expect(component.render()).toMatchSnapshot()
});

@CosmaTrix
Copy link

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?

@Frozen-byte
Copy link

Frozen-byte commented Feb 20, 2020

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.

@bwhitty
Copy link
Contributor

bwhitty commented Feb 20, 2020

I feel as though this may be caused by #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));

@michelalbers
Copy link

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();
  });

@helly0d
Copy link

helly0d commented May 26, 2020

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 ).

@kkak10
Copy link

kkak10 commented Jul 8, 2020

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();
    });
  });
});

@mlg87
Copy link

mlg87 commented Aug 6, 2020

@kkak10 try this

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

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

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

from the docs

@kkak10
Copy link

kkak10 commented Aug 10, 2020

@mlg87 i try your code, but fail that same result as my code.

@kkak10
Copy link

kkak10 commented Aug 10, 2020

if i not use waitfor(), i succeed loading state test but printing warning message about act()

@DylanRJohnston
Copy link

DylanRJohnston commented Aug 20, 2020

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();
  });
});

@DylanRJohnston
Copy link

DylanRJohnston commented Aug 20, 2020

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);
  });

@ryan-rushton
Copy link

For anyone else that comes across this I used the following

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

@DylanRJohnston
Copy link

DylanRJohnston commented Sep 14, 2020

@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))

@ryan-rushton
Copy link

ryan-rushton commented Sep 14, 2020

@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.

@sajadghawami
Copy link

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...

@benjarwar
Copy link

I'm running into the same issue as @ryan-rushton. We have props that are changing as side effects of the client completing mocked queries, switching out of "loading" state – and need to get to the "success" state to test other subsequent conditions. The documented way still throws the act warning.

I'm personally going with the act solution proposed in the comment above, as it pertains more closely to the warning. But agree that a declarative/official method to get to the "success" state would be preferable.

@hwillson
Copy link
Member

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!

@yinchuandong
Copy link

@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.

It doesn't support to test loading status

@Jero786
Copy link

Jero786 commented Jan 27, 2022

Here same would be nice to have an official waitForFinalState.
My solution:
await act(() => wait(0));

@phong-lam
Copy link

phong-lam commented Feb 11, 2022

I am still getting this issue, with all latest versions of "@testing-library/react": "^12.1.2"

Sample

    const storeData = {
      request: {
        query: GET_SOME_ID,
        variables: { id: '123456' },
      },
      result: mockDataResponse,
    }

    const { container } = render(
      <MockedProvider mocks={[storeData]}>
            <MockComponent />
      </MockedProvider>
    )

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

    expect(container).toMatchSnapshot()
  })

The only way to get rid off this currently is below code to handle error and I do not like this.

    if (/Warning.*not wrapped in act/.test(args[0])) {
      return
    }

@redreceipt
Copy link

redreceipt commented Jun 30, 2022

@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

@av
Copy link

av commented Nov 10, 2022

Possible approach for enzyme, if you can't use @testing-library/react:

import React from 'react';
import { NetworkStatus } from '@apollo/client';
import { mount, ReactWrapper } from 'enzyme';
import { act } from 'react-dom/test-utils';

import { CustomMockedProvider, CustomMockedProviderProps } from './CustomMockedProvider';
import sleep from '../sleep';

export const mountMocked = async (
  cmp: React.ReactElement,
  { timeout, ...providerProps }: CustomMockedProviderProps & { timeout: number } = { timeout: 5000 },
) => {
  let w: ReactWrapper;

  await act(async () => {
    w = mount(<CustomMockedProvider {...providerProps}>{cmp}</CustomMockedProvider>);

    if (!w) {
      throw new Error('MountMocked: Unable to mount given component');
    }

    const mockedProvider = w.find('MockedProvider');

    if (!mockedProvider) {
      throw new Error('MountMocked: given component does not use Apollo MockedProvider');
    }

    const start = Date.now();
    const hasSettled = () => {
      w.update();

      if (Date.now() - start > timeout) {
        throw new Error(`MountMocked: Pending queries have not settled after ${timeout}ms, aborting the test.`);
      }

      const queryMap = mockedProvider.state()?.client?.queryManager?.queries ?? new Map();
      const queries = Array.from(queryMap.values());

      if (queries.every((q) => q.networkStatus === NetworkStatus.ready)) {
        return true;
      }

      return false;
    };

    do {
      await sleep(5);
    } while (!hasSettled());

    w.update();
  });

  // We're either throwing or
  // it's guaranteed to be present.
  return w!;
};

export default mountMocked;

Essentially, we're just waiting up until ApolloClient from the MockedProvider doesn't have any queries that aren't ready before returning the enzyme wrapper.

Then, in your tests, you're basically using await mountMocked(...) instead of enzymes mount(...). It'll likely not handle your component issuing queries lazily during one of the re-renders.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Feb 1, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests