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

tbonelee 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 a61908949d [ZEPPELIN-1836] Make testAngularRunParagraph wait for 
rendered Angular output
a61908949d is described below

commit a61908949d3e38f2a92b82c1aa2c54c72362d6fd
Author: Jongyoul Lee <[email protected]>
AuthorDate: Thu Jul 23 10:52:53 2026 +0900

    [ZEPPELIN-1836] Make testAngularRunParagraph wait for rendered Angular 
output
    
    ### What is this PR for?
    
    `testAngularRunParagraph` has been failing on every `frontend.yml` 
`test-selenium-with-spark-module-for-spark-3-5` run on master since the last 
green build at 6353224c on 2026-04-23. The failure is always at 
`ZeppelinIT.java:348`:
    
    ```
    org.openqa.selenium.TimeoutException: Expected condition failed: waiting 
for visibility of element located by By.xpath: 
(//div[<at>ng-controller="ParagraphCtrl"])[1]//div[<at>id="angularRunParagraph"]
 (tried for 30 second(s) with 500 milliseconds interval)
    ```
    
    Tracing the server log confirms the angular paragraph re-run completes 
successfully on the server side (paragraph FINISHED, result broadcast) and the 
`stalenessOf(oldAngularDiv)` check before `visibilityWait` does succeed — so 
the old `angularRunParagraph` div is detached as expected. The new div, 
however, is not detected as visible within 30s.
    
    The race lives on the frontend side of the re-render: AngularJS' 
`renderAngular()` does `elem.html(generated)` and then 
`$compile(elem.contents())(paragraphScope)`. Between those two calls the result 
div can be momentarily detached or empty, which is exactly the window 
`visibilityWait` polls into. Once it gets a stale reference, the wait does not 
refetch.
    
    This builds on the `stalenessOf` + JavaScript-click fix from 
[ZEPPELIN-6409](https://issues.apache.org/jira/browse/ZEPPELIN-6409) (#5209). 
That fix proved the *old* output was being torn down; this PR makes the wait 
for the *new* output equally tolerant of mid-`$compile` DOM churn.
    
    ### What type of PR is it?
    Bug Fix
    
    ### Todos
    * [x] Replace single `visibilityWait` with a content-aware polling wait 
that re-finds the element and ignores `StaleElementReferenceException`
    * [x] Mirror the content-based pattern already used after the first run 
(`waitForText(\"Run second paragraph\", ...)`)
    
    ### What is the Jira issue?
    * https://issues.apache.org/jira/browse/ZEPPELIN-1836 (long-standing flaky 
test ticket, open since 2016)
    
    ### How should this be tested?
    * CI `frontend.yml` → `test-selenium-with-spark-module-for-spark-3-5` 
should run `ZeppelinIT.testAngularRunParagraph` to completion without timing 
out at line 348.
    * Local repro (slow, requires `-Pweb-classic` and Spark 3.5):
      ```
      ./mvnw verify -DfailIfNoTests=false -pl zeppelin-integration \
        -Pweb-classic -Pintegration -Pspark-scala-2.12 -Pspark-3.5 -Pweb-dist 
-Pusing-source-tree \
        -Dit.test=ZeppelinIT#testAngularRunParagraph
      ```
    
    ### Screenshots (if appropriate)
    N/A
    
    ### Questions:
    * Does the license files need to update? No
    * Is there breaking changes for older versions? No
    * Does this needs documentation? No
    
    Closes #5250 from jongyoul/ZEPPELIN-selenium-angular-paragraph.
    
    Signed-off-by: ChanHo Lee <[email protected]>
---
 .../org/apache/zeppelin/AbstractZeppelinIT.java    | 60 +++++++++++++++++++---
 .../apache/zeppelin/integration/ZeppelinIT.java    | 45 ++++++++++++----
 .../projects/zeppelin-react/package-lock.json      |  6 +--
 3 files changed, 91 insertions(+), 20 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 1c5e8be00c..58cf336a35 100644
--- 
a/zeppelin-integration/src/test/java/org/apache/zeppelin/AbstractZeppelinIT.java
+++ 
b/zeppelin-integration/src/test/java/org/apache/zeppelin/AbstractZeppelinIT.java
@@ -30,6 +30,7 @@ import org.openqa.selenium.JavascriptExecutor;
 import org.openqa.selenium.Keys;
 import org.openqa.selenium.NoSuchElementException;
 import org.openqa.selenium.OutputType;
+import org.openqa.selenium.StaleElementReferenceException;
 import org.openqa.selenium.TakesScreenshot;
 import org.openqa.selenium.TimeoutException;
 import org.openqa.selenium.WebDriver;
