Writing a Testcafe test to assert a loading spinner is visible after making a fetch request - testing

I have the following scenario:
Load page
Expect spinner is hidden
Type username Click search
Expect spinner display
After a few seconds delay, expect spinner to hide
Assert the right user details are displayed
Here is the working demo
I have mocked the network request in my test spec, but I am unable to understand how to assert spinner is visible after I click the search button
Here is my test spec:
import {Selector, RequestMock} from "testcafe";
import mockUser from "../mocks/mockUser.json";
var apiMocks = RequestMock()
.onRequestTo(/\/api\/users/)
.respond(mockUser, 200, {
'access-control-allow-credentials': "*",
'access-control-allow-origin': "*"
})
fixture `When a user is searched`
.page(`http://localhost:3000/`)
.requestHooks(apiMocks);
test("Should fetch user details", async t => {
const spinnerEl = Selector("[data-test-id='spinner']");
await t.expect(spinnerEl.exists).notOk();
await t
.typeText("[data-test-id='txt-search']", "foo")
.click("[data-test-id='btn-search']");
// This line does not work
// await t.expect(spinnerEl.exists).ok();
await t.expect(Selector("[data-test-id='username']").innerText).eql("Foo Bar");
await t.expect(Selector("[data-test-id='userid']").innerText).eql("foo");
})
I am new to TestCafe, could someone help me with this.
Thanks!

It is difficult to check whether the described spinner element is shown due to the following:
It is displayed only during a short period of time. This does not allow TestCafe to check it in time. Using mocks makes the spinner appear only for milliseconds.
TestCafe waits for all requests and does not perform any actions until XHR requests are completed. This means that assertions will not start until your request is finished.
However, it's still possible to work around the issue.
You can use MutationObserver and TestCafe ClientFunctions mechanism.
You can create your element observer using the ClientFunction. The observer will watch for the app element changes. If the spinner element appears the observer will be notified and set the window.spinnerWasShown variable to true.
After the button is clicked, you can check that the windows.spinnerWasShown variable is set to true.
Here is the full example:
import { Selector, RequestMock, ClientFunction } from "testcafe";
import mockUser from "../mocks/mockUser.json";
var apiMocks = RequestMock()
.onRequestTo(/\/api.github.com\/users/)
.respond(mockUser, 200, {
'access-control-allow-credentials': "*",
'access-control-allow-origin': "*"
});
fixture`When a user is searched`
.page(`http://localhost:3000/`)
.requestHooks(apiMocks);
const spinnerWasShown = ClientFunction(() => window.spinnerWasShown);
const observeSpinner = ClientFunction(() => {
var appEl = document.querySelector('.app');
const config = { attributes: true, childList: true };
const callback = function(mutationsList) {
for(let mutation of mutationsList) {
if (mutation.type === 'childList') {
for (var i =0; i < mutation.addedNodes.length; i++ )
window.spinnerWasShown = window.spinnerWasShown || mutation.addedNodes[i].className.indexOf('spinner') > -1;
}
}
};
const observer = new MutationObserver(callback);
observer.observe(appEl, config);
});
test("Should fetch user details", async t => {
const spinnerEl = Selector("[data-test-id='spinner']");
await t.expect(spinnerEl.exists).notOk();
await t.typeText("[data-test-id='txt-search']", "foo");
await observeSpinner();
await t.click("[data-test-id='btn-search']");
await t.expect(spinnerWasShown()).eql(true);
await t.expect(spinnerEl.exists).notOk();
await t.expect(Selector("[data-test-id='username']").innerText).eql("Foo Bar");
await t.expect(Selector("[data-test-id='userid']").innerText).eql("foo");
});

Related

How to do drag only without drop in Testcafe

