How to abstract common function out from the test file in Cypress - javascript

I just moved from TestCafe to Cypress and couldn't find a solution to abstract a common frequently used method. In this example below cy.document().then(doc).. is used twice, however I believe that these types of function must be abstracted to reusable function.
it('Test the input text field and submit button list the basket items', () => {
const allNameBeforeInput = []
const allNameAfterInput = []
cy.document().then((doc) => {
const elements = doc.querySelector('#items').querySelectorAll('.row-style > :nth-child(1)')
for (let i = 0; i <= elements.length - 1; i++) {
const basketName = elements[i].textContent
if (basketName && basketName !== '') {
allNameBeforeInput.push(`${basketName}`)
}
console.log(allNameBeforeInput.length) //this gives 0
}
})
cy.get(basket.itemInputField)
.type('Suraj')
cy.get(basket.submitInputButtonField)
.click()
cy.get(basket.itemInputField)
.type('Suraj')
cy.get(basket.submitInputButtonField)
.click()
cy.get(basket.itemInputField)
.type('Suraj')
cy.get(basket.submitInputButtonField)
.click()
cy.get('#items').children('.row-style').children('.list-item')
.contains('Suraj')
cy.document().then((doc) => {
const elements = doc.querySelector('#items').querySelectorAll('.row-style > :nth-child(1)')
for (let i = 0; i <= elements.length - 1; i++) {
const basketName = elements[i].textContent
if (basketName && basketName !== '') {
allNameAfterInput.push(`${basketName}`)
}
}
console.log(allNameAfterInput.length) //this gives 3
expect(allNameBeforeInput.length).equal(0)
expect(allNameAfterInput.length).equal(3)
expect(allNameBeforeInput.length).is.lt(allNameAfterInput.length)
})
})
This is what I want to accomplished with class Basket:
getAllBasketName() {
cy.document().then((doc) => {
const allName = []
const elements = doc.querySelector('#items').querySelectorAll('.row-style > :nth-child(1)')
for (let i = 0; i <= elements.length - 1; i++) {
const basketName = elements[i].textContent
if (basketName && basketName !== '') {
allName.push(`${basketName}`)
}
}
return allName
})
}
Now I should be able to use
const getAllBasketNamesBefore = basket.getAllBasketName()
cy.get(basket.itemInputField)
.type('Suraj')
cy.get(basket.submitInputButtonField)
.click()
cy.get(basket.itemInputField)
.type('Suraj')
cy.get(basket.submitInputButtonField)
.click()
cy.get(basket.itemInputField)
.type('Suraj')
cy.get(basket.submitInputButtonField)
.click()
const getAllBasketNamesAfter = basket.getAllBasketName()
{Assertion goes here}
This is not working because of async/await is not handled so the value of before and after are alway 0. Any clue or help will be appreciated.

The method you are using is not recommended by cypress and is considered an anti-pattern. https://docs.cypress.io/guides/references/best-practices.html#Assigning-Return-Values
Cypress recommends that you add custom commands. https://docs.cypress.io/api/cypress-api/custom-commands.html#Syntax
In the initially created folder structure, the commands.js file can be found under the support folder. Here you are able to create a command that wraps up the logic you wish to reuse. Based on the console.log portion of your code I assume this is run on the command line. There are custom commands for the console as well as for use in the UI.
for that portion you may have to add this custom command
// not a super useful custom command
// but demonstrates how subject is passed
// and how the arguments are shifted
Cypress.Commands.add('console', {
prevSubject: true
}, (subject, method) => {
// the previous subject is automatically received
// and the commands arguments are shifted
// allow us to change the console method used
method = method || 'log'
// log the subject to the console
console[method]('The subject is', subject)
// whatever we return becomes the new subject
//
// we don't want to change the subject so
// we return whatever was passed in
return subject
})
For the other functionality creating commands is pretty simple, the basic pattern is:
Cypress.Commands.add(name, callbackFn)
so you can potentially create something like
Cypress.Commands.add(allNameBeforeInput, (options, options) => {
//custom logic goes here
})
Then you can use it by calling cy.allNameBeforeInput(options, options).
For instance, I was struggling with login and all my tests had login functions to log in through the UI but I wanted to start my tests on the correct page instead of the the log in page. I added this to the command.js file in the support folder:
Cypress.Commands.add('login',(username="notsharingmyusernam#stackexchange.com",
password="somesecurepasswordshhh") => {
cy.request({
method: "POST",
url: "/api/public/login",
body: `{:person/email "${username}", :person/password "${password}"}`,
headers: {
"Accept": "application/edn",
"Content-Type": "application/edn"
}
})
})
And now I can add the cy.login and a beforeEach function at the beginning of my tests. The before each to make the request to the server and wait for the request for login and the cy.login custom command to ensure that I can use that bundled up logic with just one cy command.
describe('Test suite for page traverse', () => {
beforeEach(() => {
cy.server()
cy.route("POST","/api/graphql").as("graphql")
Cypress.Cookies.preserveOnce("company_jwt_qa")
})
it('traverses all subnav items', () => {
cy.login()
cy.visit('/index.html')
cy.wait("#graphql")
cy.get('[data-tag-component="subnav-group"]')
cy.get('[data-tag-component="subnav-title"]').eq(1).click()
})
})

