From 0fe2cda331f252128b34901661bf9fdf8eacd17e Mon Sep 17 00:00:00 2001 From: theluckystrike <51033404+theluckystrike@users.noreply.github.com> Date: Thu, 8 Oct 2026 09:41:30 +0700 Subject: [PATCH] fix: look up a missing page object element once per wait attempt Fixes #2034 Fields with mobile annotations resolve through pagefactory.bys.builder.ByChained, even with one annotation. Its findElement() wrapped the chain in a FluentWait with the default 500 ms timeout and interval, inside the wait that AppiumElementLocator already runs with the decorator timeout. A missing element got two lookups and at least 500 ms per attempt, also with Duration.ZERO. The chain now runs once and the locator owns the wait. Commands I ran, with the result. ./gradlew :test, filtered to MissingElementLookupTest master 92c2dc85 expected [accessibilityId: Not Exists] but was [accessibilityId: Not Exists, accessibilityId: Not Exists] this commit 1 test, 0 failures ./gradlew clean build (JDK 26.0.2, Selenium 4.50.0) BUILD SUCCESSFUL, core 206 tests and selenium-bridge 12 tests, 0 failures page object from the issue, Duration.ZERO and -1 ms, local endpoint 11.0.0 jar 2 POST /element per click this commit 1 POST /element per click I didn't run the Android, iOS and Flutter e2e lanes. --- .../pagefactory/bys/builder/ByChained.java | 9 +-- .../MissingElementLookupTest.java | 73 +++++++++++++++++++ 2 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 src/test/java/io/appium/java_client/pagefactory_tests/MissingElementLookupTest.java diff --git a/src/main/java/io/appium/java_client/pagefactory/bys/builder/ByChained.java b/src/main/java/io/appium/java_client/pagefactory/bys/builder/ByChained.java index 271b56d32..df391b7a1 100644 --- a/src/main/java/io/appium/java_client/pagefactory/bys/builder/ByChained.java +++ b/src/main/java/io/appium/java_client/pagefactory/bys/builder/ByChained.java @@ -16,11 +16,9 @@ package io.appium.java_client.pagefactory.bys.builder; -import io.appium.java_client.support.ui.FluentWait; import org.openqa.selenium.By; import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.SearchContext; -import org.openqa.selenium.TimeoutException; import org.openqa.selenium.WebElement; import java.util.Optional; @@ -70,10 +68,11 @@ public WebElement findElement(SearchContext context) { } requireNonNull(searchingFunction); - try { - return new FluentWait<>(context).until(searchingFunction); - } catch (TimeoutException e) { + // A single pass: the page object locator owns the wait and the timeout + WebElement result = searchingFunction.apply(context); + if (result == null) { throw new NoSuchElementException("Cannot locate an element using " + this); } + return result; } } diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/MissingElementLookupTest.java b/src/test/java/io/appium/java_client/pagefactory_tests/MissingElementLookupTest.java new file mode 100644 index 000000000..9f7c2c26f --- /dev/null +++ b/src/test/java/io/appium/java_client/pagefactory_tests/MissingElementLookupTest.java @@ -0,0 +1,73 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * 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 io.appium.java_client.pagefactory_tests; + +import io.appium.java_client.pagefactory.AndroidFindBy; +import io.appium.java_client.pagefactory.AppiumFieldDecorator; +import io.appium.java_client.pagefactory_tests.widget.tests.AbstractStubWebDriver; +import io.appium.java_client.pagefactory_tests.widget.tests.StubWebElement; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.WebElement; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +import static io.appium.java_client.support.PageFactory.initElements; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.lessThan; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * A page object field whose element is missing must be looked up once per attempt of the + * decorator's own wait, see https://github.com/appium/java-client/issues/2034. + */ +public class MissingElementLookupTest { + + private static final class CountingAndroidDriver extends AbstractStubWebDriver.StubAndroidDriver { + private final List lookups = new ArrayList<>(); + + @Override + public StubWebElement findElement(By by) { + if (!(by instanceof By.Remotable)) { + // like a remote driver, let a composite locator run its own lookups + return (StubWebElement) by.findElement(this); + } + lookups.add(by.toString()); + throw new NoSuchElementException("Cannot locate " + by); + } + } + + @AndroidFindBy(accessibility = "Not Exists") + private WebElement missingElement; + + @Test + public void zeroTimeoutLooksUpMissingElementOnce() { + CountingAndroidDriver driver = new CountingAndroidDriver(); + initElements(new AppiumFieldDecorator(driver, Duration.ZERO), this); + + long start = System.nanoTime(); + assertThrows(NoSuchElementException.class, () -> missingElement.click()); + long elapsedMs = Duration.ofNanos(System.nanoTime() - start).toMillis(); + + assertEquals(List.of("AppiumBy.accessibilityId: Not Exists"), driver.lookups); + assertThat("A zero timeout should not wait", elapsedMs, lessThan(400L)); + } +}