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": [
{