Features

Selenium 4

This page will help you in upgrading your existing Selenium WebDriver tests to use Selenium 4, with the WebDriver W3C Protocol.

TestingBot has full support for this new Selenium version. Below is more information on how to make this transition.

Updating your tests

With the new W3C protocol, some restrictions apply to using capabilities with your test. Before the W3C protocol (the JSONWP protocol), you could simply add all our custom TestingBot capabilities to your test in the desiredCapabilities.

With the new W3C protocol, only some W3C WebDriver capabilities are allowed inside the capabilities:

  • browserName
  • browserVersion
  • platformName
  • acceptInsecureCerts
  • pageLoadStrategy
  • proxy
  • timeouts
  • unhandledPromptBehavior

All other capabilities are treated as Extension capabilities and should be namespaced (include a vendor prefix).
For TestingBot specific capabilities, you'll need to wrap all capabilities inside a tb:options namespace.

Please see the table below on how the capabilities need to be changed:

Capability JSONWP Capability W3C
browserName browserName
version browserVersion
platform platformName
selenium-version, chromedriverVersion,
extra, name and more
"tb:options" : {
  "selenium-version": '...',
  "chromedriverVersion": '...',
  "extra": '...',
  "name": '...'
}

Example

Before

#!/usr/bin/env ruby
require 'rubygems'
require 'selenium-webdriver'

caps = {
  :browserName => "chrome",
  :version => "latest-1",
  :platform => "WIN10",
  :screenrecorder => true,
  :build => "testbuild",
  :name => "testname"
}

client = Selenium::WebDriver::Remote::Http::Default.new
client.timeout = 480

driver = Selenium::WebDriver.for(
  :remote,
  :url => "https://API_KEY:API_SECRET@hub.testingbot.com/wd/hub",
  :http_client => client,
  :desired_capabilities => caps)
driver.navigate.to "https://www.google.com"
element = driver.find_element(:name, 'q')
element.send_keys "TestingBot"
element.submit
puts driver.title
driver.quit
        
<?php
require_once('vendor/autoload.php');
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\WebDriverBy;
  
  $capabilities = array(
    'browserName' => "chrome",
    'version' => "latest-1",
    'platform' => "WIN10",
    'screenrecorder' => true,
    'build' => "testbuild",
    'name' => "testname"
  );
  $web_driver = RemoteWebDriver::create(
    "https://api_key:api_secret@hub.testingbot.com/wd/hub",
    $capabilities, 240000
  );
  $web_driver->get("https://google.com");

  $element = $web_driver->findElement(WebDriverBy::name("q"));
  if($element) {
      $element->sendKeys("TestingBot");
      $element->submit();
  }
  print $web_driver->getTitle();
  $web_driver->quit();
?>
import org.openqa.selenium.By;
import org.openqa.selenium.Platform;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver;

import java.net.URL;

public class JavaSample {

  public static final String KEY = "KEY";
  public static final String SECRET = "SECRET";
  public static final String URL = "https://" + KEY + ":" + SECRET + "@hub.testingbot.com/wd/hub";

  public static void main(String[] args) throws Exception {

    DesiredCapabilities caps = new DesiredCapabilities();
    caps.setCapability("browserName", "chrome");
    caps.setCapability("version", "latest-1");
    caps.setCapability("platform", "WIN10");
    caps.setCapability("screenrecorder", true);
    caps.setCapability("build", "testbuild");
    caps.setCapability("name", "testname");

    WebDriver driver = new RemoteWebDriver(new URL(URL), caps);
    driver.get("https://www.google.com/ncr");
    WebElement element = driver.findElement(By.name("q"));

    element.sendKeys("TestingBot");
    element.submit();

    System.out.println(driver.getTitle());
    driver.quit();

  }
}
import unittest
import sys

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from testingbotclient import TestingBotClient

