diff --git a/src/org/labkey/test/BaseWebDriverTest.java b/src/org/labkey/test/BaseWebDriverTest.java index 8f5e515fb0..a014c1ab9b 100644 --- a/src/org/labkey/test/BaseWebDriverTest.java +++ b/src/org/labkey/test/BaseWebDriverTest.java @@ -67,6 +67,7 @@ import org.labkey.test.params.ContainerInfo; import org.labkey.test.params.FieldDefinition; import org.labkey.test.params.FieldKey; +import org.labkey.test.params.SchemaKey; import org.labkey.test.teamcity.TeamCityUtils; import org.labkey.test.util.APIAssayHelper; import org.labkey.test.util.APIContainerHelper; @@ -140,6 +141,7 @@ import java.util.Date; import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Objects; @@ -2187,32 +2189,38 @@ public void searchFor(final String projectName, String searchFor, final Integer assertTextPresent(searchFor); } } - public void selectSchema(String schemaName) + + /** + * @param schemaKey encoded schema key + */ + public void selectSchema(String schemaKey) { - String[] schemaParts = schemaName.split("\\."); - selectSchema(schemaParts); + selectSchema(SchemaKey.parse(schemaKey)); } - // Helper methods for interacting with the query schema browser + public void selectSchema(String[] schemaParts) { - StringBuilder schemaWithParents = new StringBuilder(); - String separator = ""; - for (String schemaPart : schemaParts) + selectSchema(SchemaKey.fromParts(schemaParts)); + } + + public void selectSchema(SchemaKey schemaKey) + { + Iterator iterator = schemaKey.getIterator(); + while (iterator.hasNext()) { - schemaWithParents.append(separator).append(schemaPart); - separator = "."; + schemaKey = iterator.next(); - Locator.XPathLocator loc = Locator.tag("tr").withClass("x4-grid-row").append("/td/div/span").withText(schemaPart).precedingSibling("img").withClass("x4-tree-icon"); + Locator.XPathLocator loc = Locator.tag("tr").withClass("x4-grid-row").append("/td/div/span").withText(schemaKey.getName()).precedingSibling("img").withClass("x4-tree-icon"); //first load of schemas might a few seconds shortWait().until(ExpectedConditions.elementToBeClickable(loc)); - Locator.XPathLocator selectedSchema = Locator.xpath("//tr").withClass("x4-grid-row-selected").append("/td/div/span").withText(schemaPart); + Locator.XPathLocator selectedSchema = Locator.xpath("//tr").withClass("x4-grid-row-selected").append("/td/div/span").withText(schemaKey.getName()); - if (getDriver().getCurrentUrl().endsWith("schemaName=" + schemaPart)) + if (getDriver().getCurrentUrl().endsWith("schemaName=" + EscapeUtil.encode(schemaKey.toString()))) waitForElement(selectedSchema); if (isElementPresent(selectedSchema)) continue; // already selected - log("Selecting schema " + schemaWithParents + " in the schema browser..."); + log("Selecting schema " + schemaKey.getFullName() + " in the schema browser..."); waitForElementToDisappear(Locator.xpath("//tbody[starts-with(@id, 'treeview')]/tr[not(starts-with(@id, 'treeview'))]")); // select/expand tree node try @@ -2243,19 +2251,23 @@ public void selectSchema(String[] schemaParts) public void selectQuery(String schemaName, String queryName) { - selectQuery(schemaName.split("\\."), queryName); + selectQuery(SchemaKey.parse(schemaName), queryName); + } + + public void selectQuery(String[] schemaParts, String queryName) + { + selectQuery(SchemaKey.fromParts(schemaParts), queryName); } - public void selectQuery(String[] schemaPart, String queryName) + public void selectQuery(SchemaKey schemaKey, String queryName) { - String schemaName = StringUtils.join(schemaPart, "."); - log("Selecting query " + schemaName + "." + queryName + " in the schema browser..."); - selectSchema(schemaPart); + log("Selecting query " + schemaKey.getFullName() + "." + queryName + " in the schema browser..."); + selectSchema(schemaKey); mouseOver(Locator.byClass(".x4-tab-button")); // Move away from schema tree to dismiss tooltip - waitAndClick(Ext4Helper.Locators.tab(schemaName)); // Click schema tab to make sure query list is visible + waitAndClick(Ext4Helper.Locators.tab(schemaKey.getFullName())); // Click schema tab to make sure query list is visible WebElement queryLink = Locator.tagWithClass("table", "lk-qd-coltable").append(Locator.tagWithClass("span", "labkey-link")).withText(queryName).notHidden().waitForElement(getDriver(), WAIT_FOR_JAVASCRIPT); queryLink.click(); - waitForElement(Locator.tagWithClass("div", "lk-qd-name").startsWith(schemaName + "." + queryName), 30000); + waitForElement(Locator.tagWithClass("div", "lk-qd-name").startsWith(schemaKey.getFullName() + "." + queryName), 30000); } public DataRegionTable viewQueryData(String schemaName, String queryName) diff --git a/src/org/labkey/test/WebDriverWrapper.java b/src/org/labkey/test/WebDriverWrapper.java index fdb36fa750..1121adf65b 100644 --- a/src/org/labkey/test/WebDriverWrapper.java +++ b/src/org/labkey/test/WebDriverWrapper.java @@ -2462,11 +2462,17 @@ public static File[] getNewFiles(int minFileCount, File downloadDir, @Nullable F }, "File(s) did not appear in download dir: " + downloadDir.toString(), WAIT_FOR_PAGE); + List tempFiles = new ArrayList<>(); waitFor(() -> { final File[] files = downloadDir.listFiles(tempFilesFilter); + tempFiles.clear(); + if (files != null) + { + tempFiles.addAll(Arrays.asList(files)); + } return files != null && files.length == 0; }, - "Temp files remain in download dir: " + downloadDir, WAIT_FOR_PAGE); + () -> "Temp files remain in download dir: " + downloadDir + ": " + tempFiles.stream().map(File::getName).collect(Collectors.joining(", ")), WAIT_FOR_PAGE); MutableInt downloadSize = new MutableInt(-1); MutableInt stabilityDuration = new MutableInt(0); diff --git a/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java b/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java index 09aee4dcc9..eb02a00ee1 100644 --- a/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java +++ b/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java @@ -83,6 +83,7 @@ public EntityBulkUpdateDialog setEditableState(CharSequence fieldIdentifier, boo toggle.set(enable); if (enable) _changeCounter++; else _changeCounter--; + getWrapper().mouseOut(); // Toggle is dangerously close to field info tooltip } return this; } @@ -335,7 +336,7 @@ public List getFieldNames() List labels = Locator.tagWithClass("label", "control-label").withAttribute("for") .waitForElements(elementCache(), 2_000); - return labels.stream().map(a -> FieldKey.fromFieldKey(a.getDomAttribute("for")).getName()).toList(); + return labels.stream().map(a -> FieldKey.fromFieldKey(a.getDomAttribute("for")).getFullName()).toList(); } public EntityBulkUpdateDialog waitForFieldsToBe(List expectedFieldNames, int waitMilliseconds) diff --git a/src/org/labkey/test/components/ui/grids/FieldReferenceManager.java b/src/org/labkey/test/components/ui/grids/FieldReferenceManager.java index d70a851fd8..e23de2f26f 100644 --- a/src/org/labkey/test/components/ui/grids/FieldReferenceManager.java +++ b/src/org/labkey/test/components/ui/grids/FieldReferenceManager.java @@ -170,7 +170,7 @@ public String getLabel() public String getName() { - return getFieldKey().getName(); + return getFieldKey().getFullName(); } public int getDomIndex() @@ -192,14 +192,7 @@ protected FieldKey fieldKeySupplier() path = getElement().getDomAttribute("id"); } - if (path != null) - { - return FieldKey.fromFieldKey(path); - } - else - { - return FieldKey.EMPTY; - } + return FieldKey.fromFieldKey(path); } } } diff --git a/src/org/labkey/test/components/ui/grids/FieldSelectionDialog.java b/src/org/labkey/test/components/ui/grids/FieldSelectionDialog.java index c7c01bbcae..994ee13f92 100644 --- a/src/org/labkey/test/components/ui/grids/FieldSelectionDialog.java +++ b/src/org/labkey/test/components/ui/grids/FieldSelectionDialog.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.stream.Collectors; +import static org.labkey.test.util.TextUtils.normalizeSpace; import static org.labkey.test.util.selenium.WebElementUtils.getTextContent; /** @@ -180,13 +181,13 @@ public WebElement getAvailableFieldElement(FieldKey fieldKey) if(iterator.hasNext()) { // If the field is already expanded don't try to expand it. - if(!isFieldKeyExpanded(elementCache().findAvailableField(fieldKey.toString()))) - expandOrCollapseByFieldKey(fieldKey.toString(), true); + if(!isFieldKeyExpanded(elementCache().findAvailableField(fieldKey))) + expandOrCollapseByFieldKey(fieldKey, true); } } - return elementCache().findAvailableField(fieldKey.toString()); + return elementCache().findAvailableField(fieldKey); } /** @@ -195,7 +196,7 @@ public WebElement getAvailableFieldElement(FieldKey fieldKey) * @param fieldKey The data-fieldkey value of the field to expand. * @param expand True to expand false to collapse. */ - private void expandOrCollapseByFieldKey(String fieldKey, boolean expand) + private void expandOrCollapseByFieldKey(FieldKey fieldKey, boolean expand) { WebElement listItem = elementCache().findAvailableField(fieldKey); @@ -433,7 +434,7 @@ public FieldSelectionDialog setFieldLabel(String fieldName, String newFieldLabel */ public FieldSelectionDialog setFieldLabel(FieldKey fieldKey, String newFieldLabel) { - WebElement listItem = elementCache().findSelectedField(fieldKey.toString()); + WebElement listItem = elementCache().findSelectedField(fieldKey); WebElement updateIcon = Locator.tagWithClass("span", "edit-inline-field__toggle").findWhenNeeded(listItem); updateIcon.click(); @@ -453,9 +454,9 @@ public FieldSelectionDialog setFieldLabel(FieldKey fieldKey, String newFieldLabe getWrapper().mouseOver(elementCache().title); // Dismiss tooltip - WebDriverWrapper.waitFor(()->!elementCache().fieldLabelEdit.isDisplayed() && - elementCache().getListItemElement(elementCache().selectedFieldsPanel, newFieldLabel).isDisplayed(), + WebDriverWrapper.waitFor(()->!elementCache().fieldLabelEdit.isDisplayed(), String.format("New field label '%s' is not in the list.", newFieldLabel), 500); + Assert.assertEquals("Label after update", normalizeSpace(newFieldLabel), elementCache().getFieldLabel(fieldKey)); return this; } @@ -503,10 +504,10 @@ private List getSelectedListItems(String fieldLabel) * @param beforeTarget Will the field being moved go before (above) or after (below) the target field. * @return This dialog. */ - public FieldSelectionDialog repositionField(String fieldToMove, String targetField, boolean beforeTarget) + public FieldSelectionDialog repositionField(FieldKey fieldToMove, FieldKey targetField, boolean beforeTarget) { - WebElement elementToMove = elementCache().getListItemElement(elementCache().selectedFieldsPanel, fieldToMove); - WebElement elementTarget = elementCache().getListItemElement(elementCache().selectedFieldsPanel, targetField); + WebElement elementToMove = elementCache().findSelectedField(fieldToMove); + WebElement elementTarget = elementCache().findSelectedField(targetField); int yBefore = elementToMove.getRect().getY(); @@ -633,28 +634,27 @@ protected List getListItemElements(WebElement panel, String fieldLab .findElements(panel); } - // Will get the first list item that matches the fieldLabel. - protected WebElement getListItemElement(WebElement panel, String fieldLabel) + protected String getFieldLabel(FieldKey fieldKey) { - return Locator.tagWithClass("div", "list-group-item") - .withDescendant(Locator.tagWithClass("div", "field-caption").withText(fieldLabel)) - .findElement(panel); + return Locator.tagWithClass("div", "field-caption") + .findElement(findFieldRow(fieldKey, selectedFieldsPanel)) + .getText(); } - protected WebElement findSelectedField(String fieldKey) + protected WebElement findSelectedField(FieldKey fieldKey) { return findFieldRow(fieldKey, selectedFieldsPanel); } - protected WebElement findAvailableField(String fieldKey) + protected WebElement findAvailableField(FieldKey fieldKey) { return findFieldRow(fieldKey, availableFieldsPanel); } - protected WebElement findFieldRow(String fieldKey, WebElement panel) + protected WebElement findFieldRow(FieldKey fieldKey, WebElement panel) { return Locator.tagWithClass("div", "list-group-item") - .withAttributeIgnoreCase("data-fieldkey", fieldKey) + .withAttributeIgnoreCase("data-fieldkey", fieldKey.toString()) .findElement(panel); } diff --git a/src/org/labkey/test/params/FieldKey.java b/src/org/labkey/test/params/FieldKey.java index 6ccc37387d..9b441cc92e 100644 --- a/src/org/labkey/test/params/FieldKey.java +++ b/src/org/labkey/test/params/FieldKey.java @@ -4,51 +4,24 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; -import java.util.Iterator; import java.util.List; -public final class FieldKey implements CharSequence, WrapsFieldKey +public final class FieldKey extends QueryKey implements CharSequence, WrapsFieldKey { - private static final String[] ILLEGAL = {"$", "/", "&", "}", "~", ",", "."}; - private static final String[] REPLACEMENT = {"$D", "$S", "$A", "$B", "$T", "$C", "$P"}; - - public static final FieldKey EMPTY = new FieldKey(null, ""); // Useful as a sort of FieldKey builder starting point public static final FieldKey SOURCES_FK = FieldKey.fromParts("DataInputs"); public static final FieldKey PARENTS_FK = FieldKey.fromParts("MaterialInputs"); - private static final String SEPARATOR = "/"; - - private final FieldKey _parent; - private final String _name; - private final String _fieldKey; + private static final String DIVIDER = "/"; private FieldKey(FieldKey parent, String child) { - if (parent != null && !parent.isEmpty()) - { - _parent = parent; - _name = parent.getName() + SEPARATOR + child; - _fieldKey = parent + SEPARATOR + encodePart(child); - } - else - { - _parent = null; - _name = child; - _fieldKey = encodePart(child); - } - } - - public static List getIllegalChars() - { - return List.of(ILLEGAL); + super(parent, child); } public static FieldKey fromParts(List parts) { - return EMPTY.child(parts); + return QueryKey.fromParts(FieldKey::new, parts); } public static FieldKey fromParts(String... parts) @@ -71,7 +44,7 @@ public static FieldKey fromParts(String... parts) { try { - return fromParts(Arrays.stream(fieldKey.toString().split(SEPARATOR)).map(FieldKey::decodePart).toList()); + return fromParts(Arrays.stream(fieldKey.toString().split(DIVIDER)).map(FieldKey::decodePart).toList()); } catch (IllegalArgumentException iae) { @@ -93,21 +66,6 @@ public static FieldKey fromName(CharSequence nameOrFieldKey) return fromParts(nameOrFieldKey.toString()); } - public static String encodePart(String str) - { - return StringUtils.replaceEach(str, ILLEGAL, REPLACEMENT); - } - - public static String decodePart(String str) - { - return StringUtils.replaceEach(str, REPLACEMENT, ILLEGAL); - } - - public FieldKey getParent() - { - return _parent; - } - public FieldKey child(String... parts) { return child(Arrays.asList(parts)); @@ -127,77 +85,61 @@ public FieldKey child(List parts) return child; } - public Iterator getIterator() - { - List ancestors = new ArrayList<>(); - FieldKey temp = this; - - while (temp != null) - { - ancestors.add(temp); - temp = temp.getParent(); - } + // QueryKey - Collections.reverse(ancestors); - - return ancestors.iterator(); - } - - public String getName() + @Override + protected String getDivider() { - return _name; + return DIVIDER; } - /** - * Inverse of {@link #fromParts(String...)} - * @return decoded parts of the field key - */ - public String[] getNameArray() + @Override + protected FieldKey getThis() { - return Arrays.stream(_fieldKey.split(SEPARATOR)).map(FieldKey::decodePart).toArray(String[]::new); + return this; } + // WrapsFieldKey + @Override public FieldKey getFieldKey() { return this; } - @Override - public @NotNull String toString() - { - return _fieldKey; - } + // CharSequence @Override public int length() { - return _fieldKey.length(); + return toString().length(); } @Override public char charAt(int index) { - return _fieldKey.charAt(index); + return toString().charAt(index); } @Override public @NotNull CharSequence subSequence(int start, int end) { - return _fieldKey.subSequence(start, end); + return toString().subSequence(start, end); } + // Object + @Override public boolean equals(Object o) { if (!(o instanceof FieldKey fieldKey)) return false; - return _fieldKey.equalsIgnoreCase(fieldKey._fieldKey); // FieldKeys aren't case-sensitive? + return toString().equalsIgnoreCase(fieldKey.toString()); // FieldKeys aren't case-sensitive? } @Override public int hashCode() { - return _fieldKey.toLowerCase().hashCode(); // FieldKeys aren't case-sensitive? + return toString().toLowerCase().hashCode(); // FieldKeys aren't case-sensitive? } } diff --git a/src/org/labkey/test/params/QueryKey.java b/src/org/labkey/test/params/QueryKey.java new file mode 100644 index 0000000000..91b7eecc02 --- /dev/null +++ b/src/org/labkey/test/params/QueryKey.java @@ -0,0 +1,103 @@ +package org.labkey.test.params; + +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.function.BiFunction; + +public abstract class QueryKey> +{ + private static final String[] ILLEGAL = {"$", "/", "&", "}", "~", ",", "."}; + private static final String[] REPLACEMENT = {"$D", "$S", "$A", "$B", "$T", "$C", "$P"}; + + private final T _parent; + private final String _name; + private final String _encodedKey; + + protected QueryKey(T parent, @NotNull String name) + { + _name = Objects.requireNonNull(name); + _parent = parent; + + if (parent != null) + { + _encodedKey = parent + getDivider() + encodePart(name); + } + else + { + _encodedKey = encodePart(name); + } + } + + protected abstract String getDivider(); + + protected abstract T getThis(); + + protected static T fromParts(BiFunction factory, List parts) + { + T fieldKey = null; + for (String part : parts) + { + fieldKey = factory.apply(fieldKey, part); + } + return fieldKey; + } + + public static List getIllegalChars() + { + return List.of(ILLEGAL); + } + + public static String encodePart(String str) + { + return StringUtils.replaceEach(str, ILLEGAL, REPLACEMENT); + } + + public static String decodePart(String str) + { + return StringUtils.replaceEach(str, REPLACEMENT, ILLEGAL); + } + + public T getParent() + { + return _parent; + } + + public Iterator getIterator() + { + List ancestors = new ArrayList<>(); + T temp = getThis(); + + while (temp != null) + { + ancestors.add(temp); + temp = temp.getParent(); + } + + Collections.reverse(ancestors); + + return ancestors.iterator(); + } + + public String getName() + { + return _name; + } + + public String getFullName() + { + return decodePart(_encodedKey); + } + + @Override + public @NotNull String toString() + { + return _encodedKey; + } + +} diff --git a/src/org/labkey/test/params/SchemaKey.java b/src/org/labkey/test/params/SchemaKey.java new file mode 100644 index 0000000000..a2d0283a12 --- /dev/null +++ b/src/org/labkey/test/params/SchemaKey.java @@ -0,0 +1,41 @@ +package org.labkey.test.params; + +import java.util.Arrays; +import java.util.List; + +public class SchemaKey extends QueryKey +{ + + private SchemaKey(SchemaKey parent, String child) + { + super(parent, child); + } + + public static SchemaKey fromParts(List parts) + { + return QueryKey.fromParts(SchemaKey::new, parts); + } + + public static SchemaKey fromParts(String... parts) + { + return fromParts(Arrays.asList(parts)); + } + + public static SchemaKey parse(String schemaKey) + { + return fromParts(Arrays.stream(schemaKey.split("\\.")).map(QueryKey::decodePart).toList()); + } + + @Override + protected String getDivider() + { + return "."; + } + + @Override + protected SchemaKey getThis() + { + return this; + } + +} diff --git a/src/org/labkey/test/selenium/ReclickingWebElement.java b/src/org/labkey/test/selenium/ReclickingWebElement.java index cf0e34f256..0417cf1c19 100644 --- a/src/org/labkey/test/selenium/ReclickingWebElement.java +++ b/src/org/labkey/test/selenium/ReclickingWebElement.java @@ -15,7 +15,6 @@ */ 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; @@ -212,18 +211,15 @@ private void revealElement(WebElement el, String shortMessage) if (!blockResolved) { + // Move mouse to corner to dismiss tooltips + new Actions(getDriver()).moveToLocation(0,0).perform(); + Locator.XPathLocator interceptingElLoc = parseInterceptingElementLoc(shortMessage); if (interceptingElLoc != null) { 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/util/DomainUtils.java b/src/org/labkey/test/util/DomainUtils.java index 790cd2afa3..6d28bd91d0 100644 --- a/src/org/labkey/test/util/DomainUtils.java +++ b/src/org/labkey/test/util/DomainUtils.java @@ -102,6 +102,7 @@ public enum DomainKind { SampleSet, // aka "Sample Type" IntList, VarList, + Picklist, StudyDatasetDate, StudyDatasetVisit, ; diff --git a/src/org/labkey/test/util/EscapeUtil.java b/src/org/labkey/test/util/EscapeUtil.java index 7870369cbd..48a53d6e2e 100644 --- a/src/org/labkey/test/util/EscapeUtil.java +++ b/src/org/labkey/test/util/EscapeUtil.java @@ -17,13 +17,16 @@ import org.apache.commons.text.StringEscapeUtils; import org.eclipse.jetty.util.URIUtil; +import org.jetbrains.annotations.NotNull; import org.labkey.test.params.FieldKey; import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -213,4 +216,25 @@ public static String escapeForNameExpression(String name) return nameExpressionNeedsEscaping.matcher(name).replaceAll("\\\\$1"); } + /** + * Generate an app path for use as a URL fragment. Parts will be encoded and joined.
+ * e.g. encodeAppResourcePath("samples", "my samples") will return "/samples/my%20samples"
+ * + * @param pathParts Parts to be combined into an app path. Most likely strings and/or Integers + * @return encoded resource path + */ + public static @NotNull String encodeAppResourcePath(Object... pathParts) + { + List encodedParts = Arrays.stream(pathParts).map(Objects::requireNonNull).map(String::valueOf) + .map(EscapeUtil::encodeAppResourcePathPart).collect(Collectors.toList()); + return "/" + String.join("/", encodedParts); + } + + private static String encodeAppResourcePathPart(String pathPart) + { + return encode(pathPart) + // We generally don't encode parentheses in app resource paths + .replace("%28", "(") + .replace("%29", ")"); + } } diff --git a/src/org/labkey/test/util/FileBrowserHelper.java b/src/org/labkey/test/util/FileBrowserHelper.java index f4fee662d8..72a403853c 100644 --- a/src/org/labkey/test/util/FileBrowserHelper.java +++ b/src/org/labkey/test/util/FileBrowserHelper.java @@ -205,9 +205,10 @@ private void selectFolderTreeNode(@LoggedParam String nodeId) } else if (!folderTreeNode.getAttribute("class").contains("x4-grid-row-selected")) { + WebElement folderIcon = Locator.byClass("x4-tree-icon").findElement(folderTreeNode); // Scroll bars get in the way sometimes, need to scroll folder tree manually - scrollIntoView(folderTreeNode); - doAndWaitForFileListRefresh(folderTreeNode::click); + scrollIntoView(folderIcon); + doAndWaitForFileListRefresh(folderIcon::click); } } diff --git a/src/org/labkey/test/util/TestDataGenerator.java b/src/org/labkey/test/util/TestDataGenerator.java index e53ac7b0aa..a35918e59c 100644 --- a/src/org/labkey/test/util/TestDataGenerator.java +++ b/src/org/labkey/test/util/TestDataGenerator.java @@ -42,6 +42,7 @@ import org.labkey.test.params.FieldDefinition; import org.labkey.test.util.DomainUtils.DomainKind; import org.labkey.test.util.data.ColumnNameMapper; +import org.labkey.test.util.data.RecordIterator; import org.labkey.test.util.data.TestDataUtils; import org.labkey.test.util.query.QueryApiHelper; @@ -57,7 +58,6 @@ import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.NoSuchElementException; import java.util.Objects; import java.util.Set; import java.util.concurrent.ThreadLocalRandom; @@ -816,7 +816,7 @@ public String getDataAsTsv() */ public File writeData(String fileName) { - return writeData(fileName, new FileRowIterator(getFieldsForFile(), _rows)); + return writeData(fileName, new RecordIterator(getFieldsForFile(), _rows)); } /** @@ -827,7 +827,7 @@ public File writeData(String fileName) */ public File writeData(String fileName, int numRows) { - return writeData(fileName, new FileRowIterator(getFieldsForFile(), this::generateRow, numRows)); + return writeData(fileName, new RecordIterator(getFieldsForFile(), this::generateRow, numRows)); } /** @@ -1027,71 +1027,3 @@ public static boolean doesDomainExists(final String containerPath, final String } } - -class FileRowIterator implements Iterator> -{ - private final List headers; - private final Iterator> rows; - - private boolean firstRow = true; - - public FileRowIterator(@NotNull List headers, @NotNull Iterator> rows) - { - this.headers = Objects.requireNonNull(headers); - this.rows = Objects.requireNonNull(rows); - } - - public FileRowIterator(@NotNull List headers, @NotNull Supplier> rowSupplier, final int rowCount) - { - this(headers, new Iterator<>() - { - int count = 0; - - @Override - public boolean hasNext() - { - return count < rowCount; - } - - @Override - public Map next() - { - count++; - return rowSupplier.get(); - } - }); - } - - public FileRowIterator(@NotNull List headers, @NotNull List> rows) - { - this(headers, rows.iterator()); - } - - @Override - public boolean hasNext() - { - return firstRow || rows.hasNext(); - } - - @Override - public List next() - { - if (!hasNext()) - throw new NoSuchElementException(); - - if (firstRow) - { - firstRow = false; - return Collections.unmodifiableList(headers); - } - else - { - return rowMapToList(rows.next()); - } - } - - private List rowMapToList(Map row) - { - return headers.stream().map(h -> row.getOrDefault(h, "")).toList(); - } -} \ No newline at end of file diff --git a/src/org/labkey/test/util/URLBuilder.java b/src/org/labkey/test/util/URLBuilder.java index 4f49b5485b..8b585fee9c 100644 --- a/src/org/labkey/test/util/URLBuilder.java +++ b/src/org/labkey/test/util/URLBuilder.java @@ -5,11 +5,8 @@ import org.labkey.test.WebTestHelper; import java.nio.charset.Charset; -import java.util.Arrays; -import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.stream.Collectors; import static org.labkey.test.WebTestHelper.getBaseURL; @@ -105,18 +102,7 @@ public URLBuilder setQuery(Map query) */ public URLBuilder setAppResourcePath(Object... pathParts) { - List encodedParts = Arrays.stream(pathParts).map(Objects::requireNonNull).map(String::valueOf) - .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", ")"); + return setFragment(EscapeUtil.encodeAppResourcePath(pathParts)); } /** diff --git a/src/org/labkey/test/util/data/RecordIterator.java b/src/org/labkey/test/util/data/RecordIterator.java new file mode 100644 index 0000000000..5dce259569 --- /dev/null +++ b/src/org/labkey/test/util/data/RecordIterator.java @@ -0,0 +1,100 @@ +package org.labkey.test.util.data; + +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.function.Supplier; + +public class RecordIterator implements Iterator> +{ + private final Collection headers; + private final Iterator> rowIterator; + + private boolean firstRow = true; + + public RecordIterator(@NotNull Collection headers, @NotNull Iterator> rowIterator) + { + this.headers = Objects.requireNonNull(headers); + Objects.requireNonNull(rowIterator); + this.rowIterator = new Iterator<>() + { + @Override + public boolean hasNext() + { + return rowIterator.hasNext(); + } + + @Override + public Map next() + { + return Collections.unmodifiableMap(rowIterator.next()); + } + }; + } + + public RecordIterator(@NotNull List headers, @NotNull Supplier> rowSupplier, final int rowCount) + { + this.headers = Objects.requireNonNull((Collection) headers); + this.rowIterator = new Iterator<>() + { + int count = 0; + + @Override + public boolean hasNext() + { + return count < rowCount; + } + + @Override + public Map next() + { + count++; + return rowSupplier.get(); + } + }; + } + + public RecordIterator(@NotNull Collection headers, @NotNull List> rows) + { + this(headers, rows.iterator()); + } + + public RecordIterator(@NotNull List> rows) + { + this(rows.get(0).keySet(), rows); + } + + @Override + public boolean hasNext() + { + return firstRow || rowIterator.hasNext(); + } + + @Override + public List next() + { + if (!hasNext()) + throw new NoSuchElementException(); + + if (firstRow) + { + firstRow = false; + return List.copyOf(headers); + } + else + { + return rowMapToList(rowIterator.next()); + } + } + + private List rowMapToList(Map row) + { + return headers.stream().map(h -> row.getOrDefault(h, "")).toList(); + } +} diff --git a/src/org/labkey/test/util/data/TestDataUtils.java b/src/org/labkey/test/util/data/TestDataUtils.java index 5b662c60d1..3eb899cb29 100644 --- a/src/org/labkey/test/util/data/TestDataUtils.java +++ b/src/org/labkey/test/util/data/TestDataUtils.java @@ -455,6 +455,11 @@ public static List> replaceMapKeys(List> rowMaps, return updatedRows; } + public static File writeDataToFile(String fileName, List> rowMaps) throws IOException + { + return writeRowsToFile(fileName, new RecordIterator(rowMaps)); + } + public static File writeRowsToFile(String fileName, List> rows) throws IOException { return writeRowsToFile(fileName, rows.iterator());