Chrome Devtools Protocol with BiDi API
Usage
The following list of APIs will be growing as the Selenium project works through supporting real world use cases. If there is additional functionality you’d like to see, please raise a feature request.
As these examples are re-implemented with the WebDriver-Bidi protocol, they will be moved to the WebDriver Bidi pages.
Examples
Basic authentication
Some applications make use of browser authentication to secure pages. It used to be common to handle them in the URL, but browser stopped supporting this. With BiDi, you can now provide the credentials when necessary
Alternate implementations can be found at CDP Endpoint Basic Authentication and CDP API Basic Authentication
Predicate<URI> uriPredicate = uri -> uri.toString().contains("herokuapp.com");
Supplier<Credentials> authentication = UsernameAndPassword.of("admin", "admin");
((HasAuthentication) driver).register(uriPredicate, authentication);
An alternate implementation may be found at CDP Endpoint Basic Authentication
var handler = new NetworkAuthenticationHandler()
{
UriMatcher = uri => uri.AbsoluteUri.Contains("herokuapp"),
Credentials = new PasswordCredentials("admin", "admin")
};
var networkInterceptor = driver.Manage().Network;
networkInterceptor.AddAuthenticationHandler(handler);
await networkInterceptor.StartMonitoring();
driver.register(username: 'admin',
password: 'admin',
uri: /herokuapp/)
const {Builder} = require('selenium-webdriver');
(async function example() {
try {
let driver = await new Builder()
.forBrowser('chrome')
.build();
const pageCdpConnection = await driver.createCDPConnection('page');
await driver.register('username', 'password', pageCdpConnection);
await driver.get('https://the-internet.herokuapp.com/basic_auth');
await driver.quit();
}catch (e){
console.log(e)
}
}())
val uriPredicate = Predicate { uri: URI ->
uri.host.contains("your-domain.com")
}
(driver as HasAuthentication).register(uriPredicate, UsernameAndPassword.of("admin", "password"))
driver.get("https://your-domain.com/login")
Pin scripts
This can be especially useful when executing on a remote server. For example, whenever you check the visibility of an element, or whenever you use the classic get attribute method, Selenium is sending the contents of a js file to the script execution endpoint. These files are each about 50kB, which adds up.
var key = await new JavaScriptEngine(driver).PinScript("return arguments;");
var arguments = ((WebDriver)driver).ExecuteScript(key, 1, true, element);
key = driver.pin_script('return arguments;')
arguments = driver.execute_script(key, 1, true, element)
Mutation observation
Mutation Observation is the ability to capture events via WebDriver BiDi when there are DOM mutations on a specific element in the DOM.
CopyOnWriteArrayList<WebElement> mutations = new CopyOnWriteArrayList<>();
((HasLogEvents) driver).onLogEvent(domMutation(e -> mutations.add(e.getElement())));
async with driver.bidi_connection() as session:
log = Log(driver, session)
var mutations = new List<IWebElement>();
using IJavaScriptEngine monitor = new JavaScriptEngine(driver);
monitor.DomMutated += (_, e) =>
{
var locator = By.CssSelector($"*[data-__webdriver_id='{e.AttributeData.TargetId}']");
mutations.Add(driver.FindElement(locator));
};
await monitor.StartEventMonitoring();
await monitor.EnableDomMutationMonitoring();
mutations = []
driver.on_log_event(:mutation) { |mutation| mutations << mutation.element }
const {Builder, until} = require('selenium-webdriver');
const assert = require("assert");
(async function example() {
try {
let driver = await new Builder()
.forBrowser('chrome')
.build();
const cdpConnection = await driver.createCDPConnection('page');
await driver.logMutationEvents(cdpConnection, event => {
assert.deepStrictEqual(event['attribute_name'], 'style');
assert.deepStrictEqual(event['current_value'], "");
assert.deepStrictEqual(event['old_value'], "display:none;");
});
await driver.get('dynamic.html');
await driver.findElement({id: 'reveal'}).click();
let revealed = driver.findElement({id: 'revealed'});
await driver.wait(until.elementIsVisible(revealed), 5000);
await driver.quit();
}catch (e){
console.log(e)
}
}())
Console logs and errors
Listen to the console.log
events and register callbacks to process the event.
CDP API Console logs and WebDriver BiDi Console logs
Use the WebDriver BiDi Console logs implementation. HasLogEvents
will likely end up deprecated because it does not implement Closeable
.
CopyOnWriteArrayList<String> messages = new CopyOnWriteArrayList<>();
((HasLogEvents) driver).onLogEvent(consoleEvent(e -> messages.add(e.getMessages().get(0))));
async with driver.bidi_connection() as session:
log = Log(driver, session)
async with log.add_listener(Console.ALL) as messages:
using IJavaScriptEngine monitor = new JavaScriptEngine(driver);
var messages = new List<string>();
monitor.JavaScriptConsoleApiCalled += (_, e) =>
{
messages.Add(e.MessageContent);
};
await monitor.StartEventMonitoring();
logs = []
driver.on_log_event(:console) { |log| logs << log.args.first }
const {Builder} = require('selenium-webdriver');
(async () => {
try {
let driver = new Builder()
.forBrowser('chrome')
.build();
const cdpConnection = await driver.createCDPConnection('page');
await driver.onLogEvent(cdpConnection, function (event) {
console.log(event['args'][0]['value']);
});
await driver.executeScript('console.log("here")');
await driver.quit();
}catch (e){
console.log(e);
}
})()
fun kotlinConsoleLogExample() {
val driver = ChromeDriver()
val devTools = driver.devTools
devTools.createSession()
val logConsole = { c: ConsoleEvent -> print("Console log message is: " + c.messages)}
devTools.domains.events().addConsoleListener(logConsole)
driver.get("https://www.google.com")
val executor = driver as JavascriptExecutor
executor.executeScript("console.log('Hello World')")
val input = driver.findElement(By.name("q"))
input.sendKeys("Selenium 4")
input.sendKeys(Keys.RETURN)
driver.quit()
}
JavaScript exceptions
Listen to the JS Exceptions and register callbacks to process the exception details.
Use the WebDriver BiDi JavaScript exceptions implementation
async with driver.bidi_connection() as session:
log = Log(driver, session)
async with log.add_js_error_listener() as messages:
using IJavaScriptEngine monitor = new JavaScriptEngine(driver);
var messages = new List<string>();
monitor.JavaScriptExceptionThrown += (_, e) =>
{
messages.Add(e.Message);
};
await monitor.StartEventMonitoring();
exceptions = []
driver.on_log_event(:exception) { |exception| exceptions << exception }
Network Interception
Both requests and responses can be recorded or transformed.
Response information
CopyOnWriteArrayList<String> contentType = new CopyOnWriteArrayList<>();
try (NetworkInterceptor ignored =
new NetworkInterceptor(
driver,
(Filter)
next ->
req -> {
HttpResponse res = next.execute(req);
contentType.add(res.getHeader("Content-Type"));
return res;
})) {
var contentType = new List<string>();
INetwork networkInterceptor = driver.Manage().Network;
networkInterceptor.NetworkResponseReceived += (_, e) =>
{
contentType.Add(e.ResponseHeaders["content-type"]);
};
await networkInterceptor.StartMonitoring();
content_type = []
driver.intercept do |request, &continue|
continue.call(request) do |response|
content_type << response.headers['content-type']
end
end
Response transformation
try (NetworkInterceptor ignored =
new NetworkInterceptor(
driver,
Route.matching(req -> true)
.to(
() ->
req ->
new HttpResponse()
.setStatus(200)
.addHeader("Content-Type", MediaType.HTML_UTF_8.toString())
.setContent(Contents.utf8String("Creamy, delicious cheese!"))))) {
var handler = new NetworkResponseHandler()
{
ResponseMatcher = _ => true,
ResponseTransformer = _ => new HttpResponseData
{
StatusCode = 200,
Body = "Creamy, delicious cheese!"
}
};
INetwork networkInterceptor = driver.Manage().Network;
networkInterceptor.AddResponseHandler(handler);
await networkInterceptor.StartMonitoring();
driver.intercept do |request, &continue|
continue.call(request) do |response|
response.body = 'Creamy, delicious cheese!' if request.url.include?('blank')
end
end
const connection = await driver.createCDPConnection('page')
let url = fileServer.whereIs("/cheese")
let httpResponse = new HttpResponse(url)
httpResponse.addHeaders("Content-Type", "UTF-8")
httpResponse.body = "sausages"
await driver.onIntercept(connection, httpResponse, async function () {
let body = await driver.getPageSource()
assert.strictEqual(body.includes("sausages"), true, `Body contains: ${body}`)
})
driver.get(url)
val driver = ChromeDriver()
val interceptor = new NetworkInterceptor(
driver,
Route.matching(req -> true)
.to(() -> req -> new HttpResponse()
.setStatus(200)
.addHeader("Content-Type", MediaType.HTML_UTF_8.toString())
.setContent(utf8String("Creamy, delicious cheese!"))))
driver.get(appServer.whereIs("/cheese"))
String source = driver.getPageSource()
Request interception
var handler = new NetworkRequestHandler
{
RequestMatcher = request => request.Url.Contains("one.js"),
RequestTransformer = request =>
{
request.Url = request.Url.Replace("one", "two");
return request;
}
};
INetwork networkInterceptor = driver.Manage().Network;
networkInterceptor.AddRequestHandler(handler);
await networkInterceptor.StartMonitoring();
driver.intercept do |request, &continue|
uri = URI(request.url)
request.url = uri.to_s.gsub('one', 'two') if uri.path&.end_with?('one.js')
continue.call(request)
end