Faking a web browser environment in QtScript

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:

  1. Create a QtScript environment (QScriptEngine).
  2. Add "fake" objects and properties to satisfy the JavaScript code's dependencies.
  3. Evaluate the JavaScript code.
  4. 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:

  1. Evaluate SimpleTestHarness/sth.js.
  2. Assign the value of activeSth to the global variable ES5Harness.
  3. Evaluate one or more .js files under TestCases/.
  4. 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