How to make cy.visit() wait for ajax requests to complete? - javascript

I would like to use Cypress.Commands.overwrite() to make the cy.visit() method do what it normally does and then wait until a loader element is no longer in the DOM, indicating that AJAX requests have completed. My goal is exactly this instead of creating a new command. The reason is to avoid situations where, e.g., someone might unknowingly use the unmodified cy.visit() and then assert that some elements not exist, when they possibly could exist and just not have loaded yet.
While overwriting the default command, I appear to be running into problems with Cypress' use of promises. From the manual, it is clear how to overwrite cy.visit() when one wants to do stuff before calling the original function. However, I am unable to find an example, where the original function is called first and custom stuff happens only after that.
So what I would like to do with the overwrite() command is this:
Cypress.Commands.add('visitAndWait', (url) => {
cy.visit(url);
cy.get('[data-cy="loader"]').should('not.exist');
});
I have tested and can confirm that the above does what I need it to. Here are some attempts to make this work as an overwrite, all of which fail:
Cypress.Commands.overwrite('visit', (originalFn, url, options) => {
return originalFn(url, options).then(() => cy.get('[data-cy="loader"]').should('not.exist'));
});
Cypress.Commands.overwrite('visit', async (originalFn, url, options) => {
const res = await originalFn(url, options);
await cy.get('[data-cy="loader"]').should('not.exist');
return res;
});
Both fail with this:
Cypress.Commands.overwrite('visit', (originalFn, url, options) => {
originalFn(url, options);
return cy.get('[data-cy="loader"]').should('not.exist');
});
And the last one fails with this:
Is this kind of an overwrite possible at all in Cypress, and if so, how is it done? Thank you!
EDIT:
The test code which causes the error in the last case is here:
cy.visit('/');
cy.get('[data-cy="switch-to-staff"]').click();
Basically it tests a user role mocking panel by clicking a button that should mock a staff role.

As mentioned by #richard-matsen, you should check that your loader exists before waiting for it to disapear. That may be why you get the detached from DOM error with your switch-to-staff element.
Something like this might work for you:
Cypress.Commands.overwrite('visit', (originalFn, url, options) => {
originalFn(url, options);
cy.get('[data-cy="loader"]').should('be.visible');
cy.get('[data-cy="loader"]').should('not.be.visible');
});

You could use cy.wait() to wait for the page to completely load then check for the loader to not exist
it('Visit the app', function () {
cy.visit('http://localhost:3000')
cy.wait(3000)
cy.get('[data-cy="loader"]').should('not.exist');
})
Wait Reference: https://docs.cypress.io/api/commands/wait.html

Related

Cypress - How to switch between elements in iframe

