Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ lookfirstSardineVersion=5.13

jettyVersion=12.1.5

seleniumVersion=4.40.0
seleniumVersion=4.41.0

mockserverNettyVersion=5.15.0

Expand Down
19 changes: 4 additions & 15 deletions src/org/labkey/test/WebDriverWrapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -539,11 +540,7 @@ public Object executeScript(@Language("JavaScript") String script, Object... arg
*/
public <T> T executeScript(@Language("JavaScript") String script, Class<T> 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);
}

/**
Expand All @@ -552,20 +549,12 @@ public <T> T executeScript(@Language("JavaScript") String script, Class<T> 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> T executeAsyncScript(@Language("JavaScript") String script, Class<T> 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)
Expand Down
2 changes: 2 additions & 0 deletions src/org/labkey/test/components/domain/DomainFieldRow.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) ||
Expand Down
13 changes: 12 additions & 1 deletion src/org/labkey/test/tests/GpatAssayTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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)
{
Expand All @@ -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;
}

Expand Down
10 changes: 9 additions & 1 deletion src/org/labkey/test/util/TestDataGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -800,7 +800,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<String> getFieldsForFile()
Expand Down
7 changes: 3 additions & 4 deletions src/org/labkey/test/util/data/TestDataUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -581,14 +581,13 @@ public static List<List<String>> readRowsFromFile(File file, CSVFormat format) t
public static List<String> 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<CSVRecord> records = parser.getRecords();
List<List<String>> 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();
}
}

Expand Down
85 changes: 85 additions & 0 deletions src/org/labkey/test/util/selenium/JavascriptExecutorWrapper.java
Original file line number Diff line number Diff line change
@@ -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 <T> See {@link JavascriptExecutor#executeScript(java.lang.String, java.lang.Object...)} for valid return types
*/
public <T> @Nullable T executeScript(@Language("JavaScript") String script, Class<T> 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 <T> @Nullable T executeAsyncScript(@Language("JavaScript") String script, Class<T> expectedResultType, @Nullable Object... arguments)
{
return verifyType(expectedResultType, executeAsyncScript(script, arguments));
}

private <T> @Nullable T verifyType(Class<T> 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<ScriptKey> 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);
}
}
13 changes: 13 additions & 0 deletions src/org/labkey/test/util/selenium/WebDriverUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import org.openqa.selenium.WrapsDriver;
import org.openqa.selenium.WrapsElement;

import java.util.Objects;

public abstract class WebDriverUtils
{
/**
Expand Down Expand Up @@ -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}).
Expand Down
29 changes: 28 additions & 1 deletion src/org/labkey/test/util/selenium/WebElementUtils.java
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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.<br>
* 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;
}
}
}