Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/openrewrite/migrate-to-v11.yml
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ recipeList:
- org.openrewrite.java.ChangeType:
oldFullyQualifiedTypeName: org.openqa.selenium.support.pagefactory.AbstractAnnotations
newFullyQualifiedTypeName: io.appium.java_client.support.pagefactory.AbstractAnnotations
- org.openrewrite.java.ChangeType:
oldFullyQualifiedTypeName: org.openqa.selenium.support.pagefactory.Annotations
newFullyQualifiedTypeName: io.appium.java_client.support.pagefactory.Annotations
- org.openrewrite.java.ChangeType:
oldFullyQualifiedTypeName: org.openqa.selenium.support.pagefactory.ByAll
newFullyQualifiedTypeName: io.appium.java_client.support.pagefactory.ByAll
Expand Down Expand Up @@ -172,6 +175,12 @@ recipeList:
- org.openrewrite.java.ChangeType:
oldFullyQualifiedTypeName: org.openqa.selenium.support.How
newFullyQualifiedTypeName: io.appium.java_client.support.How
- org.openrewrite.java.ChangeType:
oldFullyQualifiedTypeName: org.openqa.selenium.support.PageFactoryFinder
newFullyQualifiedTypeName: io.appium.java_client.support.PageFactoryFinder
- org.openrewrite.java.ChangeType:
oldFullyQualifiedTypeName: org.openqa.selenium.support.AbstractFindByBuilder
newFullyQualifiedTypeName: io.appium.java_client.support.AbstractFindByBuilder
---
type: specs.openrewrite.org/v1beta/recipe
name: io.appium.java_client.FindV11Breakages
Expand Down
11 changes: 6 additions & 5 deletions docs/v10-to-v11-migration-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ rewrite {
| Recipe | What it does |
|----------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------|
| `io.appium.java_client.MigrateToV11` | Moves the types listed in the [first table](#what-moved) to their new packages. |
| `io.appium.java_client.MigrateToV11PageObjectAnnotations` | Optional. Switches `@FindBy`, `@FindBys`, `@FindAll`, `@CacheLookup` and `How` to the Appium ones. Skip it if the same code also uses the Selenium `PageFactory`. |
| `io.appium.java_client.MigrateToV11PageObjectAnnotations` | Optional. Switches `@FindBy`, `@FindBys`, `@FindAll`, `@CacheLookup`, `How`, `@PageFactoryFinder` and `AbstractFindByBuilder` to the Appium ones. Skip it if the same code also uses the Selenium `PageFactory`. |
| `io.appium.java_client.FindV11Breakages` | Marks the usages that have no automatic replacement (`RemoteWebDriver`, `HasBiDi`, `BiDi`, `DriverService`, touch actions, `AppiumFunction`, the mobile command helpers) with `/*~~>*/`. Search for `MobileCommand` yourself, as only some of its constants are removed. |

Several recipes can be activated at once, separated with commas. The recipe does not touch your build files
Expand All @@ -82,8 +82,8 @@ These types are covered by the recipe. The names stay the same, only the package
| `org.openqa.selenium.remote.HttpCommandExecutor` | `io.appium.java_client.remote.AppiumCommandExecutor` |
| `org.openqa.selenium.remote.CommandInfo` | `io.appium.java_client.AppiumCommandInfo` |
| `org.openqa.selenium.remote.http.{ClientConfig, HttpClient, HttpRequest, HttpResponse, HttpMethod, HttpHandler, HttpHeader, HttpMessage, Filter, Contents, WebSocket, Message, TextMessage, BinaryMessage, CloseMessage, ConnectionFailedException}` | `io.appium.java_client.http.*` |
| `org.openqa.selenium.support.{PageFactory}` and `org.openqa.selenium.support.pagefactory.{ElementLocator, ElementLocatorFactory, FieldDecorator, DefaultFieldDecorator, AbstractAnnotations, ByAll}` | `io.appium.java_client.support.*` and `io.appium.java_client.support.pagefactory.*` |
| `org.openqa.selenium.support.{FindBy, FindBys, FindAll, CacheLookup, How}` (optional, the Selenium ones keep working while `selenium-support` is available) | `io.appium.java_client.support.*` |
| `org.openqa.selenium.support.{PageFactory}` and `org.openqa.selenium.support.pagefactory.{ElementLocator, ElementLocatorFactory, FieldDecorator, DefaultFieldDecorator, AbstractAnnotations, Annotations, ByAll}` | `io.appium.java_client.support.*` and `io.appium.java_client.support.pagefactory.*` |
| `org.openqa.selenium.support.{FindBy, FindBys, FindAll, CacheLookup, How, PageFactoryFinder, AbstractFindByBuilder}` (optional, the Selenium ones keep working while `selenium-support` is available) | `io.appium.java_client.support.*` |
| `io.appium.java_client.functions.ExpectedCondition` | `org.openqa.selenium.support.ui.ExpectedCondition` |

## What needs a manual change
Expand Down Expand Up @@ -187,8 +187,9 @@ is not reset to the default anymore when other settings are changed.
### Page objects and waits

- The page factory and `FluentWait` engine is now `io.appium.java_client.support`. Page objects that use the
Selenium `@FindBy`, `@FindBys`, `@FindAll` and `@CacheLookup` annotations keep working if `selenium-support`
is available, but it is recommended to switch to the annotations from `io.appium.java_client.support`.
Selenium `@FindBy`, `@FindBys`, `@FindAll`, `@CacheLookup` and `@PageFactoryFinder` annotations keep working if
`selenium-support` is available, but it is recommended to switch to the annotations from
`io.appium.java_client.support`.
See [Page objects](Page-objects.md).

### Touch actions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,18 @@
import io.appium.java_client.pagefactory.bys.builder.AppiumByBuilder;
import io.appium.java_client.pagefactory.bys.builder.ByChained;
import io.appium.java_client.pagefactory.bys.builder.HowToUseSelectors;
import io.appium.java_client.support.AbstractFindByBuilder;
import io.appium.java_client.support.ByIdOrName;
import io.appium.java_client.support.CacheLookup;
import io.appium.java_client.support.FindAll;
import io.appium.java_client.support.FindBy;
import io.appium.java_client.support.FindBys;
import io.appium.java_client.support.PageFactory;
import io.appium.java_client.support.PageFactoryFinder;
import io.appium.java_client.support.pagefactory.ByAll;
import org.jspecify.annotations.Nullable;
import org.openqa.selenium.By;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
Expand Down Expand Up @@ -91,16 +91,81 @@ public DefaultElementByBuilder(
);
}

private static void checkDisallowedAnnotationPairs(Annotation a1, Annotation a2)
private static void validateFinderAnnotations(List<Annotation> finders)
throws IllegalArgumentException {
if (a1 != null && a2 != null) {
if (finders.size() > 1) {
Annotation a1 = finders.get(0);
Annotation a2 = finders.get(1);
throw new IllegalArgumentException(
"If you use a '@" + a1.getClass().getSimpleName() + "' annotation, "
+ "you must not also use a '@" + a2.getClass().getSimpleName()
"If you use a '@" + a1.annotationType().getSimpleName() + "' annotation, "
+ "you must not also use a '@" + a2.annotationType().getSimpleName()
+ "' annotation");
}
}

private static List<Annotation> resolveFinderAnnotations(AnnotatedElement annotatedElement) {
List<Annotation> declared = collectFinderAnnotations(
annotatedElement.getDeclaredAnnotations(), annotatedElement
);
if (!declared.isEmpty() || !(annotatedElement instanceof Class<?>)) {
return declared;
}
return collectFinderAnnotations(annotatedElement.getAnnotations(), annotatedElement);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] For classes, the inherited annotations are only consulted when the class declares no finder of its own. For example, a base widget has @Inherited @DataTestId("x") and a subclass declares only @FindBy(id = "y"). The inherited @DataTestId is silently ignored with no error, but the same two annotations on one field throw. Is that intended? If it is, it needs documenting. If not, resolve both declared and inherited annotations and run the same validation.

}

private static List<Annotation> collectFinderAnnotations(
Annotation[] candidates,
AnnotatedElement annotatedElement
) {
List<Annotation> finders = new ArrayList<>();
for (Annotation annotation : candidates) {
if (annotation.annotationType().isAnnotationPresent(PageFactoryFinder.class)) {
finders.add(annotation);
} else if (SeleniumAnnotationsCompat.isBuiltinSeleniumFinder(annotation)) {
Annotation adapted = SeleniumAnnotationsCompat.adaptBuiltinSeleniumFinder(
annotatedElement, annotation
);
if (adapted != null) {
finders.add(adapted);
}
} else if (SeleniumAnnotationsCompat.hasLegacyPageFactoryFinder(annotation)) {
finders.add(annotation);
}
}
return finders;
}

@Nullable
@SuppressWarnings("unchecked")
private static By buildFromFinderAnnotation(Annotation annotation, @Nullable Field field) {
PageFactoryFinder finder = annotation.annotationType().getAnnotation(PageFactoryFinder.class);
if (finder != null) {
Class<? extends AbstractFindByBuilder<?>> builderClass = finder.value();
AbstractFindByBuilder<Annotation> builder;
try {
Constructor<? extends AbstractFindByBuilder<?>> constructor =
builderClass.getDeclaredConstructor();
constructor.setAccessible(true);
builder = (AbstractFindByBuilder<Annotation>) constructor.newInstance();
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
}
if (cause instanceof Error) {
throw (Error) cause;
}
throw new IllegalStateException(cause);
} catch (ReflectiveOperationException e) {
throw new IllegalArgumentException(
"Cannot instantiate PageFactoryFinder builder " + builderClass.getName(), e
);
}
return builder.buildIt(annotation, field);
}
return SeleniumAnnotationsCompat.buildByFromLegacyFinder(annotation, field);
}