I'm trying to interact with some elements inside an iframe with cypress.
If I use the approach in https://bparkerproductions.com/how-to-interact-with-iframes-using-cypress-io/ for only one element per test, everything is fine.
# commands.js
Cypress.Commands.add(
'iframe',
{ prevSubject: 'element' },
($iframe) => {
return new Cypress.Promise(resolve => {
$iframe.on('load', () => {
resolve($iframe.contents().find('body'))
})
})
})
# landing_page.spec.js
cy.get('iframe').iframe().find('#happybutton').should('be.visible')
However, I want to look for multiple elements, click on them, and check if they are rendered correctly, but if I assign the iframe contents to a variable and reuse it to locate another element (for example, a button), cypress tries to locate the second element (for example, a menu) from the first element (the button, which is doomed to fail, because the button does not contain the menu).
# landing_page.spec.js
let iframeContent = cy.get('iframe').iframe()
iframeContent.find('#happybutton').should('be.visible')
iframeContent.find('#myMenu').should('be.visible')
I tried using different variables, or calling directly cy.get('iframe').iframe(), every time I wanted to interact with different elements, but cypress gets trapped in an infinite loop and the test never ends (but no errors or warnings are produced).
Does anybody knows a way to avoid this infinite loop? As I want to reproduce a sequence of steps to build a test case, it is not possible to isolate each interaction in a different test.
Or does anybody knows of a framework that is more suitable for working with iframes?
The problem is $iframe.on('load', only fires once, so you can't call cy.get('iframe').iframe() twice which is effectively what both .find() commands are doing.
let iframeContent = cy.get('iframe').iframe() doesn't store the iframe body, it stores a "chainer" which is treated like a function or getter.
The "infinite loop" is Cypress waiting for the promise resolve() call the second time, which never happens.
So you can nest the commands like this
cy.get('iframe').iframe().then(body => {
cy.wrap(body).find('#happybutton').should('be.visible')
cy.wrap(body).find('#myMenu').should('be.visible')
});
or you can enhance the command by adding a tag when the load event fires
Cypress.Commands.add('iframe', { prevSubject: 'element' }, ($iframe) => {
return $iframe._hasloaded
? $iframe.contents().find('body')
: new Cypress.Promise(resolve => {
$iframe.on('load', () => {
$iframe._hasloaded = true;
resolve($iframe.contents().find('body'))
})
})
})
Thanks to Marion's answer I found a way to refactor my code, so now it works!
Note: the iframe() function was left untouched
# commands.js
Cypress.Commands.add(
'iframe',
{ prevSubject: 'element' },
($iframe) => {
return new Cypress.Promise(resolve => {
$iframe.on('load', () => {
resolve($iframe.contents().find('body'))
})
})
})
# landing_page.spec.js
cy.get('iframe').iframe().as('iframeContent')
cy.get('#iframeContent').then((iframeContent) => {
cy.get(iframeContent).find('#happybutton').click()
cy.get(iframeContent).find('#myMenu')
cy.get(iframeContent).find('#anotherElement').should('be.visible')
})
The above answers pointed me to the right direction. By omittimg the 'then' phrase and the first cy.get('#iframeContent'), Semiramis' solution can be simplified a bit and made easier to understand like this:
cy.get('iframe').iframe().as('iframeContent')
cy.get('#iframeContent').find('#happybutton').click()
cy.get('#iframeContent').find('#myMenu')
cy.get('#iframeContent').find('#anotherElement').should('be.visible')
For Cypress newbees (like me): Cypress Variables and Aliases

multiple expect in a single function

I am writing the test in protractor and I wrote a function with 2 expect statements like this
this.Then(/^I should see a pane on the right with an Interactions and Remarks tab$/, () => {
return waitForPresence(mobileQADashboard.getTabPanel()).then(()=>{
return mobileQADashboard.selectInteractionsTab().then(()=>{
return waitForPresence(mobileQADashboard.pageElements.viewAllInteractionsLink).then(()=>{
return expect(mobileQADashboard.pageElements.viewAllInteractionsLink.isDisplayed()).to.eventually.be.true;
});
});
});
return waitForPresence(mobileQADashboard.getTabPanel()).then(()=>{
return mobileQADashboard.selectRemarksTab().then(()=>{
return waitForLoader().then(()=>{
return waitForPresence(mobileQADashboard.pageElements.addRemarkButton).then(()=>{
return expect(mobileQADashboard.pageElements.addRemarkButton.isDisplayed()).to.eventually.be.true;
});
})
});
})
});
Is it a fool proof methord and i wanted to know weather it is right to write a function like that
To me it looks like you overdid it a bit. Protractor should already take care of a synchronous execution for normal cases.
As long as your commands execute in the listed order (and it looks like that), you shouldn't need to build such pyramids.
Though, when you use then(), you can resolve a promise immediately, but you also start a new async task and let protractor continue with the lines outside then(). So, the moment you enter the first then() in line 2, the second part of the function gets executed in parallel with the first part (not sure, if that's intended).
About expect in the middle of a case: it works, though it's not best practice. If your expect in the middle fails, the test case continues until the end, but the test case status stays failed. More likely you should have two test cases instead of one.
this.Then(/^I should see a pane on the right with an Interactions and Remarks tab$/, () => {
waitForPresence(mobileQADashboard.getTabPanel());
mobileQADashboard.selectInteractionsTab();
waitForPresence(mobileQADashboard.pageElements.viewAllInteractionsLink);
expect(mobileQADashboard.pageElements.viewAllInteractionsLink.isDisplayed()).to.eventually.be.true;
//gets now executed after the firt expect. In your code it's executed in parallel to the first.
waitForPresence(mobileQADashboard.getTabPanel());
mobileQADashboard.selectRemarksTab();
waitForLoader();
waitForPresence(mobileQADashboard.pageElements.addRemarkButton);
expect(mobileQADashboard.pageElements.addRemarkButton.isDisplayed()).to.eventually.be.true;
});
Also to have expect inside a pageObject is not desired. To write a test case (an it()-block) you should a) stay in control of passed/fail and b) at the same time have no need to look into a pageObject. One should understand the test case without that.
So all in all the proper way seems more something like this:
it(/first case/,function(){
this.ThenFirst();
expect(mobileQADashboard.pageElements.viewAllInteractionsLink.isDisplayed()).to.eventually.be.true;
});
it(/second case/,function(){
this.ThenSecond();
expect(mobileQADashboard.pageElements.addRemarkButton.isDisplayed()).to.eventually.be.true;
});
and then these Page Objects:
this.ThenFirst(/^I should see a pane on the right with an Interactions and Remarks tab$/, () => {
waitForPresence(mobileQADashboard.getTabPanel());
mobileQADashboard.selectInteractionsTab();
waitForPresence(mobileQADashboard.pageElements.viewAllInteractionsLink);
};
this.ThenSecond(/^I should see a pane on the right with an Interactions and Remarks tab$/, () => {
waitForPresence(mobileQADashboard.getTabPanel());
mobileQADashboard.selectRemarksTab();
waitForLoader();
waitForPresence(mobileQADashboard.pageElements.addRemarkButton);
});

Starting Alexa Skill in a specific state

Earlier I ran into the issue of Alexa not changing the state back to the blank state, and found out that there is a bug in doing that. To avoid this issue altogether, I decided that I wanted to force my skill to always begin with START_MODE.
I used this as my reference, where they set the state of the skill by doing alexa.state = constants.states.START before alexa.execute() at Line 55. However, when I do the same in my code, it does not work.
Below is what my skill currently looks like:
exports.newSessionHandler = {
LaunchRequest () {
this.hander.state = states.START;
// Do something
}
};
exports.stateHandler = Alexa.CreateStateHandler(states.START, {
LaunchRequest () {
this.emit("LaunchRequest");
},
IntentA () {
// Do something
},
Unhandled () {
// Do something
}
});
I'm using Bespoken-tools to test this skill with Mocha, and when I directly feed IntentA like so:
alexa.intended("IntentA", {}, function (err, p) { /*...*/ })
The test complains, Error: No 'Unhandled' function defined for event: Unhandled. From what I gather, this can only mean that the skill, at launch, is in the blank state (because I have not defined any Unhandled for that state), which must mean that alexa.state isn't really a thing. But then that makes me wonder how they made it work in the example code above.
I guess a workaround to this would be to create an alias for every intent that I expect to have in the START_MODE, by doing:
IntentA () {
this.handler.state = states.START;
this.emitWithState("IntentA");
}
But I want to know if there is a way to force my skill to start in a specific state because that looks like a much, much better solution in my eyes.
The problem is that when you get a LaunchRequest, there is no state, as you discovered. If you look at the official Alexa examples, you will see that they solve this by doing what you said, making an 'alias' intent for all of their intents and just using them to change the state and then call themselves using 'emitWithState'.
This is likely the best way to handle it, as it gives you the most control over what state and intent is called.
Another option, assuming you want EVERY new session to start with the same state, is to leverage the 'NewSession' event. this event is triggered before a launch request, and all new sessions are funneled through it. your code will look somewhat like this:
NewSession () {
if(this.event.request.type === Events.LAUNCH_REQUEST) {
this.emit('LaunchRequest');
} else if (this.event.request.type === "IntentRequest") {
this.handler.state = states.START;
this.emitWithState(this.event.request.intent.name);
}
};
A full example of this can be seen here (check out the Handlers.js file): https://github.com/alexa/skill-sample-node-device-address-api/tree/master/src
I would also recommend reading through this section on the Alexa GitHub: https://github.com/alexa/alexa-skills-kit-sdk-for-nodejs#making-skill-state-management-simpler
EDIT:
I took a second look at the reference you provided, and it looks like they are setting the state outside of an alexa handler. So, assuming you wanted to mimic what they are doing, you would not set the state in your Intent handler, but rather the Lambda handler itself (where you create the alexa object).
exports.handler = function (event, context, callback) {
var alexa = Alexa.handler(event, context);
alexa.appId = appId;
alexa.registerHandlers(
handlers,
stateHandlers,
);
alexa.state = START_MODE;
alexa.execute();
};

javascript $.get() function execute time

Here are my demo code:
doGet('../loaderDemo/1.lst');
doGet('../loaderDemo/2.lst');
doGet('../loaderDemo/3.lst');
doGet('../loaderDemo/4.lst');
doGet('../loaderDemo/5.lst');
function doGet(filename) {
$.get(filename,function (data) {
console.log(data + filename);
});
}
the line "console.log(...)" may not be executed as the order of doGet(), the output contents is not as the order of 1.lst -> 2.lst -> 3.lst -> 4.lst -> 5.lst.
Actually the output order is just random in each execution.
how could I let it outputs in order?
Thanks for your help :-)
-------------Update-------------------
the ".lst" files are 3D models that I want to load. I just want to load the models in order so that I can render an animation properly. so which is the best solution in this case?
each ".lst" files includes the information of one frame. and in this demo,the outputs of "console.log()" must be in order as 1.lst -> 2.lst -> 3.lst -> 4.lst -> 5.lst so that I can handle rendering a frame animation.
jQuery $.get returns a Promise (of sorts)
So, with minimal rewrite, you can do as follows
doGet('../loaderDemo/1.lst')
.then(function() {
doGet('../loaderDemo/2.lst');
})
.then(function() {
doGet('../loaderDemo/3.lst');
})
.then(function() {
doGet('../loaderDemo/4.lst');
})
.then(function() {
doGet('../loaderDemo/5.lst');
});
function doGet(filename) {
// added return
return $.get(filename,function (data) {
console.log(data + filename);
});
}
If, however, the order of download completion is not important, but order of "processing" is - you can use jQuery.when to "wait" for all the downloads to complete, then process the results in the order required
$.when(doGet('../loaderDemo/1.lst'),
doGet('../loaderDemo/2.lst'),
doGet('../loaderDemo/3.lst'),
doGet('../loaderDemo/4.lst'),
doGet('../loaderDemo/5.lst')
)
.done(function(v1, v2, v3, v4, v5) {
[].forEach.call(arguments, function(arg) {}
console.log(arg.data, arg.filename);
})
});
function doGet(filename) {
return $.get(filename)
.then(function(data) {
// need this so we can access the filename and the data for each result
return {
data: data,
filename: filename
};
});
}
Welcome to the world of asynchronous programming. The best way to handle this is to call all 5 asynch functions at the same time, but delay the execution of the console log statements until they are all completed, and then run them in order.
(This is, of course, assuming that it's really important to run them in the same order all the time. An even better solution might be refactoring your code so that it doesn't matter which one completes first)
Here's an example for your problem.
Mostly, this is way faster than any of the sequential solutions posted because it's going to run 5 calls at the same time instead of 5 calls one after the other.
return $.when(
doGet('../loaderDemo/1.lst'),
doGet('../loaderDemo/2.lst'),
doGet('../loaderDemo/3.lst'),
doGet('../loaderDemo/4.lst'),
doGet('../loaderDemo/5.lst'),
).done( function(res1, res2, res3, res4, res5 ) {
console.log(res1),
console.log(res2),
console.log(res3),
console.log(res4),
console.log(res5),
});
(My previous edit used $q, but it turns out that jquery has a built-in thing that works almost the same)

