From eeb440a6231acaed9ac4316d28e24071b5965f86 Mon Sep 17 00:00:00 2001 From: Jongyoul Lee Date: Sun, 17 May 2026 11:33:46 +0900 Subject: [PATCH 1/4] [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 0d21e20d689..7cc923ab6f0 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 @@ -345,9 +345,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 766e56a386a91d73337d903a9a668e6f50909bdb Mon Sep 17 00:00:00 2001 From: Jongyoul Lee Date: Sun, 17 May 2026 22:44:48 +0900 Subject: [PATCH 2/4] [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 7cc923ab6f0..e6036f6adf3 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,11 +326,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 50f4c8269430bba7a3b3768f25d06e2680ff6694 Mon Sep 17 00:00:00 2001 From: Jongyoul Lee Date: Mon, 18 May 2026 09:08:09 +0900 Subject: [PATCH 3/4] [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 e6036f6adf3..1f81715b8a9 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 @@ -327,29 +327,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)) @@ -357,8 +359,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(), @@ -366,7 +370,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; }); From aea7c5e2bbb288f5f9acdd5ea484fb7b70c08f30 Mon Sep 17 00:00:00 2001 From: Jongyoul Lee Date: Mon, 13 Jul 2026 12:47:58 +0900 Subject: [PATCH 4/4] [ZEPPELIN-1836] Re-enable angular paragraph integration test --- .../test/java/org/apache/zeppelin/integration/ZeppelinIT.java | 1 - 1 file changed, 1 deletion(-) 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 1f81715b8a9..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 @@ -297,7 +297,6 @@ void testSparkInterpreterDependencyLoading() throws Exception { } @Test - @Disabled("ZEPPELIN-6410: testAngularRunParagraph consistently fails due to element clickability timeout") void testAngularRunParagraph() throws Exception { try { createNewNote();