wiki:Writing Layout Tests to test iOS UI features

Version 9 (modified by Simon Fraser, 6 years ago) (diff)

--

Summary

It's now possible to write layout tests that exercise code in the UI process in iOS WebKit2. This allows testing of things like tapping, zooming, keyboard interaction, selection, text input etc.

This is done by running JavaScript in an isolated JS context in the UI process form a test. That JavaScript has access to a UIScriptController, which has properties that expose information about the state of the UI process,. and functions to make things happen, including event synthesis.

The UI script itself runs asynchronously, and many of the UIScriptController functions are asynchronous. Callbacks are used to tell the test when something completes.

A simple example

<script>
function runTest()
{
    testRunner.waitUntilDone();
    testRunner.runUIScript("(function() { return uiController.zoomScale; })()", function(result) {
        console.log("Got zoom scale " + result);
    });
}
</script>
<body onload="runTest()"></body>

Some more detail

Let's say we want to write a test that reads back the zoom scale of the WKWebView's UIScrollView (e.g. to test viewport scale).

First, since we're testing viewport zooming, we need to have the test opt into iOS-style viewport behavior with an HTML comment after the doctype:

<!DOCTYPE html> <!-- webkit-test-runner [ useFlexibleViewport=true ] -->

then let's put in the viewport settings to test:

<head>
    <meta name="viewport" content="initial-scale=1.0">
...

Now we want to write some script to read the zoom scale in the UI process. TestRunner now has a runUIScript() function, which takes a string and a callback:

interface TestRunner {
    ...
    // UI Process Testing
    void runUIScript(DOMString script, object callback);

so we can use this in our test in some script that would run from the onload handler:

<script>
function runTest()
{
    testRunner.waitUntilDone();
    if (testRunner.runUIScript) {
        testRunner.runUIScript("...", function(result) {
            document.getElementById('result').innerText = result;
            testRunner.notifyDone();
        });
    }
}
</script>
<body onload="runTest()"></body>

The script passed in to runUIScript() is just a string. You can pass a string literal here, or, for longer scripts make script tag and get its content as text:

<script id="ui-script" type="text/plain">
...
</script>
<script>
    function getUIScript() {
        return document.getElementById('ui-script').text;
    }

    testRunner.runUIScript(getUIScript(), function(result) {
        ...
    });
</script>

So, what script can you run in the UI process? This UI-side script runs in a separate JS context that has no access to the DOM, no timers, no requestAnimationFrame, no XHR etc. What it can do is run vanilla JavaScript, and access properties and functions on UIScriptController, which is exposed as uiController. Here's a simple example, using function-like syntax for the inline script, that returns zoom scale:

<script>
    testRunner.runUIScript("(function() { return uiController.zoomScale; })()", function(result) {
        console.log("Got zoom scale " + result);
    });
</script>

Because testRunner.runUIScript() is asynchronous, the caller has to supply a callback function. The string result returned by the UI process script is passed to this callback function as an argument. You can use this to return simple bits of data as strings, or use JSON.stringify() in the UI script, and JSON.parse() in the test script to get more complex objects across the process boundary.

UIScriptController functions

UIScriptController is the mechanism for driving behavior in the UI process, and to accessing UI state. Many of the functions will trigger UI behaviors that take time (e.g. a zoom animation, or bringing up the keyboard). Rather than blocking, these functions take a callback that is triggered in the UI script context when that operation is complete:

(function() {
    uiController.doubleTapAtPoint(50, 50, function() {
        console.log("Taps were dispatched");
    });
})();

Note that this console.log() won't show in layout test output, since it's being generated in the UI process (but it will be logged if you run WebkitTestRunner directly, which is useful for debugging).

However, the web process needs to be told that the UI script is complete, so when your test has finished doing work in the UI process, it should call uiScriptComplete(), passing the result:

(function() {
    uiController.doubleTapAtPoint(50, 50, function() {
        uiController.uiScriptComplete("Taps were dispatched");
    });
})();

There are some callbacks you can register on UIScriptController to get notified when certain things happen, like zooming. These are useful because event dispatch doesn't imply that zooming is done, since the zoom is animated. So in the above example, we don't have the final zoom scale when calling uiScriptComplete(). We really have to wait for the zoom to finish:

(function() {
    uiController.didEndZoomingCallback = function() {
        uiController.uiScriptComplete('Zoomed to scale ' + uiController.zoomScale);
    };

    uiController.doubleTapAtPoint(50, 50, function() { /* Do nothing here */ });
})();

If your UI-side script is very simple and only accesses uiController properties, then it doesn't need to call uiScriptComplete(). If you call any uiController functions that are asynchronous (i.e. take a callback), then you need to call uiScriptComplete() at some point.

You can chain as many asynchronous things as you like in the UI-side script, as long as you call uiScriptComplete() when the chain is complete. You can also call testRunner.runUIScript() as many times as you like from the test content, making it possible to test a long sequence of operations that bounce between the UI process and the web process.

Future enhancements

UIScriptController will grow to allow tests to drive more behaviors in the UI process, for things like the keyboard, key events, selection, callouts etc.. We will also likely add ways to read back UI process state, like the state of the selection handles and callout bars.

Non-iOS platforms

testRunner.runUIScript() and UIScriptController build and are available on other WebKit2 platforms as well. There's no reason why they could not be used for testing UI-side features there too.