Checking inside dynamically modified iframe with Cypress - javascript

I have a page that includes a third party script (Xsolla login). This script modifies elements on the page, one of the particular elements being iframe.
First a placeholder element is inserted
Then the iframe is deleted and new iframe is inserted with different dynamically loading content
Both iframes have the same id
How one can detect when the second, replaced, iframe is correctly loaded as Cypress cy.get() grabs the first iframe and then never detects newly changed content within the replaced iframe?

You can use cypress-wait-until plugin and then write a custom check function that inspects deep into the iframe.
/**
* Because Xsolla does dynamic replacement of iframe element, we need to use this hacky wait.
*/
function checkLoadedXsollaIframe(doc: Document): bool {
try {
const iframe = doc.getElementById('XsollaLoginWidgetIframe') as any;
if(!iframe) {
return false;
}
// This element becomes available only when Xsolla has done its magic JS loading
const usernameInput = iframe.contentDocument.querySelector('input[name="email"]');
return usernameInput !== null;
} catch(e) {
return false;
}
}
context('Login', () => {
beforeEach(() => {
cy.visit(frontendURL);
});
it('Should login with valid credentials', () => {
// This injects Xsolla <script> tag and then
// this third party script takes its own course of actions
cy.get('.login-btn').first().click();
cy.waitUntil(() => cy.document().then(doc => checkLoadedXsollaIframe(doc)), {timeout: 5000 });

Below is the snippet that waits for the content inside the iframe to be loaded and HTMLElements be available & no timeouts required.
const iframeElement = (selector) => {
const iframe = cy.get(selector);
return iframe
.should(($iframe) => // Make sure its not blank
expect($iframe.attr('src')).not.to.include('about:blank')
)
.should(($iframe) =>
expect($iframe.attr('src')).not.to.be.empty) // Make sure its not empty
.then(($inner) => {
const iWindow = $inner[0].contentWindow;
return new Promise((resolve) => {
resolve(iWindow);
});
})
.then((iWindow) => {
return new Promise((resolve) => {
iWindow.onload = () => { // Listen to onLoad event
resolve(iWindow.document);
};
});
})
.then((iDoc) => {
return cy.wrap(iDoc.body); // Wrap the element to access Cypress API
});
};
Now access the element inside the iframeDocument
iframeElement('#my-iframe') // Grab the iframe
.find('h2')
.should('have.text', 'My header text'); //Assert iframe header
Note: Don't attempt to access CORS websites. It might fail due to
security reasons

Related

How to prevent content script from having DOM injection race conditions?

I have a content script which modifies the DOM based on the current url.
My background script listens to chrome.webNavigation.onHistoryStateUpdated and sends a message to the content script with the current url:
chrome.webNavigation.onHistoryStateUpdated.addListener(onNavigation, { url: ... });
async function onNavigation(details) {
const { url, tabId } = details;
chrome.tabs.sendMessage(tabId, { id: 'my-msg-id', url });
}
Then, based on the url, the content script injects a certain div to the current page:
chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => {
if (message.id === 'my-msg-id' && message.url === '...') {
await injectDiv();
}
...
});
My problem is that sometimes, the same div gets injected to the DOM twice.
This looks like a race condition that happens:
When the page loads slowly.
When the user navigates quickly between urls or goes back and forth between pages.
What I've tried:
Wrapping the injection logic with a guard like this:
async function injectDiv() {
if (document.getElementById('my-div-id')) {
return;
}
const myDiv = document.createElement('div');
myDiv.id = 'my-div-id';
... // Some async code that waits until the container is loaded to the DOM using setInterval
const container = document.querySelector('div.container');
container.appendChild(myDiv);
}
Using throttle to limit the url triggers sent from the background script to the content script:
const throttledNav = throttle(onNavigation, 2000, { leading: true, trailing: true });
chrome.webNavigation.onHistoryStateUpdated.addListener(throttledNav, { url: ... });
It looks like it helped with most cases but I still get a double (or even a triple) duplicate DOM injection of my div from time to time, which makes my extension look very buggy.
What can I do to fix this?

Javascript - Fetch request happening after it supposed

I'm still new to javascript, I have this javascript problem from CS50 that is supposed to open a mailbox and clicking on an email is supposed to open the email. I think my on click part of the problem is right, but when I open my page and click on an email it doesnt call the open_mail() function.
I've solved that the problem is that the load_mailbox function for being asynchronous is beign called after the DOM finishes to load, so technically theres no div with the class email-box when the DOM finishes to load, but i don't know how to solve this problem, can someone help please.
document.addEventListener('DOMContentLoaded', function() {
// Use buttons to toggle between views
document.querySelector('#inbox').addEventListener('click', () => load_mailbox('inbox'));
document.querySelector('#sent').addEventListener('click', () => load_mailbox('sent'));
document.querySelector('#archived').addEventListener('click', () => load_mailbox('archive'));
document.querySelector('#compose').addEventListener('click', compose_email);
document.querySelector('#compose-form').addEventListener('submit', send_mail);
document.querySelectorAll('.email-box').forEach(function(box) {
box.addEventListener('click', function (){
open_mail();
})
});
// By default, load the inbox
load_mailbox('inbox');
});
function load_mailbox(mailbox) {
fetch(`/emails/${mailbox}`)
.then(response => response.json())
.then(emails => {
document.querySelector('#email-content').innerHTML = "";
emails.forEach(inbox_mail);
})
};
function inbox_mail(email) {
const element = document.createElement('div');
if (document.querySelector(`#email-${email.id}`) === null) {
element.id = (`email-${email.id}`);
element.className = ("email-box");
element.innerHTML = `<p>From ${email.sender}</p><p>${email.subject}</p><p>At ${email.timestamp}
</p>`;
document.querySelector('#email-content').append(element);
}
}
I´d say the easiest solution would be to put the addEventListener to a point after the elements with class .email-box are created, e.g in your .then function after inbox_mail ran for each email
.then(emails => {
document.querySelector('#email-content').innerHTML = "";
emails.forEach(inbox_mail);
document.querySelectorAll('.email-box').forEach(function(box) {
box.addEventListener('click', function (){
open_mail();
});
});
});
DOMContentLoaded will trigger when the DOM from the initial request/response was loaded. What you are doing in your fetch callback is called "DOM-Manipulation" as you create elements and append them to the DOM that has already been loaded.

How to have all external links on my website raise a confirm

I am trying to have all external links on my website raise a confirm saying "You are being redirected to an external site." I know how to write the JS to check for if a link is external and how to raise the confirm, but is there a way I can apply this to every link in my site without going through individually? The links will always have the format <a href=URL> Link </a>. An angular script would check if the URL subdomain is the same as my site, and if not it will add onclick=return confirm('You will now be redirected to an exteral site') and target="_blank" to the link HTML.
As you already said, this can be achieved by using confirm onclick, you can easily add an EventListener to all a Elements that are external (l.hostname !== location.hostname) in your page and only redirect after the user accepts the message, just like so:
Array.from(document.querySelectorAll("a")).filter(l => l.hostname !== location.hostname).forEach(el => el.addEventListener("click", evtL));
function evtL(e) {
if (confirm("Are you sure you want to leave the page?")) {
return; //redirect
} else {
e.preventDefault();
return false; //don't redirect
}
}
internal
external
also internal
It can be achieved by the regex matching.
Instead of adding multiple event listeners, single event listener can be added on the document object and detect the external links as follows. It will give more efficiency.
For reference, Handling events for multiple elements in a single listener
var regexp = /https?:\/\/(www.){0,1}((?:[\w\d-]+\.)+[\w\d]{2,})/i;
function isExternal(url)
{
return regexp.exec(location.href)[2] !== regexp.exec(url)[2];
}
document.addEventListener("click",function(){
if(event.target.tagName.toLowerCase()==="a")
{
if(isExternal(event.target.href))
{
if(confirm("Redirecting to an external site...."))
{
console.log("Going to external site...");
return;
}
else
{
event.preventDefault();
return;
}
}
else
{
console.log("Internal URL Clicked.")
}
}
});
External Site 1 - https://www.google.com<br>
External Site 2 - https://www.stackoverflow.com<br>
External Site 3 - http://www.facebook.com (Using HTTP)<br>
Internal Site (Using HTTP) - http://www.stacksnippets.net<br>
Internal Site (without www.) - http://stacksnippets.net<br>
Internal reference - /path/to/something (Internal reference)<br>
Thanks #pseudosavant for the regex.
Though #Luca's answer works, the hostname is not supported in IE, Safari and Opera. Browser Compatibility for reference.
Since you are using Angular.js, I would recommend taking a look at how they work with <a> since they already have a directive applied to all <a> to make ngHref work. The source would be here: <a> directive.
The basic idea is to put the logic that you use to change the href to the warning or display a modal or whatever in the element.on('click', function(event) {...}).
Because Angular.js already defined an <a> directive, you may need to fiddle with the priority of your directive so that you don't accidentally break the way Angular fiddles with <a>.
The directive would look something like this:
// Logic that dictactes whether the URL should show a warning
function isSuspectUrl(l) {
return false;
}
const app = angular
.module("aWarn", [])
.directive("a", function() {
return {
restrict: "E",
compile: function(el, attr) {
element.on("click", function(e) {
if (isSuspectUrl(attr.href)) {
// Logic that would display a warning
}
});
}
}
});
2021 version
es2015+
Works with SPA
Handles local links that look external
Subdomains support
const baseUrl = window.location.origin;
const absoluteUrlRegex = new RegExp('^(?:[a-z]+:)?//', 'i');
const isAbsoluteUrl = (url) => absoluteUrlRegex.test(url);
const isLocalUrl = (url) => url.startsWith(baseUrl) || !isAbsoluteUrl(url);
// https://gist.github.com/ -> github.com
// https://stackoverflow.com/ -> stackoverflow.com
const getDomain = (url) => {
const urlInstance = new URL(url);
const dividedUrl = urlInstance.hostname.split('.');
const urlDomainsCount = dividedUrl.length;
return urlDomainsCount === 2
? urlInstance.hostname
: `${dividedUrl[urlDomainsCount - 2]}.${dividedUrl[urlDomainsCount - 1]}`;
};
// example whitelist
const whitelist = [
'twitter.com',
'github.com',
];
// url has same domain or whitelisted
const isWhitelistedUrl = (url) => {
const domain = getDomain(url);
return domain === window.location.hostname || whitelist.includes(domain);
};
// bind listener
const confirmExternalLinks = (confirmationFn) => {
document.addEventListener('click', async (e) => {
const linkElement = e.target.closest('a');
if (!linkElement) return;
const link = linkElement.getAttribute('href');
if (isLocalUrl(link) || isWhitelistedUrl(link)) return;
e.preventDefault();
const confirmation = await confirmationFn(link);
if (confirmation) window.open(link);
});
};
// tip: replace confirmationFn with your custom handler which returns Promise
confirmExternalLinks((link) => confirm(`Proceed to ${link}?`));
<a target="_blank" href="https://stacksnippets.net/">https://stacksnippets.net/</a> is local for this snippet iframe
<br>
<a target="_blank" href="https://twitter.com/">https://twitter.com/</a> is whitelisted
<br>
<a target="_blank" href="https://developer.mozilla.org/">https://developer.mozilla.org/</a> is external

Word addin, Getting the whole document loaded again

I am developing an addin for Word 2016/365 to grab Word templates and to be able via contentcontrols alter information in header and footer. To begin with there will be only two templates to work with. I'm using ooxml and getting the xmldocument via XMLHttpRequest:
//Function to get ooxmldocument via XMLHttpRequest
function getTemplate(fileName) {
var myOOXMLRequest = new XMLHttpRequest();
var myXML;
myOOXMLRequest.open('GET', fileName, false);
myOOXMLRequest.send();
if (myOOXMLRequest.status === 200) {
myXML = myOOXMLRequest.responseText;
console.log('myXML VariabelData: ', myXML);
return myXML;
}
return "" ;
}
I insert the template via this:
// Insert a 'default' template with logo and contentControllers in header and footer of the xml
function insertDefaultTemplate() {
Word.run(function (context) {
var body = context.document.body;
// Synchronize the document state by executing the queued commands, and return a promise to indicate task completion.
return context.sync().then(function () {
var t = getTemplate('/Content/Templates/BrevmallMD.xml', 'Replace');
body.insertText("Formaterar dokument...", Word.InsertLocation.start);
return context.sync().then(function () {
body.insertOoxml(t, Word.InsertLocation.replace);
return context.sync().then(function () {
showNotification("Standardmallen är införd!", "Välj användaruppgifter.");
});
});
})
}).catch(function (error) {
errorHandler();
})
}
With some other functions I get to insert new info to contentcontrollers (that are in the document from start) via buttons and jQuery.
The problem is when you want to insert the document again. It doesnt load the original document as it is. it seem to get merged in the existing one precent in the addin.
I want to be able to load a new document as it is, not load a document and then it get merged with the existing one. I have seen a lot of example on office devcenter regardin putting in stuff in body and in sections but not to the whole document. How do I do that =) ?

'load' event not firing when iframe is loaded in Chrome

I am trying to display a 'mask' on my client while a file is dynamically generated server side. Seems like the recommend work around for this (since its not ajax) is to use an iframe and listen from the onload or done event to determine when the file has actually shipped to the client from the server.
here is my angular code:
var url = // url to my api
var e = angular.element("<iframe style='display:none' src=" + url + "></iframe>");
e.load(function() {
$scope.$apply(function() {
$scope.exporting = false; // this will remove the mask/spinner
});
});
angular.element('body').append(e);
This works great in Firefox but no luck in Chrome. I have also tried to use the onload function:
e.onload = function() { //unmask here }
But I did not have any luck there either.
Ideas?
Unfortunately it is not possible to use an iframe's onload event in Chrome if the content is an attachment. This answer may provide you with an idea of how you can work around it.
I hate this, but I couldn't find any other way than checking whether it is still loading or not except by checking at intervals.
var timer = setInterval(function () {
iframe = document.getElementById('iframedownload');
var iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
// Check if loading is complete
if (iframeDoc.readyState == 'complete' || iframeDoc.readyState == 'interactive') {
loadingOff();
clearInterval(timer);
return;
}
}, 4000);
You can do it in another way:
In the main document:
function iframeLoaded() {
$scope.$apply(function() {
$scope.exporting = false; // this will remove the mask/spinner
});
}
var url = // url to my api
var e = angular.element("<iframe style='display:none' src=" + url + "></iframe>");
angular.element('body').append(e);
In the iframe document (this is, inside the html of the page referenced by url)
window.onload = function() {
parent.iframeLoaded();
}
This will work if the main page, and the page inside the iframe are in the same domain.
Actually, you can access the parent through:
window.parent
parent
//and, if the parent is the top-level document, and not inside another frame
top
window.top
It's safer to use window.parent since the variables parent and top could be overwritten (usually not intended).
you have to consider 2 points:
1- first of all, if your url has different domain name, it is not possible to do this except when you have access to the other domain to add the Access-Control-Allow-Origin: * header, to fix this go to this link.
2- but if it has the same domain or you have added Access-Control-Allow-Origin: * to the headers of your domain, you can do what you want like this:
var url = // url to my api
var e = angular.element("<iframe style='display:none' src=" + url + "></iframe>");
angular.element(document.body).append(e);
e[0].contentWindow.onload = function() {
$scope.$apply(function() {
$scope.exporting = false; // this will remove the mask/spinner
});
};
I have done this in all kinds of browsers.
I had problems with the iframe taking too long to load. The iframe registered as loaded while the request wasn't handled. I came up with the following solution:
JS
Function:
function iframeReloaded(iframe, callback) {
let state = iframe.contentDocument.readyState;
let checkLoad = setInterval(() => {
if (state !== iframe.contentDocument.readyState) {
if (iframe.contentDocument.readyState === 'complete') {
clearInterval(checkLoad);
callback();
}
state = iframe.contentDocument.readyState;
}
}, 200)
}
Usage:
iframeReloaded(iframe[0], function () {
console.log('Reloaded');
})
JQuery
Function:
$.fn.iframeReloaded = function (callback) {
if (!this.is('iframe')) {
throw new Error('The element is not an iFrame, please provide the correct element');
}
let iframe = this[0];
let state = iframe.contentDocument.readyState;
let checkLoad = setInterval(() => {
if (state !== iframe.contentDocument.readyState) {
if (iframe.contentDocument.readyState === 'complete') {
clearInterval(checkLoad);
callback();
}
state = iframe.contentDocument.readyState;
}
}, 200)
}
Usage:
iframe.iframeReloaded(function () {
console.log('Reloaded');
})
I've just noticed that Chrome is not always firing the load event for the main page so this could have an effect on iframes too as they are basically treated the same way.
Use Dev Tools or the Performance api to check if the load event is being fired at all.
I just checked http://ee.co.uk/ and if you open the console and enter window.performance.timing you'll find the entries for domComplete, loadEventStart and loadEventEnd are 0 - at least at this current time:)
Looks like there is a problem with Chrome here - I've checked it on 2 PCs using the latest version 31.0.1650.63.
Update: checked ee again and load event fired but not on subsequent reloads so this is intermittent and may possibly be related to loading errors on their site. But the load event should fire whatever.
This problem has occurred on 5 or 6 sites for me now in the last day since I noticed my own site monitoring occasionally failed. Only just pinpointed the cause to this. I need some beauty sleep then I'll investigate further when I'm more awake.

Categories

Resources