From b9a7ddbc0f7dabd7f3d0fdc35fe2ed6e3915590e Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Thu, 18 May 2023 09:59:53 -0700 Subject: [PATCH] feat: roll 1.34 beta driver, implement context events --- README.md | 4 +- .../microsoft/playwright/BrowserContext.java | 97 +++++++++++ .../microsoft/playwright/ConsoleMessage.java | 6 + .../java/com/microsoft/playwright/Dialog.java | 6 + .../com/microsoft/playwright/Locator.java | 14 ++ .../java/com/microsoft/playwright/Page.java | 17 +- .../playwright/impl/BrowserContextImpl.java | 75 ++++++++- .../playwright/impl/ConsoleMessageImpl.java | 14 ++ .../microsoft/playwright/impl/DialogImpl.java | 13 ++ .../playwright/impl/LocatorImpl.java | 8 + .../microsoft/playwright/impl/PageImpl.java | 23 +-- .../microsoft/playwright/impl/UrlMatcher.java | 5 + .../playwright/TestBrowserContextEvents.java | 159 ++++++++++++++++++ .../playwright/TestPageLocatorQuery.java | 13 ++ .../microsoft/playwright/TestPageRoute.java | 4 +- .../playwright/TestSelectorsMisc.java | 11 ++ scripts/CLI_VERSION | 2 +- 17 files changed, 438 insertions(+), 33 deletions(-) create mode 100644 playwright/src/test/java/com/microsoft/playwright/TestBrowserContextEvents.java diff --git a/README.md b/README.md index 105c09fc8..17b61b569 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,9 @@ Playwright is a Java library to automate [Chromium](https://www.chromium.org/Hom | | Linux | macOS | Windows | | :--- | :---: | :---: | :---: | -| Chromium 113.0.5672.53 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Chromium 114.0.5735.35 | :white_check_mark: | :white_check_mark: | :white_check_mark: | | WebKit 16.4 | ✅ | ✅ | ✅ | -| Firefox 112.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Firefox 113.0 | :white_check_mark: | :white_check_mark: | :white_check_mark: | Headless execution is supported for all the browsers on all platforms. Check out [system requirements](https://playwright.dev/java/docs/next/intro/#system-requirements) for details. diff --git a/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java b/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java index 2079e376c..51886ae47 100644 --- a/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java +++ b/playwright/src/main/java/com/microsoft/playwright/BrowserContext.java @@ -58,6 +58,51 @@ public interface BrowserContext extends AutoCloseable { */ void offClose(Consumer handler); + /** + * Emitted when JavaScript within the page calls one of console API methods, e.g. {@code console.log} or {@code + * console.dir}. Also emitted if the page throws an error or a warning. + * + *

The arguments passed into {@code console.log} and the page are available on the {@code ConsoleMessage} event handler + * argument. + * + *

**Usage** + *

{@code
+   * context.onConsoleMessage(msg -> {
+   *   for (int i = 0; i < msg.args().size(); ++i)
+   *     System.out.println(i + ": " + msg.args().get(i).jsonValue());
+   * });
+   * page.evaluate("() => console.log('hello', 5, { foo: 'bar' })");
+   * }
+ */ + void onConsoleMessage(Consumer handler); + /** + * Removes handler that was previously added with {@link #onConsoleMessage onConsoleMessage(handler)}. + */ + void offConsoleMessage(Consumer handler); + + /** + * Emitted when a JavaScript dialog appears, such as {@code alert}, {@code prompt}, {@code confirm} or {@code + * beforeunload}. Listener **must** either {@link Dialog#accept Dialog.accept()} or {@link Dialog#dismiss Dialog.dismiss()} + * the dialog - otherwise the page will freeze waiting for the + * dialog, and actions like click will never finish. + * + *

**Usage** + *

{@code
+   * context.onDialog(dialog -> {
+   *   dialog.accept();
+   * });
+   * }
+ * + *

NOTE: When no {@link Page#onDialog Page.onDialog()} or {@link BrowserContext#onDialog BrowserContext.onDialog()} listeners are + * present, all dialogs are automatically dismissed. + */ + void onDialog(Consumer

handler); + /** + * Removes handler that was previously added with {@link #onDialog onDialog(handler)}. + */ + void offDialog(Consumer handler); + /** * The event is emitted when a new Page is created in the BrowserContext. The page may still be loading. The event will * also fire for popup pages. See also {@link Page#onPopup Page.onPopup()} to receive events about popups relevant to a @@ -295,6 +340,33 @@ public WaitForConditionOptions setTimeout(double timeout) { return this; } } + class WaitForConsoleMessageOptions { + /** + * Receives the {@code ConsoleMessage} object and resolves to truthy value when the waiting should resolve. + */ + public Predicate predicate; + /** + * Maximum time to wait for in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The + * default value can be changed by using the {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()}. + */ + public Double timeout; + + /** + * Receives the {@code ConsoleMessage} object and resolves to truthy value when the waiting should resolve. + */ + public WaitForConsoleMessageOptions setPredicate(Predicate predicate) { + this.predicate = predicate; + return this; + } + /** + * Maximum time to wait for in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The + * default value can be changed by using the {@link BrowserContext#setDefaultTimeout BrowserContext.setDefaultTimeout()}. + */ + public WaitForConsoleMessageOptions setTimeout(double timeout) { + this.timeout = timeout; + return this; + } + } class WaitForPageOptions { /** * Receives the {@code Page} object and resolves to truthy value when the waiting should resolve. @@ -331,6 +403,9 @@ public WaitForPageOptions setTimeout(double timeout) { * browserContext.addCookies(Arrays.asList(cookieObject1, cookieObject2)); * } * + * @param cookies Adds cookies to the browser context. + * + *

For the cookie to apply to all subdomains as well, prefix domain with a dot, like this: ".example.com". * @since v1.8 */ void addCookies(List cookies); @@ -1242,6 +1317,28 @@ default void waitForCondition(BooleanSupplier condition) { * @since v1.32 */ void waitForCondition(BooleanSupplier condition, WaitForConditionOptions options); + /** + * Performs action and waits for a {@code ConsoleMessage} to be logged by in the pages in the context. If predicate is + * provided, it passes {@code ConsoleMessage} value into the {@code predicate} function and waits for {@code + * predicate(message)} to return a truthy value. Will throw an error if the page is closed before the {@link + * BrowserContext#onConsoleMessage BrowserContext.onConsoleMessage()} event is fired. + * + * @param callback Callback that performs the action triggering the event. + * @since v1.34 + */ + default ConsoleMessage waitForConsoleMessage(Runnable callback) { + return waitForConsoleMessage(null, callback); + } + /** + * Performs action and waits for a {@code ConsoleMessage} to be logged by in the pages in the context. If predicate is + * provided, it passes {@code ConsoleMessage} value into the {@code predicate} function and waits for {@code + * predicate(message)} to return a truthy value. Will throw an error if the page is closed before the {@link + * BrowserContext#onConsoleMessage BrowserContext.onConsoleMessage()} event is fired. + * + * @param callback Callback that performs the action triggering the event. + * @since v1.34 + */ + ConsoleMessage waitForConsoleMessage(WaitForConsoleMessageOptions options, Runnable callback); /** * Performs action and waits for a new {@code Page} to be created in the context. If predicate is provided, it passes * {@code Page} value into the {@code predicate} function and waits for {@code predicate(event)} to return a truthy value. diff --git a/playwright/src/main/java/com/microsoft/playwright/ConsoleMessage.java b/playwright/src/main/java/com/microsoft/playwright/ConsoleMessage.java index f50091887..577a5e6f5 100644 --- a/playwright/src/main/java/com/microsoft/playwright/ConsoleMessage.java +++ b/playwright/src/main/java/com/microsoft/playwright/ConsoleMessage.java @@ -56,6 +56,12 @@ public interface ConsoleMessage { * @since v1.8 */ String location(); + /** + * The page that produced this console message, if any. + * + * @since v1.33 + */ + Page page(); /** * The text of the console message. * diff --git a/playwright/src/main/java/com/microsoft/playwright/Dialog.java b/playwright/src/main/java/com/microsoft/playwright/Dialog.java index 78dbcc83e..b6964e3cd 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Dialog.java +++ b/playwright/src/main/java/com/microsoft/playwright/Dialog.java @@ -81,6 +81,12 @@ default void accept() { * @since v1.8 */ String message(); + /** + * The page that initiated this dialog, if available. + * + * @since v1.33 + */ + Page page(); /** * Returns dialog's type, can be one of {@code alert}, {@code beforeunload}, {@code confirm} or {@code prompt}. * diff --git a/playwright/src/main/java/com/microsoft/playwright/Locator.java b/playwright/src/main/java/com/microsoft/playwright/Locator.java index 48d052149..272857f6f 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Locator.java +++ b/playwright/src/main/java/com/microsoft/playwright/Locator.java @@ -2056,6 +2056,20 @@ public WaitForOptions setTimeout(double timeout) { * @since v1.14 */ List allTextContents(); + /** + * Creates a locator that matches both this locator and the argument locator. + * + *

**Usage** + * + *

The following example finds a button with a specific title. + *

{@code
+   * Locator button = page.getByRole(AriaRole.BUTTON).and(page.getByTitle("Subscribe"));
+   * }
+ * + * @param locator Additional locator to match. + * @since v1.33 + */ + Locator and(Locator locator); /** * Calls blur on the element. * diff --git a/playwright/src/main/java/com/microsoft/playwright/Page.java b/playwright/src/main/java/com/microsoft/playwright/Page.java index 40a51b2d6..f0f04b224 100644 --- a/playwright/src/main/java/com/microsoft/playwright/Page.java +++ b/playwright/src/main/java/com/microsoft/playwright/Page.java @@ -82,15 +82,15 @@ public interface Page extends AutoCloseable { * Emitted when JavaScript within the page calls one of console API methods, e.g. {@code console.log} or {@code * console.dir}. Also emitted if the page throws an error or a warning. * - *

The arguments passed into {@code console.log} appear as arguments on the event handler. + *

The arguments passed into {@code console.log} are available on the {@code ConsoleMessage} event handler argument. * - *

An example of handling {@code console} event: + *

**Usage** *

{@code
    * page.onConsoleMessage(msg -> {
    *   for (int i = 0; i < msg.args().size(); ++i)
    *     System.out.println(i + ": " + msg.args().get(i).jsonValue());
    * });
-   * page.evaluate("() => console.log('hello', 5, {foo: 'bar'})");
+   * page.evaluate("() => console.log('hello', 5, { foo: 'bar' })");
    * }
*/ void onConsoleMessage(Consumer handler); @@ -127,13 +127,16 @@ public interface Page extends AutoCloseable { * the dialog - otherwise the page will freeze waiting for the * dialog, and actions like click will never finish. + * + *

**Usage** *

{@code
    * page.onDialog(dialog -> {
    *   dialog.accept();
    * });
    * }
* - *

NOTE: When no {@link Page#onDialog Page.onDialog()} listeners are present, all dialogs are automatically dismissed. + *

NOTE: When no {@link Page#onDialog Page.onDialog()} or {@link BrowserContext#onDialog BrowserContext.onDialog()} listeners are + * present, all dialogs are automatically dismissed. */ void onDialog(Consumer

handler); /** @@ -2394,7 +2397,7 @@ class ScreenshotOptions { */ public ScreenshotCaret caret; /** - * An object which specifies clipping of the resulting image. Should have the following fields: + * An object which specifies clipping of the resulting image. */ public Clip clip; /** @@ -2464,13 +2467,13 @@ public ScreenshotOptions setCaret(ScreenshotCaret caret) { return this; } /** - * An object which specifies clipping of the resulting image. Should have the following fields: + * An object which specifies clipping of the resulting image. */ public ScreenshotOptions setClip(double x, double y, double width, double height) { return setClip(new Clip(x, y, width, height)); } /** - * An object which specifies clipping of the resulting image. Should have the following fields: + * An object which specifies clipping of the resulting image. */ public ScreenshotOptions setClip(Clip clip) { this.clip = clip; 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 f893ac8b9..f5f74b8f3 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/BrowserContextImpl.java @@ -53,6 +53,8 @@ class BrowserContextImpl extends ChannelOwner implements BrowserContext { PageImpl ownerPage; private static final Map eventSubscriptions() { Map result = new HashMap<>(); + result.put(EventType.CONSOLE, "console"); + result.put(EventType.DIALOG, "dialog"); result.put(EventType.REQUEST, "request"); result.put(EventType.RESPONSE, "response"); result.put(EventType.REQUESTFINISHED, "requestFinished"); @@ -77,6 +79,8 @@ static class HarRecorder { enum EventType { CLOSE, + CONSOLE, + DIALOG, PAGE, REQUEST, REQUESTFAILED, @@ -120,6 +124,26 @@ public void offClose(Consumer handler) { listeners.remove(EventType.CLOSE, handler); } + @Override + public void onConsoleMessage(Consumer handler) { + listeners.add(EventType.CONSOLE, handler); + } + + @Override + public void offConsoleMessage(Consumer handler) { + listeners.remove(EventType.CONSOLE, handler); + } + + @Override + public void onDialog(Consumer handler) { + listeners.add(EventType.DIALOG, handler); + } + + @Override + public void offDialog(Consumer handler) { + listeners.remove(EventType.DIALOG, handler); + } + @Override public void onPage(Consumer handler) { listeners.add(EventType.PAGE, handler); @@ -516,6 +540,18 @@ public void waitForCondition(BooleanSupplier predicate, WaitForConditionOptions runUntil(() -> {}, new WaitableRace<>(waitables)); } + @Override + public ConsoleMessage waitForConsoleMessage(WaitForConsoleMessageOptions options, Runnable code) { + return withWaitLogging("BrowserContext.waitForConsoleMessage", logger -> waitForConsoleMessageImpl(options, code)); + } + + private ConsoleMessage waitForConsoleMessageImpl(WaitForConsoleMessageOptions options, Runnable code) { + if (options == null) { + options = new WaitForConsoleMessageOptions(); + } + return waitForEventWithTimeout(EventType.CONSOLE, code, options.predicate, options.timeout); + } + private class WaitableContextClose extends WaitableEvent { WaitableContextClose() { super(BrowserContextImpl.this.listeners, EventType.CLOSE); @@ -554,7 +590,36 @@ WaitableResult pause() { @Override protected void handleEvent(String event, JsonObject params) { - if ("route".equals(event)) { + if ("dialog".equals(event)) { + String guid = params.getAsJsonObject("dialog").get("guid").getAsString(); + DialogImpl dialog = connection.getExistingObject(guid); + boolean hasListeners = false; + if (listeners.hasListeners(EventType.DIALOG)) { + hasListeners = true; + listeners.notify(EventType.DIALOG, dialog); + } + PageImpl page = dialog.page(); + if (page != null) { + if (page.listeners.hasListeners(PageImpl.EventType.DIALOG)) { + hasListeners = true; + page.listeners.notify(PageImpl.EventType.DIALOG, dialog); + } + } + // Although we do similar handling on the server side, we still need this logic + // on the client side due to a possible race condition between two async calls: + // a) removing "dialog" listener subscription (client->server) + // b) actual "dialog" event (server->client) + if (!hasListeners) { + if ("beforeunload".equals(dialog.type())) { + try { + dialog.accept(); + } catch (PlaywrightException e) { + } + } else { + dialog.dismiss(); + } + } + } else if ("route".equals(event)) { RouteImpl route = connection.getExistingObject(params.getAsJsonObject("route").get("guid").getAsString()); handleRoute(route); } else if ("page".equals(event)) { @@ -570,6 +635,14 @@ protected void handleEvent(String event, JsonObject params) { if (binding != null) { bindingCall.call(binding); } + } else if ("console".equals(event)) { + String guid = params.getAsJsonObject("message").get("guid").getAsString(); + ConsoleMessageImpl message = connection.getExistingObject(guid); + listeners.notify(BrowserContextImpl.EventType.CONSOLE, message); + PageImpl page = message.page(); + if (page != null) { + page.listeners.notify(PageImpl.EventType.CONSOLE, message); + } } else if ("request".equals(event)) { String guid = params.getAsJsonObject("request").get("guid").getAsString(); RequestImpl request = connection.getExistingObject(guid); diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/ConsoleMessageImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/ConsoleMessageImpl.java index 575c39ffd..eb5a239c6 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/ConsoleMessageImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/ConsoleMessageImpl.java @@ -20,6 +20,7 @@ import com.google.gson.JsonObject; import com.microsoft.playwright.ConsoleMessage; import com.microsoft.playwright.JSHandle; +import com.microsoft.playwright.Page; import java.util.ArrayList; import java.util.List; @@ -27,8 +28,16 @@ import static com.microsoft.playwright.impl.Serialization.gson; public class ConsoleMessageImpl extends ChannelOwner implements ConsoleMessage { + private PageImpl page; + public ConsoleMessageImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) { super(parent, type, guid, initializer); + // Note: currently, we only report console messages for pages and they always have a page. + // However, in the future we might report console messages for service workers or something else, + // where page() would be null. + if (initializer.has("page")) { + page = connection.getExistingObject(initializer.getAsJsonObject("page").get("guid").getAsString()); + } } public String type() { @@ -55,4 +64,9 @@ public String location() { location.get("lineNumber").getAsNumber() + ":" + location.get("columnNumber").getAsNumber(); } + + @Override + public PageImpl page() { + return page; + } } diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/DialogImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/DialogImpl.java index 62c57d4fd..b4e6b4241 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/DialogImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/DialogImpl.java @@ -18,10 +18,18 @@ import com.google.gson.JsonObject; import com.microsoft.playwright.Dialog; +import com.microsoft.playwright.Page; class DialogImpl extends ChannelOwner implements Dialog { + private PageImpl page; + DialogImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) { super(parent, type, guid, initializer); + // Note: dialogs that open early during page initialization block it. + // Therefore, we must report the dialog without a page to be able to handle it. + if (initializer.has("page")) { + page = connection.getExistingObject(initializer.getAsJsonObject("page").get("guid").getAsString()); + } } @Override @@ -50,6 +58,11 @@ public String message() { return initializer.get("message").getAsString(); } + @Override + public PageImpl page() { + return page; + } + @Override public String type() { return initializer.get("type").getAsString(); diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/LocatorImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/LocatorImpl.java index 826581d61..3cf829fa9 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/LocatorImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/LocatorImpl.java @@ -91,6 +91,14 @@ public List allTextContents() { return (List) frame.evalOnSelectorAll(selector, "ee => ee.map(e => e.textContent || '')"); } + @Override + public Locator and(Locator locator) { + LocatorImpl other = (LocatorImpl) locator; + if (other.frame != frame) + throw new Error("Locators must belong to the same frame."); + return new LocatorImpl(frame, selector + " >> internal:and=" + gson().toJson(other.selector), null); + } + @Override public void blur(BlurOptions options) { frame.withLogging("Locator.blur", () -> blurImpl(options)); diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java index e5c9b0127..c27afd77b 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/PageImpl.java @@ -51,6 +51,8 @@ public class PageImpl extends ChannelOwner implements Page { private final Set frames = new LinkedHashSet<>(); private static final Map eventSubscriptions() { Map result = new HashMap<>(); + result.put(EventType.CONSOLE, "console"); + result.put(EventType.DIALOG, "dialog"); result.put(EventType.REQUEST, "request"); result.put(EventType.RESPONSE, "response"); result.put(EventType.REQUESTFINISHED, "requestFinished"); @@ -113,22 +115,7 @@ enum EventType { @Override protected void handleEvent(String event, JsonObject params) { - if ("dialog".equals(event)) { - String guid = params.getAsJsonObject("dialog").get("guid").getAsString(); - DialogImpl dialog = connection.getExistingObject(guid); - if (listeners.hasListeners(EventType.DIALOG)) { - listeners.notify(EventType.DIALOG, dialog); - } else { - if ("beforeunload".equals(dialog.type())) { - try { - dialog.accept(); - } catch (PlaywrightException e) { - } - } else { - dialog.dismiss(); - } - } - } else if ("worker".equals(event)) { + if ("worker".equals(event)) { String guid = params.getAsJsonObject("worker").get("guid").getAsString(); WorkerImpl worker = connection.getExistingObject(guid); worker.page = this; @@ -138,10 +125,6 @@ protected void handleEvent(String event, JsonObject params) { String guid = params.getAsJsonObject("webSocket").get("guid").getAsString(); WebSocketImpl webSocket = connection.getExistingObject(guid); listeners.notify(EventType.WEBSOCKET, webSocket); - } else if ("console".equals(event)) { - String guid = params.getAsJsonObject("message").get("guid").getAsString(); - ConsoleMessageImpl message = connection.getExistingObject(guid); - listeners.notify(EventType.CONSOLE, message); } else if ("download".equals(event)) { String artifactGuid = params.getAsJsonObject("artifact").get("guid").getAsString(); ArtifactImpl artifact = connection.getExistingObject(artifactGuid); diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/UrlMatcher.java b/playwright/src/main/java/com/microsoft/playwright/impl/UrlMatcher.java index 7039e6a9b..09960ef25 100644 --- a/playwright/src/main/java/com/microsoft/playwright/impl/UrlMatcher.java +++ b/playwright/src/main/java/com/microsoft/playwright/impl/UrlMatcher.java @@ -90,6 +90,11 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; UrlMatcher that = (UrlMatcher) o; + if (rawSource instanceof Pattern && that.rawSource instanceof Pattern) { + Pattern a = (Pattern) rawSource; + Pattern b = (Pattern) that.rawSource; + return a.pattern().equals(b.pattern()) && a.flags() == b.flags(); + } return Objects.equals(rawSource, that.rawSource); } diff --git a/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextEvents.java b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextEvents.java new file mode 100644 index 000000000..630c42857 --- /dev/null +++ b/playwright/src/test/java/com/microsoft/playwright/TestBrowserContextEvents.java @@ -0,0 +1,159 @@ +package com.microsoft.playwright; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIf; + +import java.io.OutputStreamWriter; +import java.io.Writer; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class TestBrowserContextEvents extends TestBase { + @Test + void consoleEventShouldWorkSmoke() { + ConsoleMessage message = context.waitForConsoleMessage(() -> { + page.evaluate("console.log('hello')"); + }); + assertEquals("hello", message.text()); + assertEquals(page, message.page()); + } + + @Test + void consoleEventShouldWorkInPopup() { + Page[] popup = { null }; + ConsoleMessage message = context.waitForConsoleMessage(() -> { + popup[0] = page.waitForPopup(() -> { + page.evaluate("const win = window.open('');\n" + + "win.console.log('hello');\n"); + }); + }); + assertEquals("hello", message.text()); + assertEquals(popup[0], message.page()); + } + + @Test + @DisabledIf(value="com.microsoft.playwright.TestBase#isFirefox", disabledReason="console message from javascript: url is not reported at all") + void consoleEventShouldWorkInPopup2() { + Page[] popup = { null }; + ConsoleMessage message = context.waitForConsoleMessage( + new BrowserContext.WaitForConsoleMessageOptions().setPredicate(msg -> "log".equals(msg.type())), + () -> { + popup[0] = context.waitForPage(() -> { + page.evaluate("async () => {\n" + + " const win = window.open('javascript:console.log(\"hello\")');\n" + + " await new Promise(f => setTimeout(f, 0));\n" + + " win.close();\n" + + "}"); + }); + }); + assertEquals("hello", message.text()); + assertEquals(popup[0], message.page()); + } + + @Test + @DisabledIf(value="com.microsoft.playwright.TestBase#isFirefox", disabledReason="console message is not reported at all") + void consoleEventShouldWorkInImmediatelyClosedPopup() { + Page[] popup = { null }; + ConsoleMessage message = context.waitForConsoleMessage(() -> { + popup[0] = page.waitForPopup(() -> { + page.evaluate("async () => {\n" + + " const win = window.open();\n" + + " win.console.log('hello');\n" + + " win.close();\n" + + " }\n"); + }); + }); + assertEquals("hello", message.text()); + assertEquals(popup[0], message.page()); + } + + @Test + void dialogEventShouldWorkSmoke() { + Dialog[] dialog = { null }; + context.onDialog(d -> { + dialog[0] = d; + dialog[0].accept("hello"); + }); + Object result = page.evaluate("prompt('hey?')"); + assertEquals("hello", result); + context.waitForCondition(() -> dialog[0] != null); + assertEquals("hey?", dialog[0].message()); + assertEquals(page, dialog[0].page()); + } + + @Test + void dialogEventShouldWorkInPopup() { + Dialog[] dialog = { null }; + context.onDialog(d -> { + dialog[0] = d; + d.accept("hello"); + }); + Page popup = page.waitForPopup(() -> { + Object result = page.evaluate("() => {\n" + + " const win = window.open('');\n" + + " return win.prompt('hey?');\n" + + " }"); + assertEquals("hello", result); + }); + assertEquals("hey?", dialog[0].message()); + assertEquals(popup, dialog[0].page()); + } + + @Test + @DisabledIf(value="com.microsoft.playwright.TestBase#isFirefox", disabledReason="dialog from javascript: url is not reported at all") + void dialogEventShouldWorkInPopup2() { + Dialog[] dialog = { null }; + context.onDialog(d -> { + dialog[0] = d; + d.accept("hello"); + }); + page.evaluate("window.open('javascript:prompt(\"hey?\")');"); + context.waitForCondition(() -> dialog[0] != null); + assertEquals("hey?", dialog[0].message()); + assertEquals(null, dialog[0].page()); + } + + @Test + void dialogEventShouldWorkInImmdiatelyClosedPopup() { + Dialog[] dialog = { null }; + context.onDialog(d -> { + dialog[0] = d; + d.accept("hello"); + }); + Page popup = page.waitForPopup(() -> { + Object result = page.evaluate("async () => {\n" + + " const win = window.open();\n" + + " const result = win.prompt('hey?');\n" + + " win.close();\n" + + " return result;\n" + + " }"); + assertEquals("hello", result); + }); + assertEquals("hey?", dialog[0].message()); + assertEquals(popup, dialog[0].page()); + } + + @Test + void dialogEventShouldWorkWithInlineScriptTag() { + server.setRoute("/popup.html", exchange -> { + exchange.getResponseHeaders().add("content-type", "text/html"); + exchange.sendResponseHeaders(200, 0); + try (Writer writer = new OutputStreamWriter(exchange.getResponseBody())) { + writer.write(""); + } + }); + page.navigate(server.EMPTY_PAGE); + page.setContent("Click me"); + Dialog[] dialog = { null }; + context.onDialog(d -> { + dialog[0] = d; + d.accept("hello"); + }); + Page popup = context.waitForPage(() -> page.click("a")); + page.waitForCondition(() -> dialog[0] != null); + assertEquals("hey?", dialog[0].message()); + assertEquals(popup, dialog[0].page()); + page.waitForCondition(() -> "hello".equals(popup.evaluate("window.result")), + new Page.WaitForConditionOptions().setTimeout(5_000)); + } +} diff --git a/playwright/src/test/java/com/microsoft/playwright/TestPageLocatorQuery.java b/playwright/src/test/java/com/microsoft/playwright/TestPageLocatorQuery.java index 37eb6c2ed..1328d985d 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestPageLocatorQuery.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestPageLocatorQuery.java @@ -182,6 +182,19 @@ void shouldSupportLocatorFilter() { assertThat(page.locator("div").filter(new Locator.FilterOptions().setHasNotText("foo"))).hasCount(2); } + + @Test + void shouldSupportLocatorAnd() { + page.setContent("
hello
world
\n" + + " hello2world2"); + assertThat(page.locator("div").and(page.locator("div"))).hasCount(2); + assertThat(page.locator("div").and(page.getByTestId("foo"))).hasText(new String[] { "hello" }); + assertThat(page.locator("div").and(page.getByTestId("bar"))).hasText(new String[] { "world" }); + assertThat(page.getByTestId("foo").and(page.locator("div"))).hasText(new String[] { "hello" }); + assertThat(page.getByTestId("bar").and(page.locator("span"))).hasText(new String[] { "world2" }); + assertThat(page.locator("span").and(page.getByTestId(Pattern.compile("bar|foo")))).hasCount(2); + } + @Test void shouldSupportLocatorOr() { page.setContent("
hello
world"); diff --git a/playwright/src/test/java/com/microsoft/playwright/TestPageRoute.java b/playwright/src/test/java/com/microsoft/playwright/TestPageRoute.java index 3193985fb..a8826f58e 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestPageRoute.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestPageRoute.java @@ -82,12 +82,12 @@ void shouldUnroute() { intercepted.add(4); route.fallback(); }; - page.route("**/empty.html", handler4); + page.route(Pattern.compile("empty.html"), handler4); page.navigate(server.EMPTY_PAGE); assertEquals(asList(4, 3, 2, 1), intercepted); intercepted.clear(); - page.unroute("**/empty.html", handler4); + page.unroute(Pattern.compile("empty.html"), handler4); page.navigate(server.EMPTY_PAGE); assertEquals(asList(3, 2, 1), intercepted); diff --git a/playwright/src/test/java/com/microsoft/playwright/TestSelectorsMisc.java b/playwright/src/test/java/com/microsoft/playwright/TestSelectorsMisc.java index 0235929c1..2ecb9e91c 100644 --- a/playwright/src/test/java/com/microsoft/playwright/TestSelectorsMisc.java +++ b/playwright/src/test/java/com/microsoft/playwright/TestSelectorsMisc.java @@ -206,6 +206,17 @@ void shouldWorkWithInternalHasNot() { assertEquals(2, page.evalOnSelectorAll("section >> internal:has-not=\"article\"", "els => els.length")); } + @Test + void shouldWorkWithInternalAnd() { + page.setContent("
hello
world
\n" + + " hello2world2"); + assertEquals(asList(), page.evalOnSelectorAll("div >> internal:and=\"span\"", "els => els.map(e => e.textContent)")); + assertEquals(asList("hello"), page.evalOnSelectorAll("div >> internal:and=\".foo\"", "els => els.map(e => e.textContent)")); + assertEquals(asList("world"), page.evalOnSelectorAll("div >> internal:and=\".bar\"", "els => els.map(e => e.textContent)")); + assertEquals(asList("hello2", "world2"), page.evalOnSelectorAll("span >> internal:and=\"span\"", "els => els.map(e => e.textContent)")); + assertEquals(asList("hello"), page.evalOnSelectorAll(".foo >> internal:and=\"div\"", "els => els.map(e => e.textContent)")); + assertEquals(asList("world2"), page.evalOnSelectorAll(".bar >> internal:and=\"span\"", "els => els.map(e => e.textContent)")); + } @Test void shouldWorkWithInternalOr() { diff --git a/scripts/CLI_VERSION b/scripts/CLI_VERSION index 7aa332e41..83cce22fc 100644 --- a/scripts/CLI_VERSION +++ b/scripts/CLI_VERSION @@ -1 +1 @@ -1.33.0 +1.34.0-beta-1684447150000