So I have a scenario where I want to capture a popup element message whenever I drag one element to another element.
public async dragTransitiontToSegment(item: number, transitionName: string) {
const _tailSegment = Selector('.rolling').nth(item);
const _transitionPanel = Selector('.effects-selector.editor-panel .item-container')
const _transitionType = _transitionPanel.withText(transitionName);
await t.click(_transitionPanel);
await t.dragToElement(_transitionType,_tailSegment,{speed:0.01});
}
Right now I've change the speed for the drag but it was still to fast to capture the message I want, because the dragToElement fucntion will drop it. Is there a way to just drag and hold it ?
at present, TestCafe doesn't allow you to drag without drop out of the box. You can simulate the sequence of events (the mousedown, mousemove, or HTML5 drag events)
import { Selector, ClientFunction } from 'testcafe';
function triggerMouseEvent (selector, type, options) {
const dispatchFunc = ClientFunction((type, options = {}) => {
options.bubbles = true;
options.cancelable = true;
options.view = window;
const event = new MouseEvent(type, options);
const targetElement = elementSelector();
targetElement.dispatchEvent(event);
}, { dependencies: { elementSelector: selector } });
return dispatchFunc(type, options);
}
fixture`Fixture`
.page`http://devexpress.github.io/testcafe/example`;
test('Test', async t => {
await t.click('#tried-test-cafe');
const slider = Selector('span.ui-slider-handle.ui-corner-all');
await triggerMouseEvent(slider, 'mousedown');
await t.wait(1000);
const offsetLeft = await Selector('.slider-value').withText('5').offsetLeft;
await triggerMouseEvent(slider, 'mousemove', { clientX: offsetLeft });
await t.wait(1000);
await t
.expect(slider.offsetLeft).gte(352)
.expect(slider.offsetLeft).lte(353);
});
Also, TestCafe 1.15 will include the t.dispatchEvent method that allows you to trigger events using TestController.

Test different cases for Facebook login

I am implementing Facebook login in a React Native app using https://github.com/facebook/react-native-fbsdk .
In my code, I have different outcomes depending on what permissions the user grants & based on whether or not an error occurs:
handleFacebookLogin = async () => {
const result = await LoginManager.logInWithPermissions([
"public_profile",
"email"
]);
if (result.isCancelled) {
//do nothing, the user cancelled
} else {
const data = await AccessToken.getCurrentAccessToken();
if (!data) {
this.showAlert(
strings.onboarding.fb_login.error_title,
strings.onboarding.fb_login.error_body,
strings.onboarding.fb_login.ok_button
);
} else {
if (result.grantedPermissions.toString().contains("user_email")) {
//navigate to screen A
} else {
//navigate to screen B
}
}
}
};
showAlert = (title, body, button) => {
//show alert
};
I want to write a test for each case (e.g. an error occurs, the user grants email permission, the user does NOT grant email permission).
I started writing a test for showing the alert when the error occurs, but I am stuck at mocking the actual error (the if (!data) scenario from the code above).
it("Shows an alert when an error occurs", () => {
const errorMock = jest.fn();
const loginMock = jest.fn();
let ageScreen = renderer
.create(<AgeScreen showAlert={errorMock} handleFacebookLogin={loginMock} />)
.getInstance();
const spy = jest
.spyOn(ageScreen, "showAlert")
.mockImplementation(/*TODO: mock fb library error */);
ageScreen.handleFacebookLogin();
expect(spy).toHaveBeenCalled();
});
My questions are:
How do I finish this test case so that I reliably test that the Alert will be displayed in case of an error?
How do I test the other scenarios (e.g. navigation to screen A if the user grants email permission)?
Thank you in advance! :)
First you need to mock LoginManager and AccessToken
jest.mock('react-native-fbsdk', () => ({
...jest.requireActual('react-native-fbsdk'),
LoginManager: {
logInWithPermissions: jest.fn(),
},
AccessToken: {
getCurrentAccessToken: jest.fn(),
}
}));
Then in every test case, mock the result that will lead to the scenario your are verifying
it("Shows an alert when an error occurs", () => {
const errorMock = jest.fn();
const loginMock = jest.fn();
// put what result will get
LoginManager.logInWithPermissions.mockReturnValue(...);
// put what data will get
AccessToken.getCurrentAccessToken.mockReturnValue(...);
let ageScreen = renderer
.create(<AgeScreen showAlert={errorMock} handleFacebookLogin={loginMock} />)
.getInstance();
const spy = jest
.spyOn(ageScreen, "showAlert")
ageScreen.handleFacebookLogin();
expect(spy).toHaveBeenCalled();
});
and regarding your second questions it would be the same: mock dependencies and verify that the corresponding calls are being made

How can I import a redux saga yield into my detox + jest test file. I need gain access to data stored in the redux store in my test setup

