From 22e92f80cd1c0d7b61627736ee3ccb6c847dc82c Mon Sep 17 00:00:00 2001 From: kimyenac Date: Tue, 22 Sep 2026 17:48:48 +0900 Subject: [PATCH] [ZEPPELIN-6711] Wait for the login modal to close before driving the navbar 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. authenticationUser did not wait for the modal at all. It removed the backdrop and forced modal('hide') right after the navbar dropdown appeared, then slept 500 ms. The classic UI closes the modal itself - login.controller.js calls modal('toggle') when the login request succeeds - so that forced cleanup races the application instead of waiting for it, and it left no trace when the modal was in fact still up. Wait for the modal to become invisible first, and keep the forced cleanup unchanged as a 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. The modal can also come back after it was closed, so waiting once in authenticationUser is not enough on its own: NotebookServer sends SESSION_LOGOUT when a WebSocket message carries a stale ticket, and login.controller.js answers that by re-opening the modal one second later, guarded by userName != '' - true exactly after a login. logoutUser therefore waits for the modal immediately before it opens the user menu, and retries that one click if it is still intercepted; an intercepted click never reached the menu, so the dropdown is still closed and opening it again is safe. logoutUser has around 40 callers, all but one of which log in through authenticationUserViaRest and never touch the modal. Its sleeps and its logout click are left exactly as they were so their timing does not change; this only adds the guard. The wait returns immediately when the modal is absent or hidden, which is the normal case. Also fix the error message in testSimpleAuthentication, which named testCreateNewButton. --- .../apache/zeppelin/AbstractZeppelinIT.java | 60 ++++++++++++++++--- .../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 bc433bcb6fe..6b07cc2b777 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 @@ protected void authenticationUser(String userName, String password) { } 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 @@ protected void authenticationUser(String userName, String password) { 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. + * + *

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 @@ protected String extractNoteIdFromCurrentUrl() { 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 fe77c65da7e..081a7e7cfab 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 @@ void testSimpleAuthentication() throws Exception { logoutUser("admin"); } catch (Exception e) { - handleException("Exception in AuthenticationIT while testCreateNewButton ", e); + handleException("Exception in AuthenticationIT while testSimpleAuthentication ", e); } }