Function not available after document.body.appendChild(scriptElement) - javascript

Here is the code I am using for dynamically including script tag with src in my HTML page. There is a function in that newly imported javascript file that I want to use:
var scriptFile= document.createElement("script");
scriptFile.src = "something.js";
var something=document.body.appendChild(scriptFile);
something.js contains a function called doSomething(). Now when I call this function immediately after the appendChild above, it say doSomething is not defined. However, when I fire it from the Chrome console, it executes successfully. I am not sure why is this happening.

When you append the script tag with the source, first the source will be parsed and compiled which is an asyn process ( non blocking ).
If you try to invoke the method immediately this would throw an error as the source has not been compiled yet.
Bind a load event which gets triggered when the script is available. This will make sure you are running the contents of the script tag only after it has completely loaded.
var scriptFile= document.createElement("script");
scriptFile.addEventListener('load', function() {
console.log('Script is ready to execute');
// invoke your function here
});
scriptFile.src = "something.js";
var something=document.body.appendChild(scriptFile);

Listen for onload
var scriptFile= document.createElement("script")
scriptFile.src = "something.js"
document.body.appendChild(scriptFile)
scriptFile.onload = () => {
// call something.js functions here
}

Related

Call a function from js file in Robot Framework

There is a possibility to execute javascript from file.
Execute JavaScript ${CURDIR}/js_to_execute.js
But how can I call a function by name from this file?
There is no way to call a function using Execute Javascript out of the box. But it's possible to pass a function name as an argument.
Execute Javascript ${CURDIR}/js/utils.js ARGUMENTS clickElement ${locator}
The content of utils.js file:
var utils = utils || {};
(function (arguments) {
utils.clickElement = function (locator) {
// implementation of the function
}
// this piece of code does the trick
// get function name from arguments and remove it from the list of arguments
var functionName = [].shift.call(arguments);
// call the function and pass the arguments
utils[functionName].apply(this, arguments);
})(arguments);
This will be little ugly but it works. You need to inject your whole Javascript code using Execute Javascript first and than you will be able to call the function you injected.
You can inject Javascript using below javascript code on google home page,
var s = document.createElement('script');
s.text = "document.getElementById('hplogo').addEventListener('click',function(){alert('Google');});";
document.getElementsByTagName('head')[0].appendChild(s);
In robotframework you can do the same using Execute Javascript
Open Browser https://www.google.com
Wait Until Page Contains Element id=hplogo
Execute Javascript var s = document.createElement('script');s.text = "document.body.addEventListener('click',function(){alert('Google')})";document.getElementsByTagName('head')[0].appendChild(s);
Sleep 2s #wait so that injected javascript gets evaluated
Execute Javascript document.getElementById('hplogo').click();
Sleep 10s #wait and click anywhere on body of the page to see the alert
Your s.text variable will have whole javascript code from js file in one line. Of course if your javascript is long than it will be dirty work but in one of the site I had to do this for pdf export and it is working fine.

Javascript onload and script callback functions, which takes the precedence?

