-
Notifications
You must be signed in to change notification settings - Fork 35
Expand file tree
/
Copy pathJavaScriptHandling.java
More file actions
282 lines (254 loc) · 11.8 KB
/
JavaScriptHandling.java
File metadata and controls
282 lines (254 loc) · 11.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
package aquality.selenium.browser.devtools;
import aquality.selenium.browser.AqualityServices;
import aquality.selenium.core.localization.ILocalizedLogger;
import org.openqa.selenium.JavascriptException;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.ScriptKey;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.devtools.events.ConsoleEvent;
import org.openqa.selenium.devtools.events.DomMutationEvent;
import org.openqa.selenium.devtools.idealized.Events;
import org.openqa.selenium.devtools.idealized.Javascript;
import org.openqa.selenium.devtools.idealized.ScriptId;
import org.openqa.selenium.devtools.idealized.target.model.SessionID;
import org.openqa.selenium.devtools.v138.page.Page;
import org.openqa.selenium.devtools.v138.page.model.ScriptIdentifier;
import org.openqa.selenium.devtools.v138.runtime.Runtime;
import org.openqa.selenium.logging.EventType;
import org.openqa.selenium.logging.HasLogEvents;
import org.openqa.selenium.remote.Augmenter;
import java.lang.reflect.Field;
import java.util.*;
import java.util.function.Consumer;
import static org.openqa.selenium.devtools.events.CdpEventTypes.domMutation;
/**
* DevTools commands for version-independent network interception.
* For more information, see {@link Javascript}.
*/
public class JavaScriptHandling {
private final DevToolsHandling tools;
private final Javascript<?, ?> engine;
private final Events<?, ?> events;
private final ILocalizedLogger logger = AqualityServices.getLocalizedLogger();
private final Set<String> bindings = new HashSet<>();
private final Set<InitializationScript> initializationScripts = new HashSet<>();
/**
* Initializes a new instance of the {@link JavaScriptHandling} class.
* @param tools Instance of {@link DevToolsHandling}.
*/
public JavaScriptHandling(DevToolsHandling tools) {
this.tools = tools;
this.engine = tools.getDevToolsSession().getDomains().javascript();
this.events = tools.getDevToolsSession().getDomains().events();
}
/**
* Adds a binding to a callback method that will raise an event when the named binding is called by JavaScript
* executing in the browser.
* @param scriptName The name of the callback that will trigger events.
*/
public void addScriptCallbackBinding(String scriptName) {
logger.info("loc.browser.javascript.scriptcallbackbinding.add", scriptName);
bindings.add(scriptName);
tools.sendCommand(Runtime.addBinding(scriptName, Optional.empty(), Optional.empty()));
}
/**
* Removes a binding to a JavaScript callback.
* @param scriptName The name of the callback to be removed.
*/
public void removeScriptCallbackBinding(String scriptName) {
logger.info("loc.browser.javascript.scriptcallbackbinding.remove", scriptName);
bindings.remove(scriptName);
tools.sendCommand(Runtime.removeBinding(scriptName));
}
/**
* Gets the read-only list of binding callbacks added for this JavaScript engine.
* @return list of binding callbacks added for this JavaScript engine.
*/
public List<String> getScriptCallbackBindings() {
logger.info("loc.browser.javascript.scriptcallbackbindings.get");
return new ArrayList<>(bindings);
}
/**
* Removes all bindings to JavaScript callbacks.
*/
public void clearScriptCallbackBindings() {
logger.info("loc.browser.javascript.scriptcallbackbindings.clear");
bindings.forEach(scriptName -> {
bindings.remove(scriptName);
tools.sendCommand(Runtime.removeBinding(scriptName));
});
}
/**
* Adds JavaScript to be loaded on every document load, and adds a binding to a callback method
* that will raise an event when the script with that name is called.
* @param scriptName The friendly name by which to refer to this initialization script.
* @param script The JavaScript to be loaded on every page.
* @return Initialization script.
*/
public InitializationScript addInitializationScript(String scriptName, String script) {
logger.info("loc.browser.javascript.initializationscript.add", scriptName);
logger.info("loc.browser.javascript.scriptcallbackbinding.add", scriptName);
ScriptId scriptId = engine.pin(scriptName, script);
InitializationScript initializationScript = new InitializationScript(scriptId, scriptName, script);
bindings.add(scriptName);
initializationScripts.add(initializationScript);
return initializationScript;
}
private void removeInitializationScriptCore(InitializationScript script) {
tools.sendCommand(Page.removeScriptToEvaluateOnNewDocument(new ScriptIdentifier(script.getScriptId().getActualId().toString())));
try {
final Field pinnedScripts = Javascript.class.getDeclaredField("pinnedScripts");
pinnedScripts.setAccessible(true);
//noinspection unchecked
((Map<SessionID, Map<String, ScriptId>>)pinnedScripts.get(engine))
.get(tools.getDevToolsSession().getCdpSession())
.remove(script.getScriptSource());
pinnedScripts.setAccessible(false);
} catch (ReflectiveOperationException e) {
AqualityServices.getLogger().fatal("Error while removing initialization script", e);
}
initializationScripts.remove(script);
removeScriptCallbackBinding(script.getScriptName());
}
/**
* Removes JavaScript from being loaded on every document load, and removes a callback binding for it.
* @param script an instance of script to be removed.
*/
public void removeInitializationScript(InitializationScript script) {
logger.info("loc.browser.javascript.initializationscript.remove", script.getScriptName());
removeInitializationScriptCore(script);
}
/**
* Gets the read-only list of initialization scripts added for this JavaScript engine.
* @return the list of added initialization scripts.
*/
public List<InitializationScript> getInitializationScripts() {
logger.info("loc.browser.javascript.initializationscripts.get");
return new ArrayList<>(initializationScripts);
}
/**
* Removes all initialization scripts from being loaded on every document load.
*/
public void clearInitializationScripts() {
logger.info("loc.browser.javascript.initializationscripts.clear");
initializationScripts.forEach(this::removeInitializationScriptCore);
}
private JavascriptExecutor getJavascriptExecutor() {
return (JavascriptExecutor) tools.getDevToolsProvider();
}
/**
* Pins a JavaScript snippet for execution in the browser without transmitting the entire script across the wire for every execution.
* @param script The JavaScript to pin.
* @return object to use to execute the script.
*/
public ScriptKey pinScript(String script) {
logger.info("loc.browser.javascript.snippet.pin");
return getJavascriptExecutor().pin(script);
}
/**
* Unpins a previously pinned script from the browser.
* @param pinnedScript The {@link ScriptKey} object to unpin.
*/
public void unpinScript(ScriptKey pinnedScript) {
logger.info("loc.browser.javascript.snippet.unpin");
getJavascriptExecutor().unpin(pinnedScript);
}
/**
* Gets list of previously pinned scripts.
* @return a set of previously pinned scripts.
*/
public Set<ScriptKey> getPinnedScripts() {
logger.info("loc.browser.javascript.snippets.get");
return getJavascriptExecutor().getPinnedScripts();
}
/**
* Unpins previously pinned scripts from being loaded on every document load.
*/
public void clearPinnedScripts() {
logger.info("loc.browser.javascript.snippets.clear");
getJavascriptExecutor().getPinnedScripts().forEach(getJavascriptExecutor()::unpin);
}
/**
* Starts monitoring for events from the browser's JavaScript engine.
*/
public void startEventMonitoring() {
logger.info("loc.browser.javascript.event.monitoring.start");
tools.sendCommand(Runtime.enable());
}
/**
* Stops monitoring for events from the browser's JavaScript engine, and clears JavaScript console event listeners.
*/
public void stopEventMonitoring() {
logger.info("loc.browser.javascript.event.monitoring.stop");
events.disable();
}
/**
* Adds a listener for events that occur when a JavaScript callback with a named binding is executed.
* To add a binding, use {@link JavaScriptHandling#addScriptCallbackBinding(String)}.
* @param listener a listener to add, consuming a name of exposed script.
*/
public void addBindingCalledListener(Consumer<String> listener) {
logger.info("loc.browser.javascript.event.callbackexecuted.add");
engine.addBindingCalledListener(listener);
}
private HasLogEvents getDriverThatHasLogEvents() {
WebDriver driver = (WebDriver) tools.getDevToolsProvider();
if (!(driver instanceof HasLogEvents)) {
Augmenter augmenter = new Augmenter();
String browserName = AqualityServices.getBrowserProfile().getBrowserName().name().toLowerCase();
driver = augmenter.addDriverAugmentation(browserName, HasLogEvents.class, (caps, exec) -> new HasLogEvents() {
@Override
public <X> void onLogEvent(EventType<X> kind) {
kind.initializeListener((WebDriver) tools.getDevToolsProvider());
}
}).augment(driver);
if (!(driver instanceof HasLogEvents)) {
throw new UnsupportedOperationException(
String.format("Driver for the current browser [%s] doesn't implement HasLogEvents", browserName));
}
}
return (HasLogEvents) driver;
}
/**
* Adds a listener for events that occur when a value of an attribute in an element is being changed.
* @param listener a listener to add, consuming a dom mutation event.
*/
public void addDomMutatedListener(Consumer<DomMutationEvent> listener) {
logger.info("loc.browser.javascript.event.dommutated.add");
getDriverThatHasLogEvents().onLogEvent(domMutation(listener));
}
/**
* Adds a listener for events that occur when methods on the JavaScript console are called.
* @param listener a listener to add, consuming a {@link ConsoleEvent}.
*/
public void addJavaScriptConsoleApiListener(Consumer<ConsoleEvent> listener) {
logger.info("loc.browser.javascript.event.consoleapicalled.add");
events.addConsoleListener(listener);
}
/**
* Adds a listener for events that occur when an exception is thrown by JavaScript being executed in the browser.
* @param listener a listener to add, consuming a javascript exception.
*/
public void addJavaScriptExceptionThrownListener(Consumer<JavascriptException> listener) {
logger.info("loc.browser.javascript.event.exceptionthrown.add");
events.addJavascriptExceptionListener(listener);
}
/**
* Removes all bindings to JavaScript callbacks and all initialization scripts from being loaded for each document.
*/
public void clearAll() {
logger.info("loc.browser.javascript.clearall");
clearInitializationScripts();
clearScriptCallbackBindings();
}
/**
* Removes all bindings to JavaScript callbacks and all initialization scripts from being loaded for each document,
* and stops listening for events.
*/
public void reset() {
logger.info("loc.browser.javascript.reset");
engine.disable();
clearInitializationScripts();
clearScriptCallbackBindings();
}
}