This is an automated email from the ASF dual-hosted git repository.

voidmatcha pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git


The following commit(s) were added to refs/heads/master by this push:
     new e4346ceb2e [ZEPPELIN-6711] Wait for the login modal to close before 
driving the navbar
e4346ceb2e is described below

commit e4346ceb2e2af553998c59b23130451f9c05098c
Author: 김예나 <[email protected]>
AuthorDate: Sat Sep 26 00:49:41 2026 +0900

    [ZEPPELIN-6711] Wait for the login modal to close before driving the navbar
    
    ### What is this PR for?
    
    `AuthenticationIT.testSimpleAuthentication` is the only test left that 
drives the login modal, and it fails intermittently in 
`test-selenium-with-spark-module-for-spark-3-5`. The click on the navbar user 
menu is intercepted by `#loginModal`, which is still displayed:
    
    ```
    org.openqa.selenium.ElementClickInterceptedException: element click 
intercepted:
      Element <li style="margin-left: 10px">...</li> is not clickable at point 
(1858, 21).
      Other element would receive the click:
      <div id="loginModal" class="modal fade ng-scope in" role="dialog" 
style="display: block;">
            at 
org.apache.zeppelin.AbstractZeppelinIT.logoutUser(AbstractZeppelinIT.java:173)
            at 
org.apache.zeppelin.integration.AuthenticationIT.testSimpleAuthentication(AuthenticationIT.java:101)
    ```
    
    `authenticationUser` did not wait for the modal at all. Once the navbar 
dropdown appeared it removed the backdrop, forced `modal('hide')` and slept 500 
ms. The classic UI closes the modal itself — `login.controller.js` calls 
`modal('toggle')` when the login request succeeds — so that cleanup raced the 
application instead of waiting for it, and it left no trace when the modal was 
in fact still up.
    
    This PR:
    
    * Waits for `#loginModal` to become invisible in `authenticationUser`, and 
keeps the forced cleanup, unchanged, as the fallback for when that wait times 
out. The fallback, and a modal still displayed after it, are now logged, so a 
future failure says which case it hit instead of only surfacing as an 
intercepted click.
    * Waits for the modal in `logoutUser` immediately before opening the user 
menu, and retries that one click if it is still intercepted.
    * Fixes the error message in `testSimpleAuthentication`, which named 
`testCreateNewButton`.
    
    Test-only. No production code changes.
    
    ### Why `logoutUser` needs its own wait
    
    Waiting once in `authenticationUser` is not sufficient, because the modal 
can be legitimately re-opened *after* it was closed:
    
    * `NotebookServer.onMessage` answers a WebSocket message whose ticket does 
not match the one on file by sending `OP.SESSION_LOGOUT` 
(`NotebookServer.java:362`).
    * `login.controller.js:64` reacts to `session_logout` by calling 
`modal('show')` inside a `$timeout(..., 1000)`, guarded by `$rootScope.userName 
!== ''` — true exactly after a successful login.
    
    The failing run's log is consistent with this. The WebSocket opened before 
the login stays open across it and no new ticket is requested:
    
    ```
    04:49:28,276  Open connection to /0:0:0:0:0:0:0:1:43492
    04:49:29,197  Login request completed: principal=admin, success=true
    04:49:31,352  ERROR AbstractZeppelinIT - Exception in AuthenticationIT ...
    04:49:31,479  Closed connection to /0:0:0:0:0:0:0:1:43492 (1001)
    ```
    
    The next test in the same run is the contrast: after `finance1` logs in 
through `authenticationUserViaRest`, the log shows `WebSocket ticket request 
completed: principal=finance1` followed by a new connection. Only the modal 
login path keeps a connection established under the previous ticket.
    
    To be clear about the limit of that evidence: `NotebookServer` logs the 
mismatch at DEBUG and the job runs at INFO, so the log does not show whether a 
`SESSION_LOGOUT` was actually emitted. The mechanism is established from the 
code and the timing fits, but the emission itself is unverified. I have left 
the same note on the Jira issue.
    
    ### Questions a reviewer may have
    
    **Why two new timeout constants instead of `MAX_BROWSER_TIMEOUT_SEC`?**
    `MAX_BROWSER_TIMEOUT_SEC` is 30 s. `logoutUser` has around 40 callers, so a 
30 s wait on a path that is only reached when something is already wrong makes 
a bad run much slower. `MODAL_CLOSE_TIMEOUT_SEC` is 10 s, generous for a modal 
the application closes on an HTTP response, and `MODAL_CLEANUP_TIMEOUT_SEC` is 
2 s, enough for a 300 ms Bootstrap fade — there is no reason to wait 10 s more 
just to log a warning. Happy to fold them into the existing constant if you 
would rather have few [...]
    
    **Does this slow down the other tests?**
    No. `logoutUser` has around 40 callers and all but one log in through 
`authenticationUserViaRest`, which refreshes the page and never touches the 
modal. `invisibilityOfElementLocated` returns immediately when the element is 
hidden or absent, which is the normal case. The wait only costs time when the 
modal is genuinely up — where the old code failed outright.
    
    **Why is the retry only around the first click?**
    An intercepted click never reached the user menu, so the dropdown is still 