This is a react-native application and I am currently writing some end-to-end testing.
A token is stored in the redux store shown below and I am testing the login functionality using detox/jest. I need to detect if the token exists in the store in my login.spec.js . If the token exists I want to wipe it from the store so the user is not logged in automatically when i reload the app to take the user back to another scene. The main function in question is the refreshUserToken() and line:-
const { refresh_token } = yield select(token);
Here is the redux saga file User.js located at:-MyApp/App/Sagas/User.js
import { call, put, takeEvery, select } from "redux-saga/effects";
import Config from "MyApp/App/Config";
import API from "MyApp/App/Services/API";
import { when } from "MyApp/App/Helpers/Predicate";
import Credentials from "MyApp/App/Helpers/Credentials";
import ActionCreator from "MyApp/App/Actions";
const appendPayload = payload => {
return {
...payload,
// Removed because no longer needed unless for testing purposes.
// username: Config.TEST_USERNAME,
// password: Config.TEST_PASSWORD,
client_id: Config.CLIENT_ID,
client_secret: Config.CLIENT_SECRET,
};
};
const token = state => state.token;
const user = state => state.user;
const attemptUserLogin = function*(action) {
const { payload } = action;
const login = "/oauth/token";
const grant_type = "password";
const loginPayload = appendPayload(payload);
action.payload = {
...loginPayload,
grant_type,
};
yield attemptUserAuthorisation(login, action);
};
const attemptUserRegister = function*(action) {
const register = "/api/signup";
const { payload } = action;
yield Credentials.save(payload);
yield put(ActionCreator.saveUserCredentials(payload));
yield attemptUserAuthorisation(register, action);
};
const refreshUserToken = function*(action) {
const login = "/oauth/token";
const grant_type = "refresh_token";
const { refresh_token } = yield select(token);
action.payload = {
...action.payload,
grant_type,
refresh_token,
};
yield attemptUserAuthorisation(login, action);
};
const watchExampleSaga = function*() {
yield takeEvery(ActionCreator.AUTO_USER_LOGIN, autoUserLogin);
yield takeEvery(ActionCreator.USER_LOGIN, attemptUserLogin);
yield takeEvery(ActionCreator.USER_REGISTER, attemptUserRegister);
yield takeEvery(ActionCreator.USER_REFRESH_TOKEN, refreshUserToken);
};
export default watchExampleSaga;
Here is my detox/jest spec file located at:-MyApp/App/e2e/login.spec.js
describe('Login Actions', () => {
it('Should be able to enter an email address', async () => {
await element(by.id('landing-login-btn')).tap()
const email = 'banker#dovu.io'
await element(by.id('login-email')).tap()
await element(by.id('login-email')).replaceText(email)
});
it('Should be able to enter a password', async () => {
const password = 'secret'
await element(by.id('login-password')).tap()
await element(by.id('login-password')).replaceText(password)
});
it('Should be able to click the continue button and login', async () => {
await element(by.id('login-continue-btn')).tap()
await waitFor(element(by.id('dashboard-logo'))).toBeVisible().withTimeout(500)
// If token exists destroy it and relaunch app. This is where I need to grab the token from the redux saga!
await device.launchApp({newInstance: true});
});
})
This is how I handled a similar scenario:
in package.json scripts:
"start:detox": "RN_SRC_EXT=e2e.tsx,e2e.ts node node_modules/react-native/local-cli/cli.js start",
In my detox config:
"build": "ENVFILE=.env.dev;RN_SRC_EXT=e2e.tsx,e2e,ts npx react-native run-ios --simulator='iPhone 7'",
That lets me write MyFile.e2e.tsx which replaces MyFile.tsx whilst detox is running
In the test version of that component I have buttons which are tapped in the tests and the buttons dispatch redux actions
Looks like this actually cant be done unless someone can give me a solution other than mocking the state which still wouldn't work in this case my app checks for real states to auto login.
I did get to the stage of creating a new action getUserToken and exporting that into my jest file. However the action returns undefined because the jest file requires a dispatch method like in containers.js. If anyone could provide me with a method of this using jest I would be very happy.

Upload a file with puppeteer in Jest

