Skip to content

Commit

Permalink
[cdp] Add the ability to listen for page mutations
Browse files Browse the repository at this point in the history
Note, this is a pretty rubbish implementation, and needs to be made
more robust, but it suffices to show that the idea works.
  • Loading branch information
shs96c committed Aug 18, 2020
1 parent 74f3b22 commit 65f0ae8
Show file tree
Hide file tree
Showing 14 changed files with 332 additions and 0 deletions.
109 changes: 109 additions & 0 deletions java/client/src/org/openqa/selenium/devtools/events/CdpEventTypes.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,32 @@

package org.openqa.selenium.devtools.events;

import com.google.common.base.Joiner;
import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.devtools.DevTools;
import org.openqa.selenium.devtools.HasDevTools;
import org.openqa.selenium.internal.Require;
import org.openqa.selenium.json.Json;
import org.openqa.selenium.logging.EventType;
import org.openqa.selenium.logging.HasLogEvents;

import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.function.Consumer;

import static org.openqa.selenium.json.Json.MAP_TYPE;

public class CdpEventTypes {

private static final WeakHashMap<WebDriver, Set<String>> PINNED_SCRIPTS = new WeakHashMap<>();
private static final Json JSON = new Json();

private CdpEventTypes() {
// Utility class.
}
Expand Down Expand Up @@ -56,4 +72,97 @@ public void initializeLogger(HasLogEvents loggable) {
};
}

