If an element is not actionable on the page (in this case, covered by another element) and you try to click it, Cypress will show an error like this:
CypressError: Timed out retrying: cy.click() failed because this element:
<span>...</span>
is being covered by another element:
Great! But is there any way to assert that this is the case, aka that the element cannot be clicked?
This doesn't work:
should.not.exist - the element does exist
should.be.disabled - the element is not disabled
should.not.be.visible - the element is visible (just covered by another, transparent element)
using cy.on('uncaught:exception', ...), since this is not an exception
See the Cypress tests at click_spec.coffee.
it "throws when a non-descendent element is covering subject", (done) ->
$btn = $("<button>button covered</button>")
.attr("id", "button-covered-in-span")
.prependTo(cy.$$("body"))
span = $("<span>span on button</span>")
.css(position: "absolute",
left: $btn.offset().left,
top: $btn.offset().top,
padding: 5, display: "inline-block",
backgroundColor: "yellow")
.prependTo(cy.$$("body"))
cy.on "fail", (err) =>
...
expect(err.message).to.include "cy.click() failed because this element"
expect(err.message).to.include "is being covered by another element"
...
done()
cy.get("#button-covered-in-span").click()
Simplest would be to mimic this test, even though docs recommend only using cy.on('fail') for debugging.
This is similar to a unit test using expect().to.throw() to check that an exception occurs as expected so I feel the pattern is justified here.
To be thorough, I would include a call to click({force: true}).
it('should fail the click() because element is covered', (done) => {
// Check that click succeeds when forced
cy.get('button').click({ force: true })
// Use once() binding for just this fail
cy.once('fail', (err) => {
// Capturing the fail event swallows it and lets the test succeed
// Now look for the expected messages
expect(err.message).to.include('cy.click() failed because this element');
expect(err.message).to.include('is being covered by another element');
done();
});
cy.get("#button-covered-in-span").click().then(x => {
// Only here if click succeeds (so test fails)
done(new Error('Expected button NOT to be clickable, but click() succeeded'));
})
})
As a custom command
I'm not sure how to make the chai extension you asked for, but the logic could be wrapped in a custom command
/cypress/support/index.js
Cypress.Commands.add("isNotActionable", function(selector, done) {
cy.get(selector).click({ force: true })
cy.once('fail', (err) => {
expect(err.message).to.include('cy.click() failed because this element');
expect(err.message).to.include('is being covered by another element');
done();
});
cy.get(selector).click().then(x => {
done(new Error('Expected element NOT to be clickable, but click() succeeded'));
})
})
/cypress/integration/myTest.spec.js
it('should fail the click() because element is covered', (done) => {
cy.isNotActionable('button', done)
});
Note
I was expecting done() to time out when the premise of the test (i.e. that the button is covered) is false.
This does not happen (reason unknown), but by chaining .then() off the 2nd click allows done() to be called with an error message. The then() callback will only be called if the click succeeds, otherwise the cy.once('fail') callback handles click failure (as per Cypress' own test).
Another way without having to catch the error when the element is clicked is to check whether or not pointer-events has been set.
For example:
cy.get('element').should('have.css', 'pointer-events', "none") // not clickable
cy.get('element').should('have.css', 'pointer-events', "auto") // clickable
Not a general answer, but it may be sufficient just to specify the z-indexes if the elements are known and known to overlap:
cy.get('element1').should('have.css', 'z-index', '2');
cy.get('element2').should('have.css', 'z-index', '1');
Related
I want to find out if imprint links are working. Sometimes there are cookie consistent banners and you can not click the link on the page.
But is there a way to find out if there is a second imprint link is clickable on the modal?
export const ttValidateImprintClickable = () => {
cy.log("validateImprintClickable - NCA TESTIFY");
cy.get("a")
.contains("Impressum")
.each((item) => {
let isClick = item.;
debugger;
});
};
Example page https://www.qi-digital.de
Plugin to publish solution open source https://github.com/ncatestify/cypress-base-plugin/blob/main/src/commands/tt-validate-imprint-clickable.ts
The problem is not that you need to find one of the options that is clickable. All the links are all non-clickable because the cookie dialog is covering them.
This is how you can dismiss the cookie dialog and the gray mask which covers the main page
cy.visit('https://www.qi-digital.de');
// in case the cookie is already set and the mask does not appear
// use a conditional check first
cy.get('body').then($body => {
if ($body.find('#SgCookieOptin').length) {
cy.get('.sg-cookie-optin-box-close-button').click()
}
})
// now just click the link
cy.contains('a', 'Impressum').click()
// and confirm the new page appears
cy.contains('h1', 'Impressum', {timeout:10_000}).should('be.visible')
It seems to me that in the test runner, the cookie dialog always appears, in which case you can simplify the test
cy.visit('https://www.qi-digital.de');
// remove the cookie dialog
cy.get('.sg-cookie-optin-box-close-button').click()
// now just click the link
cy.contains('a', 'Impressum').click()
// and confirm the new page appears
cy.contains('h1', 'Impressum', {timeout:10_000}).should('be.visible')
Clicking the "Impressum" link on the cookie modal
This code will click the "Impressum" link that is on the footer of the cookie modal.
I added some code to clear application data to force the modal, but it's not consistently showing the cookie modal.
cy.clearCookies()
cy.clearLocalStorage()
cy.get('#SgCookieOptin') // cookie modal
.find('a:contains(Impressum)') // find the link in the modal footer
.then($a => $a.removeAttr('target')) // stop new browser tab opening
.click()
Take a look at this page Is focusable
Is focusable
Returns a boolean indicating whether an element can receive focus.
Cypress internally uses this method everywhere to figure out whether an element is hidden,
mostly for actionability.
So try mapping the elements to the true/false result of this method
cy.contains('a', 'Impressum')
.each($el => {
const isActionable = Cypress.dom.isFocusable($el)
... // continue test
})
Looking at your gist, this may be what you need
cy.contains('a', 'Impressum')
.filter((index, el) => Cypress.dom.isFocusable(Cypress.$(el)) )
.eq(0)
.click()
Where the filter command takes a function and passes only those that return true.
Since Cypress.dom.isFocusable() needs a jQuery object (same as passed to .each()) we first need to wrap the "raw" element given to .filter().
Next, take the first only in case multiple items are clickable.
Or click multiple items at once like this (but probably you only want one item).
cy.contains('a', 'Impressum')
.filter((index, el) => Cypress.dom.isFocusable(cy.wrap($el)) )
.click({multiple:true})
Using Selection.each(fn) (see the k6 docs), the callback is passed an index and an Element. Element has a different API than Selection, and within the callback I’d like to use the Selection API on the passed Element so that I can operate on each Selection individually.
In jQuery, I’d often do this:
$('li').each(function (index, element) {
let container = $(element).closest('div.listContainer');
// now do something with the `container`
});
I’ve tried inside the callback to do things like $(element) or Selection(element) but it errors saying those are undefined. (Kind of stabbing in the dark, since I don’t see in the docs how to do this.)
My code looks like:
mySelection.each((index, element) => {
// here, I'd like to do element.closest('.someAncestorSelector') if element could be 'wrapped'
})
Is there a way in the jQuery-like Selection API in k6 to do this?
From the k6 docs on Selection.closest:
For each element in the set, get the first element that matches the selector by testing the element itself and traversing up through its ancestors in the DOM tree. [emphasis mine]
Which means that each is unnecessary and will be performed automatically (returning a new Selection instance with the closest elements).
const closestSelection = mySelection.closest('.someAncestorSelector');
closestSelection.each((index, closestElement) => {
// now, do something with closestElement.
});
or as a single chain of expressions:
mySelection.closest('.someAncestorSelector')
.each((index, closestElement) => {
// now, do something with closestElement.
});
Btw, even jQuery implicitly handles collections, so your jQuery code could be changed to:
const containers = $('li').closest('div.listContainer');
containers.each(function (index, container) {
container = $(container);
// now do something with the `container`
});
I have a problem when using cy.getIframeBody().find('#some-button') that the #some-button element is not yet available, because the iframe is still loading, but the body element is not empty so the .find() is triggered.
This is the custom command to get the iframe body
Cypress.Commands.add('getIframeBody', ()=> {
return cy.get('iframe.cy-external-link-container')
.its('0.contentDocument.body')
.should('not.empty')
.then(cy.wrap)
});
How I can do it without using cy.wait()?
You can add random timeouts and .should() assertions, but if they work at all the test is likely to be flaky.
The key is to repeat the query of the body element (this line)
.its('0.contentDocument.body')
until the button shows up.
So not
.its('0.contentDocument.body')
.should('not.empty')
but something like
.its('0.contentDocument.body')
.should('have.child', '#some-button') // force a retry on body query
.should() with callback will do this
Cypress.Commands.add('getIframeBodyWithSelector', (waitForSelector) => {
return cy.get('iframe.cy-external-link-container')
.its('0.contentDocument.body')
.should(body => {
expect(Cypress.$(body).has(waitForSelector).length).gt(0)
})
.then(cy.wrap)
})
it('finds some button', () => {
cy.getIframeBodyWithSelector('#some-button')
.find('#some-button') // passes
})
You can add an timeout and also add should('be.visible'). should assertion will make sure that till the timeout value is reached it rerties and make sure that the iframe is loaded successfully.
Cypress.Commands.add('getIframeBody', () => {
return cy
.get('iframe.cy-external-link-container', {timeout: 7000})
.should('be.visible')
.its('0.contentDocument.body')
.should('not.empty')
.then(cy.wrap)
})
The default Accounts-UI widget takes a while to load. I want to check on the client when it is ready, so that I can perform some DOM manipulations on it afterwards. I am currently using a timer like so:
Template.sign_in_modal.onRendered(function (){
Tracker.afterFlush(function () {
Meteor.setTimeout(function () {
$('a#login-sign-in-link').click();
$('a#login-name-link').click();
$('a.login-close-text').remove();
}, 100);
});
});
The above hack works locally (probably because it loads faster) but not when I push to saturnapi.com. I just want it to be expanded by default as shown below. Is there a way to ensure the UI widget is fully loaded via a template helper or otherwise make it expanded by default?
I would suggest checking when the <a id="login-sigin-in-link"></a> is added to the DOM. This could be verified by checking $('a#login-sign-in-link').length. If the element is on the DOM do your manipulation.
However if it is not just check again in a few milliseconds. I would suggest using setInterval().
See below for the complete solution:
Template.sign_in_modal.onRendered(function (){
var setIntervalId = Meteor.setInterval(function() {
if($('a#login-sign-in-link').length) {
$('a#login-sign-in-link').click();
$('a.login-close-text').remove();
Meteor.clearInterval(setIntervalId);
}
}, 100);
});
Template.sign_in_modal.onDestroyed(function() {
$('.modal-backdrop.fade.in').remove();
});
Some may think that using loginButtons.onRendered(function(){}); is a good way to verify if the element has been added to the DOM, but it is not. If you try to do the same DOM manipulation in onRendered, it will throw an afterFlush error. The onRendered function has been extremely misleading.
I have the following:
function activate(routeData) {
// make dataservice call, evaluate results here. If condition is met, reroute:
if (true){
router.navigateTo("#/someRoute");
}
alert ("should not be shown");
}
The alert is getting hit however, and then the view changes.
How do I fully navigate away from the current item and prevent any further code in that vm from being hit?
Update:
I tried using guardroute but I have to activate the viewModel to call the dataservice that returns the data that determines whether or not I should re-route. Using guardroute totally prevents the dataservice from getting called (since nothing in the activate function will get hit).
I also tried returning from the if block but this still loads the view / viewAttached / etc so the UX is glitchy.
The following worked for me in Durandal 2.0:
canActivate: function() {
if(condition)
return {redirect: 'otherRoute'};
return true;
}
activate: // Do your stuff
It's mentioned in the documentation: http://durandaljs.com/documentation/Using-The-Router.html
Here's #EisenbergEffect answer to a quite similar discussion in google groups.
Implement canActivate on your view model. Return a promise of false,
then chain with a redirect.
You might want to give #JosepfGabriel's example (discussion) a try in Durandal 1.2. Check the correct router syntax for your Durandal version, you might have to substitute it with something like router.navigateTo("#/YourHash", 'replace').
canActivate: function () {
return system.defer(function (dfd) {
//if step 2 has a problem
dfd.resolve(false);
})
.promise()
.then(function () { router.navigate("wizard/step1", { trigger: true, replace: true }); });
}
However this is NOT working in Durandal 2.0 and there's a feature request https://github.com/BlueSpire/Durandal/issues/203 for it.
You can't call redirect into the active method.
You can override the guardRoute method from router, to implement redirections.
You can do somehting like that:
router.guardRoute= function(routeInfo, params, instance){
if(someConditios){
return '#/someRoute'
}
}
You can return a promise, true, false, the route to redirect... You can find more information about that in the next link: http://durandaljs.com/documentation/Router/
Rainer's answer was pretty good and works for me adding this small fix.
Inside the then() block simply call the navigation like this
setTimeout(function() { router.navigateTo('#/YOUR DESTINATION'); }, 200);
that should fix your problem. The setTimeout does the trick. Without it the newly navigated page catches the old NavigationCancel from the previous one.
Adding a return in your if (true) block should fix this.
function activate(routeData) {
if (true){
router.navigateTo("#/someRoute");
return;
}
alert ("should not be shown");
}