closed and opening it again is safe. Retrying the whole open-menu-then-logout 
sequence would not be: if the *second* click were intercepted, the dropdown 
would already be open, and clicking the user menu again would close it and 
leave the logout link unclickable. The sleeps and the logout click are 
therefore left exactly as they were, and only the first click is wrapped.
    
    **Why keep the forced `modal('hide')` at all?**
    It is the existing behaviour and it is the only lever left if the 
application does not close the modal. It is now a fallback rather than the 
first move, and it is byte-identical to what is on master — the only 
behavioural change in that path is that we wait first.
    
    **Should the WebSocket reconnect be fixed instead?**
    Arguably the classic UI should re-request the WebSocket ticket after a 
modal login, the way the REST path effectively does. That is a production 
change and the issue puts it out of scope, so I have not touched it; it seems 
worth a separate issue if you agree.
    
    ### What type of PR is it?
    
    Bug Fix
    
    ### Todos
    
    None.
    
    ### What is the Jira issue?
    
    * https://issues.apache.org/jira/browse/ZEPPELIN-6711
    
    ### How should this be tested?
    
    Done:
    
    * `./mvnw install -DskipTests -am -pl zeppelin-integration -Pweb-classic 
-Pintegration` — `BUILD SUCCESS`, and `testCompile` recompiled the module's 
sources.
    * Checked that the changed lines add no Checkstyle violations. The module 
is not wired into `checkstyle-fail-build` and carries pre-existing violations, 
so I compared the reported lines against the lines this PR touches rather than 
the module total: zero on changed lines.
    * Read the failing job's log and the code paths quoted above rather than 
assuming the cause.
    
    Not done:
    
    * **I have not run the Selenium suite.** This machine has no Chrome, 
Firefox or Edge — only Safari, which would need `safaridriver --enable` plus a 
manual Develop-menu step, and the suite runs headed. The flake is also specific 
to the CI browser, so a Safari run would not be evidence. CI is the real check 
here.
    * The issue's verification step is to re-run 
`test-selenium-with-spark-module-for-spark-3-5` several times and confirm 
`testSimpleAuthentication` no longer fails this way. Being a flake, one green 
run does not prove much; it is worth watching the job over the next few runs. 
For reference, between 2026-09-13 and 2026-09-17 the issue reports 13 runs 
failing this way, and I hit it again on 2026-09-22.
    
    ### Screenshots (if appropriate)
    
    N/A
    
    ### Questions:
    
    * Does the license files need to update? No. No files added or removed; 
both touched files keep their ASF headers.
    * Is there breaking changes for older versions? No. Test-only.
    * Does this needs documentation? No.
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    
    Closes #5497 from kimyenac/ZEPPELIN-6711.
    
    Signed-off-by: YONGJAE LEE <[email protected]>
---
 .../org/apache/zeppelin/AbstractZeppelinIT.java    | 60 +++++++++++++++++++---
 .../zeppelin/integration/AuthenticationIT.java     |  2 +-
 2 files changed, 54 insertions(+), 8 deletions(-)

diff --git 
a/zeppelin-integration/src/test/java/org/apache/zeppelin/AbstractZeppelinIT.java
 
