Search code examples
react-nativeunit-testingjestjsreact-native-testing-library

Querying conditionally rendered elements in react native jest which are rendered after a state update and not after an event


I want to query an element through jest which is conditionally rendered. The component is rendered after loading state is set to false when Api call is complete. There are ways to query elements which are rendered after an event such as button press or text input focus. But I am unable to find a way to query element which is rendered after a state change when Api call is complete.

Component:

const Home = ({navigation}) => {
  const dispatch = useDispatch();
  const {user, orders} = useSelector(state => state.user);
  const [loading, setLoading] = useState(true);

  const getOrders = () => {
    fetch(`http://192.168.1.17:6000/users/${user.userId}/orders`)
      .then(res => res.json())
      .then(orders => {
        dispatch(setOrders(orders));
        setLoading(false);
      })
      .catch(err => console.log(err));
  };

  const getUser = () => {
    fetch(`http://192.168.1.17:6000/users/${user.userId}`)
      .then(res => res.json())
      .then(user => {
        dispatch(setUserInfo(user));
        getOrders();
      })
      .catch(err => console.log(err));
  };

  ListEmptyComponent = () => (
    <View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
      <Lottie
        source={require('../../assets/empty-box')}
        style={{height: 200}}
        autoPlay
        loop={false}
      />
      <Text style={{fontWeight: 'bold', fontSize: 14}}>No Orders Assigned</Text>
    </View>
  );

  useEffect(() => {
    getUser();
  }, []);

  return loading ? (
    <ActivityIndicator
      style={{flex: 1, alignSelf: 'center'}}
      size={26}
      color={'#4D61D6'}
    />
  ) : (
    <View
      testID="test"
      style={{
        flex: 1,
        padding: 20,
        backgroundColor: '#ECECEC',
      }}>
      {orders.length > 0 && (
        <TouchableOpacity
          testID="qrCodeButton"
          onPress={() => navigation.navigate('Scan Order')}
          style={{
            alignSelf: 'center',
            padding: 20,
            backgroundColor: '#4D61D6',
            marginBottom: 30,
            borderRadius: 3,
          }}>
          <Text style={{color: 'white', fontWeight: 'bold'}}>
            Scan Order QR Code
          </Text>
        </TouchableOpacity>
      )}
      <FlatList
        data={orders}
        testID={'orderComponent'}
        contentContainerStyle={{flex: 1}}
        ListEmptyComponent={ListEmptyComponent}
        renderItem={({item}) =>
          !item.is_verified && (
            <OrderComponent
              key={item.entity_id}
              item={item}
              navigation={navigation}
            />
          )
        }
      />
    </View>
  );
};

export default Home;

Test Case:

it('Show Button to scan order QR Code', () => {
    const {queryByTestId} = render(
      <Provider store={store}>
        <Home loading={false} />
      </Provider>,
    );
    expect(queryByTestId('qrCodeButton')).toBeTruthy();
  });

Response:

 ● Home › Show Button to scan order QR Code

expect(received).toBeTruthy()

Received: null

  44 |     );
  45 |     //console.log(screen.debug(null, Infinity));
> 46 |     expect(queryByTestId('qrCodeButton')).toBeTruthy();
     |                                           ^
  47 |   });
  48 | });
  49 |

  at Object.toBeTruthy (src/screens/Home.test.js:46:43)

Solution

  • There are a few tricky thinks you have to account for.

    1, you are rendering home like this: <Home loading={false} /> but in the code you provided home only accepts navigation as a prop: const Home = ({navigation}) => {, so that doesn't make sense. you can only pass navigation as a prop.

    2, so you cannot pass loading as false. So you have to actually make it false. You can do this by mocking fetch to return whatever you need. for this to work you need to have a mock for fetch, which you can get by adding something like this in your jest.setup.js:

    // Makes fetch available globally to prevent a Warning
    import 'whatwg-fetch';
    global.fetch = require('jest-fetch-mock');
    

    In your test you can then also import fetch in your testfile: import fetch from 'jest-fetch-mock'; You can then use it inside your test like this:

    fetch.mockResponse(JSON.stringify(some orders)); //you might need to change this a bit to match your orders object.
    

    you then probably have to make your test asynchronous. and with some other clean up I think it should something like this:

    import {screen, render, waitFor} from '@testing-library/react-native';
    import fetch from 'jest-fetch-mock';
    
    const mockNavigation = jest.fn();
    
    describe('HomeScreen', () => {
      it('Show Button to scan order QR Code', async () => {
        fetch.mockResponse(JSON.stringify(some orders)); //you need to change this a bit to match your orders object.
    
        render(
          <Provider store={store}>
            <Home navigation={mockNavigation} /> // You might need a better mock for navigation, but this should do the trick for now 
          </Provider>,
        );
    
        await waitFor(() => {
          expect(screen.getByTestId('qrCodeButton')).toBeVisible(); //toBeTruthy is a weak check, you probably want to use toBeVisible() or toBeOnScreen(), you also should prefer getByText/role/label over getByTestId.
        })
        
      });
    });
    

    This might not be a 100% right yet, but I think this will point you in the right direction.