dimanche 2 juin 2019

Testing React app that uses sagas/reducers

I have a React project that has the following structure:

api/auth.js:

import BaseApi from './base-api';

class Authorization extends BaseApi {
 namespace() {
   return '/api/v1';
 }

 * login(username, password) {
   return yield this.apiFetch('/account/login', {
     method: 'POST',
     body: JSON.stringify({
       grant_type: 'password',
       username,
       password,
     }),
   });
 }
}

export default Authorization;

Where the apiFetch function calls the fetch function of JS to perform the API call.

sagas/auth.js:

import { apply, put } from 'redux-saga/effects';
import {
  loginFailed,
  loginSucceeded,
} from '../reducers/auth.reducers';

export function* login(authApi, { payload }) {
  try {
    const creds = payload;
    const response = yield apply(
      authApi,
      authApi.login,
      [creds.username, creds.password]
    );

    if (response.errorCode) {
      yield put(loginFailed({
        message: response.message || 'Failed to login',
      }));
    } else {
      yield put(loginSucceeded({
        user: response.user,
        refreshToken: response.refresh_token,
        accessToken: response.access_token,
        expiresIn: response.expires_in,
      }));
    }
  } catch (e) {
    console.error('Failed to login', e);
    yield put(loginFailed({
      message: 'Failed to login',
    }));
  }
}

sagas/index.js:

import { takeEvery } from 'redux-saga/effects';

import {
    login as loginAction,
} from '../reducers/auth.reducers';

import {
    login,
} from './auth.sagas';

import Authorization from '../api/authorization';

export default function* rootSaga() {
    let authApi = new Authorization(config.apiHost);

    yield [
        // auth
        takeEvery(loginAction, login, authApi),
    ];
}

reducers/auth.js:

import { createAction, createReducer } from 'redux-act';
import Immutable from 'seamless-immutable';
import moment from 'moment';

const INITIAL_STATE = Immutable({
  user: {},
  refreshToken: '',
  accessToken: '',
  expiresIn: 0,
  loggedIn: false,
  loading: false,
  errorMessage: null,
});

export const login = createAction("LOGIN");
export const loginSucceeded = createAction("LOGIN_SUCCEEDED");
export const loginFailed = createAction("LOGIN_FAILED");

const reducers = createReducer({
  [login]: state => state.merge({
    errorMessage: null,
    loading: true,
  }),
  [loginSucceeded]: (state, { user, refreshToken, accessToken, expiresIn }) => state.merge({
    user,
    loggedIn: true,
    refreshToken,
    accessToken,
    expiresIn,
    expiresAt: moment().add(expiresIn - 120, 's').toDate(),
    errorMessage: null,
    loading: false,
  }),
  [loginFailed]: (state, { message }) => state.merge({
    errorMessage: message,
    accessToken: null,
    refreshToken: null,
    expiresIn: 0,
    user: {},
    loading: false,
    loggedIn: false,
  }),
}, INITIAL_STATE);

export default reducers;

And then I have a Login component, which has two input fields, and a button. And when the user fills the input fields and clicks on the button it performs the API call using the saga and calls the appropriate reducers depending on the success of the API call. Now, I wanted to write an integration test, to make a failing and success login. I'm using jest with react-testing-library. I did something like this:

login.test.js:

import React from 'react';
import {
    cleanup,
    fireEvent,
    waitForElement,
} from '@testing-library/react';

import testUtils from '../app/utils/testUtils';
import Login from '../app/main/login';

afterEach(cleanup);

describe('Login Test', () => {
    it('performs a user login', async () => {
        const {
            getByText,
            getByPlaceholderText,
            getByRole,
        } = testUtils.renderWithRedux(<Login />);

        getByPlaceholderText('E-Mail').value = 'myuser@gmail.com';
        getByPlaceholderText('Password').value = 'mypass';

        fireEvent.click(getByText('Login'));
        // Incorrect login should display a snackbar.
        await waitForElement(() => getByRole('alertdialog'));
    });
});

Now this test fails at the last line. However, in my app, when the login is not successful a Material UI snackbar is displayed which has a role=alertdialog attribute. The point that I understand with tests is that it actually won't perform the actual API call when the button is clicked, right? So, maybe I somehow need to mock the result or what? However, I don't know how this procedure will look like in my code structure. Any ideas how to perform such tests?

Aucun commentaire:

Enregistrer un commentaire