feat: implement Page.exposeBinding and convert more binding tests (#21)

This commit is contained in:
Yury Semikhatsky 2020-10-19 15:49:30 -07:00 committed by GitHub
parent 36ec91762f
commit aa1448355a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 303 additions and 11792 deletions

View File

@ -29,13 +29,13 @@ jobs:
run: |
FILE_NAME="unkwnown"
if [[ $OS == *"ubuntu"* ]]; then
FILE_NAME=playwright-cli-0.5.3-linux.zip
FILE_NAME=playwright-cli-0.151.0-linux.zip
fi
if [[ $OS == *"macos"* ]]; then
FILE_NAME=playwright-cli-0.5.3-mac.zip
FILE_NAME=playwright-cli-0.151.0-mac.zip
fi
if [[ $OS == *"windows"* ]]; then
FILE_NAME=playwright-cli-0.5.3-win32_x64.zip
FILE_NAME=playwright-cli-0.151.0-win32_x64.zip
fi
echo "Downloading from $FILE_NAME"
curl -O https://playwright.azureedge.net/builds/cli/$FILE_NAME

View File

@ -16,8 +16,8 @@ cd playwright-java
```bash
mkdir driver
cd driver
curl -O https://playwright.azureedge.net/builds/cli/playwright-cli-0.5.3-linux.zip
unzip playwright-cli-0.5.3-linux.zip -d .
curl -O https://playwright.azureedge.net/builds/cli/playwright-cli-0.151.0-linux.zip
unzip playwright-cli-0.151.0-linux.zip -d .
./playwright-cli install
```

File diff suppressed because one or more lines are too long

View File

@ -88,10 +88,8 @@ public interface Browser {
public BrowserContext.HTTPCredentials httpCredentials;
public ColorScheme colorScheme;
public Logger logger;
public String relativeArtifactsPath;
public Boolean recordVideos;
public String videosPath;
public VideoSize videoSize;
public Boolean recordTrace;
public NewContextOptions withAcceptDownloads(Boolean acceptDownloads) {
this.acceptDownloads = acceptDownloads;
@ -165,22 +163,14 @@ public interface Browser {
this.logger = logger;
return this;
}
public NewContextOptions withRelativeArtifactsPath(String relativeArtifactsPath) {
this.relativeArtifactsPath = relativeArtifactsPath;
return this;
}
public NewContextOptions withRecordVideos(Boolean recordVideos) {
this.recordVideos = recordVideos;
public NewContextOptions withVideosPath(String videosPath) {
this.videosPath = videosPath;
return this;
}
public VideoSize setVideoSize() {
this.videoSize = new VideoSize();
return this.videoSize;
}
public NewContextOptions withRecordTrace(Boolean recordTrace) {
this.recordTrace = recordTrace;
return this;
}
}
class NewPageOptions {
public enum ColorScheme { DARK, LIGHT, NO_PREFERENCE }
@ -245,10 +235,8 @@ public interface Browser {
public BrowserContext.HTTPCredentials httpCredentials;
public ColorScheme colorScheme;
public Logger logger;
public String relativeArtifactsPath;
public Boolean recordVideos;
public String videosPath;
public VideoSize videoSize;
public Boolean recordTrace;
public NewPageOptions withAcceptDownloads(Boolean acceptDownloads) {
this.acceptDownloads = acceptDownloads;
@ -322,22 +310,14 @@ public interface Browser {
this.logger = logger;
return this;
}
public NewPageOptions withRelativeArtifactsPath(String relativeArtifactsPath) {
this.relativeArtifactsPath = relativeArtifactsPath;
return this;
}
public NewPageOptions withRecordVideos(Boolean recordVideos) {
this.recordVideos = recordVideos;
public NewPageOptions withVideosPath(String videosPath) {
this.videosPath = videosPath;
return this;
}
public VideoSize setVideoSize() {
this.videoSize = new VideoSize();
return this.videoSize;
}
public NewPageOptions withRecordTrace(Boolean recordTrace) {
this.recordTrace = recordTrace;
return this;
}
}
void close();
List<BrowserContext> contexts();

View File

@ -46,6 +46,14 @@ public interface BrowserContext {
void addListener(EventType type, Listener<EventType> listener);
void removeListener(EventType type, Listener<EventType> listener);
class ExposeBindingOptions {
public Boolean handle;
public ExposeBindingOptions withHandle(Boolean handle) {
this.handle = handle;
return this;
}
}
class GrantPermissionsOptions {
public String origin;
@ -85,7 +93,10 @@ public interface BrowserContext {
return cookies(null);
}
List<Object> cookies(String urls);
void exposeBinding(String name, Page.Binding playwrightBinding);
default void exposeBinding(String name, Page.Binding playwrightBinding) {
exposeBinding(name, playwrightBinding, null);
}
void exposeBinding(String name, Page.Binding playwrightBinding, ExposeBindingOptions options);
void exposeFunction(String name, Page.Function playwrightFunction);
default void grantPermissions(List<String> permissions) {
grantPermissions(permissions, null);

View File

@ -78,7 +78,6 @@ public interface BrowserType {
public Boolean ignoreDefaultArgs;
public Proxy proxy;
public String downloadsPath;
public String artifactsPath;
public Boolean chromiumSandbox;
public String firefoxUserPrefs;
public Boolean handleSIGINT;
@ -114,10 +113,6 @@ public interface BrowserType {
this.downloadsPath = downloadsPath;
return this;
}
public LaunchOptions withArtifactsPath(String artifactsPath) {
this.artifactsPath = artifactsPath;
return this;
}
public LaunchOptions withChromiumSandbox(Boolean chromiumSandbox) {
this.chromiumSandbox = chromiumSandbox;
return this;
@ -240,7 +235,6 @@ public interface BrowserType {
public Proxy proxy;
public Boolean acceptDownloads;
public String downloadsPath;
public String artifactsPath;
public Boolean chromiumSandbox;
public Boolean handleSIGINT;
public Boolean handleSIGTERM;
@ -266,10 +260,8 @@ public interface BrowserType {
public Boolean offline;
public BrowserContext.HTTPCredentials httpCredentials;
public ColorScheme colorScheme;
public String relativeArtifactsPath;
public Boolean recordVideos;
public String videosPath;
public VideoSize videoSize;
public Boolean recordTrace;
public LaunchPersistentContextOptions withHeadless(Boolean headless) {
this.headless = headless;
@ -299,10 +291,6 @@ public interface BrowserType {
this.downloadsPath = downloadsPath;
return this;
}
public LaunchPersistentContextOptions withArtifactsPath(String artifactsPath) {
this.artifactsPath = artifactsPath;
return this;
}
public LaunchPersistentContextOptions withChromiumSandbox(Boolean chromiumSandbox) {
this.chromiumSandbox = chromiumSandbox;
return this;
@ -403,22 +391,14 @@ public interface BrowserType {
this.colorScheme = colorScheme;
return this;
}
public LaunchPersistentContextOptions withRelativeArtifactsPath(String relativeArtifactsPath) {
this.relativeArtifactsPath = relativeArtifactsPath;
return this;
}
public LaunchPersistentContextOptions withRecordVideos(Boolean recordVideos) {
this.recordVideos = recordVideos;
public LaunchPersistentContextOptions withVideosPath(String videosPath) {
this.videosPath = videosPath;
return this;
}
public VideoSize setVideoSize() {
this.videoSize = new VideoSize();
return this.videoSize;
}
public LaunchPersistentContextOptions withRecordTrace(Boolean recordTrace) {
this.recordTrace = recordTrace;
return this;
}
}
class LaunchServerOptions {
public class Proxy {
@ -457,7 +437,6 @@ public interface BrowserType {
public String ignoreDefaultArgs;
public Proxy proxy;
public String downloadsPath;
public String artifactsPath;
public Boolean chromiumSandbox;
public String firefoxUserPrefs;
public Boolean handleSIGINT;
@ -496,10 +475,6 @@ public interface BrowserType {
this.downloadsPath = downloadsPath;
return this;
}
public LaunchServerOptions withArtifactsPath(String artifactsPath) {
this.artifactsPath = artifactsPath;
return this;
}
public LaunchServerOptions withChromiumSandbox(Boolean chromiumSandbox) {
this.chromiumSandbox = chromiumSandbox;
return this;

View File

@ -294,6 +294,14 @@ public interface Page {
return this;
}
}
class ExposeBindingOptions {
public Boolean handle;
public ExposeBindingOptions withHandle(Boolean handle) {
this.handle = handle;
return this;
}
}
class FillOptions {
public Boolean noWaitAfter;
public Integer timeout;
@ -836,7 +844,10 @@ public interface Page {
return evaluateHandle(pageFunction, null);
}
JSHandle evaluateHandle(String pageFunction, Object arg);
void exposeBinding(String name, Binding playwrightBinding);
default void exposeBinding(String name, Binding playwrightBinding) {
exposeBinding(name, playwrightBinding, null);
}
void exposeBinding(String name, Binding playwrightBinding, ExposeBindingOptions options);
void exposeFunction(String name, Function playwrightFunction);
default void fill(String selector, String value) {
fill(selector, value, null);
@ -939,6 +950,7 @@ public interface Page {
void unroute(Pattern url, BiConsumer<Route, Request> handler);
void unroute(Predicate<String> url, BiConsumer<Route, Request> handler);
String url();
Video video();
Viewport viewportSize();
default Deferred<Event<EventType>> waitForEvent(EventType event) {
return waitForEvent(event, null);

View File

@ -0,0 +1,24 @@
/**
* 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;
import java.util.*;
public interface Video {
String path();
}

View File

@ -21,6 +21,7 @@ import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.BrowserContext;
import com.microsoft.playwright.Frame;
import com.microsoft.playwright.JSHandle;
import com.microsoft.playwright.Page;
import java.util.ArrayList;
@ -65,9 +66,14 @@ class BindingCall extends ChannelOwner {
Frame frame = connection.getExistingObject(initializer.getAsJsonObject("frame").get("guid").getAsString());
Page.Binding.Source source = new SourceImpl(frame);
List<Object> args = new ArrayList<>();
if (initializer.has("handle")) {
JSHandle handle = connection.getExistingObject(initializer.getAsJsonObject("handle").get("guid").getAsString());
args.add(handle);
} else {
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();

View File

@ -102,19 +102,22 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext {
}
@Override
public void exposeBinding(String name, Page.Binding playwrightBinding) {
public void exposeBinding(String name, Page.Binding playwrightBinding, ExposeBindingOptions options) {
if (bindings.containsKey(name)) {
throw new RuntimeException("Function " + name + " has already been registered");
throw new RuntimeException("Function \"" + name + "\" has been already registered");
}
for (PageImpl page : pages) {
if (page.bindings.containsKey(name)) {
throw new Error("Function " + name + " has already been registered in one of the pages");
throw new RuntimeException("Function \"" + name + "\" has been already registered in one of the pages");
}
}
bindings.put(name, playwrightBinding);
JsonObject params = new JsonObject();
params.addProperty("name", name);
if (options != null && options.handle != null && options.handle) {
params.addProperty("needsHandle", true);
}
sendMessage("exposeBinding", params);
}

View File

@ -22,7 +22,6 @@ import com.google.gson.JsonObject;
import com.microsoft.playwright.*;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;
import java.util.function.Predicate;
import java.util.regex.Pattern;
@ -37,10 +36,9 @@ public class PageImpl extends ChannelOwner implements Page {
private final MouseImpl mouse;
private Viewport viewport;
private final Router routes = new Router();
// TODO: do not rely on the frame order in the tests
private final Set<FrameImpl> frames = new LinkedHashSet<>();
private final ListenerCollection<EventType> listeners = new ListenerCollection<>();
final Map<String, Binding> bindings = new HashMap<String, Binding>();
final Map<String, Binding> bindings = new HashMap<>();
BrowserContextImpl ownedContext;
private boolean isClosed;
final Set<Worker> workers = new HashSet<>();
@ -55,15 +53,6 @@ public class PageImpl extends ChannelOwner implements Page {
frames.add(mainFrame);
}
public Deferred<Page> waitForPopup() {
CompletableFuture<JsonObject> popupFuture = futureForEvent("popup");
return () -> {
JsonObject params = waitForCompletion(popupFuture);
String guid = params.getAsJsonObject("page").get("guid").getAsString();
return connection.getExistingObject(guid);
};
}
@Override
protected void handleEvent(String event, JsonObject params) {
if ("dialog".equals(event)) {
@ -96,6 +85,20 @@ public class PageImpl extends ChannelOwner implements Page {
String guid = params.getAsJsonObject("element").get("guid").getAsString();
FileChooser fileChooser = connection.getExistingObject(guid);
listeners.notify(EventType.FILECHOOSER, fileChooser);
} else if ("bindingCall".equals(event)) {
String guid = params.getAsJsonObject("binding").get("guid").getAsString();
BindingCall bindingCall = connection.getExistingObject(guid);
Binding binding = bindings.get(bindingCall.name());
if (binding == null) {
binding = browserContext.bindings.get(bindingCall.name());
}
if (binding != null) {
try {
bindingCall.call(binding);
} catch (RuntimeException e) {
e.printStackTrace();
}
}
} else if ("load".equals(event)) {
listeners.notify(EventType.LOAD, null);
} else if ("domcontentloaded".equals(event)) {
@ -303,17 +306,20 @@ public class PageImpl extends ChannelOwner implements Page {
}
@Override
public void exposeBinding(String name, Binding playwrightBinding) {
public void exposeBinding(String name, Binding playwrightBinding, ExposeBindingOptions options) {
if (bindings.containsKey(name)) {
throw new RuntimeException("Function " + name + " has already been registered");
throw new RuntimeException("Function \"" + name + "\" has been already registered");
}
if (browserContext.bindings.containsKey(name)) {
throw new RuntimeException("Function " + name + " has already been registered in the browser context");
throw new RuntimeException("Function \"" + name + "\" has been already registered in the browser context");
}
bindings.put(name, playwrightBinding);
JsonObject params = new JsonObject();
params.addProperty("name", name);
if (options != null && options.handle != null && options.handle) {
params.addProperty("needsHandle", true);
}
sendMessage("exposeBinding", params);
}
@ -609,6 +615,11 @@ public class PageImpl extends ChannelOwner implements Page {
return mainFrame.url();
}
@Override
public Video video() {
return null;
}
@Override
public Viewport viewportSize() {
return viewport;
@ -740,7 +751,7 @@ public class PageImpl extends ChannelOwner implements Page {
}));
waitables.add(createWaitForCloseHelper());
if (options != null && options.timeout != null) {
waitables.add(new WaitableTimeout(options.timeout.intValue()));
waitables.add(new WaitableTimeout(options.timeout));
}
return toDeferred(new WaitableRace(waitables));
}
@ -756,7 +767,7 @@ public class PageImpl extends ChannelOwner implements Page {
}));
waitables.add(createWaitForCloseHelper());
if (options != null && options.timeout != null) {
waitables.add(new WaitableTimeout(options.timeout.intValue()));
waitables.add(new WaitableTimeout(options.timeout));
}
return toDeferred(new WaitableRace(waitables));
}

View File

@ -24,6 +24,7 @@ import com.microsoft.playwright.Mouse;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.lang.reflect.Array;
import java.util.*;
class Serialization {
@ -49,31 +50,32 @@ class Serialization {
handles.add((JSHandleImpl) value);
return result;
}
if (value == null)
if (value == null) {
result.v = "undefined";
else if (value instanceof Double) {
} else if (value instanceof Double) {
double d = ((Double) value).doubleValue();
if (d == Double.POSITIVE_INFINITY)
if (d == Double.POSITIVE_INFINITY) {
result.v = "Infinity";
else if (d == Double.NEGATIVE_INFINITY)
} else if (d == Double.NEGATIVE_INFINITY) {
result.v = "-Infinity";
else if (d == -0)
} else if (d == -0) {
result.v = "-0";
else if (Double.isNaN(d))
result.v="NaN";
else
} else if (Double.isNaN(d)) {
result.v = "NaN";
} else {
result.n = d;
}
else if (value instanceof Boolean)
} else if (value instanceof Boolean) {
result.b = (Boolean) value;
else if (value instanceof Integer)
} else if (value instanceof Integer) {
result.n = (Integer) value;
else if (value instanceof String)
} else if (value instanceof String) {
result.s = (String) value;
else if (value instanceof List) {
} else if (value instanceof List) {
List<SerializedValue> list = new ArrayList<>();
for (Object o : (List) value)
for (Object o : (List) value) {
list.add(serializeValue(o, handles, depth + 1));
}
result.a = list.toArray(new SerializedValue[0]);
} else if (value instanceof Map) {
List<SerializedValue.O> list = new ArrayList<>();
@ -85,8 +87,15 @@ class Serialization {
list.add(o);
}
result.o = list.toArray(new SerializedValue.O[0]);
} else
} else if (value instanceof Object[]) {
List<SerializedValue> list = new ArrayList<>();
for (Object o : (Object[]) value) {
list.add(serializeValue(o, handles, depth + 1));
}
result.a = list.toArray(new SerializedValue[0]);
} else {
throw new RuntimeException("Unsupported type of argument: " + value);
}
return result;
}
@ -106,8 +115,9 @@ class Serialization {
static <T> T deserialize(SerializedValue value) {
if (value.n != null) {
if (value.n.doubleValue() == (double) value.n.intValue())
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)
@ -133,14 +143,16 @@ class Serialization {
}
if (value.a != null) {
List list = new ArrayList();
for (SerializedValue v : value.a)
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)
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));

View File

@ -0,0 +1,167 @@
/**
* 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;
import org.junit.jupiter.api.*;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;
import static com.microsoft.playwright.Page.EventType.FRAMENAVIGATED;
import static com.microsoft.playwright.Utils.expectedSSLError;
import static com.microsoft.playwright.Utils.mapOf;
import static java.util.Arrays.asList;
import static org.junit.jupiter.api.Assertions.*;
public class TestBrowserContextExposeFunction {
private static Playwright playwright;
private static Server server;
private static Server httpsServer;
private static BrowserType browserType;
private static Browser browser;
private static boolean isChromium;
private static boolean isWebKit;
private static boolean headful;
private BrowserContext context;
private Page page;
@BeforeAll
static void launchBrowser() {
playwright = Playwright.create();
BrowserType.LaunchOptions options = new BrowserType.LaunchOptions();
browserType = playwright.chromium();
browser = browserType.launch(options);
isChromium = true;
isWebKit = false;
headful = false;
}
@BeforeAll
static void startServer() throws IOException {
server = Server.createHttp(8907);
httpsServer = Server.createHttps(8908);
}
@AfterAll
static void stopServer() throws IOException {
browser.close();
server.stop();
server = null;
}
@BeforeEach
void setUp() {
server.reset();
context = browser.newContext();
page = context.newPage();
}
@AfterEach
void tearDown() {
context.close();
context = null;
page = null;
}
@Test
void exposeBindingShouldWork() {
Page.Binding.Source[] bindingSource = {null};
context.exposeBinding("add", (source, args) -> {
bindingSource[0] = source;
return (Integer) args[0] + (Integer) args[1];
});
Page page = context.newPage();
Object result = page.evaluate("add(5, 6)");
assertEquals(context, bindingSource[0].context());
assertEquals(page, bindingSource[0].page());
assertEquals(page.mainFrame(), bindingSource[0].frame());
assertEquals(11, result);
}
@Test
void shouldWork() {
context.exposeFunction("add", args -> (Integer) args[0] + (Integer) args[1]);
Page page = context.newPage();
page.exposeFunction("mul", args -> (Integer) args[0] * (Integer) args[1]);
context.exposeFunction("sub", args -> (Integer) args[0] - (Integer) args[1]);
context.exposeBinding("addHandle", (source, args) -> source.frame().evaluateHandle("([a, b]) => a + b", args));
Object result = page.evaluate("async () => ({ mul: await mul(9, 4), add: await add(9, 4), sub: await sub(9, 4), addHandle: await addHandle(5, 6) })");
assertEquals(mapOf("mul", 36, "add", 13, "sub", 5, "addHandle", 11), result);
}
@Test
void shouldThrowForDuplicateRegistrations() {
context.exposeFunction("foo", args -> null);
context.exposeFunction("bar", args -> null);
try {
context.exposeFunction("foo", args -> null);
fail("did not throw");
} catch (RuntimeException e) {
assertTrue(e.getMessage().contains("Function \"foo\" has been already registered"));
}
Page page = context.newPage();
try {
page.exposeFunction("foo", args -> null);
fail("did not throw");
} catch (RuntimeException e) {
assertTrue(e.getMessage().contains("Function \"foo\" has been already registered in the browser context"));
}
page.exposeFunction("baz", args -> null);
try {
context.exposeFunction("baz", args -> null);
fail("did not throw");
} catch (RuntimeException e) {
System.out.println(e);
assertTrue(e.getMessage().contains("Function \"baz\" has been already registered in one of the pages"));
}
}
@Test
void shouldBeCallableFromInsideAddInitScript() {
List<Object> actualArgs = new ArrayList<>();
context.exposeFunction("woof", args -> actualArgs.add(args[0]));
context.addInitScript("window['woof']('context')");
Page page = context.newPage();
page.addInitScript("window['woof']('page')");
actualArgs.clear();
page.reload();
assertEquals(asList("context", "page"), actualArgs);
}
@Test
void exposeBindingHandleShouldWork() {
JSHandle[] target = { null };
context.exposeBinding("logme", (source, args) -> {
target[0] = (JSHandle) args[0];
return 17;
}, new BrowserContext.ExposeBindingOptions().withHandle(true));
Page page = context.newPage();
Object result = page.evaluate("async function() {\n" +
" return window['logme']({ foo: 42 });\n" +
"}");
assertNotNull(target[0]);
assertEquals(42, target[0].evaluate("x => x.foo"));
assertEquals(17, result);
}
}