b/zeppelin-integration/src/test/java/org/apache/zeppelin/AbstractZeppelinIT.java
index bc433bcb6f..6b07cc2b77 100644
--- 
a/zeppelin-integration/src/test/java/org/apache/zeppelin/AbstractZeppelinIT.java
+++ 
b/zeppelin-integration/src/test/java/org/apache/zeppelin/AbstractZeppelinIT.java
@@ -51,9 +51,12 @@ abstract public class AbstractZeppelinIT {
   protected static final long MAX_BROWSER_TIMEOUT_SEC = 30;
   protected static final long MAX_PARAGRAPH_TIMEOUT_SEC = 120;
   private static final String CLASSIC_LOGIN_PATH = "/classic/api/login";
+  private static final By LOGIN_MODAL = By.id("loginModal");
+  private static final long MODAL_CLOSE_TIMEOUT_SEC = 10;
+  private static final long MODAL_CLEANUP_TIMEOUT_SEC = 2;
 
   protected void authenticationUser(String userName, String password) {
-    WebElement loginModal = 
manager.getWebDriver().findElement(By.id("loginModal"));
+    WebElement loginModal = manager.getWebDriver().findElement(LOGIN_MODAL);
     if (!loginModal.isDisplayed()) {
       try {
         clickableWait(
@@ -62,7 +65,7 @@ abstract public class AbstractZeppelinIT {
       } catch (ElementClickInterceptedException e) {
         // Authentication-required pages can open the modal between the 
visibility check
         // and the click. Continue only when that modal is now actually 
visible.
-        if 
(!manager.getWebDriver().findElement(By.id("loginModal")).isDisplayed()) {
+        if (!manager.getWebDriver().findElement(LOGIN_MODAL).isDisplayed()) {
           throw e;
         }
       }
@@ -94,17 +97,49 @@ abstract public class AbstractZeppelinIT {
         userNameInput, passwordInput, loginButton, userName, password);
 
     // Wait for the logged-in navbar user dropdown to appear (indicates login 
completed
-    // and Angular digest cycle has updated the DOM), then dismiss any 
leftover modal overlay
+    // and Angular digest cycle has updated the DOM), then wait out the login 
modal
     visibilityWait(
         By.xpath("//div[contains(@class, 
'navbar-collapse')]//li//button[contains(@class, 'nav-btn dropdown-toggle 
ng-scope')]"),
         MAX_BROWSER_TIMEOUT_SEC);
+    dismissLoginModal();
+  }
+
+  /**
+   * Waits until the login modal no longer covers the page, and only takes it 
down by hand if
+   * that does not happen.
+   *
+   * <p>The classic UI closes the modal itself: login.controller.js calls 
modal('toggle') once
+   * the login request succeeds. Removing the backdrop and forcing 
modal('hide') while that is
+   * still running fights the application instead of waiting for it, and 
leaves no evidence when
+   * the modal is in fact still displayed. Waiting first keeps the forced 
cleanup as a fallback
+   * for the cases it was meant for.
+   */
+  private void dismissLoginModal() {
+    if (loginModalClosed(MODAL_CLOSE_TIMEOUT_SEC)) {
+      return;
+    }
+    LOGGER.warn("Login modal still displayed after {}s, taking it down from 
the page",
+        MODAL_CLOSE_TIMEOUT_SEC);
     try {
       ((JavascriptExecutor) manager.getWebDriver()).executeScript(
           "$('.modal-backdrop').remove(); $('#loginModal').modal('hide');");
     } catch (Exception e) {
       // ignore if jQuery/Bootstrap not ready
     }
-    ZeppelinITUtils.sleep(500, false);
+    if (!loginModalClosed(MODAL_CLEANUP_TIMEOUT_SEC)) {
+      LOGGER.warn("Login modal is still displayed; the next click may be 
intercepted by it");
+    }
+  }
+
+  /** Returns true once the login modal is hidden or gone, false if it is 
still displayed. */
+  private boolean loginModalClosed(final long timeWait) {
+    try {
+      new WebDriverWait(manager.getWebDriver(), Duration.ofSeconds(timeWait))
+          .until(ExpectedConditions.invisibilityOfElementLocated(LOGIN_MODAL));
+      return true;
+    } catch (TimeoutException e) {
+      return false;
+    }
   }
 
   private WebElement angularModelWait(By locator) {
@@ -168,9 +203,20 @@ abstract public class AbstractZeppelinIT {
 
   protected void logoutUser(String userName) throws URISyntaxException {
     ZeppelinITUtils.sleep(500, false);
-    clickableWait(
-        By.xpath("//div[contains(@class, 'navbar-collapse')]//li[contains(.,'" 
+ userName + "')]"),
-        MAX_BROWSER_TIMEOUT_SEC).click();
+    By userMenu =
+        By.xpath("//div[contains(@class, 'navbar-collapse')]//li[contains(.,'" 
+ userName + "')]");
+    dismissLoginModal();
+    try {
+      clickableWait(userMenu, MAX_BROWSER_TIMEOUT_SEC).click();
+    } catch (ElementClickInterceptedException e) {
+      // The login modal can come back after it was closed: on a 
SESSION_LOGOUT message
+      // login.controller.js re-opens it one second later, so it can appear 
between the
+      // wait above and this click. An intercepted click never reached the 
menu, so the
+      // dropdown is still closed and opening it again is safe.
+      LOGGER.warn("Navbar user menu click was intercepted, retrying once", e);
+      dismissLoginModal();
+      clickableWait(userMenu, MAX_BROWSER_TIMEOUT_SEC).click();
+    }
     ZeppelinITUtils.sleep(500, false);
     clickableWait(
         By.xpath("//div[contains(@class, 'navbar-collapse')]//li[contains(.,'" 
+ userName + "')]//a[@ng-click='navbar.logout()']"),
diff --git 
a/zeppelin-integration/src/test/java/org/apache/zeppelin/integration/AuthenticationIT.java
 
b/zeppelin-integration/src/test/java/org/apache/zeppelin/integration/AuthenticationIT.java
index fe77c65da7..081a7e7cfa 100644
--- 
a/zeppelin-integration/src/test/java/org/apache/zeppelin/integration/AuthenticationIT.java
+++ 
b/zeppelin-integration/src/test/java/org/apache/zeppelin/integration/AuthenticationIT.java
@@ -100,7 +100,7 @@ public class AuthenticationIT extends AbstractZeppelinIT {
 
       logoutUser("admin");
     } catch (Exception e) {
-      handleException("Exception in AuthenticationIT while testCreateNewButton 
", e);
+      handleException("Exception in AuthenticationIT while 
testSimpleAuthentication ", e);
     }
   }
 

Reply via email to