class TestTestingBotClient(unittest.TestCase):

    def setUp(self):
      desired_cap = {
        'browserName': 'chrome',
        'version': 'latest-1',
        'platform': 'WIN10',
        'screenrecorder': true,
        'build': 'testbuild',
        'name' : 'testname',
      }

      self.driver = webdriver.Remote(
        command_executor='http://key:secret@hub.testingbot.com/wd/hub',
        desired_capabilities=desired_cap)

    def test_google_example(self):
      self.driver.get("https://www.google.com")
      if not "Google" in self.driver.title:
          raise Exception("Unable to load google page!")
      elem = self.driver.find_element_by_name("q")
      elem.send_keys("TestingBot")
      elem.submit()

    def tearDown(self):
      self.driver.quit()
      status = sys.exc_info() == (None, None, None)
      tb_client = TestingBotClient('key', 'secret')
      tb_client.tests.update_test(self.driver.session_id, self._testMethodName, status)

if __name__ == '__main__':
    unittest.main()
var wd = require('wd'),
testingbotKey = "api_key",
testingbotSecret = "api_secret"
 
desiredCaps = {
  'browserName': 'chrome',
  'version': 'latest-1',
  'platform': 'WIN10',
  'screenrecorder': true,
  'build': 'testbuild',
  'name' : 'testname',
}

driver = wd.remote("https://" + testingbotKey + ":" + testingbotSecret + "@" + "hub.testingbot.com/wd/hub")
driver.init(desiredCaps, function() {
  driver.get('https://www.google.com', function() {
    driver.title(function(err, title) {
        console.log(title)
        driver.quit()
    })
  })
})

After

#!/usr/bin/env ruby
require 'rubygems'
require 'selenium-webdriver'

caps = {
  :browserName => "chrome",
  :browserVersion => "latest-1",
  :platformName => "WIN10",
  "tb:options" => {
    :screenrecorder => true,
    :build => "testbuild",
    :name => "testname",
    "selenium-version" => "3.11.0"
  }
}

client = Selenium::WebDriver::Remote::Http::Default.new
client.timeout = 480

driver = Selenium::WebDriver.for(
  :remote,
  :url => "https://API_KEY:API_SECRET@hub.testingbot.com/wd/hub",
  :http_client => client,
  :desired_capabilities => caps)
driver.navigate.to "https://www.google.com"
element = driver.find_element(:name, 'q')
element.send_keys "TestingBot"
element.submit
puts driver.title
driver.quit
        
<?php
require_once('vendor/autoload.php');
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\WebDriverBy;
  
  $capabilities = array(
    'browserName' => "chrome",
    'browserVersion' => "latest-1",
    'platformName' => "WIN10",
    'tb:options' => {
      'screenrecorder' => true,
      'build' => "testbuild",
      'name' => "testname",
      'selenium-version' => "3.11.0"
    }
  );
  $web_driver = RemoteWebDriver::create(
    "https://api_key:api_secret@hub.testingbot.com/wd/hub",
    $capabilities, 240000
  );
  $web_driver->get("https://google.com");

  $element = $web_driver->findElement(WebDriverBy::name("q"));
  if($element) {
      $element->sendKeys("TestingBot");
      $element->submit();
  }
  print $web_driver->getTitle();
  $web_driver->quit();
?>
import org.openqa.selenium.By;
import org.openqa.selenium.Platform;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver;

import java.net.URL;

public class JavaSample {

  public static final String KEY = "KEY";
  public static final String SECRET = "SECRET";
  public static final String URL = "https://" + KEY + ":" + SECRET + "@hub.testingbot.com/wd/hub";

  public static void main(String[] args) throws Exception {
    MutableCapabilities tbOpts = new MutableCapabilities();
    tbOpts.setCapability("screenrecorder", true);
    tbOpts.setCapability("build", "testbuild");
    tbOpts.setCapability("name", "testname");
    tbOpts.setCapability("selenium-version", "3.11.0");

    DesiredCapabilities caps = new DesiredCapabilities();
    caps.setCapability("browserName", "chrome");
    caps.setCapability("browserVersion", "latest-1");
    caps.setCapability("platformName", "WIN10");
    caps.setCapability("tb:options", tbOpts);

    WebDriver driver = new RemoteWebDriver(new URL(URL), caps);
    driver.get("https://www.google.com/ncr");
    WebElement element = driver.findElement(By.name("q"));

    element.sendKeys("TestingBot");
    element.submit();

    System.out.println(driver.getTitle());
    driver.quit();

  }
}
import unittest
import sys

from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from testingbotclient import TestingBotClient

