So I encountered the maybe strangest thing in the last years. It took me hours to make it reproducible but still, I can not explain it at all.
I have an application that loads variable stuff in an iFrame. Then host and iFrame communicate via postMessage. The application is in use for years and there were never such problems in production or development as the one we encountered when we wanted to write e2e-tests in Testcafe. In the tests, the communication between host and client fails under certain circumstances: The iFrame sends a message with some data but the data attribute of the postMessage is undefined at the host's end. WTF.
Under what circumstances? It seems that, when certain language constructs are used in the JavaScript of the page inside the iFrame, the message gets distorted. Again: only when using Testcafe. Regardless of with Chrome, Chromium, or Firefox.
Ho to Reproduce:
Have a test.html:
<!DOCTYPE HTML>
<html>
<body>
<h1 id='output'>test</h1>
<iframe srcdoc='<!DOCTYPE html><html><body>inner<script>setTimeout(() => {window.parent.postMessage({type: `communication worked`}, `*`);}, 1500);const x = new class { y = new class {} } </script></body></html>' sandbox="allow-forms allow-scripts allow-same-origin"></iframe>
<script>
window.addEventListener('message', (event) => {
console.log({event});
document.getElementById('output').innerText = event.data.type;
});
</script>
</body>
</html>
Serve it with your favorite server. I use node with a server.js int his example, ng in the real-life application.
const connect = require('connect');
const serveStatic = require('serve-static');
var app = connect();
app.use(serveStatic(__dirname))
app.listen('8000', () => console.log('Server running on 8000...'));
And then, run the testcafe-test:
npx testcafe firefox e2e/src/test.ts
fixture`Foo`
.page`http://localhost:8080/test.html`;
test('Bar', async t => {
await t.wait(120000);
});
As you can find in the browser console the host is receiving the message from the client, but its data is empty.
Now, remove those characters y = new class {} from the srcdoc. Do the test again. Communication works, seriously WTF.
Maybe using new class inside of new class might be a strange style. If it would even be bogus somehow, how come there is no error at all? And how can it distort the postMessage without even being connected to it in any way? How does this problem only occur with Testcafe, not when you run the code by yourself or even with Protractor? I am maximum clueless.
Some Details:
In the real life the iFrame's content can contain also a more complex app, written in angular, with which the same thing happens. I am not able to say if it's the very same language construct in this case which "breaks the postMessage-system of the whole page" or a different one.
It doesn't change if the content of the src doc is not hardcoded (naturally it's not in the real-life application)
I use testcafe 1.18.2, Firefox 91.5.0, Chromium 90.0, Google Chrome, Debian 10
I am very thankful for the slightest hint!
Related
I wrote a simple test to visit the site https://auth0.github.io/device-flow-playground/
describe('My First Test Suite', () => {
it("should navigate to the application", () => {
cy.visit("https://auth0.github.io/device-flow-playground/");
});
})
But, I am getting this error :
The following error originated from your application code, not from Cypress.
> hljs is not defined
When Cypress detects uncaught errors originating from your application it will automatically fail the current test.
This behavior is configurable, and you can choose to turn this off by listening to the uncaught:exception event
I understand that Cypress cannot do much here, but still looking for some pointers if there's some way to fix this so the page loads correctly? I don't have much of JS experience here.
The line that is likely (not definitely) to be causing your problem is
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/highlight.min.js"></script>
This adds library hljs to the page.
You can see the above script tag if you open https://auth0.github.io/device-flow-playground/ in a browser (not Cypress) and look at the Elements tab in devtools.
It's difficult to know why this script is blocked, because it works ok on my machine.
Try putting this URL https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.15.6/highlight.min.js in the browser also, may give you some clues.
Is there a firewall preventing you access to the above?
I'm currently looping over a custom protocol to pass information into an Electron app from the browser. This works great on every browser except Chrome. The below code runs the protocol in an iframe that then loads the data into Electron.
Code
<iframe id="dataDiv" style="width:100%;align:center;overflow-y:hidden;" frameborder="0" scrolling="no" />
url = "custom-protocol://some-data?"
for (i = 0; i < 5; i++) {
$("#dataDiv").attr("src", url + i);
}
The above code is pseudo code of how I'm trying to achieve this.
Expectation
My expectation is that it will loop 5 times, and pass the data into the Electron app. Like mentioned, this seems to only work with IE and Firefox.
Actual Results
While debugging Chrome, I find that it is only executing the first page. From the looks of it, for whatever reason, Chrome is only executing the first protocol load in the loop.
Other Attempts
I've also tried to do a timeout in the loop. Thinking that maybe it's
just triggering the protocol too fast, and Chrome doesn't like that.
That failed as well.
I've even tried just looping over javascript that creates a new tab,
instead of an iframe.. and it gives me the same result.
Does anyone know of a security setting in Chrome that would prevent custom protocols to be loaded into Chrome multiple times simultaneously?
The answer was quite simple, don't use the protocol to POST data. Instead I ended up creating a web server within electron, and passed the data off via the URL.
var http=require('http');
var url=require('url');
var server=http.createServer(function(req,res){
var pathname=url.parse(req.url).pathname;
switch(pathname){
case '/data':
var query=url.parse(req.url).query;
someFunction(query);
res.end('200 ok');
break;
}
}).listen(8080);
This way I can use AJAX to launch and hand off data to Electron, instead of iframes.
Then you just pass the data in like:
http://localhost:8080/data?{insertyourdatahere}
I recently began working with Selenium and to make life easier to start I was using node to run my scripts so that I could visually monitor the tests. My challenge now is to convert it so that it can be run as a headless test. Unfortunately, most of the resources that I have come across only address using phantomjs and ghostdriver with Java or Python. My boss wants me to run the test through phantomjs without Java or Python. Eventually these tests will be run remotely through a Linux VM on a server without a GUI. Currently I am testing using Mac OS X 10.8 and still have many bridges to cross in order to get to my goal.
My most important question firstly, is it possible to run a script from phantomjs through a port without the use of Java or Python? I have spent hours poring through as many resources as I could come across and I've come up with no solution.
If so, how can I properly initialize the test to run headless? Here is how I scripted the start of my functioning test. I want to properly switch the capabilities from firefox to phantomjs and be able to run it headless using the appropriate port. The rest of the test navigates to a specific site, logs in through a widget, then does further navigation to the area which I will build further tests on which to manipulate after I get this working.
var webdriver = require('selenium-webdriver'),
SeleniumServer = require('selenium-webdriver/remote').SeleniumServer;
var server = new SeleniumServer("Path/selenium-server-standalone-2.39.0.jar", {
port: 8910
});
server.start();
var driver = new webdriver.Builder().
usingServer(server.address()).
withCapabilities(webdriver.Capabilities.firefox()).
build();
The test works perfectly, but I am new to this so there might be something foolish that I am overlooking. Please let me know what adjustments to make so that it will run headless through phantom. When I attempt to use node to run the script after switching capabilities to phantomjs it produces
"/Selenium/node_modules/selenium-webdriver/phantomjs.js:22
LogLevel = webdriver.logging.LevelName,
^
TypeError: Cannot read property 'LevelName' of undefined
at Object.<anonymous> (/Selenium/node_modules/selenium-webdriver/phantomjs.js:22:33)
That's a read only file that I can't adjust, any attempts that I made to define "LogLevel" or "LevelName" to the appropriate corresponding value (DEBUG, etc.) were fruitless.
And if I run it through phantomjs itself I get -
"Error: Cannot find module 'path'
phantomjs://bootstrap.js:289
phantomjs://bootstrap.js:254 in require"
(It also lists module 'http') -- (and various undefined function errors)
I feel that with that instance I didn't properly organize where the files for Selenium, phantomjs, and ghostdriver should go in order to play nice. I also removed the server setup portion and instead ran this first, then the script separately.
phantomjs --webdriver=8910
But it yielded the same result. All of my research to fix these issues turned up instructions for Java and Python but not Javascript by itself. Rather than chase through many rabbit holes I figured it wise to consult better minds.
If you know better than I do and that it is fruitless to attempt this without Java or Python, please let me know. If you know where the issue lies within my script and could propose a fix please let me know. I hope that I have properly described the nature of my issue and if you need more information I will do my best to provide it to you.
This is my second week working with Javascript so if you believe I am making a noob error you very well may be correct. Please, keep in mind that the script works through node with selenium webdriver.
Many thanks for your time!!!
~Isaac
This was a bit tricky but here is the solution I've pieced together:
var webdriver = require('selenium-webdriver'),
SeleniumServer = require('selenium-webdriver/remote').SeleniumServer,
server = new SeleniumServer('/path/to/selenium/selenium-server-standalone-2.41.0.jar', {
port: 4444
}),
capabilities = webdriver.Capabilities.phantomjs();
capabilities.set('phantomjs.binary.path', 'path/to/phantom/bin/phantomjs');
var promise = server.start().then(function() {
var client = new webdriver.Builder().
usingServer(server.address()).withCapabilities(
capabilities
).build();
return {
'client': client,
'server': server
};
}, function(err) {
console.log('error starting server', err);
});
You can then use the promise with selenium's mocha-compatible test framework to hold the test till the server has started.
I found the documentation really helpful once i figured out the navigation is on the far right of the page. Here's the URL: http://selenium.googlecode.com/git/docs/api/javascript/module_selenium-webdriver.html
Then you'll be stuck where I am. Getting selenium-webdriver to quiet down.
Do web browsers use separate executional threads for JavaScript in iframes?
I believe Chrome uses separate threads for each tab, so I am guessing that JavaScript in an iframe would share the same thread as its parent window, however, that seems like a security risk too.
Recently tested if JavaScript running in a iFrame would block JavaScript from running in the parent window.
iFrame on same domain as parent:
Chrome 68.0.3440.84: Blocks
Safari 11.0.2 (13604.4.7.1.3): Blocks
Safari 15.1 on iOS: Blocks
Firefox 96: Blocks
iFrame on different domain as parent
Chrome 68.0.3440.84: Doesn't block
Safari 11.0.2 (13604.4.7.1.3): Blocks (outdated, but I don't have a macbook)
Safari 15.1 on iOS: Doesn't block
Firefox 96: Doesn't block
Chrome for Android 96: sometimes Blocks and sometimes Doesn't block (There are some complex rules in Chrome for Android that determine when Chrome for Android does and doesn't isolate a process, see chrome://process-internals and chrome://flags)
parent.html:
<body>
<div id="count"></div>
<iframe src="./spin.html"></iframe>
<script>
let i = 0;
let div = document.getElementById("count");
setInterval(() => {
div.innerText = i++;
}, 100);
</script>
</body>
spin.html:
<body>
<button id="spin">spin</button>
<script>
const spin = document.getElementById("spin");
spin.addEventListener('click', () => {
const start = Date.now();
while (Date.now() - start < 1000) { }
})
</script>
</body>
Before chrome came along, all tabs of any browser shared the same single thread of JavaScript. Chrome upped the game here, and some others have since followed suit.
This is a browser implementation detail, so there is no solid answer. Older browsers definitely don't. I don't know of any browser that definitely uses another thread for iframes, but to be honest I've never really looked into it.
It isn't a security risk, as no objects are brought along with the thread execution.
To sum up the other answers: No, iFrames usually run in the same thread/process as the main page.
However, it appears the Chromium team are working on further isolation in this area:
Chromium Issue 99379: Out of process iframes [sorry, link not working - if you can find a link to the issue that works, please let me know]
Design Plans for Out-of-Process iframes
I've had the same question myself this night, before checking for any existing answers. In the project I'm currently working we have to load an iFrame that uses a different framework and I was curios if that iFrame could somehow block the thread and affect my app. The answer is yes, it can.
My test was done in Chrome. In the parent I've loaded a child iFrame. In the parent I've set an interval to console.log a text every amount time. Then in the iFrame I've used a timeout to launch a 'while' that blocks the thread. The answer: the iFrame uses the same thread.
Example:
In the parent:
setInterval(() => {
console.log('iFrame still using the thread');
}, 3000)
In the iFrame:
setTimeout(() => {
console.log('now the thread is not working in the iFrame anymore');
while (true) {
}
}, 10000)
2021 Update:
There is now the Origin-Agent-Cluster header which allows you to request dedicated resources for an iframe. It is currently supported on Chrome (88+) with positive reception from Mozilla and Safari.
Origin-Agent-Cluster is a new HTTP response header that instructs the browser to prevent synchronous scripting access between same-site cross-origin pages. Browsers may also use Origin-Agent-Cluster as a hint that your origin should get its own, separate resources, such as a dedicated process.
[...] For example, if https://customerservicewidget.example.com expects to use lots of resources for video chat, and will be embedded on various origins throughout https://*.example.com, the team maintaining that widget could use the Origin-Agent-Cluster header to try to decrease their performance impact on embedders.
To use the Origin-Agent-Cluster header, configure your web server to send the following HTTP response header: Origin-Agent-Cluster: ?1 The value of ?1 is the structured header syntax for a boolean true value.
More details here: https://web.dev/origin-agent-cluster/
Only chrome & firefox on desktop (no, not mobile) is separating threads.
I've created a small page that run long loop in interval in the main page, and shows an animation both in the main page and in the iframe.
You can go to the site from the browser you wish to check.
If the lower animation (under 'crossorigin') runs without stopping, it's have a separate thread.
https://eylonsu.github.io/browser_thread/
Late on this but... good point, cause iframe js seems to be concurrent in Firefox 16.
Try with alert function (blocking), you'll see dialogs opening together.
You won't see that in Chrome or IE.
iframe js may access the parent window in Firefox 16 as usual, so I can think of possible race conditions arising.
Did some experimenting with this today in Chrome 28 in Ubuntu. Used this command to see Chrome's threads and processes
ps axo pid,nlwp,cmd | grep "chrome"
It looks like Chrome does not spawn new threads or processes for iframes. An interesting note is that it does spawn a new process for the dev tools pane.
2022 Update (Experimental)
Iframes can now be run in parallel in at least Chrome Canary on desktop computers, but this is still experimental.
Download Chrome Canary (https://www.google.com/chrome/canary/).
Navigate to "chrome://flags/".
Enable "Isolated sandboxed iframes".
Create "index.html" with the following content:
<h1>index.html</h1>
<iframe src="index-child.html" sandbox="allow-scripts"></iframe>
<script>
setInterval(() => {
console.log("index.html executed one iteration");
}, 1000)
</script>
Create "index-child.html" with the following content:
<h1>index-child.html</h1>
<script>
setTimeout(() => {
console.log("index-child.html started continuous execution");
while (true) {
}
}, 3000)
</script>
Open "index.html" in the browser.
Verify that the console is consistently logging "index.html executed one iteration". Thus, the iframe is executed in parallel.
Disable "Isolated sandboxed iframes" (or just use another browser) and open "index.html" again. The console is no longer consistently logging "index.html executed one iteration". Thus, the iframe is no longer executed in parallel.
Note: The sandbox attribute on the iframe tag must be correctly set for this to work. Additionally, only one extra process per site is currently supported, which means that multiple iframes will not all run in parallel.
The specific instructions from "chrome://flags/":
Isolated sandboxed iframes
When enabled, applies process isolation to iframes with the 'sandbox' attribute and without the 'allow-same-origin' permission set on that attribute. The current isolation model is that all sandboxed iframes from a given site will be placed into the same process, but alternative models may be introduced in future experiments. – Mac, Windows, Linux, Chrome OS, Fuchsia
For iFrames, no. However if you want to use threads in JavaScript you can use Web Workers, a working html5 draft supported by the new browsers. http://www.w3.org/TR/2009/WD-workers-20091029/
I'm writing a Chrome userscript to locally auto-save content in a CKEditor. I'm using this CKEditor auto-save plugin as inspiration.
I have written a function that fires every half second (via an interval) to register the CKEditor event handler:
var intervalId = window.setInterval(function() {
if (CKEDITOR) {
window.clearInterval(intervalId);
CKEDITOR.plugins.add("user-script-auto-save", {
init : function(editor) {
editor.on('key', startTimer);
}
});
}
}, 500);
However, it never properly completes, and complains that "CKEDITOR is undefined" on the if (CKEDITOR) statement.
Meanwhile, if I drop into Chrome's console and type CKEDITOR, the console prints out the expected object.
What am I missing? The editor is embedded within an iframe; might that have an impact on scoping? Or am I fighting against Chrome's sandboxing here? And if so, is there some other way I can dig into CKEditor to pull out the content every second or something to do the auto-saves?
I have not yet tried the script in Firefox; that's next on my list.
Worth noting: I'm a long-time JavaScript novice. So I could easily be doing something dumb with scoping or something like that.
According to this little tutorial video on YouTube, all the 3 "devices" are separated from each other in order to prevent XSS attacks from the user script to the browser / website and vice versa. Although the user scripts / content scripts are running in the website's context, they are still kept separated from the actual website script context. You can easily acknowledge this by simply trying to access for example jQuery from a content script. Just as the CKEditor, it will not be available.
So what I've come up with in order to deal with this is using the content script to include external JavaScripts in the head tag. AFAIK, this is not possible for files directly in the extension's root directory, so I've taken a remote server to host my files.
I'm not sure if this is the best approach and I think it is an ugly bypass, possibly way to powerfull and disabled by the Chromium Project some time.
(Edited by OP, so I can select this answer and route karma appropriately)
This answer, combined with some of the suggestions and links in the comments, ended up getting me to where I needed to be.
I ended up with the following function:
var insertScriptIntoDocument = function(scriptUrl, doc) {
// inspired by http://blog.afterthedeadline.com/2010/05/14/how-to-jump-through-hoops-and-make-a-chrome-extension/
var scriptText = doc.createTextNode(
'(function(loc) { \
var embeddedScript = document.createElement("script"); \
embeddedScript.type = "text/javascript"; \
embeddedScript.src = loc; \
document.getElementsByTagName("head")[0].appendChild(embeddedScript); \
})("' + scriptUrl + '");');
var injectorElement = doc.createElement('script');
injectorElement.appendChild(scriptText);
doc.body.appendChild(injectorElement);
};
Usage looks like so:
var embeddedScriptUrl = chrome.extension.getURL("embedded-script.js");
insertScriptIntoDocument(embeddedScriptUrl, document);
For now, I'm executing this from within a Chrome extension, but I suspect that the pattern might work in a GreaseMonkey script deployed via the Chrome TamperMonkey extension provided that the URL of the script to be embedded was hosted somewhere reachable.
FTR, as it turns out, I did not actually need to get to the iframe -- the CKEDITOR variable was defined in the top-level document, but was simply not visible because of the rules of the Chrome sandbox