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 6e4a4543..26fb946d 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 @@ -484,18 +484,23 @@ class Field extends Element { } void writeTo(List output, String offset, String access) { - if (jsonPath.contains("Frame.waitForNavigation.options.url") || - jsonPath.contains("Page.waitForNavigation.options.url")) { + if (asList("Frame.waitForNavigation.options.url", + "Page.waitForNavigation.options.url").contains(jsonPath)) { output.add(offset + "public String glob;"); output.add(offset + "public Pattern pattern;"); output.add(offset + "public Predicate predicate;"); return; } - if (jsonPath.contains("Frame.waitForFunction.options.polling") || - jsonPath.contains("Page.waitForFunction.options.polling")) { + if (asList("Frame.waitForFunction.options.polling", + "Page.waitForFunction.options.polling").contains(jsonPath)) { output.add(offset + "public Integer pollingInterval;"); return; } + if ("Route.fulfill.response.body".equals(jsonPath)) { + output.add(offset + "public String body;"); + output.add(offset + "public byte[] bodyBytes;"); + return; + } output.add(offset + access + type.toJava() + " " + name + ";"); } @@ -506,8 +511,8 @@ class Field extends Element { } void writeBuilderMethod(List output, String offset, String parentClass) { - if (jsonPath.contains("Frame.waitForNavigation.options.url") || - jsonPath.contains("Page.waitForNavigation.options.url")) { + if (asList("Frame.waitForNavigation.options.url", + "Page.waitForNavigation.options.url").contains(jsonPath)) { output.add(offset + "public WaitForNavigationOptions withUrl(String glob) {"); output.add(offset + " this.glob = glob;"); output.add(offset + " return this;"); @@ -522,8 +527,8 @@ class Field extends Element { output.add(offset + "}"); return; } - if (jsonPath.contains("Frame.waitForFunction.options.polling") || - jsonPath.contains("Page.waitForFunction.options.polling")) { + if (asList("Frame.waitForFunction.options.polling", + "Page.waitForFunction.options.polling").contains(jsonPath)) { output.add(offset + "public WaitForFunctionOptions withRequestAnimationFrame() {"); output.add(offset + " this.pollingInterval = null;"); output.add(offset + " return this;"); @@ -553,12 +558,18 @@ class Field extends Element { return; } - if (jsonPath.equals("Route.continue.overrides.postData")) { + if ("Route.continue.overrides.postData".equals(jsonPath)) { output.add(offset + "public ContinueOverrides withPostData(String postData) {"); output.add(offset + " this.postData = postData.getBytes(StandardCharsets.UTF_8);"); output.add(offset + " return this;"); output.add(offset + "}"); } + if ("Route.fulfill.response.body".equals(jsonPath)) { + output.add(offset + "public FulfillResponse withBody(byte[] body) {"); + output.add(offset + " this.bodyBytes = body;"); + output.add(offset + " return this;"); + output.add(offset + "}"); + } if (name.equals("httpCredentials")) { output.add(offset + "public " + parentClass + " with" + toTitle(name) + "(String username, String password) {"); output.add(offset + " this." + name + " = new " + type.toJava() + "(username, password);"); @@ -632,11 +643,13 @@ class Interface extends TypeDefinition { if (jsonName.equals("Route")) { output.add("import java.nio.charset.StandardCharsets;"); } - if (asList("Page", "Frame", "ElementHandle", "FileChooser", "ChromiumBrowser", "Route").contains(jsonName)) { + if (asList("Page", "Frame", "ElementHandle", "FileChooser", "ChromiumBrowser").contains(jsonName)) { output.add("import java.io.File;"); } - if (jsonName.equals("Download")) { + if ("Download".equals(jsonName)) { output.add("import java.io.InputStream;"); + } + if (asList("Download", "Route").contains(jsonName)) { output.add("import java.nio.file.Path;"); } output.add("import java.util.*;"); 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 d071f174..8a6aed18 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 @@ -102,7 +102,8 @@ class Types { add("Frame.addScriptTag.options.path", "string", "File"); add("Frame.addStyleTag.options.path", "string", "File"); add("ElementHandle.screenshot.options.path", "string", "File"); - add("Route.fulfill.response.path", "string", "File"); + add("Route.fulfill.response.path", "string", "Path"); + add("Route.fulfill.response.status", "number", "int"); add("ChromiumBrowser.startTracing.options.path", "string", "File"); // Route diff --git a/playwright/src/main/java/com/microsoft/playwright/Route.java b/playwright/src/main/java/com/microsoft/playwright/Route.java index 042fd3aa..dc679a2a 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Route.java +++ b/playwright/src/main/java/com/microsoft/playwright/Route.java @@ -17,7 +17,7 @@ package com.microsoft.playwright; import java.nio.charset.StandardCharsets; -import java.io.File; +import java.nio.file.Path; import java.util.*; public interface Route { @@ -44,13 +44,14 @@ public interface Route { } } class FulfillResponse { - public Integer status; + public int status; public Map headers; public String contentType; public String body; - public File path; + public byte[] bodyBytes; + public Path path; - public FulfillResponse withStatus(Integer status) { + public FulfillResponse withStatus(int status) { this.status = status; return this; } @@ -62,11 +63,15 @@ public interface Route { this.contentType = contentType; return this; } + public FulfillResponse withBody(byte[] body) { + this.bodyBytes = body; + return this; + } public FulfillResponse withBody(String body) { this.body = body; return this; } - public FulfillResponse withPath(File path) { + public FulfillResponse withPath(Path path) { this.path = path; return this; } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/RouteImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/RouteImpl.java index 8da911c8..79ece786 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/RouteImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/RouteImpl.java @@ -16,11 +16,18 @@ package com.microsoft.playwright.impl; +import com.google.gson.Gson; import com.google.gson.JsonObject; +import com.microsoft.playwright.PlaywrightException; import com.microsoft.playwright.Request; import com.microsoft.playwright.Route; +import java.io.IOException; +import java.nio.file.Files; import java.util.Base64; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; public class RouteImpl extends ChannelOwner implements Route { public RouteImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) { @@ -55,6 +62,53 @@ public class RouteImpl extends ChannelOwner implements Route { @Override public void fulfill(FulfillResponse response) { + if (response == null) { + response = new FulfillResponse(); + } + + int status = response.status == 0 ? 200 : response.status; + String body = ""; + boolean isBase64 = false; + int length = 0; + if (response.path != null) { + try { + byte[] buffer = Files.readAllBytes(response.path); + body = Base64.getEncoder().encodeToString(buffer); + isBase64 = true; + length = buffer.length; + } catch (IOException e) { + throw new PlaywrightException("Failed to read from file: " + response.path, e); + } + } else if (response.body != null) { + body = response.body; + isBase64 = false; + length = body.getBytes().length; + } else if (response.bodyBytes != null) { + body = Base64.getEncoder().encodeToString(response.bodyBytes); + isBase64 = true; + length = response.bodyBytes.length; + } + + Map headers = new LinkedHashMap<>(); + if (response.headers != null) { + for (Map.Entry h : response.headers.entrySet()) { + headers.put(h.getKey().toLowerCase(), h.getValue()); + } + } + if (response.contentType != null) { + headers.put("content-type", response.contentType); + } else if (response.path != null) { + headers.put("content-type", Utils.mimeType(response.path)); + } + if (length != 0 && !headers.containsKey("content-length")) { + headers.put("content-length", Integer.toString(length)); + } + JsonObject params = new JsonObject(); + params.addProperty("status", status); + params.add("headers", Serialization.toProtocol(headers)); + params.addProperty("isBase64", isBase64); + params.addProperty("body", body); + sendMessage("fulfill", params); } @Override diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/Utils.java b/playwright/src/main/java/com/microsoft/playwright/impl/Utils.java index da08d45e..7b104453 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/Utils.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/Utils.java @@ -26,6 +26,7 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.Path; import java.util.*; class Utils { @@ -99,25 +100,29 @@ class Utils { return tokens.toString(); } + static String mimeType(Path path) { + String mimeType; + try { + mimeType = Files.probeContentType(path); + } catch (IOException e) { + throw new PlaywrightException("Failed to determine mime type", e); + } + if (mimeType == null) { + mimeType = "application/octet-stream"; + } + return mimeType; + } + static FileChooser.FilePayload[] toFilePayloads(File[] files) { List payloads = new ArrayList<>(); for (File file : files) { - String mimeType; - try { - mimeType = Files.probeContentType(file.toPath()); - } catch (IOException e) { - throw new PlaywrightException("Failed to determine mime type", e); - } - if (mimeType == null) { - mimeType = "application/octet-stream"; - } byte[] buffer; try { buffer = Files.readAllBytes(file.toPath()); } catch (IOException e) { throw new PlaywrightException("Failed to read from file", e); } - payloads.add(new FileChooser.FilePayload(file.getName(), mimeType, buffer)); + payloads.add(new FileChooser.FilePayload(file.getName(), mimeType(file.toPath()), buffer)); } return payloads.toArray(new FileChooser.FilePayload[0]); } diff --git a/playwright/src/test/java/com/microsoft/playwright/Server.java b/playwright/src/test/java/com/microsoft/playwright/Server.java index 2fd7f122..ae46ca7d 100644 --- a/playwright/src/test/java/com/microsoft/playwright/Server.java +++ b/playwright/src/test/java/com/microsoft/playwright/Server.java @@ -190,6 +190,13 @@ public class Server implements HttpHandler { exchange.getResponseHeaders().add("Content-Security-Policy", csp.get(path)); } File file = new File(resourcesDir, path.substring(1)); + if (!file.exists()) { + exchange.sendResponseHeaders(404, 0); + try (Writer writer = new OutputStreamWriter(exchange.getResponseBody())) { + writer.write("File not found: " + file.getCanonicalPath()); + } + return; + } exchange.getResponseHeaders().add("Content-Type", mimeType(file)); OutputStream output = exchange.getResponseBody(); if (gzipRoutes.contains(path)) { @@ -202,10 +209,10 @@ public class Server implements HttpHandler { } copy(input, output); } catch (IOException e) { - exchange.sendResponseHeaders(404, 0); try (Writer writer = new OutputStreamWriter(exchange.getResponseBody())) { - writer.write("File not found: " + file.getCanonicalPath()); + writer.write("Exception: " + e); } + return; } output.close(); } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextRoute.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextRoute.java new file mode 100644 index 00000000..5e703a59 --- /dev/null +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextRoute.java @@ -0,0 +1,124 @@ +/* + * 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.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; + +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.Assertions.*; + +public class TestBrowserContextRoute extends TestBase { + + @Test + void shouldIntercept() { + BrowserContext context = browser.newContext(); + boolean[] intercepted = {false}; + Page page = context.newPage(); + context.route("**/empty.html", (route, req) -> { + intercepted[0] = true; + Request request = route.request(); + assertTrue(request.url().contains("empty.html")); + assertNotNull(request.headers().get("user-agent")); + assertEquals("GET", request.method()); + assertNull(request.postData()); + assertTrue(request.isNavigationRequest()); + assertEquals("document", request.resourceType()); + assertEquals(page.mainFrame(), request.frame()); + assertEquals("about:blank", request.frame().url()); + route.continue_(); + }); + Response response = page.navigate(server.EMPTY_PAGE); + assertTrue(response.ok()); + assertTrue(intercepted[0]); + context.close(); + } + + @Test + void shouldUnroute() { + BrowserContext context = browser.newContext(); + Page page = context.newPage(); + + List intercepted = new ArrayList<>(); + BiConsumer handler1 = (route, request) -> { + intercepted.add(1); + route.continue_(); + }; + context.route("**/empty.html", handler1); + context.route("**/empty.html", (route, request) -> { + intercepted.add(2); + route.continue_(); + }); + context.route("**/empty.html", (route, request) -> { + intercepted.add(3); + route.continue_(); + }); + context.route("**/*", (route, request) -> { + intercepted.add(4); + route.continue_(); + }); + page.navigate(server.EMPTY_PAGE); + assertEquals(asList(1), intercepted); + + intercepted.clear(); + context.unroute("**/empty.html", handler1); + page.navigate(server.EMPTY_PAGE); + assertEquals(asList(2), intercepted); + + intercepted.clear(); + context.unroute("**/empty.html"); + page.navigate(server.EMPTY_PAGE); + assertEquals(asList(4), intercepted); + + context.close(); + } + + @Test + void shouldYieldToPageRoute() { + BrowserContext context = browser.newContext(); + context.route("**/empty.html", (route, request) -> { + route.fulfill(new Route.FulfillResponse().withStatus(200).withBody("context")); + }); + Page page = context.newPage(); + page.route("**/empty.html", (route, request) -> { + route.fulfill(new Route.FulfillResponse().withStatus(200).withBody("page")); + }); + Response response = page.navigate(server.EMPTY_PAGE); + assertTrue(response.ok()); + assertEquals("page", response.text()); + context.close(); + } + + @Test + void shouldFallBackToContextRoute() { + BrowserContext context = browser.newContext(); + context.route("**/empty.html", (route, request) -> { + route.fulfill(new Route.FulfillResponse().withStatus(200).withBody("context")); + }); + Page page = context.newPage(); + page.route("**/non-empty.html", (route, request) -> { + route.fulfill(new Route.FulfillResponse().withStatus(200).withBody("page")); + }); + Response response = page.navigate(server.EMPTY_PAGE); + assertTrue(response.ok()); + assertEquals("context", response.text()); + context.close(); + } +} diff --git a/playwright/src/test/java/com/microsoft/playwright/TestPageRoute.java b/playwright/src/test/java/com/microsoft/playwright/TestPageRoute.java index 161f90d6..c038999b 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestPageRoute.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestPageRoute.java @@ -18,11 +18,18 @@ package com.microsoft.playwright; import org.junit.jupiter.api.Test; +import java.io.OutputStreamWriter; import java.util.*; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiConsumer; +import java.util.regex.Pattern; +import static com.microsoft.playwright.Page.EventType.REQUEST; +import static com.microsoft.playwright.Page.EventType.REQUESTFAILED; +import static com.microsoft.playwright.Utils.mapOf; +import static java.util.Arrays.asList; import static org.junit.jupiter.api.Assertions.*; public class TestPageRoute extends TestBase { @@ -70,17 +77,17 @@ public class TestPageRoute extends TestBase { route.continue_(); }); page.navigate(server.EMPTY_PAGE); - assertEquals(Arrays.asList(1), intercepted); + assertEquals(asList(1), intercepted); intercepted.clear(); page.unroute("**/empty.html", handler1); page.navigate(server.EMPTY_PAGE); - assertEquals(Arrays.asList(2), intercepted); + assertEquals(asList(2), intercepted); intercepted.clear(); page.unroute("**/empty.html"); page.navigate(server.EMPTY_PAGE); - assertEquals(Arrays.asList(4), intercepted); + assertEquals(asList(4), intercepted); } @Test @@ -134,4 +141,486 @@ public class TestPageRoute extends TestBase { assertTrue(requests.get(1).headers().containsKey("referer")); assertTrue(requests.get(1).headers().get("referer").contains("/one-style.html")); } + + @Test + void shouldProperlyReturnNavigationResponseWhenURLHasCookies() { + // Setup cookie. + page.navigate(server.EMPTY_PAGE); + context.addCookies(asList(new BrowserContext.AddCookie() + .withUrl(server.EMPTY_PAGE).withName("foo").withValue("bar"))); + // Setup request interception. + page.route("**/*", (route, request) -> route.continue_()); + Response response = page.reload(); + assertEquals(200, response.status()); + } + + @Test + void shouldShowCustomHTTPHeaders() { + page.setExtraHTTPHeaders(mapOf("foo", "bar")); + page.route("**/*", (route, request) -> { + assertEquals("bar", request.headers().get("foo")); + route.continue_(); + }); + Response response = page.navigate(server.EMPTY_PAGE); + assertTrue(response.ok()); + } + + // @see https://github.com/GoogleChrome/puppeteer/issues/4337 + @Test + void shouldWorkWithRedirectInsideSyncXHR() { + page.navigate(server.EMPTY_PAGE); + server.setRedirect("/logo.png", "/pptr.png"); + page.route("**/*", (route, request) -> route.continue_()); + Object status = page.evaluate("async () => {\n" + + " const request = new XMLHttpRequest();\n" + + " request.open('GET', '/logo.png', false); // `false` makes the request synchronous\n" + + " request.send(null);\n" + + " return request.status;\n" + + "}"); + assertEquals(200, status); + } + + @Test + void shouldWorkWithCustomRefererHeaders() { + page.setExtraHTTPHeaders(mapOf("referer", server.EMPTY_PAGE)); + page.route("**/*", (route, request) -> { + assertEquals(server.EMPTY_PAGE, route.request().headers().get("referer")); + route.continue_(); + }); + Response response = page.navigate(server.EMPTY_PAGE); + assertTrue(response.ok()); + } + + @Test + void shouldBeAbortable() { + page.route(Pattern.compile(".*\\.css$"), (route, request) -> route.abort()); + boolean[] failed = {false}; + page.addListener(REQUESTFAILED, event -> { + Request request = (Request) event.data(); + if (request.url().contains(".css")) + failed[0] = true; + }); + Response response = page.navigate(server.PREFIX + "/one-style.html"); + assertTrue(response.ok()); + assertNull(response.request().failure()); + assertTrue(failed[0]); + } + + @Test + void shouldBeAbortableWithCustomErrorCodes() { + page.route("**/*", (route, request) -> route.abort("internetdisconnected")); + Request[] failedRequest = {null}; + page.addListener(REQUESTFAILED, event -> failedRequest[0] = (Request) event.data()); + try { + page.navigate(server.EMPTY_PAGE); + } catch (PlaywrightException e) { + } + assertNotNull(failedRequest[0]); + if (isWebKit) + assertEquals("Request intercepted", failedRequest[0].failure().errorText()); + else if (isFirefox) + assertEquals("NS_ERROR_OFFLINE", failedRequest[0].failure().errorText()); + else + assertEquals("net::ERR_INTERNET_DISCONNECTED", failedRequest[0].failure().errorText()); + } + + @Test + void shouldSendReferer() throws ExecutionException, InterruptedException { + page.setExtraHTTPHeaders(mapOf("referer", "http://google.com/")); + page.route("**/*", (route, request) -> route.continue_()); + Future request = server.waitForRequest("/grid.html"); + page.navigate(server.PREFIX + "/grid.html"); + assertEquals(asList("http://google.com/"), request.get().headers.get("referer")); + } + + @Test + void shouldFailNavigationWhenAbortingMainResource() { + page.route("**/*", (route, request) -> route.abort()); + try { + page.navigate(server.EMPTY_PAGE); + fail("did not throw"); + } catch (PlaywrightException e) { + if (isWebKit) + assertTrue(e.getMessage().contains("Request intercepted")); + else if (isFirefox) + assertTrue(e.getMessage().contains("NS_ERROR_FAILURE")); + else + assertTrue(e.getMessage().contains("net::ERR_FAILED")); + } + } + + + @Test + void shouldNotWorkWithRedirects() { + List intercepted = new ArrayList<>(); + page.route("**/*", (route, request) -> { + route.continue_(); + intercepted.add(route.request()); + }); + server.setRedirect("/non-existing-page.html", "/non-existing-page-2.html"); + server.setRedirect("/non-existing-page-2.html", "/non-existing-page-3.html"); + server.setRedirect("/non-existing-page-3.html", "/non-existing-page-4.html"); + server.setRedirect("/non-existing-page-4.html", "/empty.html"); + + Response response = page.navigate(server.PREFIX + "/non-existing-page.html"); + assertEquals(200, response.status()); + assertTrue(response.url().contains("empty.html")); + + assertEquals(1, intercepted.size()); + assertEquals("document", intercepted.get(0).resourceType()); + assertTrue(intercepted.get(0).isNavigationRequest()); + assertTrue(intercepted.get(0).url().contains("/non-existing-page.html")); + + List chain = new ArrayList<>(); + for (Request r = response.request(); r != null; r = r.redirectedFrom()) { + chain.add(r); + assertTrue(r.isNavigationRequest()); + } + assertEquals(5, chain.size()); + assertTrue(chain.get(0).url().contains("/empty.html")); + assertTrue(chain.get(1).url().contains("/non-existing-page-4.html")); + assertTrue(chain.get(2).url().contains("/non-existing-page-3.html")); + assertTrue(chain.get(3).url().contains("/non-existing-page-2.html")); + assertTrue(chain.get(4).url().contains("/non-existing-page.html")); + for (int i = 0; i < chain.size(); i++) { + assertEquals(i != 0 ? chain.get(i - 1) : null, chain.get(i).redirectedTo()); + } + } + + @Test + void shouldWorkWithRedirectsForSubresources() { + List intercepted = new ArrayList<>(); + page.route("**/*", (route, request) -> { + route.continue_(); + intercepted.add(route.request()); + }); + server.setRedirect("/one-style.css", "/two-style.css"); + server.setRedirect("/two-style.css", "/three-style.css"); + server.setRedirect("/three-style.css", "/four-style.css"); + server.setRoute("/four-style.css", exchange -> { + exchange.sendResponseHeaders(200, 0); + try (OutputStreamWriter writer = new OutputStreamWriter(exchange.getResponseBody())) { + writer.write("body {box-sizing: border-box; } "); + } + }); + Response response = page.navigate(server.PREFIX + "/one-style.html"); + assertEquals(200, response.status()); + assertTrue(response.url().contains("one-style.html")); + + assertEquals(2, intercepted.size()); + assertEquals("document", intercepted.get(0).resourceType()); + assertTrue(intercepted.get(0).url().contains("one-style.html")); + + Request r = intercepted.get(1); + for (String url : asList("/one-style.css", "/two-style.css", "/three-style.css", "/four-style.css")) { + assertEquals("stylesheet", r.resourceType()); + assertTrue(r.url().contains(url)); + r = r.redirectedTo(); + } + assertNull(r); + } + + @Test + void shouldWorkWithEqualRequests() { + page.navigate(server.EMPTY_PAGE); + AtomicInteger responseCount = new AtomicInteger(1); + server.setRoute("/zzz", exchange -> { + exchange.sendResponseHeaders(200, 0); + try (OutputStreamWriter writer = new OutputStreamWriter(exchange.getResponseBody())) { + writer.write((responseCount.getAndIncrement()) * 11 + ""); + } + }); + + boolean[] spinner = {false}; + // Cancel 2nd request. + page.route("**/*", (route, request) -> { + if (spinner[0]) { + route.abort(); + } else { + route.continue_(); + } + spinner[0] = !spinner[0]; + }); + List results = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + results.add(page.evaluate("() => fetch('/zzz').then(response => response.text()).catch(e => 'FAILED')")); + } + assertEquals(asList("11", "FAILED", "22"), results); + } + + @Test + void shouldNavigateToDataURLAndNotFireDataURLRequests() { + List requests = new ArrayList<>(); + page.route("**/*", (route, request) -> { + requests.add(route.request()); + route.continue_(); + }); + String dataURL = "data:text/html,
yo
"; + Response response = page.navigate(dataURL); + assertNull(response); + assertEquals(0, requests.size()); + } + + @Test + void shouldBeAbleToFetchDataURLAndNotFireDataURLRequests() { + page.navigate(server.EMPTY_PAGE); + List requests = new ArrayList<>(); + page.route("**/*", (route, request) -> { + requests.add(route.request()); + route.continue_(); + }); + String dataURL = "data:text/html,
yo
"; + Object text = page.evaluate("url => fetch(url).then(r => r.text())", dataURL); + assertEquals("
yo
", text); + assertEquals(0, requests.size()); + } + + @Test + void shouldNavigateToURLWithHashAndAndFireRequestsWithoutHash() { + List requests = new ArrayList<>(); + page.route("**/*", (route, request) -> { + requests.add(route.request()); + route.continue_(); + }); + Response response = page.navigate(server.EMPTY_PAGE + "#hash"); + assertEquals(200, response.status()); + assertEquals(server.EMPTY_PAGE, response.url()); + assertEquals(1, requests.size()); + assertEquals(server.EMPTY_PAGE, requests.get(0).url()); + } + + @Test + void shouldWorkWithEncodedServer() throws InterruptedException { + // The requestWillBeSent will report encoded URL, whereas interception will + // report URL as-is. @see crbug.com/759388 + page.route("**/*", (route, request) -> route.continue_()); + Response response = page.navigate(server.PREFIX + "/some nonexisting page"); + assertEquals(404, response.status()); + } + + @Test + void shouldWorkWithBadlyEncodedServer() { + server.setRoute("/malformed", exchange -> { + exchange.sendResponseHeaders(200, 0); + exchange.getResponseBody().close(); + }); + page.route("**/*", (route, request) -> route.continue_()); + Response response = page.navigate(server.PREFIX + "/malformed?rnd=%911"); + assertEquals(200, response.status()); + } + + @Test + void shouldWorkWithEncodedServer2() { + // The requestWillBeSent will report URL as-is, whereas interception will + // report encoded URL for stylesheet. @see crbug.com/759388 + List requests = new ArrayList<>(); + page.route("**/*", (route, request) -> { + route.continue_(); + requests.add(route.request()); + }); + Response response = page.navigate("data:text/html,"); + assertNull(response); + assertEquals(1, requests.size()); + assertEquals(400, (requests.get(0).response()).status()); + } + + @Test + void shouldNotThrowInvalidInterceptionIdIfTheRequestWasCancelled() { + page.setContent(""); + Route[] route = {null}; + page.route("**/*", (r, req) -> route[0] = r); + // Wait for request interception. + Deferred> event = page.waitForEvent(REQUEST); + page.evalOnSelector("iframe", "(frame, url) => frame.src = url", server.EMPTY_PAGE); + event.get(); + // Delete frame to cause request to be canceled. + page.evalOnSelector("iframe", "frame => frame.remove()"); + try { + route[0].continue_(); + } catch (PlaywrightException e) { + fail("Should not throw"); + } + } + + @Test + void shouldInterceptMainResourceDuringCrossProcessNavigation() { + page.navigate(server.EMPTY_PAGE); + boolean[] intercepted = {false}; + page.route(server.CROSS_PROCESS_PREFIX + "/empty.html", (route, request) -> { + intercepted[0] = true; + route.continue_(); + }); + Response response = page.navigate(server.CROSS_PROCESS_PREFIX + "/empty.html"); + assertTrue(response.ok()); + assertTrue(intercepted[0]); + } + + @Test + void shouldCreateARedirect() { + page.navigate(server.PREFIX + "/empty.html"); + page.route("**/*", (route, request) -> { + System.out.println(request.url()); + if (!request.url().equals(server.PREFIX + "/redirect_this")) { + route.continue_(); + return; + } + route.fulfill(new Route.FulfillResponse() + .withStatus(301) + .withHeaders(mapOf("location", "/empty.html"))); + }); + Object text = page.evaluate("async url => {\n" + + " const data = await fetch(url);\n" + + " return data.text();\n" + + "}", server.PREFIX + "/redirect_this"); + assertEquals("", text); + } + + @Test + void shouldSupportCorsWithGET() { + page.navigate(server.EMPTY_PAGE); + page.route("**/cars*", (route, request) -> { + Map headers = new HashMap<>(); + if (request.url().endsWith("allow")) { + headers.put("access-control-allow-origin", "*"); + } + route.fulfill(new Route.FulfillResponse() + .withStatus(200) + .withContentType("application/json") + .withHeaders(headers) + .withBody("[\"electric\",\"gas\"]")); + }); + { + // Should succeed + Object resp = page.evaluate("async () => {\n" + + " const response = await fetch('https://example.com/cars?allow', { mode: 'cors' });\n" + + " return response.json();\n" + + "}"); + assertEquals(asList("electric", "gas"), resp); + } + { + // Should be rejected + try { + page.evaluate("async () => {\n" + + " const response = await fetch('https://example.com/cars?reject', { mode: 'cors' });\n" + + " return response.json();\n" + + "}"); + fail("did not throw"); + } catch (PlaywrightException e) { + assertTrue(e.getMessage().contains("failed")); + } + } + } + + @Test + void shouldSupportCorsWithPOST() { + page.navigate(server.EMPTY_PAGE); + page.route("**/cars", (route, request) -> { + route.fulfill(new Route.FulfillResponse() + .withStatus(200) + .withContentType("application/json") + .withHeaders(mapOf("Access-Control-Allow-Origin", "*")) + .withBody("[\"electric\",\"gas\"]")); + }); + Object resp = page.evaluate("async () => {\n" + + " const response = await fetch('https://example.com/cars', {\n" + + " method: 'POST',\n" + + " headers: { 'Content-Type': 'application/json' },\n" + + " mode: 'cors',\n" + + " body: JSON.stringify({ 'number': 1 })\n" + + " });\n" + + " return response.json();\n" + + "}"); + assertEquals(asList("electric", "gas"), resp); + } + + @Test + void shouldSupportCorsWithCredentials() { + page.navigate(server.EMPTY_PAGE); + page.route("**/cars", (route, request) -> { + route.fulfill(new Route.FulfillResponse() + .withStatus(200) + .withContentType("application/json") + .withHeaders(mapOf("Access-Control-Allow-Origin", server.PREFIX, + "Access-Control-Allow-Credentials", "true")) + .withBody("[\"electric\",\"gas\"]")); + }); + Object resp = page.evaluate("async () => {\n" + + " const response = await fetch('https://example.com/cars', {\n" + + " method: 'POST',\n" + + " headers: { 'Content-Type': 'application/json' },\n" + + " mode: 'cors',\n" + + " body: JSON.stringify({ 'number': 1 }),\n" + + " credentials: 'include'\n" + + " });\n" + + " return response.json();\n" + + "}"); + assertEquals(asList("electric", "gas"), resp); + } + + @Test + void shouldRejectCorsWithDisallowedCredentials() { + page.navigate(server.EMPTY_PAGE); + page.route("**/cars", (route, request) -> { + route.fulfill(new Route.FulfillResponse() + .withStatus(200) + .withContentType("application/json") + // Should fail without this line below! + // "Access-Control-Allow-Credentials": "true" + .withHeaders(mapOf("Access-Control-Allow-Origin", server.PREFIX)) + .withBody("[\"electric\",\"gas\"]")); + }); + try { + page.evaluate("async () => {\n" + + " const response = await fetch('https://example.com/cars', {\n" + + " method: 'POST',\n" + + " headers: { 'Content-Type': 'application/json' },\n" + + " mode: 'cors',\n" + + " body: JSON.stringify({ 'number': 1 }),\n" + + " credentials: 'include'\n" + + " });\n" + + " return response.json();\n" + + "}"); + fail("did not throw"); + } catch (PlaywrightException e) { + } + } + + @Test + void shouldSupportCorsForDifferentMethods() { + page.navigate(server.EMPTY_PAGE); + page.route("**/cars", (route, request) -> { + route.fulfill(new Route.FulfillResponse() + .withStatus(200) + .withContentType("application/json") + .withHeaders(mapOf("Access-Control-Allow-Origin", "*")) + .withBody("[\"" + request.method() + "\",\"electric\",\"gas\"]")); + }); + // First POST + { + Object resp = page.evaluate("async () => {\n" + + " const response = await fetch('https://example.com/cars', {\n" + + " method: 'POST',\n" + + " headers: { 'Content-Type': 'application/json' },\n" + + " mode: 'cors',\n" + + " body: JSON.stringify({ 'number': 1 })\n" + + " });\n" + + " return response.json();\n" + + "}"); + assertEquals(asList("POST", "electric", "gas"), resp); + } + // Then DELETE + { + Object resp = page.evaluate("async () => {\n" + + " const response = await fetch('https://example.com/cars', {\n" + + " method: 'DELETE',\n" + + " headers: {},\n" + + " mode: 'cors',\n" + + " body: ''\n" + + " });\n" + + " return response.json();\n" + + "}"); + assertEquals(asList("DELETE", "electric", "gas"), resp); + } + } + } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestRequestFulfill.java b/playwright/src/test/java/com/microsoft/playwright/TestRequestFulfill.java new file mode 100644 index 00000000..a4b5439f --- /dev/null +++ b/playwright/src/test/java/com/microsoft/playwright/TestRequestFulfill.java @@ -0,0 +1,103 @@ +/* + * 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.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + +import static com.microsoft.playwright.Utils.mapOf; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class TestRequestFulfill extends TestBase { + @Test + void shouldWork() { + page.route("**/*", (route, request) -> { + route.fulfill(new Route.FulfillResponse() + .withStatus(201) + .withContentType("text/html") + .withHeaders(mapOf("foo", "bar")) + .withBody("Yo, page!")); + }); + Response response = page.navigate(server.EMPTY_PAGE); + assertEquals(201, response.status()); + assertEquals("bar", response.headers().get("foo")); + assertEquals("Yo, page!", page.evaluate("() => document.body.textContent")); + } + + @Test + void shouldWorkWithStatusCode422() { + page.route("**/*", (route, request) -> { + route.fulfill(new Route.FulfillResponse() + .withStatus(422) + .withBody("Yo, page!")); + }); + Response response = page.navigate(server.EMPTY_PAGE); + assertEquals(422, response.status()); + assertEquals("Unprocessable Entity", response.statusText()); + assertEquals("Yo, page!", page.evaluate("document.body.textContent")); + } + + @Test + void shouldAllowMockingBinaryResponses() { +// TODO: test.skip(browserName === "firefox" && headful, "// Firefox headful produces a different image."); + page.route("**/*", (route, request) -> { + byte[] imageBuffer; + try { + imageBuffer = Files.readAllBytes(new File("src/test/resources/pptr.png").toPath()); + } catch (IOException e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + route.fulfill(new Route.FulfillResponse() + .withContentType("image/png") + .withBody(imageBuffer)); + }); + page.evaluate("PREFIX => {\n" + + " const img = document.createElement('img');\n" + + " img.src = PREFIX + '/does-not-exist.png';\n" + + " document.body.appendChild(img);\n" + + " return new Promise(fulfill => img.onload = fulfill);\n" + + "}", server.PREFIX); + ElementHandle img = page.querySelector("img"); +// expect(img.screenshot()).toMatchSnapshot("mock-binary-response.png"); + } + + @Test + void shouldAllowMockingSvgWithCharset() { + // TODO: test.skip(browserName === "firefox" && headful, "// Firefox headful produces a different image."); + // Firefox headful produces a different image. + page.route("**/*", (route, request) -> { + route.fulfill(new Route.FulfillResponse() + .withContentType("image/svg+xml ; charset=utf-8") + .withBody("")); + }); + page.evaluate("PREFIX => {\n" + + " const img = document.createElement('img');\n" + + " img.src = PREFIX + '/does-not-exist.svg';\n" + + " document.body.appendChild(img);\n" + + " return new Promise((f, r) => { img.onload = f; img.onerror = r; });\n" + + "}", server.PREFIX); + ElementHandle img = page.querySelector("img"); +// expect(img.screenshot()).toMatchSnapshot("mock-svg.png"); + } + + +}