From 975be31e637446b85acaa68dd7046c38b23aaef6 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Wed, 2 Dec 2020 16:05:36 -0800 Subject: [PATCH] feat(har): support har logging (#84) --- .../playwright/tools/ApiGenerator.java | 2 +- .../com/microsoft/playwright/tools/Types.java | 3 + .../com/microsoft/playwright/Browser.java | 5 +- .../com/microsoft/playwright/BrowserType.java | 9 +- .../playwright/impl/BrowserTypeImpl.java | 6 +- .../TestDefaultBrowserContext2.java | 18 +- .../com/microsoft/playwright/TestHar.java | 178 ++++++++++++++++++ 7 files changed, 203 insertions(+), 18 deletions(-) create mode 100644 playwright/src/test/java/com/microsoft/playwright/TestHar.java diff --git a/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java b/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java index b4626dc8..24696202 100644 --- a/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java +++ b/api-generator/src/main/java/com/microsoft/playwright/tools/ApiGenerator.java @@ -726,7 +726,7 @@ class Interface extends TypeDefinition { if ("Download".equals(jsonName)) { output.add("import java.io.InputStream;"); } - if (asList("Page", "Frame", "ElementHandle", "FileChooser", "ChromiumBrowser", "Download", "Route", "Selectors").contains(jsonName)) { + if (asList("Page", "Frame", "ElementHandle", "FileChooser", "Browser", "BrowserType", "Download", "Route", "Selectors").contains(jsonName)) { output.add("import java.nio.file.Path;"); } output.add("import java.util.*;"); diff --git a/api-generator/src/main/java/com/microsoft/playwright/tools/Types.java b/api-generator/src/main/java/com/microsoft/playwright/tools/Types.java index 0a4f6e62..8cd35dd7 100644 --- a/api-generator/src/main/java/com/microsoft/playwright/tools/Types.java +++ b/api-generator/src/main/java/com/microsoft/playwright/tools/Types.java @@ -107,6 +107,9 @@ class Types { add("ElementHandle.screenshot.options.path", "string", "Path"); add("Route.fulfill.response.path", "string", "Path"); add("Route.fulfill.response.status", "number", "int"); + add("Browser.newContext.options.recordHar.path", "string", "Path"); + add("BrowserType.launchPersistentContext.options.recordHar.path", "string", "Path"); + add("BrowserType.launchPersistentContext.userDataDir", "string", "Path"); add("ChromiumBrowser.startTracing.options.path", "string", "Path"); // Route diff --git a/playwright/src/main/java/com/microsoft/playwright/Browser.java b/playwright/src/main/java/com/microsoft/playwright/Browser.java index e29608ab..3c7fe760 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Browser.java +++ b/playwright/src/main/java/com/microsoft/playwright/Browser.java @@ -16,6 +16,7 @@ package com.microsoft.playwright; +import java.nio.file.Path; import java.util.*; /** @@ -104,7 +105,7 @@ public interface Browser { /** * Path on the filesystem to write the HAR file to. */ - public String path; + public Path path; RecordHar() { } @@ -116,7 +117,7 @@ public interface Browser { this.omitContent = omitContent; return this; } - public RecordHar withPath(String path) { + public RecordHar withPath(Path path) { this.path = path; return this; } diff --git a/playwright/src/main/java/com/microsoft/playwright/BrowserType.java b/playwright/src/main/java/com/microsoft/playwright/BrowserType.java index 94c9ea9c..5331ac28 100644 --- a/playwright/src/main/java/com/microsoft/playwright/BrowserType.java +++ b/playwright/src/main/java/com/microsoft/playwright/BrowserType.java @@ -16,6 +16,7 @@ package com.microsoft.playwright; +import java.nio.file.Path; import java.util.*; /** @@ -307,7 +308,7 @@ public interface BrowserType { /** * Path on the filesystem to write the HAR file to. */ - public String path; + public Path path; RecordHar() { } @@ -319,7 +320,7 @@ public interface BrowserType { this.omitContent = omitContent; return this; } - public RecordHar withPath(String path) { + public RecordHar withPath(Path path) { this.path = path; return this; } @@ -799,7 +800,7 @@ public interface BrowserType { * @return Promise which resolves to browser instance. */ Browser launch(LaunchOptions options); - default BrowserContext launchPersistentContext(String userDataDir) { + default BrowserContext launchPersistentContext(Path userDataDir) { return launchPersistentContext(userDataDir, null); } /** @@ -808,7 +809,7 @@ public interface BrowserType { * @param options Set of configurable options to set on the browser. Can have the following fields: * @return Promise that resolves to the persistent browser context instance. */ - BrowserContext launchPersistentContext(String userDataDir, LaunchPersistentContextOptions options); + BrowserContext launchPersistentContext(Path userDataDir, LaunchPersistentContextOptions options); /** * Returns browser name. For example: {@code 'chromium'}, {@code 'webkit'} or {@code 'firefox'}. */ diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserTypeImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserTypeImpl.java index f08ef2f5..a1e1779a 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserTypeImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserTypeImpl.java @@ -21,6 +21,8 @@ import com.google.gson.JsonObject; import com.microsoft.playwright.BrowserContext; import com.microsoft.playwright.BrowserType; +import java.nio.file.Path; + import static com.microsoft.playwright.impl.Serialization.gson; class BrowserTypeImpl extends ChannelOwner implements BrowserType { @@ -44,7 +46,7 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType { @Override - public BrowserContext launchPersistentContext(String userDataDir, LaunchPersistentContextOptions options) { + public BrowserContext launchPersistentContext(Path userDataDir, LaunchPersistentContextOptions options) { if (options == null) { options = new LaunchPersistentContextOptions(); } @@ -53,7 +55,7 @@ class BrowserTypeImpl extends ChannelOwner implements BrowserType { params.remove("extraHTTPHeaders"); params.add("extraHTTPHeaders", Serialization.toProtocol(options.extraHTTPHeaders)); } - params.addProperty("userDataDir", userDataDir); + params.addProperty("userDataDir", userDataDir.toString()); JsonObject json = sendMessage("launchPersistentContext", params).getAsJsonObject(); return connection.getExistingObject(json.getAsJsonObject("context").get("guid").getAsString()); } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestDefaultBrowserContext2.java b/playwright/src/test/java/com/microsoft/playwright/TestDefaultBrowserContext2.java index 1356827f..2069ccb5 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestDefaultBrowserContext2.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestDefaultBrowserContext2.java @@ -42,7 +42,7 @@ public class TestDefaultBrowserContext2 extends TestBase { throw new RuntimeException(e); } assertNull(persistentContext); - persistentContext = browserType.launchPersistentContext(userDataDir.toString(), options); + persistentContext = browserType.launchPersistentContext(userDataDir, options); return persistentContext.pages().get(0); } @@ -118,7 +118,7 @@ public class TestDefaultBrowserContext2 extends TestBase { void shouldAcceptUserDataDir() throws IOException { // TODO: test.flaky(browserName === "chromium"); Path userDataDir = Files.createTempDirectory("user-data-dir-"); - BrowserContext context = browserType.launchPersistentContext(userDataDir.toString()); + BrowserContext context = browserType.launchPersistentContext(userDataDir); assertTrue(userDataDir.toFile().listFiles().length > 0); context.close(); assertTrue(userDataDir.toFile().listFiles().length > 0); @@ -129,20 +129,20 @@ public class TestDefaultBrowserContext2 extends TestBase { // TODO: test.slow(); Path userDataDir = Files.createTempDirectory("user-data-dir-"); BrowserType.LaunchPersistentContextOptions browserOptions = null; - BrowserContext browserContext = browserType.launchPersistentContext(userDataDir.toString(), browserOptions); + BrowserContext browserContext = browserType.launchPersistentContext(userDataDir, browserOptions); Page page = browserContext.newPage(); page.navigate(server.EMPTY_PAGE); page.evaluate("() => localStorage.hey = 'hello'"); browserContext.close(); - BrowserContext browserContext2 = browserType.launchPersistentContext(userDataDir.toString(), browserOptions); + BrowserContext browserContext2 = browserType.launchPersistentContext(userDataDir, browserOptions); Page page2 = browserContext2.newPage(); page2.navigate(server.EMPTY_PAGE); assertEquals("hello", page2.evaluate("localStorage.hey")); browserContext2.close(); Path userDataDir2 = Files.createTempDirectory("user-data-dir-"); - BrowserContext browserContext3 = browserType.launchPersistentContext(userDataDir2.toString(), browserOptions); + BrowserContext browserContext3 = browserType.launchPersistentContext(userDataDir2, browserOptions); Page page3 = browserContext3.newPage(); page3.navigate(server.EMPTY_PAGE); assertNotEquals("hello", page3.evaluate("localStorage.hey")); @@ -154,7 +154,7 @@ public class TestDefaultBrowserContext2 extends TestBase { // TODO: test.flaky(browserName === "chromium"); Path userDataDir = Files.createTempDirectory("user-data-dir-"); BrowserType.LaunchPersistentContextOptions browserOptions = null; - BrowserContext browserContext = browserType.launchPersistentContext(userDataDir.toString(), browserOptions); + BrowserContext browserContext = browserType.launchPersistentContext(userDataDir, browserOptions); Page page = browserContext.newPage(); page.navigate(server.EMPTY_PAGE); Object documentCookie = page.evaluate("() => {\n" + @@ -164,14 +164,14 @@ public class TestDefaultBrowserContext2 extends TestBase { assertEquals("doSomethingOnlyOnce=true", documentCookie); browserContext.close(); - BrowserContext browserContext2 = browserType.launchPersistentContext(userDataDir.toString(), browserOptions); + BrowserContext browserContext2 = browserType.launchPersistentContext(userDataDir, browserOptions); Page page2 = browserContext2.newPage(); page2.navigate(server.EMPTY_PAGE); assertEquals("doSomethingOnlyOnce=true", page2.evaluate("() => document.cookie")); browserContext2.close(); Path userDataDir2 = Files.createTempDirectory("user-data-dir-"); - BrowserContext browserContext3 = browserType.launchPersistentContext(userDataDir2.toString(), browserOptions); + BrowserContext browserContext3 = browserType.launchPersistentContext(userDataDir2, browserOptions); Page page3 = browserContext3.newPage(); page3.navigate(server.EMPTY_PAGE); assertNotEquals("doSomethingOnlyOnce=true", page3.evaluate("() => document.cookie")); @@ -192,7 +192,7 @@ public class TestDefaultBrowserContext2 extends TestBase { .withArgs(asList(server.EMPTY_PAGE)); Path userDataDir = Files.createTempDirectory("user-data-dir-"); try { - browserType.launchPersistentContext(userDataDir.toString(), options); + browserType.launchPersistentContext(userDataDir, options); fail("did not throw"); } catch (PlaywrightException e) { assertTrue(e.getMessage().contains("can not specify page")); diff --git a/playwright/src/test/java/com/microsoft/playwright/TestHar.java b/playwright/src/test/java/com/microsoft/playwright/TestHar.java new file mode 100644 index 00000000..19bef961 --- /dev/null +++ b/playwright/src/test/java/com/microsoft/playwright/TestHar.java @@ -0,0 +1,178 @@ +/* + * 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.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.stream.JsonReader; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.*; + +public class TestHar extends TestBase { + private PageWithHar pageWithHar; + + private class PageWithHar { + final Path harFile; + final BrowserContext context; + final Page page; + + PageWithHar() throws IOException { + harFile = Files.createTempFile("test-", ".har"); + context = browser.newContext(new Browser.NewContextOptions().setRecordHar() + .withPath(harFile).done().withIgnoreHTTPSErrors(true)); + page = context.newPage(); + } + + JsonObject log() throws FileNotFoundException { + context.close(); + return new Gson().fromJson(new FileReader(harFile.toFile()), JsonObject.class).getAsJsonObject("log"); + } + + void dispose() throws IOException { + context.close(); + Files.deleteIfExists(harFile); + } + } + + @BeforeEach + void createPageWithHar() throws IOException { + pageWithHar = new PageWithHar(); + } + + @AfterEach + void deletePageWithHar() throws IOException { + pageWithHar.dispose(); + } + + @Test + void shouldThrowWithoutPath() { + try { + browser.newContext(new Browser.NewContextOptions().setRecordHar().done()); + fail("did not throw"); + } catch (PlaywrightException e) { + assertTrue(e.getMessage().contains("recordHar.path: expected string, got undefined")); + } + } + + @Test + void shouldHaveVersionAndCreator() throws FileNotFoundException { + pageWithHar.page.navigate(server.EMPTY_PAGE); + JsonObject log = pageWithHar.log(); + assertEquals("1.2", log.get("version").getAsString()); + assertEquals("Playwright", log.getAsJsonObject("creator").get("name").getAsString()); + } + + @Test + void shouldHaveBrowser() throws FileNotFoundException { + pageWithHar.page.navigate(server.EMPTY_PAGE); + JsonObject log = pageWithHar.log(); + assertEquals(browserType.name(), log.getAsJsonObject("browser").get("name").getAsString().toLowerCase()); + assertEquals(browser.version(), log.getAsJsonObject("browser").get("version").getAsString()); + } + + @Test + void shouldHavePages() throws FileNotFoundException { + // For data: load comes before domcontentloaded... + Deferred loadEvent = pageWithHar.page.waitForLoadState(Page.LoadState.DOMCONTENTLOADED); + pageWithHar.page.navigate("data:text/html,Hello"); + loadEvent.get(); + JsonObject log = pageWithHar.log(); + + assertEquals(1, log.getAsJsonArray("pages").size()); + JsonObject pageEntry = log.getAsJsonArray("pages").get(0).getAsJsonObject(); + assertEquals("page_0", pageEntry.get("id").getAsString()); + assertEquals("Hello", pageEntry.get("title").getAsString()); +// expect(new Date(pageEntry.startedDateTime).valueOf()).toBeGreaterThan(Date.now() - 3600 * 1000); + assertTrue(pageEntry.getAsJsonObject("pageTimings").get("onContentLoad").getAsDouble() > 0); + assertTrue(pageEntry.getAsJsonObject("pageTimings").get("onLoad").getAsDouble() > 0); + } + + @Test + void shouldHavePagesInPersistentContext() throws IOException { + Path harPath = pageWithHar.harFile; + Path userDataDir = Files.createTempDirectory("user-data-dir-"); + BrowserContext context = browserType.launchPersistentContext(userDataDir, + new BrowserType.LaunchPersistentContextOptions() + .setRecordHar().withPath(harPath).done().withIgnoreHTTPSErrors(true)); + Page page = context.pages().get(0); + + Deferred loadEvent = page.waitForLoadState(Page.LoadState.DOMCONTENTLOADED); + page.navigate("data:text/html,Hello"); + loadEvent.get(); + context.close(); + JsonObject log = new Gson().fromJson(new FileReader(harPath.toFile()), JsonObject.class).getAsJsonObject("log"); + + assertEquals(1, log.getAsJsonArray("pages").size()); + JsonObject pageEntry = log.getAsJsonArray("pages").get(0).getAsJsonObject(); + assertEquals("page_0", pageEntry.get("id").getAsString()); + assertEquals("Hello", pageEntry.get("title").getAsString()); + + Files.deleteIfExists(harPath); + } + + @Test + void shouldIncludeRequest() throws FileNotFoundException { + pageWithHar.page.navigate(server.EMPTY_PAGE); + JsonObject log = pageWithHar.log(); + assertEquals(1, log.getAsJsonArray("entries").size()); + JsonObject entry = log.getAsJsonArray("entries").get(0).getAsJsonObject(); + assertEquals("page_0", entry.get("pageref").getAsString()); + assertEquals(server.EMPTY_PAGE, entry.getAsJsonObject("request").get("url").getAsString()); + assertEquals("GET", entry.getAsJsonObject("request").get("method").getAsString()); + assertEquals("HTTP/1.1", entry.getAsJsonObject("request").get("httpVersion").getAsString()); + assertTrue(entry.getAsJsonObject("request").get("headers").getAsJsonArray().size() > 1); + boolean foundUserAgentHeader = false; + for (JsonElement item : entry.getAsJsonObject("request").get("headers").getAsJsonArray()) { + if ("user-agent".equals(item.getAsJsonObject().get("name").getAsString().toLowerCase())) { + foundUserAgentHeader = true; + break; + } + } + assertTrue(foundUserAgentHeader); + } + + @Test + void shouldIncludeResponse() throws FileNotFoundException { + pageWithHar.page.navigate(server.EMPTY_PAGE); + JsonObject log = pageWithHar.log(); + JsonObject entry = log.getAsJsonArray("entries").get(0).getAsJsonObject(); + assertEquals(200, entry.getAsJsonObject("response").get("status").getAsInt()); + assertEquals("OK", entry.getAsJsonObject("response").get("statusText").getAsString()); + assertEquals("HTTP/1.1", entry.getAsJsonObject("response").get("httpVersion").getAsString()); + assertTrue(entry.getAsJsonObject("response").get("headers").getAsJsonArray().size() > 1); + + boolean foundUserContentType = false; + for (JsonElement item : entry.getAsJsonObject("response").get("headers").getAsJsonArray()) { + if ("content-type".equals(item.getAsJsonObject().get("name").getAsString().toLowerCase())) { + foundUserContentType = true; + assertEquals("text/html", item.getAsJsonObject().get("value").getAsString()); + break; + } + } + assertTrue(foundUserContentType); + } +}