I am in a situation where I need to use for loop and If else block in cypress
Scenario:
Once I login to an application, I need to read an element's text which is rounded in the below screenshot.
This element will appear within 20-90 seconds after I log in, when I refreshed the screen. so I need to write something like this, wait for element, if it appears reads the text and returns the value, if not wait for 10 seconds reload the page and do the process again.
function waitAndreload() {
for (let i = 0; i < 10; i++) {
cy.get("#ele").then(ele => {
if (ele.text()) {
return ele.text();
} else {
cy.wait(10000);
cy.reload();
}
});
}
}
How to write this in cypress, as cypress won't support if-else or for loops
Below is a technical solution, but first is want to explain what I believe is a better solution.
Adding to #dwelle comment, it seems like what you're trying to do is not a best-practice in terms of the the design of the test. Tests should be designed to be deterministic and should control all relevant inputs that may affect the expected result.
More specifically, is this text something that a real user should see and use, or only something that the developers put for debugging or testing purposes? If it's for a real user, then what determines if it should appear or not? (Is it purely random?, If so, see below) if it's for testing or debugging purposes, talk to the developers and come up with a better solution in which you can control whether this text appears or not directly. If it's something that the user should see and it's not random, then consider what conditions should be met in order for the text to appear, and design the test in a way that makes this condition true, either by controlling the actual necessary preconditions or by using mocks to simulate that condition. Again, I recommend that you consult with the developers to help you find the best approach.
In case it is "purely" random, then ask the developers to provide a way for you to specify the seed of the random generator, and then you'll be able to control it too.
As promised, in case you still want the technical solution for the specific problem, without redesigning the test, then there trick is to use recursion. Something like this:
function getEnvironment() {
function getEnvironmentInternal(retires) {
if (retires == 0)
throw "text didn't appear after the specified retires";
return ele.text().then(text => {
if(text)
return cy.wrap(text);
cy.wait(10000);
cy.reload();
return getEnvironmentInternal(retires-1);
});
)};
return getEnvironmentInternal(10);
}
// usage:
getEnvironment().then(text => {
// do something with text...
}
I wrote my own helper command for checking repeatedly until condition is fulfilled.
/**
* Waits until call to cb() resolves to something truthy.
*
* #param message {string} Error message on timeout
* #param cb {() => Cypress Command "promise"}
* Callback for checking if condition is met should return cypress command cy.xxxxxx.then()
* which resolves to undefined if polling should continue. Throwing an error aborts before
* waiting for timeout to complete.
*/
Cypress.Commands.add('waitFor', (message, cb, errorReporterCb = null, timeoutMs = 5000) => {
const startTime = new Date().getTime();
const giveupTime = startTime + timeoutMs;
const startTimeout = 5;
const ctx = {};
const errorReporter =
errorReporterCb ||
(err => {
throw err;
});
function checkCb(timeout) {
const currentTime = new Date().getTime();
if (currentTime > giveupTime) {
const err = new Error(`Timeout while waiting for (${currentTime - startTime}ms): ${message}`);
errorReporter(err, ctx);
} else {
cy.wait(timeout);
return cb(ctx).then(result => {
if (result === undefined || result === false) {
return checkCb(timeout * 2); // always wait twice as long as the last time
} else {
return result;
}
});
}
}
return checkCb(startTimeout);
});
With this you can implement polling loop like:
cy.waitFor(
'reload page until #ele contain text',
() => cy.reload().get("#ele").then(ele => ele.text() ? ele.text() : undefined),
null, 60000);
I would say using a for loop for something like this and refreshing is an anti-pattern. It looks like you're waiting for the text to show up in the element, not the element itself.
If so, can you stub the response to the server so it comes back right away? If that doesn't work, just do a cy.wait('#<whatever you aliased your response as>') until the call is completed
So it seems you just want to wait that element is appeared and then take text value.
So something like cy.get('#ele', {timeout: 60000}).should('exist').invoke('text').then(text => ...work with text value)
Assertions in cypress have built-in retry mechanism, so if it fails before timeout expire - it will retry previous command.
You can't use while/for loops with cypress because of the async nature of cypress. Cypress doesn't wait for everything to complete in the loop before starting the loop again. You can however do recursive functions instead and that waits for everything to complete before it hits the method/function again.
Here is a simple example to explain this. You could check to see if a button is visible, if it is visible you click it, then check again to see if it is still visible, and if it is visible you click it again, but if it isn't visible it won't click it. This will repeat, the button will continue to be clicked until the button is no longer visible. Basically the method/function is called over and over until the conditional is no longer met, which accomplishes the same thing as a loop, but actually works with cypress.
clickVisibleButton = () => {
cy.get( 'body' ).then( $mainContainer => {
const isVisible = $mainContainer.find( '#idOfElement' ).is( ':visible' );
if ( isVisible ) {
cy.get( '#idOfElement' ).click();
this.clickVisibleButton();
}
} );
}
Then obviously call the this.clickVisibleButton() in your test. I'm using typescript and this method is setup in a class, but you could do this as a regular function as well.
Related
So I am trying to migrate our existing protractor test suite to cypress. For one of the tests, we have scenario where we can have two expected conditions which easier to handle in protractor. But I was wondering if there is any similar cypress command function to achieve that?? sharing the code snippet
confirmation.getConfirmButton().click().then(() => {
// We will either get a successful cancellation OR an alert modal with an OK
// button saying that the contract cannot be cancelled yet
browser.wait(ExpectedConditions.or(
ExpectedConditions.textToBePresentInElement(this.getViewAllFirstStatus(), "Cancelled"),
ExpectedConditions.elementToBeClickable(this.getModalOkButton())
), 5000);
this.getModalOkButton().isPresent().then((present) => {
if (present) {
this.getModalOkButton().click().then(() => {
browser.sleep(8000).then(() => {
this.cancelFirstContract();
});
});
}
});
});
I think what you want to do in Cypress is use the jQuery multiple selector.
This will essentially wait for one selector or the other, equivalent to the Protractor expression ExpectedConditions.or(...).
Whichever selector appears first will pass on as "subject" on the command chain.
If neither element turns up within 10 seconds, the test fails.
const cancelledStatus = 'span:contains("Cancelled")'; // adjust as appropriate
const modalOkButton = 'button:contains("Ok")'; // adjust as appropriate
const multipleSelector = `${cancelledStatus}, ${modalOkButton}`
cy.get(multipleSelector, {timeout:10000})
.then($subject => {
if ($subject.text() === "Ok") { // equiv to "getModalOkButton().isPresent()"
cy.wrap($subject).click()
}
})
I think the most appropriate way to handle this would be to wait on the first condition to be true, and then continue to check for the second one. As suggested, we can increase the timeout for each condition.
cy.get('foo', { timeout: 15000 })
.should('have.text', 'Cancelled')
.get('bar', { timeout: 15000 })
.should('have.attr', 'enabled') // or whatever property is used to determine clickability
// continue with other actions
I'm seriously stuck here, i'm setting up ApplePay that requires a user initiated event to create an AppleSession, but i need to do a series of API calls before that happens, unfortunately due to business rules they can only happen once the user hits checkout.
So the flow is checkout method gets called, in that method we do an await as the process action gets called, then provided it is successful we initialize ApplePay. Looks something like this.
checkout( event ) {
await this.processPayments();
this.checkoutAP();
}
This fails with an error from ApplePay that it needs a user initiated event to create an ApplePaySession. The await returns a promise and wraps any code following it in it's .then. So the call to checkoutAP no longer has a user initiated event in it's stack/context. So it fails.
I tried using a setInterval and check the state value, it will be updated once the process call is done. The code doesn't wait for a setInterval unless you wrap it in a promise, which brings me back to the same issue.
This is my current hacky mess that doesn't work.
await this.processAmenityPayments(this.getPaymentList()).then(( result ) => {
this.paymentsProcessStatus = PAYMENTS_PROCESS_STATUS_COMPLETE;
}, ( error ) => {
this.paymentsProcessStatus = PAYMENTS_PROCESS_STATUS_ERROR;
this.errors.push(error);
});
let intervalCount = 0;
const interval = setInterval(()=> {
if( this.paymentsProcessStatus === PAYMENTS_PROCESS_STATUS_COMPLETE || this.paymentsProcessStatus === PAYMENTS_PROCESS_STATUS_ERROR) {
clearInterval(interval);
// I can't put it here, it's a different context and fails.
}
intervalCount++;
if( intervalCount > 20 ) {
clearInterval(interval);
return null;
}
}, 1000 );
// my hope is i can wait for the mess to run above, once the state has been updated then
// allow the code to continue at this point.
this.continueCheckout();
Any ideas, no matter how hacky would be most welcome.
Thanks Guys!
TL;DR: using puppeteer, after triggering a button click, which one is the best way to understand what is happening to a page, knowing that either a redirect / history push could happen (and the url change, in a set of known ones, but not necessarily through redirect but also through push into history object) or a dialog might appear (with a known id)?
I'm trying to write a scraper using Puppeteer (very first experience with it, never used before) to navigate a website with the final goal of retrieving a text code, with the challenge that the path to get there is not always the same, and the code might actually not be given.
In the first page - full of ads, therefore slow as well -, I do something like this to wait for the "get code" button to appear (snippet 1):
// ... code to get the page instance ...
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
while(true) {
// Puppeteer won't complain if I don't await for page reload (to avoid the ads),
// as long as I await for the container div before doing anything else.
page.reload(); // No await
await page.waitForSelector("#code-container");
const hasCode = await page.evaluate(() => {
// I cannot click on it already because I realised it could
// cause a "Execution context was destroyed" error
return document.querySelector('#get-code-button') != null;
});
if(!hasCode) {
await sleep(10000);
}
}
// out of the loop, "#get-code-button" exists
And then I click on it (snippet 2):
// For some reason, this method is more reliable than using
// await page.click('#get-code-button').
await page.evaluate(async () => {
document.querySelector('#get-code-button').click()
});
// ... at this point the real troubles begin ...
Now, after the snippet above, a few scenarios might happen:
A dialog might appear, with the "reveal code" button in it (happy days)
A redirect might happen (url change, but it could be either a redirect either a push in the history object), with ads. After clicking on the div with id "continue-without-ads" (to simplify), I end up in one of the next redirects.
A redirect might happen (as above, url change, but it could be either a redirect either a push in the history object), with the "reveal code" button in it (happy days)
A redirect might happen (same as above), with basically written "error: code not available". If I go back from this page, the "get code" button should stay in place, so I could skip snippet 1 and go straight for snippet 2.
Question is, how can I detect in which scenario am I, and act timely (e.g. without waiting for the waitForSelector timeout to happen if I want to check for element to be there)?
As well, is the idea of using page.goBack() to get to the initial link and make another attempt a stupid one (to avoid waiting for the "get-code-button" to appear again, since the page should now be cached in Chrome)?
I want to avoid the headache of myself mashing the refresh button, clicking the "get-code-button" once it appears and go back to retry until I get the code.
I found an escamotage, but I don't think it's the easiest way to achieve what I wanted, neither the most correct ...
My solution is to have two "aggregators" of waiters: (1) one for selectors (a list of IDs, but any selector is just fine), (2) one for page url changes (a list of urls which are gonna trigger the promise if navigated to). Both this aggregators accepts as input a list of string (in one case selectors, in the other urls), and returns the first one to succeed.
The code to check what changed in the page after the click:
/**
* #param page the page to monitor for changes
* #param urls the list of urls that should trigger the redirect monitor
* #param selectors the list of selectors that should trigger the page change
* #param triggerPromise the promise that triggers the events (e.g. mouse click on a button)
* #returns the url or the selector that resulted as a change
*/
async function waitForWinner(page: Page, urls: string[], selectors: string[], triggerPromise: Promise<any>) {
// waitForUrlChange takes in input a list of urls, and returns the first
// one to succeed
const urlChangeMonitor = waitForUrlChange(page, urls);
// hasSelectors takes in input the list of selectors, and returns
// the first that succeeds
const selectorsPromise = hasSelectors(page, selectors);
const results = await Promise.all([
triggerPromise,
Promise.race([ urlChangeMonitor.promise, selectorsPromise ])
]);
urlChangeMonitor.clear();
const winner = results[1];
// This check is quite stupid, but it works for me:
const isRedirect = !winner.startsWith("#");
const isSelector = winner.startsWith("#");
// ... other custom logic here
// Simplification:
return { winner, isRedirect, isSelector }
}
The hasSelectors is a bit trivial, and it's full of custom logic in my case (when there are cookies it accepts them and then keeps going again), the most interesting part is the one to wait for url change.
In my case I realised there is no redirect, thus I suppose it's a push in the history object. Regardless, this method succeeds in listening for url changes in the page:
const unboundResolve = (url: string) => logger.error("Resolved too early, error.");
const unboundReject = () => logger.error("Rejected too early, error.");
export function waitForUrlChange(page: Page, urls: string[], timeout=60000) {
if (urls.length === 0) {
throw Error("Cannot have 0 lenght array of urls.");
}
const deferred = {
resolve: unboundResolve,
reject: unboundReject
};
const promise: Promise<string> = new Promise((resolve, reject) => {
deferred.resolve = resolve;
deferred.reject = reject;
});
let promiseDone = false;
const checkForUrl = (frame: Frame) => {
const isRoot = frame.parentFrame() === null;
if (isRoot) {
// Frame might change, but page doesn't always change.
// Regardless, in this way I can detect url changes which
// occurs without redirect.
const currentUrl = page.url();
for(let u of urls) {
if (~currentUrl.indexOf(u)) {
// Resolve only once
if(!promiseDone) {
promiseDone = true;
deferred.resolve(currentUrl);
} else {
logger.warn(`Found another redirect of interest, but it's too late now.`);
}
clear();
break;
}
}
}
};
const clear = () => {
if (!promiseDone) {
deferred.reject();
promiseDone = true;
}
// Calling it multiple times doesn't make a difference
page.off("framenavigated", checkForUrl);
}
// If url doesn't change in one minute, call it a day
setTimeout(clear, timeout);
page.on("framenavigated", checkForUrl);
// Provide a way to turn off the listener from outside
return { clear, promise };
}
Main idea is to register to framenavigated event and listen for the url to contain one of the inputs (not the best, but works for me). Rather than directly returning a promise, I wrap it into an object which gives the possibility to clear the listener from outside, to keep things tidy.
The approach I presented has vast room for improvement (e.g. rather than having strings being passed around, I could add metadata and then the metadata is returned as well, to avoid very naive checks like .startsWith("#"), or the check for url could be a pattern or a callback), but it works and shows the main idea behind.
I am trying to write tow If Statements in which, if the conditions are coming alternately then my code is working fine.
But when both Statements are true together then the second If Statement always skipped because there is a time difference of 5-6 seconds after the first condition executed.
So even though second condition is valid ( waiting for a window to appear) it never went inside the second if-Statement.
I tried writing timeout but it didn't work in TestCafe.
Someone please help if there is any inbuild function to be use for If-Condition just like it is there for assertion -
// await t.expect('.boarding-pass', { timeout: 10000 });
Similar option is not working under If Condition -
// if ( '.boarding-pass'.exists, { timeout: 10000 }){ do something}
It is working only if the Boarding Pass Screen is appearing, if not then it is waiting for 10 seconds and skip second If Statement.
I am not putting the codes for now. If you really need real codes to resolve my issue then I will try to recreate it using some public application.
You can use the built-in Wait Mechanism for Selectors to conditionally execute test statements.
For example, the following code waits for two elements to appear in DOM (you can specify the timeout option), and if an element exists and is visible, clicks it.
import { Selector } from 'testcafe';
fixture('Selector')
.page('http://devexpress.github.io/testcafe/example');
test ('test1',async (t)=\> {
const selector1 = Selector('#remote-testing');
const selector2 = Selector('#remote-testing-wrong', { timeout: 10000 } ); //wrong id, it will never appear
const element1 = await selector1();
const element2 = await selector2();
if(element1 && element1.visible) {
await t.click(selector1);
}
if(element2 && element2.visible) {
await t.click(selector2);
}
});
Please note, that you should use this approach only if the page contains elements that appear occasionally and unpredictably (like a chat window from a third-party component), and you can't turn them off in a test. If an element always appears on executing the test scenario, the recommended way is to use built-in wait mechanisms for actions like click, typetext, etc
Why is Protractor running every line of code immediately?
So I have a webpage that is not written in angular. That I need my selenium based automation to hit. I have used selenium webdriver-js code to hit it. Example below. Once you login, you are taken to a page with 3 questions. The order of the questions are randomized each time you login. So you may never get the same questions in the same order each time you login.
Question 1) What is your name?
Question 2) What time is it?
Question 3) Wy are you here?
The answers to each question are the last word of the question.
Answer 1) name
Answer 2) it
Answer 3) here
So what I was thinking an easy way to solve this problem is to create an if conditional statement
var foo = browser.driver.findElement(By.id('question1')).getText();
if(foo == What is your name?) {
browser.driver.findElement(By.id('answer1')).sendKeys('name');
}
else {
blah
}
and so forth... etc...
But the problem I am running into is that Protractor immediately runs the if statement before it gets to that point. In the example below, the console immediately prints out the console log 'this sucks', because it runs through the if statement immediately without going through the first steps to get to the page and then checking.
this.foo_test = function() {
console.log('starting foo test');
browser.driver.get('http://my-test-url.com/');
browser.sleep(3000);
browser.driver.findElement(By.id('login')).click();
browser.sleep(3000);
browser.driver.findElement(By.id('user')).sendKeys('user');
browser.driver.findElement(By.id('login_button')).click();
browser.sleep(3000);
console.log('getting variable');
var foo = browser.driver.findElement(By.id('question1')).getText();
console.log(foo);
if (foo == 'What was the name of your first pet?') {
console.log('this is cool');
}
else{
console.log('this sucks');
}
};
Protractor builds on WebdriverJS, which uses an implicit-promise-queuing style of programming. See:
https://github.com/angular/protractor/blob/master/docs/control-flow.md
What that means is that each statement in a protractor test should be read as enqueuing a promise, not as actually executing. So for example, the line:
browser.driver.findElement(By.id('question1')).getText()
Does not return text, but returns a promise to return text. You must pass this promise to the other promise-expecting APIs, or provide a direct handler with .then().
The expect call you see in Protractor tests has been modified to wait for a promise to resolve. So something like:
expect(name.getText()).toEqual('Jane Doe');
Is actually enqueuing a promise to compare the result of the promise on the left to the value on the right.
I don't know much about Protractor specifically but this sounds like an issue of not recognizing asynchronous code. If the first assignment statement is asynchronous, then the rest of the code will run without waiting for it to complete. Hence, the values you expect will not be there when you try to test for them in the IF statement.
Your best bet is to run the rest of the code in a callback or promise .
It appears your page is still loading even though selenium thinks it is complete. This happens alot with dynamic/asynchronous pages.
browser.sleep() is not really appropriate, you never really know how long you need to wait for.
You can investigate the class WebDriverWait, which allows you to wait for an element to appear, or timeout.
Wait<WebDriver> wait = new WebDriverWait(driver, 50); // timeout is 50 secs
wait.until(new Function<WebDriver, Boolean>() {
public Boolean apply(WebDriver driver) {
return (driver.findElement(By.id('question1'))).isDisplayed();
}
});
You can also try running some javascript to check the document ready status :
Wait<WebDriver> wait = new WebDriverWait(driver, 60); // timeout is 60 secs
wait.until(new Function<WebDriver, Boolean>() {
public Boolean apply(WebDriver driver) {
String docReady = "";
Boolean rc = true;
if (null != ((RemoteWebDriver)driver).getSessionId()) {
docReady = String.valueOf(((JavascriptExecutor) driver).executeScript("return document.readyState"));
rc = docReady.equals("complete");
}
return rc;
}
});