From c134a6355fc2d9d4ec117c78399fec2c074968cb Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 26 Oct 2020 20:10:11 -0700 Subject: [PATCH] feat: implement remaining Response methods (#46) --- .../playwright/tools/ApiGenerator.java | 2 + .../com/microsoft/playwright/tools/Types.java | 1 + .../com/microsoft/playwright/Response.java | 3 +- .../playwright/impl/ResponseImpl.java | 28 ++-- .../playwright/impl/Serialization.java | 3 +- .../java/com/microsoft/playwright/Server.java | 26 +++- .../playwright/TestNetworkResponse.java | 135 ++++++++++++++++++ 7 files changed, 177 insertions(+), 21 deletions(-) create mode 100644 playwright/src/test/java/com/microsoft/playwright/TestNetworkResponse.java diff --git a/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java b/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java index 0471b985..62ad9851 100644 --- a/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java +++ b/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java @@ -243,6 +243,8 @@ class Method extends Element { "void route(Pattern url, BiConsumer handler);", "void route(Predicate url, BiConsumer handler);", }); + // There is no standard JSON type in Java. + customSignature.put("Response.json", new String[0]); customSignature.put("Page.frame", new String[]{ "Frame frameByName(String name);", "Frame frameByUrl(String glob);", diff --git a/api-generator/src/main/java/com/microsoft/playwright/tools/Types.java b/api-generator/src/main/java/com/microsoft/playwright/tools/Types.java index e1cc2b25..a70ba0fb 100644 --- a/api-generator/src/main/java/com/microsoft/playwright/tools/Types.java +++ b/api-generator/src/main/java/com/microsoft/playwright/tools/Types.java @@ -273,6 +273,7 @@ class Types { add("ElementHandle.screenshot", "Promise", "byte[]"); add("Request.postDataBuffer", "null|Buffer", "byte[]"); add("Response.body", "Promise", "byte[]"); + add("Response.finished", "Promise", "String"); add("ChromiumBrowser.stopTracing", "Promise", "byte[]"); // JSON type diff --git a/playwright/src/main/java/com/microsoft/playwright/Response.java b/playwright/src/main/java/com/microsoft/playwright/Response.java index 33d8b1a2..2b1d101b 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Response.java +++ b/playwright/src/main/java/com/microsoft/playwright/Response.java @@ -20,10 +20,9 @@ import java.util.*; public interface Response { byte[] body(); - Error finished(); + String finished(); Frame frame(); Map headers(); - Object json(); boolean ok(); Request request(); int status(); diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/ResponseImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/ResponseImpl.java index 5d3f04d1..f5f0020d 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/ResponseImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/ResponseImpl.java @@ -16,20 +16,27 @@ package com.microsoft.playwright.impl; -import com.google.gson.Gson; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.microsoft.playwright.Frame; import com.microsoft.playwright.Request; import com.microsoft.playwright.Response; +import java.nio.charset.StandardCharsets; import java.util.Base64; +import java.util.HashMap; import java.util.Map; -import static com.microsoft.playwright.impl.Serialization.deserialize; - public class ResponseImpl extends ChannelOwner implements Response { + private final Map headers = new HashMap<>(); + 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()); + } } @Override @@ -39,7 +46,11 @@ public class ResponseImpl extends ChannelOwner implements Response { } @Override - public Error finished() { + public String finished() { + JsonObject json = sendMessage("finished").getAsJsonObject(); + if (json.has("error")) { + return json.get("error").getAsString(); + } return null; } @@ -50,12 +61,7 @@ public class ResponseImpl extends ChannelOwner implements Response { @Override public Map headers() { - return null; - } - - @Override - public Object json() { - return null; + return headers; } @Override @@ -80,7 +86,7 @@ public class ResponseImpl extends ChannelOwner implements Response { @Override public String text() { - return null; + return new String(body(), StandardCharsets.UTF_8); } @Override diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/Serialization.java b/playwright/src/main/java/com/microsoft/playwright/impl/Serialization.java index cbbf8450..1a734e98 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/Serialization.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/Serialization.java @@ -25,6 +25,7 @@ import com.microsoft.playwright.*; import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.lang.reflect.Array; +import java.nio.charset.StandardCharsets; import java.util.*; import java.util.stream.Collectors; import java.util.stream.StreamSupport; @@ -38,7 +39,7 @@ class Serialization { ByteArrayOutputStream out = new ByteArrayOutputStream(); e.printStackTrace(new PrintStream(out)); - result.error.stack = new String(out.toByteArray()); + result.error.stack = new String(out.toByteArray(), StandardCharsets.UTF_8); return result; } diff --git a/playwright/src/test/java/com/microsoft/playwright/Server.java b/playwright/src/test/java/com/microsoft/playwright/Server.java index c5272625..641f8a29 100644 --- a/playwright/src/test/java/com/microsoft/playwright/Server.java +++ b/playwright/src/test/java/com/microsoft/playwright/Server.java @@ -24,8 +24,7 @@ import java.nio.file.FileSystems; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Future; - -import static java.util.Collections.singletonList; +import java.util.zip.GZIPOutputStream; public class Server implements HttpHandler { private final HttpServer server; @@ -40,6 +39,7 @@ public class Server implements HttpHandler { private final Map auths = Collections.synchronizedMap(new HashMap<>()); private final Map csp = Collections.synchronizedMap(new HashMap<>()); private final Map routes = Collections.synchronizedMap(new HashMap<>()); + private final Set gzipRoutes = Collections.synchronizedSet(new HashSet<>()); private static class Auth { public final String user; @@ -93,6 +93,10 @@ public class Server implements HttpHandler { this.csp.put(path, csp); } + void enableGzip(String path) { + gzipRoutes.add(path); + } + static class Request { public final String method; // TODO: make a copy to ensure thread safety? @@ -135,6 +139,7 @@ public class Server implements HttpHandler { auths.clear(); csp.clear(); routes.clear(); + gzipRoutes.clear(); } @Override @@ -154,7 +159,7 @@ public class Server implements HttpHandler { } } if (!authorized) { - exchange.getResponseHeaders().put("WWW-Authenticate", Arrays.asList("Basic realm=\"Secure Area\"")); + exchange.getResponseHeaders().add("WWW-Authenticate", "Basic realm=\"Secure Area\""); exchange.sendResponseHeaders(401, 0); try (Writer writer = new OutputStreamWriter(exchange.getResponseBody())) { writer.write("HTTP Error 401 Unauthorized: Access is denied"); @@ -180,20 +185,27 @@ public class Server implements HttpHandler { } if (csp.containsKey(path)) { - exchange.getResponseHeaders().put("Content-Security-Policy", singletonList(csp.get(path))); + exchange.getResponseHeaders().add("Content-Security-Policy", csp.get(path)); } File file = new File(resourcesDir, path.substring(1)); - exchange.getResponseHeaders().put("Content-Type", singletonList(mimeType(file))); + exchange.getResponseHeaders().add("Content-Type", mimeType(file)); + OutputStream output = exchange.getResponseBody(); + if (gzipRoutes.contains(path)) { + exchange.getResponseHeaders().add("Content-Encoding", "gzip"); + } try (FileInputStream input = new FileInputStream(file)) { exchange.sendResponseHeaders(200, 0); - copy(input, exchange.getResponseBody()); + if (gzipRoutes.contains(path)) { + output = new GZIPOutputStream(output); + } + copy(input, output); } catch (IOException e) { exchange.sendResponseHeaders(404, 0); try (Writer writer = new OutputStreamWriter(exchange.getResponseBody())) { writer.write("File not found: " + file.getCanonicalPath()); } } - exchange.getResponseBody().close(); + output.close(); } private static void copy(InputStream in, OutputStream out) throws IOException { diff --git a/playwright/src/test/java/com/microsoft/playwright/TestNetworkResponse.java b/playwright/src/test/java/com/microsoft/playwright/TestNetworkResponse.java new file mode 100644 index 00000000..fb1fe32c --- /dev/null +++ b/playwright/src/test/java/com/microsoft/playwright/TestNetworkResponse.java @@ -0,0 +1,135 @@ +/* + * 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.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.concurrent.Future; + +import static com.microsoft.playwright.Page.EventType.REQUESTFINISHED; +import static com.microsoft.playwright.Page.EventType.RESPONSE; +import static org.junit.jupiter.api.Assertions.*; + +public class TestNetworkResponse extends TestBase { + @Test + void shouldWork() { + server.setRoute("/empty.html", exchange -> { + exchange.getResponseHeaders().add("foo", "bar"); + exchange.getResponseHeaders().add("BaZ", "bAz"); + exchange.sendResponseHeaders(200, 0); + exchange.getResponseBody().close(); + }); + Response response = page.navigate(server.EMPTY_PAGE); + assertEquals("bar", response.headers().get("foo")); + assertEquals("bAz", response.headers().get("baz")); + assertNull(response.headers().get("BaZ")); + } + + @Test + void shouldReturnText() { + Response response = page.navigate(server.PREFIX + "/simple.json"); + assertEquals("{\"foo\": \"bar\"}\n", response.text()); + } + + @Test + void shouldReturnUncompressedText() { + server.enableGzip("/simple.json"); + Response response = page.navigate(server.PREFIX + "/simple.json"); + assertEquals("gzip", response.headers().get("content-encoding")); + assertEquals("{\"foo\": \"bar\"}\n", response.text()); + } + + @Test + void shouldThrowWhenRequestingBodyOfRedirectedResponse() { + server.setRedirect("/foo.html", "/empty.html"); + Response response = page.navigate(server.PREFIX + "/foo.html"); + Request redirectedFrom = response.request().redirectedFrom(); + assertNotNull(redirectedFrom); + Response redirected = redirectedFrom.response(); + assertEquals(302, redirected.status()); + try { + redirected.text(); + fail("did not throw"); + } catch (PlaywrightException e) { + assertTrue(e.getMessage().contains("Response body is unavailable for redirect responses")); + } + } + + @Test + void shouldWaitUntilResponseCompletes() { + page.navigate(server.EMPTY_PAGE); + server.setRoute("/get", exchange -> { + // In Firefox, |fetch| will be hanging until it receives |Content-Type| header + // from server. + exchange.getResponseHeaders().add("Content-Type", "text/plain; charset=utf-8"); + exchange.sendResponseHeaders(200, 0); + try (OutputStreamWriter writer = new OutputStreamWriter(exchange.getResponseBody())) { + writer.write("hello "); + writer.flush(); + writer.write("wor"); + writer.flush(); + writer.write("ld!"); + } + }); + // Setup page to trap response. + boolean[] requestFinished = {false}; + page.addListener(REQUESTFINISHED, event -> { + requestFinished[0] |= ((Request) event.data()).url().contains("/get"); + }); + // send request and wait for server response + Deferred> responseEvent = page.waitForEvent(RESPONSE); + Future request = server.waitForRequest("/get"); + page.evaluate("() => fetch('./get', { method: 'GET'})"); + assertNotNull(responseEvent.get()); + Response pageResponse = (Response) responseEvent.get().data(); + assertEquals(200, pageResponse.status()); + assertEquals(false, requestFinished[0]); + assertEquals("hello world!", pageResponse.text()); + } + + void shouldReturnJson() { + // Not exposed in Java. + } + @Test + void shouldReturnBody() throws IOException { + Response response = page.navigate(server.PREFIX + "/pptr.png"); + byte[] expected = Files.readAllBytes(new File("src/test/resources/pptr.png").toPath()); + assertTrue(Arrays.equals(expected, response.body())); + } + + @Test + void shouldReturnBodyWithCompression() throws IOException { + server.enableGzip("/pptr.png"); + Response response = page.navigate(server.PREFIX + "/pptr.png"); + byte[] expected = Files.readAllBytes(new File("src/test/resources/pptr.png").toPath()); + assertTrue(Arrays.equals(expected, response.body())); + } + + @Test + void shouldReturnStatusText() { + server.setRoute("/cool", exchange -> { + exchange.sendResponseHeaders(200, 0); + exchange.getResponseBody().close(); + }); + Response response = page.navigate(server.PREFIX + "/cool"); + assertEquals("OK", response.statusText()); + } +}