From 328267e7657baa6cc42540f1bce5ebb6d627f501 Mon Sep 17 00:00:00 2001 From: Andrei Solntsev Date: Tue, 28 Oct 2025 22:15:58 +0200 Subject: [PATCH 1/2] add BiDi method "BrowsingContext.resetViewport" Implementation detail: according to BiDi spec (https://www.w3.org/TR/webdriver-bidi/#command-browsingContext-setViewport), we have to call method "setViewport" with "viewport" parameter equal to "null". --- .../src/org/openqa/selenium/bidi/Command.java | 6 +- .../bidi/browsingcontext/BrowsingContext.java | 8 ++ .../browsingcontext/BrowsingContextInfo.java | 5 ++ .../selenium/bidi/browsingcontext/BUILD.bazel | 1 + .../browsingcontext/BrowsingContextTest.java | 82 +++++++++++-------- 5 files changed, 65 insertions(+), 37 deletions(-) diff --git a/java/src/org/openqa/selenium/bidi/Command.java b/java/src/org/openqa/selenium/bidi/Command.java index 8a120a3369cf7..dc99d5eec1124 100644 --- a/java/src/org/openqa/selenium/bidi/Command.java +++ b/java/src/org/openqa/selenium/bidi/Command.java @@ -17,7 +17,10 @@ package org.openqa.selenium.bidi; +import static java.util.Collections.unmodifiableMap; + import java.lang.reflect.Type; +import java.util.HashMap; import java.util.Map; import java.util.function.Function; import org.openqa.selenium.internal.Require; @@ -48,7 +51,8 @@ public Command( Function mapper, boolean sendsResponse) { this.method = Require.nonNull("Method name", method); - this.params = Map.copyOf(Require.nonNull("Command parameters", params)); + this.params = + unmodifiableMap(new HashMap(Require.nonNull("Command parameters", params))); this.mapper = Require.nonNull("Mapper for result", mapper); this.sendsResponse = sendsResponse; } diff --git a/java/src/org/openqa/selenium/bidi/browsingcontext/BrowsingContext.java b/java/src/org/openqa/selenium/bidi/browsingcontext/BrowsingContext.java index 6be01cbefafa0..631908d65222e 100644 --- a/java/src/org/openqa/selenium/bidi/browsingcontext/BrowsingContext.java +++ b/java/src/org/openqa/selenium/bidi/browsingcontext/BrowsingContext.java @@ -329,6 +329,14 @@ public void setViewport(double width, double height, double devicePixelRatio) { devicePixelRatio))); } + public void resetViewport() { + Map params = new HashMap<>(); + params.put(CONTEXT, id); + params.put("viewport", null); + params.put("devicePixelRatio", null); + this.bidi.send(new Command<>("browsingContext.setViewport", params)); + } + public void activate() { this.bidi.send(new Command<>("browsingContext.activate", Map.of(CONTEXT, id))); } diff --git a/java/src/org/openqa/selenium/bidi/browsingcontext/BrowsingContextInfo.java b/java/src/org/openqa/selenium/bidi/browsingcontext/BrowsingContextInfo.java index 0414e6284f8ef..1edb9627798bc 100644 --- a/java/src/org/openqa/selenium/bidi/browsingcontext/BrowsingContextInfo.java +++ b/java/src/org/openqa/selenium/bidi/browsingcontext/BrowsingContextInfo.java @@ -134,4 +134,9 @@ public static BrowsingContextInfo fromJson(JsonInput input) { return new BrowsingContextInfo( id, url, children, clientWindow, originalOpener, userContext, parentBrowsingContext); } + + @Override + public String toString() { + return String.format("BrowsingContextInfo(%s %s)", id, url); + } } diff --git a/java/test/org/openqa/selenium/bidi/browsingcontext/BUILD.bazel b/java/test/org/openqa/selenium/bidi/browsingcontext/BUILD.bazel index 71b67efb132f2..27ee17ef974d2 100644 --- a/java/test/org/openqa/selenium/bidi/browsingcontext/BUILD.bazel +++ b/java/test/org/openqa/selenium/bidi/browsingcontext/BUILD.bazel @@ -14,6 +14,7 @@ java_selenium_test_suite( "selenium-remote", ], deps = [ + "//java/src/org/openqa/selenium:core", "//java/src/org/openqa/selenium/bidi", "//java/src/org/openqa/selenium/bidi/browsingcontext", "//java/src/org/openqa/selenium/bidi/log", diff --git a/java/test/org/openqa/selenium/bidi/browsingcontext/BrowsingContextTest.java b/java/test/org/openqa/selenium/bidi/browsingcontext/BrowsingContextTest.java index 7e57600542f47..7b41a3ee37ac6 100644 --- a/java/test/org/openqa/selenium/bidi/browsingcontext/BrowsingContextTest.java +++ b/java/test/org/openqa/selenium/bidi/browsingcontext/BrowsingContextTest.java @@ -17,15 +17,17 @@ package org.openqa.selenium.bidi.browsingcontext; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.openqa.selenium.support.ui.ExpectedConditions.alertIsPresent; import static org.openqa.selenium.support.ui.ExpectedConditions.titleIs; import static org.openqa.selenium.support.ui.ExpectedConditions.visibilityOfElementLocated; import java.util.List; +import java.util.regex.Pattern; import org.junit.jupiter.api.Test; import org.openqa.selenium.By; +import org.openqa.selenium.Dimension; import org.openqa.selenium.JavascriptExecutor; import org.openqa.selenium.Rectangle; import org.openqa.selenium.WebElement; @@ -40,6 +42,9 @@ class BrowsingContextTest extends JupiterTestBase { + private static final Pattern CONTEXT_NOT_FOUND = + Pattern.compile(".*Browsing Context with id .+ not found.*", Pattern.DOTALL); + @Test @NeedsFreshDriver void canCreateABrowsingContextForGivenId() { @@ -136,9 +141,9 @@ void canGetTreeWithAChild() { List contextInfoList = parentWindow.getTree(); - assertThat(contextInfoList.size()).isEqualTo(1); + assertThat(contextInfoList).hasSize(1); BrowsingContextInfo info = contextInfoList.get(0); - assertThat(info.getChildren().size()).isEqualTo(1); + assertThat(info.getChildren()).hasSize(1); assertThat(info.getId()).isEqualTo(referenceContextId); assertThat(info.getChildren().get(0).getUrl()).contains("formPage.html"); } @@ -155,7 +160,7 @@ void canGetTreeWithDepth() { List contextInfoList = parentWindow.getTree(0); - assertThat(contextInfoList.size()).isEqualTo(1); + assertThat(contextInfoList).hasSize(1); BrowsingContextInfo info = contextInfoList.get(0); assertThat(info.getChildren()).isNull(); // since depth is 0 assertThat(info.getId()).isEqualTo(referenceContextId); @@ -176,7 +181,7 @@ void canGetTreeWithRootAndDepth() { List contextInfoList = parentWindow.getTree(referenceContextId, 1); - assertThat(contextInfoList.size()).isEqualTo(1); + assertThat(contextInfoList).hasSize(1); BrowsingContextInfo info = contextInfoList.get(0); assertThat(info.getChildren()).isNotNull(); // since depth is 1 assertThat(info.getId()).isEqualTo(referenceContextId); @@ -199,7 +204,7 @@ void canGetTreeWithRoot() { List contextInfoList = parentWindow.getTree(tab.getId()); - assertThat(contextInfoList.size()).isEqualTo(1); + assertThat(contextInfoList).hasSize(1); BrowsingContextInfo info = contextInfoList.get(0); assertThat(info.getId()).isEqualTo(tab.getId()); assertThat(info.getOriginalOpener()).isNull(); @@ -215,7 +220,7 @@ void canGetAllTopLevelContexts() { List contextInfoList = window1.getTopLevelContexts(); - assertThat(contextInfoList.size()).isEqualTo(2); + assertThat(contextInfoList).hasSize(2); } @Test @@ -226,7 +231,9 @@ void canCloseAWindow() { window2.close(); - assertThatExceptionOfType(BiDiException.class).isThrownBy(window2::getTree); + assertThatThrownBy(window2::getTree) + .isInstanceOf(BiDiException.class) + .hasMessageMatching(CONTEXT_NOT_FOUND); } @Test @@ -237,7 +244,9 @@ void canCloseATab() { tab2.close(); - assertThatExceptionOfType(BiDiException.class).isThrownBy(tab2::getTree); + assertThatThrownBy(tab2::getTree) + .isInstanceOf(BiDiException.class) + .hasMessageMatching(CONTEXT_NOT_FOUND); } @Test @@ -397,7 +406,7 @@ void canCaptureScreenshot() { String screenshot = browsingContext.captureScreenshot(); - assertThat(screenshot.length()).isPositive(); + assertThat(screenshot).isNotEmpty(); } @Test @@ -423,7 +432,7 @@ void canCaptureScreenshotWithAllParameters() { .origin(CaptureScreenshotParameters.Origin.DOCUMENT) .clipRectangle(clipRectangle)); - assertThat(screenshot.length()).isPositive(); + assertThat(screenshot).isNotEmpty(); } @Test @@ -440,7 +449,7 @@ void canCaptureScreenshotOfViewport() { browsingContext.captureBoxScreenshot( elementRectangle.getX(), elementRectangle.getY(), 5, 5); - assertThat(screenshot.length()).isPositive(); + assertThat(screenshot).isNotEmpty(); } @Test @@ -455,24 +464,22 @@ void canCaptureElementScreenshot() { String screenshot = browsingContext.captureElementScreenshot(((RemoteWebElement) element).getId()); - assertThat(screenshot.length()).isPositive(); + assertThat(screenshot).isNotEmpty(); } @Test @NeedsFreshDriver void canSetViewport() { + Dimension initialViewportSize = getViewportSize(); + BrowsingContext browsingContext = new BrowsingContext(driver, driver.getWindowHandle()); driver.get(appServer.whereIs("formPage.html")); browsingContext.setViewport(250, 300); + assertThat(getViewportSize()).isEqualTo(new Dimension(250, 300)); - List newViewportSize = - (List) - ((JavascriptExecutor) driver) - .executeScript("return [window.innerWidth, window.innerHeight];"); - - assertThat(newViewportSize.get(0)).isEqualTo(250); - assertThat(newViewportSize.get(1)).isEqualTo(300); + browsingContext.resetViewport(); + assertThat(getViewportSize()).isEqualTo(initialViewportSize); } @Test @@ -481,20 +488,10 @@ void canSetViewportWithDevicePixelRatio() { BrowsingContext browsingContext = new BrowsingContext(driver, driver.getWindowHandle()); driver.get(appServer.whereIs("formPage.html")); - browsingContext.setViewport(250, 300, 5); - - List newViewportSize = - (List) - ((JavascriptExecutor) driver) - .executeScript("return [window.innerWidth, window.innerHeight];"); - - assertThat(newViewportSize.get(0)).isEqualTo(250); - assertThat(newViewportSize.get(1)).isEqualTo(300); - - Long newDevicePixelRatio = - (Long) ((JavascriptExecutor) driver).executeScript("return window.devicePixelRatio"); + browsingContext.setViewport(250, 300, 5.5); - assertThat(newDevicePixelRatio).isEqualTo(5); + assertThat(getViewportSize()).isEqualTo(new Dimension(250, 300)); + assertThat(getDevicePixelRatio()).isEqualTo(5.5); } @Test @@ -507,7 +504,7 @@ void canPrintPage() { String printPage = browsingContext.print(printOptions); - assertThat(printPage.length()).isPositive(); + assertThat(printPage).isNotEmpty(); // Comparing expected PDF is a hard problem. // As long as we are sending the parameters correctly it should be fine. // Trusting the browsers to do the right thing. @@ -568,7 +565,20 @@ private String promptPage() { "

")); } + private T executeScript(String js) { + return (T) ((JavascriptExecutor) driver).executeScript(js); + } + private boolean getDocumentFocus() { - return (boolean) ((JavascriptExecutor) driver).executeScript("return document.hasFocus();"); + return executeScript("return document.hasFocus();"); + } + + private Dimension getViewportSize() { + List dimensions = executeScript("return [window.innerWidth, window.innerHeight];"); + return new Dimension(dimensions.get(0).intValue(), dimensions.get(1).intValue()); + } + + private double getDevicePixelRatio() { + return ((Number) executeScript("return window.devicePixelRatio")).doubleValue(); } } From 97af1c43756f4bc0ad2c0e403cc7cfc07080f003 Mon Sep 17 00:00:00 2001 From: Andrei Solntsev Date: Wed, 29 Oct 2025 09:50:09 +0200 Subject: [PATCH 2/2] add BiDi method "BrowsingContext.setViewport(null, null, null)" for resetting mobile emulation mode --- .../selenium/bidi/browsingcontext/BUILD.bazel | 1 + .../bidi/browsingcontext/BrowsingContext.java | 82 +++++++++++++------ .../browsingcontext/BrowsingContextTest.java | 17 ++-- 3 files changed, 69 insertions(+), 31 deletions(-) diff --git a/java/src/org/openqa/selenium/bidi/browsingcontext/BUILD.bazel b/java/src/org/openqa/selenium/bidi/browsingcontext/BUILD.bazel index fbf0948a2d6eb..f613f2b689506 100644 --- a/java/src/org/openqa/selenium/bidi/browsingcontext/BUILD.bazel +++ b/java/src/org/openqa/selenium/bidi/browsingcontext/BUILD.bazel @@ -22,5 +22,6 @@ java_library( "//java/src/org/openqa/selenium/json", "//java/src/org/openqa/selenium/remote/http", artifact("com.google.auto.service:auto-service-annotations"), + "@maven//:org_jspecify_jspecify", ], ) diff --git a/java/src/org/openqa/selenium/bidi/browsingcontext/BrowsingContext.java b/java/src/org/openqa/selenium/bidi/browsingcontext/BrowsingContext.java index 631908d65222e..e72251326dfae 100644 --- a/java/src/org/openqa/selenium/bidi/browsingcontext/BrowsingContext.java +++ b/java/src/org/openqa/selenium/bidi/browsingcontext/BrowsingContext.java @@ -24,6 +24,8 @@ import java.util.List; import java.util.Map; import java.util.function.Function; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WindowType; import org.openqa.selenium.bidi.BiDi; @@ -36,6 +38,7 @@ import org.openqa.selenium.json.TypeToken; import org.openqa.selenium.print.PrintOptions; +@NullMarked public class BrowsingContext { private static final Json JSON = new Json(); @@ -91,6 +94,7 @@ public BrowsingContext(WebDriver driver, String id) { public BrowsingContext(WebDriver driver, WindowType type) { Require.nonNull("WebDriver", driver); + Require.nonNull("WindowType", type); if (!(driver instanceof HasBiDi)) { throw new IllegalArgumentException("WebDriver instance must support BiDi protocol"); @@ -103,6 +107,7 @@ public BrowsingContext(WebDriver driver, WindowType type) { public BrowsingContext(WebDriver driver, CreateContextParameters parameters) { Require.nonNull("WebDriver", driver); + Require.nonNull("CreateContextParameters", parameters); if (!(driver instanceof HasBiDi)) { throw new IllegalArgumentException("WebDriver instance must support BiDi protocol"); @@ -302,41 +307,70 @@ public String captureElementScreenshot(String elementId, String handle) { })); } - public void setViewport(double width, double height) { - Require.positive("Viewport width", width); - Require.positive("Viewport height", height); + public void setViewport(int width, int height) { + setViewport((double) width, (double) height); + } - this.bidi.send( - new Command<>( - "browsingContext.setViewport", - Map.of(CONTEXT, id, "viewport", Map.of("width", width, "height", height)))); + public void setViewport(int width, int height, double devicePixelRatio) { + setViewport((double) width, (double) height, devicePixelRatio); } - public void setViewport(double width, double height, double devicePixelRatio) { - Require.positive("Viewport width", width); - Require.positive("Viewport height", height); - Require.positive("Device pixel ratio.", devicePixelRatio); + /** + * Set viewport size to given width and height (aka "mobile emulation" mode). + * + *

If both {@code width} and {@code height} are null, then resets viewport to the initial size + * (aka "desktop" mode). + * + * @param width null or positive + * @param height null or positive + */ + public void setViewport(@Nullable Double width, @Nullable Double height) { + validate(width, height); - this.bidi.send( - new Command<>( - "browsingContext.setViewport", - Map.of( - CONTEXT, - id, - "viewport", - Map.of("width", width, "height", height), - "devicePixelRatio", - devicePixelRatio))); + Map params = new HashMap<>(); + params.put(CONTEXT, id); + params.put("viewport", width == null ? null : Map.of("width", width, "height", height)); + this.bidi.send(new Command<>("browsingContext.setViewport", params)); } - public void resetViewport() { + /** + * Set viewport's size and pixel ratio (aka "mobile emulation" mode). + * + *

If both {@code width} and {@code height} are null then resets viewport to the initial size + * (aka "desktop" mode). + * + *

If {@code devicePixelRatio} is null then resets DPR to browser’s default DPR (usually 1.0 on + * desktop). + * + * @param width null or positive + * @param height null or positive + * @param devicePixelRatio null or positive + */ + public void setViewport( + @Nullable Double width, @Nullable Double height, @Nullable Double devicePixelRatio) { + validate(width, height); + validate(devicePixelRatio); + Map params = new HashMap<>(); params.put(CONTEXT, id); - params.put("viewport", null); - params.put("devicePixelRatio", null); + params.put("viewport", width == null ? null : Map.of("width", width, "height", height)); + params.put("devicePixelRatio", devicePixelRatio); this.bidi.send(new Command<>("browsingContext.setViewport", params)); } + private void validate(@Nullable Double width, @Nullable Double height) { + if (width != null || height != null) { + Require.positive("Viewport width", width); + Require.positive("Viewport height", height); + } + } + + private void validate(@Nullable Double devicePixelRatio) { + if (devicePixelRatio != null) { + Require.positive("Device pixel ratio.", devicePixelRatio); + } + } + public void activate() { this.bidi.send(new Command<>("browsingContext.activate", Map.of(CONTEXT, id))); } diff --git a/java/test/org/openqa/selenium/bidi/browsingcontext/BrowsingContextTest.java b/java/test/org/openqa/selenium/bidi/browsingcontext/BrowsingContextTest.java index 7b41a3ee37ac6..0ee75e4e5eed0 100644 --- a/java/test/org/openqa/selenium/bidi/browsingcontext/BrowsingContextTest.java +++ b/java/test/org/openqa/selenium/bidi/browsingcontext/BrowsingContextTest.java @@ -24,7 +24,6 @@ import static org.openqa.selenium.support.ui.ExpectedConditions.visibilityOfElementLocated; import java.util.List; -import java.util.regex.Pattern; import org.junit.jupiter.api.Test; import org.openqa.selenium.By; import org.openqa.selenium.Dimension; @@ -42,9 +41,6 @@ class BrowsingContextTest extends JupiterTestBase { - private static final Pattern CONTEXT_NOT_FOUND = - Pattern.compile(".*Browsing Context with id .+ not found.*", Pattern.DOTALL); - @Test @NeedsFreshDriver void canCreateABrowsingContextForGivenId() { @@ -233,7 +229,7 @@ void canCloseAWindow() { assertThatThrownBy(window2::getTree) .isInstanceOf(BiDiException.class) - .hasMessageMatching(CONTEXT_NOT_FOUND); + .hasMessageContaining("not found"); } @Test @@ -246,7 +242,7 @@ void canCloseATab() { assertThatThrownBy(tab2::getTree) .isInstanceOf(BiDiException.class) - .hasMessageMatching(CONTEXT_NOT_FOUND); + .hasMessageContaining("not found"); } @Test @@ -478,13 +474,16 @@ void canSetViewport() { browsingContext.setViewport(250, 300); assertThat(getViewportSize()).isEqualTo(new Dimension(250, 300)); - browsingContext.resetViewport(); + browsingContext.setViewport(null, null); assertThat(getViewportSize()).isEqualTo(initialViewportSize); } @Test @NeedsFreshDriver void canSetViewportWithDevicePixelRatio() { + Dimension initialViewportSize = getViewportSize(); + double initialPixelRation = getDevicePixelRatio(); + BrowsingContext browsingContext = new BrowsingContext(driver, driver.getWindowHandle()); driver.get(appServer.whereIs("formPage.html")); @@ -492,6 +491,10 @@ void canSetViewportWithDevicePixelRatio() { assertThat(getViewportSize()).isEqualTo(new Dimension(250, 300)); assertThat(getDevicePixelRatio()).isEqualTo(5.5); + + browsingContext.setViewport(null, null, null); + assertThat(getViewportSize()).isEqualTo(initialViewportSize); + assertThat(getDevicePixelRatio()).isEqualTo(initialPixelRation); } @Test