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);
}
}