react-navigation-hooks: How to test useFocusEffect
Asked Answered
A

9

17

As I understand it should be done so that useFocusEffect would work as useEffect for testing (mock). I use useFocusEffect for fetchData:

useFocusEffect(
  useCallback(() => {
    fetchData();
  }, [fetchData]),
);

Error message: react-navigation hooks require a navigation context but it couldn't be found. Make sure you didn't forget to create and render the react-navigation app container. If you need to access an optional navigation object, you can useContext(NavigationContext), which may return

Package versions:

"jest": "^24.9.0",
"react-native": "0.61.2",
"react-navigation": "^4.0.10",
"react-navigation-hooks": "^1.1.0",
"@testing-library/react-native": "^4.0.14",
Aerograph answered 7/2, 2020 at 13:41 Comment(4)
This should help: callstack.github.io/react-native-testing-library/docs/…Gob
@MarcelKalveram Thanks. But I have already tried this option and it did not help me.Aerograph
what's the problem you're facing now? Same error message as above?Gob
@MarcelKalveram not called useFocusEffectAerograph
W
19

Assuming you're rendering your component in your test, you need to wrap it in a fake <NavigationContext>. Doing so lets useFocusEffect look up the things it needs to determine if the component has been focused by your app's navigation.

This example uses render from react-native-testing-library. I think it's analogous to other rendering methods though.

import { NavigationContext } from "@react-navigation/native"
import { render } from "react-native-testing-library"

// fake NavigationContext value data
const navContext = {
  isFocused: () => true,
  // addListener returns an unscubscribe function.
  addListener: jest.fn(() => jest.fn())
}

// MyComponent needs to be inside an NavigationContext, to allow useFocusEffect to function.
const { toJSON } = render(
  <NavigationContext.Provider value={navContext}>
    <MyComponent />
  </NavigationContext.Provider>
)
Warrenwarrener answered 17/7, 2020 at 18:46 Comment(0)
A
5

This is just a fuller version of the above answer from @meshantz.

import { NavigationContext } from '@react-navigation/native';
import { render } from '@testing-library/react-native';
import React from 'react';

// This would probably be imported from elsewhere...
const ComponentUnderTest = () => {
  useFocusEffect(
    useCallback(() => {
      fetchData();
    }, [fetchData]),
  );
  
  return null;
};

const mockFetchData = jest.fn();
jest.mock('fetchData', () => mockFetchData);

describe('testing useFocusOnEffect in ComponentUnderTest', () => {
  afterAll(() => {
    jest.restoreAllMocks();
  });

  describe('when the view comes into focus', () => {
    it('calls fetchData', () => {
      const navContextValue = {
        isFocused: () => false,
        addListener: jest.fn(() => jest.fn()),
      };

      render(
        <NavigationContext.Provider value={navContextValue}>
          <ComponentUnderTest />
        </NavigationContext.Provider>,
      );

      expect(mockFetchData).toBeCalledTimes(0);

      render(
        <NavigationContext.Provider
          value={{
            ...navContextValue,
            isFocused: () => true,
          }}
        >
          <ComponentUnderTest />
        </NavigationContext.Provider>,
      );

      expect(mockFetchData).toBeCalledTimes(1);
    });
  });
});

Antabuse answered 4/11, 2020 at 10:2 Comment(4)
I'm getting the error TypeError: navigation.isFocused is not a function. @meshantz. can you pls advise.Cavalla
Updates: My apologies, it is working. What i did was mocking the react which is this jest.mock('react', () => ({ ...jest.requireActual('react') useContext: () => { user: 'sdfsf' } })); This turned out to be detrimental to what it is expected. Hence removing that, helped. Thanks for the solution @meshantz.Cavalla
This is awesome! Thanks a lot. Do you know how to write this with typescript? I tried but then navContext is missing some types like: dispatch, navigate, reset, goBack.Binding
@mohwarf to satisfy the TypeScript compiler you'll either have to add those properties (you can stub them out) to navContextValue, or just add any properties you want to use/mock in your test then cast const navContextValue like so const navContextValue = { ... } as NavigationContext which will stop you seeing the types message. The former is probably a better option but I sometimes to do the latter.Antabuse
D
3

For TypeScript, it is also required to satisfy type requirements as well, so in my case, it was done by using jest.requireActual:

    const withProvider = (element, store = defaultStore) => {
      // fake NavigationContext value data
      const actualNav = jest.requireActual("@react-navigation/native");
      const navContext = {
        ...actualNav.navigation,
        navigate: () => {},
        dangerouslyGetState: () => {},
        setOptions: () => {},
        addListener: () => () => {},
        isFocused: () => true,
      };
      return (
        <NavigationContext.Provider value={navContext}>
          <MyComponent />
        </NavigationContext.Provider>
      );
    };
    
    it("renders correctly", () => {
      render(withProvider(() => <SportsBooksScreen {...defaultProps} />));
    });
