feat: implement routeFromHAR (#960)

This commit is contained in:
Yury Semikhatsky 2022-06-24 13:44:57 -07:00 committed by GitHub
parent fdec32c650
commit efb281e016
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1875 additions and 22 deletions

View File

@ -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}.
*/

View File

@ -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}.
*/

View File

@ -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<Route> 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<Route> handler, RouteOptions options) {

View File

@ -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();

View File

@ -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<String, String> 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);
}
}

View File

@ -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);
}

View File

@ -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<Route> handler, RouteOptions options) {

View File

@ -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!");

View File

@ -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<String, String> fromNameValues(JsonArray array) {
Map<String, String> 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<String> parseStringList(JsonArray array) {
List<String> result = new ArrayList<>();
for (JsonElement e : array) {

View File

@ -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
}

View File

@ -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
}

View File

@ -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("<script src='./script.js'></script><div>hello</div>"));
});
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("<script src='./script.js'></script><div>hello</div>"));
});
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("<script src='./script.js'></script><div>hello</div>"));
});
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("<a href='https://theverge.com/'>click me</a>");
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("<html><head></head><body>Hello, world</body></html>", 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<String> values = exchange.getRequestHeaders().get("baz");
try (OutputStreamWriter writer = new OutputStreamWriter(exchange.getResponseBody())) {
writer.write(values == null ? "<no header>" : 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"));
}
}
}

View File

@ -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<String, byte[]> entries = parseTrace(trace);
Map<String, byte[]> entries = parseZip(trace);
Map<String, byte[]> sources = entries.entrySet().stream().filter(e -> e.getKey().endsWith(".txt")).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
assertEquals(1, sources.size());

View File

@ -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<String, byte[]> 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<String, byte[]> 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);
}
}

View File

@ -113,7 +113,7 @@ public class TestTracing extends TestBase {
Path trace = tmpDir.resolve("trace1.zip");
context.tracing().stop(new Tracing.StopOptions().setPath(trace));
Map<String, byte[]> entries = Utils.parseTrace(trace);
Map<String, byte[]> entries = Utils.parseZip(trace);
Map<String, byte[]> sources = entries.entrySet().stream().filter(e -> e.getKey().endsWith(".txt")).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
assertEquals(1, sources.size());

View File

@ -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<String, byte[]> parseTrace(Path trace) throws IOException {
static Map<String, byte[]> parseZip(Path trace) throws IOException {
Map<String, byte[]> 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<String, byte[]> extractZip(Path zipPath, Path toDir) throws IOException {
Map<String, byte[]> 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();

View File

@ -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": "<title>Hey</title><link rel='stylesheet' href='./style.css'><script src='./script.js'></script><div>hello</div>"
},
"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": {}
}
]
}
}

View File

@ -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": "<https://concertads-configs.vox-cdn.com/sbn/verge/config.json>; 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": "<h1>hello</h1>"
},
"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
}
}
]
}
}

View File

@ -0,0 +1 @@
Hello, world

View File

@ -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": {}
}
]
}
}

View File

@ -1 +1 @@
1.23.0-beta-1655926399000
1.23.0-beta-1656035897000