I've got an angular 5.x project. I'm trying to execute some e2e tests with Protractor. I've got a few simple tests running that simply check the browser title and check for the presence of basic elements.
I'm now trying to do more complex tests where I interact with the page. Unfortunately clicking an element causes a nasty hang that never comes back (not even after 30-60 seconds). Any ideas what could be causing this, or how I could even troubleshoot?
Tests that work:
it('should have correct title', () => {
expect(browser.getTitle()).toEqual("My App");
});
it('should have a login button', () => {
let loginButton = element(by.id('btnLogin'));
var untilLoginIsVisible = ExpectedConditions.presenceOf(loginButton);
browser.wait(untilLoginIsVisible, 20000);
expect(loginButton.isDisplayed()).toEqual(true);
});
Test that hangs - note similarity with successful test above
it('show login transition', () => {
let loginButton = element(by.id('btnLogin'));
var untilLoginIsVisible = ExpectedConditions.presenceOf(loginButton);
browser.wait(untilLoginIsVisible, 20000);
//EITHER ONE OF THESE FAILS, AND JUST HANGS FOREVER
loginButton.click().then(() => { console.log("Clicked, yo!"); });
browser.actions().mouseMove(loginButton).mouseDown(loginButton).mouseUp().perform();
});
Other info:
I've tried other buttons on the page - same result
While I get no errors, after ~60 seconds I do get an F in the output, indicating a failed test, but it never moves to the next test
After ~2 minutes it starts spitting out ERROR:process_metrics.cc(105)] NOT IMPLEMENTED which I don't think is the source of the problem (similar unrelated complaints here)
I finally found the cause for this. I'm using multiCapabilities to test various browsers & sizes. Unfortunately my first capability was using mobile emulation:
chromeOptions: {
'mobileEmulation': {
'deviceName': 'iPhone 4'
},
This was the trigger to making clicks completely hang. Below is some more info on what works and what doesn't.
Works on non-mobile emulation, hangs on mobile emulation
loginButton.click().then(() => { console.log("Clicked, yo!"); });
Works with mobile emulation
browser.touchActions().tap(loginButton).perform().then(() => { console.log("Tapped, yo!"); });
It's unfortunate that whatever is causing this made protractor hang completely and not give any errors. Moving to the next capability would have made this more obvious. A follow-up question I'll need to deal with is if all mobile emulation tests need to use tap() in place of click()...
Related
I have a Angular App and I'm running E2E-Tests with Cypress.
A normal Test would open the Site with cy.visit('/') and after the Load it starts to navigate on the site using buttons on the sidebar. The theme used for the menu is PrimeNG Ultima.
The Test runs fine on my local dev-machine, but fails in CI.
It seems like Cypress fires the click-event before Angular has registered the needed logic to handle the click on the link element.
Is there a way to let cypress know when Angular finished its bootstraping?
I would prefer a Solution without extra code inside the main-app but I could make changes inside the Angular Part.
I already tried the following Snippets without Success:
cy.window().its('getAllAngularRootElements' as any).should('exist');
cy.window().then(win => new Cypress.Promise((resolve) => win.requestIdleCallback(resolve)));
Another workaround which partially worked was listening on the pace.js progress-bar included with the PrimeNG-Theme, but it was not reliable enough for all tests.
See also
Cypress Angular When Can Test Start?
You can use the Chrome Debugger Protocol as described in this article When Can The Test Click
The example given is
it('clicks on the button when there is an event handler', () => {
cy.visit('public/index.html')
const selector = 'button#one'
cy.CDP('Runtime.evaluate', {
expression: 'frames[0].document.querySelector("' + selector + '")',
})
.should((v) => {
expect(v.result).to.have.property('objectId')
})
.its('result.objectId')
.then(cy.log)
.then((objectId) => {
cy.CDP('DOMDebugger.getEventListeners', {
objectId,
depth: -1,
pierce: true,
}).should((v) => {
expect(v.listeners).to.have.length.greaterThan(0)
})
})
// now we can click that button
cy.get(selector).click()
})
where cy.CDP is installed from this package cypress-cdp
Retrying the click
Another approach might be to retry the click until it has an effect.
The exact code would be app dependent, but the pattern would be
const clickUntilChanged = (linkSelector, changeSelector, attempt = 0) => {
if (attempt > 10) throw 'Something went wrong'
cy.get(changeSelector).invoke('text')
.then(initial => {
cy.get(linkSelector).click() // attempt a click
cy.get(changeSelector).invoke('text')
.then(next => {
if (initial === next) { // no change
cy.wait(50) // wait a bit then try again
clickUntilChanged(linkSelector, changeSelector, ++attempt)
}
// else click was effective, just exit
})
})
})
clickUntilChanged('menu-item', 'page-header')
Test retries
Another approach is to make use of a small initial test with retries set
it('tests the link click', {retries: {runMode: 10, openMode: 0}}, () => {
cy.get('menu-item').click()
cy.get('page-header')
.should('have.text', 'new-page-header') // fails and retries
// if click had no effect
})
Just wait
Looking at Gleb's latest article Solve The First Click, right in the middle he just adds a hard wait to allow the event listeners to hook up.
This is probably my preferred approach given the simplicity, and who would notice an extra 1 second in a CI run.
cy.visit(...)
if (!Cypress.config('isInteractive')) { // run mode
cy.wait(1000)
}
cy.get('menu-item').click() // should now be functioning
I am running into this problem without finding a good solution. I tried every post that I found in Google but without success. I am isolating the problem so I can share you the exact point. I am searching an specific product in "mercadolibre.com" and I want to sort the list from the lower price to the maximum. To do so you can enter directly here and clicking over the option "Menor Precio".
Doing manually this just works fine but with Cypress I am not being able to do that. This script just runs fine but the last step seems to have no effect.
// Activate intelligence
/// <reference types="Cypress" />
describe("Main test suite for price control", () => {
it("search scanners and order them by ascending price", () => {
// Web access
cy.visitAndWait("https://listado.mercadolibre.com.ar/scanner-blueetooth#D[A:scanner%20blueetooth");
// Order by ascending price
cy.get(':nth-child(2) > .andes-list__item-first-column > .andes-list__item-text > .andes-list__item-primary').click({force: true})
});
});;
Maybe Am I using a bad approach to refer the object?
Best regards!
You may have noticed the polite discussion about dismissing the popups, is it necessary or not.
I believe not, and to rely on dismissing the popups will give a flaky test.
I also think the issue is as #DizzyAl says, the event handler on the dropdown button is not attached until late in the page load.
This is what I tried, using retries to give a bit more time for page loading.
Cypress.on('uncaught:exception', () => false)
before(() => {
cy.visit('http://mercadolibre.com.ar')
cy.get(".nav-search-input").type("scanner bluetooth para auto{enter}");
cy.get('.ui-search-result__wrapper') // instead of cy.wait(3000), wait for the list
})
it('gets the dropdown menu on 1st retry', {retries: 2}, () => {
// Don't dismiss the popups
cy.get('button.andes-dropdown__trigger').click()
cy.contains('a.andes-list__item', 'Menor precio').click()
// page reload happens
cy.contains('.ui-search-result__wrapper:nth-child(1)', '$490', {timeout:20000})
});
I would retry menu until options become visible
Cypress.on('uncaught:exception', () => false)
before(() => {
cy.visit('https://listado.mercadolibre.com.ar/scanner-blueetooth#D%5BA:scanner%20blueetooth%5D')
})
it('retries the menu open command', () => {
function openMenu(attempts = 0) {
if (attempts > 6) throw 'Failed open menu'
return cy.get('button.andes-dropdown__trigger').click()
.then(() => {
const option = Cypress.$('.andes-list:visible') // is list visible
if (!option.length) {
openMenu(++attempts) // try again, up to 6 attempts
}
})
}
openMenu().then(() => {
cy.get('a.andes-list__item:contains(Menor precio)').click()
})
// Verification
cy.contains('.ui-search-result__wrapper:nth-child(1)', '$490', {timeout:20000})
})
It looks like the click event is being added to the dropdown button late, and the click is failing.
See When Can The Test Start? for an discussion.
I tried to adapt the code in the article, but couldn't get it to work.
However, adding a cy.wait() or a cy.intercept() for the last network call works.
cy.intercept('POST', 'https://bam-cell.nr-data.net/events/**').as('events')
cy.visit('https://listado.mercadolibre.com.ar/scanner-blueetooth#D%5BA:scanner%20blueetooth%5D')
cy.wait('#events', {timeout: 20000})
// Page has loaded, verify the top item price
cy.contains('.ui-search-result__wrapper:nth-child(1)', '$1.385')
// Open the sort-by dropdown
cy.get('button.andes-dropdown__trigger').click()
// Choose sort-by lowest price
cy.contains('a.andes-list__item', 'Menor precio').click()
// Verify first item has different price, long timeout to wait for page re-load
cy.contains('.ui-search-result__wrapper:nth-child(1)', '$490', {timeout:20000})
You may be able to shorten the page load wait by picking a different network call to wait on.
I noticed two things on the webpage:
There were two pop-ups that were displayed. Once you click both of them, everything worked as expected.
There are random exceptions being thrown from the webpage, so you have to tell your cypress script to ignore those, for stable tests. Having said that, this is not a good practice as you want your tests to catch exceptions like this. Also, ask your devs to look into it and fix the issue causing the exception to be thrown.
The below script worked for me without any additional timeouts.
cy.visit(
'https://listado.mercadolibre.com.ar/scanner-blueetooth#D%5BA:scanner%20blueetooth%5D'
)
Cypress.on('uncaught:exception', (err, runnable) => {
return false
}) //For ignoring exceptions
cy.get('#newCookieDisclaimerButton').click() //Click Accept cookies button
cy.get('.andes-button--filled > .andes-button__content').click() //Click the pop up button
cy.get('.andes-dropdown__trigger').click() //Clicks the dropdown menu button
cy.get('.andes-list > :nth-child(2)').click() //Clicks the first option from the dropdown
I have made a Cypress script for testing a large site "exploratory", meaning it will parse all the links and elements and interact with them all, in the hunt for unexpected JavaScript- or server-exceptions (side-note: i can't find examples of this online?). It traverses through the entire site clicking like crazy, which works quite well. However from time to time the tests stops on the dreaded "CypressError: cy.click() failed because this element is detached from the DOM."
Cypress requires elements be attached in the DOM to interact with them.
The previous command that ran was:
cy.wrap()
This DOM element likely became detached somewhere between the previous and current command.
I understand this happens as the script is interacting with the page animating and toggling state, and it is ok for the script to skip all these detached elements and continue its run, however i can't manage to work how to ignore the error. You would think it is enough to check the Cypress-dom-helpers, but they don't seem to do the job (see below). I have tried using greater values for cy.wait() but for some reason it still doesn't catch detached elements even though all page manipulation from the last interaction has finished.
Note that the wrap works, but not the click.
if (Cypress.dom.isVisible(element[0]) && !Cypress.dom.isDetached(element[0])) {
cy.wrap(element[0])
// .scrollIntoView()
.click({ force: true })
.then(() => cy.wait(100));
}
I don't want to swallow all unexpected errors, as they might be meaningful... Running out of ideas here.
EDIT
Here is more of my code leading up to cy.wrap(). As you see, it is quite rudimentary to just catch obvious javascript and server-errors:
clickAllElements = () => {
this.getAllClickableElements().each(this.clickElement); //running it twice to toggle state
this.getAllClickableElements().each(this.clickElement);
};
getClickableElementsSelector = () => "button:visible, a:visible[href='javascript:;'], a:visible[href='#'], a:visible[href='javascript:void(null)'], a:not([href])";
getAllClickableElements = () => cy.get(this.getClickableElementsSelector()).not(".disabled");
clickElement = (element) => {
if (Cypress.dom.isVisible(element[0]) && !Cypress.dom.isDetached(element[0])) {
this.checkMainError();
cy.wrap(element[0]) // i tried cy.get here as well to re-get the element, but with the same detached error on click
// .scrollIntoView() //ideally i'd like to have scrollIntoView here as well, but that won't work with hidden elements (as click({force}) does)
.click({ force: true })
.then(() => cy.wait(100));
}
}
Here is a screen shot of the runner, trying out cy.get instead of cy.wrap. The result is identical for cy.wrap:
So I built out a command that I'm using in multiple tests that looks at the page and if a prompt is there then the page is redirected to another page to handle the prompt (in this case approving a schedule). Then it additionally looks at that new page and if the page has some text instead then it redirects to the home page (where the issue lies) OR it clicks the button to approve and redirects to the home page and continues normally through the test.
Cypress.Commands.add('approval_redirect', () => {
cy.get('body').then(($body) => {
if ($body.text().includes(
'You must approve your weekly schedule before starting!'
)) {
cy.get('.container a')
.first()
.click()
cy.get('main').then(($main) => {
if ($main.text().includes('schedule')) {
cy.get('button')
.click()
cy.pause()
} else {
cy.get('ul > button')
.click()
}
})
}
})
})
Right now if it's going to the new page to verify the schedule and does NOT have a button to click it's returning home and then pausing. I put in the pause because it would then continue the test with massive failures.
So for example in one test I have:
it('starts here', function (){
cy.login()
.wait(1000)
cy.approval_redirect()
cy.get('#Route')
.click()
.wait(1000)
})
So in this if it redirects home after not clicking the button I'd like for it to completely stop the test. There's nothing to actually do.
Is there a way to completely stop the runner? How do I put that in my test to check against the command for failure?
I thought I could just wrap a function around the command with something like:
function findFailure(failure,success){cy.get...}
Then instead of cy.pause() I put
failure();
return;
And under the ul > button I put
success();
return;
Then in my test I did:
it('starts here', function (){
cy.login()
.wait(1000)
cy.approval_redirect()
const failure = function(){
this.skip()
}
const success = function(){
cy.get('#Route')
.click()
.wait(1000)
}
})
There are no errors and the test runs but it doesn't actually go through the command now. So how do I conditionally stop the cypress test?
This one is interesting. I commend your resourcefulness, especially since Cypress has strong feelings when it comes to conditional testing due to its asynchronous nature.
I had a similar problem recently. For me, nothing on a page was clickable at a certain point if there was network activity, and a toast would appear saying "Loading...", and nothing could be done until network activity was done and the toast disappeared. Basically, I needed the page to wait as long as it needed for the toast to disappear before continuing (thus conditional testing).
What I did was something like this:
In commands.js:
Cypress.Commands.add('waitForToast', () => {
cy.get('toast-loading', {timeout: 20000}).should('not.exist');
});
Then in the actual test.spec.js, using the command:
cy.waitForToast();
What's happening here is Cypress is pausing the test and looking for the toast, and waiting with a timeout of 20000 milliseconds. If the toast gets stuck, the test will exit/fail. If the toast disappears, the test will continue.
Using asserts so far has been the most effective way to do conditional testing. If you're waiting for a button to appear, use an assert with a longer timeout to wait for it. Then, if it doesn't show up, the test will exit and fail.
In your case, you can assert if you didn't go home.
Alternatively
If you want the test to just end without asserting and failing, you can try something like this:
cy.get('home-page-item`)
.its('length')
.then(numberOfItems => {
if (numberOfItems > 0) {
DO-NOTHING-ASSERT
} else {
CONTINUE-TEST
}
});
Otherwise I don't think Cypress has anything like cy.abortTest(), so you'll have to work around their anti-conditional testing methodology with asserts or if else.
After a bit I noticed an issue with my conditional as it was looking for if a prompt was there then go to the schedule; then if certain text wasn't there then go home (where the pause/desire to abort lived) OR click on the button, go home and finish. I noticed that I didn't put in another else on the first condition.
My solution was to just create a function that contained all the steps for a complete test barring no redirects/redirect that had a button click. Then I put in the failures a console log about what happened to end the test. Something like:
cy.log('Schedule not approved, redirected, no published schedule. Test ended.')
This gives a clean end/break to the test. Albeit wrapping a success/failure would be wonderful.
So i am doing an e2e using protractor and selenium for an angular 2 app, where i have a download button, which opens two new tabs. My test is to check that two new tabs are opened and each tab has one specific value in the url. Both the tabs are opened simultaneously.
Then('the title plan opens in a new tab', function (callback: any) {
console.log('inside then');
browser.getAllWindowHandles().then(function (handles) {
const newWindowHandle = handles[1];
console.log(newWindowHandle);
browser.switchTo().window(newWindowHandle).then(function (){
console.log('inside switch');
chai.expect(browser.driver.getCurrentUrl()).to.eventually.have.string('PBL1899_AUTO_TEST_TP').and.notify(callback);
});
});
});
Then('the title sheet opens in a new tab', function (callback: any) {
browser.getAllWindowHandles().then(function (handles) {
const newWindowHandle = handles[2];
browser.switchTo().window(newWindowHandle).then(function (){
console.log('inside switch');
chai.expect(browser.driver.getCurrentUrl()).to.eventually.have.string('PBL1899_AUTO_TEST_TS').and.notify(callback);
});
});
});
The first 'then' condition works fine and passes. During the second 'then', the control switches correctly to the other tab, which is already loaded, but it waits indefinitely at browser.driver.getCurrentUrl().
If i reverse the then condtions, then too, whichever is executed first works fine, but the second one goes on an indefinite wait. What is happening here?
Funnily though, during this wait period, if i press manual refresh(F5), then the wait ends and the test completes successfully, otherwise it times out.
I even tried introducing an auto_refresh, by using the code:
browser.driver.navigate().refresh()
But that too went on a wait without actually refreshing the window. Is there something with the browser.driver??. I checked and it seems that all the browser.driver methods are on wait.
Any help?? I am really stuck around here. Thanks.
EDIT: So found out that this is happening only if the tabs contain something that is loaded pretty fast. In my case, the tabs where just giving some simple html without any load time, as this was a test.
But when i ran in an SIT server, where the tabs were loading heavy web pages, this worked fine, as during the tab switch, that particular tab would still be loading and test would complete successfully.