From e4a1753444efe888a54a58a7050194836f277f5e Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 26 Oct 2020 14:41:04 -0700 Subject: [PATCH] feat: context.*cookies (#42) --- .../playwright/tools/ApiGenerator.java | 16 +- .../com/microsoft/playwright/tools/Types.java | 7 +- .../microsoft/playwright/BrowserContext.java | 94 ++++++++- .../playwright/ChromiumCoverage.java | 38 +++- .../playwright/impl/BrowserContextImpl.java | 61 ++++-- .../TestBrowserContextClearCookies.java | 58 ++++++ .../playwright/TestBrowserContextCookies.java | 185 ++++++++++++++++++ 7 files changed, 438 insertions(+), 21 deletions(-) create mode 100644 playwright/src/test/java/com/microsoft/playwright/TestBrowserContextClearCookies.java create mode 100644 playwright/src/test/java/com/microsoft/playwright/TestBrowserContextCookies.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 5d27f259..0d62c204 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 @@ -77,9 +77,13 @@ class TypeRef extends Element { void createCustomType() { boolean isEnum = jsonName.contains("|\""); - boolean isClass = jsonName.replace("null|", "").equals("Object"); + boolean isClass = jsonName.replace("null|", "").equals("Object") + || jsonName.equals("Promise>"); // Use path to the corresponding method, param of field as the key. String parentPath = parent.jsonPath; + if (jsonName.equals("Array") && "BrowserContext.addCookies.cookies".equals(jsonPath)) { + isClass = true; + } Types.Mapping mapping = TypeDefinition.types.findForPath(parentPath); if (mapping == null) { if (isEnum) { @@ -266,6 +270,14 @@ class Method extends Element { "void unroute(Pattern url, BiConsumer handler);", "void unroute(Predicate url, BiConsumer handler);", }); + customSignature.put("BrowserContext.cookies", new String[]{ + "default List cookies() { return cookies((List) null); }", + "default List cookies(String url) { return cookies(Arrays.asList(url)); }", + "List cookies(List urls);", + }); + customSignature.put("BrowserContext.addCookies", new String[]{ + "void addCookies(List cookies);" + }); customSignature.put("FileChooser.setFiles", new String[]{ "default void setFiles(File file) { setFiles(file, null); }", "default void setFiles(File file, SetFilesOptions options) { setFiles(new File[]{ file }, options); }", @@ -706,6 +718,8 @@ class Interface extends TypeDefinition { break; } case "BrowserContext": { + output.add(offset + "enum SameSite { STRICT, LAX, NONE }"); + output.add(""); output.add(offset + "class HTTPCredentials {"); output.add(offset + " private final String username;"); output.add(offset + " private final String password;"); 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 acabe8a8..0f0073fb 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 @@ -208,7 +208,12 @@ class Types { add("Page.goto.options", "Object", "NavigateOptions"); add("Frame.goto.options", "Object", "NavigateOptions"); - add("BrowserContext.cookies.urls", "string|Array", "String"); + // The method has custom signatures + add("BrowserContext.cookies", "Promise>", "Cookie"); + add("BrowserContext.cookies.sameSite", "\"Lax\"|\"None\"|\"Strict\"", "SameSite", new Empty()); + add("BrowserContext.cookies.expires", "number", "long"); + add("BrowserContext.addCookies.cookies", "Array", "AddCookie"); + add("BrowserContext.addCookies.cookies.sameSite", "\"Lax\"|\"None\"|\"Strict\"", "SameSite", new Empty()); add("BrowserContext.route.url", "string|RegExp|function(URL):boolean", "String"); add("BrowserContext.unroute.url", "string|RegExp|function(URL):boolean", "String"); add("BrowserContext.waitForEvent.event", "string", "EventType", new Empty()); diff --git a/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java b/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java index 9da456c0..2e89d657 100644 --- a/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java +++ b/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java @@ -22,6 +22,8 @@ import java.util.function.Predicate; import java.util.regex.Pattern; public interface BrowserContext { + enum SameSite { STRICT, LAX, NONE } + class HTTPCredentials { private final String username; private final String password; @@ -59,6 +61,89 @@ public interface BrowserContext { void addListener(EventType type, Listener listener); void removeListener(EventType type, Listener listener); + class AddCookie { + public String name; + public String value; + public String url; + public String domain; + public String path; + public Integer expires; + public Boolean httpOnly; + public Boolean secure; + public SameSite sameSite; + + public AddCookie withName(String name) { + this.name = name; + return this; + } + public AddCookie withValue(String value) { + this.value = value; + return this; + } + public AddCookie withUrl(String url) { + this.url = url; + return this; + } + public AddCookie withDomain(String domain) { + this.domain = domain; + return this; + } + public AddCookie withPath(String path) { + this.path = path; + return this; + } + public AddCookie withExpires(Integer expires) { + this.expires = expires; + return this; + } + public AddCookie withHttpOnly(Boolean httpOnly) { + this.httpOnly = httpOnly; + return this; + } + public AddCookie withSecure(Boolean secure) { + this.secure = secure; + return this; + } + public AddCookie withSameSite(SameSite sameSite) { + this.sameSite = sameSite; + return this; + } + } + class Cookie { + private String name; + private String value; + private String domain; + private String path; + private long expires; + private boolean httpOnly; + private boolean secure; + private SameSite sameSite; + + public String name() { + return this.name; + } + public String value() { + return this.value; + } + public String domain() { + return this.domain; + } + public String path() { + return this.path; + } + public long expires() { + return this.expires; + } + public boolean httpOnly() { + return this.httpOnly; + } + public boolean secure() { + return this.secure; + } + public SameSite sameSite() { + return this.sameSite; + } + } class ExposeBindingOptions { public Boolean handle; @@ -94,7 +179,7 @@ public interface BrowserContext { } } void close(); - void addCookies(List cookies); + void addCookies(List cookies); default void addInitScript(String script) { addInitScript(script, null); } @@ -102,10 +187,9 @@ public interface BrowserContext { Browser browser(); void clearCookies(); void clearPermissions(); - default List cookies() { - return cookies(null); - } - List cookies(String urls); + default List cookies() { return cookies((List) null); } + default List cookies(String url) { return cookies(Arrays.asList(url)); } + List cookies(List urls); default void exposeBinding(String name, Page.Binding playwrightBinding) { exposeBinding(name, playwrightBinding, null); } diff --git a/playwright/src/main/java/com/microsoft/playwright/ChromiumCoverage.java b/playwright/src/main/java/com/microsoft/playwright/ChromiumCoverage.java index 064214cf..bc6b070e 100644 --- a/playwright/src/main/java/com/microsoft/playwright/ChromiumCoverage.java +++ b/playwright/src/main/java/com/microsoft/playwright/ChromiumCoverage.java @@ -40,6 +40,40 @@ public interface ChromiumCoverage { return this; } } + class ChromiumCoverageStopCSSCoverage { + private String url; + private String text; + private List ranges; + + public String url() { + return this.url; + } + public String text() { + return this.text; + } + public List ranges() { + return this.ranges; + } + } + class ChromiumCoverageStopJSCoverage { + private String url; + private String scriptId; + private String source; + private List functions; + + public String url() { + return this.url; + } + public String scriptId() { + return this.scriptId; + } + public String source() { + return this.source; + } + public List functions() { + return this.functions; + } + } default void startCSSCoverage() { startCSSCoverage(null); } @@ -48,7 +82,7 @@ public interface ChromiumCoverage { startJSCoverage(null); } void startJSCoverage(StartJSCoverageOptions options); - List stopCSSCoverage(); - List stopJSCoverage(); + ChromiumCoverageStopCSSCoverage stopCSSCoverage(); + ChromiumCoverageStopJSCoverage stopJSCoverage(); } 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 69c626f8..37756cb7 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java @@ -16,15 +16,15 @@ package com.microsoft.playwright.impl; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; +import com.google.gson.*; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; import com.microsoft.playwright.*; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletableFuture; +import java.io.IOException; +import java.util.*; import java.util.function.BiConsumer; import java.util.function.Predicate; import java.util.regex.Pattern; @@ -67,8 +67,10 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext { } @Override - public void addCookies(List cookies) { - + public void addCookies(List cookies) { + JsonObject params = new JsonObject(); + params.add("cookies", new Gson().toJsonTree(cookies)); + sendMessage("addCookies", params); } @Override @@ -89,17 +91,52 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext { @Override public void clearCookies() { - + sendMessage("clearCookies"); } @Override public void clearPermissions() { + sendMessage("clearPermissions"); + } + private static class SameSiteAdapter extends TypeAdapter { + @Override + public void write(JsonWriter out, SameSite value) throws IOException { + String stringValue; + switch (value) { + case STRICT: + stringValue = "Strict"; + break; + case LAX: + stringValue = "Lax"; + break; + case NONE: + stringValue = "None"; + break; + default: + throw new PlaywrightException("Unexpected value: " + value); + } + out.value(stringValue); + } + + @Override + public SameSite read(JsonReader in) throws IOException { + String value = in.nextString(); + return SameSite.valueOf(value.toUpperCase()); + } } @Override - public List cookies(String urls) { - return null; + public List cookies(List urls) { + JsonObject params = new JsonObject(); + if (urls == null) { + urls = Collections.emptyList(); + } + params.add("urls", new Gson().toJsonTree(urls)); + JsonObject json = sendMessage("cookies", params).getAsJsonObject(); + Gson gson = new GsonBuilder().registerTypeAdapter(SameSite.class, new SameSiteAdapter().nullSafe()).create(); + Cookie[] cookies = gson.fromJson(json.getAsJsonArray("cookies"), Cookie[].class); + return Arrays.asList(cookies); } @Override diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextClearCookies.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextClearCookies.java new file mode 100644 index 00000000..340df05d --- /dev/null +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextClearCookies.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.playwright; + +import org.junit.jupiter.api.Test; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class TestBrowserContextClearCookies extends TestBase { + @Test + void shouldClearCookies() { + page.navigate(server.EMPTY_PAGE); + context.addCookies(asList( + new BrowserContext.AddCookie().withUrl(server.EMPTY_PAGE).withName("cookie1").withValue("1"))); + assertEquals("cookie1=1", page.evaluate("document.cookie")); + context.clearCookies(); + assertEquals(emptyList(), context.cookies()); + page.reload(); + assertEquals("", page.evaluate("document.cookie")); + } + + @Test + void shouldIsolateCookiesWhenClearing() { + BrowserContext anotherContext = browser.newContext(); + context.addCookies(asList( + new BrowserContext.AddCookie().withUrl(server.EMPTY_PAGE).withName("page1cookie").withValue("page1value"))); + anotherContext.addCookies(asList( + new BrowserContext.AddCookie().withUrl(server.EMPTY_PAGE).withName("page2cookie").withValue("page2value"))); + + assertEquals(1, (context.cookies()).size()); + assertEquals(1, (anotherContext.cookies()).size()); + + context.clearCookies(); + assertEquals(0, (context.cookies()).size()); + assertEquals(1, (anotherContext.cookies()).size()); + + anotherContext.clearCookies(); + assertEquals(0, (context.cookies()).size()); + assertEquals(0, (anotherContext.cookies()).size()); + anotherContext.close(); + } +} diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextCookies.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextCookies.java new file mode 100644 index 00000000..0993ecd8 --- /dev/null +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextCookies.java @@ -0,0 +1,185 @@ +/* + * 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.JsonParser; +import org.junit.jupiter.api.Test; + +import java.util.Comparator; +import java.util.List; + +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class TestBrowserContextCookies extends TestBase { + private static void assertJsonEquals(String expected, Object actual) { + JsonElement actualJson = JsonParser.parseString(new Gson().toJson(actual)); + assertEquals(JsonParser.parseString(expected), actualJson); + } + + @Test + void shouldGetACookie() { + page.navigate(server.EMPTY_PAGE); + Object documentCookie = page.evaluate("() => {\n" + + " document.cookie = 'username=John Doe';\n" + + " return document.cookie;\n" + + "}"); + assertEquals("username=John Doe", documentCookie); + List cookies = context.cookies(); + assertJsonEquals("[{\n" + + " name: 'username',\n" + + " value: 'John Doe',\n" + + " domain: 'localhost',\n" + + " path: '/',\n" + + " expires: -1,\n" + + " httpOnly: false,\n" + + " secure: false,\n" + + " sameSite: 'NONE'\n" + + " }]", cookies); + } + + + @Test + void shouldGetANonSessionCookie() { + page.navigate(server.EMPTY_PAGE); + // @see https://en.wikipedia.org/wiki/Year_2038_problem + Object documentCookie = page.evaluate("() => {\n" + + " const date= new Date('1/1/2038');\n" + + " document.cookie = `username=John Doe;expires=${date.toUTCString()}`;\n" + + " return document.cookie;\n" + + " }"); + assertEquals("username=John Doe", documentCookie); + int timestamp = (Integer) page.evaluate("+(new Date('1/1/2038'))/1000"); + BrowserContext.Cookie cookie = context.cookies().get(0); + assertEquals("username", cookie.name()); + assertEquals("John Doe", cookie.value()); + assertEquals("localhost", cookie.domain()); + assertEquals("/", cookie.path()); + assertEquals(timestamp, cookie.expires()); + assertEquals(false, cookie.httpOnly()); + assertEquals(false, cookie.secure()); + assertEquals(BrowserContext.SameSite.NONE, cookie.sameSite()); + } + + @Test + void shouldProperlyReportHttpOnlyCookie() { + server.setRoute("/empty.html", exchange -> { + exchange.getResponseHeaders().add("Set-Cookie", "name=value;HttpOnly; Path=/"); + exchange.sendResponseHeaders(200, 0); + exchange.getResponseBody().close(); + }); + page.navigate(server.EMPTY_PAGE); + List cookies = context.cookies(); + assertEquals(1, cookies.size()); + assertTrue(cookies.get(0).httpOnly()); + } + + @Test + void shouldProperlyReportStrictSameSiteCookie() { +// TODO: test.fail(browserName === "webkit" && platform === "win32"); + server.setRoute("/empty.html", exchange -> { + exchange.getResponseHeaders().add("Set-Cookie", "name=value;SameSite=Strict"); + exchange.sendResponseHeaders(200, 0); + exchange.getResponseBody().close(); + }); + page.navigate(server.EMPTY_PAGE); + List cookies = context.cookies(); + assertEquals(1, cookies.size()); + assertEquals(BrowserContext.SameSite.STRICT, cookies.get(0).sameSite()); + } + + @Test + void shouldProperlyReportLaxSameSiteCookie() { +// TODO: test.fail(browserName === "webkit" && platform === "win32"); + server.setRoute("/empty.html", exchange -> { + exchange.getResponseHeaders().add("Set-Cookie", "name=value;SameSite=Lax"); + exchange.sendResponseHeaders(200, 0); + exchange.getResponseBody().close(); + }); + page.navigate(server.EMPTY_PAGE); + List cookies = context.cookies(); + assertEquals(1, cookies.size()); + assertEquals(BrowserContext.SameSite.LAX, cookies.get(0).sameSite()); + } + + @Test + void shouldGetMultipleCookies() { + page.navigate(server.EMPTY_PAGE); + Object documentCookie = page.evaluate("() => {\n" + + " document.cookie = 'username=John Doe';\n" + + " document.cookie = 'password=1234';\n" + + " return document.cookie.split('; ').sort().join('; ');\n" + + "}"); + List cookies = context.cookies(); + cookies.sort(Comparator.comparing(BrowserContext.Cookie::name)); + assertEquals("password=1234; username=John Doe", documentCookie); + assertJsonEquals("[\n" + + " {\n" + + " name: 'password',\n" + + " value: '1234',\n" + + " domain: 'localhost',\n" + + " path: '/',\n" + + " expires: -1,\n" + + " httpOnly: false,\n" + + " secure: false,\n" + + " sameSite: 'NONE'\n" + + " },\n" + + " {\n" + + " name: 'username',\n" + + " value: 'John Doe',\n" + + " domain: 'localhost',\n" + + " path: '/',\n" + + " expires: -1,\n" + + " httpOnly: false,\n" + + " secure: false,\n" + + " sameSite: 'NONE'\n" + + " }\n" + + "]", cookies); + } + + @Test + void shouldGetCookiesFromMultipleUrls() { + context.addCookies(asList( + new BrowserContext.AddCookie().withUrl("https://foo.com").withName("doggo").withValue("woofs"), + new BrowserContext.AddCookie().withUrl("https://bar.com").withName("catto").withValue("purrs"), + new BrowserContext.AddCookie().withUrl("https://baz.com").withName("birdo").withValue("tweets"))); + List cookies = context.cookies(asList("https://foo.com", "https://baz.com")); + cookies.sort(Comparator.comparing(BrowserContext.Cookie::name)); + assertJsonEquals("[{\n" + + " name: 'birdo',\n" + + " value: 'tweets',\n" + + " domain: 'baz.com',\n" + + " path: '/',\n" + + " expires: -1,\n" + + " httpOnly: false,\n" + + " secure: true,\n" + + " sameSite: 'NONE'\n" + + "}, {\n" + + " name: 'doggo',\n" + + " value: 'woofs',\n" + + " domain: 'foo.com',\n" + + " path: '/',\n" + + " expires: -1,\n" + + " httpOnly: false,\n" + + " secure: true,\n" + + " sameSite: 'NONE'\n" + + "}]", cookies); + } +}