diff --git a/java/src/org/openqa/selenium/bidi/emulation/BUILD.bazel b/java/src/org/openqa/selenium/bidi/emulation/BUILD.bazel new file mode 100644 index 0000000000000..5a7f05cef4564 --- /dev/null +++ b/java/src/org/openqa/selenium/bidi/emulation/BUILD.bazel @@ -0,0 +1,23 @@ +load("//java:defs.bzl", "java_library") + +java_library( + name = "emulation", + srcs = glob( + [ + "*.java", + ], + ), + visibility = [ + "//java/src/org/openqa/selenium/bidi:__subpackages__", + "//java/src/org/openqa/selenium/remote:__pkg__", + "//java/test/org/openqa/selenium/bidi:__subpackages__", + "//java/test/org/openqa/selenium/grid:__subpackages__", + ], + 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/json", + "//java/src/org/openqa/selenium/remote/http", + ], +) diff --git a/java/src/org/openqa/selenium/bidi/emulation/Emulation.java b/java/src/org/openqa/selenium/bidi/emulation/Emulation.java new file mode 100644 index 0000000000000..6aa3fda1b09c2 --- /dev/null +++ b/java/src/org/openqa/selenium/bidi/emulation/Emulation.java @@ -0,0 +1,46 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you 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 org.openqa.selenium.bidi.emulation; + +import java.util.Map; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.bidi.BiDi; +import org.openqa.selenium.bidi.Command; +import org.openqa.selenium.bidi.HasBiDi; +import org.openqa.selenium.internal.Require; + +public class Emulation { + private final BiDi bidi; + + public Emulation(WebDriver driver) { + Require.nonNull("WebDriver", driver); + + if (!(driver instanceof HasBiDi)) { + throw new IllegalArgumentException("WebDriver must implement BiDi interface"); + } + + this.bidi = ((HasBiDi) driver).getBiDi(); + } + + public Map setGeolocationOverride(SetGeolocationOverrideParameters parameters) { + Require.nonNull("SetGeolocationOverride parameters", parameters); + + return bidi.send( + new Command<>("emulation.setGeolocationOverride", parameters.toMap(), Map.class)); + } +} diff --git a/java/src/org/openqa/selenium/bidi/emulation/GeolocationCoordinates.java b/java/src/org/openqa/selenium/bidi/emulation/GeolocationCoordinates.java new file mode 100644 index 0000000000000..cb8bc8c42b08e --- /dev/null +++ b/java/src/org/openqa/selenium/bidi/emulation/GeolocationCoordinates.java @@ -0,0 +1,118 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you 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 org.openqa.selenium.bidi.emulation; + +import java.util.HashMap; +import java.util.Map; + +public class GeolocationCoordinates { + private final Map map = new HashMap<>(); + + public GeolocationCoordinates(double latitude, double longitude) { + if (latitude < -90.0 || latitude > 90.0) { + throw new IllegalArgumentException("Latitude must be between -90.0 and 90.0"); + } + if (longitude < -180.0 || longitude > 180.0) { + throw new IllegalArgumentException("Longitude must be between -180.0 and 180.0"); + } + + map.put("latitude", latitude); + map.put("longitude", longitude); + map.put("accuracy", 1.0); // Default accuracy + } + + public GeolocationCoordinates accuracy(double accuracy) { + if (accuracy < 0.0) { + throw new IllegalArgumentException("Accuracy must be >= 0.0"); + } + map.put("accuracy", accuracy); + return this; + } + + public GeolocationCoordinates altitude(Double altitude) { + if (altitude != null) { + map.put("altitude", altitude); + } + return this; + } + + public GeolocationCoordinates altitudeAccuracy(Double altitudeAccuracy) { + if (altitudeAccuracy != null) { + if (!map.containsKey("altitude")) { + throw new IllegalArgumentException("altitudeAccuracy cannot be set without altitude"); + } + if (altitudeAccuracy < 0.0) { + throw new IllegalArgumentException("Altitude accuracy must be >= 0.0"); + } + map.put("altitudeAccuracy", altitudeAccuracy); + } + return this; + } + + public GeolocationCoordinates heading(Double heading) { + if (heading != null) { + if (heading < 0.0 || heading >= 360.0) { + throw new IllegalArgumentException("Heading must be between 0.0 and 360.0"); + } + map.put("heading", heading); + } + return this; + } + + public GeolocationCoordinates speed(Double speed) { + if (speed != null) { + if (speed < 0.0) { + throw new IllegalArgumentException("Speed must be >= 0.0"); + } + map.put("speed", speed); + } + return this; + } + + public double getLatitude() { + return (Double) map.get("latitude"); + } + + public double getLongitude() { + return (Double) map.get("longitude"); + } + + public double getAccuracy() { + return (Double) map.get("accuracy"); + } + + public Double getAltitude() { + return (Double) map.get("altitude"); + } + + public Double getAltitudeAccuracy() { + return (Double) map.get("altitudeAccuracy"); + } + + public Double getHeading() { + return (Double) map.get("heading"); + } + + public Double getSpeed() { + return (Double) map.get("speed"); + } + + public Map toMap() { + return Map.copyOf(map); + } +} diff --git a/java/src/org/openqa/selenium/bidi/emulation/GeolocationPositionError.java b/java/src/org/openqa/selenium/bidi/emulation/GeolocationPositionError.java new file mode 100644 index 0000000000000..7acf3faf6df4d --- /dev/null +++ b/java/src/org/openqa/selenium/bidi/emulation/GeolocationPositionError.java @@ -0,0 +1,28 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you 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 org.openqa.selenium.bidi.emulation; + +import java.util.Map; + +public class GeolocationPositionError { + + public Map toMap() { + String type = "positionUnavailable"; + return Map.of("type", type); + } +} diff --git a/java/src/org/openqa/selenium/bidi/emulation/SetGeolocationOverrideParameters.java b/java/src/org/openqa/selenium/bidi/emulation/SetGeolocationOverrideParameters.java new file mode 100644 index 0000000000000..8913ea4d49f51 --- /dev/null +++ b/java/src/org/openqa/selenium/bidi/emulation/SetGeolocationOverrideParameters.java @@ -0,0 +1,72 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you 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 org.openqa.selenium.bidi.emulation; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class SetGeolocationOverrideParameters { + private final Map map = new HashMap<>(); + + // Constructor for coordinates - must specify either contexts or userContexts later + public SetGeolocationOverrideParameters(GeolocationCoordinates coordinates) { + if (coordinates == null) { + throw new IllegalArgumentException("GeolocationCoordinates cannot be null"); + } + map.put("coordinates", coordinates.toMap()); + } + + // Constructor for error - must specify either contexts or userContexts later + public SetGeolocationOverrideParameters(GeolocationPositionError error) { + if (error == null) { + throw new IllegalArgumentException("GeolocationPositionError cannot be null"); + } + map.put("error", error.toMap()); + } + + public SetGeolocationOverrideParameters contexts(List contexts) { + if (contexts == null || contexts.isEmpty()) { + throw new IllegalArgumentException("Contexts cannot be null or empty"); + } + if (map.containsKey("userContexts")) { + throw new IllegalArgumentException("Cannot specify both contexts and userContexts"); + } + map.put("contexts", contexts); + return this; + } + + public SetGeolocationOverrideParameters userContexts(List userContexts) { + if (userContexts == null || userContexts.isEmpty()) { + throw new IllegalArgumentException("User contexts cannot be null or empty"); + } + if (map.containsKey("contexts")) { + throw new IllegalArgumentException("Cannot specify both contexts and userContexts"); + } + map.put("userContexts", userContexts); + return this; + } + + public Map toMap() { + // Validate that either contexts or userContexts is set + if (!map.containsKey("contexts") && !map.containsKey("userContexts")) { + throw new IllegalStateException("Must specify either contexts or userContexts"); + } + return Map.copyOf(map); + } +} diff --git a/java/src/org/openqa/selenium/bidi/module/BUILD.bazel b/java/src/org/openqa/selenium/bidi/module/BUILD.bazel index bb8e51cb62a9d..2cac42b3c72e6 100644 --- a/java/src/org/openqa/selenium/bidi/module/BUILD.bazel +++ b/java/src/org/openqa/selenium/bidi/module/BUILD.bazel @@ -20,6 +20,7 @@ java_library( "//java/src/org/openqa/selenium/bidi", "//java/src/org/openqa/selenium/bidi/browser", "//java/src/org/openqa/selenium/bidi/browsingcontext", + "//java/src/org/openqa/selenium/bidi/emulation", "//java/src/org/openqa/selenium/bidi/log", "//java/src/org/openqa/selenium/bidi/network", "//java/src/org/openqa/selenium/bidi/permissions", diff --git a/java/src/org/openqa/selenium/remote/BUILD.bazel b/java/src/org/openqa/selenium/remote/BUILD.bazel index e1bac58daa94f..0da07320601c2 100644 --- a/java/src/org/openqa/selenium/remote/BUILD.bazel +++ b/java/src/org/openqa/selenium/remote/BUILD.bazel @@ -28,6 +28,7 @@ java_export( "//java/src/org/openqa/selenium/bidi", "//java/src/org/openqa/selenium/bidi:augmenter", "//java/src/org/openqa/selenium/bidi/browsingcontext", + "//java/src/org/openqa/selenium/bidi/emulation", "//java/src/org/openqa/selenium/bidi/log", "//java/src/org/openqa/selenium/bidi/module", "//java/src/org/openqa/selenium/bidi/network", diff --git a/java/test/org/openqa/selenium/bidi/emulation/BUILD.bazel b/java/test/org/openqa/selenium/bidi/emulation/BUILD.bazel new file mode 100644 index 0000000000000..3feda72c9ca32 --- /dev/null +++ b/java/test/org/openqa/selenium/bidi/emulation/BUILD.bazel @@ -0,0 +1,37 @@ +load("@rules_jvm_external//:defs.bzl", "artifact") +load("//java:defs.bzl", "JUNIT5_DEPS", "java_selenium_test_suite") + +java_selenium_test_suite( + name = "large-tests", + size = "large", + srcs = glob(["*Test.java"]), + browsers = [ + "firefox", + "chrome", + "edge", + ], + tags = [ + "selenium-remote", + ], + deps = [ + "//java/src/org/openqa/selenium/bidi", + "//java/src/org/openqa/selenium/bidi/browsingcontext", + "//java/src/org/openqa/selenium/bidi/emulation", + "//java/src/org/openqa/selenium/bidi/log", + "//java/src/org/openqa/selenium/bidi/module", + "//java/src/org/openqa/selenium/bidi/network", + "//java/src/org/openqa/selenium/bidi/script", + "//java/src/org/openqa/selenium/firefox", + "//java/src/org/openqa/selenium/grid/security", + "//java/src/org/openqa/selenium/json", + "//java/src/org/openqa/selenium/remote", + "//java/src/org/openqa/selenium/support", + "//java/test/org/openqa/selenium/environment", + "//java/test/org/openqa/selenium/testing:annotations", + "//java/test/org/openqa/selenium/testing:test-base", + "//java/test/org/openqa/selenium/testing/drivers", + artifact("com.google.guava:guava"), + artifact("org.junit.jupiter:junit-jupiter-api"), + artifact("org.assertj:assertj-core"), + ] + JUNIT5_DEPS, +) diff --git a/java/test/org/openqa/selenium/bidi/emulation/EmulationTest.java b/java/test/org/openqa/selenium/bidi/emulation/EmulationTest.java new file mode 100644 index 0000000000000..366f2498c679b --- /dev/null +++ b/java/test/org/openqa/selenium/bidi/emulation/EmulationTest.java @@ -0,0 +1,199 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you 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 org.openqa.selenium.bidi.emulation; + +import static java.lang.Math.abs; +import static org.openqa.selenium.testing.drivers.Browser.FIREFOX; + +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WindowType; +import org.openqa.selenium.bidi.browsingcontext.BrowsingContext; +import org.openqa.selenium.bidi.browsingcontext.CreateContextParameters; +import org.openqa.selenium.bidi.browsingcontext.ReadinessState; +import org.openqa.selenium.bidi.module.Browser; +import org.openqa.selenium.bidi.module.Permission; +import org.openqa.selenium.bidi.permissions.PermissionState; +import org.openqa.selenium.testing.Ignore; +import org.openqa.selenium.testing.JupiterTestBase; +import org.openqa.selenium.testing.NeedsFreshDriver; +import org.openqa.selenium.testing.NeedsSecureServer; + +@NeedsSecureServer +class EmulationTest extends JupiterTestBase { + Object getBrowserGeolocation(WebDriver driver, String userContext, String origin) { + JavascriptExecutor executor = (JavascriptExecutor) driver; + Permission permission = new Permission(driver); + + permission.setPermission( + Map.of("name", "geolocation"), PermissionState.GRANTED, origin, userContext); + + return executor.executeAsyncScript( + "const callback = arguments[arguments.length - 1];\n" + + " navigator.geolocation.getCurrentPosition(\n" + + " position => {\n" + + " const coords = position.coords;\n" + + " callback({\n" + + " latitude: coords.latitude,\n" + + " longitude: coords.longitude,\n" + + " accuracy: coords.accuracy,\n" + + " altitude: coords.altitude,\n" + + " altitudeAccuracy: coords.altitudeAccuracy,\n" + + " heading: coords.heading,\n" + + " speed: coords.speed,\n" + + " timestamp: position.timestamp\n" + + " });\n" + + " },\n" + + " error => {\n" + + " callback({ error: error.message });\n" + + " },\n" + + " { enableHighAccuracy: false, timeout: 10000, maximumAge: 0 }\n" + + " );"); + } + + @Test + @NeedsFreshDriver + void canSetGeolocationOverrideWithCoordinatesInContext() { + BrowsingContext context = new BrowsingContext(driver, driver.getWindowHandle()); + String contextId = context.getId(); + + String url = appServer.whereIsSecure("blank.html"); + context.navigate(url, ReadinessState.COMPLETE); + driver.switchTo().window(context.getId()); + + String origin = + (String) ((JavascriptExecutor) driver).executeScript("return window.location.origin;"); + + Emulation emul = new Emulation(driver); + GeolocationCoordinates coords = new GeolocationCoordinates(37.7749, -122.4194); + emul.setGeolocationOverride( + new SetGeolocationOverrideParameters(coords).contexts(List.of(contextId))); + + Object result = getBrowserGeolocation(driver, null, origin); + Map r = ((Map) result); + + assert !r.containsKey("error") : "Geolocation failed with error: " + r.get("error"); + + double latitude = ((Number) r.get("latitude")).doubleValue(); + double longitude = ((Number) r.get("longitude")).doubleValue(); + double accuracy = ((Number) r.get("accuracy")).doubleValue(); + + assert abs(latitude - coords.getLatitude()) < 0.0001 + : "Latitude mismatch: expected " + coords.getLatitude() + ", got " + latitude; + assert abs(longitude - coords.getLongitude()) < 0.0001 + : "Longitude mismatch: expected " + coords.getLongitude() + ", got " + longitude; + assert abs(accuracy - coords.getAccuracy()) < 0.0001 + : "Accuracy mismatch: expected " + coords.getAccuracy() + ", got " + accuracy; + } + + @Test + void canSetGeolocationOverrideWithMultipleUserContexts() { + Browser browser = new Browser(driver); + String userContext1 = browser.createUserContext(); + String userContext2 = browser.createUserContext(); + + BrowsingContext context1 = + new BrowsingContext( + driver, new CreateContextParameters(WindowType.TAB).userContext(userContext1)); + BrowsingContext context2 = + new BrowsingContext( + driver, new CreateContextParameters(WindowType.TAB).userContext(userContext2)); + + GeolocationCoordinates coords = new GeolocationCoordinates(45.5, -122.4194); + + Emulation emulation = new Emulation(driver); + emulation.setGeolocationOverride( + new SetGeolocationOverrideParameters(coords) + .userContexts(List.of(userContext1, userContext2))); + + driver.switchTo().window(context1.getId()); + String url1 = appServer.whereIsSecure("blank.html"); + context1.navigate(url1, ReadinessState.COMPLETE); + + String origin1 = + (String) ((JavascriptExecutor) driver).executeScript("return window.location.origin;"); + + Map r = + (Map) getBrowserGeolocation(driver, userContext1, origin1); + + assert !r.containsKey("error") : "Context1 geolocation failed with error: " + r.get("error"); + + double latitude1 = ((Number) r.get("latitude")).doubleValue(); + double longitude1 = ((Number) r.get("longitude")).doubleValue(); + double accuracy1 = ((Number) r.get("accuracy")).doubleValue(); + + assert abs(latitude1 - coords.getLatitude()) < 0.0001 : "Context1 latitude mismatch"; + assert abs(longitude1 - coords.getLongitude()) < 0.0001 : "Context1 longitude mismatch"; + assert abs(accuracy1 - coords.getAccuracy()) < 0.0001 : "Context1 accuracy mismatch"; + + driver.switchTo().window(context2.getId()); + String url2 = appServer.whereIsSecure("blank.html"); + context2.navigate(url2, ReadinessState.COMPLETE); + + String origin2 = + (String) ((JavascriptExecutor) driver).executeScript("return window.location.origin;"); + + Map r2 = + (Map) getBrowserGeolocation(driver, userContext2, origin2); + + assert !r2.containsKey("error") : "Context2 geolocation failed with error: " + r2.get("error"); + + double latitude2 = ((Number) r2.get("latitude")).doubleValue(); + double longitude2 = ((Number) r2.get("longitude")).doubleValue(); + double accuracy2 = ((Number) r2.get("accuracy")).doubleValue(); + + assert abs(latitude2 - coords.getLatitude()) < 0.0001 : "Context2 latitude mismatch"; + assert abs(longitude2 - coords.getLongitude()) < 0.0001 : "Context2 longitude mismatch"; + assert abs(accuracy2 - coords.getAccuracy()) < 0.0001 : "Context2 accuracy mismatch"; + + context1.close(); + context2.close(); + browser.removeUserContext(userContext1); + browser.removeUserContext(userContext2); + } + + @Test + @Ignore(FIREFOX) + void canSetGeolocationOverrideWithError() { + + BrowsingContext context = new BrowsingContext(driver, WindowType.TAB); + String contextId = context.getId(); + + String url = appServer.whereIsSecure("blank.html"); + context.navigate(url, ReadinessState.COMPLETE); + driver.switchTo().window(contextId); + + String origin = + (String) ((JavascriptExecutor) driver).executeScript("return window.location.origin;"); + + GeolocationPositionError error = new GeolocationPositionError(); + Emulation emul = new Emulation(driver); + emul.setGeolocationOverride( + new SetGeolocationOverrideParameters(error).contexts(List.of(contextId))); + + Object result = getBrowserGeolocation(driver, null, origin); + Map r = ((Map) result); + + assert r.containsKey("error") : "Expected geolocation to fail with error, but got: " + r; + + context.close(); + } +} diff --git a/java/test/org/openqa/selenium/testing/drivers/Browser.java b/java/test/org/openqa/selenium/testing/drivers/Browser.java index 6ba4f073a3283..fb3e67324ec9b 100644 --- a/java/test/org/openqa/selenium/testing/drivers/Browser.java +++ b/java/test/org/openqa/selenium/testing/drivers/Browser.java @@ -60,6 +60,9 @@ public Capabilities getCapabilities() { "no-sandbox", "disable-search-engine-choice-screen"); + // Accept self-signed and invalid certificates for HTTPS testing + options.setAcceptInsecureCerts(true); + Map prefs = new HashMap<>(); prefs.put("exit_type", "None"); prefs.put("exited_cleanly", true); @@ -94,6 +97,9 @@ public Capabilities getCapabilities() { "disable-dev-shm-usage", "no-sandbox"); + // Accept self-signed and invalid certificates for HTTPS testing + options.setAcceptInsecureCerts(true); + Map prefs = new HashMap<>(); prefs.put("exit_type", "None"); prefs.put("exited_cleanly", true);