, , ,

Firefox WebDriver Newsletter — 124

Posted by

WebDriver is a remote control interface that enables introspection and control of user agents. As such it can help developers to verify that their websites are working and performing well with all major browsers. The protocol is standardized by the W3C and consists of two separate specifications: WebDriver classic (HTTP) and the new WebDriver BiDi (Bi-Directional).

This newsletter gives an overview of the work we’ve done as part of the Firefox 124 release cycle.

Contributions

With Firefox being an open source project, we are grateful to get contributions from people outside of Mozilla.

WebDriver code is written in JavaScript, Python, and Rust so any web developer can contribute! Read how to setup the work environment and check the list of mentored issues for Marionette.

WebDriver BiDi

New: Support for the “storage.getCookies” and “storage.setCookie” commands

First off, we are introducing 2 new commands from the “storage” module to interact with HTTP cookies.

The storage.getCookies command allows to retrieve cookies currently stored in the browser. You can provide a filter argument to only return cookies matching specific criteria (for instance size, domain, path, …). You can also use the partition argument to retrieve cookies owned by a certain partition. You can read more about state partitioning in this MDN article.

The second command storage.setCookie allows to create a new cookie. This command expects a cookie argument with properties describing the cookie to create: name, value, domain, … Similarly to getCookies, you can provide a partition argument which will be used to build the partition key of the partition which should own the cookie.

Below is a quick example using those commands to set and get a cookie, without using partition keys.

Create a new cookie

-> {
  "method": "storage.setCookie",
  "params": {
    "cookie": {
      "name": "test",
      "value": {
        "type": "string",
        "value": "Set from WebDriver BiDi"
      },
      "domain": "fxdx.dev"
    }
  },
  "id": 8
}

<- { "type": "success", "id": 8, "result": { "partitionKey": {} } }

Retrieve the new cookie

-> { "method": "storage.getCookies", "params": { "filter": { "domain": "fxdx.dev" } }, "id": 9 }

<- {
  "type": "success",
  "id": 9,
  "result": {
    "cookies": [
      {
        "domain": "fxdx.dev",
        "httpOnly": false,
        "name": "test",
        "path": "/",
        "sameSite": "none",
        "secure": false,
        "size": 27,
        "value": {
          "type": "string",
          "value": "Set from WebDriver BiDi"
        }
      }
    ],
    "partitionKey": {}
  }
}

New: Basic support for network request interception

With Firefox 124, we are enabling several commands from the network module to allow you to intercept network requests.

Before diving into the details of the various commands, a high level summary of the way this feature works. Requests can be intercepted in three different phases: beforeRequestSent, responseStarted and authRequired. In order to intercept requests, you first need to register network “intercepts”. A network intercept tells the browser which URLs should be intercepted (using simple URL patterns), and in which phase. You might notice that the name of the three phases correspond to some of the network events. This is intentional: when raising a network event, the request will be intercepted if any intercept registered for the corresponding phase matches the URL of the request. Those events will contain additional information in their payload to let you know if the request was blocked or not. When a request is intercepted, it will be paused until you use one of the network interception commands to resume it, modify it or cancel it.

The first two commands will allow you to manage the list of intercepts currently in use. The network.addIntercept command expects a phases argument, which is the list of phases where the intercept will be active, and an optional urlPatterns argument which is a list of URL patterns that will be used to match individual requests. If urlPatterns is omitted, the intercept will match all requests regardless of their URL. This command returns the unique id for the created intercept. You can then use the network.removeIntercept command to remove an intercept, by providing its unique id in the intercept argument.

For instance, adding an intercept to block all requests to https://fxdx.dev/ in the beforeRequestSent phase can be done as shown below:

-> {
  "method": "network.addIntercept",
  "params": {
    "phases": ["beforeRequestSent"],
    "urlPatterns": [{ "type": "string", "pattern": "https://fxdx.dev/" }]
  },
  "id": 10
}

<- { "type": "success", "id": 10, "result": { "intercept": "cc08a9a7-5266-46c5-a5bc-36e842959b0a" } }

Now, assuming you are subscribed to network.beforeRequestSent events for a context where you try to navigate to https://fxdx.dev/, you will get an event payload similar to this:

{
  "type": "event",
  "method": "network.beforeRequestSent",
  "params": {
    "context": "cfca5f66-0b20-49fb-9973-8543956552d1",
    "isBlocked": true,
    "navigation": "e73d918c-985b-4acf-a12b-679271f2fa98",
    "redirectCount": 0,
    "request": { "request": "9", ... },
    "timestamp": 1710354067190,
    "intercepts": ["cc08a9a7-5266-46c5-a5bc-36e842959b0a"],
    "initiator": {
      "type": "other"
    }
  }
}

Note the isBlocked flag set to true, and the intercepts list containing the id for the intercept added earlier. At this point, the request is blocked, and it’s a good time to take a look at which methods you can use to unblock it. Two small notes before moving forward. First remember to subscribe to network events, adding an intercept for “beforeRequestSent” will not transparently subscribe you to network.beforeRequestSent events. Second, removing an intercept with network.removeIntercept will not automatically unblock intercepted requests, they still need to be unblocked individually.