I am using Jest and have puppeteer set up as in this repository, which is linked to from the Jest documentation.
I am trying to write some automated smoke tests on a WordPress website using puppeteer. One of the tests attempts to upload an image to the WordPress Media Library.
This is the test:
it('Create test media', async () => {
// go to Media > Add New
await page.goto(`${env.WP_HOME}/wp/wp-admin/media-new.php`)
const display = await page.evaluate(() => {
const el = document.querySelector('#html-upload-ui')
return window.getComputedStyle(el).display
})
if (display !== 'block') {
// ensure we use "built-in uploader" as it has `input[type=file]`
await page.click('.upload-flash-bypass > a')
}
const input = await page.$('#async-upload')
await input.uploadFile(testMedia.path)
})
The file input field's value is populated as expected (I know this because if I save out a screenshot after the call to uploadFile it shows the path of the file in the input), and the form is submitted, however when I go to view the media library there are no items.
I have tried the following amendments to the uploadFile part of the test, to no avail:
// 1. attempt to give time for the upload to complete
await input.uploadFile(testMedia.path)
await page.waitFor(5000)
// 2. attempt to wait until there is no network activity
await Promise.all([
input.uploadFile(testMedia.path),
page.waitForNavigation({waitUntil: 'networkidle0'})
])
// 3. attempt to submit form manually (programmatic)
input.uploadFile(testMedia.path)
page.evaluate(() => document.querySelector('#file-form').submit())
await page.waitFor(5000) // or w/ `waitForNavigation()`
// 4. attempt to submit form manually (by interaction)
input.uploadFile(testMedia.path)
page.click('#html-upload')
await page.waitFor(5000) // or w/ `waitForNavigation()`
The problem is that file uploading doesn't work when connecting to a Browser instance via WebSocket as in jest-puppeteer-example. (GitHub issue here: #2120.)
So instead of doing that just use puppeteer.launch() directly when setting up your test suite (instead of via the custom "Jest Node environment"):
let browser
, page
beforeAll(async () => {
// get a page via puppeteer
browser = await puppeteer.launch({headless: false})
page = await browser.newPage()
})
afterAll(async () => {
await browser.close()
})
You also then have to manually submit the form on the page, as in my experience uploadFile() doesn't do this. So in your case, for the WordPress Media Library single file upload form, the test would become:
it('Create test media', async () => {
// go to Media > Add New
await page.goto(`${env.WP_HOME}/wp/wp-admin/media-new.php`)
const display = await page.evaluate(() => {
const el = document.querySelector('#html-upload-ui')
return window.getComputedStyle(el).display
})
if (display !== 'block') {
// ensure we use the built-in uploader as it has an `input[type=file]`
await page.click('.upload-flash-bypass > a')
}
const input = await page.$('#async-upload')
await input.uploadFile(testMedia.path)
// now manually submit the form and wait for network activity to stop
await page.click('#html-upload')
await page.waitForNavigation({waitUntil: 'networkidle0'})
})

Conditional test to bypass pop up with Testcafe

I'm using testcafe to run some tests in an ecommerce page, but a random pop up is breaking the test. When it appears on the window, the Testcafe is unable to click on the next selector and move forward with the test, and then fail.
Currently, I'm using .js files to hold the selectors, like:
import { Selector } from 'testcafe';
export default class Checkout {
constructor () {
//address
this.addressName = Selector('input#CC-checkoutCepAddressBook-sfirstname');
this.addressLastname = Selector('input#CC-checkoutCepAddressBook-slastname');
//Rest of selectors...
}
Then, I import them to another .js and declare the tests like functions:
import { ClientFunction } from 'testcafe';
import { Selector } from 'testcafe';
import Fixture from '../../../DesktopModel/Chrome/fixture.js';
import Home from '../../../DesktopModel/Chrome/home.js';
import Cart from '../../../DesktopModel/Chrome/cart.js';
...
const fixtureUrlBase = new Fixture();
const home = new Home();
const pdp = new Pdp();
const cart = new Cart();
...
export async function checkoutLoggedBoleto(t) {
await t
.click(pdp.addToCartBtn)
.click(home.finishOrderBtn)
.click(cart.finishOrderBtn)
//Rest of the test actions...}
Finally, I'm executing another.js where I declare the tests using test command:
test
.before(async t => {
await login(t);
})
('Desktop - User Login + Checkout with Invoice', async t => {
// Function Login => Search => PDP => Checkout with Invoice
await checkoutLoggedBoleto(t);
});
Since it is a random event (it happens in different moments, like sometimes in the product page and sometimes in the checkout page), is possible to use some conditional test just bypass this popup, like if the pop up 'x' appears on the screen, click on 'close popup' and continue with test, else continue with the test.
I search in testcafe Test API and have not found such a function.
I'm using testcafe 0.17.0.
TestCafe doesn't provide an API for that. To handle you case, you can check whether the popup appears before each action.
Optionally, to make your code cleaner, you can wrap TestCafe API actions in the following way:
import { t, Selector } from 'testcafe';
const closePopupBtn = Selector('.close-popup');
async function checkPopup () {
if(await closePopupBtn.exists)
await t.click(closePopupBtn);
}
const tc = {
click: async selector => {
await checkPopup();
await t.click(selector);
}
}
test('my test', async () => {
await tc.click('.btn1');
await tc.click('.btn2');
});