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 thebeforeRequestSent
phase. It expects a unique id for the request in therequest
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, onlyrequest
is supported.network.continueResponse
allows to continue a request blocked either in theresponseStarted
or theAuthRequired
phase. Similar tonetwork.continueRequest
, the command expects a mandatoryrequest
argument, and other optional arguments are not supported yet.network.continueWithAuth
is dedicated to handle requests in theAuthRequired
phase, which is especially useful when testing websites usingHTTP Authentication
. This command expects anaction
argument which can either be"cancel"
,"default"
or"provideCredentials"
. Thecancel
action will fail the authentication attempt. Thedefault
action will let the browser handle the authentication via a username / password prompt. Finally, theprovideCredentials
action expects acredentials
argument containing a username and a password. If the credentials are wrong, you will receive anothernetwork.authRequired
event when the authentication attempt fails, and you can usenetwork.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 anetwork.fetchError
event. This command only expects arequest
argument to identify the request to cancel.network.provideResponse
can be used in any of the intercept phases. Similar tonetwork.continueRequest
, we only support therequest
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.
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
- We fixed a bug with
browsingContext.close
, which was not able to close the last tab of a window.
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