Path: blob/trunk/java/src/org/openqa/selenium/By.java
1865 views
// Licensed to the Software Freedom Conservancy (SFC) under one1// or more contributor license agreements. See the NOTICE file2// distributed with this work for additional information3// regarding copyright ownership. The SFC licenses this file4// to you under the Apache License, Version 2.0 (the5// "License"); you may not use this file except in compliance6// with the License. You may obtain a copy of the License at7//8// http://www.apache.org/licenses/LICENSE-2.09//10// Unless required by applicable law or agreed to in writing,11// software distributed under the License is distributed on an12// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY13// KIND, either express or implied. See the License for the14// specific language governing permissions and limitations15// under the License.1617package org.openqa.selenium;1819import java.util.Collections;20import java.util.HashMap;21import java.util.List;22import java.util.Map;23import java.util.Objects;24import java.util.regex.Pattern;25import org.jspecify.annotations.NullMarked;26import org.jspecify.annotations.Nullable;27import org.openqa.selenium.internal.Require;2829/**30* Mechanism used to locate elements within a document. In order to create your own locating31* mechanisms, it is possible to subclass this class and override the protected methods as required,32* though it is expected that all subclasses rely on the basic finding mechanisms provided through33* static methods of this class:34*35* <pre><code>36* public WebElement findElement(WebDriver driver) {37* WebElement element = driver.findElement(By.id(getSelector()));38* if (element == null)39* element = driver.findElement(By.name(getSelector());40* return element;41* }42* </code></pre>43*/44@NullMarked45public abstract class By {46/**47* @param id The value of the "id" attribute to search for.48* @return A By which locates elements by the value of the "id" attribute.49*/50public static By id(String id) {51return new ById(id);52}5354/**55* @param linkText The exact text to match against.56* @return A By which locates A elements by the exact text it displays.57*/58public static By linkText(String linkText) {59return new ByLinkText(linkText);60}6162/**63* @param partialLinkText The partial text to match against64* @return a By which locates elements that contain the given link text.65*/66public static By partialLinkText(String partialLinkText) {67return new ByPartialLinkText(partialLinkText);68}6970/**71* @param name The value of the "name" attribute to search for.72* @return A By which locates elements by the value of the "name" attribute.73*/74public static By name(String name) {75return new ByName(name);76}7778/**79* @param tagName The element's tag name.80* @return A By which locates elements by their tag name.81*/82public static By tagName(String tagName) {83return new ByTagName(tagName);84}8586/**87* @param xpathExpression The XPath to use.88* @return A By which locates elements via XPath.89*/90public static By xpath(String xpathExpression) {91return new ByXPath(xpathExpression);92}9394/**95* Find elements based on the value of the "class" attribute. Only one class name should be used.96* If an element has multiple classes, please use {@link By#cssSelector(String)}.97*98* @param className The value of the "class" attribute to search for.99* @return A By which locates elements by the value of the "class" attribute.100*/101public static By className(String className) {102return new ByClassName(className);103}104105/**106* Find elements via the driver's underlying W3C Selector engine. If the browser does not107* implement the Selector API, the best effort is made to emulate the API. In this case, we strive108* for at least CSS2 support, but offer no guarantees.109*110* @param cssSelector CSS expression.111* @return A By which locates elements by CSS.112*/113public static By cssSelector(String cssSelector) {114return new ByCssSelector(cssSelector);115}116117/**118* Find a single element. Override this method if necessary.119*120* @param context A context to use to find the element.121* @return The WebElement that matches the selector.122*/123public WebElement findElement(SearchContext context) {124List<WebElement> allElements = findElements(context);125if (allElements == null || allElements.isEmpty()) {126throw new NoSuchElementException("Cannot locate an element using " + this);127}128return allElements.get(0);129}130131/**132* Find many elements.133*134* @param context A context to use to find the elements.135* @return A list of WebElements matching the selector.136*/137public abstract List<WebElement> findElements(SearchContext context);138139protected WebDriver getWebDriver(SearchContext context) {140if (context instanceof WebDriver) {141return (WebDriver) context;142}143144if (!(context instanceof WrapsDriver)) {145throw new IllegalArgumentException("Context does not wrap a webdriver: " + context);146}147148return ((WrapsDriver) context).getWrappedDriver();149}150151protected JavascriptExecutor getJavascriptExecutor(SearchContext context) {152WebDriver driver = getWebDriver(context);153154if (!(context instanceof JavascriptExecutor)) {155throw new IllegalArgumentException(156"Context does not provide a mechanism to execute JS: " + context);157}158159return (JavascriptExecutor) driver;160}161162@Override163public boolean equals(@Nullable Object o) {164if (!(o instanceof By)) {165return false;166}167168By that = (By) o;169170return this.toString().equals(that.toString());171}172173@Override174public int hashCode() {175return toString().hashCode();176}177178@Override179public String toString() {180// A stub to prevent endless recursion in hashCode()181return "[unknown locator]";182}183184public static class ById extends PreW3CLocator {185186private final String id;187188public ById(String id) {189super(190"id", Require.argument("Id", id).nonNull("Cannot find elements when id is null."), "#%s");191192this.id = id;193}194195@Override196public String toString() {197return "By.id: " + id;198}199}200201public static class ByLinkText extends BaseW3CLocator {202203private final String linkText;204205public ByLinkText(String linkText) {206super(207"link text",208Require.argument("Link text", linkText)209.nonNull("Cannot find elements when the link text is null."));210211this.linkText = linkText;212}213214@Override215public String toString() {216return "By.linkText: " + linkText;217}218}219220public static class ByPartialLinkText extends BaseW3CLocator {221222private final String partialLinkText;223224public ByPartialLinkText(String partialLinkText) {225super(226"partial link text",227Require.argument("Partial link text", partialLinkText)228.nonNull("Cannot find elements when the link text is null."));229230this.partialLinkText = partialLinkText;231}232233@Override234public String toString() {235return "By.partialLinkText: " + partialLinkText;236}237}238239public static class ByName extends PreW3CLocator {240private final String name;241242public ByName(String name) {243super(244"name",245Require.argument("Name", name).nonNull("Cannot find elements when name text is null."),246String.format("*[name='%s']", name.replace("'", "\\'")));247248this.name = name;249}250251@Override252public String toString() {253return "By.name: " + name;254}255}256257public static class ByTagName extends BaseW3CLocator {258259private final String tagName;260261public ByTagName(String tagName) {262super(263"tag name",264Require.argument("Tag name", tagName)265.nonNull("Cannot find elements when the tag name is null."));266267if (tagName.isEmpty()) {268throw new InvalidSelectorException("Tag name must not be blank");269}270271this.tagName = tagName;272}273274@Override275public String toString() {276return "By.tagName: " + tagName;277}278}279280public static class ByXPath extends BaseW3CLocator {281282private final String xpathExpression;283284public ByXPath(String xpathExpression) {285super(286"xpath",287Require.argument("XPath", xpathExpression)288.nonNull("Cannot find elements when the XPath is null."));289290this.xpathExpression = xpathExpression;291}292293@Override294public String toString() {295return "By.xpath: " + xpathExpression;296}297}298299public static class ByClassName extends PreW3CLocator {300301private static final Pattern AT_LEAST_ONE_WHITESPACE = Pattern.compile(".*\\s.*");302private final String className;303304public ByClassName(String className) {305super(306"class name",307Require.argument("Class name", className)308.nonNull("Cannot find elements when the class name expression is null."),309".%s");310311if (AT_LEAST_ONE_WHITESPACE.matcher(className).matches()) {312throw new InvalidSelectorException("Compound class names not permitted");313}314315this.className = className;316}317318@Override319public String toString() {320return "By.className: " + className;321}322}323324public static class ByCssSelector extends BaseW3CLocator {325private final String cssSelector;326327public ByCssSelector(String cssSelector) {328super(329"css selector",330Require.argument("CSS selector", cssSelector)331.nonNull("Cannot find elements when the selector is null"));332333this.cssSelector = cssSelector;334}335336@Override337public String toString() {338return "By.cssSelector: " + cssSelector;339}340}341342public interface Remotable {343Parameters getRemoteParameters();344345class Parameters {346private final String using;347private final @Nullable Object value;348349public Parameters(String using, @Nullable Object value) {350this.using = Require.nonNull("Search mechanism", using);351// There may be subclasses where the value is optional. Allow for this.352this.value = value;353}354355public String using() {356return using;357}358359public @Nullable Object value() {360return value;361}362363@Override364public String toString() {365return "[" + using + ": " + value + "]";366}367368@Override369public boolean equals(@Nullable Object o) {370if (!(o instanceof Parameters)) {371return false;372}373Parameters that = (Parameters) o;374return using.equals(that.using) && Objects.equals(value, that.value);375}376377@Override378public int hashCode() {379return Objects.hash(using, value);380}381382private Map<String, @Nullable Object> toJson() {383Map<String, @Nullable Object> params = new HashMap<>();384params.put("using", using);385params.put("value", value);386return Collections.unmodifiableMap(params);387}388}389}390391private abstract static class BaseW3CLocator extends By implements Remotable {392private final Parameters params;393394protected BaseW3CLocator(String using, String value) {395this.params = new Parameters(using, value);396}397398@Override399public WebElement findElement(SearchContext context) {400Require.nonNull("Search Context", context);401return context.findElement(this);402}403404@Override405public List<WebElement> findElements(SearchContext context) {406Require.nonNull("Search Context", context);407return context.findElements(this);408}409410@Override411public final Parameters getRemoteParameters() {412return params;413}414415protected final Map<String, @Nullable Object> toJson() {416return getRemoteParameters().toJson();417}418}419420private abstract static class PreW3CLocator extends By implements Remotable {421private static final Pattern CSS_ESCAPE =422Pattern.compile("([\\s'\"\\\\#.:;,!?+<>=~*^$|%&@`{}\\-\\/\\[\\]\\(\\)])");423private final Parameters remoteParams;424private final ByCssSelector fallback;425426private PreW3CLocator(String using, String value, String formatString) {427this.remoteParams = new Remotable.Parameters(using, value);428this.fallback = new ByCssSelector(String.format(formatString, cssEscape(value)));429}430431@Override432public WebElement findElement(SearchContext context) {433return context.findElement(fallback);434}435436@Override437public List<WebElement> findElements(SearchContext context) {438return context.findElements(fallback);439}440441@Override442public final Parameters getRemoteParameters() {443return remoteParams;444}445446protected final Map<String, @Nullable Object> toJson() {447return fallback.toJson();448}449450private String cssEscape(String using) {451using = CSS_ESCAPE.matcher(using).replaceAll("\\\\$1");452if (!using.isEmpty() && Character.isDigit(using.charAt(0))) {453using = "\\" + (30 + Integer.parseInt(using.substring(0, 1))) + " " + using.substring(1);454}455return using;456}457}458}459460461