diff --git a/java/src/org/openqa/selenium/bidi/browsingcontext/BrowsingContext.java b/java/src/org/openqa/selenium/bidi/browsingcontext/BrowsingContext.java index f4eb2b7284b43..1aad5ac75b555 100644 --- a/java/src/org/openqa/selenium/bidi/browsingcontext/BrowsingContext.java +++ b/java/src/org/openqa/selenium/bidi/browsingcontext/BrowsingContext.java @@ -17,6 +17,7 @@ package org.openqa.selenium.bidi.browsingcontext; +import java.io.StringReader; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.HashMap; @@ -28,6 +29,7 @@ import org.openqa.selenium.bidi.BiDi; import org.openqa.selenium.bidi.Command; import org.openqa.selenium.bidi.HasBiDi; +import org.openqa.selenium.bidi.script.RemoteValue; import org.openqa.selenium.internal.Require; import org.openqa.selenium.json.Json; import org.openqa.selenium.json.JsonInput; @@ -36,6 +38,8 @@ public class BrowsingContext { + private static final Json JSON = new Json(); + private final String id; private final BiDi bidi; private static final String CONTEXT = "context"; @@ -338,6 +342,22 @@ public void forward() { this.traverseHistory(1); } + public List locateNodes(LocateNodeParameters parameters) { + Map params = new HashMap<>(parameters.toMap()); + params.put("context", id); + return this.bidi.send( + new Command<>( + "browsingContext.locateNodes", + params, + jsonInput -> { + Map result = jsonInput.read(Map.class); + try (StringReader reader = new StringReader(JSON.toJson(result.get("nodes"))); + JsonInput input = JSON.newInput(reader)) { + return input.read(new TypeToken>() {}.getType()); + } + })); + } + public void close() { // This might need more clean up actions once the behavior is defined. // Specially when last tab or window is closed. diff --git a/java/src/org/openqa/selenium/bidi/browsingcontext/LocateNodeParameters.java b/java/src/org/openqa/selenium/bidi/browsingcontext/LocateNodeParameters.java new file mode 100644 index 0000000000000..cfdcdc2b3f03e --- /dev/null +++ b/java/src/org/openqa/selenium/bidi/browsingcontext/LocateNodeParameters.java @@ -0,0 +1,107 @@ +// 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 java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.openqa.selenium.bidi.script.RemoteReference; +import org.openqa.selenium.bidi.script.ResultOwnership; +import org.openqa.selenium.bidi.script.SerializationOptions; + +public class LocateNodeParameters { + + private final Locator locator; + private final Optional maxNodeCount; + private final Optional ownership; + private final Optional sandbox; + private final Optional serializationOptions; + private final Optional> startNodes; + + private LocateNodeParameters(Builder builder) { + this.locator = builder.locator; + this.maxNodeCount = Optional.ofNullable(builder.maxNodeCount); + this.ownership = Optional.ofNullable(builder.ownership); + this.sandbox = Optional.ofNullable(builder.sandbox); + this.serializationOptions = Optional.ofNullable(builder.serializationOptions); + this.startNodes = Optional.ofNullable(builder.startNodes); + } + + public Map toMap() { + final Map map = new HashMap<>(); + + map.put("locator", locator.toMap()); + maxNodeCount.ifPresent(value -> map.put("maxNodeCount", value)); + ownership.ifPresent(value -> map.put("ownership", value.toString())); + sandbox.ifPresent(value -> map.put("sandbox", value)); + serializationOptions.ifPresent(value -> map.put("serializationOptions", value.toJson())); + startNodes.ifPresent( + value -> { + List> startNodesJson = new ArrayList<>(); + value.forEach(remoteNode -> startNodesJson.add(remoteNode.toJson())); + map.put("startNodes", startNodesJson); + }); + + return map; + } + + public static class Builder { + + private final Locator locator; + private Long maxNodeCount = null; + private ResultOwnership ownership; + private String sandbox; + private SerializationOptions serializationOptions; + private List startNodes; + + public Builder(Locator locator) { + this.locator = locator; + } + + public Builder setMaxNodeCount(long maxNodeCount) { + this.maxNodeCount = maxNodeCount; + return this; + } + + public Builder setOwnership(ResultOwnership ownership) { + this.ownership = ownership; + return this; + } + + public Builder setSandbox(String sandbox) { + this.sandbox = sandbox; + return this; + } + + public Builder setSerializationOptions(SerializationOptions options) { + this.serializationOptions = options; + return this; + } + + public Builder setStartNodes(List startNodes) { + this.startNodes = startNodes; + return this; + } + + public LocateNodeParameters build() { + return new LocateNodeParameters(this); + } + } +} diff --git a/java/src/org/openqa/selenium/bidi/browsingcontext/Locator.java b/java/src/org/openqa/selenium/bidi/browsingcontext/Locator.java new file mode 100644 index 0000000000000..fabfc5abc5024 --- /dev/null +++ b/java/src/org/openqa/selenium/bidi/browsingcontext/Locator.java @@ -0,0 +1,101 @@ +// 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 java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public class Locator { + private enum Type { + CSS("css"), + INNER("innerText"), + XPATH("xpath"); + + private final String value; + + Type(String value) { + this.value = value; + } + + @Override + public String toString() { + return value; + } + } + + private final Type type; + + private final String value; + + private Optional ignoreCase = Optional.empty(); + + private Optional matchType = Optional.empty(); + + private Optional maxDepth = Optional.empty(); + + private Locator(Type type, String value) { + this.type = type; + this.value = value; + } + + public Locator( + Type type, + String value, + Optional ignoreCase, + Optional matchType, + Optional maxDepth) { + this.type = type; + this.value = value; + this.ignoreCase = ignoreCase; + this.matchType = matchType; + this.maxDepth = maxDepth; + } + + public static Locator css(String value) { + return new Locator(Type.CSS, value); + } + + public static Locator innerText( + String value, + Optional ignoreCase, + Optional matchType, + Optional maxDepth) { + return new Locator(Type.INNER, value, ignoreCase, matchType, maxDepth); + } + + public static Locator innerText(String value) { + return new Locator(Type.INNER, value); + } + + public static Locator xpath(String value) { + return new Locator(Type.XPATH, value); + } + + public Map toMap() { + final Map map = new HashMap<>(); + map.put("type", type.toString()); + map.put("value", value); + + ignoreCase.ifPresent(val -> map.put("ignoreCase", val)); + matchType.ifPresent(val -> map.put("matchType", val)); + maxDepth.ifPresent(val -> map.put("maxDepth", val)); + + return map; + } +} diff --git a/java/src/org/openqa/selenium/bidi/script/RemoteValue.java b/java/src/org/openqa/selenium/bidi/script/RemoteValue.java index ad6932843f5ff..eeab36108c1bd 100644 --- a/java/src/org/openqa/selenium/bidi/script/RemoteValue.java +++ b/java/src/org/openqa/selenium/bidi/script/RemoteValue.java @@ -197,6 +197,7 @@ private static Object deserializeValue(Object value, Type type) { switch (type) { case ARRAY: + case NODE_LIST: case SET: try (StringReader reader = new StringReader(JSON.toJson(value)); JsonInput input = JSON.newInput(reader)) { diff --git a/java/test/org/openqa/selenium/bidi/browsingcontext/LocateNodesTest.java b/java/test/org/openqa/selenium/bidi/browsingcontext/LocateNodesTest.java new file mode 100644 index 0000000000000..7c984da6cb788 --- /dev/null +++ b/java/test/org/openqa/selenium/bidi/browsingcontext/LocateNodesTest.java @@ -0,0 +1,322 @@ +// 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.openqa.selenium.testing.Safely.safelyCall; +import static org.openqa.selenium.testing.drivers.Browser.CHROME; +import static org.openqa.selenium.testing.drivers.Browser.EDGE; +import static org.openqa.selenium.testing.drivers.Browser.FIREFOX; +import static org.openqa.selenium.testing.drivers.Browser.IE; +import static org.openqa.selenium.testing.drivers.Browser.SAFARI; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.bidi.Script; +import org.openqa.selenium.bidi.script.EvaluateResult; +import org.openqa.selenium.bidi.script.EvaluateResultSuccess; +import org.openqa.selenium.bidi.script.LocalValue; +import org.openqa.selenium.bidi.script.NodeProperties; +import org.openqa.selenium.bidi.script.RemoteReference; +import org.openqa.selenium.bidi.script.RemoteValue; +import org.openqa.selenium.bidi.script.ResultOwnership; +import org.openqa.selenium.environment.webserver.AppServer; +import org.openqa.selenium.environment.webserver.NettyAppServer; +import org.openqa.selenium.testing.JupiterTestBase; +import org.openqa.selenium.testing.NotYetImplemented; + +public class LocateNodesTest extends JupiterTestBase { + private AppServer server; + + @BeforeEach + public void setUp() { + server = new NettyAppServer(); + server.start(); + } + + @Test + @NotYetImplemented(SAFARI) + @NotYetImplemented(IE) + @NotYetImplemented(CHROME) + @NotYetImplemented(EDGE) + @NotYetImplemented(FIREFOX) + void canLocateNodes() { + BrowsingContext browsingContext = new BrowsingContext(driver, driver.getWindowHandle()); + assertThat(browsingContext.getId()).isNotEmpty(); + + driver.get(pages.xhtmlTestPage); + + LocateNodeParameters parameters = new LocateNodeParameters.Builder(Locator.css("div")).build(); + + List elements = browsingContext.locateNodes(parameters); + assertThat(elements.size()).isEqualTo(13); + } + + @Test + @NotYetImplemented(SAFARI) + @NotYetImplemented(IE) + @NotYetImplemented(CHROME) + @NotYetImplemented(EDGE) + @NotYetImplemented(FIREFOX) + void canLocateNodesWithCSSLocator() { + BrowsingContext browsingContext = new BrowsingContext(driver, driver.getWindowHandle()); + assertThat(browsingContext.getId()).isNotEmpty(); + + driver.get(pages.xhtmlTestPage); + + LocateNodeParameters parameters = + new LocateNodeParameters.Builder(Locator.css("div.extraDiv, div.content")) + .setMaxNodeCount(1) + .build(); + + List elements = browsingContext.locateNodes(parameters); + assertThat(elements.size()).isGreaterThanOrEqualTo(1); + + RemoteValue value = elements.get(0); + assertThat(value.getType()).isEqualTo("node"); + assertThat(value.getValue().isPresent()).isTrue(); + NodeProperties properties = (NodeProperties) value.getValue().get(); + assertThat(properties.getLocalName().get()).isEqualTo("div"); + assertThat(properties.getAttributes().get().size()).isEqualTo(1); + assertThat(properties.getAttributes().get().get("class")).isEqualTo("content"); + } + + @Test + @NotYetImplemented(SAFARI) + @NotYetImplemented(IE) + @NotYetImplemented(CHROME) + @NotYetImplemented(EDGE) + @NotYetImplemented(FIREFOX) + void canLocateNodesWithXPathLocator() { + BrowsingContext browsingContext = new BrowsingContext(driver, driver.getWindowHandle()); + assertThat(browsingContext.getId()).isNotEmpty(); + + driver.get(pages.xhtmlTestPage); + + LocateNodeParameters parameters = + new LocateNodeParameters.Builder(Locator.xpath("/html/body/div[2]")) + .setMaxNodeCount(1) + .build(); + + List elements = browsingContext.locateNodes(parameters); + assertThat(elements.size()).isGreaterThanOrEqualTo(1); + + RemoteValue value = elements.get(0); + assertThat(value.getType()).isEqualTo("node"); + assertThat(value.getValue().isPresent()).isTrue(); + NodeProperties properties = (NodeProperties) value.getValue().get(); + assertThat(properties.getLocalName().get()).isEqualTo("div"); + assertThat(properties.getAttributes().get().size()).isEqualTo(1); + assertThat(properties.getAttributes().get().get("class")).isEqualTo("content"); + } + + @Test + @NotYetImplemented(SAFARI) + @NotYetImplemented(IE) + @NotYetImplemented(CHROME) + @NotYetImplemented(EDGE) + @NotYetImplemented(FIREFOX) + void canLocateNodesWithInnerText() { + BrowsingContext browsingContext = new BrowsingContext(driver, driver.getWindowHandle()); + assertThat(browsingContext.getId()).isNotEmpty(); + + driver.get(pages.xhtmlTestPage); + + LocateNodeParameters parameters = + new LocateNodeParameters.Builder(Locator.innerText("Spaced out")) + .setMaxNodeCount(1) + .build(); + + List elements = browsingContext.locateNodes(parameters); + assertThat(elements.size()).isGreaterThanOrEqualTo(1); + + RemoteValue value = elements.get(0); + assertThat(value.getType()).isEqualTo("node"); + assertThat(value.getValue().isPresent()).isTrue(); + } + + @Test + @NotYetImplemented(SAFARI) + @NotYetImplemented(IE) + @NotYetImplemented(CHROME) + @NotYetImplemented(EDGE) + @NotYetImplemented(FIREFOX) + void canLocateNodesWithMaxNodeCount() { + BrowsingContext browsingContext = new BrowsingContext(driver, driver.getWindowHandle()); + assertThat(browsingContext.getId()).isNotEmpty(); + + driver.get(pages.xhtmlTestPage); + + LocateNodeParameters parameters = + new LocateNodeParameters.Builder(Locator.css("div")).setMaxNodeCount(4).build(); + + List elements = browsingContext.locateNodes(parameters); + assertThat(elements.size()).isEqualTo(4); + } + + @Test + @NotYetImplemented(SAFARI) + @NotYetImplemented(IE) + @NotYetImplemented(CHROME) + @NotYetImplemented(EDGE) + @NotYetImplemented(FIREFOX) + void canLocateNodesWithNoneOwnershipParameter() { + BrowsingContext browsingContext = new BrowsingContext(driver, driver.getWindowHandle()); + assertThat(browsingContext.getId()).isNotEmpty(); + + driver.get(pages.xhtmlTestPage); + + LocateNodeParameters parameters = + new LocateNodeParameters.Builder(Locator.css("div")) + .setOwnership(ResultOwnership.NONE) + .build(); + + List elements = browsingContext.locateNodes(parameters); + assertThat(elements.size()).isEqualTo(13); + assertThat(elements.get(0).getHandle().isPresent()).isFalse(); + } + + @Test + @NotYetImplemented(SAFARI) + @NotYetImplemented(IE) + @NotYetImplemented(CHROME) + @NotYetImplemented(EDGE) + @NotYetImplemented(FIREFOX) + void canLocateNodesWithRootOwnershipParameter() { + BrowsingContext browsingContext = new BrowsingContext(driver, driver.getWindowHandle()); + assertThat(browsingContext.getId()).isNotEmpty(); + + driver.get(pages.xhtmlTestPage); + + LocateNodeParameters parameters = + new LocateNodeParameters.Builder(Locator.css("div")) + .setOwnership(ResultOwnership.ROOT) + .build(); + + List elements = browsingContext.locateNodes(parameters); + assertThat(elements.size()).isEqualTo(13); + assertThat(elements.get(0).getHandle().isPresent()).isTrue(); + } + + @Test + @NotYetImplemented(SAFARI) + @NotYetImplemented(IE) + @NotYetImplemented(CHROME) + @NotYetImplemented(EDGE) + @NotYetImplemented(FIREFOX) + void canLocateNodesGivenStartNodes() { + String handle = driver.getWindowHandle(); + BrowsingContext browsingContext = new BrowsingContext(driver, handle); + assertThat(browsingContext.getId()).isNotEmpty(); + + driver.get(pages.formPage); + + Script script = new Script(driver); + EvaluateResult result = + script.evaluateFunctionInBrowsingContext( + handle, + "document.querySelectorAll(\"form\")", + false, + Optional.of(ResultOwnership.ROOT)); + + assertThat(result.getResultType()).isEqualTo(EvaluateResult.Type.SUCCESS); + + EvaluateResultSuccess resultSuccess = (EvaluateResultSuccess) result; + List startNodes = new ArrayList<>(); + + RemoteValue remoteValue = resultSuccess.getResult(); + List remoteValues = (List) remoteValue.getValue().get(); + + remoteValues.forEach( + value -> + startNodes.add( + new RemoteReference(RemoteReference.Type.SHARED_ID, value.getSharedId().get()))); + + LocateNodeParameters parameters = + new LocateNodeParameters.Builder(Locator.css("input")) + .setStartNodes(startNodes) + .setMaxNodeCount(50) + .build(); + + List elements = browsingContext.locateNodes(parameters); + assertThat(elements.size()).isEqualTo(35); + } + + @Test + @NotYetImplemented(SAFARI) + @NotYetImplemented(IE) + @NotYetImplemented(CHROME) + @NotYetImplemented(EDGE) + @NotYetImplemented(FIREFOX) + void canLocateNodesInAGivenSandbox() { + String sandbox = "sandbox"; + BrowsingContext browsingContext = new BrowsingContext(driver, driver.getWindowHandle()); + assertThat(browsingContext.getId()).isNotEmpty(); + + browsingContext.navigate(pages.xhtmlTestPage, ReadinessState.COMPLETE); + + LocateNodeParameters parameters = + new LocateNodeParameters.Builder(Locator.css("div")) + .setSandbox(sandbox) + .setMaxNodeCount(1) + .build(); + + List elements = browsingContext.locateNodes(parameters); + assertThat(elements.size()).isEqualTo(1); + + String nodeId = elements.get(0).getSharedId().get(); + + List arguments = new ArrayList<>(); + + LocalValue value = LocalValue.mapValue(Map.of("sharedId", LocalValue.stringValue(nodeId))); + arguments.add(value); + + Script script = new Script(driver); + + // Since the node was present in the sandbox, the script run in the same sandbox should be able + // to retrieve it + EvaluateResult result = + script.callFunctionInBrowsingContext( + driver.getWindowHandle(), + sandbox, + "function(){ return arguments[0]; }", + true, + Optional.of(arguments), + Optional.empty(), + Optional.empty()); + + assertThat(result.getResultType()).isEqualTo(EvaluateResult.Type.SUCCESS); + Map sharedIdMap = + (Map) ((EvaluateResultSuccess) result).getResult().getValue().get(); + + String sharedId = (String) ((RemoteValue) sharedIdMap.get("sharedId")).getValue().get(); + assertThat(sharedId).isEqualTo(nodeId); + } + + @AfterEach + public void quitDriver() { + if (driver != null) { + driver.quit(); + } + safelyCall(server::stop); + } +}