diff --git a/src/org/labkey/test/AppLocators.java b/src/org/labkey/test/AppLocators.java new file mode 100644 index 0000000000..d61f6980e5 --- /dev/null +++ b/src/org/labkey/test/AppLocators.java @@ -0,0 +1,8 @@ +package org.labkey.test; + +public abstract class AppLocators +{ + private AppLocators() {} + + public static final Locator.XPathLocator detailHeaderName = Locator.tagWithClass("h2", "detail__header--name"); +} diff --git a/src/org/labkey/test/Locators.java b/src/org/labkey/test/Locators.java index bd688dfced..ec7ec8fdb4 100644 --- a/src/org/labkey/test/Locators.java +++ b/src/org/labkey/test/Locators.java @@ -17,6 +17,8 @@ public abstract class Locators { + private Locators() { } + public static final Locator documentRoot = Locator.css(":root"); public static final Locator.IdLocator folderMenu = Locator.id("folderBar"); public static final Locator.XPathLocator labkeyError = Locator.byClass("labkey-error"); diff --git a/src/org/labkey/test/WebDriverWrapper.java b/src/org/labkey/test/WebDriverWrapper.java index 5a53c34bd7..1aa4c3b1f9 100644 --- a/src/org/labkey/test/WebDriverWrapper.java +++ b/src/org/labkey/test/WebDriverWrapper.java @@ -1129,6 +1129,12 @@ public String call() } } + @Contract(pure = true) + public WebDriverWait quickWait() + { + return new WebDriverWait(getDriver(), Duration.ofSeconds(1)); + } + @Contract(pure = true) public WebDriverWait shortWait() { diff --git a/src/org/labkey/test/components/ui/Pager.java b/src/org/labkey/test/components/ui/Pager.java index f364bed03a..6d87817239 100644 --- a/src/org/labkey/test/components/ui/Pager.java +++ b/src/org/labkey/test/components/ui/Pager.java @@ -93,8 +93,7 @@ private int pageSize(String pageSize) // only works on GridPanel } else { - // Tooltip sometimes blocks button. Click active option to dismiss menu. - activeLi.click(); + elementCache().jumpToDropdown.collapse(); } return initialSize; diff --git a/src/org/labkey/test/components/ui/grids/EditableGrid.java b/src/org/labkey/test/components/ui/grids/EditableGrid.java index c0a0d5442a..2e7dba6535 100644 --- a/src/org/labkey/test/components/ui/grids/EditableGrid.java +++ b/src/org/labkey/test/components/ui/grids/EditableGrid.java @@ -769,6 +769,33 @@ public List getFilteredDropdownListForCell(int row, CharSequence columnI return lookupSelect.getOptions(); } + /** + * Values will be quoted appropriately for pasting into editable grid lookups. + */ + public static String getPastableColumn(List values) + { + List valueList = new ArrayList<>(); + for (Object value : values) + { + String strVal = CSVFormat.DEFAULT.format(value); // Just quote commas + valueList.add(strVal); + } + return String.join("\n", valueList); + } + + /** + * Pastes text to a single column of the grid. + * @param columnIdentifier fieldKey, name, or label of column + * @param pasteValues list of values to paste + * @return A Reference to this editableGrid object. + */ + public EditableGrid pasteColumn(CharSequence columnIdentifier, List pasteValues) + { + if (pasteValues.isEmpty()) + throw new IllegalArgumentException("No paste values provided"); + return pasteFromCell(0, columnIdentifier, getPastableColumn(pasteValues), false); + } + /** * Pastes delimited text to the grid, via a single target. The component is clever enough to target * text into cells based on text delimiters; thus we can paste a square of data into the grid. diff --git a/src/org/labkey/test/components/ui/grids/FieldReferenceManager.java b/src/org/labkey/test/components/ui/grids/FieldReferenceManager.java index cb2c4de07c..d70a851fd8 100644 --- a/src/org/labkey/test/components/ui/grids/FieldReferenceManager.java +++ b/src/org/labkey/test/components/ui/grids/FieldReferenceManager.java @@ -1,5 +1,6 @@ package org.labkey.test.components.ui.grids; +import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.labkey.test.params.FieldKey; @@ -132,7 +133,11 @@ else if (_fieldLabels.size() < _fieldReferences.size()) } } - return null; + String capitalized = StringUtils.capitalize(label); + if (capitalized.equals(label)) + return null; + else + return findColumnHeaderByLabel(capitalized); // Handle domain names that aren't capitalized } public static class FieldReference diff --git a/src/org/labkey/test/components/ui/notifications/ServerNotificationItem.java b/src/org/labkey/test/components/ui/notifications/ServerNotificationItem.java index 16d0dec587..298446e9a2 100644 --- a/src/org/labkey/test/components/ui/notifications/ServerNotificationItem.java +++ b/src/org/labkey/test/components/ui/notifications/ServerNotificationItem.java @@ -7,6 +7,7 @@ import org.labkey.test.util.TestLogger; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.ui.ExpectedConditions; /** *

@@ -43,6 +44,12 @@ public WebElement getComponentElement() return componentElement; } + @Override + protected void waitForReady() + { + getWrapper().quickWait().until(ExpectedConditions.visibilityOf(getComponentElement())); + } + /** * Get the status as indicated by the icon. * @@ -73,7 +80,7 @@ else if (status.contains("is-complete")) */ public String getMessage() { - if(elementCache().message.isDisplayed()) + if (elementCache().message.isDisplayed()) { return elementCache().message.getText(); } diff --git a/src/org/labkey/test/selenium/ReclickingWebElement.java b/src/org/labkey/test/selenium/ReclickingWebElement.java index 65dc66e8e6..cf0e34f256 100644 --- a/src/org/labkey/test/selenium/ReclickingWebElement.java +++ b/src/org/labkey/test/selenium/ReclickingWebElement.java @@ -15,6 +15,7 @@ */ package org.labkey.test.selenium; +import org.apache.commons.lang3.Strings; import org.apache.commons.lang3.mutable.Mutable; import org.apache.commons.lang3.mutable.MutableObject; import org.jetbrains.annotations.NotNull; @@ -49,7 +50,7 @@ public class ReclickingWebElement extends WebElementDecorator { // Extract the element info from ElementClickInterceptedException message. - private static final Pattern interceptingElPattern = Pattern.compile("Element .* is not clickable .*<(?[a-zA-Z]+) (?.+)> obscures it"); + private static final Pattern interceptingElPattern = Pattern.compile("Element .* is not clickable .*<(?[a-zA-Z0-9]+) (?.+)> obscures it"); private static final Pattern elAttributePattern = Pattern.compile("(?[a-zA-Z-]+)=\"(?[^\"]+)\""); public ReclickingWebElement(@NotNull WebElement decoratedElement) @@ -216,6 +217,13 @@ private void revealElement(WebElement el, String shortMessage) { List interceptingElements = interceptingElLoc.findElements(getDriver()); TestLogger.debug("Found %s element(s) matching extracted locator: %s".formatted(interceptingElements.size(), shortMessage)); + + if (Strings.CI.containsAny(interceptingElLoc.toString(), "popover", "ws-pre-wrap", "tip")) + { + // Move mouse to corner to dismiss tooltips + new Actions(getDriver()).moveToLocation(0,0).perform(); + } + if (interceptingElements.size() == 1) { //noinspection ResultOfMethodCallIgnored diff --git a/src/org/labkey/test/tests/TextChoiceSampleTypeTest.java b/src/org/labkey/test/tests/TextChoiceSampleTypeTest.java index e14f10e49c..ff9ddaf7f5 100644 --- a/src/org/labkey/test/tests/TextChoiceSampleTypeTest.java +++ b/src/org/labkey/test/tests/TextChoiceSampleTypeTest.java @@ -450,12 +450,29 @@ public void testUpdatingAndDeletingValuesInSampleType() throws IOException, Comm updatePage.clickSave(); log("Validate that the expected rows after the update are in the log."); - String fieldOldValues = "Name=TextChoice_Field_1&Type=String&Scale=4000&PHI=Not%20PHI&DefaultScale=Linear&Required=false&Hidden=false&MvEnabled=false&Measure=false&Dimension=false" + - "&ShownInInsert=true&ShownInDetails=true&ShownInUpdate=true&ShownInLookupView=false&RecommendedVariable=false&ExcludedFromShifting=false&Scannable=false" + - "&DefaultValueType=Editable%20default&Validator=Text%20Choice%20Validator%2C%20%C3%85%5C%7C%C3%85%7CBB%7CCC%7CDD%7CE%20E%20E%7C%C2%83%C2%83%7CGG%7CH%2C%20Text%20Choice%20Validator"; - String fieldUpdateValues = "Name=TextChoice_Field_1&Type=String&Scale=4000&PHI=Not%20PHI&DefaultScale=Linear&Required=false&Hidden=false&MvEnabled=false&Measure=false&Dimension=false" + - "&ShownInInsert=true&ShownInDetails=true&ShownInUpdate=true&ShownInLookupView=false&RecommendedVariable=false&ExcludedFromShifting=false&Scannable=false" + - "&DefaultValueType=Editable%20default&Validator=Text%20Choice%20Validator%2C%20BB%7CCC%20and%20here%20is%20an%20update%7CE%20E%20E%7CGG%7CH%7C%C2%83%C2%83%20updated%7C%C3%85%5C%7C%C3%85%2C%20Text%20Choice%20Validator"; + String fieldSharedValues = AuditLogHelper.encodeValues( + "Name", textChoiceFieldName, + "Type", "String", + "Scale", "4000", + "PHI", "Not PHI", + "DefaultScale", "Linear", + "Required", "false", + "Hidden", "false", + "MvEnabled", "false", + "Measure", "false", + "Dimension", "false", + "ShownInInsert", "true", + "ShownInDetails", "true", + "ShownInUpdate", "true", + "ShownInLookupView", "false", + "RecommendedVariable", "false", + "ExcludedFromShifting", "false", + "Scannable", "false", + "DefaultValueType", "Editable default"); + String fieldOldValues = fieldSharedValues + "&" + AuditLogHelper.encodeValues( + "Validator", "Text Choice Validator, \u00C5\\|\u00C5|BB|CC|DD|E E E|\u0083\u0083|GG|H, Text Choice Validator"); + String fieldUpdateValues = fieldSharedValues + "&" + AuditLogHelper.encodeValues( + "Validator", "Text Choice Validator, BB|CC and here is an update|E E E|GG|H|\u0083\u0083 updated|\u00C5\\|\u00C5, Text Choice Validator"); AuditLogHelper.DetailedAuditEventRow fieldEvent = new AuditLogHelper.DetailedAuditEventRow(null, textChoiceFieldName, "Modified", "The following property was updated: Validator", "", fieldOldValues, fieldUpdateValues, null); AuditLogHelper.DetailedAuditEventRow expectedDomainEvent = new AuditLogHelper.DetailedAuditEventRow(null, sampleTypeName, null, @@ -548,9 +565,8 @@ public void testUpdatingAndDeletingValuesInSampleType() throws IOException, Comm updatePage.clickSave(); log("Validate that the expected rows after the update are in the log."); - String fieldUpdateValues2 = "Name=TextChoice_Field_1&Type=String&Scale=4000&PHI=Not%20PHI&DefaultScale=Linear&Required=false&Hidden=false&MvEnabled=false&Measure=false" + - "&Dimension=false&ShownInInsert=true&ShownInDetails=true&ShownInUpdate=true&ShownInLookupView=false&RecommendedVariable=false&ExcludedFromShifting=false" + - "&Scannable=false&DefaultValueType=Editable%20default&Validator=Text%20Choice%20Validator%2C%20BB%7CCC%20and%20here%20is%20an%20update%7CE%20E%20E%7CGG%7CH%20no%20change%7C%C2%83%C2%83%20updated%7C%C3%85%5C%7C%C3%85%2C%20Text%20Choice%20Validator"; + String fieldUpdateValues2 = fieldSharedValues + "&" + AuditLogHelper.encodeValues( + "Validator", "Text Choice Validator, BB|CC and here is an update|E E E|GG|H no change|\u0083\u0083 updated|\u00C5\\|\u00C5, Text Choice Validator"); fieldEvent = new AuditLogHelper.DetailedAuditEventRow(null, textChoiceFieldName, "Modified", "The following property was updated: Validator", "", fieldUpdateValues, fieldUpdateValues2, null); pass = _auditLogHelper.validateLastDomainAuditEvents(sampleTypeName, getProjectName(), expectedDomainEvent, Map.of(textChoiceFieldName, fieldEvent)); diff --git a/src/org/labkey/test/util/AuditLogHelper.java b/src/org/labkey/test/util/AuditLogHelper.java index a91b5c377f..8a5b3d8fc5 100644 --- a/src/org/labkey/test/util/AuditLogHelper.java +++ b/src/org/labkey/test/util/AuditLogHelper.java @@ -453,6 +453,46 @@ public String getLogString() } } + /** + * URL-encode fields and values for {@link DetailedAuditEventRow#newValues} or {@link DetailedAuditEventRow#oldValues} + * @param pairs alternating field names and their associated values + * @return URL-encoded String for use in DetailedAuditEventRow + */ + public static String encodeValues(String... pairs) + { + if (pairs.length % 2 != 0) + { + throw new IllegalArgumentException("pairs length must be even"); + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < pairs.length; i = i + 2) + { + if (!sb.isEmpty()) + sb.append("&"); + sb.append(encodeValue(pairs[i])).append("=").append(encodeValue(pairs[i + 1])); + } + return sb.toString(); + } + + /** + * Perform selective URL encoding for use {@link DetailedAuditEventRow#newValues} or + * {@link DetailedAuditEventRow#oldValues} + * @param value raw key or value + * @return Partially URL-encoded value + */ + private static String encodeValue(String value) + { + return EscapeUtil.encode(value) + // Parentheses aren't encoded + .replace("%28", "(") + .replace("%29", ")"); + } + + public static String formatDataChange(String name, String oldValue, String newValue) + { + return name + ": " + oldValue + " > " + newValue; + } + public @NotNull Map getDomainPropertyEvents(String domainName, Integer domainEventId) { if (domainEventId == null) diff --git a/src/org/labkey/test/util/DomainUtils.java b/src/org/labkey/test/util/DomainUtils.java index 9cc20bce6b..9266495c22 100644 --- a/src/org/labkey/test/util/DomainUtils.java +++ b/src/org/labkey/test/util/DomainUtils.java @@ -95,11 +95,18 @@ public static void ensureDeleted(String containerPath, String schema, String tab } public enum DomainKind { - DataClass, - SampleSet, + Assay, + DataClass, // aka "Sources" + SampleSet, // aka "Sample Type" IntList, VarList, StudyDatasetDate, - StudyDatasetVisit + StudyDatasetVisit, + ; + + public String randomName(String namePart) + { + return TestDataGenerator.randomDomainName(namePart, this); + } } } diff --git a/src/org/labkey/test/util/ExcelHelper.java b/src/org/labkey/test/util/ExcelHelper.java index 16b0c5c1ab..cb09a721ee 100644 --- a/src/org/labkey/test/util/ExcelHelper.java +++ b/src/org/labkey/test/util/ExcelHelper.java @@ -15,6 +15,7 @@ */ package org.labkey.test.util; +import org.apache.commons.lang3.StringUtils; import org.apache.poi.ss.format.CellGeneralFormatter; import org.apache.poi.ss.formula.FormulaParseException; import org.apache.poi.ss.usermodel.Cell; @@ -28,6 +29,7 @@ import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.apache.poi.ss.util.WorkbookUtil; import org.jetbrains.annotations.Nullable; import java.io.File; @@ -35,6 +37,7 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -266,7 +269,7 @@ public static Map>> loadData(File file) { try (Workbook workbook = ExcelHelper.create(file)) { - Map>> allData = new LinkedHashMap<>(); + Map>> allData = new WorsheetMap(); for (int s = 0; s < workbook.getNumberOfSheets(); s++) { @@ -291,11 +294,35 @@ public static Map>> loadData(File file) allData.put(sheet.getSheetName(), rowMaps); } - return allData; + return Collections.unmodifiableMap(allData); } catch (IOException e) { throw new RuntimeException(e); } } + + /** + * Truncate and make safe a proposed Excel sheet name + * @see org.labkey.api.data.ExcelWriter#cleanSheetName(String) + */ + public static String sheetName(String sheetName) + { + return WorkbookUtil.createSafeSheetName(StringUtils.truncate(sheetName, 31), '_'); + } +} + +class WorsheetMap extends LinkedHashMap>> +{ + @Override + public List> get(Object key) + { + return super.get(ExcelHelper.sheetName((String) key)); + } + + @Override + public List> getOrDefault(Object key, List> defaultValue) + { + return super.getOrDefault(ExcelHelper.sheetName((String) key), defaultValue); + } } diff --git a/src/org/labkey/test/util/URLBuilder.java b/src/org/labkey/test/util/URLBuilder.java index 03a976c626..4f49b5485b 100644 --- a/src/org/labkey/test/util/URLBuilder.java +++ b/src/org/labkey/test/util/URLBuilder.java @@ -106,11 +106,19 @@ public URLBuilder setQuery(Map query) public URLBuilder setAppResourcePath(Object... pathParts) { List encodedParts = Arrays.stream(pathParts).map(Objects::requireNonNull).map(String::valueOf) - .map(s -> EscapeUtil.encode(s).replace("+", " ")).collect(Collectors.toList()); + .map(this::encodeAppResourcePathPart).collect(Collectors.toList()); setFragment("/" + String.join("/", encodedParts)); return this; } + private String encodeAppResourcePathPart(String pathPart) + { + return EscapeUtil.encode(pathPart) + // We generally don't encode parentheses in app resource paths + .replace("%28", "(") + .replace("%29", ")"); + } + /** * Append a fragment to the URL.
* e.g. setResourcePath("marker") will append "#marker" to the built URL