class TestTestingBotClient(unittest.TestCase):

    def setUp(self):
      desired_cap = {
        'browserName': 'chrome',
        'browserVersion': 'latest-1',
        'platformName': 'WIN10',
        'tb:options' : {
          'screenrecorder': true,
          'build': 'testbuild',
          'name' : 'testname',
          'selenium-version' : '3.11.0'
        }
      }

      self.driver = webdriver.Remote(
        command_executor='http://key:secret@hub.testingbot.com/wd/hub',
        desired_capabilities=desired_cap)

    def test_google_example(self):
      self.driver.get("https://www.google.com")
      if not "Google" in self.driver.title:
          raise Exception("Unable to load google page!")
      elem = self.driver.find_element_by_name("q")
      elem.send_keys("TestingBot")
      elem.submit()

    def tearDown(self):
      self.driver.quit()
      status = sys.exc_info() == (None, None, None)
      tb_client = TestingBotClient('key', 'secret')
      tb_client.tests.update_test(self.driver.session_id, self._testMethodName, status)

if __name__ == '__main__':
    unittest.main()
var wd = require('wd'),
testingbotKey = "api_key",
testingbotSecret = "api_secret"
 
desiredCaps = {
  'browserName': 'chrome',
  'browserVersion': 'latest-1',
  'platformName': 'WIN10',
  'tb:options' : {
    'screenrecorder': true,
    'build': 'testbuild',
    'name' : 'testname',
    'selenium-version' : '3.11.0'
  }
}

driver = wd.remote("https://" + testingbotKey + ":" + testingbotSecret + "@" + "hub.testingbot.com/wd/hub")
driver.init(desiredCaps, function() {
  driver.get('https://www.google.com', function() {
    driver.title(function(err, title) {
        console.log(title)
        driver.quit()
    })
  })
})

New Selenium 4 Features

Below are some of the new features introduced by Selenium 4 which you can use in your TestingBot tests.

New Window

Create or switch to a new (blank) tab or window.

@driver.switch_to.new_window(:window)
@driver.manage.window.position = Selenium::WebDriver::Point.new(200, 400)

@driver.switch_to.new_window(:tab)
driver.switchTo().newWindow(WindowType.WINDOW);
driver.manage().window().setPosition(new Point(200, 400));

driver.switchTo().newWindow(WindowType.TAB);
driver.switch_to.new_window('window')
driver.set_window_position(200, 400)

driver.switch_to.new_window('tab')
Driver.SwitchTo().NewWindow(WindowType.Window);
Driver.Manage().Window.Position = new Point(200, 400);

Driver.SwitchTo().NewWindow(WindowType.Tab);

Print the current page as a PDF, available in Chrome, Edge and Firefox.
You can pass additional options to customize the PDF, such as page size, range, margins, background and shrink to fit.

driver.navigate.to url_for("https://testingbot.com")
path = "PrintPage.pdf"
driver.save_print_page path
Path printPage = Paths.get(directory + "PrintPage.pdf");
Pdf print = driver.print(new PrintOptions());

Files.write(printPage, OutputType.BYTES.convertFromBase64Png(print.getContent()));
headless_driver.get("https://testingbot.com")

pdf = b64decode(headless_driver.print_page())

with open("print_page.pdf", 'wb') as f:
    f.write(pdf)
Driver.Navigate().GoToUrl("https://testingbot.com");
var parentFullName = Directory.GetParent(Environment.CurrentDirectory)?.Parent?.Parent?.FullName;

((ISupportsPrint) Driver).Print(new PrintOptions()).SaveAsFile(parentFullName + "/PrintPage.pdf");

For Chrome and Edge, you need to use headless mode to print pages.

Relative Locators

With Relative Locators, you can identify DOM elements in relationship to other DOM elements.
You can use a more natural syntax such as:

  • above
  • below
  • left of
  • right of
  • near
@driver.get("https://testingbot.com")
element = @driver.find_element(relative: {tag_name: 'div', left: {id: 'outer-orbit'}, below: {id: 'circle-orbit-container'}})
driver.get("https://testingbot.com");
WebElement element = driver.findElement(with(By.tagName("div"))
        .toLeftOf(By.id("outer-orbit"))
        .below(By.id("circle-orbit-container")));
