Permalink
Switch branches/tags
MultipartContent annevk-navigate-to-weird-schemes annevk/EventSource-U+0000 annevk/autofocus annevk/bc-closing annevk/content-length annevk/document-domain annevk/document-open-load-event annevk/document-open-steps-readiness annevk/document-write-reloading annevk/dom-listener-order annevk/encodeInto annevk/fetch-fragments annevk/h1-message-parsing annevk/html-ancestororigins annevk/http-versioning annevk/legacy-pre-activation-behavior-phase annevk/meta-global-annevk annevk/navigate-redirect annevk/navigate-source annevk/opaque-redirect-handling annevk/origin-header annevk/simplify-license annevk/sw-redirect-fragment annevk/template-content-node-document annevk/url-host-parser annevk/window.opener annevk/www-authenticate-parsing annevk/x-frame-options annevk/xhr-h2-statusText annevk/xhr-no-headers-received annevk/xhr-responseurl-with-fragment appveyor ayg/document-named-item bburg-unidentified-keys billing-address-redactions cache-manifest canvas/getContext-second-argument chrome_experimental chromium-export-cl-532595 chromium-export-cl-558917 chromium-export-cl-660842 chromium-export-cl-670844 chromium-export-cl-676883 chromium-export-cl-728365 chromium-export-cl-738353 chromium-export-cl-741361 chromium-export-cl-766373 chromium-export-cl-773321 chromium-export-cl-775179 chromium-export-cl-778160 chromium-export-cl-854092 chromium-export-cl-881121 chromium-export-cl-882906 chromium-export-cl-899411 chromium-export-cl-930542 chromium-export-cl-961719 chromium-export-cl-985959 chromium-export-cl-986593 chromium-export-cl-993812 chromium-export-cl-1013322 chromium-export-cl-1032554 chromium-export-cl-1118750 chromium-export-cl-1128385 chromium-export-cl-1136543 chromium-export-cl-1144133 chromium-export-cl-1146644 chromium-export-cl-1154225 chromium-export-cl-1188018 chromium-export-cl-1188643 chromium-export-cl-1199810 chromium-export-cl-1200863 chromium-export-cl-1212270 chromium-export-cl-1221696 chromium-export-cl-1222913 chromium-export-cl-1249391 chromium-export-cl-1264165 chromium-export-cl-1264520 chromium-export-cl-1270699 chromium-export-cl-1272339 chromium-export-cl-1277666 chromium-export-cl-1288573 chromium-export-cl-1291229 chromium-export-cl-1293050 chromium-export-cl-1293256 chromium-export-cl-1294874 chromium-export-cl-1295618 chromium-export-cl-1306337 chromium-export-cl-1306967 chromium-export-cl-1319616 chromium-export-cl-1322260 chromium-export-cl-1338461 chromium-export-cl-1340567 chromium-export-cl-1344261 chromium-export-cl-1345090 chromium-export-cl-1351490 chromium-export-cl-1355923 chromium-export-cl-1356966 chromium-export-cl-1363178 chromium-export-cl-1363848 chromium-export-cl-1363971 chromium-export-cl-1365235 chromium-export-cl-1365984 chromium-export-cl-1366318 chromium-export-cl-1367725 chromium-export-cl-1367910 chromium-export-cl-1368826 chromium-export-cl-1371186 chromium-export-cl-1372556 chromium-export-cl-1373848 chromium-export-cl-1374113 chromium-export-cl-1374988 chromium-export-cl-1375714 chromium-export-cl-1376317 chromium-export-cl-1377129 chromium-export-cl-1377711 chromium-export-cl-1378805 chromium-export-cl-1378811 chromium-export-cl-1379752 chromium-export-cl-1381144 chromium-export-cl-1381240 chromium-export-cl-1383297 chromium-export-cl-I03bf57a24da3a7479199d04a05c4487342af20ca chromium-export-cl-I53e5b576adebf65ede8ebd1dabe084713ebdf875 chromium-export-cl-I865bb06f8b879e180d4aa6de57010e2609afb8b1 chromium-export-cl-I944ea2ea69447c612c01b9e6f723f110fa28a1f5 chromium-export-cl-I44767f3f65065e9fe0d84a051d4a6b503b439b89 chromium-export-cl-I05157521c98b50fc3816932045a13f6b0eefc9f0 chromium-export-cl-I9846168be36b1de26cc2a508e134c3dd555dafa7 chromium-export-cl-Ib8e35215c5b58721a043087130c9951a56fba3c1 chromium-export-cl-Ib629d1b7e8b86faffc0ddd51888f721df9da4d22 chromium-export-cl-Id7060b7d1292654fc8f23dd72eef629f6a874ef0 chromium-export-cl-Ie2ca10581a3e28974a1eff4e589ea8b62f4cfa62 chromium-export-cl-Ieaa430d9303ab7277440d96b61327b4ec997cd04 chromium-export-cl-Ied400facf474ad18f632c650b636bbbcf725b116 code-of-conduct.md codecov copy_pasta createElement-cereactions csswg-test-pr-561 csswg-test-pr-567 csswg-test-pr-1021 currencysys document-promises drawimage_svg_image_1 dup_shipping_ops epochs/daily epochs/weekly errorfields_warn event-handler-attribute-realm eventtarget-group executorwebdriver ffs-descriptor file_is_error fix-stp-version flaky-test-history foolip/audio-output-securecontext foolip/azure-win10 foolip/fullscreen-catch-throw foolip/getComputedStyle-border foolip/manifest-download-by-tag foolip/rm-travis-stability from-to-element fullscreen-request-vs-exit fullscreen-unloading-steps gecko/sync/upstream/open/1169290/1 gecko/sync/upstream/open/1436763/1 gecko/sync/upstream/open/1437255/1 gecko/1352355 gecko/1383454 gecko/1436763 gecko/1477117 gecko/1487295 gecko/1495601 gecko/1500001 gecko/1507244 gecko/1513959 gwhit-flex-changes-2 gwhit-flex-changes hallvors/clipboard-api-2 hallvors/window-opener idlharness-iterable idlharness-member-tostring initial-selection initial-value-selectionDirection jdm-branch-3 jdm-patch-1 jdm-patch-2 jdm-patch-7 jdm-patch-9 jdm-patch-10 jdm-patch-11 js-urls jugglinmike-master-demo jugglinmike-master-revert-switch jugglinmike-mimic-results-collection-2 jugglinmike-mimic-results-collection jugglinmike-testharness-late-tests-locate jugglinmike-wptrunner_tlbc-demo liamquin-patch-1 link-text-css-quirk lint-meta mach_wpt_serve manifest_optimise manifest_performance marcoscaceres-patch-1 marcoscaceres-patch-3-1 marcoscaceres-patch-3 master matchall-is-slow meta_global mkwst/navigation-domain nikos-svg2-marker nonce-hiding payment-request-abort-method-undefined pendingoperation py3-lint py3-make_hosts_file py3-test-handlers readme reftest_fuzzy requestBillingAddress revert-6614-support_firefox_prefixed_implementation_in_mediacapture-fromelementtests revert-9258-host_ip revert-13200-chrome-webdriver-default scripts-between-documents scroll-behavior scrolling-element show_global show_payment sideshowbarker/assert_object_equals speech-api-historical-onmark sync/upstream/open/1420672 sync/upstream/open/1436659 testfiles_space_path tokenlist updateWith-upon-fulfillment-manual user_activation_iframe valid-pmi wdspec_fixup webdriver-update-existing webdriver_script_timeout webidl-legacy-platform-object webmessaging-user-gesture window-open-optional-features woff2-binary-fonts wptreport_surrogates wptrunner-cdp-less-bad-9-demo wptrunner_testdriver wptserve_list_manifest zcorpan/allowed-to-use-snapshot zcorpan/assert-throws-no-name zcorpan/content-before-legend zcorpan/css-transitions-disabled zcorpan/fieldset-accessibility zcorpan/fieldset-border-radius-hittest zcorpan/fieldset-float-and-flex-or-grid zcorpan/fieldset-replaced-element zcorpan/fieldset-translatez-fixup zcorpan/fieldset-webkit-appearance zcorpan/heading-pseudo zcorpan/joint-session-history-timeout zcorpan/single-opt-in zcorpan/update-mediasource-duration zcorpan/webcrypto-any-js zcorpan/window-open-top-vs-screeny
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
324 lines (251 sloc) 12.8 KB
layout title order
page
testdriver.js Tutorial
8.6