Qunit error: assertion outside test context

I've searched all over and it appears this error is due to not using asyncTest properly. However, per the documentation, it appears that I am doing it correctly. I'm guessing I'm missing a small detail somewhere and need an extra pair of eyes...
I'm trying to test some code that makes an ajax request to get a page and then loads it in a lightbox. lightbox-content does not show up in the DOM until after the ajax call has completed and can be displayed. So, I can only check for it in my onComplete call back, which is where I have my test to see if it loaded it correctly.
Here is my code:
asyncTest('mytest', 1, function() {
utils.lightbox.show('/login', {
onComplete: function() {
ok($('#lighbox-content').is(':visible'), 'Lightbox loaded the /login page.');
start();
}
});
});
I get the error:
Uncaught Error: assertion outside test context, was at HTMLDivElement.window.utils
Can anyone see where I'm going wrong?
I agree that your code matches the documentation as far as I can tell.
Update
Even though the documentation doesn't show it, I wonder if you must tell QUnit to stop at some point so it knows to wait after the test function returns. I would think that QUnit assumes this since it's an async test, but it's worth a shot.
asyncTest('mytest', 1, function() {
stop();
...
});
I've been using Sinon.JS to avoid making the AJAX calls in the first place. This has three immediate benefits:
I don't depend on a server to respond to the requests.
I can specify different results for each test.
The tests run much faster.
The mocking can be done at the XMLHttpRequest level or on the jQuery method and is quite easy. Here's an example from one of my tests:
module("geo", {
setup: function () {
this.server = sinon.fakeServer.create();
},
teardown: function () {
this.server.restore();
}
}
test("returns detected ZIP code", function () {
this.server.respondWith("/geo/detect-zip-from-ip",
[ 200, { "Content-Type": "text/html" }, '90210' ]);
geo.detectZip(function (zip) {
assertThat(zip, is('90210'));
});
this.server.respond();
});
I have found a solution for my case, hope your problem has the same source.
Explaining in words:
I have a complicated asynchronous test
I have delayed events, and there are ok and equal assertions inside
Of course, all this is wrapped inside asyncTest
But, when the test is "completed" and I call start(), the event handlers remain there
After calling start(), all further calls of ok inside that asyncTest become illegal
And throw exceptions
I wonder what happens if the number in expect(in your example it's the second parameter) is exceeded. The same exception?
Explaining in code:
asyncTest('mytest', /*1,*/ function() {
function imgLoadedOrFailed (result) {
clearTimeout(imageTimeToLive);
img.off();
ok(result, 'Image in carousel pane has been loaded');
}
var imageTimeToLive = setTimeout(
imgLoadedOrFailed.bind(this, false),
5000),
img = panes[index].find('img:first');
if (img) {
img.on('load', imgLoadedOrFailed.bind(this, true));
img.on('error', imgLoadedOrFailed.bind(this, false));
}
});
// at some point I call: start();
In this example, when I "finish" the test calling start(), the onload and onerror events can still happen.

Categories

Resources