driver.get('https://testingbot.com')
element = driver.find_elements(locate_with(By.TAG_NAME, "div")
                               .to_left_of({By.ID: "outer-orbit"})
                               .below({By.ID: "circle-orbit-container"}))[0]
Driver.Navigate().GoToUrl("https://testingbot.com");
IWebElement element = Driver.FindElement(RelativeBy.WithLocator(By.TagName("div"))
    .LeftOf(By.Id("outer-orbit"))
    .Below(By.Id("circle-orbit-container")));

Retrieve Timeouts

Selenium 4 now allows you to query the driver for the current timeout values.
Please see the examples below on how to do this, for example with implicit waits, pageload timeouts or script timeout.

@driver.manage.timeouts.implicit_wait = 1
p @driver.manage.timeouts.implicit_wait
p @driver.manage.timeouts.page_load
p @driver.manage.timeouts.script
WebDriver.Timeouts timeouts = driver.manage().timeouts();

timeouts.pageLoadTimeout(Duration.ofSeconds(10));
timeouts.implicitlyWait(Duration.ofMillis(11));
timeouts.scriptTimeout(Duration.ofSeconds(12));
timeouts.getPageLoadTimeout();
Assertions.assertEquals(Duration.ofSeconds(10), timeouts.getPageLoadTimeout());
Assertions.assertEquals(Duration.ofMillis(11), timeouts.getImplicitWaitTimeout());
Assertions.assertEquals(Duration.ofSeconds(12), timeouts.getScriptTimeout());
timeouts = Timeouts()
timeouts.implicit_wait = 1
driver.timeouts = timeouts

assert driver.timeouts.implicit_wait == 1
assert driver.timeouts.page_load == 20
assert driver.timeouts.script == 20
Driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(0.4);
Assert.AreEqual(TimeSpan.FromSeconds(0.4), Driver.Manage().Timeouts().ImplicitWait);
Assert.AreEqual(TimeSpan.FromSeconds(4), Driver.Manage().Timeouts().PageLoad);
Assert.AreEqual(TimeSpan.FromSeconds(40), Driver.Manage().Timeouts().AsynchronousJavaScript);

Full Page Screenshots

Firefox only

Firefox implemented a feature to take screenshots of the entire page, not just the viewport, which is the default setting for other browsers or for Selenium 3.

@driver.get('https://testingbot.com')
@driver.save_screenshot('FullScreenshotOnFirefox.png', full_page: true)
WebDriver augmentedDriver = new Augmenter().augment(driver);
File file = ((HasFullPageScreenshot) augmentedDriver).getFullPageScreenshotAs(OutputType.FILE);

Path fullPageScreenshot = Paths.get(directory + "FullScreenshotOnFirefox.png");
Files.move(file.toPath(), fullPageScreenshot);
IHasCommandExecutor hasCommandExecutor = Driver as IHasCommandExecutor;
var addFullPageScreenshotCommandInfo = new HttpCommandInfo(HttpCommandInfo.GetCommand, 
    "/session/{sessionId}/moz/screenshot/full");
hasCommandExecutor.CommandExecutor.TryAddCommand("fullPageScreenshot", addFullPageScreenshotCommandInfo);

SessionId sessionId = ((RemoteWebDriver)Driver).SessionId;
var fullPageScreenshotCommand = new Command(sessionId, "fullPageScreenshot", null);

Driver.Navigate().GoToUrl("https://testingbot.com");
var screenshotResponse = hasCommandExecutor.CommandExecutor.Execute(fullPageScreenshotCommand);
string base64 = screenshotResponse.Value.ToString();
Screenshot image = new Screenshot(base64);

var parentFullName = Directory.GetParent(Environment.CurrentDirectory)?.Parent?.Parent?.FullName;
image.SaveAsFile(parentFullName + "/FullScreenshotOnFirefox.png", ScreenshotImageFormat.Png);

Network Conditions

Chrome and Edge only

Selenium 4 offers the possibility to modify network conditions for your tests, such as:

  • Setting latency
  • Modifying upstream/downstream throughput
  • Going into offline mode
