I'm testing an application that uses iframes, I wanted to know if with cypress it is possible to wait for an iframe to load, because I need to ensure that the iframe has been fully loaded to perform the test.
I tried using a cy.wait(), it even worked, however, setting a time on it makes my test very slow, since there are several use cases in this iframe
The package cypress-iframe has some built-in routines to wait for the iframe to load.
From the source, it waits for the URL, ready state and load event:
const hasNavigated = fullOpts.url
? () => typeof fullOpts.url === 'string'
? contentWindow.location.toString().includes(fullOpts.url)
: fullOpts.url?.test(contentWindow.location.toString())
: () => contentWindow.location.toString() !== 'about:blank'
while (!hasNavigated()) {
await sleep(100)
}
if (contentWindow.document.readyState === 'complete') {
return $frame
}
await new Promise(resolve => {
Cypress.$(contentWindow).on('load', resolve)
})
return $frame
You can use that package directly without needing to implement your own wait strategy, but note the docs are a little out of date if you are using Cypress with version greater than 10.
Related
Why it works without waiting for the page to be full loaded. Also, how can I run a function when the whole page is full loaded on Javascript?
It depends on your definition of "loaded".
The load event will fire...
[...]once a web page has completely loaded all content (including images, script files, CSS files, etc.). (Source)
This is the case here. All the images and script files and CSS files in the original HTML document have been loaded! However, the "loaded" state in case of Twitter is a state in which a script will later initiate the loading of new elements. That is outside the scope of the original page load.
If you want to wait for what you call "loaded", you'd have to ask yourself technically what point that is. I guess the answer would be something like "when element X is visible". In that case you can install a MutationObserver to listen on that element getting added/modified, after identifying what element you want to wait for by checking the DOM in your browser console. (The worse solution would be an interval that checks if that element exists yet.)
For Twitter, this works for me for example (watching for the first story to be loaded):
const observer = new MutationObserver(mutations => {
if (mutations.some(mutation => {
return Array.from(mutation.addedNodes).some(node => node.querySelector('article'))
})) {
setTimeout(() => { // Wait for all the updates of this frame to go through
console.log('Page fully loaded!')
}, 0)
observer.disconnect()
}
})
observer.observe(document, {
attributes: false,
childList: true,
characterData: false,
subtree: true
})
change your onload function syntax to any one of the following and try
window.addEventListener('load', (event) => {
console.log('page is fully loaded');
});
with onload
window.onload = (event) => {
console.log('page is fully loaded');
};
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
My goal is to systematically collect information about every element present on a web page. Specifically, I would like to perform el.getBoundingClientRect() and window.getComputedStyle(el) for each element.
I have been using Selenium WebDriver for NodeJS to load the pages and manage the browser interaction. To simplify, let's just focus on getComputedStyle:
driver.findElements(By.xpath("//*"))
.then(elements => {
var elementsLeft = elements.length;
console.log('Entering async map');
async.map(elements, el => {
driver.executeScript("return window.getComputedStyle(arguments[0]).cssText",el)
.then((styles: any) => {
//stuff would be done here with the styles
console.log(elements.indexOf(el));
});
});
});
This code will loop through all the elements and retrieve their styles, but it is very slow. It may take a few minutes to complete for a page. I would like the driver to execute the scripts asynchronously, but this does not appear possible because each Selenium driver has a 'ControlFlow' that ensures each command to the driver is only started after the last has completed. I need to find a workaround for this so I can execute javascript asynchronously on the page (and make my data gathering faster).
Note: I have also tried Selenium's executeAsyncScript, which turns out to be just a wrapper around executeScript and will still block until it is finished. Here is my code using executeAsyncScript - it performs just as well as the previous code:
driver.findElements(By.xpath("//*"))
.then(elements => {
var elementsLeft = elements.length;
async.map(elements, el => {
driver.executeAsyncScript(
function(el: any) {
var cb = arguments[arguments.length - 1];
cb(window.getComputedStyle(el).cssText);
}, el)
.then((styles: any) => {
//stuff would be done here with the styles
console.log(elements.indexOf(el));
});
});
});
I am looking for a way to either bypass Selenium's ControlFlow in order to execute my javascript asynchronously, find a way to extract the objects and not be bound by the driver, or to find an alternative tool/solution for getting the data that I need.
Since executeScript can take in WebElements, did you see if it is faster to do all the work in one call rather than repeatedly calling executeScript?
driver.findElements(By.xpath("//*"))
.then(elements => {
driver.executeScript("return arguments[0].map(function(el) {return [el, window.getComputedStyle(el).cssText];})", elements)
.then((styles: any) => {
//stuff would be done here with the styles
console.log(styles);
});
});
});
If that is too slow, did you consider locating all the elements inside the script instead of passing them in?
driver.findElements(By.xpath("//*"))
.then(elements => {
driver.executeScript("return Array.prototype.slice.call(document.getElementsByTagName('*'))" +
".map(function(el) {return [el, window.getComputedStyle(el).cssText];})", elements)
.then((styles: any) => {
//stuff would be done here with the styles
console.log(styles);
});
});
});
I'm writing a Javascript script.
This script will probably be loaded asynchronously (AMD format).
In this script, I'd like to do nothing important until the window.load event was fired.
So I listen to the window "load" event.
But if the script is loaded after window.load event... how can I know window.load was already fired?
And of course I don't want to add something in any other scripts (they are all loaded async, the problem is the same) :)
Edit :
Imagine an HTML doc with no Javascript in it at all.
Than someone insert in this doc a tag, and this script tag loads my Javascript file.
This will execute my script.
How this script can know if window.load was already fired ?
No jQuery, not any script in the HTML doc before mine.
Is it possible to know ??
I found the window.document.readystate property. This property is for document "ready" event I guess, not for window "load".
Is there anything similar for window "load" event ?
The easiest solution might be checking for document.readyState == 'complete', see http://www.w3schools.com/jsref/prop_doc_readystate.asp
Quick Answer
To quickly answer the question's title:
document.readyState === 'complete'
Deeper Example
Below is a nice helper if you want to call code upon a window load, while still handling the case where the window may have already loaded by the time your code runs.
function winLoad(callback) {
if (document.readyState === 'complete') {
callback();
} else {
window.addEventListener("load", callback);
}
}
winLoad(function() {
console.log('Window is loaded');
});
Note: code snippets on here actually don't run in the same window context so document.readyState === 'complete' actually evaluates to false when you run this. If you put the same into your console right now for this window it should evaluate as true.
See also: What is the non-jQuery equivalent of '$(document).ready()'?
Handling the Edge Case from #IgorBykov via Comments
Igor brought up an interesting issue in the comments, which the following code can try to handle given a best-effort-timeout.
The problem is that the document.readyState can be complete before the load event fires. I'm not certain what potential problems this may cause.
Some Documentation About the Flow and Event Firing
https://developer.mozilla.org/en-US/docs/Web/API/Document/readyState
Complete: The state indicates that the load event is about to fire.
https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event
Gives a live example of event firing ie:
readyState: interactive
Event Fired: DOMContentLoaded
readyState: complete
Event Fired: load
There's a brief moment where the readyState may be complete before load fires. I'm not sure what issues you may run into during this period.
The below code registers the load event listener, and sets a timeout to check the readyState. By default it will wait 200ms before checking the readyState. If the load event fires before the timeout we make sure to prevent firing the callback again. If we get to the end of the timeout and load wasn't fired we check the readyState and make sure to avoid a case where the load event could potentially still fire at a later time.
Depending on what you're trying to accomplish you may want to run the load callback no matter what (remove the if (!called) { check). In your callback you might want to wrap potential code in a try/catch statement or check for something that you can gate the execution on so that when it calls twice it only performs the work when everything is available that you expect.
function winLoad(callback, timeout = 200) {
let called = false;
window.addEventListener("load", () => {
if (!called) {
called = true;
callback();
}
});
setTimeout(() => {
if (!called && document.readyState === 'complete') {
called = true;
callback();
}
}, timeout);
}
winLoad(function() {
console.log('Window is loaded');
});
Browser navigation performance loadEventEnd metric can be used to determinate if load event was triggered:
let navData = window.performance.getEntriesByType("navigation");
if (navData.length > 0 && navData[0].loadEventEnd > 0)
{
console.log('Document is loaded');
} else {
console.log('Document is not loaded');
}
Based on #CTS_AE's approach, I have put together a solution for envrionments where:
window.addEventListener('load', activateMyFunction); and
window.addEventListener('DOMContentLoaded', activateMyFunction);
don't work.
It requires a single character substitution (eg. from
window.addEventListener('load', activateMyFunction);
to
window_addEventListener('load', activateMyFunction);)
The function window_addEventListener() looks like this:
const window_addEventListener = (eventName, callback, useCapture = false) => {
if ((document.readyState === 'interactive') || (document.readyState === 'complete')) {
callback();
}
}
If you don't want to use jQuery, the logic it uses is:
if( !document.body )
setTimeout( checkAgain, 1 );
So between the windows loaded event and checking if the body property of the document is available, you can check if the DOM is ready
Easy method:
window.onload = (event) => {
console.log('page is fully loaded');
};
You can find other methods from resources here.
what about overriding window.load?
window._load = window.load;
window.load = function(){
window.loaded = true;
window._load();
}
Then check for
if (window.loaded != undefined && window.loaded == true){
//do stuff
}
I have an issue. One of my JS scripts needs Facebook SDK and Twitter widgets JS to load first. Facebook creates FB object, Twitter creates twttr object. Both of them create these objects AFTER my script fires, even though they're loaded from <head>.
I think solution is to periodically check if FB and twttr are defined, and then proceed with executing my script. But I have no idea how to do this.
I tried creating a loop
while (typeof FB === 'undefined' || typeof twttr === 'undefined' || typeof twttr.widgets === 'undefined') {
// run timeout for 100 ms with noop inside
}
But this clearly does not work as it keeps firing timeouts at a high speed and page hangs.
Please help me, I can't sleep because of this issue.
If the scripts are loaded in the normal, synchronous way, then just make sure that your <script> include appears after the library scripts in the document's <head>. If, on the other hand, those scripts are loading objects asynchronously (as seems to be the case), then create something like this:
function whenAvailable(name, callback) {
var interval = 10; // ms
window.setTimeout(function() {
if (window[name]) {
callback(window[name]);
} else {
whenAvailable(name, callback);
}
}, interval);
}
And use it like this:
whenAvailable("twttr", function(t) {
// do something
});
The function given in the second argument to whenAvailable will not execute until twttr is defined on the global window object. You can do the same thing for FB.
Important note: Those libraries probably also provide some built-in way to execute code after they have loaded. You should look for such hooks in their respective documentation.
Have you put your script to be executed on page load? (ie. body onload="do_this ();")
That should make your code execute once all external resources has been loaded.
Regarding the use of setTimeout
setTimeout will return immediately, if you'd like to wait for certain variable to be defined, use something as the below.
function when_external_loaded (callback) {
if (typeof FB === 'undefined' || typeof twtter === 'undefined') {
setTimeout (function () {
when_external_loaded (callback);
}, 100); // wait 100 ms
} else { callback (); }
}
...
when_external_loaded (function () {
alert (FB);
alert (twtter);
});
const checkIfLoaded = ('lib', cb) => {
const interval = setInterval(() => {
if (lib) {
typeof cb === 'function' && cb();
clearInterval(interval);
} else {
console.log('not yet');
}
}, 100);
}
If the Facebook scripts are being loaded asynchronously, Facebook has a supported way to execute code when it's library loads which should be much more efficient than polling for it. See this answer for an example: https://stackoverflow.com/a/5336483/816620.
If the Facebook scripts are being loaded synchronously, then you don't have to wait for them - they will load before any other scripts after them run.