Skip to content

feat: allow @WithTimeout annotation on page object and widget classes - #2481

Merged
mykola-mokhnach merged 2 commits into
appium:masterfrom
hoanle0126:feat/class-level-with-timeout-2235
Oct 28, 2026
Merged

mykola-mokhnach merged 2 commits into
appium:masterfrom
hoanle0126:feat/class-level-with-timeout-2235

Conversation

@hoanle0126

Copy link
Copy Markdown
Contributor

Change list

Allows @WithTimeout to be placed on page object and widget classes (ElementType.TYPE) in addition to individual fields (ElementType.FIELD):

  • Updates @WithTimeout target to @Target({ElementType.FIELD, ElementType.TYPE}).
  • Resolves @WithTimeout in AppiumElementLocatorFactory from the field first, then from the field's declaring class (and superclasses), falling back to the factory's default duration when neither is present.
  • Adds unit tests in TimeoutTest covering class-level @WithTimeout, field-level overrides on an annotated class, and subclass inheritance.

Fixes #2235

Types of changes

  • No changes in production code.
  • 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)

@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.

Left a few inline comments. One more point that can't be anchored inline: docs/Page-objects.md (the @WithTimeout section) only shows field-level usage. It could mention that class-level is now supported and that precedence is field-level > class-level > decorator default.


Class<?> candidateClass = null;
if (annotatedElement instanceof Field) {
candidateClass = ((Field) annotatedElement).getDeclaringClass();

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.

The class-level timeout is resolved from the field's declaring class, not the page object's runtime class. A @WithTimeout on a subclass is therefore ignored for fields declared in a superclass: @WithTimeout(time = 600) class LoginPage extends BasePage {} with List<WebElement> items declared in BasePage falls back to the default. The inheritance test only covers the opposite direction (subclass field picks up the parent's annotation). Is that the intended semantics?

Class<?> candidateClass = null;
if (annotatedElement instanceof Field) {
candidateClass = ((Field) annotatedElement).getDeclaringClass();
} else if (annotatedElement instanceof Class<?>) {

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.

The new Javadoc says the annotation applies to a "page object/widget class", but a @WithTimeout on a Widget subclass has no effect on locating the widget field itself: createLocator(field) resolves against the page class. Only the widget's own inner fields honor it. Worth either handling the field type here or narrowing the Javadoc.

return build(annotatedElement.getAnnotation(WithTimeout.class));
}

Class<?> candidateClass = null;

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.

Class-level lookup only handles Field and Class. Other AnnotatedElements silently use the default, and enclosing classes are not consulted (e.g. a static nested page inside an annotated outer class). Probably fine, but worth documenting.

candidateClass = (Class<?>) annotatedElement;
}

while (candidateClass != null && !Object.class.equals(candidateClass)) {

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.

Behavior change: a class-level @WithTimeout now wins over the explicit Duration passed to AppiumFieldDecorator, so users can't override it for page classes they don't own. Is that the intended precedence? Also, marking WithTimeout as @Inherited would let isAnnotationPresent/getAnnotation handle the superclass walk and make this loop unnecessary.

assertWaitsFor(ofMillis(900), () -> page.fieldOverrideElements.size());
}

@Test public void withInheritedClassLevelTimeOutTest() {

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.

These tests assert on wall-clock time with a tight overrun bound (ACCEPTABLE_OVERRUN_MS), so they may be flaky on slow CI. Coverage is also missing for a field declared in a superclass of an annotated subclass and for the Widget path, which is why the gaps noted above go unnoticed.

@hoanle0126

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review @mykola-mokhnach! I have updated the PR to address all points:

  1. @Inherited on @WithTimeout & Precedence Javadoc: Marked @WithTimeout with @Inherited so Class#getAnnotation(WithTimeout.class) walks superclasses natively, and documented the precedence (field @WithTimeout > widget class @WithTimeout > page/declaring class @WithTimeout (including superclasses & enclosing classes) > AppiumFieldDecorator default Duration, since the 1-arg AppiumFieldDecorator(SearchContext) delegates to DEFAULT_WAITING_TIMEOUT).
  2. Runtime Page Class & Enclosing Classes: Tracked the active page/widget class being initialized in PageFactory.initElements so fields declared in an unannotated base page also pick up @WithTimeout placed on the runtime subclass, and checked getEnclosingClass() for nested classes.
  3. Widget Type Resolution: Added extractWidgetClass(field) in AppiumElementLocatorFactory.resolveCustomDuration so @WithTimeout on a Widget subclass applies both when locating a Widget / List<Widget> field on a page and when locating the widget's inner fields.
  4. Tests: Widened ACCEPTABLE_OVERRUN_MS, separated the test timeouts into distinct 500ms FluentWait intervals (600ms vs 1100ms), and added coverage for superclass fields on an annotated subclass as well as Widget and List<Widget> class-level timeouts.

@mykola-mokhnach
mykola-mokhnach merged commit 7847dcd into appium:master Oct 28, 2026
11 of 12 checks passed
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.

FR: Make @WithTimeout applicable to classes

2 participants