Disservice answered 2/2, 2022 at 12:10 Comment(0)
N
3

I had problems/limitations with proposed solutions in this thread, so I ended up mocking "useFocusEffect" with "React.useEffect".

It did the job well: my tests are green now!

jest.mock('@react-navigation/native', () => {
  const { useEffect } = require('react');
  const actualModule = jest.requireActual('@react-navigation/native');

  return {
    ...actualModule,
    useFocusEffect: useEffect,
  };
});
Natheless answered 1/4, 2022 at 14:37 Comment(1)
Or mock mockImplementation jest.mock('@react-navigation/native', () => ({ ...jest.requireActual('@react-navigation/native'), useFocusEffect: jest.fn().mockImplementation((func) => func()), }));Aerograph
D
1

Instead of useFocusEffect, use useIsFocused with useEffect, and the code just works fine.

In Your component:

import React, { useEffect } from 'react';
import { useIsFocused } from '@react-navigation/native';

const Component = () => {
  const isFocused = useIsFocused();
useEffect(() => {
    if (isFocused) {
      fetchData();
    }
  }, [isFocused]);
  return (<><View testID="child_test_id">{'render child nodes'}</View></>)
}



For Testing:


import Component from '--path-to-component--';
jest.mock('--path-to-fetchData--');
jest.mock('@react-navigation/native', () => {
  return {
    useIsFocused: () => true
  };
});

it('should render child component when available', async () => {
  const mockedData = [];
  fetchData.mockImplementation(() => mockedData);
  let screen = null;
  await act(async () => {
    screen = renderer.create(<Component  />);
  });
  const childNode = screen.root.findByProps({ testID: 'child_test_id' });
  expect(childNode.children).toHaveLength(1);
});
Dinny answered 11/1, 2021 at 13:6 Comment(3)
This question about unit testing, but your answer about work code.Aerograph
@VasylNahuliak Updated my code snippet. Please checkDinny
its work, but change the work code special for unit testing is bad practiceAerograph
A
1

useFocusEffect makes use of navigation.isFocused() which is accessible from jest's renderHookWithProviders.

Doing a navigation.isFocused.mockReturnValue(true); should do the trick! Just tried it now and it works fine.

Aryan answered 3/1 at 21:41 Comment(0)
C
-1

Create component FocusEffect

import { useFocusEffect } from "@react-navigation/native";
import { BackHandler } from "react-native";
import React from "react";

export default function FocusEffect({ onFocus, onFocusRemoved }) {
  useFocusEffect(
    React.useCallback(() => {
      onFocus();

      return () => onFocusRemoved();
    }, [onFocus, onFocusRemoved]),
  );
  return null;
}

Usage Example :

import React from 'react';
import { Text, View } from 'react-native';
import { FocusEffect } from './components';
    
const App = () => {

onFocus = () => {
   // ============>>>> onFocus <<<<==============
   fetchData();
};

onFocusRemoved = () => {
   // ============>>>> onFocusRemoved <<<<==============
};

return (
    <View>
       <FocusEffect
            onFocus={this.onFocus}
            onFocusRemoved={this.onFocusRemoved}
       />
       <Text>Hello, world!</Text>
   </View>
   )
}
export default App;
Chopfallen answered 13/10, 2020 at 8:50 Comment(2)
Where is the test example?Aerograph
People that don't read questions...Unrepair
V
-1

use waitFor if you do inside your useFocusEffet an async call, like this

import { render,  waitFor } from '@testing-library/react-native';

jest.mock('@react-navigation/native', () => ({
  useFocusEffect: jest.fn().mockImplementation(cb => cb())
}));

test('Test', async () => {
  const tree = await waitFor(() => render(<YourComponent />));
  expect(tree.toJSON()).toMatchSnapshot();
})'

your component does somenthing like this

import React, { useState, useCallback } from 'react';
import { useFocusEffect } from '@react-navigation/native';

function YourComponent() {
  const [data, setData] = useState(null);
  useFocusEffect(
    useCallback(() => {
      fetchData().then((response) => setData(response));
    }, [])
  );
  return <View />;
} 
Valvule answered 25/4 at 0:30 Comment(1)
Using waitFor for render is antipattern kentcdodds.com/blog/fix-the-not-wrapped-in-act-warningAerograph
S
-3

If the code within useFocusEffect() is not consequential to your testing, you can mock the hook as follows:

jest.mock("@react-navigation/native", () => ({
  useFocusEffect: jest.fn(),
  // ...
}));
Supplejack answered 14/4, 2021 at 5:59 Comment(1)
It's not make sense, because without mock this code doesn't runAerograph

© 2022 - 2024 — McMap. All rights reserved.