Adding new commands to testdriver.js

Assumptions

We assume the following in this writeup:

  • You know what web-platform-tests is and you have a working checkout and can run tests
  • You know what WebDriver or Selenium is
  • Familiarity with JavaScript and Python

Introduction!

Let's implement window resizing. We can do this via the Set Window Rect command in WebDriver.

First, we need to think of what the API will look like a little. We will be using Selenium and Marionette for this, so we can look and see that they take in x, y coordinates, width and height integers.

The first part of this will be browser agnostic, but later we will need to implement a specific layer for each browser (here we will do Firefox and Chrome).

Code!

resources/testdriver.js

This is the main entry point the tests get. Here we need to add a function to the test_driver object that will call the test_driver_internal object.

window.test_driver = {

    // other commands...

    /**
    * Triggers browser window to be resized and relocated
    *
    * This matches the behaviour of the {@link
    * https://w3c.github.io/webdriver/webdriver-spec.html#dfn-set-window-rect|WebDriver
    * Set Window Rect command}.
    *
    * @param {Integer} x - The x coordinate of the top left of the window
    * @param {Integer} y - The y coordinate of the top left of the window
    * @param {Integer} width - The width of the window
    * @param {Integer} height - The width of the window
    * @returns {Promise} fulfilled after window rect is set occurs, or rejected in
    *                    the cases the WebDriver command errors
    */
    set_window_rect: function(x, y, width, height) {
        return window.test_driver_internal.set_element_rect(x, y, width, height);
    }

In the same file, lets add to the internal object. ( do we need to do this?) (make sure to do this if the internal call has different arguments than the external call, especially if it calls multiple internal calls)

window.test_driver_internal = {

    // other commands...

    /**
     * Triggers browser window to be resized and relocated
     *
     * This matches the behaviour of the {@link
     * https://w3c.github.io/webdriver/webdriver-spec.html#dfn-set-window-rect|WebDriver
     * Set Window Rect command}.
     *
     * @param {Integer} x - The x coordinate of the top left of the window
     * @param {Integer} y - The x coordinate of the top left of the window
     * @param {Integer} width - The width of the window
     * @param {Integer} height - The height of the window
     * @returns {Promise} fulfilled after window rect is set occurs, or rejected in
     *                    the cases the WebDriver command errors
     */
    set_window_rect: function(x, y, width, height) {
        return Promise.reject(new Error("unimplemented"))
    }

We will leave this unimplemented and override it in another file. Lets do that now!

wptrunner/wptrunner/testdriver-extra.js

This will be the default function called when invoking the test driver commands (sometimes it is overridden by testdriver-vendor.js, but this is outside the scope of this writeup).

window.test_driver_internal.set_element_rect = function(x, y, width, height) {
    const pending_promise = new Promise(function(resolve, reject) {
        pending_resolve = resolve;
        pending_reject = reject;
    });
    window.opener.postMessage(
        {"type": "action", "action": "set_window_rect", "x": x, "y": y, "width": width, "height": height}, "*");
    return pending_promise;
};

The main thing here is the postMessage argument. The first argument is an object with properties

  • type: this always has to be the string "action"
  • action: the name of the testdriver command this defines (in this case, set_window_rect)
  • any other things you want to pass to the next point of execution (in this case, the x, y coordinates and the width and height)

The pending promise is out of scope of this function and is resolved when the window recieves a completion message from the executor. This happens here in the same file:

    let pending_resolve = null;
    let pending_reject = null;
    window.addEventListener("message", function(event) {
        const data = event.data;

        if (typeof data !== "object" && data !== null) {
            return;
        }

        if (data.type !== "testdriver-complete") {
            return;
        }

        if (data.status === "success") {
            pending_resolve();
        } else {
            pending_reject();
        }
    });

One limitation this introduces is that only one testdriver call can be made at one time since the pending_resolve and pending_reject variables are in an outer scope.

Next, this is passed to the executor and protocol in wptrunner. Time to switch to Python!

tools/wptrunner/wptrunner/executors/protocol.py

class SetWindowRectProtocolPart(ProtocolPart):
    """Protocol part for resizing and changing location of window"""
    __metaclass__ = ABCMeta

    name = "set_window_rect"

    @abstractmethod
    def set_window_rect(self, x, y, width, height):
        """Change the window rect

        :param x: The x coordinate of the top left of the window.
        :param y: The y coordinate of the top left of the window.
        :param width: The width of the window.
        :param height: The height of the window."""
        pass

Next we change the base executor.

tools/wptrunner/wptrunner/executors/base.py

class CallbackHandler(object):
    """Handle callbacks from testdriver-using tests.

    The default implementation here makes sense for things that are roughly like
    WebDriver. Things that are more different to WebDriver may need to create a
    fully custom implementation."""

    def __init__(self, logger, protocol, test_window):
        self.protocol = protocol
        self.test_window = test_window
        self.logger = logger
        self.callbacks = {
            "action": self.process_action,
            "complete": self.process_complete
        }

        self.actions = {
            "click": ClickAction(self.logger, self.protocol),
            "send_keys": SendKeysAction(self.logger, self.protocol),
            {other actions},
            "set_window_rect": SetWindowRectAction(self.logger, self.protocol) # add this!
        }
class SetWindowRectAction(object):
    def __init__(self, logger, protocol):
        self.logger = logger
        self.protocol = protocol

    def __call__(self, payload):
        x, y, width, height = payload["x"], payload["y"], payload["width"], payload["height"]
        self.logger.debug("Setting window rect to be: x=%s, y=%s, width=%s, height=%s"
                          .format(x, y, width, height))
        self.protocol.set_window_rect.set_window_rect(x, y, width, height)

Don't forget to write docs in testdriver.md. Now we write the browser specific implementations.

Chrome

We will use executorselenium and use the Selenium API (in the future there are plans to use the WebDriver API directly).

There isn't too much work to do here, we just need to define a subclass of the protocol part we defined earlier.

class SeleniumSetWindowRectProtocolPart(SetWindowRectProtocolPart):
    def setup(self):
        self.webdriver = self.parent.webdriver

    def set_window_rect(self, x, y, width, height):
        return self.webdriver.set_window_rect(x, y, width, height)

Make sure to import the protocol part too!

from .protocol import (BaseProtocolPart,
                       TestharnessProtocolPart,
                       Protocol,
                       SelectorProtocolPart,
                       ClickProtocolPart,
                       SendKeysProtocolPart,
                       {... other protocol parts}
                       SetWindowRectProtocolPart, # add this!
                       TestDriverProtocolPart)

Here we have the setup method which just redefines the webdriver object at this level. The important part is the set_window_rect function (and it's important it is named that since we called it that earlier). This will be call the Selenium API for set window rect (self.webdriver is a Selenium WebDriver instance here).

Finally, we just need to tell the SeleniumProtocol to implement this part.

class SeleniumProtocol(Protocol):
    implements = [SeleniumBaseProtocolPart,
                  SeleniumTestharnessProtocolPart,
                  SeleniumSelectorProtocolPart,
                  SeleniumClickProtocolPart,
                  SeleniumSendKeysProtocolPart,
                  {... other protocol parts}
                  SeleniumSetWindowRectProtocolPart,
                  SeleniumTestDriverProtocolPart]

Firefox

We use the set window rect Marionette command.

We will use executormarionette and use the Marionette Python API.

We have little actual work to do here! We just need to define a subclass of the protocol part we defined earlier.

class MarionetteSetWindowRectProtocolPart(SetWindowRectProtocolPart):
    def setup(self):
        self.marionette = self.parent.marionette

    def set_window_rect(self, x, y, width, height):
        return self.marionette.set_window_rect(x, y, width, height)

Make sure to import the protocol part too!

from .protocol import (BaseProtocolPart,
                       TestharnessProtocolPart,
                       Protocol,
                       SelectorProtocolPart,
                       ClickProtocolPart,
                       SendKeysProtocolPart,
                       {... other protocol parts}
                       SetWindowRectProtocolPart, # add this!
                       TestDriverProtocolPart)

Here we have the setup method which just redefines the webdriver object at this level. The important part is the set_window_rect function (and it's important it is named that since we called it that earlier). This will be call the Marionette API for set window rect (self.marionette is a marionette instance here).

Finally, we just need to tell the SeleniumProtocol to implement this part.

class MarionetteProtocol(Protocol):
    implements = [MarionetteBaseProtocolPart,
                  MarionetteTestharnessProtocolPart,
                  MarionettePrefsProtocolPart,
                  MarionetteStorageProtocolPart,
                  MarionetteSelectorProtocolPart,
                  MarionetteClickProtocolPart,
                  MarionetteSendKeysProtocolPart,
                  {... other protocol parts}
                  MarionetteSetWindowRectProtocolPart # add this
                  MarionetteTestDriverProtocolPart]

Other Browsers

Other browsers may also use executorselenium (such as safari), or a completely new executor (such as servo). For these, you must change the executor in the same way as we did with chrome and firefox.

Write an infra test

Make sure to add a test to infrastructure/testdriver :)

Here is some template code!

<!DOCTYPE html>
<meta charset="utf-8">
<title>TestDriver set window rect method</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/resources/testdriver.js"></script>
<script src="/resources/testdriver-vendor.js"></script>

<script>
promise_test(async t => {
  await test_driver.set_window_rect(100, 100, 100, 100);
  // do something
}
</script>

What about testdriver-vendor.js?

The file testdriver-vendor.js is the equivalent to testdriver-extra.js above, except it is run instead of testdriver-extra.js in browser specific test environments. For example, in Chromium LayoutTests.

What if I need to return a value from my testdriver API?

We currently don't have this capability, but it is coming soon and will be documented. The bug is here