From 5bb98d8fcec5f88b77a4fd4dfa9308e65f695562 Mon Sep 17 00:00:00 2001 From: Cory Nathe Date: Fri, 27 Feb 2026 16:05:06 -0600 Subject: [PATCH 1/3] GitHub Issue 875: Standard Assay Multi-File Transform Import Skips First Data Row (#2896) - GpatAssayTest to randomlyAddTransformScript for testing GitHub Issue 875 --- src/org/labkey/test/tests/GpatAssayTest.java | 13 ++++++++++++- src/org/labkey/test/util/TestDataGenerator.java | 10 +++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/org/labkey/test/tests/GpatAssayTest.java b/src/org/labkey/test/tests/GpatAssayTest.java index 1770094f38..14b04e28cd 100644 --- a/src/org/labkey/test/tests/GpatAssayTest.java +++ b/src/org/labkey/test/tests/GpatAssayTest.java @@ -41,6 +41,7 @@ import org.labkey.test.util.EscapeUtil; import org.labkey.test.util.LogMethod; import org.labkey.test.util.LoggedParam; +import org.labkey.test.util.RReportHelper; import org.labkey.test.util.TestDataGenerator; import org.labkey.test.util.core.webdav.WebDavUploadHelper; import org.openqa.selenium.WebElement; @@ -72,11 +73,13 @@ public class GpatAssayTest extends BaseWebDriverTest private static final String ASSAY_NAME_FNA = "FASTA Assay"; private static final String ASSAY_NAME_FNA_MULTIPLE = "FASTA Assay - Multiple file upload"; private static final String ASSAY_NAME_FNA_MULTIPLE_SINGLE_INPUT = "FASTA Assay - Multiple file single input upload"; + private static final File RTRANSFORM_SCRIPT_FILE_NOOP = TestFileUtils.getSampleData("qc/noopTransform.R"); @BeforeClass public static void doSetup() { GpatAssayTest init = getCurrentTest(); + new RReportHelper(init).ensureRConfig(); init._containerHelper.createProject(init.getProjectName(), "Assay"); init.goToProjectHome(); } @@ -256,6 +259,14 @@ private void importFastaGpatAssay(File fnaFile, String assayName) clickButton("Save and Finish", defaultWaitForPage); } + // GitHub Issue #875: Optionally add transform scripts in GPAT assay design to test code path with and without transform script + private void randomlyAddTransformScript(ReactAssayDesignerPage assayDesignerPage) + { + boolean shouldAddTransformScript = TestDataGenerator.randomBoolean("whether to add transform script in assay design"); + if (shouldAddTransformScript) + assayDesignerPage.addTransformScript(RTRANSFORM_SCRIPT_FILE_NOOP); + } + @LogMethod private ReactAssayDesignerPage startCreateGpatAssay(File dataFile, @LoggedParam String assayName) { @@ -265,9 +276,9 @@ private ReactAssayDesignerPage startCreateGpatAssay(File dataFile, @LoggedParam _fileBrowserHelper.importFile(dataFile.getName(), "Create New Standard Assay Design"); ReactAssayDesignerPage assayDesignerPage = new ReactAssayDesignerPage(getDriver()); - if (assayName != null) assayDesignerPage.setName(assayName); + randomlyAddTransformScript(assayDesignerPage); return assayDesignerPage; } diff --git a/src/org/labkey/test/util/TestDataGenerator.java b/src/org/labkey/test/util/TestDataGenerator.java index 854b2d2952..bd0436eab5 100644 --- a/src/org/labkey/test/util/TestDataGenerator.java +++ b/src/org/labkey/test/util/TestDataGenerator.java @@ -799,7 +799,15 @@ public String randomDateString(String dateFormat, Date min, Date max) public static boolean randomBoolean() { - return ThreadLocalRandom.current().nextBoolean(); + return randomBoolean(null); + } + + public static boolean randomBoolean(@Nullable String message) + { + boolean value = ThreadLocalRandom.current().nextBoolean(); + if (message != null) + TestLogger.log("Generated random boolean value for %s: %s".formatted(message, value)); + return value; } private @NotNull List getFieldsForFile() From c7a5caeb6d3afdb28517d02e090134c4e4ba98b8 Mon Sep 17 00:00:00 2001 From: Trey Chadick Date: Mon, 2 Mar 2026 11:44:21 -0800 Subject: [PATCH 2/3] Handle behavior change in `WebElement.isDisplayed` (#2895) - Centralize JavascriptExecutor customization - Upgrade Selenium to 4.41.0 --- gradle.properties | 2 +- src/org/labkey/test/WebDriverWrapper.java | 19 +---- .../components/ui/grids/ResponsiveGrid.java | 2 +- .../selenium/JavascriptExecutorWrapper.java | 85 +++++++++++++++++++ .../test/util/selenium/WebDriverUtils.java | 13 +++ .../test/util/selenium/WebElementUtils.java | 29 ++++++- 6 files changed, 132 insertions(+), 18 deletions(-) create mode 100644 src/org/labkey/test/util/selenium/JavascriptExecutorWrapper.java diff --git a/gradle.properties b/gradle.properties index de493ea18a..c1d3217e49 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,7 +10,7 @@ lookfirstSardineVersion=5.13 jettyVersion=12.1.5 -seleniumVersion=4.40.0 +seleniumVersion=4.41.0 mockserverNettyVersion=5.15.0 diff --git a/src/org/labkey/test/WebDriverWrapper.java b/src/org/labkey/test/WebDriverWrapper.java index 4065e31c28..af3e2b6a67 100644 --- a/src/org/labkey/test/WebDriverWrapper.java +++ b/src/org/labkey/test/WebDriverWrapper.java @@ -63,6 +63,7 @@ import org.labkey.test.util.TextSearcher; import org.labkey.test.util.TextSearcher.TextTransformers; import org.labkey.test.util.Timer; +import org.labkey.test.util.selenium.JavascriptExecutorWrapper; import org.labkey.test.util.selenium.ScrollUtils; import org.labkey.test.util.selenium.WebDriverUtils; import org.openqa.selenium.Alert; @@ -539,11 +540,7 @@ public Object executeScript(@Language("JavaScript") String script, Object... arg */ public T executeScript(@Language("JavaScript") String script, Class expectedResultType, Object... arguments) { - Object o = executeScript(script, arguments); - if (o != null && !expectedResultType.isAssignableFrom(o.getClass())) - Assert.fail("Script return wrong type. Expected '" + expectedResultType.getSimpleName() + "'. Got: " + o.getClass().getName() + ". Result: " + o); - - return (T) o; + return new JavascriptExecutorWrapper(getDriver()).executeScript(script, expectedResultType, arguments); } /** @@ -552,20 +549,12 @@ public T executeScript(@Language("JavaScript") String script, Class expec */ public Object executeAsyncScript(@Language("JavaScript") String script, Object... arguments) { - script = "var callback = arguments[arguments.length - 1];\n" + // See WebDriver documentation for details on injected callback - "try {" + - script + - "} catch (error) { callback(error); }"; // ensure that the callback is invoked when an exception would otherwise prevent it - return ((JavascriptExecutor) getDriver()).executeAsyncScript(script, arguments); + return new JavascriptExecutorWrapper(getDriver()).executeAsyncScript(script, arguments); } public T executeAsyncScript(@Language("JavaScript") String script, Class expectedResultType, Object... arguments) { - Object o = executeAsyncScript(script, arguments); - if (o != null && !expectedResultType.isAssignableFrom(o.getClass())) - Assert.fail("Script return wrong type. Expected '" + expectedResultType.getSimpleName() + "'. Got: " + o.getClass().getName() + ". Result: " + o); - - return (T) o; + return new JavascriptExecutorWrapper(getDriver()).executeAsyncScript(script, expectedResultType, arguments); } @LogMethod(quiet = true) diff --git a/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java b/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java index 625e9d4909..f21859cec3 100644 --- a/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java +++ b/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java @@ -75,7 +75,7 @@ public WebElement getComponentElement() public Boolean isLoaded() { - return getComponentElement().isDisplayed() && + return WebElementUtils.checkVisibility(getComponentElement()) && !Locators.loadingGrid.existsIn(this) && !Locators.spinner.existsIn(this) && (Locator.tag("td").existsIn(this) || diff --git a/src/org/labkey/test/util/selenium/JavascriptExecutorWrapper.java b/src/org/labkey/test/util/selenium/JavascriptExecutorWrapper.java new file mode 100644 index 0000000000..ceb3928b8a --- /dev/null +++ b/src/org/labkey/test/util/selenium/JavascriptExecutorWrapper.java @@ -0,0 +1,85 @@ +package org.labkey.test.util.selenium; + +import org.intellij.lang.annotations.Language; +import org.jetbrains.annotations.Nullable; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.ScriptKey; +import org.openqa.selenium.WebDriver; + +import java.util.Set; + +public class JavascriptExecutorWrapper implements JavascriptExecutor +{ + private final JavascriptExecutor _wrappedExecutor; + + public JavascriptExecutorWrapper(WebDriver driver) + { + _wrappedExecutor = (JavascriptExecutor) driver; + } + + @Override + public @Nullable Object executeScript(@Language("JavaScript") String script, @Nullable Object... args) + { + return _wrappedExecutor.executeScript(script, args); + } + + @Override + public @Nullable Object executeScript(ScriptKey key, @Nullable Object... args) + { + return _wrappedExecutor.executeScript(key, args); + } + + /** + * Wrapper for executing JavaScript through WebDriver and verifying return type. + * @param See {@link JavascriptExecutor#executeScript(java.lang.String, java.lang.Object...)} for valid return types + */ + public @Nullable T executeScript(@Language("JavaScript") String script, Class expectedResultType, @Nullable Object... arguments) + { + return verifyType(expectedResultType, executeScript(script, arguments)); + } + + /** + * Wrapper for synchronous execution of asynchronous JavaScript. This wrapper extracts the 'callback' from the argument list + * See {@link JavascriptExecutor#executeAsyncScript(java.lang.String, java.lang.Object...)} for details + */ + @Override + public @Nullable Object executeAsyncScript(@Language("JavaScript") String script, @Nullable Object... arguments) + { + script = "var callback = arguments[arguments.length - 1];\n" + // See WebDriver documentation for details on injected callback + "try {" + + script + + "} catch (error) { callback(error); }"; // ensure that the callback is invoked when an exception would otherwise prevent it + return _wrappedExecutor.executeAsyncScript(script, arguments); + } + + public @Nullable T executeAsyncScript(@Language("JavaScript") String script, Class expectedResultType, @Nullable Object... arguments) + { + return verifyType(expectedResultType, executeAsyncScript(script, arguments)); + } + + private @Nullable T verifyType(Class expectedResultType, @Nullable Object o) + { + if (o != null && !expectedResultType.isAssignableFrom(o.getClass())) + throw new IllegalStateException("Script return wrong type. Expected '" + expectedResultType.getName() + "'. Got: " + o.getClass().getName() + ". Result: " + o); + + return (T) o; + } + + @Override + public Set getPinnedScripts() + { + return _wrappedExecutor.getPinnedScripts(); + } + + @Override + public void unpin(ScriptKey key) + { + _wrappedExecutor.unpin(key); + } + + @Override + public ScriptKey pin(@Language("JavaScript") String script) + { + return _wrappedExecutor.pin(script); + } +} diff --git a/src/org/labkey/test/util/selenium/WebDriverUtils.java b/src/org/labkey/test/util/selenium/WebDriverUtils.java index eff8ec79bc..3e6b78dd41 100644 --- a/src/org/labkey/test/util/selenium/WebDriverUtils.java +++ b/src/org/labkey/test/util/selenium/WebDriverUtils.java @@ -25,6 +25,8 @@ import org.openqa.selenium.WrapsDriver; import org.openqa.selenium.WrapsElement; +import java.util.Objects; + public abstract class WebDriverUtils { /** @@ -74,6 +76,17 @@ public static WebDriver extractWrappedDriver(Object peeling) return null; } + /** + * Extract a WebDriver instance from an arbitrarily wrapped object and the JavascriptExecutor tied to it. + * + * @param object Object that wraps a WebDriver. Typically, a Component, SearchContext, or WebElement + * @return JavascriptExecutor instance + */ + public static JavascriptExecutorWrapper getJavascriptExecutor(Object object) + { + return Objects.requireNonNull(new JavascriptExecutorWrapper(extractWrappedDriver(object)), () -> "No WebDriver found in " + object.getClass()); + } + /** * Attempts to get alert text from an {@link UnhandledAlertException}. If exception does not supply the alert text, * attempt to get it from the alert directly (requires {@link org.openqa.selenium.UnexpectedAlertBehaviour#IGNORE}). diff --git a/src/org/labkey/test/util/selenium/WebElementUtils.java b/src/org/labkey/test/util/selenium/WebElementUtils.java index d6866f73de..5f7c823561 100644 --- a/src/org/labkey/test/util/selenium/WebElementUtils.java +++ b/src/org/labkey/test/util/selenium/WebElementUtils.java @@ -1,13 +1,17 @@ package org.labkey.test.util.selenium; import org.intellij.lang.annotations.Language; +import org.labkey.test.selenium.LazyWebElement; import org.openqa.selenium.JavascriptExecutor; import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.StaleElementReferenceException; import org.openqa.selenium.WebDriverException; import org.openqa.selenium.WebElement; import java.util.Collections; import java.util.List; +import java.util.Objects; +import java.util.Optional; import static org.labkey.test.Locator.NBSP; @@ -93,6 +97,29 @@ public static String getTextNodeWithin(WebElement element) */ public static String getTextContent(WebElement element) { - return element.getDomProperty("textContent").replace(NBSP, " "); + return Optional.ofNullable(element.getDomProperty("textContent")).map(s -> s.replace(NBSP, " ")).orElse(null); + } + + /** + * Determines whether the specified element is visible. {@link WebElement#isDisplayed()} might return false if the + * element is out the viewport and scrolling is disabled due to a modal dialog.
+ * TODO: Consider moving to {@link LazyWebElement#isDisplayed()} + * + * @param element element to inspect + * @return true if the element is visible, false otherwise + */ + public static boolean checkVisibility(WebElement element) + { + try + { + return element.isDisplayed() || + Objects.requireNonNullElse(WebDriverUtils.getJavascriptExecutor(element) + .executeScript("return arguments[0].checkVisibility();", Boolean.class, element), + false); + } + catch (NoSuchElementException | StaleElementReferenceException e) + { + return false; + } } } From f4cd961b176942b51fbbd61d3badd79a3f01563c Mon Sep 17 00:00:00 2001 From: Daria Bodiakova <70635654+DariaBod@users.noreply.github.com> Date: Mon, 2 Mar 2026 16:11:37 -0800 Subject: [PATCH 3/3] 26.3 Fix for export tests (#2898) #### Rationale Fix for export tests, changed the " " symbol processing. #### Related Pull Requests - [https://github.com/LabKey/limsModules/pull/2017](https://github.com/LabKey/limsModules/pull/2017) --- src/org/labkey/test/components/domain/DomainFieldRow.java | 2 ++ src/org/labkey/test/util/data/TestDataUtils.java | 7 +++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/org/labkey/test/components/domain/DomainFieldRow.java b/src/org/labkey/test/components/domain/DomainFieldRow.java index dc1ea4f328..5261edb788 100644 --- a/src/org/labkey/test/components/domain/DomainFieldRow.java +++ b/src/org/labkey/test/components/domain/DomainFieldRow.java @@ -769,6 +769,8 @@ public DomainFieldRow clickRemoveOntologyConcept() public void setAllowMultipleSelections(Boolean allowMultipleSelections) { + WebDriverWrapper.waitFor(() -> elementCache().allowMultipleSelectionsCheckbox.isDisplayed(), + "Allow Multiple Selections checkbox did not become visible", 2000); elementCache().allowMultipleSelectionsCheckbox.set(allowMultipleSelections); } diff --git a/src/org/labkey/test/util/data/TestDataUtils.java b/src/org/labkey/test/util/data/TestDataUtils.java index b7ed252c1a..2795dd4916 100644 --- a/src/org/labkey/test/util/data/TestDataUtils.java +++ b/src/org/labkey/test/util/data/TestDataUtils.java @@ -581,14 +581,13 @@ public static List> readRowsFromFile(File file, CSVFormat format) t public static List parseMultiValueText(String multiValueString) throws IOException { CSVFormat format = CSVFormat.RFC4180.builder() - .setIgnoreSurroundingSpaces(true).get(); + .setIgnoreSurroundingSpaces(true).setTrim(true).get(); try (CSVParser parser = format.parse(new StringReader(multiValueString))) { List records = parser.getRecords(); - List> list = records.stream().map(CSVRecord::toList).toList(); - if (list.size() != 1) + if (records.size() != 1) throw new IllegalArgumentException("Invalid multi-value text string: " + multiValueString); - return list.getFirst(); + return records.getFirst().toList(); } }