I'm loading an external script that uses callback function, which returns some specific data. If this data is not received error should be displayed.
Here is the code I've made:
<script>
//setting initial state so that function will only work once
var visitors_loaded=false;
var my_callback = function( data ) {
if (visitors_loaded) return 0;
if (data) {
//success: callback function is called and it has a proper data
visitors_loaded=true;
alert(JSON.stringify(data));
}
else alert ('error'); //something went wrong
};
</script>
<script onload="my_callback(null)" onerror="my_callback(null)"
src="https://api.clicky.com/api/stats/4?site_id=32020&sitekey=9a19b1a4d1171193&type=visitors&date=this-month&output=json&json_callback=my_callback"></script>
As you can see... many things that can go wrong with the script, so I naturally added an onerror event. This on error event actually fires if you change host name or domain of the script to something non-existent.
However, if you only make changes to the url of the script, it can still connects to the server and fires an onload event instead. My callback function will not be called for those invalid requests, so I added an onload handler as well.
Now the problem is, if all loaded normally and data was returned, it will fire both, callback function and onload. I have noticed that callback function is triggered before the onload and set the visitors_loaded variable so that the handler function is only called once.
So far it works perfectly in JS fiddle and my offline site but I wonder if this is an expected behavior? Will that json_callback function always have precedence before the onload handler?
https://jsfiddle.net/5sfk9ht5/4/
Will that json_callback function always have precedence before the onload handler?
If the external script calls my_callback synchronously then yes.
The scripting section in the official html5 specification describes how these things are supposed to work. The specification is quite general and has to deal with a lot of details conserning encoding, CORS, ignore-destructive-writes counter and so on. But for this question we don't care about these specifics.
In step 4 there is a note:
Note: This is where the script is compiled and actually executed.
And in step 7 the load event is fired:
fire a simple event named load at the script element.
So the specification defines that the load event is always fired after the script has been executed.
As you see the specification also tells us why the onerror event is not fired if you change the URL. The error event is only created if loading the script fails. But all requests to https://api.clicky.com/api/stats/ return a HTTP 200 status. The invalid URLs return XML and thus a SyntaxError is thrown. But this does not cause the onerror handler to be triggered.
As others have mentioned if the callback is called asynchronously they can call your callback after the onload event. But I don't see a reason why they would do this async in your external script.
onload in older IE might not work for you 'onload' handler for 'script' tag in internet explorer, if you want to run all browsers you might need something like this https://jsfiddle.net/e287523t/2/ and should work for all
function addScript(fileSrc, callback) {
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.onreadystatechange = function() {
if (this.readyState == 'complete') {
callback();
}
}
script.onload = callback;
script.src = fileSrc;
head.appendChild(script);
}
Then the rest is defining my_callback and calling addScript
my_callback = function(someData) {
alert(JSON.stringify(someData));
};
Now the problem is, if all loaded normally and data was returned, it will fire both, callback function and onload. I have noticed that callback function is triggered before the onload and set the visitors_loaded variable so that the handler function is only called once.
This is because the callback is launched from within the called script from api.clicky.com
So far it works perfectly in JS fiddle and my offline site but I wonder if this is an expected behavior?
I see what you are getting at, a related question about what happens when a script fails is here, but I did some tests for you and here are the results.
tester.html:
<script>
var onLoadTest = function() {
console.log("Onload Called!");
};
var callbacktest = function() {
console.log("Called from other script!");
};
var errortest = function() {
console.log("Callback OnError!");
};
</script>
<script onload="onLoadTest()" onerror="errortest()"
src="script.js"></script>
script.js:
function otherScriptFunc()
{
//call the function in the original script
callbacktest()
}
otherScriptFunc(); // first call to other script
setTimeout(otherScriptFunc, 0); // final call to other script
Results from the console log
Called from other script!
Onload Called!
Called from other script!
Your OnLoad will be called when the JS in the other place has finished being parsed (async functions will do their own thing). For example, otherScriptFunc(); will call before onload but setTimeout(otherScriptFunc, 0); will be called after onload
Your OnError will only be called if there is a GET request error. IE, the file cannot be found, or URL cannot be resolved - nothing about what is in the file. (I tested it separately, just mess with the file name)
Your callback passed to the other script could be called whenever the other script feels like it. It has a reference to it and could decide to hold onto it for a little while and call it later after it has played around. Which means it could be in an async call waiting for data elsewhere. Which means, theoretically, your onload could in fact be called before the callback, but it depends on the other script and there is not a lot you can do about it.
Will that json_callback function always have precedence before the onload handler?
It's not about precedence, it is just dependent upon when the other script decides to call it.

How to call a function from injected script?

