javascriptreact-nativejestjsfirebase-authenticationreact-native-testing-library

How to test setState inside Firebase Auth onAuthStateChanged


I have already mocked Firebase onAuthStateChanged, so tests work flawlessly but I have this use case, in which it set's the user inside:

const [isSignedIn, setIsSignedIn] = useState<boolean>(false);
const [displayName, setDisplayName] = useState<string | null>('');
const auth = getAuth();
onAuthStateChanged(auth, (user) => {
console.log('OnAuthStateChanged USER', user);
if (user) {
  setDisplayName(user.email);
  setIsSignedIn(true);
} else {
  setIsSignedIn(false);
  setDisplayName('');
}

I know that you can easily mock what it returns, but I don't know how to deal with the "insides" of a function if that it's even possible.

I need to set the state specifically since the DrawerNavigator will show different options depending if the user is signedIn or not:

 {isSignedIn && (
      <>
        <Drawer.Screen name='Dashboard' component={Dashboard} />
      </>
    )}
    {!isSignedIn && (
      <>
        <Drawer.Screen name='Welcome' component={Welcome} />
        <Drawer.Screen name='CreateAccount' component={CreateAccount} />
        <Drawer.Screen name='Login' component={Login} />
      </>
    )}

I can test the functionality but only on the default state as it is initialized, how can I make this work? Or is there any other change of how I set the state in which this could be testable?

Here it is also how I'm mocking firebase/auth module right now:

/**
* Firebase
*/
jest.mock('firebase/auth', () => {
   return {
     getAuth: () => jest.fn(),
     onAuthStateChanged: () => jest.fn(),
   };
});

Solution

  • Assuming you want to emulate the delay that occurs when signing in a user:

    The mock below assumes you aren't using more than one app at a time. If so, getting specific app instances must be implemented.

    /**
    * Firebase Auth Module
    */
    jest.mock('firebase/auth', () => {
       const authInstance = {
         // while handshaking with the Firebase Auth servers, currentUser
         // is null, regardless if someone is logged in or not.
         currentUser: null
       };
    
       const mockedUserInfo = Object.freeze({ // force read-only
         // mocked user info here - display name, email, etc
         email: 'example@example.com'
       });
       
       // container for attached callbacks and state variables
       const authChangeCallbacks = [];
       let authCurrentUserInfo = mockedUserInfo;
       let authTimer = null;
       let authTimerCompleted = false;
    
       // invoke all callbacks with current data
       const fireOnChangeCallbacks = () => {
         authMock.currentUser = authCurrentUserInfo;
         callbacks.forEach((cb) => {
           try {
             cb(mockedUserInfo)); // invoke any active listeners
           } catch (err) {
             console.error('Error invoking callback', err);
           }
         });
         authTimerCompleted = true;
       }
    
       authInstance.signOut = () => { // signInWithX will look similar to this
         authCurrentUserInfo = null;
         fireOnChangeCallbacks();
       };
    
       return {
         getAuth: jest.fn(() => authInstance),
         onAuthStateChanged: jest.fn((authMock, onChangeCallback) => {
           if (!authTimer) {
             // increase this delay to emulate slower connections
             authTimer = setTimeout(fireOnChangeCallbacks, 2000);
           }
    
           callbacks.push(onChangeCallback);
           const unsubscriber = () => {
             const foundIndex = callbacks.indexOf(onChangeCallback);
             if (foundIndex > -1) callbacks.splice(foundIndex, 1);
           }
    
           if (authTimerCompleted) {
             // auth is "resolved" already, fire callback immediately
             onChangeCallback(mockedUserInfo);
           }
    
           return unsubscriber;
         })
       };
    });