I'm currently doing some automation, and I'm having trouble getting over the Plaid iframe. This how it looks inside of my app:
This how is setup inside of my app:
<div class="PaneActions PaneActions--no-padding-top"><button
class="TextButton TextButton--is-action TextButton--is-threads-treatment TextButton--no-margin">
<p class="FinePrint FinePrint--is-threads-treatment">By selecting “Continue” you agree to the <u>Plaid End User
Privacy Policy</u></p>
</button><button
class="Touchable-module_resetButtonOrLink__hwe7O Touchable-module_block__WBbZm Touchable-module_wide__EYer3 Button-module_button__1yqRw Button-module_large__1nbMn Button-module_centered__3BGqS"
id="aut-continue-button" type="button" role="button"><span class="Button-module_flex__2To5J"><span
class="Button-module_text__38wV0">Continue</span></span></button></div>
I'm getting the parent and the child elements, I'm looking by the text, and many other options and I'm unable to test this product. Does anyone has been working with plaid before?
Using the Plaid demo page as a test app and following the steps in Working with iframes in Cypress, I managed to get a consistently working test.
From the blog, I used this sequence to ensure the iframe body has fully loaded
iframe -> body -> should.not.be.empty
The page loads a placeholder first while is waits for a GET request to complete, so just getting a loaded iframe body is not sufficient.
placeholder
<p>iframe placeholder for https://cdn.plaid.com/link/v2/stable/link.html?isLinkInitialize=true&origin=https%3A%2F%2Fplaid.com&token=link-sandbox-170bce6a-fe90-44a4-8b8a-54064fbc8032&uniqueId=1&version=2.0.917</p>
We need to wait for the "Continue" button, which takes a bit of time to show so give it a long timeout.
Using .contains('button', 'Continue', { timeout: 10000 }) actually returned two button, although there is only one visible.
I changed the button selector to use an id and all works consistently.
The test
cy.visit('https://plaid.com/demo/?countryCode=US&language=en&product=transactions');
cy.contains('button', 'Launch Demo').click()
cy.get('iframe#plaid-link-iframe-1', { timeout: 30000 }).then(iframe => {
cy.wrap(iframe) // from Cypress blog ref above
.its('0.contentDocument.body')
.should('not.be.empty') // ensure the iframe body is loaded
.as('iframeBody') // save for further commands within iframe.body
//.contains('button', 'Continue', { timeout: 10000 }) // returns 2 buttons!
.find('button#aut-continue-button', { timeout: 10000 }) // this is the best selector
.click()
cy.get('#iframeBody')
.contains('h1', 'Select your bank') // confirm the result of the click()
})
You will have to do the call for your app link
You will have to add the following code:
describe('Plad Testing', () => {
it('Request Loan', () => {
//cy.find('Button-module_large__1nbMn', [0], { timeout: 10000 }).click()
cy.visit('https://plaid.com/demo/?countryCode=US&language=en&product=transactions');
cy.contains('button', 'Launch Demo').click()
cy.get('iframe#plaid-link-iframe-1', { timeout: 30000 }).then(iframe => {
let plaid = cy.wrap(iframe)
plaid // from Cypress blog ref above
.its('0.contentDocument.body')
.should('not.be.empty') // ensure the iframe body is loaded
.as('iframeBody') // save for further commands within iframe.body
//.contains('button', 'Continue', { timeout: 10000 }) // returns 2 buttons!
.find('button#aut-continue-button', { timeout: 10000 }) // this is the best selector
.click()
let plaid_choose_bank = cy.get('#iframeBody')
plaid_choose_bank
.contains('h1', 'Select your bank') // confirm the result of the click()
.xpath('/html/body/reach-portal/div[3]/div/div/div/div/div/div/div[2]/div[2]/div[2]/div/ul/li[1]/button/div/div[2]/p[1]').click()
let plaid_bank_username = cy.get('#iframeBody')
plaid_bank_username
.find('input[name="username"]').type('user_good', { delay: 100 })
let plaid_bank_password = cy.get('#iframeBody')
plaid_bank_password
.find('input[name="password"]').type('pass_good', { delay: 100 })
let plaid_bank_button = cy.get('#iframeBody')
plaid_bank_button
.find('button#aut-submit-button').click()
})
})
})
It could be possible that you get that you are not able to find the body of your iFrame. To solve this issue, we will need to add some configuration to the Cypress.json file:
{
"chromeWebSecurity": false,
"pageLoadTimeout": 300000,
"viewportWidth": 1536,
"viewportHeight": 960,
"includeShadowDom": true,
}
Chrome web security will prevent any CORS security from fire-up inside of the current test scenario that we have since you will have that 0.contentDocument.body will return null if the parent origin is different from the iframe origin. This will cause the CORS security issue!
Page load time will help to slow loading the pages and have more time to process things
Viewport will help to make the browser window render like a laptop screen
Include shadow dom will make it easier to look for this type of element without including the "includeShadowDom" inside of your find() elements.
All of the other answers here use *.plaid.com as the origin, which is why contentDocument is not null.
If you are testing this in the real world, you will be running a cross-origin iframe which causes contentDocument to be null a per the MDN page
Cypress is in the process of adding official iframe support, but until then you can use cypress-iframe which just worked for me out of the box.
My test
it.only('users should be able to import holdings from their broker', () => {
cy.visit('/portfolios/created');
cy.findByText('Import from broker').click();
cy.frameLoaded();
cy.iframe().findByText('Continue').click();
});
I'm following an example from nighwatch page:
https://github.com/nightwatchjs/nightwatch/blob/master/examples/tests/onbeforeunload.js
with custom command for onbeforeunload event.
module.exports = {
disabled: true,
'go to page with unload handler': function(client) {
client
.url('https://web.archive.org/web/20211028110528/http://www.4guysfromrolla.com/demos/OnBeforeUnloadDemo1.htm')
.waitForElementVisible('body', 1000);
},
'navigate away from page WITH unload handler': function(client) {
var hasDialog = false;
client
.hasOnBeforeUnload(function(result) {
this.verify.equal(result, true, 'The page should have an onbeforeunload handler');
hasDialog = result;
})
.url('http://google.com', function() {
if (hasDialog) {
this.acceptAlert();
}
})
.waitForElementVisible('body', 1000);
},
///....
/// other parts
}
Expect: has alert for changing url
Actual: Nothing. This test is passed!. Even if no alert is displayed when i tried to switch to new url http://google.com from current url https://web.archive.org/web/20211028110528/http://www.4guysfromrolla.com/demos/OnBeforeUnloadDemo1.htm
Logs:
i Connected to localhost on port 4444 (3241ms).
Using: firefox (74.0.1) on windows 10.0 platform.
Running: go to page with unload handler
√ Element was visible after 24 milliseconds.
OK. 1 assertions passed. (661ms)
Running: navigate away from page WITH unload handler
√ Passed [equal]: The page should have an onbeforeunload handler
Error while running .acceptAlert() protocol action: An attempt was made to operate on a modal dialog when one was not open.
√ Element was visible after 14 milliseconds.
.....
P/s: its OK if i do above steps manually instead of nightwatch test
Test is still passed after removing this.acceptAlert(); function
Do you know root causes of this issue?
Thanks
I'm new to Selenium and I'm running my selenium script on Browserstack.
Everything works fine, until i reach the bottom 10% of my page.
I get the following error:
Uncaught WebDriverError: Appium error: unknown error: Element is not clickable at point (20, 324). Other
element would receive the click: ...
(Session info: chrome=58.0.3029.83)
(Driver info: chromedriver=2.29.461571 (8a88bbe0775e2a23afda0ceaf2ef7ee74e822cc5),platform=Linux
3.19.8-100.fc20.x86_64 x86_64)
This is my code:
describe.only(testTitle, function () {
before(function () {
driver = driverConfiguration.getDriverConfiguration(testTitle, 'chrome')
})
after(function (done) {
driver.quit().then(done)
})
it('Sample tests', function (done) {
driver.get('https://www.test.com').then(function(){
driver.findElement(webdriver.By.name('cardNumber')).sendKeys('0000000000').then(function(){
driver.findElement(webdriver.By.id('billingLine1')).sendKeys('test');
driver.findElement(webdriver.By.id('billingLine2')).sendKeys('test');
driver.findElement(webdriver.By.id('billingCity')).sendKeys('San Jose');
driver.findElement(webdriver.By.id('agree')).click(); // ERROR!!!!!
}).then(function() {
driver.quit().then(done);
})
});
})
})
When I do the following:
// return driver.wait(function() {
// return driver.findElement(webdriver.By.id('agree')).isDisplayed();
// }, 1000);
It says True. The element is visible.
Using Chrome on Samsung Galaxy S8
I'm not sure how to solve this problem.
You've omitted the most important part of the error message in your question
Other element would receive the click: ...
The element in the ... section was the element that was blocking the click. As you discovered, Selenium was reporting that the element was displayed/visible. This error message is just stating that when Selenium attempted to click on the element, another element was blocking the click. If you take a look at the HTML of the blocking element and search that in the HTML of the page, you should be able to identify the element. In my experience, it's a dialog or maybe a banner at the bottom of the page, etc. Sometimes you will need to close it, other times you will need to scroll down/up a little to get the desired element from behind the blocking UI.
Continued from comments above ...
I encountered this problem as well, when I needed to click a button but it was not visible on the screen (however, it was detected by the code).
To resolve this, I used the WebDriver's executeScript() method to run some JavaScript on the page to scroll until my button was in view.
driver.executeScript(`
var target = document.getElementById('agree');
var tarTop = target.getBoundingClientRect().top;
document.body.scrollTop = tarTop;
`);
You can try driver.executeAsyncScript() if you want want to add a timeout to the scroll, to make sure the page has reached its destination first. At that point you'll be using async/await...
await driver.executeAsyncScript(`
var callback = arguments[arguments.length - 1];
var target = document.getElementById('agree');
var tarTop = target.getBoundingClientRect().top;
document.body.scrollTop = tarTop;
setTimeout(()=>{ callback( true ); }, 1500);
`).then((data)=>{
return data;
});
I'm writing a page scraper for a dynamic web page. The page has an initial load and then loads the remainder of the content after a short load time.
I've accounted for the load and have successfully scraped the HTML from the page, but the page doesn't load ALL the content at once. Instead it loads a specified amount of content via GET request URL and then has a "Get more" button on the page. My objective is to click this "Get More" button until all the content is loaded on the page. For those wondering, I don't wish to load all the content at once via GET URL because of impact to their server.
I'm stuck forming the loop or iteration that would allow me to repeatedly click on the page.
const NIGHTMARE = require("nightmare");
const BETHESDA = NIGHTMARE({ show: true });
BETHESDA
// Open the bethesda web page. Web page will contain 20 mods to start.
.goto("https://bethesda.net/en/mods/skyrim?number_results=40&order=desc&page=1&platform=XB1&product=skyrim&sort=published&text=")
// Bethesda website serves all requested mods at once. Each mod has the class "tile". Wait for any tile class to appear, then proceed.
.wait(".tile");
let additionalModsPresent = true;
while(additionalModsPresent) {
setTimeout(function() {
BETHESDA
.wait('div[data-is="main-mods-pager"] > button')
.click('div[data-is="main-mods-pager"] > button')
}, 10000)
additionalModsPresent = false;
}
// let moreModsBtn = document.querySelector('div[data-is="main-mods-pager"] > button');
// .end()
BETHESDA.catch(function (error) {
console.error('Search failed:', error);
});
My thinking thus far has been to use a while loop that attempts to click the button after some interval of time. If an error occurs, it's likely because the button doesn't exist. The issue I'm having is that I can't seem to get the click to work inside of a setTimeout or setInterval. I believe there is some sort of scoping issue but I don't know what exactly is going on.
If I can get the click method to work in setInterval or something similar, the issue would be solved.
Thoughts?
You can refer to the issue (Problem running nightmare in loops)[https://github.com/segmentio/nightmare/issues/522]
I modified your code with given guidelines. It seem to work fine
const NIGHTMARE = require("nightmare");
const BETHESDA = NIGHTMARE({
show: true
});
BETHESDA
// Open the bethesda web page. Web page will contain 20 mods to start.
.goto("https://bethesda.net/en/mods/skyrim?number_results=40&order=desc&page=1&platform=XB1&product=skyrim&sort=published&text=")
// Bethesda website serves all requested mods at once. Each mod has the class "tile". Wait for any tile class to appear, then proceed.
.wait(".tile");
next();
function next() {
BETHESDA.wait('div[data-is="main-mods-pager"] > button')
.click('div[data-is="main-mods-pager"] > button')
.then(function() {
console.log("click done");
next();
})
.catch(function(err) {
console.log(err);
console.log("All done.");
});
}
Ultimately, it should timeout on wait() for button and then you can handle the error in catch() block. Beware it goes on and on :) I did not wait till the end (you might run out of memory).
I'm using an <iframe> (I know, I know, ...) in my app (single-page application with ExtJS 4.2) to do file downloads because they contain lots of data and can take a while to generate the Excel file (we're talking anything from 20 seconds to 20 minutes depending on the parameters).
The current state of things is : when the user clicks the download button, he is "redirected" by Javascript (window.location.href = xxx) to the page doing the export, but since it's done in PHP, and no headers are sent, the browser continuously loads the page, until the file is downloaded. But it's not very user-friendly, because nothing shows him whether it's still loading, done (except the file download), or failed (which causes the page to actually redirect, potentially making him lose the work he was doing).
So I created a small non-modal window docked in the bottom right corner that contains the iframe as well as a small message to reassure the user. What I need is to be able to detect when it's loaded and be able to differenciate 2 cases :
No data : OK => Close window
Text data : Error message => Display message to user + Close window
But I tried all 4 events (W3Schools doc) and none is ever fired. I could at least understand that if it's not HTML data returned, it may not be able to fire the event, but even if I force an error to return text data, it's not fired.
If anyone know of a solution for this, or an alternative system that may fit here, I'm all ears ! Thanks !
EDIT : Added iframe code. The idea is to get a better way to close it than a setTimeout.
var url = 'http://mywebsite.com/my_export_route';
var ifr = $('<iframe class="dl-frame" src="'+url+'" width="0" height="0" frameborder="0"></iframe>');
ifr.appendTo($('body'));
setTimeout(function() {
$('.dl-frame').remove();
}, 3000);
I wonder if it would require some significant changes in both frontend and backend code, but have you considered using AJAX? The workflow would be something like this: user sends AJAX request to start file generating and frontend constantly polls it's status from the server, when it's done - show a download link to the user. I believe that workflow would be more straightforward.
Well, you could also try this trick. In parent window create a callback function for the iframe's complete loading myOnLoadCallback, then call it from the iframe with parent.myOnLoadCallback(). But you would still have to use setTimeout to handle server errors/connection timeouts.
And one last thing - how did you tried to catch iframe's events? Maybe it something browser-related. Have you tried setting event callbacks in HTML attributes directly? Like
<iframe onload="done()" onerror="fail()"></iframe>
That's a bad practice, I know, but sometimes job need to be done fast, eh?
UPDATE
Well, I'm afraid you have to spend a long and painful day with a JS debugger. load event should work. I still have some suggestions, though:
1) Try to set event listener before setting element's src. Maybe onload event fires so fast that it slips between creating element and setting event's callback
2) At the same time try to check if your server code plays nicely with iframes. I have made a simple test which attempts to download a PDF from Dropbox, try to replace my URL with your backed route's.
<script src="https://code.jquery.com/jquery-1.11.1.min.js"></script>
<iframe id="book"></iframe>
<button id="go">Request downloads!</button>
<script>
var bookUrl = 'https://www.dropbox.com/s/j4o7tw09lwncqa6/thinkpython.pdf';
$('#book').on('load', function(){
console.log('WOOT!', arguments);
});
$('#go').on('click', function(){
$('#book').attr('src', bookUrl);
});
</script>
UPDATE 2
3) Also, look at the Network tab of your browser's debugger, what happens when you set src to the iframe, it should show request and server's response with headers.
I've tried with jQuery and it worked just fine as you can see in this post.
I made a working example here.
It's basically this:
<iframe src="http://www.example.com" id="myFrame"></iframe>
And the code:
function test() {
alert('iframe loaded');
}
$('#myFrame').load(test);
Tested on IE11.
I guess I'll give a more hacky alternative to the more proper ways of doing it that the others have posted. If you have control over the PHP download script, perhaps you can just simply output javascript when the download is complete. Or perhaps redirect to a html page that runs javascript. The javascript run, can then try to call something in the parent frame. What will work depends if your app runs in the same domain or not
Same domain
Same domain frame can just use frame javascript objects to reference each other. so it could be something like, in your single page application you can have something like
window.downloadHasFinished=function(str){ //Global pollution. More unique name?
//code to be run when download has finished
}
And for your download php script, you can have it output this html+javascript when it's done
<script>
if(parent && parent.downloadHasFinished)
parent.downloadHasFinished("if you want to pass a data. maybe export url?")
</script>
Demo jsfiddle (Must run in fullscreen as the frames have different domain)
Parent jsfiddle
Child jsfiddle
Different Domains
For different domains, We can use postMessage. So in your single page application it will be something like
$(window).on("message",function(e){
var e=e.originalEvent
if(e.origin=="http://downloadphp.anotherdomain.com"){ //for security
var message=e.data //data passed if any
//code to be run when download has finished
}
});
and in your php download script you can have it output this html+javascript
<script>
parent.postMessage("if you want to pass data",
"http://downloadphp.anotherdomain.com");
</script>
Parent Demo
Child jsfiddle
Conclusion
Honestly, if the other answers work, you should probably use those. I just thought this was an interesting alternative so I posted it up.
You can use the following script. It comes from a project of mine.
$("#reportContent").html("<iframe id='reportFrame' sandbox='allow-same-origin allow-scripts' width='100%' height='300' scrolling='yes' onload='onReportFrameLoad();'\></iframe>");
Maybe you should use
$($('.dl-frame')[0].contentWindow.document).ready(function () {...})
Try this (pattern)
$(function () {
var session = function (url, filename) {
// `url` : URL of resource
// `filename` : `filename` for resource (optional)
var iframe = $("<iframe>", {
"class": "dl-frame",
"width": "150px",
"height": "150px",
"target": "_top"
})
// `iframe` `load` `event`
.one("load", function (e) {
$(e.target)
.contents()
.find("html")
.html("<html><body><div>"
+ $(e.target)[0].nodeName
+ " loaded" + "</div><br /></body></html>");
alert($(e.target)[0].nodeName
+ " loaded" + "\nClick link to download file");
return false
});
var _session = $.when($(iframe).appendTo("body"));
_session.then(function (data) {
var link = $("<a>", {
"id": "file",
"target": "_top",
"tabindex": "1",
"href": url,
"download": url,
"html": "Click to start {filename} download"
});
$(data)
.contents()
.find("body")
.append($(link))
.addBack()
.find("#file")
.attr("download", function (_, o) {
return (filename || o)
})
.html(function (_, o) {
return o.replace(/{filename}/,
(filename || $(this).attr("download")))
})
});
_session.always(function (data) {
$(data)
.contents()
.find("a#file")
.focus()
// start 6 second `download` `session`,
// on `link` `click`
.one("click", function (e) {
var timer = 6;
var t = setInterval(function () {
$(data)
.contents()
.find("div")
// `session` notifications
.html("Download session started at "
+ new Date() + "\n" + --timer);
}, 1000);
setTimeout(function () {
clearInterval(t);
$(data).replaceWith("<span class=session-notification>"
+ "Download session complete at\n"
+ new Date()
+ "</span><br class=session-notification />"
+ "<a class=session-restart href=#>"
+ "Restart download session</a>");
if ($("body *").is(".session-restart")) {
// start new `session`,
// on `.session-restart` `click`
$(".session-restart")
.on("click", function () {
$(".session-restart, .session-notification")
.remove()
// restart `session` (optional),
// or, other `session` `complete` `callback`
&& session(url, filename ? filename : null)
})
};
}, 6000);
});
});
};
// usage
session("http://www.ecma-international.org/publications/files/ECMA-ST/Ecma-262.pdf", "ECMA_JS.pdf")
});
jsfiddle http://jsfiddle.net/guest271314/frc82/
In regards to your comment about to get a better way to close it instead of setTimeout. You could use jQuery fadeOut option or any of the transitions and in the 'complete' callback remove the element. Below is an example you can dump right into a fiddle and only need to reference jQuery.
I also wrapped inside listener for 'load' event to not do the fade until the iFrame has been loaded as question originally was asking.
// plugin your URL here
var url = 'http://jquery.com';
// create the iFrame, set attrs, and append to body
var ifr = $("<iframe>")
.attr({
"src": url,
"width": 300,
"height": 100,
"frameborder": 0
})
.addClass("dl-frame")
.appendTo($('body'))
;
// log to show its part of DOM
console.log($(".dl-frame").length + " items found");
// create listener for load
ifr.one('load', function() {
console.log('iframe is loaded');
// call $ fadeOut to fade the iframe
ifr.fadeOut(3000, function() {
// remove iframe when fadeout is complete
ifr.remove();
// log after, should no longer exist in DOM
console.log($(".dl-frame").length + " items found");
});
});
If you are doing a file download from a iframe the load event wont fire :) I was doing this a week ago. The only solution to this problem is to call a download proxy script with a tag and then return that tag trough a cookie then the file is loaded. min while yo need to have a setInterval on the page witch will watch for that specific cookie.
// Jst to clearyfy
var token = new Date().getTime(); // ticks
$('<iframe>',{src:"yourproxy?file=somefile.file&token="+token}).appendTo('body');
var timers = [];
timers[timers.length+1] = setInterval(function(){
var _index = timers.length+1;
var cookie = $.cooke(token);
if(typeof cookie != "undefined"){
// File has been downloaded
$.removeCookie(token);
clearInterval(_index);
}
},400);
in your proxy script add the cookie with the name set to the string sent bay the token url parameter.
If you control the script in server that generates excel or whatever you are sending to iframe why don't you put a UID flag and store it in session with value 0, so... when iframe is created and server script is called just set UID flag to 1 and when script is finished (the iframe will be loaded) just put it to 2.
Then you only need a timer and a periodic AJAX call to the server to check the UID flag... if it's set to 0 the process doesn't started, if it's 1 the file is creating, and finally if it's 2 the process has been ended.
What do you think? If you need more information about this approach just ask.
What you are saying could be done for images and other media formats using $(iframe).load(function() {...});
For PDF files or other rich media, you can use the following Library:
http://johnculviner.com/jquery-file-download-plugin-for-ajax-like-feature-rich-file-downloads/
Note: You will need JQuery UI
You can use this library. The code snippet for you purpose would be something like:
window.onload = function () {
rajax_obj = new Rajax('',
{
action : 'http://mywebsite.com/my_export_route',
onComplete : function(response) {
//This will only called if you have returned any response
// instead of file from your export script
// In your case 2
// Text data : Error message => Display message to user
}
});
}
Then you can call rajax_obj.post() on your download link click.
Download
NB: You should add some header to your PHP script so it force file download
header('Content-Disposition: attachment; filename="'.$file.'"');
header('Content-Transfer-Encoding: binary');
There is two solutions that i can think of. Either you have PHP post it's progress to a MySQL table where from frontend will be pulling information from using AJAX calls to check up on the progress of the generation. Using somekind of unique key that is being generated when accessing the page would be ideal for multiple people generating excel files at the same time.
Another solution would be to use nodejs & then in PHP post the progress of the excel file using cURL or a socket to a nodejs service. Then when receiving updates from PHP in nodejs you simply write the progress of the excel file for the right socket. This will cut off some browser support though. Unless you go through with it using external libraries to bring websocket support for pretty much all browsers & versions.
Hope this answer helped. I was having the same issue previous year. Ended up doing AJAX polling having PHP post progress on the fly.
Try this:
Note: You should be on the same domain.
var url = 'http://mywebsite.com/my_export_route',
iFrameElem = $('body')
.append('<iframe class="dl-frame" src="' + url + '" width="0" height="0" frameborder="0"></iframe>')
.find('.dl-frame').get(0),
iDoc = iFrameElem.contentDocument || iFrameElem.contentWindow.document;
$(iDoc).ready(function (event) {
console.log('iframe ready!');
// do stuff here
});