diff --git a/playwright/src/main/java/com/microsoft/playwright/Browser.java b/playwright/src/main/java/com/microsoft/playwright/Browser.java index 9a52034a..1efc7ede 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Browser.java +++ b/playwright/src/main/java/com/microsoft/playwright/Browser.java @@ -144,6 +144,17 @@ public interface Browser extends AutoCloseable { * 'http://per-context' } })}. */ public Proxy proxy; + /** + * Optional setting to control resource content management. If {@code omit} is specified, content is not persisted. If {@code attach} + * is specified, resources are persistet as separate files and all of these files are archived along with the HAR file. + * Defaults to {@code embed}, which stores content inline the HAR file as per HAR specification. + */ + public HarContentPolicy recordHarContent; + /** + * When set to {@code minimal}, only record information necessary for routing from HAR. This omits sizes, timing, page, cookies, + * security and other types of HAR information that are not used when replaying from HAR. Defaults to {@code full}. + */ + public HarMode recordHarMode; /** * Optional setting to control whether to omit request content from the HAR. Defaults to {@code false}. */ @@ -374,6 +385,23 @@ public interface Browser extends AutoCloseable { this.proxy = proxy; return this; } + /** + * Optional setting to control resource content management. If {@code omit} is specified, content is not persisted. If {@code attach} + * is specified, resources are persistet as separate files and all of these files are archived along with the HAR file. + * Defaults to {@code embed}, which stores content inline the HAR file as per HAR specification. + */ + public NewContextOptions setRecordHarContent(HarContentPolicy recordHarContent) { + this.recordHarContent = recordHarContent; + return this; + } + /** + * When set to {@code minimal}, only record information necessary for routing from HAR. This omits sizes, timing, page, cookies, + * security and other types of HAR information that are not used when replaying from HAR. Defaults to {@code full}. + */ + public NewContextOptions setRecordHarMode(HarMode recordHarMode) { + this.recordHarMode = recordHarMode; + return this; + } /** * Optional setting to control whether to omit request content from the HAR. Defaults to {@code false}. */ @@ -602,6 +630,17 @@ public interface Browser extends AutoCloseable { * 'http://per-context' } })}. */ public Proxy proxy; + /** + * Optional setting to control resource content management. If {@code omit} is specified, content is not persisted. If {@code attach} + * is specified, resources are persistet as separate files and all of these files are archived along with the HAR file. + * Defaults to {@code embed}, which stores content inline the HAR file as per HAR specification. + */ + public HarContentPolicy recordHarContent; + /** + * When set to {@code minimal}, only record information necessary for routing from HAR. This omits sizes, timing, page, cookies, + * security and other types of HAR information that are not used when replaying from HAR. Defaults to {@code full}. + */ + public HarMode recordHarMode; /** * Optional setting to control whether to omit request content from the HAR. Defaults to {@code false}. */ @@ -832,6 +871,23 @@ public interface Browser extends AutoCloseable { this.proxy = proxy; return this; } + /** + * Optional setting to control resource content management. If {@code omit} is specified, content is not persisted. If {@code attach} + * is specified, resources are persistet as separate files and all of these files are archived along with the HAR file. + * Defaults to {@code embed}, which stores content inline the HAR file as per HAR specification. + */ + public NewPageOptions setRecordHarContent(HarContentPolicy recordHarContent) { + this.recordHarContent = recordHarContent; + return this; + } + /** + * When set to {@code minimal}, only record information necessary for routing from HAR. This omits sizes, timing, page, cookies, + * security and other types of HAR information that are not used when replaying from HAR. Defaults to {@code full}. + */ + public NewPageOptions setRecordHarMode(HarMode recordHarMode) { + this.recordHarMode = recordHarMode; + return this; + } /** * Optional setting to control whether to omit request content from the HAR. Defaults to {@code false}. */ diff --git a/playwright/src/main/java/com/microsoft/playwright/BrowserType.java b/playwright/src/main/java/com/microsoft/playwright/BrowserType.java index ef1dd8eb..b9545387 100644 --- a/playwright/src/main/java/com/microsoft/playwright/BrowserType.java +++ b/playwright/src/main/java/com/microsoft/playwright/BrowserType.java @@ -516,6 +516,17 @@ public interface BrowserType { * Network proxy settings. */ public Proxy proxy; + /** + * Optional setting to control resource content management. If {@code omit} is specified, content is not persisted. If {@code attach} + * is specified, resources are persistet as separate files and all of these files are archived along with the HAR file. + * Defaults to {@code embed}, which stores content inline the HAR file as per HAR specification. + */ + public HarContentPolicy recordHarContent; + /** + * When set to {@code minimal}, only record information necessary for routing from HAR. This omits sizes, timing, page, cookies, + * security and other types of HAR information that are not used when replaying from HAR. Defaults to {@code full}. + */ + public HarMode recordHarMode; /** * Optional setting to control whether to omit request content from the HAR. Defaults to {@code false}. */ @@ -854,6 +865,23 @@ public interface BrowserType { this.proxy = proxy; return this; } + /** + * Optional setting to control resource content management. If {@code omit} is specified, content is not persisted. If {@code attach} + * is specified, resources are persistet as separate files and all of these files are archived along with the HAR file. + * Defaults to {@code embed}, which stores content inline the HAR file as per HAR specification. + */ + public LaunchPersistentContextOptions setRecordHarContent(HarContentPolicy recordHarContent) { + this.recordHarContent = recordHarContent; + return this; + } + /** + * When set to {@code minimal}, only record information necessary for routing from HAR. This omits sizes, timing, page, cookies, + * security and other types of HAR information that are not used when replaying from HAR. Defaults to {@code full}. + */ + public LaunchPersistentContextOptions setRecordHarMode(HarMode recordHarMode) { + this.recordHarMode = recordHarMode; + return this; + } /** * Optional setting to control whether to omit request content from the HAR. Defaults to {@code false}. */ diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java index 7f77c576..939beef3 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java @@ -20,10 +20,7 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.microsoft.playwright.*; -import com.microsoft.playwright.options.BindingCallback; -import com.microsoft.playwright.options.Cookie; -import com.microsoft.playwright.options.FunctionCallback; -import com.microsoft.playwright.options.Geolocation; +import com.microsoft.playwright.options.*; import java.io.IOException; import java.net.MalformedURLException; @@ -336,7 +333,7 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext { @Override public void route(String url, Consumer handler, RouteOptions options) { - route(new UrlMatcher(this.baseUrl, url), handler, options); + route(new UrlMatcher(baseUrl, url), handler, options); } @Override @@ -351,7 +348,13 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext { @Override public void routeFromHAR(Path har, RouteFromHAROptions options) { - // TODO: + if (options == null) { + options = new RouteFromHAROptions(); + } + UrlMatcher matcher = UrlMatcher.forOneOf(baseUrl, options.url); + HARRouter harRouter = new HARRouter(browser.localUtils, har, options.notFound); + onClose(context -> harRouter.dispose()); + route(matcher, route -> harRouter.handle(route), null); } private void route(UrlMatcher matcher, Consumer handler, RouteOptions options) { diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserImpl.java index ae4f9628..039c9033 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserImpl.java @@ -20,6 +20,7 @@ import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.microsoft.playwright.*; +import com.microsoft.playwright.options.HarContentPolicy; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -140,8 +141,13 @@ class BrowserImpl extends ChannelOwner implements Browser { if (options.recordHarPath != null) { recordHar = new JsonObject(); recordHar.addProperty("path", options.recordHarPath.toString()); - if (options.recordHarOmitContent != null) { - recordHar.addProperty("omitContent", true); + if (options.recordHarContent != null) { + recordHar.addProperty("content", options.recordHarContent.toString().toLowerCase()); + } else if (options.recordHarOmitContent != null && options.recordHarOmitContent) { + recordHar.addProperty("content", HarContentPolicy.OMIT.toString().toLowerCase()); + } + if (options.recordHarMode != null) { + recordHar.addProperty("mode", options.recordHarMode.toString().toLowerCase()); } if (options.recordHarUrlFilter instanceof String) { recordHar.addProperty("urlGlob", (String) options.recordHarUrlFilter); @@ -151,7 +157,9 @@ class BrowserImpl extends ChannelOwner implements Browser { recordHar.addProperty("urlRegexFlags", toJsRegexFlags(pattern)); } options.recordHarPath = null; + options.recordHarMode = null; options.recordHarOmitContent = null; + options.recordHarContent = null; options.recordHarUrlFilter = null; } else { if (options.recordHarOmitContent != null) { @@ -160,6 +168,12 @@ class BrowserImpl extends ChannelOwner implements Browser { if (options.recordHarUrlFilter != null) { throw new PlaywrightException("recordHarUrlFilter is set but recordHarPath is null"); } + if (options.recordHarMode != null) { + throw new PlaywrightException("recordHarMode is set but recordHarPath is null"); + } + if (options.recordHarContent != null) { + throw new PlaywrightException("recordHarContent is set but recordHarPath is null"); + } } JsonObject params = gson().toJsonTree(options).getAsJsonObject(); diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/HARRouter.java b/playwright/src/main/java/com/microsoft/playwright/impl/HARRouter.java new file mode 100644 index 00000000..07b47835 --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/impl/HARRouter.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.playwright.impl; + +import com.google.gson.JsonObject; +import com.microsoft.playwright.PlaywrightException; +import com.microsoft.playwright.Request; +import com.microsoft.playwright.Route; +import com.microsoft.playwright.options.HarNotFound; + +import java.nio.file.Path; +import java.util.Base64; +import java.util.Map; + +import static com.microsoft.playwright.impl.LoggingSupport.logApi; +import static com.microsoft.playwright.impl.Serialization.fromNameValues; +import static com.microsoft.playwright.impl.Serialization.gson; + +public class HARRouter { + private final LocalUtils localUtils; + private final HarNotFound defaultAction; + private final String harId; + + HARRouter(LocalUtils localUtils, Path harFile, HarNotFound defaultAction) { + this.localUtils = localUtils; + this.defaultAction = defaultAction; + + JsonObject params = new JsonObject(); + params.addProperty("file", harFile.toString()); + JsonObject json = localUtils.sendMessage("harOpen", params).getAsJsonObject(); + if (json.has("error")) { + throw new PlaywrightException(json.get("error").getAsString()); + } + harId = json.get("harId").getAsString(); + } + + void handle(Route route) { + Request request = route.request(); + + JsonObject params = new JsonObject(); + params.addProperty("harId", harId); + params.addProperty("url", request.url()); + params.addProperty("method", request.method()); + params.add("headers", gson().toJsonTree(request.headersArray())); + if (request.postDataBuffer() != null) { + String base64 = Base64.getEncoder().encodeToString(request.postDataBuffer()); + params.addProperty("postData", base64); + } + params.addProperty("isNavigationRequest", request.isNavigationRequest()); + JsonObject response = localUtils.sendMessage("harLookup", params).getAsJsonObject(); + + String action = response.get("action").getAsString(); + if ("redirect".equals(action)) { + String redirectURL = response.get("redirectURL").getAsString(); + logApi("HAR: " + route.request().url() + " redirected to " + redirectURL); + ((RouteImpl) route).redirectNavigationRequest(redirectURL); + return; + } + + if ("fulfill".equals(action)) { + int status = response.get("status").getAsInt(); + Map headers = fromNameValues(response.getAsJsonArray("headers")); + byte[] buffer = Base64.getDecoder().decode(response.get("body").getAsString()); + route.fulfill(new Route.FulfillOptions() + .setStatus(status) + .setHeaders(headers) + .setBodyBytes(buffer)); + return; + } + + if ("error".equals(action)) { + logApi("HAR: " + response.get("message").getAsString()); + // Report the error, but fall through to the default handler. + } + + if (defaultAction == HarNotFound.FALLBACK) { + route.fallback(); + return; + } + + // By default abort not matching requests. + route.abort(); + } + + void dispose() { + JsonObject params = new JsonObject(); + params.addProperty("harId", harId); + localUtils.sendMessageAsync("harClose", params); + } +} diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/LoggingSupport.java b/playwright/src/main/java/com/microsoft/playwright/impl/LoggingSupport.java index 73467bd4..b2598a69 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/LoggingSupport.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/LoggingSupport.java @@ -60,7 +60,7 @@ class LoggingSupport { System.err.println(timestamp + " " + message); } - private void logApi(String message) { + static void logApi(String message) { // This matches log format produced by the server. logWithTimestamp("pw:api " + message); } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java index 92466839..e1e352e8 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java @@ -971,7 +971,13 @@ public class PageImpl extends ChannelOwner implements Page { @Override public void routeFromHAR(Path har, RouteFromHAROptions options) { - // TODO: + if (options == null) { + options = new RouteFromHAROptions(); + } + UrlMatcher matcher = UrlMatcher.forOneOf(browserContext.baseUrl, options.url); + HARRouter harRouter = new HARRouter(browserContext.browser().localUtils, har, options.notFound); + onClose(context -> harRouter.dispose()); + route(matcher, route -> harRouter.handle(route), null); } private void route(UrlMatcher matcher, Consumer handler, RouteOptions options) { 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 f859335f..edaeedf8 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/RouteImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/RouteImpl.java @@ -199,6 +199,14 @@ public class RouteImpl extends ChannelOwner implements Route { return connection.getExistingObject(initializer.getAsJsonObject("request").get("guid").getAsString()); } + void redirectNavigationRequest(String redirectURL) { + startHandling(); + JsonObject params = new JsonObject(); + params.addProperty("url", redirectURL); + // TODO: _raceWithPageClose ? + sendMessageAsync("redirectNavigationRequest", params); + } + private void startHandling() { if (handled) { throw new PlaywrightException("Route is already handled!"); 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 042b7f42..824a6b9f 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/Serialization.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/Serialization.java @@ -31,7 +31,6 @@ import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.*; -import java.util.regex.Pattern; class Serialization { private static final Gson gson = new GsonBuilder() @@ -311,6 +310,15 @@ class Serialization { return array; } + static Map fromNameValues(JsonArray array) { + Map map = new LinkedHashMap<>(); + for (JsonElement element : array) { + JsonObject pair = element.getAsJsonObject(); + map.put(pair.get("name").getAsString(), pair.get("value").getAsString()); + } + return map; + } + static List parseStringList(JsonArray array) { List result = new ArrayList<>(); for (JsonElement e : array) { diff --git a/playwright/src/main/java/com/microsoft/playwright/options/HarContentPolicy.java b/playwright/src/main/java/com/microsoft/playwright/options/HarContentPolicy.java new file mode 100644 index 00000000..8fca16f5 --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/options/HarContentPolicy.java @@ -0,0 +1,23 @@ +/* + * 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.options; + +public enum HarContentPolicy { + OMIT, + EMBED, + ATTACH +} \ No newline at end of file diff --git a/playwright/src/main/java/com/microsoft/playwright/options/HarMode.java b/playwright/src/main/java/com/microsoft/playwright/options/HarMode.java new file mode 100644 index 00000000..8e95f6f4 --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/options/HarMode.java @@ -0,0 +1,22 @@ +/* + * 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.options; + +public enum HarMode { + FULL, + MINIMAL +} \ No newline at end of file diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextHar.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextHar.java new file mode 100644 index 00000000..19e83683 --- /dev/null +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextHar.java @@ -0,0 +1,381 @@ +/* + * 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.HarMode; +import com.microsoft.playwright.options.HarNotFound; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.regex.Pattern; + +import static com.microsoft.playwright.Utils.copy; +import static com.microsoft.playwright.Utils.extractZip; +import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +public class TestBrowserContextHar extends TestBase { + @Test + void shouldContextRouteFromHARMatchingTheMethodAndFollowingRedirects() { + Path path = Paths.get("src/test/resources/har-fulfill.har"); + context.routeFromHAR(path); + Page page = context.newPage(); + page.navigate("http://no.playwright/"); + // HAR contains a redirect for the script that should be followed automatically. + assertEquals("foo", page.evaluate("window.value")); + // HAR contains a POST for the css file that should not be used. + assertThat(page.locator("body")).hasCSS("background-color", "rgb(255, 0, 0)"); + } + + @Test + void shouldPageRouteFromHARMatchingTheMethodAndFollowingRedirects() { + Path path = Paths.get("src/test/resources/har-fulfill.har"); + Page page = context.newPage(); + page.routeFromHAR(path); + page.navigate("http://no.playwright/"); + // HAR contains a redirect for the script that should be followed automatically. + assertEquals("foo", page.evaluate("window.value")); + // HAR contains a POST for the css file that should not be used. + assertThat(page.locator("body")).hasCSS("background-color", "rgb(255, 0, 0)"); + } + + @Test + void fallbackContinueShouldContinueWhenNotFoundInHar() { + Path path = Paths.get("src/test/resources/har-fulfill.har"); + context.routeFromHAR(path, new BrowserContext.RouteFromHAROptions().setNotFound(HarNotFound.FALLBACK)); + Page page = context.newPage(); + page.navigate(server.PREFIX + "/one-style.html"); + assertThat(page.locator("body")).hasCSS("background-color", "rgb(255, 192, 203)"); + } + + @Test + void byDefaultShouldAbortRequestsNotFoundInHar() { + Path path = Paths.get("src/test/resources/har-fulfill.har"); + context.routeFromHAR(path); + Page page = context.newPage(); + try { + page.navigate(server.EMPTY_PAGE); + fail("did not throw"); + } catch (PlaywrightException e) { + } + } + + @Test + void fallbackContinueShouldContinueRequestsOnBadHar(@TempDir Path tmpDir) throws IOException { + Path path = tmpDir.resolve("test.har"); + try (Writer stream = new OutputStreamWriter(Files.newOutputStream(path))) { + stream.write("{ \"log\" : {} }"); + } + context.routeFromHAR(path, new BrowserContext.RouteFromHAROptions().setNotFound(HarNotFound.FALLBACK)); + Page page = context.newPage(); + page.navigate(server.PREFIX + "/one-style.html"); + assertThat(page.locator("body")).hasCSS("background-color", "rgb(255, 192, 203)"); + } + + @Test + void shouldOnlyHandleRequestsMatchingUrlFilter() { + Path path = Paths.get("src/test/resources/har-fulfill.har"); + context.routeFromHAR(path, new BrowserContext.RouteFromHAROptions().setNotFound(HarNotFound.FALLBACK).setUrl("**/*.js")); + Page page = context.newPage(); + context.route("http://no.playwright/", route -> { + assertEquals("http://no.playwright/", route.request().url()); + route.fulfill(new Route.FulfillOptions() + .setStatus(200) + .setContentType("text/html") + .setBody("
hello
")); + }); + page.navigate("http://no.playwright/"); + // HAR contains a redirect for the script that should be followed automatically. + assertEquals("foo", page.evaluate("window.value")); + assertThat(page.locator("body")).hasCSS("background-color", "rgba(0, 0, 0, 0)"); + } + + @Test + void shouldOnlyContextRouteFromHARRequestsMatchingUrlFilter() { + Path path = Paths.get("src/test/resources/har-fulfill.har"); + context.routeFromHAR(path, new BrowserContext.RouteFromHAROptions().setUrl("**/*.js")); + Page page = context.newPage(); + context.route("http://no.playwright/", route -> { + assertEquals("http://no.playwright/", route.request().url()); + route.fulfill(new Route.FulfillOptions() + .setStatus(200) + .setContentType("text/html") + .setBody("
hello
")); + }); + page.navigate("http://no.playwright/"); + // HAR contains a redirect for the script that should be followed automatically. + assertEquals("foo", page.evaluate("window.value")); + assertThat(page.locator("body")).hasCSS("background-color", "rgba(0, 0, 0, 0)"); + } + + @Test + void shouldOnlyPageRouteFromHARRequestsMatchingUrlFilter() { + Path path = Paths.get("src/test/resources/har-fulfill.har"); + Page page = context.newPage(); + page.routeFromHAR(path, new Page.RouteFromHAROptions().setUrl("**/*.js")); + context.route("http://no.playwright/", route -> { + assertEquals("http://no.playwright/", route.request().url()); + route.fulfill(new Route.FulfillOptions() + .setStatus(200) + .setContentType("text/html") + .setBody("
hello
")); + }); + page.navigate("http://no.playwright/"); + // HAR contains a redirect for the script that should be followed automatically. + assertEquals("foo", page.evaluate("window.value")); + assertThat(page.locator("body")).hasCSS("background-color", "rgba(0, 0, 0, 0)"); + } + + @Test + void shouldSupportRegexFilter() { + Path path = Paths.get("src/test/resources/har-fulfill.har"); + context.routeFromHAR(path, new BrowserContext.RouteFromHAROptions().setUrl(Pattern.compile(".*(\\.js|.*\\.css|no.playwright\\/)$"))); + Page page = context.newPage(); + page.navigate("http://no.playwright/"); + assertEquals("foo", page.evaluate("window.value")); + assertThat(page.locator("body")).hasCSS("background-color", "rgb(255, 0, 0)"); + } + + @Test + void newPageShouldFulfillFromHarMatchingTheMethodAndFollowingRedirects() { + Path path = Paths.get("src/test/resources/har-fulfill.har"); + Page page = browser.newPage(); + page.routeFromHAR(path); + page.navigate("http://no.playwright/"); + // HAR contains a redirect for the script that should be followed automatically. + assertEquals("foo", page.evaluate("window.value")); + // HAR contains a POST for the css file that should not be used. + assertThat(page.locator("body")).hasCSS("background-color", "rgb(255, 0, 0)"); + page.close(); + } + + @Test + void shouldChangeDocumentURLAfterRedirectedNavigation() { + Path path = Paths.get("src/test/resources/har-redirect.har"); + context.routeFromHAR(path); + Page page = context.newPage(); + Response response = page.waitForNavigation(() -> { + page.navigate("https://theverge.com/"); + page.waitForURL("https://www.theverge.com/"); + }); + assertThat(page).hasURL("https://www.theverge.com/"); + assertEquals("https://www.theverge.com/", response.request().url()); + assertEquals("https://www.theverge.com/", page.evaluate("location.href")); + } + + @Test + void shouldChangeDocumentURLAfterRedirectedNavigationOnClick() { + Path path = Paths.get("src/test/resources/har-redirect.har"); + context.routeFromHAR(path, new BrowserContext.RouteFromHAROptions().setUrl(Pattern.compile(".*theverge.*"))); + Page page = context.newPage(); + page.navigate(server.EMPTY_PAGE); + page.setContent("click me"); + Response response = page.waitForNavigation(() -> page.click("text=click me")); + assertThat(page).hasURL("https://www.theverge.com/"); + assertEquals("https://www.theverge.com/", response.request().url()); + assertEquals("https://www.theverge.com/", page.evaluate("location.href")); + } + + @Test + void shouldGoBackToRedirectedNavigation() { + Path path = Paths.get("src/test/resources/har-redirect.har"); + context.routeFromHAR(path, new BrowserContext.RouteFromHAROptions().setUrl(Pattern.compile(".*theverge.*"))); + Page page = context.newPage(); + page.navigate("https://theverge.com/"); + page.navigate(server.EMPTY_PAGE); + assertThat(page).hasURL(server.EMPTY_PAGE); + Response response = page.goBack(); + assertThat(page).hasURL("https://www.theverge.com/"); + assertEquals("https://www.theverge.com/", response.request().url()); + assertEquals("https://www.theverge.com/", page.evaluate("location.href")); + } + + @Test + void shouldGoForwardToRedirectedNavigation() { + Path path = Paths.get("src/test/resources/har-redirect.har"); + context.routeFromHAR(path, new BrowserContext.RouteFromHAROptions().setUrl(Pattern.compile(".*theverge.*"))); + Page page = context.newPage(); + page.navigate(server.EMPTY_PAGE); + assertThat(page).hasURL(server.EMPTY_PAGE); + page.navigate("https://theverge.com/"); + assertThat(page).hasURL("https://www.theverge.com/"); + page.goBack(); + assertThat(page).hasURL(server.EMPTY_PAGE); + Response response = page.goForward(); + assertThat(page).hasURL("https://www.theverge.com/"); + assertEquals("https://www.theverge.com/", response.request().url()); + assertEquals("https://www.theverge.com/", page.evaluate("() => location.href")); + } + + @Test + void shouldReloadRedirectedNavigation() { + Path path = Paths.get("src/test/resources/har-redirect.har"); + context.routeFromHAR(path, new BrowserContext.RouteFromHAROptions().setUrl(Pattern.compile(".*theverge.*"))); + Page page = context.newPage(); + page.navigate("https://theverge.com/"); + assertThat(page).hasURL("https://www.theverge.com/"); + Response response = page.reload(); + assertThat(page).hasURL("https://www.theverge.com/"); + assertEquals("https://www.theverge.com/", response.request().url()); + assertEquals("https://www.theverge.com/", page.evaluate("() => location.href")); + } + + @Test + void shouldFulfillFromHarWithContentInAFile() { + Path path = Paths.get("src/test/resources/har-sha1.har"); + context.routeFromHAR(path); + Page page = context.newPage(); + page.navigate("http://no.playwright/"); + assertEquals("Hello, world", page.content()); + } + + @Test + void shouldRoundTripHarZip(@TempDir Path tmpDir) { + Path harPath = tmpDir.resolve("har.zip"); + try (BrowserContext context1 = browser.newContext(new Browser.NewContextOptions() + .setRecordHarPath(harPath) + .setRecordHarMode(HarMode.MINIMAL))) { + Page page1 = context1.newPage(); + page1.navigate(server.PREFIX + "/one-style.html"); + } + try (BrowserContext context2 = browser.newContext()) { + context2.routeFromHAR(harPath, new BrowserContext.RouteFromHAROptions().setNotFound(HarNotFound.ABORT)); + Page page2 = context.newPage(); + page2.navigate(server.PREFIX + "/one-style.html"); + assertTrue(page2.content().contains("hello, world!")); + assertThat(page2.locator("body")).hasCSS("background-color", "rgb(255, 192, 203)"); + } + } + + @Test + void shouldRoundTripExtractedHarZip(@TempDir Path tmpDir) throws IOException { + Path harPath = tmpDir.resolve("har.zip"); + try (BrowserContext context1 = browser.newContext(new Browser.NewContextOptions() + .setRecordHarPath(harPath) + .setRecordHarMode(HarMode.MINIMAL))) { + Page page1 = context1.newPage(); + page1.navigate(server.PREFIX + "/one-style.html"); + } + + Path harDir = tmpDir.resolve("hardir"); + extractZip(harPath, harDir); + + try (BrowserContext context2 = browser.newContext()) { + context2.routeFromHAR(harDir.resolve("har.har")); + Page page2 = context.newPage(); + page2.navigate(server.PREFIX + "/one-style.html"); + assertTrue(page2.content().contains("hello, world!")); + assertThat(page2.locator("body")).hasCSS("background-color", "rgb(255, 192, 203)"); + } + } + + @Test + void shouldRoundTripHarWithPostData(@TempDir Path tmpDir) { + server.setRoute("/echo", exchange -> { + exchange.sendResponseHeaders(200, 0); + try (OutputStream out = exchange.getResponseBody()) { + copy(exchange.getRequestBody(), out); + } + }); + + String fetchFunction = "async body => {\n" + + " const response = await fetch('/echo', { method: 'POST', body });\n" + + " return await response.text();\n" + + " }\n"; + Path harPath = tmpDir.resolve("har.zip"); + try (BrowserContext context1 = browser.newContext(new Browser.NewContextOptions() + .setRecordHarPath(harPath) + .setRecordHarMode(HarMode.MINIMAL))) { + Page page1 = context1.newPage(); + page1.navigate(server.EMPTY_PAGE); + assertEquals("1", page1.evaluate(fetchFunction, "1")); + assertEquals("2", page1.evaluate(fetchFunction, "2")); + assertEquals("3", page1.evaluate(fetchFunction, "3")); + } + server.reset(); + try (BrowserContext context2 = browser.newContext()) { + context2.routeFromHAR(harPath); + Page page2 = context2.newPage(); + page2.navigate(server.EMPTY_PAGE); + assertEquals("1", page2.evaluate(fetchFunction, "1")); + assertEquals("2", page2.evaluate(fetchFunction, "2")); + assertEquals("3", page2.evaluate(fetchFunction, "3")); + assertEquals("3", page2.evaluate(fetchFunction, "3")); + try { + Object result = page2.evaluate(fetchFunction, "4"); + System.out.println(result); + fail("did not throw"); + } catch (PlaywrightException e) { + } + } + } + + @Test + void shouldDisambiguateByHeader(@TempDir Path tmpDir) { + server.setRoute("/echo", exchange -> { + exchange.sendResponseHeaders(200, 0); + try (OutputStream out = exchange.getResponseBody()) { + List values = exchange.getRequestHeaders().get("baz"); + try (OutputStreamWriter writer = new OutputStreamWriter(exchange.getResponseBody())) { + writer.write(values == null ? "" : String.join(", ", values)); + } + } + }); + + String fetchFunction = "async bazValue => {\n" + + " const response = await fetch('/echo', {\n" + + " method: 'POST',\n" + + " body: '',\n" + + " headers: {\n" + + " foo: 'foo-value',\n" + + " bar: 'bar-value',\n" + + " baz: bazValue,\n" + + " }\n" + + " });\n" + + " return await response.text();\n" + + " }\n"; + Path harPath = tmpDir.resolve("har.zip"); + try (BrowserContext context1 = browser.newContext(new Browser.NewContextOptions() + .setRecordHarPath(harPath) + .setRecordHarMode(HarMode.MINIMAL))) { + Page page1 = context1.newPage(); + page1.navigate(server.EMPTY_PAGE); + assertEquals("baz1", page1.evaluate(fetchFunction, "baz1")); + assertEquals("baz2", page1.evaluate(fetchFunction, "baz2")); + assertEquals("baz3", page1.evaluate(fetchFunction, "baz3")); + } + server.reset(); + try (BrowserContext context2 = browser.newContext()) { + context2.routeFromHAR(harPath); + Page page2 = context2.newPage(); + page2.navigate(server.EMPTY_PAGE); + assertEquals("baz1", page2.evaluate(fetchFunction, "baz1")); + assertEquals("baz2", page2.evaluate(fetchFunction, "baz2")); + assertEquals("baz3", page2.evaluate(fetchFunction, "baz3")); + assertEquals("baz1", page2.evaluate(fetchFunction, "baz4")); + } + } + +} diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowserTypeConnect.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowserTypeConnect.java index af5ac0d7..6c8a2613 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestBrowserTypeConnect.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowserTypeConnect.java @@ -17,7 +17,6 @@ package com.microsoft.playwright; import com.microsoft.playwright.impl.Driver; -import com.microsoft.playwright.options.WaitForSelectorState; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.BeforeAll; @@ -499,7 +498,7 @@ public class TestBrowserTypeConnect extends TestBase { Path trace = tmpDir.resolve("trace1.zip"); context.tracing().stop(new Tracing.StopOptions().setPath(trace)); - Map entries = parseTrace(trace); + Map entries = parseZip(trace); Map sources = entries.entrySet().stream().filter(e -> e.getKey().endsWith(".txt")).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); assertEquals(1, sources.size()); diff --git a/playwright/src/test/java/com/microsoft/playwright/TestHar.java b/playwright/src/test/java/com/microsoft/playwright/TestHar.java index 62d82cf1..7d41d01b 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestHar.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestHar.java @@ -20,16 +20,17 @@ import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import com.microsoft.playwright.options.HarContentPolicy; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import java.io.FileReader; -import java.io.IOException; -import java.io.Reader; +import java.io.*; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Map; import java.util.regex.Pattern; import static com.microsoft.playwright.options.LoadState.DOMCONTENTLOADED; @@ -50,8 +51,12 @@ public class TestHar extends TestBase { final Page page; PageWithHar() throws IOException { - harFile = Files.createTempFile("test-", ".har"); - context = browser.newContext(new Browser.NewContextOptions() + this(new Browser.NewContextOptions(), null); + } + + PageWithHar(Browser.NewContextOptions options, Path harFilePath) throws IOException { + harFile = harFilePath == null ? Files.createTempFile("test-", ".har") : harFilePath; + context = browser.newContext(options .setRecordHarPath(harFile).setIgnoreHTTPSErrors(true)); page = context.newPage(); } @@ -61,6 +66,11 @@ public class TestHar extends TestBase { return parseHar(harFile); } + Map parseZip() throws IOException { + context.close(); + return Utils.parseZip(harFile); + } + void dispose() throws IOException { context.close(); Files.deleteIfExists(harFile); @@ -217,4 +227,94 @@ public class TestHar extends TestBase { assertEquals(1, entries.size()); assertTrue(entries.get(0).getAsJsonObject().getAsJsonObject("request").get("url").getAsString().endsWith("har.html")); } + + @Test + void shouldOmitContent(@TempDir Path tmpDir) throws IOException { + Path harPath = tmpDir.resolve("test.har"); + PageWithHar pageWithHar = new PageWithHar(new Browser.NewContextOptions() + .setRecordHarContent(HarContentPolicy.OMIT), harPath); + pageWithHar.page.navigate(server.PREFIX + "/har.html"); + pageWithHar.page.evaluate("() => fetch('/pptr.png').then(r => r.arrayBuffer())"); + JsonObject log = pageWithHar.log(); + pageWithHar.dispose(); + JsonArray entries = log.getAsJsonArray("entries"); + assertFalse(entries.get(0).getAsJsonObject() + .getAsJsonObject("response") + .getAsJsonObject("content") + .has("text")); + assertFalse(entries.get(0).getAsJsonObject() + .getAsJsonObject("response") + .getAsJsonObject("content") + .has("_file")); + } + + @Test + void shouldOmitContentLegacy(@TempDir Path tmpDir) throws IOException { + Path harPath = tmpDir.resolve("test.har"); + PageWithHar pageWithHar = new PageWithHar(new Browser.NewContextOptions() + .setRecordHarOmitContent(true), harPath); + pageWithHar.page.navigate(server.PREFIX + "/har.html"); + pageWithHar.page.evaluate("() => fetch('/pptr.png').then(r => r.arrayBuffer())"); + JsonObject log = pageWithHar.log(); + pageWithHar.dispose(); + JsonArray entries = log.getAsJsonArray("entries"); + assertFalse(entries.get(0).getAsJsonObject() + .getAsJsonObject("response") + .getAsJsonObject("content") + .has("text")); + assertFalse(entries.get(0).getAsJsonObject() + .getAsJsonObject("response") + .getAsJsonObject("content") + .has("_file")); + } + + @Test + void shouldAttachContent(@TempDir Path tmpDir) throws IOException { + Path harPath = tmpDir.resolve("test.har.zip"); + PageWithHar pageWithHar = new PageWithHar(new Browser.NewContextOptions() + .setRecordHarContent(HarContentPolicy.ATTACH), harPath); + pageWithHar.page.navigate(server.PREFIX + "/har.html"); + pageWithHar.page.evaluate("() => fetch('/pptr.png').then(r => r.arrayBuffer())"); + Map zip = pageWithHar.parseZip(); + JsonObject log = new Gson().fromJson(new InputStreamReader(new ByteArrayInputStream(zip.get("har.har"))), JsonObject.class).getAsJsonObject("log"); + pageWithHar.dispose(); + + JsonArray entries = log.getAsJsonArray("entries"); + { + JsonObject content = entries.get(0).getAsJsonObject() + .getAsJsonObject("response") + .getAsJsonObject("content"); + assertFalse(content.has("encoding")); + assertEquals("text/html", content.get("mimeType").getAsString()); + assertTrue(content.get("_file").getAsString().contains("75841480e2606c03389077304342fac2c58ccb1b")); + assertTrue(content.get("size").getAsInt() >= 96); + assertEquals(0, content.get("compression").getAsInt()); + } + { + JsonObject content = entries.get(1).getAsJsonObject() + .getAsJsonObject("response") + .getAsJsonObject("content"); + assertFalse(content.has("encoding")); + assertEquals("text/css", content.get("mimeType").getAsString()); + assertTrue(content.get("_file").getAsString().contains("79f739d7bc88e80f55b9891a22bf13a2b4e18adb")); + assertTrue(content.get("size").getAsInt() >= 37); + assertEquals(0, content.get("compression").getAsInt()); + } + { + JsonObject content = entries.get(2).getAsJsonObject() + .getAsJsonObject("response") + .getAsJsonObject("content"); + assertFalse(content.has("encoding")); + assertEquals("image/png", content.get("mimeType").getAsString()); + assertTrue(content.get("_file").getAsString().contains("a4c3a18f0bb83f5d9fe7ce561e065c36205762fa")); + assertTrue(content.get("size").getAsInt() >= 6000); + assertEquals(0, content.get("compression").getAsInt()); + } + assertTrue(new String(zip.get("75841480e2606c03389077304342fac2c58ccb1b.html"), StandardCharsets.UTF_8).contains("HAR Page")); + assertTrue(new String(zip.get("79f739d7bc88e80f55b9891a22bf13a2b4e18adb.css"), StandardCharsets.UTF_8).contains("pink")); + assertEquals(entries.get(2).getAsJsonObject() + .getAsJsonObject("response") + .getAsJsonObject("content") + .get("size").getAsInt(), zip.get("a4c3a18f0bb83f5d9fe7ce561e065c36205762fa.png").length); + } } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestTracing.java b/playwright/src/test/java/com/microsoft/playwright/TestTracing.java index a273c942..d749772c 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestTracing.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestTracing.java @@ -113,7 +113,7 @@ public class TestTracing extends TestBase { Path trace = tmpDir.resolve("trace1.zip"); context.tracing().stop(new Tracing.StopOptions().setPath(trace)); - Map entries = Utils.parseTrace(trace); + Map entries = Utils.parseZip(trace); Map sources = entries.entrySet().stream().filter(e -> e.getKey().endsWith(".txt")).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); assertEquals(1, sources.size()); diff --git a/playwright/src/test/java/com/microsoft/playwright/Utils.java b/playwright/src/test/java/com/microsoft/playwright/Utils.java index fdea5174..0f558dc8 100644 --- a/playwright/src/test/java/com/microsoft/playwright/Utils.java +++ b/playwright/src/test/java/com/microsoft/playwright/Utils.java @@ -22,7 +22,7 @@ import com.google.gson.JsonParser; import java.io.*; import java.net.ServerSocket; -import java.net.Socket; +import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; import java.util.HashMap; @@ -89,7 +89,7 @@ class Utils { } } - static Map parseTrace(Path trace) throws IOException { + static Map parseZip(Path trace) throws IOException { Map entries = new HashMap<>(); try (ZipInputStream zis = new ZipInputStream(new FileInputStream(trace.toFile()))) { for (ZipEntry zipEntry = zis.getNextEntry(); zipEntry != null; zipEntry = zis.getNextEntry()) { @@ -104,6 +104,25 @@ class Utils { return entries; } + static Map extractZip(Path zipPath, Path toDir) throws IOException { + Map entries = new HashMap<>(); + Files.createDirectories(toDir); + try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipPath.toFile()))) { + for (ZipEntry zipEntry = zis.getNextEntry(); zipEntry != null; zipEntry = zis.getNextEntry()) { + System.out.println(zipEntry.getName()); + Path toPath = toDir.resolve(zipEntry.getName()); + if (zipEntry.isDirectory()) { + Files.createDirectories(toPath); + } else { + Files.copy(zis, toPath); + } + zis.closeEntry(); + } + } + return entries; + } + + enum OS { WINDOWS, MAC, LINUX, UNKNOWN } static OS getOS() { String name = System.getProperty("os.name").toLowerCase(); diff --git a/playwright/src/test/resources/har-fulfill.har b/playwright/src/test/resources/har-fulfill.har new file mode 100644 index 00000000..5b679098 --- /dev/null +++ b/playwright/src/test/resources/har-fulfill.har @@ -0,0 +1,366 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "Playwright", + "version": "1.23.0-next" + }, + "browser": { + "name": "chromium", + "version": "103.0.5060.33" + }, + "pages": [ + { + "startedDateTime": "2022-06-10T04:27:32.125Z", + "id": "page@b17b177f1c2e66459db3dcbe44636ffd", + "title": "Hey", + "pageTimings": { + "onContentLoad": 70, + "onLoad": 70 + } + } + ], + "entries": [ + { + "_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea", + "_monotonicTime": 270572145.898, + "startedDateTime": "2022-06-10T04:27:32.146Z", + "time": 8.286, + "request": { + "method": "GET", + "url": "http://no.playwright/", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" + }, + { + "name": "Upgrade-Insecure-Requests", + "value": "1" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.33 Safari/537.36" + } + ], + "queryString": [], + "headersSize": 326, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-length", + "value": "111" + }, + { + "name": "content-type", + "value": "text/html" + } + ], + "content": { + "size": 111, + "mimeType": "text/html", + "compression": 0, + "text": "Hey
hello
" + }, + "headersSize": 65, + "bodySize": 170, + "redirectURL": "", + "_transferSize": 170 + }, + "cache": { + "beforeRequest": null, + "afterRequest": null + }, + "timings": { + "dns": -1, + "connect": -1, + "ssl": -1, + "send": 0, + "wait": 8.286, + "receive": -1 + }, + "pageref": "page@b17b177f1c2e66459db3dcbe44636ffd", + "_securityDetails": {} + }, + { + "_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea", + "_monotonicTime": 270572174.683, + "startedDateTime": "2022-06-10T04:27:32.172Z", + "time": 7.132, + "request": { + "method": "POST", + "url": "http://no.playwright/style.css", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Accept", + "value": "text/css,*/*;q=0.1" + }, + { + "name": "Referer", + "value": "http://no.playwright/" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.33 Safari/537.36" + } + ], + "queryString": [], + "headersSize": 220, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-length", + "value": "24" + }, + { + "name": "content-type", + "value": "text/css" + } + ], + "content": { + "size": 24, + "mimeType": "text/css", + "compression": 0, + "text": "body { background:cyan }" + }, + "headersSize": 63, + "bodySize": 81, + "redirectURL": "", + "_transferSize": 81 + }, + "cache": { + "beforeRequest": null, + "afterRequest": null + }, + "timings": { + "dns": -1, + "connect": -1, + "ssl": -1, + "send": 0, + "wait": 8.132, + "receive": -1 + }, + "pageref": "page@b17b177f1c2e66459db3dcbe44636ffd", + "_securityDetails": {} + }, + { + "_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea", + "_monotonicTime": 270572174.683, + "startedDateTime": "2022-06-10T04:27:32.174Z", + "time": 8.132, + "request": { + "method": "GET", + "url": "http://no.playwright/style.css", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Accept", + "value": "text/css,*/*;q=0.1" + }, + { + "name": "Referer", + "value": "http://no.playwright/" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.33 Safari/537.36" + } + ], + "queryString": [], + "headersSize": 220, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-length", + "value": "24" + }, + { + "name": "content-type", + "value": "text/css" + } + ], + "content": { + "size": 24, + "mimeType": "text/css", + "compression": 0, + "text": "body { background: red }" + }, + "headersSize": 63, + "bodySize": 81, + "redirectURL": "", + "_transferSize": 81 + }, + "cache": { + "beforeRequest": null, + "afterRequest": null + }, + "timings": { + "dns": -1, + "connect": -1, + "ssl": -1, + "send": 0, + "wait": 8.132, + "receive": -1 + }, + "pageref": "page@b17b177f1c2e66459db3dcbe44636ffd", + "_securityDetails": {} + }, + { + "_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea", + "_monotonicTime": 270572175.042, + "startedDateTime": "2022-06-10T04:27:32.175Z", + "time": 15.997, + "request": { + "method": "GET", + "url": "http://no.playwright/script.js", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Accept", + "value": "*/*" + }, + { + "name": "Referer", + "value": "http://no.playwright/" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.33 Safari/537.36" + } + ], + "queryString": [], + "headersSize": 205, + "bodySize": 0 + }, + "response": { + "status": 301, + "statusText": "Moved Permanently", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "location", + "value": "http://no.playwright/script2.js" + } + ], + "content": { + "size": -1, + "mimeType": "x-unknown", + "compression": 0 + }, + "headersSize": 77, + "bodySize": 0, + "redirectURL": "http://no.playwright/script2.js", + "_transferSize": 77 + }, + "cache": { + "beforeRequest": null, + "afterRequest": null + }, + "timings": { + "dns": -1, + "connect": -1, + "ssl": -1, + "send": 0, + "wait": 7.673, + "receive": 8.324 + }, + "pageref": "page@b17b177f1c2e66459db3dcbe44636ffd", + "_securityDetails": {} + }, + { + "_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea", + "_monotonicTime": 270572181.822, + "startedDateTime": "2022-06-10T04:27:32.182Z", + "time": 6.735, + "request": { + "method": "GET", + "url": "http://no.playwright/script2.js", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Accept", + "value": "*/*" + }, + { + "name": "Referer", + "value": "http://no.playwright/" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.33 Safari/537.36" + } + ], + "queryString": [], + "headersSize": 206, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-length", + "value": "18" + }, + { + "name": "content-type", + "value": "text/javascript" + } + ], + "content": { + "size": 18, + "mimeType": "text/javascript", + "compression": 0, + "text": "window.value='foo'" + }, + "headersSize": 70, + "bodySize": 82, + "redirectURL": "", + "_transferSize": 82 + }, + "cache": { + "beforeRequest": null, + "afterRequest": null + }, + "timings": { + "dns": -1, + "connect": -1, + "ssl": -1, + "send": 0, + "wait": 6.735, + "receive": -1 + }, + "pageref": "page@b17b177f1c2e66459db3dcbe44636ffd", + "_securityDetails": {} + } + ] + } +} \ No newline at end of file diff --git a/playwright/src/test/resources/har-redirect.har b/playwright/src/test/resources/har-redirect.har new file mode 100644 index 00000000..5b50e7bf --- /dev/null +++ b/playwright/src/test/resources/har-redirect.har @@ -0,0 +1,620 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "Playwright", + "version": "1.23.0-next" + }, + "browser": { + "name": "chromium", + "version": "103.0.5060.42" + }, + "pages": [ + { + "startedDateTime": "2022-06-16T21:41:23.901Z", + "id": "page@8f314969edc000996eb5c2ab22f0e6b3", + "title": "Microsoft", + "pageTimings": { + "onContentLoad": 8363, + "onLoad": 8896 + } + } + ], + "entries": [ + { + "_frameref": "frame@3767e074ecde4cb8372abba2f6f9bb4f", + "_monotonicTime": 110928357.437, + "startedDateTime": "2022-06-16T21:41:23.951Z", + "time": 93.99, + "request": { + "method": "GET", + "url": "https://theverge.com/", + "httpVersion": "HTTP/2.0", + "cookies": [], + "headers": [ + { + "name": ":authority", + "value": "theverge.com" + }, + { + "name": ":method", + "value": "GET" + }, + { + "name": ":path", + "value": "/" + }, + { + "name": ":scheme", + "value": "https" + }, + { + "name": "accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" + }, + { + "name": "accept-encoding", + "value": "gzip, deflate, br" + }, + { + "name": "accept-language", + "value": "en-US,en;q=0.9" + }, + { + "name": "sec-ch-ua", + "value": "\"Chromium\";v=\"103\", \".Not/A)Brand\";v=\"99\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"Linux\"" + }, + { + "name": "sec-fetch-dest", + "value": "document" + }, + { + "name": "sec-fetch-mode", + "value": "navigate" + }, + { + "name": "sec-fetch-site", + "value": "none" + }, + { + "name": "sec-fetch-user", + "value": "?1" + }, + { + "name": "upgrade-insecure-requests", + "value": "1" + }, + { + "name": "user-agent", + "value": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.42 Safari/537.36" + } + ], + "queryString": [], + "headersSize": 644, + "bodySize": 0 + }, + "response": { + "status": 301, + "statusText": "", + "httpVersion": "HTTP/2.0", + "cookies": [ + { + "name": "vmidv1", + "value": "9faf31ab-1415-4b90-b367-24b670205f41", + "expires": "2027-06-15T21:41:24.000Z", + "domain": "theverge.com", + "path": "/", + "sameSite": "Lax", + "secure": true + } + ], + "headers": [ + { + "name": "accept-ranges", + "value": "bytes" + }, + { + "name": "content-length", + "value": "0" + }, + { + "name": "date", + "value": "Thu, 16 Jun 2022 21:41:24 GMT" + }, + { + "name": "location", + "value": "http://www.theverge.com/" + }, + { + "name": "retry-after", + "value": "0" + }, + { + "name": "server", + "value": "Varnish" + }, + { + "name": "set-cookie", + "value": "vmidv1=9faf31ab-1415-4b90-b367-24b670205f41;Expires=Tue, 15 Jun 2027 21:41:24 GMT;Domain=theverge.com;Path=/;SameSite=Lax;Secure" + }, + { + "name": "via", + "value": "1.1 varnish" + }, + { + "name": "x-cache", + "value": "HIT" + }, + { + "name": "x-cache-hits", + "value": "0" + }, + { + "name": "x-served-by", + "value": "cache-pao17442-PAO" + }, + { + "name": "x-timer", + "value": "S1655415684.005867,VS0,VE0" + } + ], + "content": { + "size": -1, + "mimeType": "x-unknown", + "compression": 0 + }, + "headersSize": 425, + "bodySize": 0, + "redirectURL": "http://www.theverge.com/", + "_transferSize": 425 + }, + "cache": { + "beforeRequest": null, + "afterRequest": null + }, + "timings": { + "dns": 0, + "connect": 34.151, + "ssl": 28.074, + "send": 0, + "wait": 27.549, + "receive": 4.216 + }, + "pageref": "page@8f314969edc000996eb5c2ab22f0e6b3", + "serverIPAddress": "151.101.65.52", + "_serverPort": 443, + "_securityDetails": { + "protocol": "TLS 1.2", + "subjectName": "*.americanninjawarriornation.com", + "issuer": "GlobalSign Atlas R3 DV TLS CA 2022 Q1", + "validFrom": 1644853133, + "validTo": 1679153932 + } + }, + { + "_frameref": "frame@3767e074ecde4cb8372abba2f6f9bb4f", + "_monotonicTime": 110928427.603, + "startedDateTime": "2022-06-16T21:41:24.022Z", + "time": 44.39499999999999, + "request": { + "method": "GET", + "url": "http://www.theverge.com/", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" + }, + { + "name": "Accept-Encoding", + "value": "gzip, deflate" + }, + { + "name": "Accept-Language", + "value": "en-US,en;q=0.9" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Host", + "value": "www.theverge.com" + }, + { + "name": "Upgrade-Insecure-Requests", + "value": "1" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.42 Safari/537.36" + } + ], + "queryString": [], + "headersSize": 423, + "bodySize": 0 + }, + "response": { + "status": 301, + "statusText": "Moved Permanently", + "httpVersion": "HTTP/1.1", + "cookies": [ + { + "name": "_chorus_geoip_continent", + "value": "NA" + }, + { + "name": "vmidv1", + "value": "4e0c1265-10f8-4cb1-a5de-1c3cf70b531c", + "expires": "2027-06-15T21:41:24.000Z", + "domain": "www.theverge.com", + "path": "/", + "sameSite": "Lax", + "secure": true + } + ], + "headers": [ + { + "name": "Accept-Ranges", + "value": "bytes" + }, + { + "name": "Age", + "value": "2615" + }, + { + "name": "Connection", + "value": "keep-alive" + }, + { + "name": "Content-Length", + "value": "0" + }, + { + "name": "Content-Type", + "value": "text/html" + }, + { + "name": "Date", + "value": "Thu, 16 Jun 2022 21:41:24 GMT" + }, + { + "name": "Location", + "value": "https://www.theverge.com/" + }, + { + "name": "Server", + "value": "nginx" + }, + { + "name": "Set-Cookie", + "value": "_chorus_geoip_continent=NA; expires=Fri, 17 Jun 2022 21:41:24 GMT; path=/;" + }, + { + "name": "Set-Cookie", + "value": "vmidv1=4e0c1265-10f8-4cb1-a5de-1c3cf70b531c;Expires=Tue, 15 Jun 2027 21:41:24 GMT;Domain=www.theverge.com;Path=/;SameSite=Lax;Secure" + }, + { + "name": "Vary", + "value": "X-Forwarded-Proto, Cookie, X-Chorus-Unison-Testing, X-Chorus-Require-Privacy-Consent, X-Chorus-Restrict-In-Privacy-Consent-Region, Accept-Encoding" + }, + { + "name": "Via", + "value": "1.1 varnish" + }, + { + "name": "X-Cache", + "value": "HIT" + }, + { + "name": "X-Cache-Hits", + "value": "2" + }, + { + "name": "X-Served-By", + "value": "cache-pao17450-PAO" + }, + { + "name": "X-Timer", + "value": "S1655415684.035748,VS0,VE0" + } + ], + "content": { + "size": -1, + "mimeType": "text/html", + "compression": 0 + }, + "headersSize": 731, + "bodySize": 0, + "redirectURL": "https://www.theverge.com/", + "_transferSize": 731 + }, + "cache": { + "beforeRequest": null, + "afterRequest": null + }, + "timings": { + "dns": 2.742, + "connect": 10.03, + "ssl": 14.123, + "send": 0, + "wait": 15.023, + "receive": 2.477 + }, + "pageref": "page@8f314969edc000996eb5c2ab22f0e6b3", + "serverIPAddress": "151.101.189.52", + "_serverPort": 80, + "_securityDetails": {} + }, + { + "_frameref": "frame@3767e074ecde4cb8372abba2f6f9bb4f", + "_monotonicTime": 110928455.901, + "startedDateTime": "2022-06-16T21:41:24.050Z", + "time": 50.29199999999999, + "request": { + "method": "GET", + "url": "https://www.theverge.com/", + "httpVersion": "HTTP/2.0", + "cookies": [ + { + "name": "vmidv1", + "value": "9faf31ab-1415-4b90-b367-24b670205f41" + }, + { + "name": "_chorus_geoip_continent", + "value": "NA" + } + ], + "headers": [ + { + "name": ":authority", + "value": "www.theverge.com" + }, + { + "name": ":method", + "value": "GET" + }, + { + "name": ":path", + "value": "/" + }, + { + "name": ":scheme", + "value": "https" + }, + { + "name": "accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" + }, + { + "name": "accept-encoding", + "value": "gzip, deflate, br" + }, + { + "name": "accept-language", + "value": "en-US,en;q=0.9" + }, + { + "name": "cookie", + "value": "vmidv1=9faf31ab-1415-4b90-b367-24b670205f41; _chorus_geoip_continent=NA" + }, + { + "name": "sec-ch-ua", + "value": "\"Chromium\";v=\"103\", \".Not/A)Brand\";v=\"99\"" + }, + { + "name": "sec-ch-ua-mobile", + "value": "?0" + }, + { + "name": "sec-ch-ua-platform", + "value": "\"Linux\"" + }, + { + "name": "sec-fetch-dest", + "value": "document" + }, + { + "name": "sec-fetch-mode", + "value": "navigate" + }, + { + "name": "sec-fetch-site", + "value": "none" + }, + { + "name": "sec-fetch-user", + "value": "?1" + }, + { + "name": "upgrade-insecure-requests", + "value": "1" + }, + { + "name": "user-agent", + "value": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.42 Safari/537.36" + } + ], + "queryString": [], + "headersSize": 729, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "", + "httpVersion": "HTTP/2.0", + "cookies": [ + { + "name": "_chorus_geoip_continent", + "value": "NA" + }, + { + "name": "vmidv1", + "value": "40d8fd14-5ac3-4757-9e9c-efb106e82d3a", + "expires": "2027-06-15T21:41:24.000Z", + "domain": "www.theverge.com", + "path": "/", + "sameSite": "Lax", + "secure": true + } + ], + "headers": [ + { + "name": "accept-ranges", + "value": "bytes" + }, + { + "name": "age", + "value": "263" + }, + { + "name": "cache-control", + "value": "max-age=0, public, must-revalidate" + }, + { + "name": "content-encoding", + "value": "br" + }, + { + "name": "content-length", + "value": "14" + }, + { + "name": "content-security-policy", + "value": "default-src https: data: 'unsafe-inline' 'unsafe-eval'; child-src https: data: blob:; connect-src https: data: blob: ; font-src https: data:; img-src https: data: blob:; media-src https: data: blob:; object-src https:; script-src https: data: blob: 'unsafe-inline' 'unsafe-eval'; style-src https: 'unsafe-inline'; block-all-mixed-content; upgrade-insecure-requests" + }, + { + "name": "content-type", + "value": "text/html; charset=utf-8" + }, + { + "name": "date", + "value": "Thu, 16 Jun 2022 21:41:24 GMT" + }, + { + "name": "etag", + "value": "W/\"d498ef668223d015000070a66a181e85\"" + }, + { + "name": "link", + "value": "; rel=preload; as=fetch; crossorigin" + }, + { + "name": "referrer-policy", + "value": "strict-origin-when-cross-origin" + }, + { + "name": "server", + "value": "nginx" + }, + { + "name": "set-cookie", + "value": "_chorus_geoip_continent=NA; expires=Fri, 17 Jun 2022 21:41:24 GMT; path=/;" + }, + { + "name": "set-cookie", + "value": "vmidv1=40d8fd14-5ac3-4757-9e9c-efb106e82d3a;Expires=Tue, 15 Jun 2027 21:41:24 GMT;Domain=www.theverge.com;Path=/;SameSite=Lax;Secure" + }, + { + "name": "strict-transport-security", + "value": "max-age=31556952; preload" + }, + { + "name": "vary", + "value": "Accept-Encoding, X-Chorus-Unison-Testing, X-Chorus-Require-Privacy-Consent, X-Chorus-Restrict-In-Privacy-Consent-Region, Origin, X-Forwarded-Proto, Cookie, X-Chorus-Unison-Testing, X-Chorus-Require-Privacy-Consent, X-Chorus-Restrict-In-Privacy-Consent-Region" + }, + { + "name": "via", + "value": "1.1 varnish" + }, + { + "name": "x-cache", + "value": "HIT" + }, + { + "name": "x-cache-hits", + "value": "1" + }, + { + "name": "x-content-type-options", + "value": "nosniff" + }, + { + "name": "x-download-options", + "value": "noopen" + }, + { + "name": "x-frame-options", + "value": "SAMEORIGIN" + }, + { + "name": "x-permitted-cross-domain-policies", + "value": "none" + }, + { + "name": "x-request-id", + "value": "97363ad70e272e63641c0bb784fa06a01b848dfd" + }, + { + "name": "x-runtime", + "value": "0.257911" + }, + { + "name": "x-served-by", + "value": "cache-pao17436-PAO" + }, + { + "name": "x-timer", + "value": "S1655415684.075077,VS0,VE1" + }, + { + "name": "x-xss-protection", + "value": "1; mode=block" + } + ], + "content": { + "size": 14, + "mimeType": "text/html", + "compression": 0, + "text": "

hello

" + }, + "headersSize": 1742, + "bodySize": 48716, + "redirectURL": "", + "_transferSize": 48716 + }, + "cache": { + "beforeRequest": null, + "afterRequest": null + }, + "timings": { + "dns": 0.016, + "connect": 24.487, + "ssl": 17.406, + "send": 0, + "wait": 8.383, + "receive": -1 + }, + "pageref": "page@8f314969edc000996eb5c2ab22f0e6b3", + "serverIPAddress": "151.101.189.52", + "_serverPort": 443, + "_securityDetails": { + "protocol": "TLS 1.2", + "subjectName": "*.americanninjawarriornation.com", + "issuer": "GlobalSign Atlas R3 DV TLS CA 2022 Q1", + "validFrom": 1644853133, + "validTo": 1679153932 + } + } + ] + } +} \ No newline at end of file diff --git a/playwright/src/test/resources/har-sha1-main-response.txt b/playwright/src/test/resources/har-sha1-main-response.txt new file mode 100644 index 00000000..dbe9dba5 --- /dev/null +++ b/playwright/src/test/resources/har-sha1-main-response.txt @@ -0,0 +1 @@ +Hello, world \ No newline at end of file diff --git a/playwright/src/test/resources/har-sha1.har b/playwright/src/test/resources/har-sha1.har new file mode 100644 index 00000000..d918acd0 --- /dev/null +++ b/playwright/src/test/resources/har-sha1.har @@ -0,0 +1,95 @@ +{ + "log": { + "version": "1.2", + "creator": { + "name": "Playwright", + "version": "1.23.0-next" + }, + "browser": { + "name": "chromium", + "version": "103.0.5060.33" + }, + "pages": [ + { + "startedDateTime": "2022-06-10T04:27:32.125Z", + "id": "page@b17b177f1c2e66459db3dcbe44636ffd", + "title": "Hey", + "pageTimings": { + "onContentLoad": 70, + "onLoad": 70 + } + } + ], + "entries": [ + { + "_frameref": "frame@c7467fc0f1f86f09fc3b0d727a3862ea", + "_monotonicTime": 270572145.898, + "startedDateTime": "2022-06-10T04:27:32.146Z", + "time": 8.286, + "request": { + "method": "GET", + "url": "http://no.playwright/", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "Accept", + "value": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" + }, + { + "name": "Upgrade-Insecure-Requests", + "value": "1" + }, + { + "name": "User-Agent", + "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.33 Safari/537.36" + } + ], + "queryString": [], + "headersSize": 326, + "bodySize": 0 + }, + "response": { + "status": 200, + "statusText": "OK", + "httpVersion": "HTTP/1.1", + "cookies": [], + "headers": [ + { + "name": "content-length", + "value": "12" + }, + { + "name": "content-type", + "value": "text/html" + } + ], + "content": { + "size": 12, + "mimeType": "text/html", + "compression": 0, + "_file": "har-sha1-main-response.txt" + }, + "headersSize": 64, + "bodySize": 71, + "redirectURL": "", + "_transferSize": 71 + }, + "cache": { + "beforeRequest": null, + "afterRequest": null + }, + "timings": { + "dns": -1, + "connect": -1, + "ssl": -1, + "send": 0, + "wait": 8.286, + "receive": -1 + }, + "pageref": "page@b17b177f1c2e66459db3dcbe44636ffd", + "_securityDetails": {} + } + ] + } +} \ No newline at end of file diff --git a/scripts/CLI_VERSION b/scripts/CLI_VERSION index 0929d648..7ac6e08a 100644 --- a/scripts/CLI_VERSION +++ b/scripts/CLI_VERSION @@ -1 +1 @@ -1.23.0-beta-1655926399000 +1.23.0-beta-1656035897000