public static EventType<Void> domMutation(Consumer<DomMutationEvent> handler) {
String script = Joiner.on("\n").join(new String[]{
"(function() {",
"const observer = new MutationObserver((mutations) => {",
" for (const mutation of mutations) {",
" switch (mutation.type) {",
" case \"attributes\":",
// Don't report our own attribute has changed.
" if (mutation.attributeName == 'data-__webdriver_id') {",
" break;",
" }",
" const curr = mutation.target.getAttribute(mutation.attributeName);",
" var id = mutation.target.dataset.__webdriver_id",
" if (!id) {",
" id = Math.random().toString(36).substring(2) + Date.now().toString(36);",
" mutation.target.dataset.__webdriver_id = id;",
" }",
" const json = JSON.stringify({",
" \"target\": id,",
" \"name\": mutation.attributeName,",
" \"value\": curr,",
" \"oldValue\": mutation.oldValue",
" });",
" __webdriver_attribute(json);",
" break;",
" default:",
" break;",
" }",
" }",
"});",
"observer.observe(document, {",
" \"attributes\": true,",
" \"attributeOldValue\": true,",
" \"characterData\": true,",
" \"characterDataOldValue\": true,",
" \"childList\": true,",
" \"subtree\": true",
"});",
"})();"
});

return new EventType<Void>() {
@Override
public void consume(Void event) {
handler.accept(null);
}

@Override
public void initializeLogger(HasLogEvents loggable) {
Require.precondition(loggable instanceof WebDriver, "Loggable must be a WebDriver");
Require.precondition(loggable instanceof HasDevTools, "Loggable must implement HasDevTools");

DevTools tools = ((HasDevTools) loggable).getDevTools();
tools.createSession();

WebDriver driver = (WebDriver) loggable;
Set<String> scripts = PINNED_SCRIPTS.computeIfAbsent(driver, ignored -> new HashSet<>());
if (!scripts.contains(script)) {
// Pin the script
tools.send(tools.getDomains().runtime().enable());
tools.send(tools.getDomains().runtime().addBinding("__webdriver_attribute"));

tools.send(tools.getDomains().page().enable());
tools.send(tools.getDomains().page().addScriptToEvaluateOnNewDocument(script));

// And add the script to the current page
((JavascriptExecutor) driver).executeScript(script);

scripts.add(script);
}

tools.addListener(
tools.getDomains().runtime().bindingCalled(),
bindingCalled -> {
Map<String, Object> values = JSON.toType(bindingCalled.getPayload(), MAP_TYPE);
String id = (String) values.get("target");

List<WebElement> elements = driver.findElements(By.cssSelector(String.format("*[data-__webdriver_id='%s']", id)));

if (!elements.isEmpty()) {
DomMutationEvent event = new DomMutationEvent(
elements.get(0),
String.valueOf(values.get("name")),
String.valueOf(values.get("value")),
String.valueOf(values.get("oldValue")));
handler.accept(event);
}
}
);
}
};
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package org.openqa.selenium.devtools.events;

import org.openqa.selenium.WebElement;

public class DomMutationEvent {

private final WebElement element;
private final String attributeName;
private final String currentValue;
private final String oldValue;

public DomMutationEvent(WebElement element, String attributeName, String currentValue, String oldValue) {
this.element = element;
this.attributeName = attributeName;
this.currentValue = currentValue;
this.oldValue = oldValue;
}

public WebElement getElement() {
return element;
}

public String getAttributeName() {
return attributeName;
}

public String getCurrentValue() {
return currentValue;
}

public String getOldValue() {
return oldValue;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import org.openqa.selenium.devtools.idealized.fetch.Fetch;
import org.openqa.selenium.devtools.idealized.log.Log;
import org.openqa.selenium.devtools.idealized.page.Page;
import org.openqa.selenium.devtools.idealized.runtime.RuntimeDomain;
import org.openqa.selenium.devtools.idealized.target.Target;

Expand All @@ -33,6 +34,8 @@ public interface Domains {

Log log();

Page page();

Target target();

RuntimeDomain runtime();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.openqa.selenium.devtools.idealized.page;

import org.openqa.selenium.devtools.Command;
import org.openqa.selenium.devtools.idealized.page.model.ScriptIdentifier;

public interface Page {

Command<Void> enable();

Command<ScriptIdentifier> addScriptToEvaluateOnNewDocument(String source);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.openqa.selenium.devtools.idealized.page.model;

import org.openqa.selenium.internal.Require;

public class ScriptIdentifier {

private final Object actualIdentifier;

public ScriptIdentifier(Object actualIdentifier) {
this.actualIdentifier = Require.nonNull("Actual identifier", actualIdentifier);
}

public Object getActualIdentifier() {
return actualIdentifier;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,16 @@

import org.openqa.selenium.devtools.Command;
import org.openqa.selenium.devtools.Event;
import org.openqa.selenium.devtools.idealized.runtime.model.BindingCalled;
import org.openqa.selenium.devtools.idealized.runtime.model.ConsoleAPICalled;

public interface RuntimeDomain {

Command<Void> enable();

Event<ConsoleAPICalled> consoleAPICalled();

Command<Void> addBinding(String webdriver_attribute);

Event<BindingCalled> bindingCalled();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.openqa.selenium.devtools.idealized.runtime.model;

import org.openqa.selenium.internal.Require;

public class BindingCalled {

private final String name;
private final String payload;

public BindingCalled(String name, String payload) {
this.name = Require.nonNull("Name", name);
this.payload = Require.nonNull("Payload", payload);
}

public String getName() {
return name;
}

public String getPayload() {
return payload;
}

@Override
public String toString() {
return "BindingCalled{" +
"name='" + name + '\'' +
", payload='" + payload + '\'' +
'}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.openqa.selenium.devtools.idealized.Domains;
import org.openqa.selenium.devtools.idealized.fetch.Fetch;
import org.openqa.selenium.devtools.idealized.log.Log;
import org.openqa.selenium.devtools.idealized.page.Page;
import org.openqa.selenium.devtools.idealized.runtime.RuntimeDomain;
import org.openqa.selenium.devtools.idealized.target.Target;

Expand All @@ -44,6 +45,11 @@ public Log log() {
throw new DevToolsException(WARNING);
}

@Override
public Page page() {
throw new DevToolsException(WARNING);
}

@Override
public RuntimeDomain runtime() {
throw new DevToolsException(WARNING);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.openqa.selenium.devtools.idealized.Domains;
import org.openqa.selenium.devtools.idealized.fetch.Fetch;
import org.openqa.selenium.devtools.idealized.log.Log;
import org.openqa.selenium.devtools.idealized.page.Page;
import org.openqa.selenium.devtools.idealized.runtime.RuntimeDomain;
import org.openqa.selenium.devtools.idealized.target.Target;

Expand All @@ -34,6 +35,11 @@ public Log log() {
return new V84Log();
}

@Override
public Page page() {
throw new UnsupportedOperationException("page");
}

@Override
public RuntimeDomain runtime() {
return new V84Runtime();
Expand Down
21 changes: 21 additions & 0 deletions java/client/src/org/openqa/selenium/devtools/v84/V84Runtime.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@
import org.openqa.selenium.devtools.Command;
import org.openqa.selenium.devtools.Event;
import org.openqa.selenium.devtools.idealized.runtime.RuntimeDomain;
import org.openqa.selenium.devtools.idealized.runtime.model.BindingCalled;
import org.openqa.selenium.devtools.idealized.runtime.model.RemoteObject;
import org.openqa.selenium.devtools.v84.runtime.Runtime;
import org.openqa.selenium.devtools.v84.runtime.model.ConsoleAPICalled;

import java.math.BigDecimal;
import java.time.Instant;
import java.util.List;
import java.util.Optional;

public class V84Runtime implements RuntimeDomain {
@Override
Expand Down Expand Up @@ -56,4 +59,22 @@ public Event<org.openqa.selenium.devtools.idealized.runtime.model.ConsoleAPICall
}
);
}

@Override
public Command<Void> addBinding(String name) {
return Runtime.addBinding(name, Optional.empty());
}

@Override
public Event<BindingCalled> bindingCalled() {
return new Event<BindingCalled>(
org.openqa.selenium.devtools.v84.runtime.Runtime.bindingCalled().getMethod(),
input -> {
org.openqa.selenium.devtools.v84.runtime.model.BindingCalled res = input.read(
org.openqa.selenium.devtools.v84.runtime.model.BindingCalled.class);

return new BindingCalled(res.getName(), res.getPayload());
}
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.openqa.selenium.devtools.idealized.Domains;
import org.openqa.selenium.devtools.idealized.fetch.Fetch;
import org.openqa.selenium.devtools.idealized.log.Log;
import org.openqa.selenium.devtools.idealized.page.Page;
import org.openqa.selenium.devtools.idealized.runtime.RuntimeDomain;
import org.openqa.selenium.devtools.idealized.target.Target;

Expand All @@ -34,6 +35,11 @@ public Log log() {
return new V85Log();
}

@Override
public Page page() {
return new V85Page();
}

@Override
public RuntimeDomain runtime() {
return new V85Runtime();
Expand Down
36 changes: 36 additions & 0 deletions java/client/src/org/openqa/selenium/devtools/v85/V85Page.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.openqa.selenium.devtools.v85;

import com.google.common.collect.ImmutableMap;
import org.openqa.selenium.devtools.Command;
import org.openqa.selenium.devtools.ConverterFunctions;
import org.openqa.selenium.devtools.idealized.page.Page;
import org.openqa.selenium.devtools.v85.page.model.ScriptIdentifier;
import org.openqa.selenium.internal.Require;
import org.openqa.selenium.json.JsonInput;

import java.util.function.Function;

public class V85Page implements Page {

@Override
public Command<Void> enable() {
return org.openqa.selenium.devtools.v85.page.Page.enable();
}

@Override
public Command<org.openqa.selenium.devtools.idealized.page.model.ScriptIdentifier> addScriptToEvaluateOnNewDocument(String source) {
Require.nonNull("Source", source);
ImmutableMap.Builder<String, Object> params = ImmutableMap.builder();
params.put("source", source);

Function<JsonInput, ScriptIdentifier> mapper = ConverterFunctions.map("identifier", ScriptIdentifier.class);

return new Command<>(
"Page.addScriptToEvaluateOnNewDocument",
ImmutableMap.of("source", source),
input -> {
ScriptIdentifier actualId = mapper.apply(input);
return new org.openqa.selenium.devtools.idealized.page.model.ScriptIdentifier(actualId);
});
}
}
Loading

0 comments on commit 65f0ae8

Please sign in to comment.