I am using jest and trying to test an asynchronous login request. I am able to check that the call has resolved and is successful. I would also like to test the case that the call wasn't successful.
I have been following the docs from here.
I understand I am not doing the reject correctly, but if I move the jest.mock('.utils/api', () => {... into the test block it doesn't work, it needs to be outside. Can anyone advise the correct way to do this?
See my code below:
import React from 'react';
import { render, fireEvent } from 'react-testing-library';
import Login from './index';
import { login as mockLogin } from './api';
let mockData = {
token: '12345'
};
let errorData = {
message: 'Your username/password is incorrect'
};
jest.mock('.utils/api', () => {
return {
jsonRequest: jest.fn(() => new Promise((resolve, reject) => {
resolve(mockData,);
// I am not doing this correctly.
reject(errorData);
})),
};
});
describe('<Login />', () => {
it('returns a sessionId if successful and error if not', () => {
const { getByLabelText, getByText } = render(<Login />);
const loginButton = getByText(/login/i);
fireEvent.click(loginButton);
expect(mockLogin).toBeCalledTimes(1);
expect(mockLogin).toHaveBeenCalledWith('/login', {
data: {
password: 'test',
username: 'test',
},
method: 'POST',
});
expect(mockLogin()).resolves.toBe(mockData);
expect(mockLogin()).rejects(mockData);
});
});
What you need to test here is the behavour of your component when the API rejects the request for some reason.
Assume this scenario:
Lets say the reason for rejection is that the "Entered password is not correct".
Then you need to make sure that the Login component will show an error message to the DOM where the user can see it and re-enter his password
To test this you need to make a check inside the mocked API, something like:
jsonRequest: jest.fn((formDataSubmittedFromComponent) => new Promise((resolve, reject) => {
// Notice that the mocked function will recieve the same arguments that the real function recieves from the Login component
// so you can check on these arguments here
if (formDataSubmittedFromComponent.password !== mockData.password) {
reject('Entered password is not correct') // << this is an error that your component will get and should handle it
}
resolve();
})),
After that you should test how did your component handle the reject
For example, you could test whether or not it displayed an error message to the DOM:
const errorMessageNode = getByTestId('error-message');
expect(errorMessageNode).toBeTruthy()
Edit: before you fire the login event you should make sure that the form is populated with the mocked data
Related
I have a very simple axios call that deletes a record. If successful, it will call the notify function (custom function) with specific parameters. I don't actually want the notify to run, but all I want to check is that it's called with those specific params when it gets inside the then.
export function deleteRecord(id) {
return axios
.delete(`/${id}`)
.then(() => notify('success', 'Delete successful'))
.catch(() => notify('error', 'Delete failed'));
}
I've been going down the rabbit hole jest.fn(), spyOn and toHaveBeenCalledWith but I'm just stuck. This is where I've ended up:
it('deleteRecord success', async () => {
const id = 1;
const notify = jest.fn()
axios.delete.mockResolvedValueOnce({ status: 200 });
await deleteRecord(id);
expect(notify).toHaveBeenCalledWith('success', 'Delete successful');
});
This is what I get as an error.
expect(jest.fn()).toHaveBeenCalledWith(...expected)
Expected: "success", "Rolled back"
Number of calls: 0
All I want is to test that it gets to the successful notify specifically, because I want to then test for the failed notification in another test. I just don't understand what I'm missing. I've gone through so many other threads, but I can't seem to find a solution.
You need to mock the notify function.
import { deleteRecord } from './deleteRecord';
jest.mock('./notify', () => jest.fn());
it('deleteRecord success', async () => {
const id = 1;
const notify = require('./notify');
axios.delete.mockResolvedValueOnce({ status: 200 });
await deleteRecord(id);
expect(notify).toHaveBeenCalledWith('success', 'Delete successful');
});
I tried to use the saved session in the 'it' block, but it's opening the "baseurl/index.php/dashboard" so it means opening the login page afresh. I am trying to use the saved cookies to directly visit or use in another 'it' block.
import testdata from '../../fixtures/testdata.json'
describe('Example to demonstrate the use `each` in Cypress', function () {
const loginPage = (username, password) => {
cy.session([username, password], () => {
cy.visit('/') // baseUrl: "https://opensource-demo.orangehrmlive.com/"
cy.get('#txtUsername').type(username)
cy.get('#txtPassword').type(username)
cy.get('#btnLogin').click()
})
}
beforeEach('Loading test data', function () {
loginPage(testdata.username, testdata.password)
})
it('Validating the Quick Launch menu', function () {
cy.visit('/index.php/dashboard')
cy.get('.quickLaungeContainer').each(($el, index) => {
expect($el).to.contain(this.testdata.quickLaunch[index])
})
})
})
The code looks like it should work, you have username twice instead of password but I assume that's a typo.
In case the cookie isn't set as expected you can add a validate function to help you debug the cy.session()
See Gleb's video Use cy.session Command To Prepare Test Data But Only When Needed, about 4 minutes in is the TLDR point.
The idea is to use validate() to assert the condition you assume will be true after the setup() has run.
In this case, just want to check a cookie has been set on the domain. If it does not happen, the setup() tries again and if still not passing validate() the test fails.
It's also useful if one of your tests deliberately removes the cookie, then the next test will cause it to be re-fetched.
testdata.json
{
username: ...,
password: ...,
quicklaunch: [...],
cookieName: 'orangehrmlive' // check actual name in devtools
}
Test
import testdata from '../../fixtures/testdata.json'
describe('Example to demonstrate the use `each` in Cypress', function () {
const loginPage = (username, password) => {
const setup = () => {
cy.visit('/') // baseUrl: "https://opensource-demo.orangehrmlive.com/"
cy.get('#txtUsername').type(username)
cy.get('#txtPassword').type(password)
cy.get('#btnLogin').click()
}
const validate = () => {
cy.getCookie(testdata.cookieName).should('exist')
}
cy.session([username, password], setup, {validate})
}
beforeEach('Loading test data', function () {
loginPage(testdata.username, testdata.password)
})
it('Validating the Quick Launch menu', function () {
cy.visit('/index.php/dashboard')
cy.get('.quickLaungeContainer').each(($el, index) => {
expect($el).to.contain(this.testdata.quickLaunch[index])
})
})
})
I have a redux action called academyRedirect.js which is invoked whenever an user is redirected to the path /user/academy when he pushes the 'academy' button in my main page.
export const getAcademyAutoLogin = () => {
axios.get(`/user/academy`)
.then((response) => {
window.location.replace(response.data); // this is replaced by an URL coming from backend
})
.catch((error) => {
reject(error.response);
});
return null;
};
Whenever the user is not allowed to access the academy (for not having credentials) or whenever i get an error 500 or 404, i need to display a modal or something to inform the user that an error occurred while trying to log into the academy. Right now im not being able to do it, the page just stays blank and the console output is the error.response.
Any help is appreciated
Redux Store
export const messageSlice = createSlice({
name: 'message',
initialState: { isDisplayed: false, errorMessage: ''},
reducers: {
displayError(state, action) {
state.isDisplayed = true
state.errorMessage = action.message
},
resetErrorState() {
state.isDisplayed = false
state.errorMessage = ''
},
}
})
export const messageActions = messageSlice.actions;
Inside the component:-
const Login = () => {
const errorState = useSelector(globalState => globalState.message)
const onClickHandler = async => {
axios.get(`/user/academy`)
.then((response) => { window.location.replace(response.data) })
.catch((error) => {
dispatch(messageActions.displayError(error))
});
}
return (
{errorState.isDisplayed && <div>{errorState.errorMessage}</div>}
{!errorState.isDisplayed &&<button onClick={onClickHandler}>Fetch Data </button>}
)
}
Maybe this is of help to you
You can try to add interceptor to your axios.
Find a place where you create your axios instance and apply an interceptor to it like this
const instance = axios.create({
baseURL: *YOUR API URL*,
headers: { "Content-Type": "application/json" },
});
instance.interceptors.request.use(requestResolveInterceptor, requestRejectInterceptor);
And then, in your requestRejectInterceptor you can configure default behavior for the case when you get an error from your request. You can show user an error using toast for example, or call an action to add your error to redux.
For second case, when you want to put your error to redux, its better to use some tools that created to make async calls to api and work with redux, for example it can be redux-saga, redux-thunk, redux-axios-middleware etc.
With their docs you would be able to configure your app and handle all cases easily.
Im starting with react-testing-library, and Im trying to test API calls. I have two sets, one for success request and another for error request.
import React from "react";
import { render, waitForElementToBeRemoved } from "#testing-library/react";
import user from "#testing-library/user-event";
import App from "./App";
import { getUser } from "./serviceGithub";
jest.mock("./serviceGithub");
//Mock data for success and error, Im using the github api
const dataSuccess = {
id: "2231231",
name: "enzouu",
};
const dataError = {
message: "not found",
};
const renderInit = () => {
const utils = render(<App />);
const inputUser = utils.getByPlaceholderText("ingrese usuario", {
exact: false,
});
const buttonSearch = utils.getByRole("button", { name: /buscar/i });
return { utils, buttonSearch, inputUser };
};
test("should success request to api", async () => {
getUser.mockResolvedValue([dataSuccess]);
const { utils, buttonSearch, inputUser } = renderInit();
expect(utils.getByText(/esperando/i)).toBeInTheDocument();
expect(buttonSearch).toBeDisabled();
user.type(inputUser, "enzzoperez");
expect(buttonSearch).toBeEnabled();
user.click(buttonSearch);
await waitForElementToBeRemoved(() =>
utils.getByText("cargando", { exact: false })
);
expect(getUser).toHaveBeenCalledWith("enzzoperez");
expect(getUser).toHaveBeenCalledTimes(1);
expect(utils.getByText("enzouu", { exact: false })).toBeInTheDocument();
});
test("should error request to api", async () => {
getUser.mockResolvedValue(dataError)
const { utils, buttonSearch, inputUser } = renderInit();
expect(buttonSearch).toBeDisabled();
user.type(inputUser, "i4334jnrkni43");
expect(buttonSearch).toBeEnabled();
user.click(buttonSearch)
await waitForElementToBeRemoved(()=>utils.getByText(/cargando/i))
expect(getUser).toHaveBeenCalledWith('i4334jnrkni43')
expect(getUser).toHaveBeenCalledTimes(1)
});
The problem here is that in the second test the last line expect(getUser).toHaveBeenCalledTimes(1) get error because getUseris calling 2 times, but if I comment the first test, the second pass..
So, how should I do to test this case? Its ok the way that Im doing the tests?
Thanks!
You can use jest.mockClear() with beforeEach() or afterEach()
For clean-up purpose, afterEach() would be more appropriate.
mockClear resets all the information stored in the mockFn.mock.calls which means that for every test, you can expect getUser being called, started from zero times.
afterEach(() => {
jest.clearAllMocks()
})
Furthermore, use screen from #testing-library/react instead of returned value of render when using queries. Also, mockResolvedValueOnce would be better in this case.
I'm testing a website that needs authentication with different users. It's working most of the time but sometimes, login fails and Testcafé doesn't detect it before running into the actual test code. Rather than raising an error into the login method, it fails when finding a DOM element in the test page. So it keeps the wrong login information and other tests with the same user will fail too.
I know the way to detect a login error on my website but I can't say to Testcafé:
"Hey! Something wrong appends when login, don't save login information for this user and try again in next tests"
EDIT:
Rather than using hardcoded login information, I use a separate file logins.ts with the following structure and I adapt it to add loggedIn and role fields :
adminUserCredentials: { login: 'mylogin', pwd: 'mypass', role: null, loggedIn: false }
Then I use it as follow:
function createUserForSpecificEnv(user: User, baseUrl: string): Role {
if(!user.loggedIn) {
user.role = Role(baseUrl, async t => {
await t
.wait(1000)
.typeText('#loginInput', user.login)
.typeText('#passwordInput', user.pwd)
.click('#Btn')
if(await Selector('#user-info').visible) {
user.loggedIn = true
}
})
}
return user.role
}
const adminUserRole = getRole(adminUserCredentials)
test("test 1", async t => {
t.useRole(adminUserRole)
}) // The test go on the login page, auth failed (expected) and can't find #user-info (expected)
test("test 2", async t => {
t.useRole(adminUserRole)
}) // The test doesn't go to the login page and directly says : can't find #user-info
But it's still not working... TestCafe tries to log in on the first test and then it directly reuses the same login information.
Any help would be appreciated! Thank you :-)
EDIT 2
I clarify the fact that I use variable to store the role (see comments)
If login fail is expected I'd suggest not using Role for this particular test. You can extract an authentication logiс into a separate function and use it in this test directly and in the Role constructor:
const logIn = async t => {
//login steps
};
user.role = Role(baseUrl, logIn);
test("test 1", async t => {
await logIn(t);
}); // The test go on the login page, auth failed (expected) and can't find #user-info (expected)
test("test 2", async t => {
t.useRole(adminUserRole)
});
I think you can check some Selector on the last step of Role initialization to make sure that you are correctly logged in. If you are not, you need to recreate your role for further usage in the next tests. Please see the following code, which demonstrates my idea:
import { Role, Selector } from 'testcafe';
let role = null;
let loggedIn = false;
function getRole() {
if (!loggedIn) {
role = new Role('http://example.com', async t => {
console.log('role initialize');
// await t.typeText('#login', 'login');
// await t.typeText('#password', 'password');
// await t.click('#signin');
// NOTE: ensure that we are actually logged in
if(await Selector('#user_profile').exists)
loggedIn = true;
});
}
return role;
}
fixture `fixture`
.page `../pages/index.html`;
test(`test1`, async t => {
await t.useRole(getRole());
});
test(`test2`, async t => {
await t.useRole(getRole());
});
test(`test3`, async t => {
await t.useRole(getRole());
});