Path: blob/trunk/java/src/org/openqa/selenium/By.java
3990 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.Nullable;26import org.openqa.selenium.internal.Require;2728/**29* Mechanism used to locate elements within a document. In order to create your own locating30* mechanisms, it is possible to subclass this class and override the protected methods as required,31* though it is expected that all subclasses rely on the basic finding mechanisms provided through32* static methods of this class:33*34* <pre><code>35* public WebElement findElement(WebDriver driver) {36* WebElement element = driver.findElement(By.id(getSelector()));37* if (element == null)38* element = driver.findElement(By.name(getSelector());39* return element;40* }41* </code></pre>42*/43public abstract class By {44/**45* @param id The value of the "id" attribute to search for.46* @return A By which locates elements by the value of the "id" attribute.47*/48public static By id(String id) {49return new ById(id);50}5152/**53* @param linkText The exact text to match against.54* @return A By which locates A elements by the exact text it displays.55*/56public static By linkText(String linkText) {57return new ByLinkText(linkText);58}5960/**61* @param partialLinkText The partial text to match against62* @return a By which locates elements that contain the given link text.63*/64public static By partialLinkText(String partialLinkText) {65return new ByPartialLinkText(partialLinkText);66}6768/**69* @param name The value of the "name" attribute to search for.70* @return A By which locates elements by the value of the "name" attribute.71*/72public static By name(String name) {73return new ByName(name);74}7576/**77* @param tagName The element's tag name.78* @return A By which locates elements by their tag name.79*/80public static By tagName(String tagName) {81return new ByTagName(tagName);82}8384/**85* @param xpathExpression The XPath to use.86* @return A By which locates elements via XPath.87*/88public static By xpath(String xpathExpression) {89return new ByXPath(xpathExpression);90}9192/**93* Find elements based on the value of the "class" attribute. Only one class name should be used.94* If an element has multiple classes, please use {@link By#cssSelector(String)}.95*96* @param className The value of the "class" attribute to search for.97* @return A By which locates elements by the value of the "class" attribute.98*/99public static By className(String className) {100return new ByClassName(className);101}102103/**104* Find elements via the driver's underlying W3C Selector engine. If the browser does not105* implement the Selector API, the best effort is made to emulate the API. In this case, we strive106* for at least CSS2 support, but offer no guarantees.107*108* @param cssSelector CSS expression.109* @return A By which locates elements by CSS.110*/111public static By cssSelector(String cssSelector) {112return new ByCssSelector(cssSelector);113}114115/**116* Find a single element. Override this method if necessary.117*118* @param context A context to use to find the element.119* @return The WebElement that matches the selector.120*/121public WebElement findElement(SearchContext context) {122List<WebElement> allElements = findElements(context);123if (allElements == null || allElements.isEmpty()) {124throw new NoSuchElementException("Cannot locate an element using " + this);125}126return allElements.get(0);127}128129/**130* Find many elements.131*132* @param context A context to use to find the elements.133* @return A list of WebElements matching the selector.134*/135public abstract List<WebElement> findElements(SearchContext context);136137protected WebDriver getWebDriver(SearchContext context) {138if (context instanceof WebDriver) {139return (WebDriver) context;140}141142if (!(context instanceof WrapsDriver)) {143throw new IllegalArgumentException("Context does not wrap a webdriver: " + context);144}145146return ((WrapsDriver) context).getWrappedDriver();147}148149protected JavascriptExecutor getJavascriptExecutor(SearchContext context) {150WebDriver driver = getWebDriver(context);151152if (!(context instanceof JavascriptExecutor)) {153throw new IllegalArgumentException(154"Context does not provide a mechanism to execute JS: " + context);155}156157return (JavascriptExecutor) driver;158}159160@Override161public boolean equals(@Nullable Object o) {162if (!(o instanceof By)) {163return false;164}165166By that = (By) o;167168return this.toString().equals(that.toString());169}170171@Override172public int hashCode() {173return toString().hashCode();174}175176@Override177public String toString() {178// A stub to prevent endless recursion in hashCode()179return "[unknown locator]";180}181182public static class ById extends PreW3CLocator {183184private final String id;185186public ById(String id) {187super(188"id", Require.argument("Id", id).nonNull("Cannot find elements when id is null."), "#%s");189190this.id = id;191}192193@Override194public String toString() {195return "By.id: " + id;196}197}198199public static class ByLinkText extends BaseW3CLocator {200201private final String linkText;202203public ByLinkText(String linkText) {204super(205"link text",206Require.argument("Link text", linkText)207.nonNull("Cannot find elements when the link text is null."));208209this.linkText = linkText;210}211212@Override213public String toString() {214return "By.linkText: " + linkText;215}216}217218public static class ByPartialLinkText extends BaseW3CLocator {219220private final String partialLinkText;221222public ByPartialLinkText(String partialLinkText) {223super(224"partial link text",225Require.argument("Partial link text", partialLinkText)226.nonNull("Cannot find elements when the link text is null."));227228this.partialLinkText = partialLinkText;229}230231@Override232public String toString() {233return "By.partialLinkText: " + partialLinkText;234}235}236237public static class ByName extends PreW3CLocator {238private final String name;239240public ByName(String name) {241super(242"name",243Require.argument("Name", name).nonNull("Cannot find elements when name text is null."),244String.format("*[name='%s']", name.replace("'", "\\'")));245246this.name = name;247}248249@Override250public String toString() {251return "By.name: " + name;252}253}254255public static class ByTagName extends BaseW3CLocator {256257private final String tagName;258259public ByTagName(String tagName) {260super(261"tag name",262Require.argument("Tag name", tagName)263.nonNull("Cannot find elements when the tag name is null."));264265if (tagName.isEmpty()) {266throw new InvalidSelectorException("Tag name must not be blank");267}268269this.tagName = tagName;270}271272@Override273public String toString() {274return "By.tagName: " + tagName;275}276}277278public static class ByXPath extends BaseW3CLocator {279280private final String xpathExpression;281282public ByXPath(String xpathExpression) {283super(284"xpath",285Require.argument("XPath", xpathExpression)286.nonNull("Cannot find elements when the XPath is null."));287288this.xpathExpression = xpathExpression;289}290291@Override292public String toString() {293return "By.xpath: " + xpathExpression;294}295}296297public static class ByClassName extends PreW3CLocator {298299private static final Pattern AT_LEAST_ONE_WHITESPACE = Pattern.compile(".*\\s.*");300private final String className;301302public ByClassName(String className) {303super(304"class name",305Require.argument("Class name", className)306.nonNull("Cannot find elements when the class name expression is null."),307".%s");308309if (AT_LEAST_ONE_WHITESPACE.matcher(className).matches()) {310throw new InvalidSelectorException("Compound class names not permitted");311}312313this.className = className;314}315316@Override317public String toString() {318return "By.className: " + className;319}320}321322public static class ByCssSelector extends BaseW3CLocator {323private final String cssSelector;324325public ByCssSelector(String cssSelector) {326super(327"css selector",328Require.argument("CSS selector", cssSelector)329.nonNull("Cannot find elements when the selector is null"));330331this.cssSelector = cssSelector;332}333334@Override335public String toString() {336return "By.cssSelector: " + cssSelector;337}338}339340public interface Remotable {341Parameters getRemoteParameters();342343class Parameters {344private final String using;345private final @Nullable Object value;346347public Parameters(String using, @Nullable Object value) {348this.using = Require.nonNull("Search mechanism", using);349// There may be subclasses where the value is optional. Allow for this.350this.value = value;351}352353public String using() {354return using;355}356357public @Nullable Object value() {358return value;359}360361@Override362public String toString() {363return "[" + using + ": " + value + "]";364}365366@Override367public boolean equals(@Nullable Object o) {368if (!(o instanceof Parameters)) {369return false;370}371Parameters that = (Parameters) o;372return using.equals(that.using) && Objects.equals(value, that.value);373}374375@Override376public int hashCode() {377return Objects.hash(using, value);378}379380private Map<String, @Nullable Object> toJson() {381Map<String, @Nullable Object> params = new HashMap<>();382params.put("using", using);383params.put("value", value);384return Collections.unmodifiableMap(params);385}386}387}388389private abstract static class BaseW3CLocator extends By implements Remotable {390private final Parameters params;391392protected BaseW3CLocator(String using, String value) {393this.params = new Parameters(using, value);394}395396@Override397public WebElement findElement(SearchContext context) {398Require.nonNull("Search Context", context);399return context.findElement(this);400}401402@Override403public List<WebElement> findElements(SearchContext context) {404Require.nonNull("Search Context", context);405return context.findElements(this);406}407408@Override409public final Parameters getRemoteParameters() {410return params;411}412413protected final Map<String, @Nullable Object> toJson() {414return getRemoteParameters().toJson();415}416}417418private abstract static class PreW3CLocator extends By implements Remotable {419private static final Pattern CSS_ESCAPE =420Pattern.compile("([\\s'\"\\\\#.:;,!?+<>=~*^$|%&@`{}\\-\\/\\[\\]\\(\\)])");421private final Parameters remoteParams;422private final ByCssSelector fallback;423424private PreW3CLocator(String using, String value, String formatString) {425this.remoteParams = new Remotable.Parameters(using, value);426this.fallback = new ByCssSelector(String.format(formatString, cssEscape(value)));427}428429@Override430public WebElement findElement(SearchContext context) {431return context.findElement(fallback);432}433434@Override435public List<WebElement> findElements(SearchContext context) {436return context.findElements(fallback);437}438439@Override440public final Parameters getRemoteParameters() {441return remoteParams;442}443444protected final Map<String, @Nullable Object> toJson() {445return fallback.toJson();446}447448private String cssEscape(String using) {449using = CSS_ESCAPE.matcher(using).replaceAll("\\\\$1");450if (!using.isEmpty() && Character.isDigit(using.charAt(0))) {451using = "\\" + (30 + Integer.parseInt(using.substring(0, 1))) + " " + using.substring(1);452}453return using;454}455}456}457458459