1
0
mirror of synced 2026-05-22 18:53:15 +00:00

feat: roll driver, headerValue(s), wheel (#609)

This commit is contained in:
Yury Semikhatsky
2021-09-17 16:20:14 -07:00
committed by GitHub
parent 958813201a
commit 16cc466622
17 changed files with 384 additions and 32 deletions
+2 -2
View File
@@ -11,9 +11,9 @@ Playwright is a Java library to automate [Chromium](https://www.chromium.org/Hom
| | Linux | macOS | Windows |
| :--- | :---: | :---: | :---: |
| Chromium <!-- GEN:chromium-version -->95.0.4630.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Chromium <!-- GEN:chromium-version -->96.0.4641.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| WebKit <!-- GEN:webkit-version -->15.0<!-- GEN:stop --> | ✅ | ✅ | ✅ |
| Firefox <!-- GEN:firefox-version -->91.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| Firefox <!-- GEN:firefox-version -->92.0<!-- GEN:stop --> | :white_check_mark: | :white_check_mark: | :white_check_mark: |
Headless execution is supported for all the browsers on all platforms. Check out [system requirements](https://playwright.dev/java/docs/next/intro/#system-requirements) for details.
@@ -212,5 +212,15 @@ public interface Mouse {
* Dispatches a {@code mouseup} event.
*/
void up(UpOptions options);
/**
* Dispatches a {@code wheel} event.
*
* <p> <strong>NOTE:</strong> Wheel events may cause scrolling if they are not handled, and this method does not wait for the scrolling to finish
* before returning.
*
* @param deltaX Pixels to scroll horizontally.
* @param deltaY Pixels to scroll vertically.
*/
void wheel(double deltaX, double deltaY);
}
@@ -64,10 +64,16 @@ public interface Request {
Map<String, String> headers();
/**
* An array with all the request HTTP headers associated with this request. Unlike {@link Request#allHeaders
* Request.allHeaders()}, header names are not lower-cased. Headers with multiple entries, such as {@code Set-Cookie}, appear in
* Request.allHeaders()}, header names are NOT lower-cased. Headers with multiple entries, such as {@code Set-Cookie}, appear in
* the array multiple times.
*/
List<HttpHeader> headersArray();
/**
* Returns the value of the header matching the name. The name is case insensitive.
*
* @param name Name of the header.
*/
String headerValue(String name);
/**
* Whether this request is driving frame's navigation.
*/
@@ -32,7 +32,7 @@ public interface Response {
*/
byte[] body();
/**
* Waits for this response to finish, returns failure error if request failed.
* Waits for this response to finish, returns always {@code null}.
*/
String finished();
/**
@@ -46,10 +46,24 @@ public interface Response {
Map<String, String> headers();
/**
* An array with all the request HTTP headers associated with this response. Unlike {@link Response#allHeaders
* Response.allHeaders()}, header names are not lower-cased. Headers with multiple entries, such as {@code Set-Cookie}, appear in
* Response.allHeaders()}, header names are NOT lower-cased. Headers with multiple entries, such as {@code Set-Cookie}, appear in
* the array multiple times.
*/
List<HttpHeader> headersArray();
/**
* Returns the value of the header matching the name. The name is case insensitive. If multiple headers have the same name
* (except {@code set-cookie}), they are returned as a list separated by {@code , }. For {@code set-cookie}, the {@code \n} separator is used. If
* no headers are found, {@code null} is returned.
*
* @param name Name of the header.
*/
String headerValue(String name);
/**
* Returns all values of the headers matching the name, for example {@code set-cookie}. The name is case insensitive.
*
* @param name Name of the header.
*/
List<String> headerValues(String name);
/**
* Contains a boolean stating whether the response was successful (status in the range 200-299) or not.
*/
@@ -305,6 +305,10 @@ public class Connection {
case "ElementHandle":
result = new ElementHandleImpl(parent, type, guid, initializer);
break;
case "FetchRequest":
// Create fake object as this API is experimental an only exposed in Node.js.
result = new ChannelOwner(parent, type, guid, initializer);
break;
case "Frame":
result = new FrameImpl(parent, type, guid, initializer);
break;
@@ -93,6 +93,16 @@ class MouseImpl implements Mouse {
page.withLogging("Mouse.up", () -> upImpl(options));
}
@Override
public void wheel(double deltaX, double deltaY) {
page.withLogging("Mouse.wheel", () -> {
JsonObject params = new JsonObject();
params.addProperty("deltaX", deltaX);
params.addProperty("deltaY", deltaY);
page.sendMessage("mouseWheel", params);
});
}
private void upImpl(UpOptions options) {
if (options == null) {
options = new UpOptions();
@@ -125,7 +125,14 @@ public class PageImpl extends ChannelOwner implements Page {
if (listeners.hasListeners(EventType.DIALOG)) {
listeners.notify(EventType.DIALOG, dialog);
} else {
dialog.dismiss();
if ("beforeunload".equals(dialog.type())) {
try {
dialog.accept();
} catch (PlaywrightException e) {
}
} else {
dialog.dismiss();
}
}
} else if ("worker".equals(event)) {
String guid = params.getAsJsonObject("worker").get("guid").getAsString();
@@ -0,0 +1,67 @@
/*
* 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.microsoft.playwright.options.HttpHeader;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
class RawHeaders {
private final List<HttpHeader> headersArray;
private final Map<String, List<String>> headersMap = new LinkedHashMap<>();
RawHeaders(List<HttpHeader> headers) {
headersArray = headers;
for (HttpHeader h: headers) {
String name = h.name.toLowerCase();
List<String> values = headersMap.get(name);
if (values == null) {
values = new ArrayList<>();
headersMap.put(name, values);
}
values.add(h.value);
}
}
String get(String name) {
List<String> values = getAll(name);
if (values == null) {
return null;
}
return String.join("set-cookie".equals(name.toLowerCase()) ? "\n" : ", ", values);
}
List<String> getAll(String name) {
return headersMap.get(name.toLowerCase());
}
Map<String, String> headers() {
Map<String, String> result = new LinkedHashMap<>();
for (String name: headersMap.keySet()) {
result.put(name, get(name));
}
return result;
}
List<HttpHeader> headersArray() {
return headersArray;
}
}
@@ -37,8 +37,8 @@ public class RequestImpl extends ChannelOwner implements Request {
private final byte[] postData;
private RequestImpl redirectedFrom;
private RequestImpl redirectedTo;
private final List<HttpHeader> headers;
private List<HttpHeader> rawHeaders;
private final RawHeaders headers;
private RawHeaders rawHeaders;
String failure;
Timing timing;
boolean didFailOrFinish;
@@ -50,7 +50,7 @@ public class RequestImpl extends ChannelOwner implements Request {
redirectedFrom = connection.getExistingObject(initializer.getAsJsonObject("redirectedFrom").get("guid").getAsString());
redirectedFrom.redirectedTo = this;
}
headers = asList(gson().fromJson(initializer.getAsJsonArray("headers"), HttpHeader[].class));
headers = new RawHeaders(asList(gson().fromJson(initializer.getAsJsonArray("headers"), HttpHeader[].class)));
if (initializer.has("postData")) {
postData = Base64.getDecoder().decode(initializer.get("postData").getAsString());
} else {
@@ -60,7 +60,7 @@ public class RequestImpl extends ChannelOwner implements Request {
@Override
public Map<String, String> allHeaders() {
return withLogging("Request.allHeaders", () -> toHeadersMap(getRawHeaders()));
return withLogging("Request.allHeaders", () -> getRawHeaders().headers());
}
@Override
@@ -75,12 +75,17 @@ public class RequestImpl extends ChannelOwner implements Request {
@Override
public Map<String, String> headers() {
return toHeadersMap(headers);
return headers.headers();
}
@Override
public List<HttpHeader> headersArray() {
return withLogging("Request.headersArray", () -> getRawHeaders());
return withLogging("Request.headersArray", () -> getRawHeaders().headersArray());
}
@Override
public String headerValue(String name) {
return withLogging("Request.headerValue", () -> getRawHeaders().get(name));
}
@Override
@@ -158,7 +163,7 @@ public class RequestImpl extends ChannelOwner implements Request {
return redirectedTo != null ? redirectedTo.finalRequest() : this;
}
private List<HttpHeader> getRawHeaders() {
private RawHeaders getRawHeaders() {
if (rawHeaders != null) {
return rawHeaders;
}
@@ -173,7 +178,7 @@ public class RequestImpl extends ChannelOwner implements Request {
});
// The field may have been initialized in a nested call but it is ok.
rawHeaders = asList(gson().fromJson(rawHeadersJson, HttpHeader[].class));
rawHeaders = new RawHeaders(asList(gson().fromJson(rawHeadersJson, HttpHeader[].class)));
return rawHeaders;
}
}
@@ -16,7 +16,6 @@
package com.microsoft.playwright.impl;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.playwright.Frame;
import com.microsoft.playwright.Response;
@@ -26,32 +25,29 @@ import com.microsoft.playwright.options.ServerAddr;
import com.microsoft.playwright.options.Timing;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import static com.microsoft.playwright.impl.Serialization.gson;
import static com.microsoft.playwright.impl.Utils.toHeadersMap;
import static java.util.Arrays.asList;
public class ResponseImpl extends ChannelOwner implements Response {
private final Map<String, String> headers = new HashMap<>();
private List<HttpHeader> rawHeaders;
private final RawHeaders headers;
private RawHeaders rawHeaders;
private final RequestImpl request;
ResponseImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
super(parent, type, guid, initializer);
for (JsonElement e : initializer.getAsJsonArray("headers")) {
JsonObject item = e.getAsJsonObject();
headers.put(item.get("name").getAsString().toLowerCase(), item.get("value").getAsString());
}
headers = new RawHeaders(asList(gson().fromJson(initializer.getAsJsonArray("headers"), HttpHeader[].class)));
request = connection.getExistingObject(initializer.getAsJsonObject("request").get("guid").getAsString());
request.timing = gson().fromJson(initializer.get("timing"), Timing.class);
}
@Override
public Map<String, String> allHeaders() {
return withLogging("Response.allHeaders", () -> toHeadersMap(getRawHeaders()));
return withLogging("Response.allHeaders", () -> getRawHeaders().headers());
}
@Override
@@ -89,12 +85,22 @@ public class ResponseImpl extends ChannelOwner implements Response {
@Override
public Map<String, String> headers() {
return headers;
return headers.headers();
}
@Override
public List<HttpHeader> headersArray() {
return withLogging("Response.headersArray", () -> getRawHeaders());
return withLogging("Response.headersArray", () -> getRawHeaders().headersArray());
}
@Override
public String headerValue(String name) {
return getRawHeaders().get(name);
}
@Override
public List<String> headerValues(String name) {
return getRawHeaders().getAll(name);
}
@Override
@@ -149,10 +155,10 @@ public class ResponseImpl extends ChannelOwner implements Response {
return initializer.get("url").getAsString();
}
private List<HttpHeader> getRawHeaders() {
private RawHeaders getRawHeaders() {
if (rawHeaders == null) {
JsonObject json = sendMessage("rawResponseHeaders").getAsJsonObject();
rawHeaders = asList(gson().fromJson(json.getAsJsonArray("headers"), HttpHeader[].class));
rawHeaders = new RawHeaders(asList(gson().fromJson(json.getAsJsonArray("headers"), HttpHeader[].class)));
}
return rawHeaders;
}
@@ -0,0 +1,30 @@
/*
* 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.Test;
public class TestBeforeunload extends TestBase {
@Test
void shouldBeAbleToNavigateAwayFromPageWithBeforeunload() {
page.navigate(server.PREFIX + "/beforeunload.html");
// We have to interact with a page so that "beforeunload" handlers
// fire.
page.click("body");
page.navigate(server.EMPTY_PAGE);
}
}
@@ -78,6 +78,8 @@ public class TestPageNetworkRequest extends TestBase {
expectedHeaders.sort(comparator);
headers.sort(comparator);
assertEquals(new Gson().toJsonTree(expectedHeaders), new Gson().toJsonTree(headers));
assertEquals("value-a, value-a-1, value-a-2", request.headerValue("header-a"));
assertEquals(null, request.headerValue("not-there"));
}
@Test
@@ -0,0 +1,48 @@
/*
* 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 com.microsoft.playwright.options.HttpHeader;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.stream.Collectors;
import static java.util.Arrays.asList;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class TestPageNetworkResponse extends TestBase {
@Test
void shouldReportMultipleSetCookieHeaders() {
server.setRoute("/headers", exchange -> {
exchange.getResponseHeaders().add("Set-Cookie", "a=b");
exchange.getResponseHeaders().add("Set-Cookie", "c=d");
exchange.sendResponseHeaders(200, 0);
exchange.getResponseBody().close();
});
page.navigate(server.EMPTY_PAGE);
Response response = page.waitForResponse("**/*", () -> page.evaluate("fetch('/headers')"));
List<HttpHeader> headers = response.headersArray();
List<String> cookies = headers.stream().filter(
httpHeader -> "set-cookie".equals(httpHeader.name.toLowerCase())).map(h -> h.value).collect(Collectors.toList());
assertEquals(asList("a=b", "c=d"), cookies);
assertEquals(null, response.headerValue("not-there"));
assertEquals("a=b\nc=d", response.headerValue("set-cookie"));
assertEquals(asList("a=b", "c=d"), response.headerValues("set-cookie"));
}
}
@@ -75,6 +75,7 @@ public class TestPageNetworkSizes extends TestBase {
}
@Test
@Disabled("responseBodySize == 5")
void shouldSetBodySizeTo0WhenThereWasNoResponseBody() {
Response response = page.navigate(server.EMPTY_PAGE);
Sizes sizes = response.request().sizes();
@@ -189,7 +189,9 @@ public class TestPageRoute extends TestBase {
void shouldWorkWithCustomRefererHeaders() {
page.setExtraHTTPHeaders(mapOf("referer", server.EMPTY_PAGE));
page.route("**/*", route -> {
assertEquals(server.EMPTY_PAGE, route.request().headers().get("referer"));
String referer = route.request().headers().get("referer");
assertNotNull(referer);
assertTrue(referer.contains(server.EMPTY_PAGE), referer);
route.resume();
});
Response response = page.navigate(server.EMPTY_PAGE);
@@ -0,0 +1,140 @@
/*
* 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.Test;
import java.util.Map;
import static com.microsoft.playwright.Utils.mapOf;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class TestWheel extends TestBase {
@Test
void shouldDispatchWheelEvents() {
page.setContent("<div style='width: 5000px; height: 5000px;'></div>");
page.mouse().move(50, 60);
listenForWheelEvents(page, "div");
page.mouse().wheel(0, 100);
Map<String, Object> expected = mapOf(
"deltaX", 0,
"deltaY", 100,
"clientX", 50,
"clientY", 60,
"deltaMode", 0,
"ctrlKey", false,
"shiftKey", false,
"altKey", false,
"metaKey", false);
assertEquals(expected, page.evaluate("window.lastEvent"));
page.waitForFunction("window.scrollY === 100");
}
@Test
void shouldScrollWhenNobodyIsListening() {
page.navigate(server.PREFIX + "/input/scrollable.html");
page.mouse().move(50, 60);
page.mouse().wheel(0, 100);
page.waitForFunction("window.scrollY === 100");
}
@Test
void shouldSetTheModifiers() {
page.setContent("<div style='width: 5000px; height: 5000px;'></div>");
page.mouse().move(50, 60);
listenForWheelEvents(page, "div");
page.keyboard().down("Shift");
page.mouse().wheel(0, 100);
Map<String, Object> expected = mapOf(
"deltaX", 0,
"deltaY", 100,
"clientX", 50,
"clientY", 60,
"deltaMode", 0,
"ctrlKey", false,
"shiftKey", true,
"altKey", false,
"metaKey", false);
assertEquals(expected, page.evaluate("window.lastEvent"));
}
@Test
void shouldScrollHorizontally() {
page.setContent("<div style='width: 5000px; height: 5000px;'></div>");
page.mouse().move(50, 60);
listenForWheelEvents(page, "div");
page.mouse().wheel(100, 0);
Map<String, Object> expected = mapOf(
"deltaX", 100,
"deltaY", 0,
"clientX", 50,
"clientY", 60,
"deltaMode", 0,
"ctrlKey", false,
"shiftKey", false,
"altKey", false,
"metaKey", false);
assertEquals(expected, page.evaluate("window.lastEvent"));
page.waitForFunction("window.scrollX === 100");
}
@Test
void shouldWorkWhenTheEventIsCanceled() {
page.setContent("<div style='width: 5000px; height: 5000px;'></div>");
page.mouse().move(50, 60);
listenForWheelEvents(page, "div");
page.evaluate("() => {\n" +
" document.querySelector('div').addEventListener('wheel', e => e.preventDefault());\n" +
" }");
page.mouse().wheel(0, 100);
Map<String, Object> expected = mapOf(
"deltaX", 0,
"deltaY", 100,
"clientX", 50,
"clientY", 60,
"deltaMode", 0,
"ctrlKey", false,
"shiftKey", false,
"altKey", false,
"metaKey", false);
assertEquals(expected, page.evaluate("window.lastEvent"));
// give the page a chacne to scroll
page.waitForTimeout(100);
// ensure that it did not.
assertEquals(0, page.evaluate("window.scrollY"));
}
private static void listenForWheelEvents(Page page, String selector) {
page.evaluate("selector => {\n" +
" document.querySelector(selector).addEventListener('wheel', e => {\n" +
" window['lastEvent'] = {\n" +
" deltaX: e.deltaX,\n" +
" deltaY: e.deltaY,\n" +
" clientX: e.clientX,\n" +
" clientY: e.clientY,\n" +
" deltaMode: e.deltaMode,\n" +
" ctrlKey: e.ctrlKey,\n" +
" shiftKey: e.shiftKey,\n" +
" altKey: e.altKey,\n" +
" metaKey: e.metaKey,\n" +
" };\n" +
" }, { passive: false });\n" +
" }", selector);
}
}
+1 -1
View File
@@ -1 +1 @@
1.15.0-next-1631203211000
1.15.0-1631797286000