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

hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new 090e5478bd [IT] fix selenium tests (#8416)
090e5478bd is described below

commit 090e5478bd49b110965ea08baf538b1c9a8d24ab
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Wed Sep 16 12:05:29 2026 +0200

    [IT] fix selenium tests (#8416)
---
 docker/unified.Dockerfile                          |  2 +-
 docker/web.Dockerfile                              |  2 +-
 .../org/apache/hop/web/it/HopWebEnvironment.java   |  5 +-
 .../java/org/apache/hop/web/it/HopWebTestBase.java | 18 ++++-
 .../test/java/org/apache/hop/web/it/ServerLog.java | 18 ++++-
 .../java/org/apache/hop/web/it/ServerLogTest.java  | 19 +++++
 .../org/apache/hop/web/it/pages/ContextDialog.java |  8 +-
 .../org/apache/hop/web/it/pages/HopGuiPage.java    | 86 ++++++++++++++++++++--
 .../apache/hop/web/it/pages/PipelineGraphPage.java |  1 +
 9 files changed, 145 insertions(+), 14 deletions(-)

diff --git a/docker/unified.Dockerfile b/docker/unified.Dockerfile
index 5d290ceaa7..6424ea20f5 100644
--- a/docker/unified.Dockerfile
+++ b/docker/unified.Dockerfile
@@ -363,7 +363,7 @@ ENV HOP_AES_ENCODER_KEY_FILE=""
 ENV HOP_AUDIT_FOLDER="/tmp/hop-web-audit"
 ENV HOP_CONFIG_FOLDER="${CATALINA_HOME}/webapps/ROOT/config"
 ENV HOP_LOG_LEVEL="Basic"
-ENV HOP_OPTIONS="-XX:+AggressiveHeap 
-Dorg.eclipse.rap.rwt.resourceLocation=/tmp/rwt-resources"
+ENV HOP_OPTIONS="-XX:+AggressiveHeap"
 ENV HOP_PASSWORD_ENCODER_PLUGIN="Hop"
 ENV HOP_PLUGIN_BASE_FOLDERS=${CATALINA_HOME}/plugins
 ENV HOP_SHARED_JDBC_FOLDERS="${CATALINA_HOME}/jdbc-drivers"
diff --git a/docker/web.Dockerfile b/docker/web.Dockerfile
index 94540aa18a..e1bf3b8592 100644
--- a/docker/web.Dockerfile
+++ b/docker/web.Dockerfile
@@ -34,7 +34,7 @@ ENV HOP_LOG_LEVEL="Basic"
 # any JRE settings you want to pass on
 # The “-XX:+AggressiveHeap” tells the container to use all memory assigned to 
the container. 
 # this removed the need to calculate the necessary heap Xmx
-ENV HOP_OPTIONS="-XX:+AggressiveHeap 
-Dorg.eclipse.rap.rwt.resourceLocation=/tmp/rwt-resources"
+ENV HOP_OPTIONS="-XX:+AggressiveHeap"
 ENV HOP_PASSWORD_ENCODER_PLUGIN="Hop"
 ENV HOP_PLUGIN_BASE_FOLDERS="plugins"
 # path to jdbc drivers
diff --git 
a/web-tests/src/test/java/org/apache/hop/web/it/HopWebEnvironment.java 
b/web-tests/src/test/java/org/apache/hop/web/it/HopWebEnvironment.java
index da2be285a5..23fccc3f98 100644
--- a/web-tests/src/test/java/org/apache/hop/web/it/HopWebEnvironment.java
+++ b/web-tests/src/test/java/org/apache/hop/web/it/HopWebEnvironment.java
@@ -112,10 +112,9 @@ public final class HopWebEnvironment {
   /**
    * A fixed heap instead of the image's {@code -XX:+AggressiveHeap}: that 
flag hands the JVM most
    * of the host's memory, and on a build agent shared with other jobs and 
containers that is how
-   * Hop Web fails to come up at all. The RWT resource location is what the 
image sets; keep it.
+   * Hop Web fails to come up at all.
    */
-  private static final String DEFAULT_JAVA_OPTIONS =
-      "-Xmx2g -Dorg.eclipse.rap.rwt.resourceLocation=/tmp/rwt-resources";
+  private static final String DEFAULT_JAVA_OPTIONS = "-Xmx2g";
 
   /** Written next to the screenshots when Hop Web does not start, so CI 
archives it. */
   private static final String STARTUP_LOG_FILE = "hop-web-startup.log";
diff --git a/web-tests/src/test/java/org/apache/hop/web/it/HopWebTestBase.java 
b/web-tests/src/test/java/org/apache/hop/web/it/HopWebTestBase.java
index d98ac801e3..e40fca9d83 100644
--- a/web-tests/src/test/java/org/apache/hop/web/it/HopWebTestBase.java
+++ b/web-tests/src/test/java/org/apache/hop/web/it/HopWebTestBase.java
@@ -87,7 +87,23 @@ public abstract class HopWebTestBase {
                 + (browserErrors.isEmpty()
                     ? ""
                     : "\n  browser: " + String.join("\n           ", 
browserErrors))
-                + (errorDialog == null ? "" : "\n  dialog: " + errorDialog));
+                + (errorDialog == null ? "" : "\n  dialog: " + errorDialog)
+                + serverLogForServerErrors(browserErrors));
+  }
+
+  /**
+   * The server log of this test when the browser saw the server fail, 
whatever the crash detector
+   * made of it. A "status of 500" on a resource says nothing about why; the 
reason is on the server
+   * side, and a pattern the detector does not know yet must not lose it.
+   */
+  private String serverLogForServerErrors(List<String> browserErrors) {
+    if (browserErrors.stream().noneMatch(error -> error.contains("status of 
5"))) {
+      return "";
+    }
+    String log = HopWebEnvironment.get().serverLog();
+    String sinceTestStart = log.length() > serverLogMark ? 
log.substring(serverLogMark) : "";
+    return "\n  server log since this test started:\n    "
+        + (sinceTestStart.isBlank() ? "(nothing)" : 
sinceTestStart.strip().replace("\n", "\n    "));
   }
 
   /**
diff --git a/web-tests/src/test/java/org/apache/hop/web/it/ServerLog.java 
b/web-tests/src/test/java/org/apache/hop/web/it/ServerLog.java
index 95f2bb2517..3afe6fd813 100644
--- a/web-tests/src/test/java/org/apache/hop/web/it/ServerLog.java
+++ b/web-tests/src/test/java/org/apache/hop/web/it/ServerLog.java
@@ -41,6 +41,14 @@ public final class ServerLog {
    */
   private static final String UNCAUGHT = "Exception in thread ";
 
+  /**
+   * Tomcat's level for a servlet or filter that threw: {@code SEVERE 
[http-nio-8080-exec-3]
+   * org.apache.catalina.core.StandardWrapperValve.invoke Servlet.service() 
for servlet [default]
+   * ... threw exception}. That is the server side of a browser seeing an HTTP 
500 - for a static
+   * image under rwt-resources, for instance - and the only place the cause is 
written down.
+   */
+  private static final String TOMCAT_SEVERE = " SEVERE ";
+
   /**
    * Failures that are a Hop Web bug whether or not somebody caught them.
    *
@@ -85,7 +93,7 @@ public final class ServerLog {
   }
 
   private static boolean isCrash(String line) {
-    if (line.startsWith(UNCAUGHT)) {
+    if (line.startsWith(UNCAUGHT) || line.contains(TOMCAT_SEVERE)) {
       return true;
     }
     // Not on continuation lines: the signature appears again in every "Caused 
by" and in the
@@ -110,10 +118,18 @@ public final class ServerLog {
       String line = lines[i].trim();
       if (line.startsWith(HOP_FRAME)) {
         deepest = line;
+      } else if (isExceptionLine(line, i == start + 1)) {
+        // Still the same stack trace: the exception itself (Tomcat prints it 
on the line after
+        // its SEVERE header) or a cause, with more frames to come.
+        continue;
       } else if (!line.isEmpty() && !line.startsWith("at ")) {
         break;
       }
     }
     return deepest == null ? "" : " (" + deepest + ")";
   }
+
+  private static boolean isExceptionLine(String line, boolean 
rightAfterHeader) {
+    return line.startsWith("Caused by") || (rightAfterHeader && 
!line.startsWith("at "));
+  }
 }
diff --git a/web-tests/src/test/java/org/apache/hop/web/it/ServerLogTest.java 
b/web-tests/src/test/java/org/apache/hop/web/it/ServerLogTest.java
index 34ecf3108a..4dc00ad48b 100644
--- a/web-tests/src/test/java/org/apache/hop/web/it/ServerLogTest.java
+++ b/web-tests/src/test/java/org/apache/hop/web/it/ServerLogTest.java
@@ -83,6 +83,25 @@ class ServerLogTest {
     assertEquals(1, ServerLog.crashes(log).size());
   }
 
+  @Test
+  @DisplayName("a servlet that threw, as Tomcat logs it, is a crash")
+  void reportsTomcatSevere() {
+    // What the server writes when the browser sees an HTTP 500 - here for a 
static image.
+    String log =
+        "16-Sep-2026 08:58:09.123 SEVERE [http-nio-8080-exec-3]"
+            + " org.apache.catalina.core.StandardWrapperValve.invoke 
Servlet.service() for servlet"
+            + " [default] in context with path [] threw exception\n"
+            + "\tjava.io.IOException: Stream closed\n"
+            + "\t\tat 
org.apache.catalina.servlets.DefaultServlet.copy(DefaultServlet.java:2400)\n"
+            + "\t\tat 
org.apache.hop.ui.hopgui.security.HopBasicAuthFilter.doFilter(HopBasicAuthFilter.java:80)\n";
+
+    List<String> crashes = ServerLog.crashes(log);
+
+    assertEquals(1, crashes.size(), () -> "expected one crash, got " + 
crashes);
+    assertTrue(crashes.get(0).contains("threw exception"), crashes.get(0));
+    assertTrue(crashes.get(0).contains("HopBasicAuthFilter.doFilter"), 
crashes.get(0));
+  }
+
   @Test
   @DisplayName("the same failure logged over and over is reported once")
   void collapsesRepeats() {
diff --git 
a/web-tests/src/test/java/org/apache/hop/web/it/pages/ContextDialog.java 
b/web-tests/src/test/java/org/apache/hop/web/it/pages/ContextDialog.java
index ef7f87c3ef..67ab96d01e 100644
--- a/web-tests/src/test/java/org/apache/hop/web/it/pages/ContextDialog.java
+++ b/web-tests/src/test/java/org/apache/hop/web/it/pages/ContextDialog.java
@@ -65,7 +65,10 @@ public class ContextDialog {
   }
 
   void awaitOpen() {
-    wait.until(d -> isOpen());
+    wait.withMessage(
+            () ->
+                "the context dialog did not open; on screen: " + 
HopGuiPage.describeShells(driver))
+        .until(d -> isOpen());
   }
 
   public boolean isOpen() {
@@ -93,6 +96,7 @@ public class ContextDialog {
    */
   public void choose(String search, int skip) {
     // The dialog focuses its search field on open, so the keystrokes need no 
target.
+    HopGuiPage.awaitIdle(driver);
     new Actions(driver).sendKeys(search).perform();
     awaitFilterSettled();
     for (int i = 0; i < skip; i++) {
@@ -101,6 +105,8 @@ public class ContextDialog {
     if (skip > 0) {
       awaitFilterSettled();
     }
+    // Idle as well as settled: the last keystroke's request may still be on 
its way back.
+    HopGuiPage.awaitIdle(driver);
     new Actions(driver).sendKeys(Keys.ENTER).perform();
     wait.until(d -> !isOpen());
   }
diff --git 
a/web-tests/src/test/java/org/apache/hop/web/it/pages/HopGuiPage.java 
b/web-tests/src/test/java/org/apache/hop/web/it/pages/HopGuiPage.java
index c67f3774c1..8c40c0b2fb 100644
--- a/web-tests/src/test/java/org/apache/hop/web/it/pages/HopGuiPage.java
+++ b/web-tests/src/test/java/org/apache/hop/web/it/pages/HopGuiPage.java
@@ -216,11 +216,31 @@ public class HopGuiPage {
   public String awaitDialog() {
     // A blank title is not a dialog: nothing Hop opens is untitled, so it 
would be the detector
     // catching something else mid-render.
-    return wait.until(
-        d -> {
-          String title = topDialogTitle();
-          return title == null || title.isBlank() ? null : title;
-        });
+    return wait.withMessage(() -> "no dialog opened; on screen: " + 
describeShells(driver))
+        .until(
+            d -> {
+              String title = topDialogTitle();
+              return title == null || title.isBlank() ? null : title;
+            });
+  }
+
+  /**
+   * Every body-level element with the RAP widget behind it, for the message 
of a wait that gave up:
+   * which shells exist, how big they are and what they say is what tells a 
dialog that never opened
+   * apart from one the detector does not recognise.
+   */
+  private static final String DESCRIBE_SHELLS =
+      "return [...document.body.children].map(d=>{"
+          + "const w=d.rwtWidget;const r=d.getBoundingClientRect();"
+          + "const cls=w?w.classname:'(no widget)';"
+          + "const title=w&&typeof 
w.hasState==='function'&&w.hasState('rwt_TITLE')?' titled':'';"
+          + "return cls+title+' '+Math.round(r.width)+'x'+Math.round(r.height)"
+          + "+' z='+getComputedStyle(d).zIndex+' '+getComputedStyle(d).display"
+          + "+' 
\"'+(d.innerText||'').split('\\n')[0].trim().slice(0,50)+'\"';}).join('; ');";
+
+  public static String describeShells(WebDriver driver) {
+    Object description = ((JavascriptExecutor) 
driver).executeScript(DESCRIBE_SHELLS);
+    return description == null ? "" : description.toString();
   }
 
   /** How long to give Escape before falling back to the dialog's own button. 
*/
@@ -416,9 +436,62 @@ public class HopGuiPage {
     awaitNoDialog();
   }
 
-  /** Waits until nothing is stacked on top of the Hop GUI any more. */
+  /**
+   * Waits until nothing is stacked on top of the Hop GUI any more, and Hop 
Web has finished what
+   * closing the dialog set in motion. Opening a file, say, closes the file 
dialog first and loads
+   * the pipeline afterwards; a click that arrives in between is swallowed by 
the input blocker RAP
+   * puts up while a request is in flight.
+   */
   public void awaitNoDialog() {
     wait.until(d -> openDialogTitles().isEmpty());
+    awaitIdle();
+  }
+
+  /**
+   * Whether the RAP client will deliver a click right now.
+   *
+   * <p>Two things have to hold. The connection is between requests - nothing 
in flight, nothing
+   * queued: the client batches events for a few milliseconds before sending, 
and the server can ask
+   * for a follow-up request when it has {@code asyncExec} work left. And no 
input blocker is on
+   * screen without a dialog behind it: RAP blocks the page behind a modal 
dialog and keeps the
+   * blocker up a moment after the dialog has gone (it fades), and it blocks 
the page while a slow
+   * request is answered; a click in either state is swallowed. A blocker 
behind an open dialog is
+   * fine - that is the dialog being modal, and the click is going to the 
dialog.
+   */
+  private static final String IS_IDLE =
+      "if(typeof rwt==='undefined'||!rwt.remote||!rwt.remote.Connection)return 
true;"
+          + "const c=rwt.remote.Connection.getInstance();"
+          + "const armed=t=>t&&(typeof 
t.getEnabled==='function'?t.getEnabled():t.isEnabled());"
+          + 
"if(c._requestPending||armed(c._sendTimer)||armed(c._delayTimer))return false;"
+          + "const blocked=[...document.body.children].some(d=>{const 
w=d.rwtWidget;"
+          + 
"if(!w||w.classname!=='rwt.widgets.base.ClientDocumentBlocker')return false;"
+          + "const r=d.getBoundingClientRect();return 
r.width>0&&r.height>0;});"
+          + "if(!blocked)return true;"
+          + DIALOG_SHELLS
+          + "return shells.length>0;";
+
+  /**
+   * Waits until the RAP client is idle, which is when a click or a keystroke 
is sure to reach Hop.
+   *
+   * <p>While a request is in flight RAP blocks input behind a transparent 
full-screen element, so
+   * anything sent then is lost without a trace: no dialog opens, no error 
appears, the test times
+   * out on whatever it was going to wait for next.
+   */
+  public static void awaitIdle(WebDriver driver) {
+    waitFor(driver, IDLE_TIMEOUT)
+        .withMessage(() -> "Hop Web did not go idle; on screen: " + 
describeShells(driver))
+        .until(d -> isIdle(driver));
+  }
+
+  public void awaitIdle() {
+    awaitIdle(driver);
+  }
+
+  /** Long enough for any single request Hop Web makes while a test is driving 
it. */
+  private static final Duration IDLE_TIMEOUT = Duration.ofSeconds(30);
+
+  private static boolean isIdle(WebDriver driver) {
+    return Boolean.TRUE.equals(((JavascriptExecutor) 
driver).executeScript(IS_IDLE));
   }
 
   /** Opens a pipeline file and returns its graph once the transform named is 
on screen. */
@@ -567,6 +640,7 @@ public class HopGuiPage {
     waitFor(driver, ENABLE_TIMEOUT)
         .withMessage(() -> "widget " + describe(element) + " is disabled")
         .until(d -> isEnabled(element));
+    awaitIdle();
     new Actions(driver).moveToElement(element).click().perform();
   }
 
diff --git 
a/web-tests/src/test/java/org/apache/hop/web/it/pages/PipelineGraphPage.java 
b/web-tests/src/test/java/org/apache/hop/web/it/pages/PipelineGraphPage.java
index e4d36d2893..a0c0997183 100644
--- a/web-tests/src/test/java/org/apache/hop/web/it/pages/PipelineGraphPage.java
+++ b/web-tests/src/test/java/org/apache/hop/web/it/pages/PipelineGraphPage.java
@@ -332,6 +332,7 @@ public class PipelineGraphPage {
         }
       }
     }
+    HopGuiPage.awaitIdle(driver);
     new Actions(driver).moveToElement(canvas(), offsetX, 
offsetY).click().perform();
     lastClickAt = System.currentTimeMillis();
     lastClickX = offsetX;

Reply via email to