diff --git a/java/src/org/openqa/selenium/bidi/BUILD.bazel b/java/src/org/openqa/selenium/bidi/BUILD.bazel index 99ba289e08cdb..a5ce3ff6b3f05 100644 --- a/java/src/org/openqa/selenium/bidi/BUILD.bazel +++ b/java/src/org/openqa/selenium/bidi/BUILD.bazel @@ -6,6 +6,7 @@ java_library( srcs = glob([ "*.java", "log/*.java", + "browsingcontext/*.java" ]), visibility = [ "//java/src/org/openqa/selenium/bidi:__subpackages__", diff --git a/java/src/org/openqa/selenium/bidi/browsingcontext/BrowsingContext.java b/java/src/org/openqa/selenium/bidi/browsingcontext/BrowsingContext.java new file mode 100644 index 0000000000000..98700e9a3e2e2 --- /dev/null +++ b/java/src/org/openqa/selenium/bidi/browsingcontext/BrowsingContext.java @@ -0,0 +1,241 @@ +// 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.browsingcontext; + +import com.google.common.collect.ImmutableMap; + +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WindowType; +import org.openqa.selenium.bidi.BiDi; +import org.openqa.selenium.bidi.Command; +import org.openqa.selenium.bidi.HasBiDi; +import org.openqa.selenium.internal.Require; +import org.openqa.selenium.json.Json; +import org.openqa.selenium.json.JsonInput; +import org.openqa.selenium.json.TypeToken; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +public class BrowsingContext { + + private final String id; + private final BiDi bidi; + private static final String CONTEXT = "context"; + private static final String RELOAD = "browsingContext.reload"; + private static final String HANDLE_USER_PROMPT = "browsingContext.handleUserPrompt"; + + protected static final Type LIST_OF_BROWSING_CONTEXT_INFO = + new TypeToken>() {}.getType(); + + private final Function browsingContextIdMapper = jsonInput -> { + Map result = jsonInput.read(Map.class); + return result.getOrDefault(CONTEXT, "").toString(); + }; + + private final Function navigationInfoMapper = + jsonInput -> (NavigationResult) jsonInput.read(NavigationResult.class); + + private final Function> browsingContextInfoListMapper = + jsonInput -> { + Map result = jsonInput.read(Map.class); + List contexts = (List) result.getOrDefault("contexts", new ArrayList<>()); + + if (contexts.isEmpty()) { + return new ArrayList<>(); + } + + Json json = new Json(); + String dtr = json.toJson(contexts); + + return json.toType(dtr, LIST_OF_BROWSING_CONTEXT_INFO); + }; + + public BrowsingContext(WebDriver driver, String id) { + Require.nonNull("WebDriver", driver); + Require.nonNull("Browsing Context id", id); + + if (!(driver instanceof HasBiDi)) { + throw new IllegalArgumentException("WebDriver instance must support BiDi protocol"); + } + + this.bidi = ((HasBiDi) driver).getBiDi(); + this.id = id; + } + + public BrowsingContext(WebDriver driver, WindowType type) { + Require.nonNull("WebDriver", driver); + + if (!(driver instanceof HasBiDi)) { + throw new IllegalArgumentException("WebDriver instance must support BiDi protocol"); + } + + this.bidi = ((HasBiDi) driver).getBiDi(); + this.id = this.create(type); + } + + public BrowsingContext(WebDriver driver, WindowType type, String referenceContextId) { + Require.nonNull("WebDriver", driver); + Require.nonNull("Reference browsing context id", referenceContextId); + if (!(driver instanceof HasBiDi)) { + throw new IllegalArgumentException("WebDriver instance must support BiDi protocol"); + } + + this.bidi = ((HasBiDi) driver).getBiDi(); + this.id = this.create(type, referenceContextId); + } + + public String getId() { + return this.id; + } + + private String create(WindowType type) { + return this.bidi.send( + new Command<>("browsingContext.create", + ImmutableMap.of("type", type.toString()), + browsingContextIdMapper)); + } + + private String create(WindowType type, String referenceContext) { + return this.bidi.send( + new Command<>("browsingContext.create", + ImmutableMap.of("type", type.toString(), + "referenceContext", referenceContext), + browsingContextIdMapper)); + } + + public NavigationResult navigate(String url) { + return this.bidi.send( + new Command<>("browsingContext.navigate", + ImmutableMap.of(CONTEXT, id, + "url", url), + navigationInfoMapper)); + } + + public NavigationResult navigate(String url, ReadinessState readinessState) { + return this.bidi.send( + new Command<>("browsingContext.navigate", + ImmutableMap.of(CONTEXT, id, + "url", url, + "wait", readinessState.toString()), + navigationInfoMapper)); + } + + public List getTree() { + return this.bidi.send( + new Command<>("browsingContext.getTree", + ImmutableMap.of("root", id), + browsingContextInfoListMapper)); + } + + public List getTree(int maxDepth) { + return this.bidi.send( + new Command<>("browsingContext.getTree", + ImmutableMap.of("root", id, + "maxDepth", maxDepth), + browsingContextInfoListMapper)); + } + + public List getTopLevelContexts() { + return this.bidi.send( + new Command<>("browsingContext.getTree", + new HashMap<>(), + browsingContextInfoListMapper)); + } + + // Yet to be implemented by browser vendors + private void reload() { + this.bidi.send(new Command<>(RELOAD, ImmutableMap.of(CONTEXT, id))); + } + + // Yet to be implemented by browser vendors + private void reload(boolean ignoreCache) { + this.bidi.send(new Command<>( + RELOAD, + ImmutableMap.of(CONTEXT, id, + "ignoreCache", ignoreCache))); + } + + // Yet to be implemented by browser vendors + private void reload(ReadinessState readinessState) { + this.bidi.send(new Command<>( + RELOAD, + ImmutableMap.of(CONTEXT, id, + "wait", readinessState.toString()))); + } + + // Yet to be implemented by browser vendors + private void reload(boolean ignoreCache, ReadinessState readinessState) { + this.bidi.send(new Command<>( + RELOAD, + ImmutableMap.of(CONTEXT, id, + "ignoreCache", ignoreCache, + "wait", readinessState.toString()))); + } + + // Yet to be implemented by browser vendors + private void handleUserPrompt() { + this.bidi.send(new Command<>( + HANDLE_USER_PROMPT, + ImmutableMap.of(CONTEXT, id))); + } + + // Yet to be implemented by browser vendors + private void handleUserPrompt(String userText) { + this.bidi.send(new Command<>( + HANDLE_USER_PROMPT, + ImmutableMap.of(CONTEXT, id, + "userText", userText))); + + } + + // Yet to be implemented by browser vendors + private void handleUserPrompt(boolean accept, String userText) { + this.bidi.send(new Command<>( + HANDLE_USER_PROMPT, + ImmutableMap.of(CONTEXT, id, + "accept", accept, + "userText", userText))); + + } + + // Yet to be implemented by browser vendors + private String captureScreenshot() { + return this.bidi.send(new Command<>( + HANDLE_USER_PROMPT, + ImmutableMap.of(CONTEXT, id), + jsonInput -> { + Map result = jsonInput.read(Map.class); + return (String) result.get("data"); + } + )); + } + + public void close() { + // This might need more clean up actions once the behavior is defined. + // Specially when last tab or window is closed. + // Refer: https://github.com/w3c/webdriver-bidi/issues/187 + this.bidi.send(new Command<>( + "browsingContext.close", + ImmutableMap.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 new file mode 100644 index 0000000000000..2806eae6b5a09 --- /dev/null +++ b/java/src/org/openqa/selenium/bidi/browsingcontext/BrowsingContextInfo.java @@ -0,0 +1,95 @@ +// 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.browsingcontext; + +import static org.openqa.selenium.bidi.browsingcontext.BrowsingContext.LIST_OF_BROWSING_CONTEXT_INFO; + +import org.openqa.selenium.json.JsonInput; + +import java.util.List; + +public class BrowsingContextInfo { + + private final String id; + + private final String url; + + private final List children; + + private final String parentBrowsingContext; + + public String getId() { + return id; + } + + public String getUrl() { + return url; + } + + public List getChildren() { + return children; + } + + public String getParentBrowsingContext() { + return parentBrowsingContext; + } + + public BrowsingContextInfo( + String id, String url, List children, String parentBrowsingContext) { + this.id = id; + this.url = url; + this.children = children; + this.parentBrowsingContext = parentBrowsingContext; + } + + public static BrowsingContextInfo fromJson(JsonInput input) { + String id = null; + String url = null; + List children = null; + String parentBrowsingContext = null; + + input.beginObject(); + while (input.hasNext()) { + switch (input.nextName()) { + case "context": + id = input.read(String.class); + break; + + case "url": + url = input.read(String.class); + break; + + case "children": + children = input.read(LIST_OF_BROWSING_CONTEXT_INFO); + break; + + case "parent": + parentBrowsingContext = input.read(String.class); + break; + + default: + input.skipValue(); + break; + } + } + + input.endObject(); + + return new BrowsingContextInfo(id, url, children, parentBrowsingContext); + } +} diff --git a/java/src/org/openqa/selenium/bidi/browsingcontext/NavigationResult.java b/java/src/org/openqa/selenium/bidi/browsingcontext/NavigationResult.java new file mode 100644 index 0000000000000..420942311bf2c --- /dev/null +++ b/java/src/org/openqa/selenium/bidi/browsingcontext/NavigationResult.java @@ -0,0 +1,66 @@ +// 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.browsingcontext; + +import org.openqa.selenium.json.JsonInput; + +public class NavigationResult { + + private final String navigationId; + private final String url; + + public String getNavigationId() { + return navigationId; + } + + public String getUrl() { + return url; + } + + public NavigationResult(String navigationId, String url) { + this.navigationId = navigationId; + this.url = url; + } + + + public static NavigationResult fromJson(JsonInput input) { + String navigationId = null; + String url = null; + + input.beginObject(); + while (input.hasNext()) { + switch (input.nextName()) { + case "navigation": + navigationId = input.read(String.class); + break; + + case "url": + url = input.read(String.class); + break; + + default: + input.skipValue(); + break; + } + } + + input.endObject(); + + return new NavigationResult(navigationId, url); + } +} diff --git a/java/src/org/openqa/selenium/bidi/browsingcontext/ReadinessState.java b/java/src/org/openqa/selenium/bidi/browsingcontext/ReadinessState.java new file mode 100644 index 0000000000000..1cc6e11dfa8e4 --- /dev/null +++ b/java/src/org/openqa/selenium/bidi/browsingcontext/ReadinessState.java @@ -0,0 +1,37 @@ +// 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.browsingcontext; + +public enum ReadinessState { + + NONE("none"), + INTERACTIVE("interactive"), + COMPLETE("complete"); + + private final String text; + + ReadinessState(String text) { + this.text = text; + } + + @Override + public String toString() { + return String.valueOf(text); + } + +} diff --git a/java/test/org/openqa/selenium/bidi/browsingcontext/BUILD.bazel b/java/test/org/openqa/selenium/bidi/browsingcontext/BUILD.bazel new file mode 100644 index 0000000000000..d4bca13992800 --- /dev/null +++ b/java/test/org/openqa/selenium/bidi/browsingcontext/BUILD.bazel @@ -0,0 +1,30 @@ +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", + ], + tags = [ + "selenium-remote", + ], + deps = [ + "//java/src/org/openqa/selenium/bidi", + "//java/src/org/openqa/selenium/grid/security", + "//java/src/org/openqa/selenium/firefox", + "//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"), + artifact("org.hamcrest:hamcrest"), + ] + JUNIT5_DEPS, +) diff --git a/java/test/org/openqa/selenium/bidi/browsingcontext/BrowsingContextTest.java b/java/test/org/openqa/selenium/bidi/browsingcontext/BrowsingContextTest.java new file mode 100644 index 0000000000000..f5ce8d1535cc7 --- /dev/null +++ b/java/test/org/openqa/selenium/bidi/browsingcontext/BrowsingContextTest.java @@ -0,0 +1,187 @@ +// 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.browsingcontext; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; +import static org.openqa.selenium.testing.Safely.safelyCall; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.WindowType; +import org.openqa.selenium.bidi.BiDiException; +import org.openqa.selenium.environment.webserver.AppServer; +import org.openqa.selenium.environment.webserver.NettyAppServer; +import org.openqa.selenium.firefox.FirefoxDriver; +import org.openqa.selenium.firefox.FirefoxOptions; + +import java.io.IOException; +import java.util.List; + +class BrowsingContextTest { + + private AppServer server; + private FirefoxDriver driver; + + @BeforeEach + public void setUp() { + FirefoxOptions options = new FirefoxOptions(); + options.setCapability("webSocketUrl", true); + + driver = new FirefoxDriver(options); + + server = new NettyAppServer(); + server.start(); + } + + @Test + void canCreateABrowsingContextForGivenId() { + String id = driver.getWindowHandle(); + BrowsingContext browsingContext = new BrowsingContext(driver, id); + assertThat(browsingContext.getId()).isEqualTo(id); + } + + @Test + void canCreateAWindow() { + BrowsingContext browsingContext = new BrowsingContext(driver, WindowType.WINDOW); + assertThat(browsingContext.getId()).isNotEmpty(); + } + + @Test + void canCreateAWindowWithAReferenceContext() { + BrowsingContext + browsingContext = + new BrowsingContext(driver, WindowType.WINDOW, driver.getWindowHandle()); + assertThat(browsingContext.getId()).isNotEmpty(); + } + + @Test + void canCreateATab() { + BrowsingContext browsingContext = new BrowsingContext(driver, WindowType.TAB); + assertThat(browsingContext.getId()).isNotEmpty(); + } + + @Test + void canCreateATabWithAReferenceContext() { + BrowsingContext + browsingContext = + new BrowsingContext(driver, WindowType.TAB, driver.getWindowHandle()); + assertThat(browsingContext.getId()).isNotEmpty(); + } + + @Test + void canNavigateToAUrl() { + BrowsingContext browsingContext = new BrowsingContext(driver, WindowType.TAB); + + String url = server.whereIs("/bidi/logEntryAdded.html"); + NavigationResult info = browsingContext.navigate(url); + + assertThat(browsingContext.getId()).isNotEmpty(); + assertThat(info.getNavigationId()).isNull(); + assertThat(info.getUrl()).contains("/bidi/logEntryAdded.html"); + } + + @Test + void canNavigateToAUrlWithReadinessState() { + BrowsingContext browsingContext = new BrowsingContext(driver, WindowType.TAB); + + String url = server.whereIs("/bidi/logEntryAdded.html"); + NavigationResult info = browsingContext.navigate(url, ReadinessState.COMPLETE); + + assertThat(browsingContext.getId()).isNotEmpty(); + assertThat(info.getNavigationId()).isNull(); + assertThat(info.getUrl()).contains("/bidi/logEntryAdded.html"); + } + + @Test + void canGetTreeWithAChild() { + String referenceContextId = driver.getWindowHandle(); + BrowsingContext parentWindow = new BrowsingContext(driver, referenceContextId); + + String url = server.whereIs("iframes.html"); + + parentWindow.navigate(url, ReadinessState.COMPLETE); + + List contextInfoList = parentWindow.getTree(); + + assertThat(contextInfoList.size()).isEqualTo(1); + BrowsingContextInfo info = contextInfoList.get(0); + assertThat(info.getChildren().size()).isEqualTo(1); + assertThat(info.getId()).isEqualTo(referenceContextId); + assertThat(info.getChildren().get(0).getUrl()).contains("formPage.html"); + } + + @Test + void canGetTreeWithDepth() { + String referenceContextId = driver.getWindowHandle(); + BrowsingContext parentWindow = new BrowsingContext(driver, referenceContextId); + + String url = server.whereIs("iframes.html"); + + parentWindow.navigate(url, ReadinessState.COMPLETE); + + List contextInfoList = parentWindow.getTree(0); + + assertThat(contextInfoList.size()).isEqualTo(1); + BrowsingContextInfo info = contextInfoList.get(0); + assertThat(info.getChildren()).isNull(); // since depth is 0 + assertThat(info.getId()).isEqualTo(referenceContextId); + } + + @Test + void canGetAllTopLevelContexts() { + BrowsingContext window1 = new BrowsingContext(driver, driver.getWindowHandle()); + BrowsingContext window2 = new BrowsingContext(driver, WindowType.WINDOW); + + List contextInfoList = window1.getTopLevelContexts(); + + assertThat(contextInfoList.size()).isEqualTo(2); + } + + @Test + void canCloseAWindow() { + BrowsingContext window1 = new BrowsingContext(driver, WindowType.WINDOW); + BrowsingContext window2 = new BrowsingContext(driver, WindowType.WINDOW); + + window2.close(); + + assertThatExceptionOfType(BiDiException.class).isThrownBy(window2::getTree); + } + + @Test + void canCloseATab() { + BrowsingContext tab1 = new BrowsingContext(driver, WindowType.TAB); + BrowsingContext tab2 = new BrowsingContext(driver, WindowType.TAB); + + tab2.close(); + + assertThatExceptionOfType(BiDiException.class).isThrownBy(tab2::getTree); + } + + // TODO: Add a test for closing the last tab once the behavior is finalized + // Refer: https://github.com/w3c/webdriver-bidi/issues/187 + + @AfterEach + public void quitDriver() { + if (driver != null) { + driver.quit(); + } + safelyCall(server::stop); + } +}