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
Related
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
I came across the following code:
let timeoutHandler;
clearTimeout(timeoutHandler);
timeoutHandler = setTimeout(() => {...});
This is an overly simplification since the original code is contained in a Vue application as follow:
public handleMultiSelectInput(value): void {
if (value === "") {
return;
}
clearTimeout(this.inputTimeoutHandler);
this.inputTimeoutHandler = setTimeout(() => {
axios.get(`${this.endpoint}?filter[${this.filterName}]=${value}`)
.then(response => {
console.log(response);
})
}, 400);
}
Does this mean this is some kind of cheap-ass debounce function?
Could someone explain what this exactly means.
Yes, it is a debounce function, which is when we wait for some amount of time to pass after the last event before we actually run some function.
There are actually many different scenarios where we might want to debounce some event inside of a web application.
The one you posted above seems to handle a text input. So it's debouncing the input, meaning that instead of fetching that endpoint as soon as the user starts to enter some character into the input, it's going to wait until the user stops entering anything in that input. It appears it's going to wait 400 milliseconds and then execute the network request.
The code you posted is kind of hard to read and understand, but yes, that is the idea of it.
I would have extracted out the network request like so:
const fetchData = async searchTerm => {
const response = await axios.get(`${this.endpoint}?filter[${this.filterName}]=${value}`);
console.log(response.data);
}
let timeoutHandler;
const onInput = event => {
if (timeoutHandler) {
clearTimeout(timeoutHandler);
}
timeoutHandler = setTimeout(() => {
fetchData(event.target.value);
}, 400);
}
Granted I am just using vanilla JS and the above is inside a Vuejs application and I am not familiar with the API the user is reaching out to. Also, even what I offer above could be made a lot clearer by hiding some of its logic.
The cypress docs(https://docs.cypress.io/guides/core-concepts/variables-and-aliases.html#Elements) are pretty unclear on how alias and variables can be used to store information during a test.
I'm trying to store the text of a div on one page to use later, for example:
// let vpcName;
it('Store current name to use later', () => {
// save name for later use - doesn't work
// cy.get('#value').then(elem => {
// vpcName = Cypress.$(elem).text;
// });
// using alias - also doesn't work
cy.get('#value')
.invoke('text')
.as('vpcName');
});
it('Use previous value to return to correct page', () => {
cy.contains(this.vpcName).click();
});
I just came across this article that explains how and why you store a variable and then use it later in the "Cypress way":
https://www.stevenhicks.me/blog/2020/02/working-with-variables-in-cypress-tests/
In respect to how it should work, here is my example which first, collects the message (the message is shown for only 3 secs then disappears). Secondly, it gets the value using the # sign. Lastly my code passes the stored message through to an empty function constructed to assert that the value Portsmouth is contained within the message.
it('Current Port change', () => {
cy.get('#viewport').find('div[id=message]').then(message => {
let wags = message;
cy.wrap(wags).as('wags')
});
cy.get('#wags').then(wags => {
expect(wags).to.contain("Portsmouth")
});
});
let me know if you need further clarification
Try this:
cy.get('button').then(($btn) => {
const txt = $btn.text()
// $btn is the object that the previous command yielded
})
Source: https://docs.cypress.io/guides/core-concepts/variables-and-aliases.html#Return-Values
I had to resort to
writeFile
readFile
When I was trying to save cookies from one test to another in Cypress
An alternative solution which doesn't actually store the element to a variable but serves some of the same purpose is to wrap the cy.get call in a function. Such as:
const btn = () => cy.get('[data-testid="reset-password-button"]')
btn().should('have.attr', 'disabled')
cy.get('[data-testid="new-password-input"]').type('someNewPW')
btn().should('not.have.attr', 'disabled')
This could be a good option if readability is your main goal.
I've been struggling with this for few days. Here is my approach if you want to use the saved text (for example) multiple times. However it seems too long to me and I think it could be optimized. Tested on Cypress v11.2.0
cy.xpath("//tbody/tr[2]/td[2]").then(($text) => {
let txt = $text.text()
//expect(txt).to.eq('SomeText')
cy.wrap(txt).as('txt')
})
cy.get('#txt').then(txt => {
//expect(txt).to.contain("SomeText")
cy.xpath("xpath of search field").type(txt)
})
Is there any way to assert that an element is never visible at any point when routing within Cypress?
I have a server-rendered web app that is sometimes showing a "loading" state when it shouldn't. So when I navigate between pages, a "loading" indicator is showing for a few seconds and then disappearing.
I know that Cypress's assertions will sometimes "wait" -- in this case Cypress waits until loading indicator goes away and that makes the test think that it has passed. But I want the test to fail because the loading indicator was visible at some point.
I'm using these two assertions:
cy.get('[data-test="loading"]').should('not.exist');
cy.get('[data-test="loading"]').should('not.be.visible');
But both of them are passing because the loading indicator goes away.
I've checked through all the documentation but there doesn't seem to be some kind of method for checking that an element is never visible. Is there some method I'm missing or some hack to test this a different way?
I might be crazy, and i have not tested this yet, but I wanted to throw this out there
I assume you are testing that there should NEVER be a loading indicator and it is waiting the default 4 seconds and the indicator goes away, and thus your test pass. So below I set the wait to zero, so it does not wait. I am also confused as to why you don't fix the actual code so you don't see the indicator if you are not supposed to. Perhaps you don't have access to the code..
cy.get('[data-test="loading"]',{ timeout: 0 }).should('not.exist');
cy.get('[data-test="loading"]',{ timeout: 0 }).should('not.be.visible');
Cypress has a lite version of jQuery, so we can watch for changes to the parent of the element that should not exist.
#Maccurt's tests are applied whenever a change occurs.
You want to keep watch firing to a minimum, so find the immediate (or nearest) parent of tested element.
Note this covers exists tests, but should not be necessary for visible tests if the element is present all the time but is just not visible.
In this example a button is added to body.
The first test watches for a span (which is never added so the test succeeds).
The 2nd test watches for the button and fails.
describe('watching for an element to not appear', () => {
const watchAndTest = function(parentSelector, assert) {
Cypress.$(parentSelector).bind('DOMNodeInserted', function(event) {
assert()
});
}
it('should succeed because "span" does not exist', () => {
const parentSelector = 'body'
const watchForSelector = 'span'
watchAndTest(parentSelector,
() => {
// Place your 'assert' code here
cy.get(`${parentSelector} ${watchForSelector}`,{ timeout: 0 })
.should('not.exist');
}
)
// Place your 'act' code here
cy.get(parentSelector).then(parent => {
var newElement = document.createElement('button');
parent[0].appendChild(newElement)
})
Cypress.$(parentSelector).unbind('DOMNodeInserted')
})
it('should fail because "button" exists', () => {
const parentSelector = 'body'
const watchForSelector = 'button'
watchAndTest(parentSelector,
() => {
// Place your 'assert' code here
cy.get(`${parentSelector} ${watchForSelector}`,{ timeout: 0 })
.should('not.exist');
}
)
// Place your 'act' code here
cy.get(parentSelector).then(parent => {
var newElement = document.createElement('button');
parent[0].appendChild(newElement)
})
Cypress.$(parentSelector).unbind('DOMNodeInserted')
})
})
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);
});
});
});