1
0
mirror of synced 2026-08-05 23:16:54 +00:00

feat: support exposeBinding/exposeFunction

This commit is contained in:
Yury Semikhatsky
2020-10-05 11:41:37 -07:00
parent 96b4bf57b2
commit aa9bba1782
13 changed files with 387 additions and 143 deletions
@@ -531,6 +531,22 @@ class Interface extends TypeDefinition {
output.add(offset + " return height;");
output.add(offset + " }");
output.add(offset + "}");
output.add("");
output.add(offset + "interface Function {");
output.add(offset + " Object call(Object... args);");
output.add(offset + "}");
output.add("");
output.add(offset + "interface Binding {");
output.add(offset + " interface Source {");
output.add(offset + " BrowserContext context();");
output.add(offset + " Page page();");
output.add(offset + " Frame frame();");
output.add(offset + " }");
output.add("");
output.add(offset + " Object call(Source source, Object... args);");
output.add(offset + "}");
break;
}
case "BrowserContext": {
@@ -152,20 +152,19 @@ class Types {
// js functions are always passed as text in java.
add("BrowserContext.exposeBinding.playwrightBinding", "function", "String");
add("BrowserContext.exposeFunction.playwrightFunction", "function", "String");
add("Page.$eval.pageFunction", "function(Element)", "String");
add("Page.$$eval.pageFunction", "function(Array<Element>)", "String");
add("Page.exposeBinding.playwrightBinding", "function", "String");
add("Page.exposeFunction.playwrightFunction", "function", "String");
add("Frame.$eval.pageFunction", "function(Element)", "String");
add("Frame.$$eval.pageFunction", "function(Array<Element>)", "String");
add("ElementHandle.$eval.pageFunction", "function(Element)", "String");
add("ElementHandle.$$eval.pageFunction", "function(Array<Element>)", "String");
add("ElementHandle.evaluate.pageFunction", "function", "String");
add("JSHandle.evaluate.pageFunction", "function", "String");
add("ChromiumBrowserContext.exposeBinding.playwrightBinding", "function", "String");
add("ChromiumBrowserContext.exposeFunction.playwrightFunction", "function", "String");
add("BrowserContext.exposeBinding.playwrightBinding", "function", "Page.Binding");
add("BrowserContext.exposeFunction.playwrightFunction", "function", "Page.Function");
add("Page.exposeBinding.playwrightBinding", "function", "Binding");
add("Page.exposeFunction.playwrightFunction", "function", "Function");
add("BrowserContext.addInitScript.script", "function|string|Object", "String");
add("Page.addInitScript.script", "function|string|Object", "String");
@@ -78,8 +78,8 @@ public interface BrowserContext {
return cookies(null);
}
List<Object> cookies(String urls);
void exposeBinding(String name, String playwrightBinding);
void exposeFunction(String name, String playwrightFunction);
void exposeBinding(String name, Page.Binding playwrightBinding);
void exposeFunction(String name, Page.Function playwrightFunction);
default void grantPermissions(List<String> permissions) {
grantPermissions(permissions, null);
}
@@ -38,6 +38,20 @@ public interface Page {
}
}
interface Function {
Object call(Object... args);
}
interface Binding {
interface Source {
BrowserContext context();
Page page();
Frame frame();
}
Object call(Source source, Object... args);
}
enum LoadState { DOMCONTENTLOADED, LOAD, NETWORKIDLE }
class CloseOptions {
public Boolean runBeforeUnload;
@@ -789,8 +803,8 @@ public interface Page {
return evaluateHandle(pageFunction, null);
}
JSHandle evaluateHandle(String pageFunction, Object arg);
void exposeBinding(String name, String playwrightBinding);
void exposeFunction(String name, String playwrightFunction);
void exposeBinding(String name, Binding playwrightBinding);
void exposeFunction(String name, Function playwrightFunction);
default void fill(String selector, String value) {
fill(selector, value, null);
}
@@ -0,0 +1,82 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.BrowserContext;
import com.microsoft.playwright.Frame;
import com.microsoft.playwright.Page;
import java.util.ArrayList;
import java.util.List;
import static com.microsoft.playwright.impl.Serialization.*;
class BindingCall extends ChannelOwner {
private static class SourceImpl implements Page.Binding.Source {
private final Frame frame;
public SourceImpl(Frame frame) {
this.frame = frame;
}
@Override
public BrowserContext context() {
return page().context();
}
@Override
public Page page() {
return frame.page();
}
@Override
public Frame frame() {
return frame;
}
}
BindingCall(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
}
String name() {
return initializer.get("name").getAsString();
}
void call(Page.Binding binding) {
try {
Frame frame = connection.getExistingObject(initializer.getAsJsonObject("frame").get("guid").getAsString());
Page.Binding.Source source = new SourceImpl(frame);
List<Object> args = new ArrayList<>();
for (JsonElement arg : initializer.getAsJsonArray("args")) {
args.add(deserialize(new Gson().fromJson(arg, SerializedValue.class)));
}
Object result = binding.call(source, args.toArray());
JsonObject params = new JsonObject();
params.add("result", new Gson().toJsonTree(serializeArgument(result)));
sendMessage("resolve", params);
} catch (RuntimeException exception) {
JsonObject params = new JsonObject();
params.add("error", new Gson().toJsonTree(serializeError(exception)));
sendMessage("reject", params);
}
}
}
@@ -21,18 +21,22 @@ import com.google.gson.JsonObject;
import com.microsoft.playwright.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import static com.microsoft.playwright.impl.Utils.globToRegex;
import static com.microsoft.playwright.impl.Utils.isFunctionBody;
class BrowserContextImpl extends ChannelOwner implements BrowserContext {
private final List<PageImpl> pages = new ArrayList<>();
private List<RouteInfo> routes = new ArrayList<>();
final Map<String, Page.Binding> bindings = new HashMap<String, Page.Binding>();
private class RouteInfo {
private String url;
private BiConsumer<Route, Request> handler;
@@ -61,7 +65,13 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
@Override
public void addInitScript(String script, Object arg) {
// TODO: serialize arg
JsonObject params = new JsonObject();
if (isFunctionBody(script)) {
script = "(" + script + ")()";
}
params.addProperty("source", script);
sendMessage("addInitScript", params);
}
@Override
@@ -85,13 +95,24 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
}
@Override
public void exposeBinding(String name, String playwrightBinding) {
public void exposeBinding(String name, Page.Binding playwrightBinding) {
if (bindings.containsKey(name)) {
throw new RuntimeException("Function " + name + " has already been registered");
}
for (PageImpl page : pages) {
if (page.bindings.containsKey(name))
throw new Error("Function " + name + " has already been registered in one of the pages");
}
bindings.put(name, playwrightBinding);
JsonObject params = new JsonObject();
params.addProperty("name", name);
sendMessage("exposeBinding", params);
}
@Override
public void exposeFunction(String name, String playwrightFunction) {
public void exposeFunction(String name, Page.Function playwrightFunction) {
exposeBinding(name, (Page.Binding.Source source, Object... args) -> playwrightFunction.call(args));
}
@Override
@@ -191,6 +212,15 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
}
}
route.continue_();
} else if ("page".equals(event)) {
PageImpl page = connection.getExistingObject(params.getAsJsonObject("page").get("guid").getAsString());
pages.add(page);
} else if ("bindingCall".equals(event)) {
BindingCall bindingCall = connection.getExistingObject(params.getAsJsonObject("binding").get("guid").getAsString());
Page.Binding binding = bindings.get(bindingCall.name());
if (binding != null) {
bindingCall.call(binding);
}
}
}
}
@@ -174,6 +174,9 @@ public class Connection {
ChannelOwner result = null;
// initializer = this._replaceGuidsWithChannels(initializer);
switch (type) {
case "BindingCall":
result = new BindingCall(parent, type, guid, initializer);
break;
case "BrowserType":
result = new BrowserTypeImpl(parent, type, guid, initializer);
break;
@@ -23,10 +23,11 @@ import com.google.gson.JsonObject;
import com.microsoft.playwright.*;
import java.util.*;
import java.util.function.Supplier;
import static com.microsoft.playwright.Frame.LoadState.*;
import static com.microsoft.playwright.impl.Helpers.isFunctionBody;
import static com.microsoft.playwright.impl.Serialization.deserialize;
import static com.microsoft.playwright.impl.Serialization.serializeArgument;
import static com.microsoft.playwright.impl.Utils.isFunctionBody;
public class FrameImpl extends ChannelOwner implements Frame {
PageImpl page;
@@ -49,99 +50,6 @@ public class FrameImpl extends ChannelOwner implements Frame {
}
}
private static SerializedValue serializeValue(Object value) {
SerializedValue result = new SerializedValue();
if (value == null)
result.v = "undefined";
else if (value instanceof Double) {
double d = ((Double) value).doubleValue();
if (d == Double.POSITIVE_INFINITY)
result.v = "Infinity";
else if (d == Double.NEGATIVE_INFINITY)
result.v = "-Infinity";
else if (d == -0)
result.v = "-0";
else if (Double.isNaN(d))
result.v="NaN";
else
result.n = d;
}
// if (value instanceof Date)
else if (value instanceof Boolean)
result.b = (Boolean) value;
else if (value instanceof Integer)
result.n = (Integer) value;
else if (value instanceof String)
result.s = (String) value;
else if (value instanceof List) {
List<SerializedValue> list = new ArrayList<>();
for (Object o : (List) value)
list.add(serializeValue(o));
result.a = list.toArray(new SerializedValue[0]);
} else if (value instanceof Map) {
List<SerializedValue.O> list = new ArrayList<>();
Map<String, Object> map = (Map<String, Object>) value;
for (Map.Entry<String, Object> e : map.entrySet()) {
SerializedValue.O o = new SerializedValue.O();
o.k = e.getKey();
o.v = serializeValue(e.getValue());
list.add(o);
}
result.o = list.toArray(new SerializedValue.O[0]);
} else
throw new RuntimeException("Unsupported type of argument: " + value);
return result;
}
private static SerializedArgument serializeArgument(Object arg) {
SerializedArgument result = new SerializedArgument();
result.value = serializeValue(arg);
result.handles = new Channel[0];
return result;
}
private static <T> T deserialize(SerializedValue value) {
if (value.n != null) {
if (value.n.doubleValue() == (double) value.n.intValue())
return (T) Integer.valueOf(value.n.intValue());
return (T) Double.valueOf(value.n.doubleValue());
}
if (value.b != null)
return (T) value.b;
if (value.s != null)
return (T) value.s;
if (value.v != null) {
switch (value.v) {
case "undefined":
case "null":
return null;
case "Infinity":
return (T) Double.valueOf(Double.POSITIVE_INFINITY);
case "-Infinity":
return (T) Double.valueOf(Double.NEGATIVE_INFINITY);
case "-0":
return (T) Double.valueOf(-0);
case "NaN":
return (T) Double.valueOf(Double.NaN);
default:
throw new RuntimeException("Unexpected value: " + value.v);
}
}
if (value.a != null) {
List list = new ArrayList();
for (SerializedValue v : value.a)
list.add(deserialize(v));
return (T) list;
}
if (value.o != null) {
Map map = new LinkedHashMap<>();
for (SerializedValue.O o : value.o)
map.put(o.k, deserialize(o.v));
return (T) map;
}
throw new RuntimeException("Unexpected result: " + new Gson().toJson(value));
}
public <T> T evalTyped(String expression) {
return (T) evaluate(expression, null, false);
}
@@ -1,26 +0,0 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
class Helpers {
static boolean isFunctionBody(String expression) {
expression = expression.trim();
return expression.startsWith("function") ||
expression.startsWith("async ") ||
expression.contains("=>");
}
}
@@ -16,15 +16,13 @@
package com.microsoft.playwright.impl;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
@@ -32,14 +30,17 @@ import static com.microsoft.playwright.impl.Utils.convertViaJson;
public class PageImpl extends ChannelOwner implements Page {
private final BrowserContextImpl browserContext;
private final FrameImpl mainFrame;
private final KeyboardImpl keyboard;
private final MouseImpl mouse;
private final List<DialogHandler> dialogHandlers = new ArrayList<>();
private final List<Listener<ConsoleMessage>> consoleListeners = new ArrayList<>();
final Map<String, Binding> bindings = new HashMap<String, Binding>();
PageImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
browserContext = (BrowserContextImpl) parent;
mainFrame = connection.getExistingObject(initializer.getAsJsonObject("mainFrame").get("guid").getAsString());
mainFrame.page = this;
keyboard = new KeyboardImpl(this);
@@ -202,13 +203,23 @@ public class PageImpl extends ChannelOwner implements Page {
}
@Override
public void exposeBinding(String name, String playwrightBinding) {
public void exposeBinding(String name, Binding playwrightBinding) {
if (bindings.containsKey(name)) {
throw new RuntimeException("Function " + name + " has already been registered");
}
if (browserContext.bindings.containsKey(name)) {
throw new RuntimeException("Function " + name + " has already been registered in the browser context");
}
bindings.put(name, playwrightBinding);
JsonObject params = new JsonObject();
params.addProperty("name", name);
sendMessage("exposeBinding", params);
}
@Override
public void exposeFunction(String name, String playwrightFunction) {
public void exposeFunction(String name, Function playwrightFunction) {
exposeBinding(name, (Binding.Source source, Object... args) -> playwrightFunction.call(args));
}
@Override
@@ -0,0 +1,132 @@
/**
* Copyright (c) Microsoft Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.microsoft.playwright.impl;
import com.google.gson.Gson;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
class Serialization {
static SerializedError serializeError(Throwable e) {
SerializedError result = new SerializedError();
result.error = new SerializedError.Error();
result.error.message = e.getMessage();
result.error.name = e.getClass().getName();
ByteArrayOutputStream out = new ByteArrayOutputStream();
e.printStackTrace(new PrintStream(out));
result.error.stack = new String(out.toByteArray());
return result;
}
private static SerializedValue serializeValue(Object value) {
SerializedValue result = new SerializedValue();
if (value == null)
result.v = "undefined";
else if (value instanceof Double) {
double d = ((Double) value).doubleValue();
if (d == Double.POSITIVE_INFINITY)
result.v = "Infinity";
else if (d == Double.NEGATIVE_INFINITY)
result.v = "-Infinity";
else if (d == -0)
result.v = "-0";
else if (Double.isNaN(d))
result.v="NaN";
else
result.n = d;
}
else if (value instanceof Boolean)
result.b = (Boolean) value;
else if (value instanceof Integer)
result.n = (Integer) value;
else if (value instanceof String)
result.s = (String) value;
else if (value instanceof List) {
List<SerializedValue> list = new ArrayList<>();
for (Object o : (List) value)
list.add(serializeValue(o));
result.a = list.toArray(new SerializedValue[0]);
} else if (value instanceof Map) {
List<SerializedValue.O> list = new ArrayList<>();
Map<String, Object> map = (Map<String, Object>) value;
for (Map.Entry<String, Object> e : map.entrySet()) {
SerializedValue.O o = new SerializedValue.O();
o.k = e.getKey();
o.v = serializeValue(e.getValue());
list.add(o);
}
result.o = list.toArray(new SerializedValue.O[0]);
} else
throw new RuntimeException("Unsupported type of argument: " + value);
return result;
}
static SerializedArgument serializeArgument(Object arg) {
SerializedArgument result = new SerializedArgument();
result.value = serializeValue(arg);
result.handles = new Channel[0];
return result;
}
static <T> T deserialize(SerializedValue value) {
if (value.n != null) {
if (value.n.doubleValue() == (double) value.n.intValue())
return (T) Integer.valueOf(value.n.intValue());
return (T) Double.valueOf(value.n.doubleValue());
}
if (value.b != null)
return (T) value.b;
if (value.s != null)
return (T) value.s;
if (value.v != null) {
switch (value.v) {
case "undefined":
case "null":
return null;
case "Infinity":
return (T) Double.valueOf(Double.POSITIVE_INFINITY);
case "-Infinity":
return (T) Double.valueOf(Double.NEGATIVE_INFINITY);
case "-0":
return (T) Double.valueOf(-0);
case "NaN":
return (T) Double.valueOf(Double.NaN);
default:
throw new RuntimeException("Unexpected value: " + value.v);
}
}
if (value.a != null) {
List list = new ArrayList();
for (SerializedValue v : value.a)
list.add(deserialize(v));
return (T) list;
}
if (value.o != null) {
Map map = new LinkedHashMap<>();
for (SerializedValue.O o : value.o)
map.put(o.k, deserialize(o.v));
return (T) map;
}
throw new RuntimeException("Unexpected result: " + new Gson().toJson(value));
}
}
@@ -27,6 +27,13 @@ class Utils {
return new Gson().fromJson(json, t);
}
static boolean isFunctionBody(String expression) {
expression = expression.trim();
return expression.startsWith("function") ||
expression.startsWith("async ") ||
expression.contains("=>");
}
static Set<Character> escapeGlobChars = new HashSet<>(Arrays.asList('/', '$', '^', '+', '.', '(', ')', '=', '!', '|'));
static String globToRegex(String glob) {
@@ -83,5 +90,4 @@ class Utils {
tokens.append('$');
return tokens.toString();
}
}
@@ -196,4 +196,73 @@ public class TestPopup {
assertEquals(mapOf("width", 600, "height", 300), size);
assertEquals(mapOf("width", 500, "height", 400), resized);
}
@Test
void should_respect_routes_from_browser_context_with_window_open() {
BrowserContext context = browser.newContext();
Page page = context.newPage();
page.navigate(server.EMPTY_PAGE);
boolean[] intercepted = {false};
context.route("**/empty.html", (route, request) -> {
route.continue_();
intercepted[0] = true;
});
Deferred<Page> popupEvent = page.waitForPopup();
page.evaluate("url => window['__popup'] = window.open(url)", server.EMPTY_PAGE);
popupEvent.get();
assertTrue(intercepted[0]);
context.close();
}
@Test
void BrowserContext_addInitScript_should_apply_to_an_in_process_popup() {
BrowserContext context = browser.newContext();
context.addInitScript("() => window['injected'] = 123");
Page page = context.newPage();
page.navigate(server.EMPTY_PAGE);
Object injected = page.evaluate("() => {\n" +
" const win = window.open('about:blank');\n" +
" return win['injected'];\n" +
"}");
context.close();
assertEquals(123, injected);
}
@Test
void BrowserContext_addInitScript_should_apply_to_a_cross_process_popup() {
BrowserContext context = browser.newContext();
context.addInitScript("() => window['injected'] = 123");
Page page = context.newPage();
page.navigate(server.EMPTY_PAGE);
Deferred<Page> popupEvent = page.waitForPopup();
page.evaluate("url => window.open(url)", server.CROSS_PROCESS_PREFIX + "/title.html");
Page popup = popupEvent.get();
assertEquals(123, popup.evaluate("injected"));
popup.reload();
assertEquals(123, popup.evaluate("injected"));
context.close();
}
@Test
void should_expose_function_from_browser_context() {
BrowserContext context = browser.newContext();
List<String> messages = new ArrayList<>();
context.exposeFunction("add", args -> {
messages.add("binding");
return (int) args[0] + (int) args[1];
});
Page page = context.newPage();
// context.on("page", () => messages.push('page'));
page.navigate(server.EMPTY_PAGE);
Object added = page.evaluate("async () => {\n" +
" const win = window.open('about:blank');\n" +
" return win['add'](9, 4);\n" +
"}");
context.close();
assertEquals(13, added);
// assertEquals(messages.join("|"), "page|binding");
}
}