@driver.network_conditions = {offline: true, latency: 1, throughput: 10000}
WebDriver augmentedDriver = new Augmenter().augment(driver);
ChromiumNetworkConditions networkConditions = new ChromiumNetworkConditions();
networkConditions.setOffline(true);
((HasNetworkConditions) augmentedDriver).setNetworkConditions(networkConditions);

try {
    driver.get("https://testingbot.com");
} catch (WebDriverException ex) {
    ((HasNetworkConditions) augmentedDriver).setNetworkConditions(new ChromiumNetworkConditions());
}

TestingBot has been offering throtteling, mocking and intercepting network requests before Selenium 4.

Change Preferences During Session

Firefox only

Before Selenium 4, it was only possible to set preferences (through capabilities) at the start of a test.
Firefox now provides the option to change preferences during a session. You can do this by toggling the context between chrome and content.

Make sure to switch back to the correct context, or some commands might fail.

@driver.get("https://www.google.com")

language = @driver.find_element(id: "gws-output-pages-elements-homepage_additional_languages__als")
p language
@driver.context = 'chrome'
@driver.execute_script("Services.prefs.setStringPref('intl.accept_languages', 'es-ES')")
@driver.context = 'content'
@driver.navigate.refresh
language = @driver.find_element(id: "gws-output-pages-elements-homepage_additional_languages__als")
p language
@driver.quit
driver.get("https://www.google.com");

String lang1 = driver.findElement(By.id("gws-output-pages-elements-homepage_additional_languages__als")).getText();
Assertions.assertTrue(lang1.contains("offered in"));

WebDriver augmentedDriver = new Augmenter().augment(driver);
((HasContext) augmentedDriver).setContext(FirefoxCommandContext.CHROME);

((JavascriptExecutor) driver).executeScript("Services.prefs.setStringPref('intl.accept_languages', 'es-ES')");

((HasContext) augmentedDriver).setContext(FirefoxCommandContext.CONTENT);
driver.navigate().refresh();

String lang2 = driver.findElement(By.id("gws-output-pages-elements-homepage_additional_languages__als")).getText();
Assertions.assertTrue(lang2.contains("Ofrecido por"));
This feature is not yet supported in the Python bindings.
IHasCommandExecutor hasCommandExecutor = Driver as IHasCommandExecutor;
var getContextCommandInfo = new HttpCommandInfo(HttpCommandInfo.GetCommand, "/session/{sessionId}/moz/context");
var setContextCommandInfo = new HttpCommandInfo(HttpCommandInfo.PostCommand, "/session/{sessionId}/moz/context");
hasCommandExecutor.CommandExecutor.TryAddCommand("getContext", getContextCommandInfo);
hasCommandExecutor.CommandExecutor.TryAddCommand("setContext", setContextCommandInfo);

SessionId sessionId = ((RemoteWebDriver)Driver).SessionId;

Driver.Navigate().GoToUrl("https://www.google.com");
var element = Driver.FindElement(By.CssSelector("#gws-output-pages-elements-homepage_additional_languages__als"));

Assert.IsTrue(element.Text.Contains("offered in"));

try
{
    Driver.ExecuteJavaScript("Services.prefs.setStringPref('intl.accept_languages', 'es-ES')");
    Assert.Fail("Can not change Service prefs in content context, so previous method should fail");
}
catch (WebDriverException)
{
    // This is expected
}

var getContextCommand = new Command(sessionId, "getContext", null);
Response response = hasCommandExecutor.CommandExecutor.Execute(getContextCommand);

Assert.AreEqual("content", response.Value);

Dictionary<String, Object> payload = new Dictionary<string, object>();
payload.Add("context", "chrome");
var setContextCommand = new Command(sessionId, "setContext", payload);
hasCommandExecutor.CommandExecutor.Execute(setContextCommand);

response = hasCommandExecutor.CommandExecutor.Execute(getContextCommand);
Assert.AreEqual("chrome", response.Value);

Driver.ExecuteJavaScript("Services.prefs.setStringPref('intl.accept_languages', 'es-ES')");

try
{
    Driver.Navigate().Refresh();
    Assert.Fail("Can not navigate in chrome context, so previous method should fail");
}
catch (WebDriverException)
{
    // This is expected
}

