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;