Faking a web browser environment in QtScript
March 04, 2011 by Kent Hansen | Comments
The following question was recently asked on qt-interest: How can I evaluate [some arbitrary] JavaScript code (that's included in a web page, say), using QtScript? It's an interesting topic, and I can't resist elaborating.
Short answer: You can't.
Long answer: It depends on what the script does -- more precisely, which JavaScript APIs the script uses. Try passing the script to QScriptEngine::evaluate() and see if you get a ReferenceError back; if not, you're done! But JavaScript code generally expects to be running in a full JavaScript environment. This environment is a super-set of the standardized ECMA-262 environment that QtScript provides out of the box. JavaScript scripts will expect the DOM APIs to be there. Scripts will also expect the window
object and document
object to be present. Scripts might check window.navigator.userAgent
to attempt to determine the presence of certain features (despite how useless that is). And so on. If either of these things are missing, evaluating JavaScript code on a vanilla QScriptEngine is likely to produce a reference error saying something like "Can't find variable: window"
. What to do?
Faking it
We can try to fake the environment. The idea is to just implement enough of the JavaScript APIs so that the scripts we want to run, run. This is precisely what the QtScript Context2D example does. In addition to most of the HTML5 Canvas API, it implements a subset of the DOM API (including basic event handling -- thanks to Zack for writing most of this cool example!), so that a number of scripts from the web can be run without modification.
The faked/partial environment can either be implemented in QtScript code, or as native objects (QObjects and/or function pointers); it depends on your use case. But in general, the process is as follows:
- Create a QtScript environment (QScriptEngine).
- Add "fake" objects and properties to satisfy the JavaScript code's dependencies.
- Evaluate the JavaScript code.
- If you get any reference errors, repeat.
An example
To demonstrate the above recipe, let's consider a real use case. es5conform is an ECMA-262 conformance suite; it checks how well an ECMA-262 implementation conforms to the standard. If you download the test suite, you'll notice that the provided test runner is a web page (runtests.html
). Let's make it run in QtScript instead. This should be perfectly possible, since the tests themselves only exercise ECMA-262 parts of JavaScript. And the test results could provide valuable information about QtScript's level of conformance.
Here are some essential parts of runtests.html
:
<script type="text/javascript" src="SimpleTestHarness/sth.js"></script>
<script>
var ES5Harness = activeSth;
</script>
...
<script type="text/javascript" src="TestCases/chapter11/11.4/11.4.1/11.4.1-3-3.js"></script>
<script type="text/javascript" src="TestCases/chapter11/11.4/11.4.1/11.4.1-4.a-1.js"></script>
<script type="text/javascript" src="TestCases/chapter11/11.4/11.4.1/11.4.1-4.a-10.js"></script>
...
<script>
ES5Harness.startTesting();
</script>
Opening SimpleTestHarness/sth.js
and studying it a bit, it becomes clear that it's a small library for registering and running tests. The tests themselves are stored in separate .js files (under TestCases/
). So the action plan for making es5conform QtScript-able is as follows:
- Evaluate
SimpleTestHarness/sth.js
. - Assign the value of
activeSth
to the global variableES5Harness
. - Evaluate one or more .js files under
TestCases/
. - Call the
ES5Harness.startTesting()
function.
Using the QtScript shell
The above steps can be implemented using the QtScript shell applicaton (examples/script/qscript
in Qt), which is essentially a stand-alone wrapper around QScriptEngine::evaluate()
that you can pass scripts to. Trying step 1:
qscript SimpleTestHarness/sth.js
ReferenceError: Can't find variable: window
<anonymous>()@SimpleTestHarness/sth.js:295
It turns out that the script uses the global window
variable to obtain a reference to the global object. We create a file pre.js
containing the following:
window = this;
When the above statement is evaluated as global code, this
will be a reference to the global object. We run qscript again, but pass pre.js
before sth.js
:
qscript pre.js SimpleTestHarness/sth.js
ReferenceError: Can't find variable: document
<anonymous>()@SimpleTestHarness/sth.js:259
OK, a new error. The script line in question is
this.resultsDiv = document.createElement("div");
Let's add a fake document
object to pre.js
:
document = {
createElement: function(tagName) {
return { nodeName: tagName };
}
};
After a couple more iterations, sth.js
will be successfully evaluated. Now create a post.js
file:
ES5Harness = activeSth;
And run.js
:
ES5Harness.startTesting();
Finally, we can run an actual test:
qscript pre.js SimpleTestHarness/sth.js post.js TestCases/chapter11/11.4/11.4.1/11.4.1-0-1.js run.js
No error, but no output either!
Output handling / post-processing
Looking at the test report code in sth.js
, the output is generated using a println()
function defined as follows:
sth.prototype.println = function (s) {
this.innerHTML += s;
this.innerHTML += "<BR/>";
}
Again, web page-oriented. We could either replace this function with the QtScript shell's built-in print()
function (e.g. in post.js
), or dump the innerHTML
property of the ES5Harness
object after testing is done. A third option would be to define innerHTML
to be a getter/setter property (such as a Qt/C++ property), so a setter function is called whenever the property is assigned.
A more flexible solution would be to ignore the HTML output completely and process the internal representation of the test results ourselves; this way we have complete control of the output format. Looking at the sth.prototype.report()
function, we see how to access the description and result properties of each test record. Putting all of this together, es5conform could easily be integrated into Qt's autotest system, for example (something we've already done in Qt with Mozilla's and V8's test suites, which work in a similar way).
At this point we could embed es5conform into a Qt application that uses QScriptEngine::evaluate()
and friends to do everything, thus removing the dependency on the QtScript shell application. This would also be necessary if you need/want to implement any API in native code.
In conclusion
The principle for evaluating arbitrary JavaScript code in QtScript is simple: Figure out how the script works, and implement ("fake") the JavaScript APIs it needs. The technique can be applied when importing JavaScript into QML as well; create elements with IDs and properties that match the properties accessed from JavaScript. For an example of the latter, have a look at the qmlfbench.tar.gz
attachment of QTBUG-8576 (nice bug report!).
Next, it would be cool to see a "fake" environment for something like prototype.js (making it possible to use Class.create()
and friends in QtScript/QML). It needs a bit more DOM API, but looks feasible.
Blog Topics:
Comments
Subscribe to our newsletter
Subscribe Newsletter
Try Qt 6.5 Now!
Download the latest release here: www.qt.io/download.
Qt 6.5 is the latest Long-Term-Support release with all you need for C++ cross-platform app development.
Qt World Summit 2023: Berlin awaits!
We're Hiring
Check out all our open positions here and follow us on Instagram to see what it's like to be #QtPeople.