@@ -51,15 +52,45 @@ abstract public class AbstractZeppelinIT {
   protected static final long MAX_PARAGRAPH_TIMEOUT_SEC = 120;
 
   protected void authenticationUser(String userName, String password) {
-    clickableWait(
-        By.xpath("//div[contains(@class, 
'navbar-collapse')]//li//button[contains(.,'Login')]"),
-        MAX_BROWSER_TIMEOUT_SEC).click();
+    WebElement loginModal = 
manager.getWebDriver().findElement(By.id("loginModal"));
+    if (!loginModal.isDisplayed()) {
+      try {
+        clickableWait(
+            By.xpath("//div[contains(@class, 
'navbar-collapse')]//li//button[contains(.,'Login')]"),
+            MAX_BROWSER_TIMEOUT_SEC).click();
+      } 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()) {
+          throw e;
+        }
+      }
+    }
 
-    visibilityWait(By.xpath("//*[@id='userName']"), 
MAX_BROWSER_TIMEOUT_SEC).sendKeys(userName);
-    visibilityWait(By.xpath("//*[@id='password']"), 
MAX_BROWSER_TIMEOUT_SEC).sendKeys(password);
-    clickableWait(
-        By.xpath("//*[@id='loginModalContent']//button[contains(.,'Login')]"),
-        MAX_BROWSER_TIMEOUT_SEC).click();
+    By userNameLocator = By.id("userName");
+    By passwordLocator = By.id("password");
+    WebElement userNameInput = angularModelWait(userNameLocator);
+    WebElement passwordInput = angularModelWait(passwordLocator);
+    WebElement loginButton = manager.getWebDriver().findElement(
+        By.xpath("//*[@id='loginModalContent']//button[contains(.,'Login')]"));
+
+    // Send both input events and click in one browser task. Bootstrap can 
finish a stale
+    // modal transition between separate WebDriver commands and reset 
loginParams.
+    ((JavascriptExecutor) manager.getWebDriver()).executeScript(
+        "function update(element, value) {"
+            + "element.value = value;"
+            + "element.dispatchEvent(new Event('input', {bubbles: true}));"
+            + "}"
+            + "update(arguments[0], arguments[3]);"
+            + "update(arguments[1], arguments[4]);"
+            + "if 
(angular.element(arguments[0]).controller('ngModel').$viewValue"
+            + " !== arguments[3] ||"
+            + " angular.element(arguments[1]).controller('ngModel').$viewValue"
+            + " !== arguments[4]) {"
+            + "throw new Error('Login form model did not receive 
credentials');"
+            + "}"
+            + "arguments[2].click();",
+        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
@@ -75,6 +106,19 @@ abstract public class AbstractZeppelinIT {
     ZeppelinITUtils.sleep(500, false);
   }
 
+  private WebElement angularModelWait(By locator) {
+    WebDriverWait wait = new WebDriverWait(manager.getWebDriver(),
+        Duration.ofSeconds(MAX_BROWSER_TIMEOUT_SEC));
+    wait.ignoring(StaleElementReferenceException.class);
+    return wait.until(driver -> {
+      WebElement element = driver.findElement(locator);
+      Boolean modelReady = (Boolean) ((JavascriptExecutor) 
driver).executeScript(
+          "return !!(window.angular && 
angular.element(arguments[0]).controller('ngModel'));",
+          element);
+      return element.isDisplayed() && Boolean.TRUE.equals(modelReady) ? 
element : null;
+    });
+  }
+
   protected void logoutUser(String userName) throws URISyntaxException {
     ZeppelinITUtils.sleep(500, false);
     clickableWait(
diff --git 
a/zeppelin-integration/src/test/java/org/apache/zeppelin/integration/ZeppelinIT.java
 
b/zeppelin-integration/src/test/java/org/apache/zeppelin/integration/ZeppelinIT.java
index 0d21e20d68..ff45f12354 100644
--- 
a/zeppelin-integration/src/test/java/org/apache/zeppelin/integration/ZeppelinIT.java
+++ 
b/zeppelin-integration/src/test/java/org/apache/zeppelin/integration/ZeppelinIT.java
@@ -297,7 +297,6 @@ class ZeppelinIT extends AbstractZeppelinIT {
   }
 
   @Test
-  @Disabled("ZEPPELIN-6410: testAngularRunParagraph consistently fails due to 
element clickability timeout")
   void testAngularRunParagraph() throws Exception {
     try {
       createNewNote();
@@ -326,17 +325,31 @@ class ZeppelinIT extends AbstractZeppelinIT {
 
       assertTrue(isNotBlank(secondParagraphId), "Cannot find paragraph id for 
the 2nd paragraph");
 
-      // Update first paragraph to call z.runParagraph() with 2nd paragraph id
-      setTextOfParagraph(1,
-              "%angular <div id=\\'angularRunParagraph\\' 
ng-click=\\'z.runParagraph(\""
+      // Update first paragraph to call z.runParagraph() with 2nd paragraph id.
+      // Bypass ACE + the play button: setTextOfParagraph toggles editor
+      // visibility and then calls ace.edit().setValue(), which races with the
+      // editor re-init that follows the toggle. ACE setValue also does not
+      // fire the 'input' event paragraph.controller.js binds to, so
+      // paragraph.text never commits — and when $scope.editor is falsy or
+      // freshly rebound, getEditorValue() falls back to the previous text or
+      // the empty buffer, causing the rerun to echo stale/empty ANGULAR data.
+      // Drive the paragraph straight through its controller scope instead.
+      final String newAngularText =
+              "%angular <div id='angularRunParagraph' 
ng-click='z.runParagraph(\""
                       + secondParagraphId.trim()
-                      + "\")\\'>Run second paragraph</div>");
+                      + "\")'>Run second paragraph</div>";
 
       // Capture old output element before re-run to detect when it gets 
replaced
       WebElement oldAngularDiv = manager.getWebDriver().findElement(By.xpath(
               getParagraphXPath(1) + "//div[@id=\"angularRunParagraph\"]"));
 
-      runParagraph(1);
+      ((JavascriptExecutor) manager.getWebDriver()).executeScript(
+              "var els = 
document.querySelectorAll('div[ng-controller=\"ParagraphCtrl\"]');"
+                      + "var s = angular.element(els[0]).scope();"
+                      + "s.paragraph.text = arguments[0];"
+                      + "if (s.editor) { s.editor.setValue(arguments[0], 1); 
s.editor.clearSelection(); }"
+                      + "s.runParagraph(arguments[0], true, false);",
+              newAngularText);
 
       // Wait for the old output element to become stale (proves the paragraph 
output
       // was actually refreshed, avoiding race where waitForParagraph sees the 
old FINISHED state)
@@ -345,9 +358,23 @@ class ZeppelinIT extends AbstractZeppelinIT {
 
       waitForParagraph(1, "FINISHED");
 
-      // Wait for new Angular output to render
-      WebElement newAngularDiv = visibilityWait(By.xpath(
-              getParagraphXPath(1) + "//div[@id=\"angularRunParagraph\"]"), 
MAX_BROWSER_TIMEOUT_SEC);
+      // Poll for the new render: visible, expected text, and the ng-click
+      // attribute from the second version. Re-find each iteration to tolerate
+      // mid-$compile detaches; requiring ng-click rejects an empty/stale rerun
+      // where the same "Run second paragraph" string slips through.
+      final By newAngularDivLocator = By.xpath(
+              getParagraphXPath(1) + "//div[@id=\"angularRunParagraph\"]");
+      WebElement newAngularDiv = new WebDriverWait(manager.getWebDriver(),
+              Duration.ofSeconds(MAX_BROWSER_TIMEOUT_SEC))
+              .ignoring(StaleElementReferenceException.class)
+              .until(driver -> {
+                WebElement el = driver.findElement(newAngularDivLocator);
+                String ngClick = el.getAttribute("ng-click");
+                return el.isDisplayed()
+                        && "Run second paragraph".equals(el.getText())
+                        && ngClick != null && 
ngClick.contains("z.runParagraph")
+                        ? el : null;
+              });
 
       // Set new text value for 2nd paragraph
       setTextOfParagraph(2, "%sh echo NEW_VALUE");
diff --git a/zeppelin-web-angular/projects/zeppelin-react/package-lock.json 
b/zeppelin-web-angular/projects/zeppelin-react/package-lock.json
index 00c5813aec..2dd285a13c 100644
--- a/zeppelin-web-angular/projects/zeppelin-react/package-lock.json
+++ b/zeppelin-web-angular/projects/zeppelin-react/package-lock.json
@@ -5089,9 +5089,9 @@
       "license": "MIT"
     },
     "node_modules/fast-uri": {
-      "version": "3.1.2",
-      "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz";,
-      "integrity": 
"sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
+      "version": "3.1.4",
+      "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz";,
+      "integrity": 
"sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==",
       "dev": true,
       "funding": [
         {

Reply via email to