This is code from my contentScript.js:
function loadScript(script_url)
{
var head= document.getElementsByTagName('head')[0];
var script= document.createElement('script');
script.type= 'text/javascript';
script.src= chrome.extension.getURL('mySuperScript.js');
head.appendChild(script);
someFunctionFromMySuperScript(request.widgetFrame);// ReferenceError: someFunctionFromMySuperScript is not defined
}
but i got an error when calling a function from injected script:
ReferenceError: someFunctionFromMySuperScript is not defined
Is there is a way to call this function without modifying mySuperScript.js?
Your code suffers from multiple problems:
As you've noticed, the functions and variables from the injected script (mySuperScript.js) are not directly visible to the content script (contentScript.js). That is because the two scripts run in different execution environments.
Inserting a <script> element with a script referenced through a src attribute does not immediately cause the script to execute. Therefore, even if the scripts were to run in the same environment, then you can still not access it.
To solve the issue, first consider whether it is really necessary to run mySuperScript.js in the page. If you don't to access any JavaScript objects from the page itself, then you don't need to inject a script. You should try to minimize the amount of code that runs in the page itself to avoid conflicts.
If you don't have to run the code in the page, then run mySuperScript.js before contentScript.js, and then any functions and variables are immediately available (as usual, via the manifest or by programmatic injection).
If for some reason the script really needs to be loaded dynamically, then you could declare it in web_accessible_resources and use fetch or XMLHttpRequest to load the script, and then eval to run it in your content script's context.
For example:
function loadScript(scriptUrl, callback) {
var scriptUrl = chrome.runtime.getURL(scriptUrl);
fetch(scriptUrl).then(function(response) {
return response.text();
}).then(function(responseText) {
// Optional: Set sourceURL so that the debugger can correctly
// map the source code back to the original script URL.
responseText += '\n//# sourceURL=' + scriptUrl;
// eval is normally frowned upon, but we are executing static
// extension scripts, so that is safe.
window.eval(responseText);
callback();
});
}
// Usage:
loadScript('mySuperScript.js', function() {
someFunctionFromMySuperScript();
});
If you really have to call a function in the page from the script (i.e. mySuperScript.js must absolutely run in the context of the page), then you could inject another script (via any of the techniques from Building a Chrome Extension - Inject code in a page using a Content script) and then pass the message back to the content script (e.g. using custom events).
For example:
var script = document.createElement('script');
script.src = chrome.runtime.getURL('mySuperScript.js');
// We have to use .onload to wait until the script has loaded and executed.
script.onload = function() {
this.remove(); // Clean-up previous script tag
var s = document.createElement('script');
s.addEventListener('my-event-todo-rename', function(e) {
// TODO: Do something with e.detail
// (= result of someFunctionFromMySuperScript() in page)
console.log('Potentially untrusted result: ', e.detail);
// ^ Untrusted because anything in the page can spoof the event.
});
s.textContent = `(function() {
var currentScript = document.currentScript;
var result = someFunctionFromMySuperScript();
currentScript.dispatchEvent(new CustomEvent('my-event-todo-rename', {
detail: result,
}));
})()`;
// Inject to run above script in the page.
(document.head || document.documentElement).appendChild(s);
// Because we use .textContent, the script is synchronously executed.
// So now we can safely remove the script (to clean up).
s.remove();
};
(document.head || document.documentElement).appendChild(script);
(in the above example I'm using template literals, which are supported in Chrome 41+)
As long as the someFunctionFromMySuperScript function is global you can call it, however you need to wait for the code actually be loaded.
function loadScript(script_url)
{
var head= document.getElementsByTagName('head')[0];
var script= document.createElement('script');
script.type= 'text/javascript';
script.src= chrome.extension.getURL('mySuperScript.js');
script.onload = function () {
someFunctionFromMySuperScript(request.widgetFrame);
}
head.appendChild(script);
}
You can also use jQuery's getScript method.
This doesn't work, because your content script and the injected script live in different contexts: what you inject into the page is in the page context instead.
If you just want to load code dynamically into the content script context, you can't do it from the content script - you need to ask a background page to do executeScript on your behalf.
// Content script
chrome.runtime.sendMessage({injectScript: "mySuperScript.js"}, function(response) {
// You can use someFunctionFromMySuperScript here
});
// Background script
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
if (message.injectScript) {
chrome.tabs.executeScript(
sender.tab.id,
{
frameId: sender.frameId},
file: message.injectScript
},
function() { sendResponse(true); }
);
return true; // Since sendResponse is called asynchronously
}
});
If you need to inject code in the page context, then your method is correct but you can't call it directly. Use other methods to communicate with it, such as custom DOM events.

Is there a callback for element.innerHTML assignments?

When using Head.js, and setting the .src attribute of a script element, there is callback method that is called when the script is ready.
However, I wanted to load a script by assigning text to .innerHTML. When doing this the same callback did not fire when I updated/edited the code to use this property instead.
/*addScriptText1
** modified from head.js
**
**
*/
function addScriptText1(file_name, callback, key) {
var element = document.createElement('script');
element.async = true;
element.innerHTML = localStorage[file_name];
element.onreadystatechange = element.onload = function () {
$A.log('callback for element called');
if ((!element.readyState || /loaded|complete/.test(element.readyState))) {
localStorage[key + '_loaded'] = true;
callback();
}
};
document.head.appendChild(element);
}
Scripts are executed immediately when inline scripts are inserted into the DOM. This all happens synchronously, so you don't need a callback.
async has no effect here because you are not doing a network request. Similarly, the readyState is not useful because you are creating it programatically, so the script is going to be immediately loaded.
Using innerHTML on a script element is not supported cross-browser (including Firefox iirc). I would not recommend this approach. I would suggest eval or new Function instead.
Can scripts be inserted with innerHTML?

Run Two Functions (One In External File, One In Inline Javascript) On window.onload

I'm developing a website and I have an external JavaScript file that is linked to every page of the site which executes when the window.onload event is fired. The JavaScript executes fine on all pages which do not contain any inline JavaScript.
Any page that contains inline JavaScript also contains a JavaScript function which executes when the window.onload event is fired.
The problem I'm having is that the external JavaScript does not execute when window.onload is fired, only the internal JavaScript does. It appears as if the inline JavaScript function overwrites the function from the external JavaScript file.
I know that my external JavaScript file is first executed and then the inline JavaScript is executed. Is there anyway that I can execute both functions on window.onload?
How about changing the script that executes second to something like this:
var onload = function () {
// Do something
alert('Hello');
};
if(window.onload) {
// If a function is already bound to the onload event, execute that too.
var fn = window.onload;
window.onload = function () {
fn();
onload();
};
} else {
window.onload = onload;
}
Or use a library like jQuery which lets you do:
$(document).ready(function() {
alert('Hello');
}
This might be more of a work around than a proper fix but could you put your inline JS at the bottom of the html page so it is automatically called after the page is loaded. That will mean you don't have to actually use window.onload()
Two ways :
1 - Use addEventListener.
window.addEventListener("load", function(){ /*code*/ }, false);
2 - Use a hack like this:
var func = window.onload;
window.onload = function() {
if ( func ) func();
/* code */
};

Categories