Skip to content

feat(pagefactory): support custom @PageFactoryFinder locator annotations - #2499

Open
hoanle0126 wants to merge 1 commit into
appium:masterfrom
hoanle0126:feat/custom-findby-annotations
Open

hoanle0126 wants to merge 1 commit into
appium:masterfrom
hoanle0126:feat/custom-findby-annotations

Conversation

@hoanle0126

Copy link
Copy Markdown
Contributor

Proposed changes

Fixes #1043

  • Add @PageFactoryFinder and Annotations to io.appium.java_client.support, and attach finder metadata to the built-in FindBy, FindBys, and FindAll annotations.
  • Update DefaultElementByBuilder and DefaultFieldDecorator to resolve custom locator annotations extending AbstractFindByBuilder across single elements, element lists, widgets (both field-level and class-level), and By / AppiumBy fields.
  • Support legacy org.openqa.selenium.support.PageFactoryFinder custom annotations reflectively in SeleniumAnnotationsCompat when selenium-support is on the classpath, and include the new types in the v11 OpenRewrite migration recipe and guide.
  • Reject mutually exclusive finder annotation combinations on fields and widget classes with IllegalArgumentException using the declared annotation names.

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation Update (if none of the other choices apply)

@hoanle0126
hoanle0126 force-pushed the feat/custom-findby-annotations branch from 5810b53 to a0789c5 Compare November 11, 2026 05:36

@mykola-mokhnach mykola-mokhnach left a comment

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] The PR ticks "New feature (non-breaking change)", but it changes how existing page objects behave, so it looks like a breaking change:

    • A field or widget class that combines @FindBy, @FindBys or @FindAll (Appium or Selenium) with another annotation carrying @PageFactoryFinder used to work, because only FindBy, FindBys and FindAll were inspected. It now throws IllegalArgumentException from initElements (DefaultElementByBuilder.java:194).
    • A field annotated only with an existing Selenium @PageFactoryFinder annotation used to fall back to the default locator (the field name). It is now resolved through the custom builder, so it returns different elements.
    • DefaultFieldDecorator now decorates List<WebElement> fields it used to skip (DefaultFieldDecorator.java:110).

    Either mark the PR as a breaking change (feat!:, tick the box, describe the impact), or make the new resolution not affect page objects that work today.

  • [major] Revert the changes to docs/openrewrite/migrate-to-v11.yml and docs/v10-to-v11-migration-guide.md. A non-breaking feature must not change the v10→v11 migration guides, and these edits contradict the "non-breaking" tick. If the change really needs migration notes, that confirms it is breaking.

  • [minor] docs/Page-objects.md doesn't describe custom @PageFactoryFinder annotations. It should cover the builder contract, that the builder needs a no-arg constructor, that buildIt gets a null field for class-level annotations, and what happens when finders are combined.

return field.getAnnotation(FindBy.class) != null
|| field.getAnnotation(FindBys.class) != null
|| field.getAnnotation(FindAll.class) != null;
return Annotations.hasLocatorAnnotation(field);

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] hasLocatorAnnotation is now true for Selenium annotations and for any annotation carrying Selenium's @PageFactoryFinder. Before, only Appium @FindBy, @FindBys and @FindAll made a List<WebElement> field decoratable. A subclass of DefaultFieldDecorator with its own ElementLocatorFactory that doesn't understand those annotations now gets createLocator(field) for list fields it skipped before. This is a behavior change, so it's part of the breaking-change point in the review summary.

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 (!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.


@Nullable
@SuppressWarnings("unchecked")
private static By buildFromFinder(Annotation annotation, Field field) {

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] The finder-resolution logic exists three times: here, in DefaultElementByBuilder and in SeleniumAnnotationsCompat. Each copy has its own builder instantiation, InvocationTargetException unwrapping, rethrowUnchecked, findLegacyFinderMeta and SELENIUM_* constants. DefaultElementByBuilder.buildFromFinderAnnotation doesn't even use rethrowUnchecked. A fix in one copy won't reach the others. Annotations is not used anywhere in main, apart from hasLocatorAnnotation, so the support layer duplicates the pagefactory layer. Keep one implementation in support.pagefactory (collect finders / validate / build By) and have DefaultElementByBuilder and SeleniumAnnotationsCompat delegate to it, since pagefactory already depends on support.

}

@Override
public By buildBy() {

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] buildBy() passes a Selenium @FindAll or @FindBys straight to Selenium's builder, which returns Selenium's ByAll or ByChained. DefaultElementByBuilder adapts the same annotation to the Appium FindAll/FindBys and returns io.appium.java_client.support.pagefactory.ByAll/ByChained. Identical source gives different By classes depending on the entry point, and this path needs selenium-support at runtime. Adapt the annotation the same way in both places, which is another reason to merge the two copies.

@@ -0,0 +1,15 @@
package io.appium.java_client.support;

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] The new main files PageFactoryFinder.java and Annotations.java have no Apache license header, unlike the other main sources (for example AbstractFindByBuilder.java, FindBy.java).


/** Marks a custom locator annotation with its builder class. */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})

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.

[nit] ElementType.TYPE already covers annotation types, so ANNOTATION_TYPE is redundant.

@mykola-mokhnach mykola-mokhnach left a comment

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.

in the current state this PR could only be merged as part of the next major release of java client

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Custom FindBy annotations

2 participants