From c5ba29ad4d3b6bafbb68110c820d77ef9761838c Mon Sep 17 00:00:00 2001 From: Jongyoul Lee Date: Sun, 17 May 2026 11:33:46 +0900 Subject: [PATCH 1/3] [ZEPPELIN-1836] Make testAngularRunParagraph wait for rendered Angular output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After the rerun, visibilityWait alone races against AngularJS $compile — the result div can briefly detach or render empty before settling, so the wait times out at 30s on master frontend.yml runs. Replace the single visibilityWait with a polling lambda that re-finds the element each iteration, tolerates StaleElementReferenceException, and only returns once the div is displayed *and* shows the expected text. This mirrors the content-based pattern already used after the first run. Builds on the stalenessOf + JS click fix from ZEPPELIN-6409. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../apache/zeppelin/integration/ZeppelinIT.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) 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 745cfd284bf..008f28bcf88 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 @@ -344,9 +344,18 @@ void testAngularRunParagraph() throws Exception { 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 visibility + rendered text and re-find each iteration; AngularJS may + // briefly leave the result div detached or empty during $compile after a rerun. + 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); + return el.isDisplayed() && "Run second paragraph".equals(el.getText()) + ? el : null; + }); // Set new text value for 2nd paragraph setTextOfParagraph(2, "%sh echo NEW_VALUE"); From bb5aef4a02ec4904d0644c637d305673cb74d4ef Mon Sep 17 00:00:00 2001 From: Jongyoul Lee Date: Sun, 17 May 2026 22:44:48 +0900 Subject: [PATCH 2/3] [ZEPPELIN-1836] Commit paragraph.text via scope before rerunning angular paragraph MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 4ms FINISHED on the rerun + missing replacement div pointed at the paragraph executing with stale (or empty) text: ACE's setValue does not fire the 'input' event that paragraph.controller.js binds aceChanged to, so paragraph.text never commits. If the angular paragraph's editor was closed after the first run, $scope.editor is falsy and getEditorValue() falls back to paragraph.text — sending the old body to the interpreter, which echoes back an empty/stale ANGULAR result. That explains the stalenessOf success followed by no new div. Force the commit by writing the new text directly onto the paragraph scope, syncing the ACE buffer when present, and running $apply. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../zeppelin/integration/ZeppelinIT.java | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) 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 008f28bcf88..ae6fc4a6018 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 @@ -325,11 +325,23 @@ void testAngularRunParagraph() throws Exception { 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
Run second paragraph
"); + + "\")'>Run second paragraph"; + setTextOfParagraph(1, newAngularText.replace("'", "\\'")); + ((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(); }" + + "if (!s.$$phase && !s.$root.$$phase) { s.$apply(); }", + newAngularText); // Capture old output element before re-run to detect when it gets replaced WebElement oldAngularDiv = manager.getWebDriver().findElement(By.xpath( From 92b58d26996290664dfa7b090b8025267b0f0b74 Mon Sep 17 00:00:00 2001 From: Jongyoul Lee Date: Mon, 18 May 2026 09:08:09 +0900 Subject: [PATCH 3/3] [ZEPPELIN-1836] Run rerun via controller scope to avoid ACE/play-button race MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous scope-commit attempt still saw the rerun finish in ~3ms with no new div in 30s. That matches a stale/empty paragraph.text reaching the backend, not just a render race: * setTextOfParagraph(1, ...) clicks `icon-size-fullscreen`. After the first %angular run the paragraph has editorHide=true, so this toggle re-mounts the ACE editor; ace.edit(id).setValue() races with that re-mount. * ACE setValue() does not fire the editor's 'input' event, so the binding $scope.editor.on('input', aceChanged) never runs and paragraph.text is never committed. * runParagraphFromButton calls getEditorValue(), which returns paragraph.text when $scope.editor is falsy (between teardown and the new binding from the toggle). The backend then receives the previous body — or an empty one — and the AngularInterpreter echoes back identical/empty ANGULAR data. elem.html(empty) detaches the old div (stalenessOf passes) while leaving no replacement, which is exactly the observed timeline. Drive the rerun through the controller's own runParagraph(text, ...) so the new text is delivered no matter what the editor is doing. Tighten the new-render wait to also require the ng-click attribute, so a stale-text echo cannot satisfy the condition. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../zeppelin/integration/ZeppelinIT.java | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) 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 ae6fc4a6018..ff45f123540 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 @@ -326,29 +326,31 @@ void testAngularRunParagraph() throws Exception { assertTrue(isNotBlank(secondParagraphId), "Cannot find paragraph id for the 2nd paragraph"); // Update first paragraph to call z.runParagraph() with 2nd paragraph id. - // ACE setValue does not fire the editor's 'input' event, so paragraph.text - // does not commit on its own; if the editor was closed after the first run - // ($scope.editor falsy), runParagraph would resend the previous text. Set - // paragraph.text on the scope and sync the ACE buffer, then $apply. + // 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
Run second paragraph
"; - setTextOfParagraph(1, newAngularText.replace("'", "\\'")); + + // 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\"]")); + ((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(); }" - + "if (!s.$$phase && !s.$root.$$phase) { s.$apply(); }", + + "s.runParagraph(arguments[0], true, false);", newAngularText); - // 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); - // 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) new WebDriverWait(manager.getWebDriver(), Duration.ofSeconds(MAX_BROWSER_TIMEOUT_SEC)) @@ -356,8 +358,10 @@ void testAngularRunParagraph() throws Exception { waitForParagraph(1, "FINISHED"); - // Poll for visibility + rendered text and re-find each iteration; AngularJS may - // briefly leave the result div detached or empty during $compile after a rerun. + // 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(), @@ -365,7 +369,10 @@ void testAngularRunParagraph() throws Exception { .ignoring(StaleElementReferenceException.class) .until(driver -> { WebElement el = driver.findElement(newAngularDivLocator); - return el.isDisplayed() && "Run second paragraph".equals(el.getText()) + String ngClick = el.getAttribute("ng-click"); + return el.isDisplayed() + && "Run second paragraph".equals(el.getText()) + && ngClick != null && ngClick.contains("z.runParagraph") ? el : null; });