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 b70fc0ba72 [IT] fix selenium tests (#8399)
b70fc0ba72 is described below
commit b70fc0ba723c763a6f75d87b22ca7249d59625e1
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Tue Sep 15 16:08:35 2026 +0200
[IT] fix selenium tests (#8399)
---
web-tests/pom.xml | 3 +-
.../org/apache/hop/web/it/HopWebEnvironment.java | 31 ++++++++---
.../org/apache/hop/web/it/pages/ContextDialog.java | 8 +--
.../org/apache/hop/web/it/pages/HopGuiPage.java | 65 ++++++++++++----------
.../apache/hop/web/it/pages/PreviewDataDialog.java | 6 +-
5 files changed, 66 insertions(+), 47 deletions(-)
diff --git a/web-tests/pom.xml b/web-tests/pom.xml
index 33fcab02ce..d2ed463124 100644
--- a/web-tests/pom.xml
+++ b/web-tests/pom.xml
@@ -31,7 +31,8 @@
<properties>
<hopweb.browser>auto</hopweb.browser>
-
<hopweb.browserImage>selenium/standalone-chrome:4.47.0</hopweb.browserImage>
+ <!-- Empty: HopWebEnvironment picks selenium/standalone-chrome or
-chromium by architecture -->
+ <hopweb.browserImage></hopweb.browserImage>
<hopweb.build>false</hopweb.build>
<hopweb.headless>true</hopweb.headless>
<hopweb.image>apache/hop-web:Development</hopweb.image>
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 21802d4346..da2be285a5 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
@@ -65,8 +65,10 @@ import org.testcontainers.utility.MountableFile;
* ago. Re-pulling an unchanged image transfers nothing.
* <li>{@code hopweb.url} - URL of an already running Hop Web. Set it while
writing tests to skip
* container startup entirely, e.g. {@code
-Dhopweb.url=http://localhost:8080/ui}
- * <li>{@code hopweb.browser} - {@code auto} (default), {@code container} or
{@code local}
- * <li>{@code hopweb.browserImage} - the browser image, for a containerised
browser
+ * <li>{@code hopweb.browser} - {@code auto} (default, a container), {@code
container} or {@code
+ * local}
+ * <li>{@code hopweb.browserImage} - the browser image, for a containerised
browser; defaults to
+ * selenium/standalone-chrome on amd64 and selenium/standalone-chromium
on arm64
* <li>{@code hopweb.headless} - only meaningful for a local browser
* <li>{@code hopweb.javaOptions} - the JVM options Hop Web runs with,
replacing the image's
* {@code HOP_OPTIONS}. The image default is {@code
-XX:+AggressiveHeap}, which sizes the heap
@@ -87,7 +89,19 @@ public final class HopWebEnvironment {
* matching image is out; the client and the browser only have to speak the
same WebDriver
* protocol, not carry the same version.
*/
- private static final String DEFAULT_BROWSER_IMAGE =
"selenium/standalone-chrome:4.47.0";
+ private static final String BROWSER_IMAGE_VERSION = "4.47.0";
+
+ /**
+ * selenium/standalone-chrome is amd64 only; selenium/standalone-chromium is
the same image built
+ * for arm64 as well. Both run the same browser, so an arm64 machine gets a
containerised browser
+ * too instead of falling back to whatever Chrome is installed locally - a
local browser is fast
+ * enough to hide the request-timing races the CI browser trips over.
+ */
+ private static final String DEFAULT_BROWSER_IMAGE =
+ ("aarch64".equals(System.getProperty("os.arch"))
+ ? "selenium/standalone-chromium:"
+ : "selenium/standalone-chrome:")
+ + BROWSER_IMAGE_VERSION;
private static final String HOP_WEB_ALIAS = "hop-web";
private static final int HOP_WEB_PORT = 8080;
@@ -410,7 +424,10 @@ public final class HopWebEnvironment {
private WebDriver containerBrowser(Network network) {
BrowserWebDriverContainer<?> browser =
new BrowserWebDriverContainer<>(
- DockerImageName.parse(property("hopweb.browserImage",
DEFAULT_BROWSER_IMAGE)))
+ DockerImageName.parse(property("hopweb.browserImage",
DEFAULT_BROWSER_IMAGE))
+ // Testcontainers only accepts selenium/standalone-chrome
by name; the
+ // multi-arch selenium/standalone-chromium is the same
thing for arm64.
+ .asCompatibleSubstituteFor("selenium/standalone-chrome"))
.withNetwork(network)
.withCapabilities(chromeOptions())
.withStartupTimeout(Duration.ofSeconds(startupTimeoutSeconds()));
@@ -469,9 +486,9 @@ public final class HopWebEnvironment {
static BrowserMode resolve(String value) {
if ("auto".equalsIgnoreCase(value)) {
- // The selenium/standalone-chrome images are amd64 only, so an arm64
developer machine
- // has to drive the browser it already has.
- return "aarch64".equals(System.getProperty("os.arch")) ? LOCAL :
CONTAINER;
+ // Always the container: it is what CI runs, and the default image is
picked per
+ // architecture. -Dhopweb.browser=local is there for watching a test
in a real window.
+ return CONTAINER;
}
return "local".equalsIgnoreCase(value) ? LOCAL : CONTAINER;
}
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 13ed8f4198..ef7f87c3ef 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
@@ -50,12 +50,8 @@ public class ContextDialog {
* immediately, so Enter fired before the results had filtered at all.
*/
private static final String RENDER_STATE =
- "const sh=[...document.body.children].filter(d=>{"
- + "if(d.tagName!=='DIV')return false;"
- + "const z=parseInt(getComputedStyle(d).zIndex);"
- + "const r=d.getBoundingClientRect();"
- + "return z>=100000&&r.width>100&&r.height>100;});"
- + "const top=sh[sh.length-1];"
+ HopGuiPage.DIALOG_SHELLS
+ + "const top=shells[shells.length-1];"
+ "if(!top)return '';"
+ "const r=top.getBoundingClientRect();"
+ "return
Math.round(r.width)+'x'+Math.round(r.height)+'|'+(top.innerText||'');";
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 80503c86c7..c67f3774c1 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
@@ -33,18 +33,36 @@ import org.openqa.selenium.support.ui.WebDriverWait;
public class HopGuiPage {
/**
- * Every visible RAP shell that is big enough to be a window, outermost
first. Index 0 is the Hop
- * GUI itself, so anything beyond it is an open dialog. A shell's first line
of text is its title,
+ * Script prologue that leaves {@code shells}: the dialogs on screen,
outermost first.
+ *
+ * <p>A dialog is recognised by what RAP hangs off its element - a {@code
rwt.widgets.Shell} with
+ * a title bar - rather than by looking like one. The previous rule, "any
body-level div at
+ * z-index 100000 or more that is bigger than 100x100", also matched the
transparent full-screen
+ * blocker RAP shows while a request is in flight. On a slow browser that
blocker is on screen in
+ * the gap between choosing "Edit" in the context dialog and the transform
dialog opening (the
+ * action runs in an asyncExec, one request later), and it read as a dialog
with an empty title:
+ * {@code awaitDialog()} returned "", {@code closeAllDialogs()} "closed" it,
and the real dialog
+ * then opened under the next click. Shells without a title bar are Hop's
canvas tooltip and the
+ * Hop GUI window itself; a title-bar shell with no parent that covers the
viewport is also the
+ * GUI window, should it ever get a caption.
+ */
+ public static final String DIALOG_SHELLS =
+ "const shells=[...document.body.children].filter(d=>{"
+ + "const w=d.rwtWidget;"
+ + "if(!w||w.classname!=='rwt.widgets.Shell')return false;"
+ + "if(typeof
w.hasState!=='function'||!w.hasState('rwt_TITLE'))return false;"
+ + "const r=d.getBoundingClientRect();"
+ + "if(!(r.width>0&&r.height>0))return false;"
+ + "return !!w._parentShell"
+ + "||r.width<window.innerWidth||r.height<window.innerHeight;});";
+
+ /**
+ * The titles of the open dialogs, outermost first. A shell's first line of
text is its title,
* which is how these tests identify dialogs - far steadier than the
positional {@code
* //body/div[5]/div[1]} the previous suite matched on.
*/
private static final String SHELL_TITLES =
- "return [...document.body.children].filter(d=>{"
- + "if(d.tagName!=='DIV')return false;"
- + "const z=parseInt(getComputedStyle(d).zIndex);"
- + "const r=d.getBoundingClientRect();"
- + "return z>=100000&&r.width>100&&r.height>100;})"
- + ".map(d=>(d.innerText||'').split('\\n')[0].trim());";
+ DIALOG_SHELLS + "return
shells.map(d=>(d.innerText||'').split('\\n')[0].trim());";
/**
* How often to re-check a condition. The default of 500ms is the single
biggest cost in this
@@ -107,11 +125,7 @@ public class HopGuiPage {
* and a dialog builds its fields in plain SWT code.
*/
private static final String TOP_INPUTS =
- "const shells=[...document.body.children].filter(d=>{"
- + "if(d.tagName!=='DIV')return false;"
- + "const z=parseInt(getComputedStyle(d).zIndex);"
- + "const r=d.getBoundingClientRect();"
- + "return z>=100000&&r.width>100&&r.height>100;});"
+ DIALOG_SHELLS
+ "const top=shells[shells.length-1]||document;"
+ "return [...top.querySelectorAll('input')]"
+ ".filter(i=>i.type==='text'&&i.offsetParent!==null);";
@@ -138,8 +152,7 @@ public class HopGuiPage {
public static List<String> openDialogTitles(WebDriver driver) {
@SuppressWarnings("unchecked")
List<String> titles = (List<String>) ((JavascriptExecutor)
driver).executeScript(SHELL_TITLES);
- // Drop the Hop GUI shell itself; only the dialogs above it are
interesting.
- return titles.isEmpty() ? titles : titles.subList(1, titles.size());
+ return titles;
}
public static String topDialogTitle(WebDriver driver) {
@@ -162,13 +175,7 @@ public class HopGuiPage {
}
private static final String TOP_SHELL_TEXT =
- "const shells=[...document.body.children].filter(d=>{"
- + "if(d.tagName!=='DIV')return false;"
- + "const z=parseInt(getComputedStyle(d).zIndex);"
- + "const r=d.getBoundingClientRect();"
- + "return z>=100000&&r.width>100&&r.height>100;});"
- + "const top=shells[shells.length-1];"
- + "return top?top.innerText:'';";
+ DIALOG_SHELLS + "const top=shells[shells.length-1];" + "return
top?top.innerText:'';";
/**
* Closes the welcome dialog if this Hop Web was configured to show it. The
image used by the
@@ -207,7 +214,13 @@ public class HopGuiPage {
* reading the title straight afterwards is racing it.
*/
public String awaitDialog() {
- return wait.until(d -> topDialogTitle());
+ // 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;
+ });
}
/** How long to give Escape before falling back to the dialog's own button.
*/
@@ -354,11 +367,7 @@ public class HopGuiPage {
private static final String FIELD_BESIDE_LABEL =
"const wanted=arguments[0];"
- + "const shells=[...document.body.children].filter(d=>{"
- + "if(d.tagName!=='DIV')return false;"
- + "const z=parseInt(getComputedStyle(d).zIndex);"
- + "const r=d.getBoundingClientRect();"
- + "return z>=100000&&r.width>100&&r.height>100;});"
+ + DIALOG_SHELLS
+ "const top=shells[shells.length-1];"
+ "if(!top)return null;"
+ "const label=[...top.querySelectorAll('div')].find("
diff --git
a/web-tests/src/test/java/org/apache/hop/web/it/pages/PreviewDataDialog.java
b/web-tests/src/test/java/org/apache/hop/web/it/pages/PreviewDataDialog.java
index 8d01d33727..e1dc677395 100644
--- a/web-tests/src/test/java/org/apache/hop/web/it/pages/PreviewDataDialog.java
+++ b/web-tests/src/test/java/org/apache/hop/web/it/pages/PreviewDataDialog.java
@@ -44,11 +44,7 @@ public class PreviewDataDialog {
* left out.
*/
private static final String GRID_ROWS =
- "const shells=[...document.body.children].filter(d=>{"
- + "if(d.tagName!=='DIV')return false;"
- + "const z=parseInt(getComputedStyle(d).zIndex);"
- + "const r=d.getBoundingClientRect();"
- + "return z>=100000&&r.width>100&&r.height>100;});"
+ HopGuiPage.DIALOG_SHELLS
+ "const top=shells[shells.length-1];"
+ "if(!top)return [];"
+ "return [...top.querySelectorAll('div')]"