In order to handle intercepted requests, you can use several network commands:

  • network.continueRequest allows to continue a request blocked in the beforeRequestSent phase. It expects a unique id for the request in the request argument. As you can see in the specification, the command also supports several optional arguments to modify the request headers, cookies, method, URL, … However at the moment we do not support those optional arguments, only request is supported.
  • network.continueResponse allows to continue a request blocked either in the responseStarted or the AuthRequired phase. Similar to network.continueRequest, the command expects a mandatory request argument, and other optional arguments are not supported yet.
  • network.continueWithAuth is dedicated to handle requests in the AuthRequired phase, which is especially useful when testing websites using HTTP Authentication. This command expects an action argument which can either be "cancel", "default" or "provideCredentials". The cancel action will fail the authentication attempt. The default action will let the browser handle the authentication via a username / password prompt. Finally, the provideCredentials action expects a credentials argument containing a username and a password. If the credentials are wrong, you will receive another network.authRequired event when the authentication attempt fails, and you can use network.continueWithAuth again to unblock the request.
  • network.failRequest can be used in any of the intercept phases, and will cancel the ongoing request, leading to a network.fetchError event. This command only expects a request argument to identify the request to cancel.
  • network.provideResponse can be used in any of the intercept phases. Similar to network.continueRequest, we only support the request argument at the moment. But in the future this command will allow to set the response body, headers, cookies, …

Back to the example of our request to https://fxdx.dev, we can use network.failRequest to cancel the request, and receive a network.fetchError event in case we are subscribed to those events:

-> { "method": "network.failRequest", "params": { "request": "9" }, "id": 11 }
<- { "type": "success", "id": 11, "result": {} }
<- {
  "type": "event",
  "method": "network.fetchError",
  "params": {
    "context": "cfca5f66-0b20-49fb-9973-8543956552d1",
    "isBlocked": false,
    "navigation": "e73d918c-985b-4acf-a12b-679271f2fa98",
    "redirectCount": 0,
    "request": { "request": "9", ...},
    "timestamp": 1710406136382,
    "errorText": "NS_ERROR_ABORT"
  }
}

New: Support for user contexts

With Firefox 124 we are also adding support for user contexts. User contexts are collections of top-level browsing contexts (tabs) which share the same storage partition. Different user contexts cannot share the same storage partition, so this provides a way to avoid side effects between tests. In Firefox, user contexts are implemented via Containers.

The browser.getUserContexts command will return the list of current user contexts as a list of UserContextInfo objects containing the unique id for each context. This will always include the default user context, which is assigned the id "default".

The browser.createUserContext command allows to create a new user context and returns the UserContextInfo for this user context.

The browser.removeUserContext command allows to remove any existing user context, except for the default one. This command expects a userContext argument to provide the unique id of the user context that should be removed.

-> { "method": "browser.createUserContext", "params": {}, "id": 8 }
<- { "type": "success", "id": 8, "result": { "userContext": "6ade5b81-ef5b-4669-83d6-8119c238a3f7" } }
-> { "method": "browser.getUserContexts", "params": {}, "id": 9 }
<- {
  "type": "success",
  "id": 9,
  "result": {
    "userContexts": [
      { "userContext": "default" },
      { "userContext": "6ade5b81-ef5b-4669-83d6-8119c238a3f7" }
    ]
  }
}
-> { 
  "method": "browser.removeUserContext",
  "params": { 
    "userContext": "6ade5b81-ef5b-4669-83d6-8119c238a3f7" 
  }, 
  "id": 10
}
<- { "type": "success", "id": 10, "result": {} }

Support for userContext with “browsingContext.create” and “browsingContext.Info”

The browsingContext.create command has been updated to support the optional userContext argument, which should be the unique id of an existing user context. If provided, the new tab or window will be owned by the corresponding user context. If not, it will be assigned to the “default” user context.

Firefox windows and tabs created in different containers using browsingContext.create

Also, all browsingContext.Info objects now contain a userContext property, which is set to the unique id of the user context owning the corresponding browsing context.

Support for contexts argument in “script.addPreloadScript”

The script.addPreloadScript command now supports an optional contexts argument so that you can restrict in which browsing contexts a given preload script will be executed. This argument should be a list of unique context ids. If not provided, the preload script will still be applied to all browsing contexts.

Bug Fixes

Marionette (WebDriver classic)

Bug Fixes

  • We fixed an issue with Get Element Text, which ignored the slot value of a web component when no custom text is specified. To give an example, with the following markup, the command will now successfully return “foobar” as the text for the custom element, instead of “bar” previously:
<head>
  <script>
    class TestContainer extends HTMLElement {
      connectedCallback() {
        this.attachShadow({ mode: 'open' });
        this.shadowRoot.innerHTML = `<slot><span>foo</span></slot>bar`;
      }
    }

    customElements.define('test-container', TestContainer);
  </script>
</head>

<body>
  <test-container></test-container>
</body>

Leave a Reply

Your email address will not be published. Required fields are marked *