payload.Remove("context");
payload.Add("context", "content");
setContextCommand = new Command(sessionId, "setContext", payload);
hasCommandExecutor.CommandExecutor.Execute(setContextCommand);

Driver.Navigate().Refresh();
element = Driver.FindElement(By.CssSelector("#gws-output-pages-elements-homepage_additional_languages__als"));
Assert.IsTrue(element.Text.Contains("Ofrecido por"));
          

BiDirectional Functionality

The WebDriver BiDirectional Protocol is a new protocol enhancement that should improve the speed and reliability of Webdriver tests. Its mechanism is similar to CDP (Chrome DevTools Protocol), but is implemented as a vendor agnostic protocol.

At this point, TestingBot does not yet providi BiDi support. We are working on adding this functionality in Q2 of 2023.

Upgrading dependencies

Please see the examples below on how to upgrade to Selenium 4.

gem 'selenium-webdriver', '~> 4.0.0'
<dependencies>
    <dependency>
        <groupId>org.seleniumhq.selenium</groupId>
        <artifactId>selenium-java</artifactId>
        <version>4.0.0</version>
    </dependency>
</dependencies>

You can run npm install selenium-webdriver to install the latest version, or create/modify your package.json file:

{
  "name": "selenium-tests",
  "version": "1.0.0",
  "dependencies": {
    "selenium-webdriver": "^4.0.0"
  }
}
pip install selenium==4.0.0
PM> Install-Package Selenium.WebDriver -Version 4.0.0

Deprecation Messages and Errors

Below is a list of potential errors or deprecation messages you might see when upgrading to Selenium 4.

AddAdditionalOption

The AddAdditionalCapability capability is deprecated in Selenium 4, it has been replaced by AddAdditionalOption.

var browserOptions = new ChromeOptions();
browserOptions.PlatformName = "Windows 10";
browserOptions.BrowserVersion = "latest";
var tbOptions = new Dictionary<string, object>();
browserOptions.AddAdditionalCapability("tb:options", tbOptions, true);
var browserOptions = new ChromeOptions();
browserOptions.PlatformName = "Windows 10";
browserOptions.BrowserVersion = "latest";
var tbOptions = new Dictionary<string, object>();
browserOptions.AddAdditionalOption("tb:options", tbOptions);

Timeout Parameters

The parameters passed to Timeout functions have switched from long (TimeUnit) to duration (Duration).

driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
driver.manage().timeouts().setScriptTimeout(4, TimeUnit.MINUTES);
driver.manage().timeouts().pageLoadTimeout(5, TimeUnit.SECONDS);
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
driver.manage().timeouts().scriptTimeout(Duration.ofMinutes(4));
driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(5));

Wait Parameters

WebDriverWait, withTimeout and pollingEvery now expect Duration instead of a long time.

new WebDriverWait(driver, 3)
.until(ExpectedConditions.elementToBeClickable(By.cssSelector("#id")));

Wait<webdriver> wait = new FluentWait<webdriver>(driver)
  .withTimeout(30, TimeUnit.SECONDS)
  .pollingEvery(5, TimeUnit.SECONDS)
  .ignoring(NoSuchElementException.class);</webdriver></webdriver>
new WebDriverWait(driver, Duration.ofSeconds(3))
.until(ExpectedConditions.elementToBeClickable(By.cssSelector("#id")));

Wait<webdriver> wait = new FluentWait<webdriver>(driver)
  .withTimeout(Duration.ofSeconds(30))
  .pollingEvery(Duration.ofSeconds(5))
  .ignoring(NoSuchElementException.class);</webdriver></webdriver>

Merging Capabilities

Before Selenium 4 you could merge capabilities, mutating the calling object.
Since Selenium 4 this is deprecated, you now need to manually assign the result of the merge operation.

MutableCapabilities capabilities = new MutableCapabilities();
capabilities.setCapability("platformVersion", "Windows 10");
FirefoxOptions options = new FirefoxOptions();
options.setHeadless(true);
options.merge(capabilities);
MutableCapabilities capabilities = new MutableCapabilities();
capabilities.setCapability("platformVersion", "Windows 10");
FirefoxOptions options = new FirefoxOptions();
options.setHeadless(true);
options = options.merge(capabilities);