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 9dbf66ee..d90f3af9 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 @@ -376,6 +376,13 @@ class Method extends Element { .replace("String selector, ", "") .replace("(selector, ", "(") .replace("ElementHandle.", "")).toArray(String[]::new)); + + customSignature.put("Selectors.register", new String[] { + "default void register(String name, String script) { register(name, script, null); }", + "void register(String name, String script, RegisterOptions options);", + "default void register(String name, Path path) { register(name, path, null); }", + "void register(String name, Path path, RegisterOptions options);" + }); } Method(TypeDefinition parent, JsonObject jsonElement) { @@ -656,7 +663,7 @@ class Interface extends TypeDefinition { if ("Download".equals(jsonName)) { output.add("import java.io.InputStream;"); } - if (asList("Page", "Frame", "ElementHandle", "FileChooser", "ChromiumBrowser", "Download", "Route").contains(jsonName)) { + if (asList("Page", "Frame", "ElementHandle", "FileChooser", "ChromiumBrowser", "Download", "Route", "Selectors").contains(jsonName)) { output.add("import java.nio.file.Path;"); } output.add("import java.util.*;"); diff --git a/playwright/src/main/java/com/microsoft/playwright/Playwright.java b/playwright/src/main/java/com/microsoft/playwright/Playwright.java index 096d0bd9..2a09850d 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Playwright.java +++ b/playwright/src/main/java/com/microsoft/playwright/Playwright.java @@ -31,4 +31,6 @@ public interface Playwright { BrowserType webkit(); Map devices(); + + Selectors selectors(); } diff --git a/playwright/src/main/java/com/microsoft/playwright/Selectors.java b/playwright/src/main/java/com/microsoft/playwright/Selectors.java index a3fefd45..3fa79cda 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Selectors.java +++ b/playwright/src/main/java/com/microsoft/playwright/Selectors.java @@ -16,6 +16,7 @@ package com.microsoft.playwright; +import java.nio.file.Path; import java.util.*; public interface Selectors { @@ -27,9 +28,9 @@ public interface Selectors { return this; } } - default void register(String name, String script) { - register(name, script, null); - } + default void register(String name, String script) { register(name, script, null); } void register(String name, String script, RegisterOptions options); + default void register(String name, Path path) { register(name, path, null); } + void register(String name, Path path, RegisterOptions options); } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java b/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java index e441338e..57426e30 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java @@ -228,7 +228,7 @@ public class Connection { result = new Stream(parent, type, guid, initializer); break; case "Selectors": -// result = new Playwright(parent, type, guid, initializer); + result = new SelectorsImpl(parent, type, guid, initializer); break; case "Worker": result = new WorkerImpl(parent, type, guid, initializer); diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/ElementHandleImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/ElementHandleImpl.java index a0ec27d8..7563f68d 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/ElementHandleImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/ElementHandleImpl.java @@ -34,7 +34,7 @@ import java.util.List; import static com.microsoft.playwright.impl.Serialization.*; import static com.microsoft.playwright.impl.Utils.isFunctionBody; -class ElementHandleImpl extends JSHandleImpl implements ElementHandle { +public class ElementHandleImpl extends JSHandleImpl implements ElementHandle { ElementHandleImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) { super(parent, type, guid, initializer); } @@ -384,6 +384,16 @@ class ElementHandleImpl extends JSHandleImpl implements ElementHandle { return toDeferred(sendMessageAsync("waitForElementState", params).apply(json -> null)); } + public String createSelectorForTest(String name) { + JsonObject params = new JsonObject(); + params.addProperty("name", name); + JsonObject json = sendMessage("createSelectorForTest", params).getAsJsonObject(); + if (json.has("value")) { + return json.get("value").getAsString(); + } + return null; + } + private static String toProtocol(WaitForSelectorOptions.State state) { if (state == null) { state = WaitForSelectorOptions.State.VISIBLE; diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/PlaywrightImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/PlaywrightImpl.java index b312b75d..ff32aa9f 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/PlaywrightImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/PlaywrightImpl.java @@ -20,10 +20,7 @@ import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import com.microsoft.playwright.BrowserType; -import com.microsoft.playwright.DeviceDescriptor; -import com.microsoft.playwright.Playwright; -import com.microsoft.playwright.PlaywrightException; +import com.microsoft.playwright.*; import java.io.File; import java.io.IOException; @@ -51,6 +48,7 @@ public class PlaywrightImpl extends ChannelOwner implements Playwright { private final BrowserTypeImpl chromium; private final BrowserTypeImpl firefox; private final BrowserTypeImpl webkit; + private final Selectors selectors; private final Map devices = new HashMap<>(); public PlaywrightImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) { @@ -58,6 +56,7 @@ public class PlaywrightImpl extends ChannelOwner implements Playwright { chromium = parent.connection.getExistingObject(initializer.getAsJsonObject("chromium").get("guid").getAsString()); firefox = parent.connection.getExistingObject(initializer.getAsJsonObject("firefox").get("guid").getAsString()); webkit = parent.connection.getExistingObject(initializer.getAsJsonObject("webkit").get("guid").getAsString()); + selectors = parent.connection.getExistingObject(initializer.getAsJsonObject("selectors").get("guid").getAsString()); Gson gson = Serialization.gson(); for (JsonElement item : initializer.getAsJsonArray("deviceDescriptors")) { @@ -87,4 +86,9 @@ public class PlaywrightImpl extends ChannelOwner implements Playwright { public Map devices() { return devices; } + + @Override + public Selectors selectors() { + return selectors; + } } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/SelectorsImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/SelectorsImpl.java new file mode 100644 index 00000000..166b3c45 --- /dev/null +++ b/playwright/src/main/java/com/microsoft/playwright/impl/SelectorsImpl.java @@ -0,0 +1,56 @@ +/* + * 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.Selectors; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import static com.microsoft.playwright.impl.Serialization.gson; +import static java.nio.charset.StandardCharsets.UTF_8; + +class SelectorsImpl extends ChannelOwner implements Selectors { + SelectorsImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) { + super(parent, type, guid, initializer); + } + + @Override + public void register(String name, String script, RegisterOptions options) { + if (options == null) { + options = new RegisterOptions(); + } + JsonObject params = gson().toJsonTree(options).getAsJsonObject(); + params.addProperty("name", name); + params.addProperty("source", script); + sendMessage("register", params); + } + + @Override + public void register(String name, Path path, RegisterOptions options) { + byte[] buffer; + try { + buffer = Files.readAllBytes(path); + } catch (IOException e) { + throw new PlaywrightException("Failed to read selector from file: " + path, e); + } + register(name, new String(buffer, UTF_8), options); + } +} diff --git a/playwright/src/test/java/com/microsoft/playwright/TestNetworkResponse.java b/playwright/src/test/java/com/microsoft/playwright/TestNetworkResponse.java index fb1fe32c..8a030fab 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestNetworkResponse.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestNetworkResponse.java @@ -18,9 +18,10 @@ package com.microsoft.playwright; import org.junit.jupiter.api.Test; -import java.io.*; +import java.io.IOException; +import java.io.OutputStreamWriter; import java.nio.file.Files; -import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Arrays; import java.util.concurrent.Future; @@ -111,7 +112,7 @@ public class TestNetworkResponse extends TestBase { @Test void shouldReturnBody() throws IOException { Response response = page.navigate(server.PREFIX + "/pptr.png"); - byte[] expected = Files.readAllBytes(new File("src/test/resources/pptr.png").toPath()); + byte[] expected = Files.readAllBytes(Paths.get("src/test/resources/pptr.png")); assertTrue(Arrays.equals(expected, response.body())); } @@ -119,7 +120,7 @@ public class TestNetworkResponse extends TestBase { void shouldReturnBodyWithCompression() throws IOException { server.enableGzip("/pptr.png"); Response response = page.navigate(server.PREFIX + "/pptr.png"); - byte[] expected = Files.readAllBytes(new File("src/test/resources/pptr.png").toPath()); + byte[] expected = Files.readAllBytes(Paths.get("src/test/resources/pptr.png")); assertTrue(Arrays.equals(expected, response.body())); } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestRequestFulfill.java b/playwright/src/test/java/com/microsoft/playwright/TestRequestFulfill.java index 48d86934..0d3c54ed 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestRequestFulfill.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestRequestFulfill.java @@ -61,7 +61,7 @@ public class TestRequestFulfill extends TestBase { page.route("**/*", route -> { byte[] imageBuffer; try { - imageBuffer = Files.readAllBytes(new File("src/test/resources/pptr.png").toPath()); + imageBuffer = Files.readAllBytes(Paths.get("src/test/resources/pptr.png")); } catch (IOException e) { e.printStackTrace(); throw new RuntimeException(e); diff --git a/playwright/src/test/java/com/microsoft/playwright/TestSelectorsRegister.java b/playwright/src/test/java/com/microsoft/playwright/TestSelectorsRegister.java new file mode 100644 index 00000000..c7593688 --- /dev/null +++ b/playwright/src/test/java/com/microsoft/playwright/TestSelectorsRegister.java @@ -0,0 +1,150 @@ +/* + * 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.impl.ElementHandleImpl; +import org.junit.jupiter.api.Test; + +import java.nio.file.Paths; + +import static org.junit.jupiter.api.Assertions.*; + +public class TestSelectorsRegister extends TestBase { + @Test + void shouldWork() { + String selectorScript = "{\n" + + " create(root, target) {\n" + + " return target.nodeName;\n" + + " },\n" + + " query(root, selector) {\n" + + " return root.querySelector(selector);\n" + + " },\n" + + " queryAll(root, selector) {\n" + + " return Array.from(root.querySelectorAll(selector));\n" + + " }\n" + + "}"; + // Register one engine before creating context. + playwright.selectors().register("tag", selectorScript); + + BrowserContext context = browser.newContext(); + // Register another engine after creating context. + playwright.selectors().register("tag2", selectorScript); + + Page page = context.newPage(); + page.setContent("
"); + + assertEquals("DIV", ((ElementHandleImpl) page.querySelector("div")).createSelectorForTest("tag")); + assertEquals("DIV", page.evalOnSelector("tag=DIV", "e => e.nodeName")); + assertEquals("SPAN", page.evalOnSelector("tag=SPAN", "e => e.nodeName")); + assertEquals(2, page.evalOnSelectorAll("tag=DIV", "es => es.length")); + + assertEquals("DIV", ((ElementHandleImpl) page.querySelector("div")).createSelectorForTest("tag2")); + assertEquals("DIV", page.evalOnSelector("tag2=DIV", "e => e.nodeName")); + assertEquals("SPAN", page.evalOnSelector("tag2=SPAN", "e => e.nodeName")); + assertEquals(2, page.evalOnSelectorAll("tag2=DIV", "es => es.length")); + try { + // Selector names are case-sensitive. + page.querySelector("tAG=DIV"); + fail("did not throw"); + } catch (PlaywrightException e) { + assertTrue(e.getMessage().contains("Unknown engine \"tAG\" while parsing selector tAG=DIV")); + } + context.close(); + } + + @Test + void shouldWorkWithPath() { + playwright.selectors().register("foo", Paths.get("src/test/resources/sectionselectorengine.js")); + page.setContent("
"); + assertEquals("SECTION", page.evalOnSelector("foo=whatever", "e => e.nodeName")); + } + + @Test + void shouldWorkInMainAndIsolatedWorld() { + String createDummySelector = "{\n" + + " create(root, target) { },\n" + + " query(root, selector) {\n" + + " return window['__answer'];\n" + + " },\n" + + " queryAll(root, selector) {\n" + + " return [document.body, document.documentElement, window['__answer']];\n" + + " }\n" + + "}"; + playwright.selectors().register("main", createDummySelector); + playwright.selectors().register("isolated", createDummySelector, new Selectors.RegisterOptions().withContentScript(true)); + page.setContent("
"); + page.evaluate("() => window['__answer'] = document.querySelector('span')"); + // Works in main if asked. + assertEquals("SPAN", page.evalOnSelector("main=ignored", "e => e.nodeName")); + assertEquals("SPAN", page.evalOnSelector("css=div >> main=ignored", "e => e.nodeName")); + assertEquals(true, page.evalOnSelectorAll("main=ignored", "es => window['__answer'] !== undefined")); + assertEquals(3, page.evalOnSelectorAll("main=ignored", "es => es.filter(e => e).length")); + // Works in isolated by default. + assertNull(page.querySelector("isolated=ignored")); + assertNull(page.querySelector("css=div >> isolated=ignored")); + // $$eval always works in main, to avoid adopting nodes one by one. + assertEquals(true, page.evalOnSelectorAll("isolated=ignored", "es => window['__answer'] !== undefined")); + assertEquals(3, page.evalOnSelectorAll("isolated=ignored", "es => es.filter(e => e).length")); + // At least one engine in main forces all to be in main. + assertEquals("SPAN", page.evalOnSelector("main=ignored >> isolated=ignored", "e => e.nodeName")); + assertEquals("SPAN", page.evalOnSelector("isolated=ignored >> main=ignored", "e => e.nodeName")); + // Can be chained to css. + assertEquals("SECTION", page.evalOnSelector("main=ignored >> css=section", "e => e.nodeName")); + } + + @Test + void shouldHandleErrors() { + try { + page.querySelector("neverregister=ignored"); + fail("did not throw"); + } catch (PlaywrightException e) { + assertTrue(e.getMessage().contains("Unknown engine \"neverregister\" while parsing selector neverregister=ignored")); + } + String createDummySelector = "{\n" + + " create(root, target) {\n" + + " return target.nodeName;\n" + + " },\n" + + " query(root, selector) {\n" + + " return root.querySelector(\"dummy\");\n" + + " },\n" + + " queryAll(root, selector) {\n" + + " return Array.from(root.querySelectorAll(\"dummy\"));\n" + + " }\n" + + "}"; + try { + playwright.selectors().register("$", createDummySelector); + fail("did not throw"); + } catch (PlaywrightException e) { + assertTrue(e.getMessage().contains("Selector engine name may only contain [a-zA-Z0-9_] characters")); + } + // Selector names are case-sensitive. + playwright.selectors().register("dummy", createDummySelector); + playwright.selectors().register("duMMy", createDummySelector); + try { + playwright.selectors().register("dummy", createDummySelector); + fail("did not throw"); + } catch (PlaywrightException e) { + assertTrue(e.getMessage().contains("\"dummy\" selector engine has been already registered")); + } + try { + playwright.selectors().register("css", createDummySelector); + fail("did not throw"); + } catch (PlaywrightException e) { + assertTrue(e.getMessage().contains("\"css\" is a predefined selector engine")); + } + } +}