diff --git a/java/src/org/openqa/selenium/support/AbstractFindByBuilder.java b/java/src/org/openqa/selenium/support/AbstractFindByBuilder.java index c80dfea948081..fbb9c1fbf9141 100644 --- a/java/src/org/openqa/selenium/support/AbstractFindByBuilder.java +++ b/java/src/org/openqa/selenium/support/AbstractFindByBuilder.java @@ -17,105 +17,74 @@ package org.openqa.selenium.support; -import java.lang.reflect.Field; -import java.util.HashSet; -import java.util.Set; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; import org.openqa.selenium.By; -public abstract class AbstractFindByBuilder { - - public abstract By buildIt(T annotation, Field field); +/** + * Compatibility adapter that delegates to {@link + * org.openqa.selenium.support.pagefactory.AbstractFindByBuilder}. + * + * @deprecated Use {@link org.openqa.selenium.support.pagefactory.AbstractFindByBuilder} instead. + */ +@Deprecated(forRemoval = false) +public abstract class AbstractFindByBuilder + extends org.openqa.selenium.support.pagefactory.AbstractFindByBuilder { protected By buildByFromFindBy(FindBy findBy) { - assertValidFindBy(findBy); - - By ans = buildByFromShortFindBy(findBy); - if (ans == null) { - ans = buildByFromLongFindBy(findBy); - } - - return ans; - } - - protected By buildByFromShortFindBy(FindBy findBy) { - if (!"".equals(findBy.className())) { - return By.className(findBy.className()); - } - - if (!"".equals(findBy.css())) { - return By.cssSelector(findBy.css()); - } - - if (!"".equals(findBy.id())) { - return By.id(findBy.id()); - } - - if (!"".equals(findBy.linkText())) { - return By.linkText(findBy.linkText()); - } - - if (!"".equals(findBy.name())) { - return By.name(findBy.name()); - } - - if (!"".equals(findBy.partialLinkText())) { - return By.partialLinkText(findBy.partialLinkText()); - } - - if (!"".equals(findBy.tagName())) { - return By.tagName(findBy.tagName()); - } - - if (!"".equals(findBy.xpath())) { - return By.xpath(findBy.xpath()); - } - - // Fall through - return null; - } - - protected By buildByFromLongFindBy(FindBy findBy) { - return findBy.how().buildBy(findBy.using()); + return super.buildByFromFindBy(adapt(findBy)); } protected void assertValidFindBys(FindBys findBys) { for (FindBy findBy : findBys.value()) { - assertValidFindBy(findBy); + super.assertValidFindBy(adapt(findBy)); } } protected void assertValidFindBy(FindBy findBy) { - if (findBy.how() != null) { - if (findBy.using() == null) { - throw new IllegalArgumentException( - "If you set the 'how' property, you must also set 'using'"); - } - } - - Set finders = new HashSet<>(); - if (!"".equals(findBy.using())) finders.add("how: " + findBy.using()); - if (!"".equals(findBy.className())) finders.add("class name:" + findBy.className()); - if (!"".equals(findBy.css())) finders.add("css:" + findBy.css()); - if (!"".equals(findBy.id())) finders.add("id: " + findBy.id()); - if (!"".equals(findBy.linkText())) finders.add("link text: " + findBy.linkText()); - if (!"".equals(findBy.name())) finders.add("name: " + findBy.name()); - if (!"".equals(findBy.partialLinkText())) - finders.add("partial link text: " + findBy.partialLinkText()); - if (!"".equals(findBy.tagName())) finders.add("tag name: " + findBy.tagName()); - if (!"".equals(findBy.xpath())) finders.add("xpath: " + findBy.xpath()); - - // A zero count is okay: it means to look by name or id. - if (finders.size() > 1) { - throw new IllegalArgumentException( - String.format( - "You must specify at most one location strategy. Number found: %d (%s)", - finders.size(), finders)); - } + super.assertValidFindBy(adapt(findBy)); } protected void assertValidFindAll(FindAll findBys) { for (FindBy findBy : findBys.value()) { - assertValidFindBy(findBy); + super.assertValidFindBy(adapt(findBy)); } } + + private org.openqa.selenium.support.pagefactory.FindBy adapt(final FindBy findBy) { + return (org.openqa.selenium.support.pagefactory.FindBy) + Proxy.newProxyInstance( + org.openqa.selenium.support.pagefactory.FindBy.class.getClassLoader(), + new Class[] {org.openqa.selenium.support.pagefactory.FindBy.class}, + new InvocationHandler() { + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if ("how".equals(method.getName())) { + return org.openqa.selenium.support.pagefactory.How.valueOf(findBy.how().name()); + } + if ("annotationType".equals(method.getName())) { + return org.openqa.selenium.support.pagefactory.FindBy.class; + } + // For all other methods (id, name, etc.), they return String and match exactly. + // We delegate to the 'findBy' instance. + try { + return findBy.annotationType().getMethod(method.getName()).invoke(findBy); + } catch (NoSuchMethodException e) { + // Handle equals/hashCode/toString if necessary, though usually not called in + // builder logic + if ("equals".equals(method.getName())) { + return proxy == args[0]; + } + if ("hashCode".equals(method.getName())) { + return System.identityHashCode(proxy); + } + if ("toString".equals(method.getName())) { + return "Proxy for " + findBy.toString(); + } + throw e; + } + } + }); + } } diff --git a/java/src/org/openqa/selenium/support/BUILD.bazel b/java/src/org/openqa/selenium/support/BUILD.bazel index ddc2313b8410f..7b1e1e9199a36 100644 --- a/java/src/org/openqa/selenium/support/BUILD.bazel +++ b/java/src/org/openqa/selenium/support/BUILD.bazel @@ -51,12 +51,10 @@ java_library( "How.java", "PageFactory.java", "PageFactoryFinder.java", - ] + glob([ - "pagefactory/*.java", - "pagefactory/internal/*.java", - ]), + ], deps = [ "//java/src/org/openqa/selenium:core", + "//java/src/org/openqa/selenium/support/pagefactory", "//java/src/org/openqa/selenium/support/ui:components", artifact("org.jspecify:jspecify"), ], diff --git a/java/src/org/openqa/selenium/support/ByIdOrName.java b/java/src/org/openqa/selenium/support/ByIdOrName.java index fd4ffccdf4a9b..909cda89ea596 100644 --- a/java/src/org/openqa/selenium/support/ByIdOrName.java +++ b/java/src/org/openqa/selenium/support/ByIdOrName.java @@ -18,54 +18,19 @@ package org.openqa.selenium.support; import java.io.Serializable; -import java.util.ArrayList; -import java.util.List; import org.jspecify.annotations.NullMarked; -import org.openqa.selenium.By; -import org.openqa.selenium.NoSuchElementException; -import org.openqa.selenium.SearchContext; -import org.openqa.selenium.WebElement; +/** + * @deprecated Use {@link org.openqa.selenium.support.pagefactory.ByIdOrName} instead. + */ +@Deprecated(forRemoval = false) @NullMarked -public class ByIdOrName extends By implements Serializable { +public class ByIdOrName extends org.openqa.selenium.support.pagefactory.ByIdOrName + implements Serializable { private static final long serialVersionUID = 3986638402799576701L; - private final By idFinder; - private final By nameFinder; - private final String idOrName; - public ByIdOrName(String idOrName) { - this.idOrName = idOrName; - idFinder = By.id(idOrName); - nameFinder = By.name(idOrName); - } - - @Override - public WebElement findElement(SearchContext context) { - try { - // First, try to locate by id - return idFinder.findElement(context); - } catch (NoSuchElementException e) { - // Then by name - return nameFinder.findElement(context); - } - } - - @Override - public List findElements(SearchContext context) { - List elements = new ArrayList<>(); - - // First: Find by id ... - elements.addAll(idFinder.findElements(context)); - // Second: Find by name ... - elements.addAll(nameFinder.findElements(context)); - - return elements; - } - - @Override - public String toString() { - return "by id or name \"" + idOrName + '"'; + super(idOrName); } } diff --git a/java/src/org/openqa/selenium/support/CacheLookup.java b/java/src/org/openqa/selenium/support/CacheLookup.java index 731964b512cb4..85bdc859a8e80 100644 --- a/java/src/org/openqa/selenium/support/CacheLookup.java +++ b/java/src/org/openqa/selenium/support/CacheLookup.java @@ -25,7 +25,10 @@ /** * Marker annotation to be applied to WebElements to indicate that it never changes (that is, that * the same instance in the DOM will always be used) + * + * @deprecated Use {@link org.openqa.selenium.support.pagefactory.CacheLookup} instead. */ +@Deprecated(forRemoval = false) @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface CacheLookup {} diff --git a/java/src/org/openqa/selenium/support/FindAll.java b/java/src/org/openqa/selenium/support/FindAll.java index 64ec4a6d95164..1b60c9caaab32 100644 --- a/java/src/org/openqa/selenium/support/FindAll.java +++ b/java/src/org/openqa/selenium/support/FindAll.java @@ -38,10 +38,13 @@ * @FindAll({@FindBy(how = How.ID, using = "foo"), * @FindBy(className = "bar")}) * + * + * @deprecated Use {@link org.openqa.selenium.support.pagefactory.FindAll} instead. */ +@Deprecated(forRemoval = false) @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.TYPE}) -@PageFactoryFinder(FindAll.FindByBuilder.class) +@org.openqa.selenium.support.pagefactory.PageFactoryFinder(FindAll.FindByBuilder.class) public @interface FindAll { FindBy[] value(); diff --git a/java/src/org/openqa/selenium/support/FindBy.java b/java/src/org/openqa/selenium/support/FindBy.java index adfb07d2d137c..18f2837042e86 100644 --- a/java/src/org/openqa/selenium/support/FindBy.java +++ b/java/src/org/openqa/selenium/support/FindBy.java @@ -49,10 +49,13 @@ * @FindBy(tagName = "a") List<WebElement> links; * @FindBy(how = How.TAG_NAME, using = "a") List<WebElement> links; * + * + * @deprecated Use {@link org.openqa.selenium.support.pagefactory.FindBy} instead. */ +@Deprecated(forRemoval = false) @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.TYPE}) -@PageFactoryFinder(FindBy.FindByBuilder.class) +@org.openqa.selenium.support.pagefactory.PageFactoryFinder(FindBy.FindByBuilder.class) public @interface FindBy { How how() default How.UNSET; @@ -78,13 +81,7 @@ class FindByBuilder extends AbstractFindByBuilder { @Override public By buildIt(FindBy findBy, Field field) { assertValidFindBy(findBy); - - By ans = buildByFromShortFindBy(findBy); - if (ans == null) { - ans = buildByFromLongFindBy(findBy); - } - - return ans; + return buildByFromFindBy(findBy); } } } diff --git a/java/src/org/openqa/selenium/support/FindBys.java b/java/src/org/openqa/selenium/support/FindBys.java index 1877e85d2d9dd..7d0ca6b6ad5dc 100644 --- a/java/src/org/openqa/selenium/support/FindBys.java +++ b/java/src/org/openqa/selenium/support/FindBys.java @@ -37,10 +37,13 @@ * @FindBys({@FindBy(id = "foo"), * @FindBy(className = "bar")}) * + * + * @deprecated Use {@link org.openqa.selenium.support.pagefactory.FindBys} instead. */ +@Deprecated(forRemoval = false) @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.FIELD, ElementType.TYPE}) -@PageFactoryFinder(FindBys.FindByBuilder.class) +@org.openqa.selenium.support.pagefactory.PageFactoryFinder(FindBys.FindByBuilder.class) public @interface FindBys { FindBy[] value(); diff --git a/java/src/org/openqa/selenium/support/How.java b/java/src/org/openqa/selenium/support/How.java index 83a2d7341cde0..e3dc2ad552284 100644 --- a/java/src/org/openqa/selenium/support/How.java +++ b/java/src/org/openqa/selenium/support/How.java @@ -19,67 +19,34 @@ import org.openqa.selenium.By; +/** + * @deprecated Use {@link org.openqa.selenium.support.pagefactory.How} instead. + */ +@Deprecated(forRemoval = false) public enum How { - CLASS_NAME { - @Override - public By buildBy(String value) { - return By.className(value); - } - }, - CSS { - @Override - public By buildBy(String value) { - return By.cssSelector(value); - } - }, - ID { - @Override - public By buildBy(String value) { - return By.id(value); - } - }, - ID_OR_NAME { + CLASS_NAME(org.openqa.selenium.support.pagefactory.How.CLASS_NAME), + CSS(org.openqa.selenium.support.pagefactory.How.CSS), + ID(org.openqa.selenium.support.pagefactory.How.ID), + ID_OR_NAME(org.openqa.selenium.support.pagefactory.How.ID_OR_NAME) { @Override public By buildBy(String value) { return new ByIdOrName(value); } }, - LINK_TEXT { - @Override - public By buildBy(String value) { - return By.linkText(value); - } - }, - NAME { - @Override - public By buildBy(String value) { - return By.name(value); - } - }, - PARTIAL_LINK_TEXT { - @Override - public By buildBy(String value) { - return By.partialLinkText(value); - } - }, - TAG_NAME { - @Override - public By buildBy(String value) { - return By.tagName(value); - } - }, - XPATH { - @Override - public By buildBy(String value) { - return By.xpath(value); - } - }, - UNSET { - @Override - public By buildBy(String value) { - return ID.buildBy(value); - } - }; + LINK_TEXT(org.openqa.selenium.support.pagefactory.How.LINK_TEXT), + NAME(org.openqa.selenium.support.pagefactory.How.NAME), + PARTIAL_LINK_TEXT(org.openqa.selenium.support.pagefactory.How.PARTIAL_LINK_TEXT), + TAG_NAME(org.openqa.selenium.support.pagefactory.How.TAG_NAME), + XPATH(org.openqa.selenium.support.pagefactory.How.XPATH), + UNSET(org.openqa.selenium.support.pagefactory.How.UNSET); + + private final org.openqa.selenium.support.pagefactory.How delegate; + + How(org.openqa.selenium.support.pagefactory.How delegate) { + this.delegate = delegate; + } - public abstract By buildBy(String value); + public By buildBy(String value) { + return delegate.buildBy(value); + } } diff --git a/java/src/org/openqa/selenium/support/PageFactory.java b/java/src/org/openqa/selenium/support/PageFactory.java index 0f91d677ae823..ab4b1ecd48de0 100644 --- a/java/src/org/openqa/selenium/support/PageFactory.java +++ b/java/src/org/openqa/selenium/support/PageFactory.java @@ -17,12 +17,7 @@ package org.openqa.selenium.support; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; import org.openqa.selenium.SearchContext; -import org.openqa.selenium.WebDriver; -import org.openqa.selenium.support.pagefactory.DefaultElementLocatorFactory; -import org.openqa.selenium.support.pagefactory.DefaultFieldDecorator; import org.openqa.selenium.support.pagefactory.ElementLocatorFactory; import org.openqa.selenium.support.pagefactory.FieldDecorator; @@ -30,7 +25,9 @@ * Factory class to make using Page Objects simpler and easier. * * @see Page Objects Wiki + * @deprecated Use {@link org.openqa.selenium.support.pagefactory.PageFactory} instead. */ +@Deprecated(forRemoval = false) public class PageFactory { /** * Instantiate an instance of the given class, and set a lazy proxy for each of the WebElement and @@ -59,9 +56,8 @@ public class PageFactory { * @see CacheLookup */ public static T initElements(SearchContext searchContext, Class pageClassToProxy) { - T page = instantiatePage(searchContext, pageClassToProxy); - initElements(searchContext, page); - return page; + return org.openqa.selenium.support.pagefactory.PageFactory.initElements( + searchContext, pageClassToProxy); } /** @@ -73,7 +69,7 @@ public static T initElements(SearchContext searchContext, Class pageClass * proxied. */ public static void initElements(SearchContext searchContext, Object page) { - initElements(new DefaultElementLocatorFactory(searchContext), page); + org.openqa.selenium.support.pagefactory.PageFactory.initElements(searchContext, page); } /** @@ -85,7 +81,7 @@ public static void initElements(SearchContext searchContext, Object page) { * @param page The object to decorate the fields of */ public static void initElements(ElementLocatorFactory factory, Object page) { - initElements(new DefaultFieldDecorator(factory), page); + org.openqa.selenium.support.pagefactory.PageFactory.initElements(factory, page); } /** @@ -96,38 +92,6 @@ public static void initElements(ElementLocatorFactory factory, Object page) { * @param page The object to decorate the fields of */ public static void initElements(FieldDecorator decorator, Object page) { - Class proxyIn = page.getClass(); - while (proxyIn != Object.class) { - proxyFields(decorator, page, proxyIn); - proxyIn = proxyIn.getSuperclass(); - } - } - - private static void proxyFields(FieldDecorator decorator, Object page, Class proxyIn) { - Field[] fields = proxyIn.getDeclaredFields(); - for (Field field : fields) { - Object value = decorator.decorate(page.getClass().getClassLoader(), field); - if (value != null) { - try { - field.setAccessible(true); - field.set(page, value); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - } - } - } - - private static T instantiatePage(SearchContext searchContext, Class pageClassToProxy) { - try { - try { - Constructor constructor = pageClassToProxy.getConstructor(WebDriver.class); - return constructor.newInstance(searchContext); - } catch (NoSuchMethodException e) { - return pageClassToProxy.getDeclaredConstructor().newInstance(); - } - } catch (ReflectiveOperationException e) { - throw new RuntimeException(e); - } + org.openqa.selenium.support.pagefactory.PageFactory.initElements(decorator, page); } } diff --git a/java/src/org/openqa/selenium/support/PageFactoryFinder.java b/java/src/org/openqa/selenium/support/PageFactoryFinder.java index c4698f492fb66..d3b876d831c9b 100644 --- a/java/src/org/openqa/selenium/support/PageFactoryFinder.java +++ b/java/src/org/openqa/selenium/support/PageFactoryFinder.java @@ -20,6 +20,10 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +/** + * @deprecated Use {@link org.openqa.selenium.support.pagefactory.PageFactoryFinder} instead. + */ +@Deprecated(forRemoval = false) @Retention(RetentionPolicy.RUNTIME) public @interface PageFactoryFinder { Class value(); diff --git a/java/src/org/openqa/selenium/support/pagefactory/AbstractFindByBuilder.java b/java/src/org/openqa/selenium/support/pagefactory/AbstractFindByBuilder.java new file mode 100644 index 0000000000000..fab6000dbf10e --- /dev/null +++ b/java/src/org/openqa/selenium/support/pagefactory/AbstractFindByBuilder.java @@ -0,0 +1,121 @@ +// 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.support.pagefactory; + +import java.lang.reflect.Field; +import java.util.HashSet; +import java.util.Set; +import org.openqa.selenium.By; + +public abstract class AbstractFindByBuilder { + + public abstract By buildIt(T annotation, Field field); + + protected By buildByFromFindBy(FindBy findBy) { + assertValidFindBy(findBy); + + By ans = buildByFromShortFindBy(findBy); + if (ans == null) { + ans = buildByFromLongFindBy(findBy); + } + + return ans; + } + + protected By buildByFromShortFindBy(FindBy findBy) { + if (!"".equals(findBy.className())) { + return By.className(findBy.className()); + } + + if (!"".equals(findBy.css())) { + return By.cssSelector(findBy.css()); + } + + if (!"".equals(findBy.id())) { + return By.id(findBy.id()); + } + + if (!"".equals(findBy.linkText())) { + return By.linkText(findBy.linkText()); + } + + if (!"".equals(findBy.name())) { + return By.name(findBy.name()); + } + + if (!"".equals(findBy.partialLinkText())) { + return By.partialLinkText(findBy.partialLinkText()); + } + + if (!"".equals(findBy.tagName())) { + return By.tagName(findBy.tagName()); + } + + if (!"".equals(findBy.xpath())) { + return By.xpath(findBy.xpath()); + } + + // Fall through + return null; + } + + protected By buildByFromLongFindBy(FindBy findBy) { + return findBy.how().buildBy(findBy.using()); + } + + protected void assertValidFindBys(FindBys findBys) { + for (FindBy findBy : findBys.value()) { + assertValidFindBy(findBy); + } + } + + protected void assertValidFindBy(FindBy findBy) { + if (findBy.how() != null) { + if (findBy.using() == null) { + throw new IllegalArgumentException( + "If you set the 'how' property, you must also set 'using'"); + } + } + + Set finders = new HashSet<>(); + if (!"".equals(findBy.using())) finders.add("how: " + findBy.using()); + if (!"".equals(findBy.className())) finders.add("class name:" + findBy.className()); + if (!"".equals(findBy.css())) finders.add("css:" + findBy.css()); + if (!"".equals(findBy.id())) finders.add("id: " + findBy.id()); + if (!"".equals(findBy.linkText())) finders.add("link text: " + findBy.linkText()); + if (!"".equals(findBy.name())) finders.add("name: " + findBy.name()); + if (!"".equals(findBy.partialLinkText())) + finders.add("partial link text: " + findBy.partialLinkText()); + if (!"".equals(findBy.tagName())) finders.add("tag name: " + findBy.tagName()); + if (!"".equals(findBy.xpath())) finders.add("xpath: " + findBy.xpath()); + + // A zero count is okay: it means to look by name or id. + if (finders.size() > 1) { + throw new IllegalArgumentException( + String.format( + "You must specify at most one location strategy. Number found: %d (%s)", + finders.size(), finders)); + } + } + + protected void assertValidFindAll(FindAll findBys) { + for (FindBy findBy : findBys.value()) { + assertValidFindBy(findBy); + } + } +} diff --git a/java/src/org/openqa/selenium/support/pagefactory/AjaxElementLocator.java b/java/src/org/openqa/selenium/support/pagefactory/AjaxElementLocator.java index 74a1e62d7bf7f..d2cc7963b972d 100644 --- a/java/src/org/openqa/selenium/support/pagefactory/AjaxElementLocator.java +++ b/java/src/org/openqa/selenium/support/pagefactory/AjaxElementLocator.java @@ -19,13 +19,12 @@ import java.lang.reflect.Field; import java.time.Clock; -import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; import java.util.List; import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.SearchContext; import org.openqa.selenium.WebElement; -import org.openqa.selenium.support.ui.SlowLoadableComponent; /** * An element locator that will wait for the specified number of seconds for an element to appear, @@ -82,14 +81,35 @@ public AjaxElementLocator( */ @Override public WebElement findElement() { - SlowLoadingElement loadingElement = new SlowLoadingElement(clock, timeOutInSeconds); - try { - return loadingElement.get().getElement(); - } catch (NoSuchElementError e) { - throw new NoSuchElementException( - String.format("Timed out after %d seconds. %s", timeOutInSeconds, e.getMessage()), - e.getCause()); + Instant end = clock.instant().plusSeconds(timeOutInSeconds); + NoSuchElementException lastException = null; + + while (true) { + try { + WebElement element = super.findElement(); + if (isElementUsable(element)) { + return element; + } + } catch (NoSuchElementException e) { + lastException = e; + } + + if (clock.instant().isAfter(end)) { + break; + } + + try { + Thread.sleep(sleepFor()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new NoSuchElementException("Interrupted while waiting for element", e); + } } + + throw new NoSuchElementException( + String.format( + "Timed out after %d seconds. %s", timeOutInSeconds, "Unable to locate the element"), + lastException); } /** @@ -99,12 +119,40 @@ public WebElement findElement() { */ @Override public List findElements() { - SlowLoadingElementList list = new SlowLoadingElementList(clock, timeOutInSeconds); - try { - return list.get().getElements(); - } catch (NoSuchElementError e) { - return new ArrayList<>(); + Instant end = clock.instant().plusSeconds(timeOutInSeconds); + + while (true) { + try { + List elements = super.findElements(); + if (!elements.isEmpty()) { + boolean allUsable = true; + for (WebElement element : elements) { + if (!isElementUsable(element)) { + allUsable = false; + break; + } + } + if (allUsable) { + return elements; + } + } + } catch (NoSuchElementException e) { + // Ignored + } + + if (clock.instant().isAfter(end)) { + break; + } + + try { + Thread.sleep(sleepFor()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } } + + return new ArrayList<>(); } /** @@ -132,97 +180,4 @@ protected long sleepFor() { protected boolean isElementUsable(WebElement element) { return true; } - - private class SlowLoadingElement extends SlowLoadableComponent { - private NoSuchElementException lastException; - private WebElement element; - - public SlowLoadingElement(Clock clock, int timeOutInSeconds) { - super(clock, Duration.ofSeconds(timeOutInSeconds)); - } - - @Override - protected void load() { - // Does nothing - } - - @Override - protected long sleepFor() { - return AjaxElementLocator.this.sleepFor(); - } - - @Override - protected void isLoaded() throws Error { - try { - element = AjaxElementLocator.super.findElement(); - if (!isElementUsable(element)) { - throw new NoSuchElementException("Element is not usable"); - } - } catch (NoSuchElementException e) { - lastException = e; - // Should use JUnit's AssertionError, but it may not be present - throw new NoSuchElementError("Unable to locate the element", e); - } - } - - public NoSuchElementException getLastException() { - return lastException; - } - - public WebElement getElement() { - return element; - } - } - - private class SlowLoadingElementList extends SlowLoadableComponent { - private NoSuchElementException lastException; - private List elements; - - public SlowLoadingElementList(Clock clock, int timeOutInSeconds) { - super(clock, Duration.ofSeconds(timeOutInSeconds)); - } - - @Override - protected void load() { - // Does nothing - } - - @Override - protected long sleepFor() { - return AjaxElementLocator.this.sleepFor(); - } - - @Override - protected void isLoaded() throws Error { - try { - elements = AjaxElementLocator.super.findElements(); - if (elements.isEmpty()) { - throw new NoSuchElementException("Unable to locate the element"); - } - for (WebElement element : elements) { - if (!isElementUsable(element)) { - throw new NoSuchElementException("Element is not usable"); - } - } - } catch (NoSuchElementException e) { - lastException = e; - // Should use JUnit's AssertionError, but it may not be present - throw new NoSuchElementError("Unable to locate the element", e); - } - } - - public NoSuchElementException getLastException() { - return lastException; - } - - public List getElements() { - return elements; - } - } - - private static class NoSuchElementError extends Error { - private NoSuchElementError(String message, Throwable throwable) { - super(message, throwable); - } - } } diff --git a/java/src/org/openqa/selenium/support/pagefactory/Annotations.java b/java/src/org/openqa/selenium/support/pagefactory/Annotations.java index 836b34332d838..9bb3d7f80655e 100644 --- a/java/src/org/openqa/selenium/support/pagefactory/Annotations.java +++ b/java/src/org/openqa/selenium/support/pagefactory/Annotations.java @@ -19,14 +19,8 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Field; +import java.lang.reflect.Method; import org.openqa.selenium.By; -import org.openqa.selenium.support.AbstractFindByBuilder; -import org.openqa.selenium.support.ByIdOrName; -import org.openqa.selenium.support.CacheLookup; -import org.openqa.selenium.support.FindAll; -import org.openqa.selenium.support.FindBy; -import org.openqa.selenium.support.FindBys; -import org.openqa.selenium.support.PageFactoryFinder; public class Annotations extends AbstractAnnotations { private final Field field; @@ -45,7 +39,8 @@ public Annotations(Field field) { */ @Override public boolean isLookupCached() { - return (field.getAnnotation(CacheLookup.class) != null); + return (field.getAnnotation(CacheLookup.class) != null) + || isAnnotationPresent(field, "org.openqa.selenium.support.CacheLookup"); } /** @@ -58,6 +53,7 @@ public boolean isLookupCached() { * @throws IllegalArgumentException when more than one annotation on a field provided */ @Override + @SuppressWarnings("unchecked") public By buildBy() { assertValidAnnotations(); @@ -77,7 +73,23 @@ public By buildBy() { } catch (ReflectiveOperationException e) { // Fall through. } + } else { + // Fallback for support package PageFactoryFinder + Class annotationType = annotation.annotationType(); + for (Annotation metaAnnotation : annotationType.getDeclaredAnnotations()) { + if ("org.openqa.selenium.support.PageFactoryFinder" + .equals(metaAnnotation.annotationType().getName())) { + try { + Method valueMethod = metaAnnotation.annotationType().getMethod("value"); + Class builderClass = (Class) valueMethod.invoke(metaAnnotation); + builder = (AbstractFindByBuilder) builderClass.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + // Ignore + } + } + } } + if (builder != null) { ans = builder.buildIt(annotation, field); break; @@ -103,17 +115,34 @@ protected void assertValidAnnotations() { FindBys findBys = field.getAnnotation(FindBys.class); FindAll findAll = field.getAnnotation(FindAll.class); FindBy findBy = field.getAnnotation(FindBy.class); - if (findBys != null && findBy != null) { + + boolean findBysPresent = + findBys != null || isAnnotationPresent(field, "org.openqa.selenium.support.FindBys"); + boolean findAllPresent = + findAll != null || isAnnotationPresent(field, "org.openqa.selenium.support.FindAll"); + boolean findByPresent = + findBy != null || isAnnotationPresent(field, "org.openqa.selenium.support.FindBy"); + + if (findBysPresent && findByPresent) { throw new IllegalArgumentException( "If you use a '@FindBys' annotation, " + "you must not also use a '@FindBy' annotation"); } - if (findAll != null && findBy != null) { + if (findAllPresent && findByPresent) { throw new IllegalArgumentException( "If you use a '@FindAll' annotation, " + "you must not also use a '@FindBy' annotation"); } - if (findAll != null && findBys != null) { + if (findAllPresent && findBysPresent) { throw new IllegalArgumentException( "If you use a '@FindAll' annotation, " + "you must not also use a '@FindBys' annotation"); } } + + private boolean isAnnotationPresent(Field field, String className) { + for (Annotation a : field.getDeclaredAnnotations()) { + if (a.annotationType().getName().equals(className)) { + return true; + } + } + return false; + } } diff --git a/java/src/org/openqa/selenium/support/pagefactory/BUILD.bazel b/java/src/org/openqa/selenium/support/pagefactory/BUILD.bazel new file mode 100644 index 0000000000000..18be50bf7f2af --- /dev/null +++ b/java/src/org/openqa/selenium/support/pagefactory/BUILD.bazel @@ -0,0 +1,17 @@ +load("//java:defs.bzl", "java_library") + +java_library( + name = "pagefactory", + srcs = glob([ + "*.java", + "internal/*.java", + ]), + visibility = [ + "//java/src/org/openqa/selenium/support:__pkg__", + "//java/test/org/openqa/selenium/support:__subpackages__", + ], + deps = [ + "//java/src/org/openqa/selenium:core", + "@maven//:org_jspecify_jspecify", + ], +) diff --git a/java/src/org/openqa/selenium/support/pagefactory/ByIdOrName.java b/java/src/org/openqa/selenium/support/pagefactory/ByIdOrName.java new file mode 100644 index 0000000000000..865b3e6b6fd11 --- /dev/null +++ b/java/src/org/openqa/selenium/support/pagefactory/ByIdOrName.java @@ -0,0 +1,71 @@ +// 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.support.pagefactory; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import org.jspecify.annotations.NullMarked; +import org.openqa.selenium.By; +import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.SearchContext; +import org.openqa.selenium.WebElement; + +@NullMarked +public class ByIdOrName extends By implements Serializable { + + private static final long serialVersionUID = 3986638402799576701L; + + private final By idFinder; + private final By nameFinder; + private final String idOrName; + + public ByIdOrName(String idOrName) { + this.idOrName = idOrName; + idFinder = By.id(idOrName); + nameFinder = By.name(idOrName); + } + + @Override + public WebElement findElement(SearchContext context) { + try { + // First, try to locate by id + return idFinder.findElement(context); + } catch (NoSuchElementException e) { + // Then by name + return nameFinder.findElement(context); + } + } + + @Override + public List findElements(SearchContext context) { + List elements = new ArrayList<>(); + + // First: Find by id ... + elements.addAll(idFinder.findElements(context)); + // Second: Find by name ... + elements.addAll(nameFinder.findElements(context)); + + return elements; + } + + @Override + public String toString() { + return "by id or name \"" + idOrName + '"'; + } +} diff --git a/java/src/org/openqa/selenium/support/pagefactory/CacheLookup.java b/java/src/org/openqa/selenium/support/pagefactory/CacheLookup.java new file mode 100644 index 0000000000000..3c9cdf47e5f2c --- /dev/null +++ b/java/src/org/openqa/selenium/support/pagefactory/CacheLookup.java @@ -0,0 +1,31 @@ +// 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.support.pagefactory; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marker annotation to be applied to WebElements to indicate that it never changes (that is, that + * the same instance in the DOM will always be used) + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface CacheLookup {} diff --git a/java/src/org/openqa/selenium/support/pagefactory/DefaultFieldDecorator.java b/java/src/org/openqa/selenium/support/pagefactory/DefaultFieldDecorator.java index 2c8f36c594a90..8ad5933275581 100644 --- a/java/src/org/openqa/selenium/support/pagefactory/DefaultFieldDecorator.java +++ b/java/src/org/openqa/selenium/support/pagefactory/DefaultFieldDecorator.java @@ -17,6 +17,7 @@ package org.openqa.selenium.support.pagefactory; +import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.InvocationHandler; import java.lang.reflect.ParameterizedType; @@ -26,9 +27,6 @@ import org.openqa.selenium.WebElement; import org.openqa.selenium.WrapsElement; import org.openqa.selenium.interactions.Locatable; -import org.openqa.selenium.support.FindAll; -import org.openqa.selenium.support.FindBy; -import org.openqa.selenium.support.FindBys; import org.openqa.selenium.support.pagefactory.internal.LocatingElementHandler; import org.openqa.selenium.support.pagefactory.internal.LocatingElementListHandler; @@ -84,9 +82,22 @@ protected boolean isDecoratableList(Field field) { return false; } - return field.getAnnotation(FindBy.class) != null + if (field.getAnnotation(FindBy.class) != null || field.getAnnotation(FindBys.class) != null - || field.getAnnotation(FindAll.class) != null; + || field.getAnnotation(FindAll.class) != null) { + return true; + } + + for (Annotation a : field.getDeclaredAnnotations()) { + String name = a.annotationType().getName(); + if ("org.openqa.selenium.support.FindBy".equals(name) + || "org.openqa.selenium.support.FindBys".equals(name) + || "org.openqa.selenium.support.FindAll".equals(name)) { + return true; + } + } + + return false; } protected WebElement proxyForLocator(ClassLoader loader, ElementLocator locator) { diff --git a/java/src/org/openqa/selenium/support/pagefactory/FindAll.java b/java/src/org/openqa/selenium/support/pagefactory/FindAll.java new file mode 100644 index 0000000000000..c6101f69b76c8 --- /dev/null +++ b/java/src/org/openqa/selenium/support/pagefactory/FindAll.java @@ -0,0 +1,61 @@ +// 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.support.pagefactory; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Field; +import org.openqa.selenium.By; + +/** + * Used to mark a field on a Page Object to indicate that lookup should use a series of @FindBy tags + * It will then search for all elements that match any of the FindBy criteria. Note that elements + * are not guaranteed to be in document order. + * + *

It can be used on a types as well, but will not be processed by default. + * + *

Eg: + * + *

+ * @FindAll({@FindBy(how = How.ID, using = "foo"),
+ *           @FindBy(className = "bar")})
+ * 
+ */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.TYPE}) +@PageFactoryFinder(FindAll.FindByBuilder.class) +public @interface FindAll { + FindBy[] value(); + + class FindByBuilder extends AbstractFindByBuilder { + @Override + public By buildIt(FindAll findBys, Field field) { + assertValidFindAll(findBys); + + FindBy[] findByArray = findBys.value(); + By[] byArray = new By[findByArray.length]; + for (int i = 0; i < findByArray.length; i++) { + byArray[i] = buildByFromFindBy(findByArray[i]); + } + + return new ByAll(byArray); + } + } +} diff --git a/java/src/org/openqa/selenium/support/pagefactory/FindBy.java b/java/src/org/openqa/selenium/support/pagefactory/FindBy.java new file mode 100644 index 0000000000000..b4bcf410c091b --- /dev/null +++ b/java/src/org/openqa/selenium/support/pagefactory/FindBy.java @@ -0,0 +1,90 @@ +// 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.support.pagefactory; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Field; +import org.openqa.selenium.By; + +/** + * Used to mark a field on a Page Object to indicate an alternative mechanism for locating the + * element or a list of elements. Used in conjunction with {@link + * org.openqa.selenium.support.pagefactory.PageFactory} this allows users to quickly and easily + * create PageObjects. + * + *

It can be used on a types as well, but will not be processed by default. + * + *

You can either use this annotation by specifying both "how" and "using" or by specifying one + * of the location strategies (eg: "id") with an appropriate value to use. Both options will + * delegate down to the matching {@link org.openqa.selenium.By} methods in By class. + * + *

For example, these two annotations point to the same element: + * + *

+ * @FindBy(id = "foobar") WebElement foobar;
+ * @FindBy(how = How.ID, using = "foobar") WebElement foobar;
+ * 
+ * + *

and these two annotations point to the same list of elements: + * + *

+ * @FindBy(tagName = "a") List<WebElement> links;
+ * @FindBy(how = How.TAG_NAME, using = "a") List<WebElement> links;
+ * 
+ */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.TYPE}) +@PageFactoryFinder(FindBy.FindByBuilder.class) +public @interface FindBy { + How how() default How.UNSET; + + String using() default ""; + + String id() default ""; + + String name() default ""; + + String className() default ""; + + String css() default ""; + + String tagName() default ""; + + String linkText() default ""; + + String partialLinkText() default ""; + + String xpath() default ""; + + class FindByBuilder extends AbstractFindByBuilder { + @Override + public By buildIt(FindBy findBy, Field field) { + assertValidFindBy(findBy); + + By ans = buildByFromShortFindBy(findBy); + if (ans == null) { + ans = buildByFromLongFindBy(findBy); + } + + return ans; + } + } +} diff --git a/java/src/org/openqa/selenium/support/pagefactory/FindBys.java b/java/src/org/openqa/selenium/support/pagefactory/FindBys.java new file mode 100644 index 0000000000000..6d60643085fa1 --- /dev/null +++ b/java/src/org/openqa/selenium/support/pagefactory/FindBys.java @@ -0,0 +1,60 @@ +// 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.support.pagefactory; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Field; +import org.openqa.selenium.By; + +/** + * Used to mark a field on a Page Object to indicate that lookup should use a series of @FindBy tags + * in a chain as described in {@link org.openqa.selenium.support.pagefactory.ByChained} + * + *

It can be used on a types as well, but will not be processed by default. + * + *

Eg: + * + *

+ * @FindBys({@FindBy(id = "foo"),
+ *           @FindBy(className = "bar")})
+ * 
+ */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.TYPE}) +@PageFactoryFinder(FindBys.FindByBuilder.class) +public @interface FindBys { + FindBy[] value(); + + class FindByBuilder extends AbstractFindByBuilder { + @Override + public By buildIt(FindBys findBys, Field field) { + assertValidFindBys(findBys); + + FindBy[] findByArray = findBys.value(); + By[] byArray = new By[findByArray.length]; + for (int i = 0; i < findByArray.length; i++) { + byArray[i] = buildByFromFindBy(findByArray[i]); + } + + return new ByChained(byArray); + } + } +} diff --git a/java/src/org/openqa/selenium/support/pagefactory/How.java b/java/src/org/openqa/selenium/support/pagefactory/How.java new file mode 100644 index 0000000000000..0a6e81cfd3c2e --- /dev/null +++ b/java/src/org/openqa/selenium/support/pagefactory/How.java @@ -0,0 +1,85 @@ +// 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.support.pagefactory; + +import org.openqa.selenium.By; + +public enum How { + CLASS_NAME { + @Override + public By buildBy(String value) { + return By.className(value); + } + }, + CSS { + @Override + public By buildBy(String value) { + return By.cssSelector(value); + } + }, + ID { + @Override + public By buildBy(String value) { + return By.id(value); + } + }, + ID_OR_NAME { + @Override + public By buildBy(String value) { + return new ByIdOrName(value); + } + }, + LINK_TEXT { + @Override + public By buildBy(String value) { + return By.linkText(value); + } + }, + NAME { + @Override + public By buildBy(String value) { + return By.name(value); + } + }, + PARTIAL_LINK_TEXT { + @Override + public By buildBy(String value) { + return By.partialLinkText(value); + } + }, + TAG_NAME { + @Override + public By buildBy(String value) { + return By.tagName(value); + } + }, + XPATH { + @Override + public By buildBy(String value) { + return By.xpath(value); + } + }, + UNSET { + @Override + public By buildBy(String value) { + return ID.buildBy(value); + } + }; + + public abstract By buildBy(String value); +} diff --git a/java/src/org/openqa/selenium/support/pagefactory/PageFactory.java b/java/src/org/openqa/selenium/support/pagefactory/PageFactory.java new file mode 100644 index 0000000000000..57c4eb0635016 --- /dev/null +++ b/java/src/org/openqa/selenium/support/pagefactory/PageFactory.java @@ -0,0 +1,129 @@ +// 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.support.pagefactory; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import org.openqa.selenium.SearchContext; +import org.openqa.selenium.WebDriver; + +/** + * Factory class to make using Page Objects simpler and easier. + * + * @see Page Objects Wiki + */ +public class PageFactory { + /** + * Instantiate an instance of the given class, and set a lazy proxy for each of the WebElement and + * List<WebElement> fields that have been declared, assuming that the field name is also the + * HTML element's "id" or "name". This means that for the class: + * + *
 public class Page { private WebElement submit; } 
+ * + * there will be an element that can be located using the xpath expression "//*[@id='submit']" or + * "//*[@name='submit']" + * + *

By default, the element or the list is looked up each and every time a method is called upon + * it. To change this behaviour, simply annotate the field with the {@link CacheLookup}. To change + * how the element is located, use the {@link FindBy} annotation. + * + *

This method will attempt to instantiate the class given to it, preferably using a + * constructor which takes a WebDriver instance as its only argument or falling back on a no-arg + * constructor. An exception will be thrown if the class cannot be instantiated. + * + * @param searchContext The search context that will be used to look up the elements + * @param pageClassToProxy A class which will be initialised. + * @param Class of the PageObject + * @return An instantiated instance of the class with WebElement and List<WebElement> fields + * proxied + * @see FindBy + * @see CacheLookup + */ + public static T initElements(SearchContext searchContext, Class pageClassToProxy) { + T page = instantiatePage(searchContext, pageClassToProxy); + initElements(searchContext, page); + return page; + } + + /** + * As {@link #initElements(SearchContext, Class)} but will only replace the fields of an already + * instantiated Page Object. + * + * @param searchContext The driver that will be used to look up the elements + * @param page The object with WebElement and List<WebElement> fields that should be + * proxied. + */ + public static void initElements(SearchContext searchContext, Object page) { + initElements(new DefaultElementLocatorFactory(searchContext), page); + } + + /** + * Similar to the other "initElements" methods, but takes an {@link ElementLocatorFactory} which + * is used for providing the mechanism for finding elements. If the ElementLocatorFactory returns + * null then the field won't be decorated. + * + * @param factory The factory to use + * @param page The object to decorate the fields of + */ + public static void initElements(ElementLocatorFactory factory, Object page) { + initElements(new DefaultFieldDecorator(factory), page); + } + + /** + * Similar to the other "initElements" methods, but takes an {@link FieldDecorator} which is used + * for decorating each of the fields. + * + * @param decorator the decorator to use + * @param page The object to decorate the fields of + */ + public static void initElements(FieldDecorator decorator, Object page) { + Class proxyIn = page.getClass(); + while (proxyIn != Object.class) { + proxyFields(decorator, page, proxyIn); + proxyIn = proxyIn.getSuperclass(); + } + } + + private static void proxyFields(FieldDecorator decorator, Object page, Class proxyIn) { + Field[] fields = proxyIn.getDeclaredFields(); + for (Field field : fields) { + Object value = decorator.decorate(page.getClass().getClassLoader(), field); + if (value != null) { + try { + field.setAccessible(true); + field.set(page, value); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + } + + private static T instantiatePage(SearchContext searchContext, Class pageClassToProxy) { + try { + try { + Constructor constructor = pageClassToProxy.getConstructor(WebDriver.class); + return constructor.newInstance(searchContext); + } catch (NoSuchMethodException e) { + return pageClassToProxy.getDeclaredConstructor().newInstance(); + } + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } +} diff --git a/java/src/org/openqa/selenium/support/pagefactory/PageFactoryFinder.java b/java/src/org/openqa/selenium/support/pagefactory/PageFactoryFinder.java new file mode 100644 index 0000000000000..7d665c47d48ae --- /dev/null +++ b/java/src/org/openqa/selenium/support/pagefactory/PageFactoryFinder.java @@ -0,0 +1,26 @@ +// 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.support.pagefactory; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface PageFactoryFinder { + Class value(); +}