private static By buildMobileBy(LocatorGroupStrategy locatorGroupStrategy, By[] bys) {
if (bys.length == 0) {
return null;
Expand All @@ -114,38 +179,24 @@ private static By buildMobileBy(LocatorGroupStrategy locatorGroupStrategy, By[]
@Override
protected void assertValidAnnotations() {
AnnotatedElement annotatedElement = annotatedElementContainer.getAnnotated();
FindBy findBy = SeleniumAnnotationsCompat.find(annotatedElement, FindBy.class);
FindBys findBys = SeleniumAnnotationsCompat.find(annotatedElement, FindBys.class);
checkDisallowedAnnotationPairs(findBy, findBys);
FindAll findAll = SeleniumAnnotationsCompat.find(annotatedElement, FindAll.class);
checkDisallowedAnnotationPairs(findBy, findAll);
checkDisallowedAnnotationPairs(findBys, findAll);
if (annotatedElement != null) {
validateFinderAnnotations(resolveFinderAnnotations(annotatedElement));
}
}

@Override
protected By buildDefaultBy() {
AnnotatedElement annotatedElement = annotatedElementContainer.getAnnotated();
Field field = annotatedElement instanceof Field ? (Field) annotatedElement : null;
By defaultBy = null;
FindBy findBy = SeleniumAnnotationsCompat.find(annotatedElement, FindBy.class);
if (findBy != null) {
defaultBy = new FindBy.FindByBuilder().buildIt(findBy, field);
}

if (defaultBy == null) {
FindBys findBys = SeleniumAnnotationsCompat.find(annotatedElement, FindBys.class);
if (findBys != null) {
defaultBy = new FindBys.FindByBuilder().buildIt(findBys, field);
}
if (annotatedElement == null) {
return null;
}

if (defaultBy == null) {
FindAll findAll = SeleniumAnnotationsCompat.find(annotatedElement, FindAll.class);
if (findAll != null) {
defaultBy = new FindAll.FindByBuilder().buildIt(findAll, field);
}
List<Annotation> finders = resolveFinderAnnotations(annotatedElement);
validateFinderAnnotations(finders);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[major] Combinations that used to be accepted now throw. For example, @FindBy(id = "x") plus another annotation meta-annotated with @PageFactoryFinder (Appium or Selenium) used to work, because the second one was ignored. Now validateFinderAnnotations throws IllegalArgumentException for it. This is a breaking change for existing page objects. Please document and flag it, or limit the conflict check to combinations that already failed before.

if (finders.isEmpty()) {
return null;
}
return defaultBy;
Field field = annotatedElement instanceof Field ? (Field) annotatedElement : null;
return buildFromFinderAnnotation(finders.get(0), field);
}

private By[] getBys(Class<? extends Annotation> singleLocator, Class<? extends Annotation> chainedLocator,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,25 @@

package io.appium.java_client.pagefactory;

import io.appium.java_client.support.FindAll;
import io.appium.java_client.support.FindBy;
import io.appium.java_client.support.FindBys;
import org.jspecify.annotations.Nullable;
import org.openqa.selenium.By;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;

import static java.util.Objects.requireNonNull;

/**
* Lets page objects that still use Selenium's {@code @FindBy}, {@code @FindBys}, {@code @FindAll}
* and {@code @CacheLookup} keep working when selenium-support is on the classpath.
Expand All @@ -34,6 +43,10 @@
*/
final class SeleniumAnnotationsCompat {
private static final String SELENIUM_SUPPORT_PACKAGE = "org.openqa.selenium.support.";
private static final String SELENIUM_PAGE_FACTORY_FINDER = SELENIUM_SUPPORT_PACKAGE + "PageFactoryFinder";
private static final String SELENIUM_FIND_BY = SELENIUM_SUPPORT_PACKAGE + "FindBy";
private static final String SELENIUM_FIND_BYS = SELENIUM_SUPPORT_PACKAGE + "FindBys";
private static final String SELENIUM_FIND_ALL = SELENIUM_SUPPORT_PACKAGE + "FindAll";

private SeleniumAnnotationsCompat() {
}
Expand Down Expand Up @@ -63,6 +76,92 @@ static boolean isPresent(AnnotatedElement element, Class<? extends Annotation> t
return find(element, type) != null;
}

static boolean isBuiltinSeleniumFinder(Annotation annotation) {
String name = annotation.annotationType().getName();
return SELENIUM_FIND_BY.equals(name)
|| SELENIUM_FIND_BYS.equals(name)
|| SELENIUM_FIND_ALL.equals(name);
}

@Nullable
static Annotation adaptBuiltinSeleniumFinder(AnnotatedElement element, Annotation annotation) {
String name = annotation.annotationType().getName();
if (SELENIUM_FIND_BY.equals(name)) {
return element.getAnnotation(FindBy.class) != null ? null : adapt(annotation, FindBy.class);
}
if (SELENIUM_FIND_BYS.equals(name)) {
return element.getAnnotation(FindBys.class) != null ? null : adapt(annotation, FindBys.class);
}
if (SELENIUM_FIND_ALL.equals(name)) {
return element.getAnnotation(FindAll.class) != null ? null : adapt(annotation, FindAll.class);
}
return null;
}

static boolean hasLegacyPageFactoryFinder(Annotation annotation) {
return findLegacyFinderMeta(annotation.annotationType()) != null;
}

@Nullable
static By buildByFromLegacyFinder(Annotation annotation, @Nullable Field field) {
Annotation legacyFinder = requireNonNull(findLegacyFinderMeta(annotation.annotationType()));
Class<?> builderClass;
try {
builderClass = (Class<?>) legacyFinder.annotationType().getMethod("value").invoke(legacyFinder);
} catch (InvocationTargetException e) {
throw rethrowUnchecked(e.getCause());
} catch (ReflectiveOperationException e) {
throw new IllegalArgumentException(
"Cannot read builder class from " + legacyFinder.annotationType().getName(), e
);
}

Object builder;
try {
Constructor<?> constructor = builderClass.getDeclaredConstructor();
constructor.setAccessible(true);
builder = constructor.newInstance();
} catch (InvocationTargetException e) {
throw rethrowUnchecked(e.getCause());
} catch (ReflectiveOperationException e) {
throw new IllegalArgumentException(
"Cannot instantiate PageFactoryFinder builder " + builderClass.getName(), e
);
}

try {
Method buildIt = builderClass.getMethod("buildIt", Object.class, Field.class);
buildIt.setAccessible(true);
return (By) buildIt.invoke(builder, annotation, field);
} catch (InvocationTargetException e) {
throw rethrowUnchecked(e.getCause());
} catch (ReflectiveOperationException e) {
throw new IllegalArgumentException(
"Cannot invoke buildIt on PageFactoryFinder builder " + builderClass.getName(), e
);
}
}

@Nullable
private static Annotation findLegacyFinderMeta(Class<? extends Annotation> annotationType) {
for (Annotation meta : annotationType.getAnnotations()) {
if (SELENIUM_PAGE_FACTORY_FINDER.equals(meta.annotationType().getName())) {
return meta;
}
}
return null;
}

private static RuntimeException rethrowUnchecked(@Nullable Throwable cause) {
if (cause instanceof RuntimeException) {
return (RuntimeException) cause;
}
if (cause instanceof Error) {
throw (Error) cause;
}
return new IllegalStateException(cause);
}

private static boolean isSeleniumCounterpart(Annotation annotation, Class<? extends Annotation> type) {
return annotation.annotationType().getName().equals(SELENIUM_SUPPORT_PACKAGE + type.getSimpleName());
}
Expand Down
1 change: 1 addition & 0 deletions src/main/java/io/appium/java_client/support/FindAll.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
@PageFactoryFinder(FindAll.FindByBuilder.class)
public @interface FindAll {
/**
* The locators to search with.
Expand Down
1 change: 1 addition & 0 deletions src/main/java/io/appium/java_client/support/FindBy.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
@PageFactoryFinder(FindBy.FindByBuilder.class)
public @interface FindBy {
/**
* The location strategy to use together with {@link #using()}.
Expand Down
1 change: 1 addition & 0 deletions src/main/java/io/appium/java_client/support/FindBys.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
@PageFactoryFinder(FindBys.FindByBuilder.class)
public @interface FindBys {
/**
* The locators to chain.
Expand Down
Loading
Loading