Related

How to find the calling test in cypress custom command

I have a command that overwrites pause to add the input from a dialog to the reports. I want to know if there is a way to know what test is calling the function so that I can customize the message on the inputs.
Cypress.Commands.overwrite('pause', (originalFn, element, options) => {
var tryThis = '';
if (//place calling the function == file1) {
tryThis = 'message1';
} else if (//place calling the function == file2) {
...
} else if (//place calling the function == file3) {
...
}
var datalog = window.prompt(tryThis, "Log your results");
cy.addContext("DATALOG:" + datalog);
return originalFn(element, options)
})
As well as access via the Mocha properties there is also
For the spec file Cypress.spec
Properties for my.spec.js
Cypress.spec.absolute: "C:/.../my.spec.js"
Cypress.spec.name: "my.spec.js"
Cypress.spec.relative: "cypress\integration\my.spec.js"
Cypress.spec.specFilter: "my"
Cypress.spec.specType: "integration"
For the test cy.state('runnable')
For
describe('my-context', () => {
it('my-test', () => {
Properties and methods,
const title = cy.state('runnable').title; // "my-test"
const fullTitle = cy.state('runnable').fullTitle(); // "my-context my-test"
const titlePath = cy.state('runnable').titlePath(); // ["my-context", "my-test"]
You can also add metadata to the test
describe('my-context', () => {
it('my-test', { message: "my-message" }, () => {
and grab it in the command overwrite
const message = cy.state('runnable').cfg.message; // "my-message"
I tried this and it worked for me (my version of cypress is 6.1.0):
cy.log(Cypress.mocha.getRunner().suite.ctx.test.title);
More info: https://github.com/cypress-io/cypress/issues/2972

asynchronous loop for in Javascript

I'm trying to iterate and print out in order an array in Javascript that contains the title of 2 events that I obtained from doing web scraping to a website but it prints out in disorder. I know Javascript is asynchronous but I'm new in this world of asynchronism. How can I implement the loop for to print the array in order and give customized info?
agent.add('...') is like console.log('...'). I'm doing a chatbot with DialogFlow and NodeJs 8 but that's not important at this moment. I used console.log() in the return just for debug.
I tried the next:
async function printEvent(event){
agent.add(event)
}
async function runLoop(eventsTitles){
for (let i = 0; i<eventsTitles.length; i++){
aux = await printEvent(eventsTitles[i])
}
}
But i got this error error Unexpected await inside a loop no-await-in-loop
async function showEvents(agent) {
const cheerio = require('cheerio');
const rp = require('request-promise');
const options = {
uri: 'https://www.utb.edu.co/eventos',
transform: function (body) {
return cheerio.load(body);
}
}
return rp(options)
.then($ => {
//** HERE START THE PROBLEM**
var eventsTitles = [] // array of event's titles
agent.add(`This mont we have these events available: \n`)
$('.product-title').each(function (i, elem) {
var event = $(this).text()
eventsTitles.push(event)
})
agent.add(`${eventsTitles}`) // The array prints out in order but if i iterate it, it prints out in disorder.
// *** IMPLEMENT LOOP FOR ***
agent.add(`To obtain more info click on this link https://www.utb.edu.co/eventos`)
return console.log(`Show available events`);
}).catch(err => {
agent.add(`${err}`)
return console.log(err)
})
}
I would like to always print out Event's title #1 and after Event's title #2. Something like this:
events titles.forEach((index,event) => {
agent.add(`${index}. ${event}`) // remember this is like console.log(`${index}. ${event}`)
})
Thanks for any help and explanation!
There no async case here but if you still face difficultly than use this loop
for (let index = 0; index < eventsTitles.length; index++) {
const element = eventsTitles[index];
agent.add(${index}. ${element})
}

Looping through Protractor's code in `it` statement

Relatively new to writing end to end tests with Protractor. Also relatively inexperienced at working with promises.
I am writing a test where in some cases I need to loop through my code b/c the record that I select does not meet certain criteria. In those cases I would like to proceed back to a previous step and try another record (and continue doing so until I find a suitable record). I am not able to get my test to enter into my loop though.
I can write regular e2e tests with Protractor, but solving this looping issue is proving difficult. I know it must be because I'm dealing with Promises, and am not handling them correctly. Although I've seen examples of looping through protractor code, they often involve a single method that needs to be done to every item in a list. Here I have multiple steps that need to be done in order to arrive at the point where I can find and set my value to break out of the loop.
Here are some of the threads I've looked at trying to resolve this:
protractor and for loops
https://www.angularjsrecipes.com/recipes/27910331/using-protractor-with-loops
Using protractor with loops
Looping through fields in an Angular form and testing input validations using Protractor?
Protractors, promises, parameters, and closures
Asynchronously working of for loop in protractor
My code as it currently stands:
it('should select a customer who has a valid serial number', () => {
const products = new HomePage();
let serialIsValid: boolean = false;
let selectedElement, serialNumber, product, recordCount, recordList;
recordList = element.all(by.css(`mat-list.desco-list`));
recordList.then((records) => {
recordCount = records.length;
console.log('records', records.length, 'recordCount', recordCount);
}
);
for (let i = 0; i < recordCount; i++) {
if (serialIsValid === false) {
const j = i + 1;
products.btnFormsSelector.click();
products.formSelectorRepossession.click();
browser.wait(EC.visibilityOf(products.itemSearch));
products.itemSearch.element(by.tagName('input')).sendKeys(browser.params.search_string);
products.itemSearch.element(by.id('btnSearch')).click();
browser.wait(EC.visibilityOf(products.itemSearch.element(by.id('list-container'))));
selectedElement = element(by.tagName(`#itemSearch mat-list:nth-child(${{j}})`));
selectedElement.click();
browser.wait(EC.visibilityOf(products.doStuffForm));
browser.sleep(1000);
element(by.css('#successful mat-radio-button:nth-child(1) label')).click();
browser.sleep(1000);
expect(element(by.css('.itemDetailsContainer'))).toBeTruthy();
product = products.productIDNumber.getText();
product.then((item) => {
serialNumber = item;
if (item !== 'Unknown') {
expect(serialNumber).not.toContain('Unknown');
serialIsValid = true;
} else {
i++
}
})
} else {
console.log('serial is valid: ' + serialIsValid);
expect(serialNumber).not.toContain('Unknown');
break;
}
}
console.log('serial number validity: ', serialIsValid);
})
I have rewritten and reorganized my code several times, including trying to break out my code into functions grouping related steps together (as recommended in one of the threads above, and then trying to chain them together them together, like this:
findValidCustomer() {
const gotoProductSearch = (function () {...})
const searchForRecord = (function () {...})
const populateForm = (function (j) {...})
for (let i = 0; i < recordCount; i++) {
const j = i + 1;
if (serialIsValid === false) {
gotoProductSearch
.then(searchForRecord)
.then(populateForm(j))
.then(findValidSerial(i))
} else {
console.log('serial number validity' + serialIsValid);
expect(serialIsValid).not.toContain('Unknown');
break;
}
}
console.log('serial number validity' + serialIsValid);
}
When I've tried to chain them like that, I received this error
- TS2345: Argument of type 'number | undefined' is not assignable to parameter of type 'number'
Have edited my code from my actual test and apologies if I've made mistakes in doing so. Would greatly appreciate comments or explanation on how to do this in general though, b/c I know I'm not doing it correctly. Thanks in advance.
I would suggest looking into async / await and migrating this test. Why migrate? Protractor 6 and moving forward will require async / await. In order to do that, you will need to have SELENIUM_PROMISE_MANAGER: false in your config and await your promises. In my answer below, I'll use async / await.
Below is my attempt to rewrite this as async / await. Also try to define your ElementFinders, numbers, and other stuff when you need them so you can define them as consts.
it('should select a customer who has a valid serial number', async () => {
const products = new HomePage();
let serialIsValid = false; // Setting the value to false is enough
// and :boolean is not needed
const recordList = element.all(by.css(`mat-list.desco-list`));
const recordCount = await recordList.count();
console.log(`recordCount ${recordCount}`);
// This could be rewritten with .each
// See https://github.com/angular/protractor/blob/master/lib/element.ts#L575
// await recordList.each(async (el: WebElement, index: number) => {
for (let i = 0; i < recordCount; i++) {
if (serialIsValid === false) {
const j = index + 1; // Not sure what j is being used for...
await products.btnFormsSelector.click();
await products.formSelectorRepossession.click();
await browser.wait(EC.visibilityOf(products.itemSearch));
await products.itemSearch.element(by.tagName('input'))
.sendKeys(browser.params.search_string);
await products.itemSearch.element(by.id('btnSearch')).click();
await browser.wait(
EC.visibilityOf(await products.itemSearch.element(
by.id('list-container')))); // Maybe use a boolean check?
const selectedElement = element(by.tagName(
`#itemSearch mat-list:nth-child(${{j}})`));
await selectedElement.click();
// not sure what doStuffForm is but I'm guessing it returns a promise.
await browser.wait(EC.visibilityOf(await products.doStuffForm));
await browser.sleep(1000); // I would avoid sleeps since this might
// cause errors (if ran on a slower machine)
// or just cause your test to run slow
await element(by.css(
'#successful mat-radio-button:nth-child(1) label')).click();
await browser.sleep(1000);
expect(await element(by.css('.itemDetailsContainer'))).toBeTruthy();
const serialNumber = await products.productIDNumber.getText();
if (item !== 'Unknown') {
expect(serialNumber).not.toContain('Unknown');
serialIsValid = true;
}
// The else statement if you were using i in a for loop, it is not
// a good idea to increment it twice.
} else {
// So according to this, if the last item is invalid, you will not break
// and not log this. This will not fail the test. It might be a good idea
// to not have this in an else statement.
console.log(`serial is valid: ${serialIsValid}`);
expect(serialNumber).not.toContain('Unknown');
break;
}
}
console.log('serial number validity: ', serialIsValid);
});
Can you check the count again after updating your code by following snippet
element.all(by.css(`mat-list.desco-list`)).then(function(records) => {
recordCount = records.length;
console.log(recordCount);
});
OR
There is count() function in ElementArrayFinder class which returns promise with count of locator
element.all(by.css(`mat-list.desco-list`)).then(function(records) => {
records.count().then(number => {
console.log(number); })
});

How do I wait until a cookie is set?

I am writing the acceptance tests for my application's login feature. At some point, I want to double-check the cookie's expiry time.
Upon clicking on the "Login" button, a graphql query is sent to my server which responds with a Jwt. Upon reception of the jwt, the application sets the cookie with
document.cookie = ...
In my Cypress test, I check the token in the following way:
Then("sa session s'ouvre pour {SessionDurationType}", expectedDuration => {
cy.get('#graphql').then(() => {
cy.wait(1000)
cy.getCookie('token').then(cookie => {
const tokenDuration = getTokenDuration(cookie.value)
expect(tokenDuration.asSeconds()).to.equal(expectedDuration.asSeconds())
})
})
})
With cy.get('#graphql'), I am waiting for the graphql query to return a response. The alias is defined like this:
cy.stub(win, 'fetch', fetch).as('graphql')
Upon reception, the application sets the cookie.
My problem is that I am not fond of the following call:
cy.wait(1000)
Without that call, I always get an undefined cookie.
Is there a way to get that cookie within some time that might be much less than 1000 ms? I tried many things without success...
You must write a recursive promise function, try the following
function checkCookie() {
// cy.getCookie returns a thenebale
return cy.getCookie('token').then(cookie => {
const tokenDuration = getTokenDuration(cookie.value);
// it checks the seconds right now, without unnecessary waitings
if(tokenDuration.asSeconds() !== expectedDuration.asSeconds()) {
// waits for a fixed milliseconds amount
cy.wait(100);
// returns the same function recursively, the next `.then()` will be the checkCookie function itself
return checkCookie();
}
// only when the condition passes returns a resolving promise
return Promise.resolve(tokenDuration.asSeconds());
})
}
Then("sa session s'ouvre pour {SessionDurationType}", expectedDuration => {
cy.get('#graphql').then(() => {
checkCookie()
.then(seconds => {
expect(seconds).to.equal(expectedDuration.asSeconds())
})
})
})
Note that the function must be improved because
I didn't parametrize the expectedDuration etc. (it's out of the scope of showing you how to do that)
it waits forever without a loop counter check
But it works (I checked in another context before replying to you) and if you have some more troubles please share a "working" GitHub repo so I can clone and check it with your own solution.
Let me know if it isn't enough clear 😉
UPDATE
We (me and Tommaso) have written a plugin to help you with this kind of checks, its name is cypress-wait-until.
Please thank the Open Source Saturday community for that, we developed it during one of them Saturdays 😊
I dont like the timeout in this i have to say for dom changes. I have come up with this solution based on #NoriSte Answer together with DomMutation Observers.
getFileUploadItem().get(".upload-item--state i")
.should("have.class", "ngx-fileupload-icon--start")
.then(item => {
const iconEl = item.get(0);
const states: string[] = [];
return new Promise((resolve, reject) => {
const observer = new MutationObserver((mutations: MutationRecord[]) => {
const mutationEl = mutations[0].target as HTMLElement;
const className = mutationEl.getAttribute("class");
states.push(className);
if (className === "ngx-fileupload-icon--uploaded") {
resolve(states);
}
});
observer.observe(iconEl, {
subtree: true,
attributes: true,
attributeFilter: ["class"]
});
});
})
.then((value) => expect(value).to.deep.equal(
["ngx-fileupload-icon--progress", "ngx-fileupload-icon--uploaded"])
);
Based on #NoriSte's answer, I came up with the following working code:
function awaitNonNullToken(elapsedTimeInMs = 0) {
let timeDeltaInMs = 10
if (elapsedTimeInMs > Cypress.env('timeoutInMs')) {
return Promise.reject(new Error('Awaiting token timeout'))
}
return getTokenCookie().then(cookie => {
if (cookie === null) {
cy.wait(timeDeltaInMs)
elapsedTimeInMs += timeDeltaInMs
return awaitNonNullToken(elapsedTimeInMs)
}
return Promise.resolve(cookie.value)
})
}
I transformed that into an ES6 class that I find a bit more elegant:
class TokenHandler {
constructor () {
this.TIME_DELTA_IN_MS = Cypress.env('timeDeltaInMs')
this.TIMEOUT_IN_MS = Cypress.env('timeoutInMs')
this.elapsedTimeInMs = 0
}
getToken () {
if (this.elapsedTimeInMs > this.TIMEOUT_IN_MS) {
return Promise.reject(new Error('Awaiting token timeout'))
}
return getTokenCookie().then(cookie => {
if (cookie === null) {
cy.wait(this.TIME_DELTA_IN_MS)
this.elapsedTimeInMs += this.TIME_DELTA_IN_MS
return this.getToken()
}
return Promise.resolve(cookie.value)
})
}
}
and reworked my step like this:
cy.get('#graphql').then(() => {
const handler = new TokenHandler
handler.getToken().then(token => {
const tokenDuration = getTokenDuration(token)
expect(tokenDuration.asSeconds()).to.equal(expectedDuration.asSeconds())
})
})
This is working perfectly, thanks.

Topic based pub/sub for JS

I want to be able to have a pubsub mechanism similar to found in jQuery's custom events and PubSubJS (http://github.com/mroderick/PubSubJS).
The problem is that each one of these pubsub libraries does an exact match on the subject. IO want to be able to publish a subject like:
"Order/Sent/1234"
And have a listen subscribe to either:
"Order/Sent/1234"
"Order/Sent/*"
"Order/*/1234"
Does anyone know of anything like this for JS?
Just modify the one you like. Fork it on github, go open pubsub.js and do something like:
var deliverMessage = function(){
var subscribers = [];
for(var n in messages){
if( new RegExp('^' + n + '$').test( message ) ){
subscribers = subscribers.concat( messages[n] );
}
...
}
}
You'll probably need to modify that a bit, do something like replace all *'s with .* or [^\/]* before converting to a regex, etc...
// Paytm's turbo churged Publish - Subscribe Library
export const PaytmConnect = (() => {
const topics = {};
return {
subscribe: (topic, listener) => {
// Create the topic's object if not yet created
if (!topics.hasOwnProperty(topic)) topics[topic] = [];
// Add the listener to queue
const index = topics[topic].push(listener) - 1;
// Provide handle back for removal of topic
return {
remove: () => {
delete topics[topic][index];
}
};
},
publish: (topic, info) => {
// If the topic doesn't exist, or there's no listeners in queue, just leave
if (!topics.hasOwnProperty(topic)) return;
const allProperty = Object.getOwnPropertyNames(topic);
allProperty.forEach((property) => {
if (property.match(topic)) {
// Cycle through topics queue, fire!
topics[topic].forEach((item) => {
item(info !== undefined ? info : {});
});
}
});
}
};
})();
You will have to modify the code if (property.match(topic)) { a bit to satisfy your requirement.

Categories