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

Warning An update to ForwardRef inside a test was not wrapped in act(...) #1668

Closed
obaricisse opened this issue Sep 13, 2024 · 8 comments
Closed

Comments

@obaricisse
Copy link

obaricisse commented Sep 13, 2024

Describe the bug

How do I get rid of this warning an update to ForwardRef inside a test was not wrapped in act?
I am already using act in my test but keep getting this warning even though test passes. see repro step.

Expected behavior

Steps to Reproduce

Here is an example component and the output.

import React, { PureComponent } from "react";
import { Button, Text, View } from "react-native";
import { connect } from "react-redux";
import { render, fireEvent, act } from "@testing-library/react-native";
import configureMockStore from "redux-mock-store";
import { Provider } from "react-redux";
import "core-js"; 

interface Props {
  count: number;
  incrementCount: () => void;
}

interface State {
  internalCount: number;
}

export class MyPureComponent extends PureComponent<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = {
      internalCount: props.count,
    };
  }

  componentDidUpdate(prevProps: Props) {
    if (prevProps.count !== this.props.count) {
      this.setState({ internalCount: this.props.count });
    }
  }

  render() {
    return (
      <View>
        <Text testID="internal-count">{this.state.internalCount}</Text>
        <Button
          disabled={this.props.count >= 1}
          title="Increment"
          testID="increment-button"
          onPress={this.props.incrementCount}
        />
      </View>
    );
  }
}

const mapStateToProps = (state: { count: number }) => ({
  count: state.count,
});

const mapDispatchToProps = (dispatch: any) => ({
  incrementCount: () => dispatch({ type: "INCREMENT" }),
});

export const ConnectedMyPureComponent = connect(
  mapStateToProps,
  mapDispatchToProps
)(MyPureComponent);

const mockStore = configureMockStore([]);

describe("MyPureComponent", () => {
  let store: any;

  beforeEach(() => {
    // Initial state for the store
    store = mockStore({
      count: 0, // Initial Redux state
    });

    store.dispatch = jest.fn(); // Mock dispatch
  });

  it("should update the internal state when the Redux count changes", () => {
    const { getByTestId, rerender } = render(
      <Provider store={store}>
        <ConnectedMyPureComponent />
      </Provider>
    );

    // Initial state should be 0
    expect(getByTestId("internal-count").props.children).toBe(0);

    // Update Redux state and rerender the component
    act(() => {
      store = mockStore({ count: 5 });
      rerender(
        <Provider store={store}>
          <ConnectedMyPureComponent />
        </Provider>
      );
    });

    // Now the internal state should be updated to 5
    expect(getByTestId("internal-count").props.children).toBe(5);
  });
});

Screenshots

image

Versions

12.7.2

image
@mdjastrzebski
Copy link
Member

Our of curiosity, why are you externally triggering state update instead of triggering by a button press? That would be a recommended way in RNTL as it would resemble a real user interaction.

@obaricisse
Copy link
Author

obaricisse commented Sep 13, 2024

you need the button to be enabled in order to press it. the state update is to enable or disable the button. and it is triggered by a componentDidUpdate.
Also this is just an example of how to repro the error. The use case makes more sense in my specific case.

@mdjastrzebski
Copy link
Member

could you put it in a repro repo so I could reproduce it too?

@obaricisse
Copy link
Author

obaricisse commented Sep 13, 2024

Here is a code sandbox

let me know if you have any access issue.

@bytehala
Copy link

bytehala commented Sep 28, 2024

I tried my hand at solving this problem, but I only have a workaround.

Short answer: it's caused by the Button going from disabled=false to disabled=true, causing an animation cycle. I don't know why RNTL does not like that.

To prove that it's the Button: To rule out the suspicion that it's caused by the manual call to rerender I used a real redux store, so that the dispatch() propagates to the component and RNTL handles the rerender when the state updates. code sandbox

Here, I mocked TouchableOpacity and the warning message disappears.

  • comment out the jest.mock above the file to see the warning again

The same solution can be applied to the reproduction code without changing to a real redux store:

// Mock TouchableOpacity so that Button doesn't animate when changing disabled state
jest.mock(
    "react-native/Libraries/Components/Touchable/TouchableOpacity",
    () => {
      const React = require("react");
      const { View } = require("react-native");
      return (props) => <View {...props}>{props.children}</View>;
    }
);

Bonus, by mocking the TouchableOpacity, the original code doesn't even have to wrap the rerender in act anymore:

it("should update the internal state when the Redux count changes without wrapping in act", async () => {
    const { getByTestId, rerender } = render(
      <Provider store={store}>
        <ConnectedMyPureComponent />
      </Provider>
    );

    // Initial state should be 0
    expect(getByTestId("internal-count").props.children).toBe(0);

    // Update Redux state and rerender the component

    store = mockStore({ count: 5 });
    rerender(
      <Provider store={store}>
        <ConnectedMyPureComponent />
      </Provider>
    );

    // Now the internal state should be updated to 5
    await waitFor(() =>
      expect(getByTestId("internal-count").props.children).toBe(5)
    );
  });

@mdjastrzebski
Copy link
Member

It seems like the cause you are seeing the error message is that the animation triggered by the Button/TouchableOpacity is finishing after the act returns (it returns immediately after executing the callback). The animation seem to have non-zero duration which finished in probably couple hundreds of milliseconds afterwards. Personally I would not worry about it.

If you want to solve it, consider using fake timers and experimenting with something like jest.runOnlyPendingTimers() (here).

BTW the error does not come from RNTL, but rather underlying React Test Renderer.

@mdjastrzebski
Copy link
Member

Closing as stale.

@mdjastrzebski mdjastrzebski closed this as not planned Won't fix, can't repro, duplicate, stale Oct 31, 2024
@rob-langridge-lego
Copy link

For anyone who faces this mdjastrzebski's comment really helped. Our issue was animation running on render of a component and completing after the assertion, even those the assertions were in an act.

We created a helper function to mock animated views for this instances.

export const mockAnimatedView = () => {
  jest.mock('react-native', () => {
    const rn = jest.requireActual('react-native');
    const spy = jest.spyOn(rn.Animated, 'View', 'get');
    spy.mockImplementation(() => jest.fn(({ children }) => children));
    return rn;
  });
  jest.useFakeTimers();
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants