From 5cc2f383fc8aedaa89c8332ff7afe6368c0ea162 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Mon, 21 Apr 2025 14:53:04 -0700 Subject: [PATCH 01/21] Use 'textContent' to pull raw field labels from page --- .../test/components/ui/grids/DetailTable.java | 3 ++- .../test/components/ui/grids/EditableGrid.java | 3 ++- .../ui/grids/FieldSelectionDialog.java | 9 ++++++--- .../components/ui/grids/GridFilterModal.java | 5 +++-- .../components/ui/grids/ResponsiveGrid.java | 5 +++-- src/org/labkey/test/params/FieldDefinition.java | 17 ++++------------- src/org/labkey/test/util/TestDataGenerator.java | 8 +------- .../test/util/selenium/WebDriverUtils.java | 17 +++++++++++++++++ 8 files changed, 38 insertions(+), 29 deletions(-) diff --git a/src/org/labkey/test/components/ui/grids/DetailTable.java b/src/org/labkey/test/components/ui/grids/DetailTable.java index 9a20d5345f..d574e26072 100644 --- a/src/org/labkey/test/components/ui/grids/DetailTable.java +++ b/src/org/labkey/test/components/ui/grids/DetailTable.java @@ -15,6 +15,7 @@ import java.util.Map; import static org.labkey.test.WebDriverWrapper.WAIT_FOR_JAVASCRIPT; +import static org.labkey.test.util.selenium.WebDriverUtils.getTextContent; /** * This is a 'special' table that has only two columns, and no header. An example of this table can be seen in the @@ -155,7 +156,7 @@ public Map getTableDataByLabel() { List tds = tableRow.findElements(By.tagName("td")); - tableData.put(tds.get(0).getText(), tds.get(1).getText()); + tableData.put(getTextContent(tds.get(0)), tds.get(1).getText()); } return tableData; diff --git a/src/org/labkey/test/components/ui/grids/EditableGrid.java b/src/org/labkey/test/components/ui/grids/EditableGrid.java index 9a7294a4a2..d0eb861684 100644 --- a/src/org/labkey/test/components/ui/grids/EditableGrid.java +++ b/src/org/labkey/test/components/ui/grids/EditableGrid.java @@ -49,6 +49,7 @@ import static org.labkey.test.util.TestLogger.log; import static org.labkey.test.util.selenium.ScrollUtils.Alignment.center; import static org.labkey.test.util.selenium.WebDriverUtils.MODIFIER_KEY; +import static org.labkey.test.util.selenium.WebDriverUtils.getTextContent; public class EditableGrid extends WebDriverComponent { @@ -1136,7 +1137,7 @@ public List getColumnLabels() { for (WebElement el : headerCells) { - fieldLabels.add(el.getText().trim()); + fieldLabels.add(getTextContent(el)); } int rowNumberColumn = 0; diff --git a/src/org/labkey/test/components/ui/grids/FieldSelectionDialog.java b/src/org/labkey/test/components/ui/grids/FieldSelectionDialog.java index 7e31e9036b..f07c2234f1 100644 --- a/src/org/labkey/test/components/ui/grids/FieldSelectionDialog.java +++ b/src/org/labkey/test/components/ui/grids/FieldSelectionDialog.java @@ -7,6 +7,7 @@ import org.labkey.test.components.UpdatingComponent; import org.labkey.test.components.bootstrap.ModalDialog; import org.labkey.test.components.html.Checkbox; +import org.labkey.test.util.selenium.WebDriverUtils; import org.openqa.selenium.Keys; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; @@ -20,6 +21,8 @@ import java.util.List; import java.util.stream.Collectors; +import static org.labkey.test.util.selenium.WebDriverUtils.getTextContent; + /** * Wraps ColumnSelectionModal.tsx in UI components. */ @@ -88,7 +91,7 @@ public boolean isShowAllChecked() public List getAvailableFieldLabels() { List listItemElements = elementCache().getListItemNameElements(elementCache().availableFieldsPanel); - return listItemElements.stream().map(WebElement::getText).collect(Collectors.toList()); + return listItemElements.stream().map(WebDriverUtils::getTextContent).collect(Collectors.toList()); } /** @@ -240,7 +243,7 @@ private boolean isFieldKeyExpanded(WebElement listItem) public List getSelectedFieldLabels() { List listItemElements = elementCache().getListItemNameElements(elementCache().selectedFieldsPanel); - return listItemElements.stream().map(WebElement::getText).collect(Collectors.toList()); + return listItemElements.stream().map(WebDriverUtils::getTextContent).collect(Collectors.toList()); } /** @@ -256,7 +259,7 @@ public String getActiveSelectedFieldLabel() if(active.isDisplayed()) { - return Locator.tagWithClass("div", "field-name").findElement(active).getText(); + return getTextContent(Locator.tagWithClass("div", "field-name").findElement(active)); } else { diff --git a/src/org/labkey/test/components/ui/grids/GridFilterModal.java b/src/org/labkey/test/components/ui/grids/GridFilterModal.java index 5beb6814d8..409e304edd 100644 --- a/src/org/labkey/test/components/ui/grids/GridFilterModal.java +++ b/src/org/labkey/test/components/ui/grids/GridFilterModal.java @@ -7,6 +7,7 @@ import org.labkey.test.components.react.Tabs; import org.labkey.test.components.ui.search.FilterExpressionPanel; import org.labkey.test.components.ui.search.FilterFacetedPanel; +import org.labkey.test.util.selenium.WebDriverUtils; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.ui.ExpectedConditions; @@ -67,7 +68,7 @@ public GridFilterModal checkNoDataCheckbox(boolean checked) */ public List getAvailableFieldLabels() { - return getWrapper().getTexts(elementCache().findFieldOptions()); + return elementCache().findFieldOptions().stream().map(WebDriverUtils::getTextContent).collect(Collectors.toList()); } public List getFilteredFieldLabels() @@ -76,7 +77,7 @@ public List getFilteredFieldLabels() Locator.tagWithClass("span", "field-modal__field_dot")) .findElements(elementCache().fieldsSelectionPanel); - return filteredElements.stream().map(WebElement::getText).collect(Collectors.toList()); + return filteredElements.stream().map(WebDriverUtils::getTextContent).collect(Collectors.toList()); } /** diff --git a/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java b/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java index 1acedf682f..651a607295 100644 --- a/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java +++ b/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java @@ -15,6 +15,7 @@ import org.labkey.test.components.html.RadioButton; import org.labkey.test.components.react.ReactCheckBox; import org.labkey.test.components.ui.search.FilterExpressionPanel; +import org.labkey.test.util.selenium.WebDriverUtils; import org.openqa.selenium.Keys; import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.NotFoundException; @@ -318,7 +319,7 @@ public void editColumnLabel(String fieldLabel, String newColumnLabel) .until(ExpectedConditions.stalenessOf(textEdit)); doAndWaitForUpdate(()-> - WebDriverWrapper.waitFor(()->headerCell.getText().equals(newColumnLabel), + WebDriverWrapper.waitFor(()-> WebDriverUtils.getTextContent(headerCell).equals(newColumnLabel), "Column header not updated.", 1_000) ); waitForLoaded(); @@ -829,7 +830,7 @@ protected Map initColumnsAndIndices() headerCellElements.remove(0); offset = 1; } - fieldLabels = getWrapper().getTexts(headerCellElements); + fieldLabels = headerCellElements.stream().map(WebDriverUtils::getTextContent).toList(); indexes = new HashMap<>(); for (int i = 0; i < headerCellElements.size(); i++) { diff --git a/src/org/labkey/test/params/FieldDefinition.java b/src/org/labkey/test/params/FieldDefinition.java index effdec74fa..cb2cb03d89 100644 --- a/src/org/labkey/test/params/FieldDefinition.java +++ b/src/org/labkey/test/params/FieldDefinition.java @@ -85,13 +85,8 @@ public FieldDefinition(String name) this(name, ColumnType.String); } - public static String labelFromName(String name) - { - return labelFromName(name, true); - } - // See BaseColumnInfo.labelFromName - public static String labelFromName(String name, boolean collapseSpaces) + public static String labelFromName(String name) { if (name == null) return null; @@ -123,13 +118,9 @@ else if (Character.isUpperCase(c) && Character.isLowerCase(chars[i - 1])) } } - if (collapseSpaces) - { - // This differs from BaseColumnInfo.labelForName because for testing purposes - // we need the label as shown in the UI, which will contract multiple spaces - return buf.toString().replaceAll("\\s+", " "); - } - return buf.toString(); + // This differs from BaseColumnInfo.labelForName because for testing purposes + // we need the label as shown in the UI, which will contract multiple spaces + return buf.toString().replaceAll("\\s+", " "); } @Override diff --git a/src/org/labkey/test/util/TestDataGenerator.java b/src/org/labkey/test/util/TestDataGenerator.java index d99a5f57e9..d0528a005c 100644 --- a/src/org/labkey/test/util/TestDataGenerator.java +++ b/src/org/labkey/test/util/TestDataGenerator.java @@ -523,13 +523,7 @@ public static String randomMultiLineString(int size, @Nullable String exclusion) */ public static String randomName(@NotNull String part, int numStartChars, int numEndChars, String charSet, @Nullable String exclusions) { - String name = randomString(numStartChars, exclusions, charSet) + part + randomString(numEndChars, exclusions, charSet); - - // Multiple spaces in the UI are collapsed into a single space so we collapse them here so we can find things by name. - // See Issue 52193 for details. - // If we need to test for handling of multiple spaces, we'll not use this generator. - name = name.trim().replaceAll("\\s+", " "); - return name; + return randomString(numStartChars, exclusions, charSet) + part + randomString(numEndChars, exclusions, charSet); } public static String randomDomainName() diff --git a/src/org/labkey/test/util/selenium/WebDriverUtils.java b/src/org/labkey/test/util/selenium/WebDriverUtils.java index b6306ef67f..a98286720c 100644 --- a/src/org/labkey/test/util/selenium/WebDriverUtils.java +++ b/src/org/labkey/test/util/selenium/WebDriverUtils.java @@ -146,6 +146,23 @@ public static String getTextNodeWithin(WebElement element) return textChildren.get(0); } + /** + * {@link WebElement#getText()} matches the browser's rendering, which collapses and trims whitespace. + * If you need the actual text written by the server, the element's {@code textContent} property is unmodified.
+ * Given a WebElement representing the following div: + *
{@code
+     * 
three spaces
+ * }
+ * {@link WebElement#getText()} would return {@code "three spaces"} but this method will retain the extra spaces. + * @param element element to inspect + * @return textContent for the given element + */ + @SuppressWarnings("unchecked") + public static String getTextContent(WebElement element) + { + return element.getDomProperty("textContent"); + } + /** * 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}). From ee7cdbf639f2b35afc4c6c50ef387059d70861af Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Mon, 21 Apr 2025 16:59:33 -0700 Subject: [PATCH 02/21] Don't remove double-spaces --- src/org/labkey/test/params/FieldDefinition.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/org/labkey/test/params/FieldDefinition.java b/src/org/labkey/test/params/FieldDefinition.java index cb2cb03d89..c80a01b295 100644 --- a/src/org/labkey/test/params/FieldDefinition.java +++ b/src/org/labkey/test/params/FieldDefinition.java @@ -118,9 +118,7 @@ else if (Character.isUpperCase(c) && Character.isLowerCase(chars[i - 1])) } } - // This differs from BaseColumnInfo.labelForName because for testing purposes - // we need the label as shown in the UI, which will contract multiple spaces - return buf.toString().replaceAll("\\s+", " "); + return buf.toString(); } @Override From afa99fa31668edf960c7e7a960da913616977071 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Tue, 22 Apr 2025 16:33:27 -0700 Subject: [PATCH 03/21] Handle "Remove Column" menu --- .../components/ui/grids/EditableGrid.java | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/org/labkey/test/components/ui/grids/EditableGrid.java b/src/org/labkey/test/components/ui/grids/EditableGrid.java index d0eb861684..85c36fdcb0 100644 --- a/src/org/labkey/test/components/ui/grids/EditableGrid.java +++ b/src/org/labkey/test/components/ui/grids/EditableGrid.java @@ -15,6 +15,7 @@ import org.labkey.test.components.ui.entities.EntityBulkUpdateDialog; import org.labkey.test.params.FieldDefinition; import org.labkey.test.util.selenium.ScrollUtils; +import org.labkey.test.util.selenium.WebDriverUtils; import org.openqa.selenium.By; import org.openqa.selenium.Keys; import org.openqa.selenium.NoSuchElementException; @@ -49,7 +50,6 @@ import static org.labkey.test.util.TestLogger.log; import static org.labkey.test.util.selenium.ScrollUtils.Alignment.center; import static org.labkey.test.util.selenium.WebDriverUtils.MODIFIER_KEY; -import static org.labkey.test.util.selenium.WebDriverUtils.getTextContent; public class EditableGrid extends WebDriverComponent { @@ -1137,7 +1137,7 @@ public List getColumnLabels() { for (WebElement el : headerCells) { - fieldLabels.add(getTextContent(el)); + fieldLabels.add(getLabelFromHeaderCell(el)); } int rowNumberColumn = 0; @@ -1156,6 +1156,35 @@ public List getColumnLabels() return fieldLabels; } + /** + * Extract label from header cell. Editable grid header cells have several different layouts. What they have in + * common is that the label is the first text node in the cell, possibly within a <span> + */ + private String getLabelFromHeaderCell(WebElement el) + { + // Use text nodes to ignore browser whitespace formatting + List textNodes = WebDriverUtils.getTextNodesWithin(el); + if (textNodes.isEmpty()) + { + List children = Locator.xpath("./*").findElements(el); + if (children.isEmpty()) + { + return ""; // probably the selection checkbox column + } + else + { + // Depth-first search until we find some text + return getLabelFromHeaderCell(children.get(0)); + } + } + else + { + boolean required = Locator.byClass("required-symbol").existsIn(el); + String label = textNodes.get(0).trim(); // trim trailing NBSP + return label + (required ? " *" : ""); // re-add required asterisk for tests that expect it + } + } + public WebElement inputCell() { return Locators.inputCell.refindWhenNeeded(table); From 9d963882fe9650e5b0f44ccbbf9d6ba3b0986ee6 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Wed, 23 Apr 2025 14:40:04 -0700 Subject: [PATCH 04/21] Create WebElementUtils --- .../test/components/ui/grids/DetailTable.java | 2 +- .../components/ui/grids/EditableGrid.java | 4 +- .../ui/grids/FieldSelectionDialog.java | 8 +- .../components/ui/grids/GridFilterModal.java | 6 +- .../test/components/ui/grids/QueryGrid.java | 4 +- .../components/ui/grids/ResponsiveGrid.java | 6 +- .../test/util/selenium/WebDriverUtils.java | 89 ----------------- .../test/util/selenium/WebElementUtils.java | 95 +++++++++++++++++++ 8 files changed, 110 insertions(+), 104 deletions(-) create mode 100644 src/org/labkey/test/util/selenium/WebElementUtils.java diff --git a/src/org/labkey/test/components/ui/grids/DetailTable.java b/src/org/labkey/test/components/ui/grids/DetailTable.java index d574e26072..41ceaa78e4 100644 --- a/src/org/labkey/test/components/ui/grids/DetailTable.java +++ b/src/org/labkey/test/components/ui/grids/DetailTable.java @@ -15,7 +15,7 @@ import java.util.Map; import static org.labkey.test.WebDriverWrapper.WAIT_FOR_JAVASCRIPT; -import static org.labkey.test.util.selenium.WebDriverUtils.getTextContent; +import static org.labkey.test.util.selenium.WebElementUtils.getTextContent; /** * This is a 'special' table that has only two columns, and no header. An example of this table can be seen in the diff --git a/src/org/labkey/test/components/ui/grids/EditableGrid.java b/src/org/labkey/test/components/ui/grids/EditableGrid.java index 85c36fdcb0..ca33d6a355 100644 --- a/src/org/labkey/test/components/ui/grids/EditableGrid.java +++ b/src/org/labkey/test/components/ui/grids/EditableGrid.java @@ -15,7 +15,7 @@ import org.labkey.test.components.ui.entities.EntityBulkUpdateDialog; import org.labkey.test.params.FieldDefinition; import org.labkey.test.util.selenium.ScrollUtils; -import org.labkey.test.util.selenium.WebDriverUtils; +import org.labkey.test.util.selenium.WebElementUtils; import org.openqa.selenium.By; import org.openqa.selenium.Keys; import org.openqa.selenium.NoSuchElementException; @@ -1163,7 +1163,7 @@ public List getColumnLabels() private String getLabelFromHeaderCell(WebElement el) { // Use text nodes to ignore browser whitespace formatting - List textNodes = WebDriverUtils.getTextNodesWithin(el); + List textNodes = WebElementUtils.getTextNodesWithin(el); if (textNodes.isEmpty()) { List children = Locator.xpath("./*").findElements(el); diff --git a/src/org/labkey/test/components/ui/grids/FieldSelectionDialog.java b/src/org/labkey/test/components/ui/grids/FieldSelectionDialog.java index f07c2234f1..424114d6be 100644 --- a/src/org/labkey/test/components/ui/grids/FieldSelectionDialog.java +++ b/src/org/labkey/test/components/ui/grids/FieldSelectionDialog.java @@ -7,7 +7,7 @@ import org.labkey.test.components.UpdatingComponent; import org.labkey.test.components.bootstrap.ModalDialog; import org.labkey.test.components.html.Checkbox; -import org.labkey.test.util.selenium.WebDriverUtils; +import org.labkey.test.util.selenium.WebElementUtils; import org.openqa.selenium.Keys; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; @@ -21,7 +21,7 @@ import java.util.List; import java.util.stream.Collectors; -import static org.labkey.test.util.selenium.WebDriverUtils.getTextContent; +import static org.labkey.test.util.selenium.WebElementUtils.getTextContent; /** * Wraps ColumnSelectionModal.tsx in UI components. @@ -91,7 +91,7 @@ public boolean isShowAllChecked() public List getAvailableFieldLabels() { List listItemElements = elementCache().getListItemNameElements(elementCache().availableFieldsPanel); - return listItemElements.stream().map(WebDriverUtils::getTextContent).collect(Collectors.toList()); + return listItemElements.stream().map(WebElementUtils::getTextContent).collect(Collectors.toList()); } /** @@ -243,7 +243,7 @@ private boolean isFieldKeyExpanded(WebElement listItem) public List getSelectedFieldLabels() { List listItemElements = elementCache().getListItemNameElements(elementCache().selectedFieldsPanel); - return listItemElements.stream().map(WebDriverUtils::getTextContent).collect(Collectors.toList()); + return listItemElements.stream().map(WebElementUtils::getTextContent).collect(Collectors.toList()); } /** diff --git a/src/org/labkey/test/components/ui/grids/GridFilterModal.java b/src/org/labkey/test/components/ui/grids/GridFilterModal.java index 409e304edd..2ffd46c9ad 100644 --- a/src/org/labkey/test/components/ui/grids/GridFilterModal.java +++ b/src/org/labkey/test/components/ui/grids/GridFilterModal.java @@ -7,7 +7,7 @@ import org.labkey.test.components.react.Tabs; import org.labkey.test.components.ui.search.FilterExpressionPanel; import org.labkey.test.components.ui.search.FilterFacetedPanel; -import org.labkey.test.util.selenium.WebDriverUtils; +import org.labkey.test.util.selenium.WebElementUtils; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.ui.ExpectedConditions; @@ -68,7 +68,7 @@ public GridFilterModal checkNoDataCheckbox(boolean checked) */ public List getAvailableFieldLabels() { - return elementCache().findFieldOptions().stream().map(WebDriverUtils::getTextContent).collect(Collectors.toList()); + return elementCache().findFieldOptions().stream().map(WebElementUtils::getTextContent).collect(Collectors.toList()); } public List getFilteredFieldLabels() @@ -77,7 +77,7 @@ public List getFilteredFieldLabels() Locator.tagWithClass("span", "field-modal__field_dot")) .findElements(elementCache().fieldsSelectionPanel); - return filteredElements.stream().map(WebDriverUtils::getTextContent).collect(Collectors.toList()); + return filteredElements.stream().map(WebElementUtils::getTextContent).collect(Collectors.toList()); } /** diff --git a/src/org/labkey/test/components/ui/grids/QueryGrid.java b/src/org/labkey/test/components/ui/grids/QueryGrid.java index f8c52acbd1..78e6eb21eb 100644 --- a/src/org/labkey/test/components/ui/grids/QueryGrid.java +++ b/src/org/labkey/test/components/ui/grids/QueryGrid.java @@ -14,7 +14,7 @@ import org.labkey.test.components.react.QueryChartPanel; import org.labkey.test.components.react.ReactCheckBox; import org.labkey.test.components.ui.FilterStatusValue; -import org.labkey.test.util.selenium.WebDriverUtils; +import org.labkey.test.util.selenium.WebElementUtils; import org.openqa.selenium.Keys; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; @@ -587,7 +587,7 @@ public String getViewName() if(panelHeader.isDisplayed()) { // The view name in the header is not in a separate element. - viewName = WebDriverUtils.getTextNodeWithin(panelHeader); + viewName = WebElementUtils.getTextNodeWithin(panelHeader); } else { diff --git a/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java b/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java index 651a607295..7639dd875b 100644 --- a/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java +++ b/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java @@ -15,7 +15,7 @@ import org.labkey.test.components.html.RadioButton; import org.labkey.test.components.react.ReactCheckBox; import org.labkey.test.components.ui.search.FilterExpressionPanel; -import org.labkey.test.util.selenium.WebDriverUtils; +import org.labkey.test.util.selenium.WebElementUtils; import org.openqa.selenium.Keys; import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.NotFoundException; @@ -319,7 +319,7 @@ public void editColumnLabel(String fieldLabel, String newColumnLabel) .until(ExpectedConditions.stalenessOf(textEdit)); doAndWaitForUpdate(()-> - WebDriverWrapper.waitFor(()-> WebDriverUtils.getTextContent(headerCell).equals(newColumnLabel), + WebDriverWrapper.waitFor(()-> WebElementUtils.getTextContent(headerCell).equals(newColumnLabel), "Column header not updated.", 1_000) ); waitForLoaded(); @@ -830,7 +830,7 @@ protected Map initColumnsAndIndices() headerCellElements.remove(0); offset = 1; } - fieldLabels = headerCellElements.stream().map(WebDriverUtils::getTextContent).toList(); + fieldLabels = headerCellElements.stream().map(WebElementUtils::getTextContent).toList(); indexes = new HashMap<>(); for (int i = 0; i < headerCellElements.size(); i++) { diff --git a/src/org/labkey/test/util/selenium/WebDriverUtils.java b/src/org/labkey/test/util/selenium/WebDriverUtils.java index a98286720c..eff8ec79bc 100644 --- a/src/org/labkey/test/util/selenium/WebDriverUtils.java +++ b/src/org/labkey/test/util/selenium/WebDriverUtils.java @@ -17,21 +17,14 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.SystemUtils; -import org.intellij.lang.annotations.Language; import org.openqa.selenium.Alert; -import org.openqa.selenium.JavascriptExecutor; import org.openqa.selenium.Keys; import org.openqa.selenium.NoAlertPresentException; -import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.UnhandledAlertException; import org.openqa.selenium.WebDriver; -import org.openqa.selenium.WebDriverException; -import org.openqa.selenium.WebElement; import org.openqa.selenium.WrapsDriver; import org.openqa.selenium.WrapsElement; -import java.util.List; - public abstract class WebDriverUtils { /** @@ -81,88 +74,6 @@ public static WebDriver extractWrappedDriver(Object peeling) return null; } - /** - * {@link WebElement} cannot represent a text node. JavaScript can though, so we can use it to isolate the text - * children of a WebElement and get their text. - * Given a WebElement representing the following div: - *
{@code
-     * 
- * A - * B - * - * D - * D - *
- * }
- * This method will return a list containing {@code ["B", "D"]} - * @param element element to search - * @return text from all child text nodes - */ - @SuppressWarnings("unchecked") - public static List getTextNodesWithin(WebElement element) - { - JavascriptExecutor executor = (JavascriptExecutor) extractWrappedDriver(element); - - @Language("JavaScript") - final String script = """ - var iterator = document.evaluate("text()", arguments[0]); - var texts = []; - - let thisNode = iterator.iterateNext(); - - while (thisNode) { - texts.push(thisNode.textContent); - thisNode = iterator.iterateNext(); - } - return texts; - """; - - List nodeTexts; - try - { - nodeTexts = (List) executor.executeScript(script, element); - } - catch (WebDriverException retry) - { - // Script might throw if the document tree is modified during iteration. Retry once. - nodeTexts = (List) executor.executeScript(script, element); - } - - return nodeTexts.stream().map(t -> (String) t).toList(); - } - - /** - * Gets text from the first text node under the specified WebElement. - * - * @see #getTextNodesWithin(WebElement) - */ - public static String getTextNodeWithin(WebElement element) - { - List textChildren = getTextNodesWithin(element); - if (textChildren.isEmpty()) - { - throw new NoSuchElementException("Element does not have any text children: " + element.toString()); - } - return textChildren.get(0); - } - - /** - * {@link WebElement#getText()} matches the browser's rendering, which collapses and trims whitespace. - * If you need the actual text written by the server, the element's {@code textContent} property is unmodified.
- * Given a WebElement representing the following div: - *
{@code
-     * 
three spaces
- * }
- * {@link WebElement#getText()} would return {@code "three spaces"} but this method will retain the extra spaces. - * @param element element to inspect - * @return textContent for the given element - */ - @SuppressWarnings("unchecked") - public static String getTextContent(WebElement element) - { - return element.getDomProperty("textContent"); - } - /** * 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 new file mode 100644 index 0000000000..d702bc33fc --- /dev/null +++ b/src/org/labkey/test/util/selenium/WebElementUtils.java @@ -0,0 +1,95 @@ +package org.labkey.test.util.selenium; + +import org.intellij.lang.annotations.Language; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.WebDriverException; +import org.openqa.selenium.WebElement; + +import java.util.List; + +public abstract class WebElementUtils +{ + private WebElementUtils() {} + + /** + * {@link WebElement} cannot represent a text node. JavaScript can though, so we can use it to isolate the text + * children of a WebElement and get their text. + * Given a WebElement representing the following div: + *
{@code
+     * 
+ * A + * B + * + * D + * D + *
+ * }
+ * This method will return a list containing {@code ["B", "D"]} + * @param element element to search + * @return text from all child text nodes + */ + @SuppressWarnings("unchecked") + public static List getTextNodesWithin(WebElement element) + { + JavascriptExecutor executor = (JavascriptExecutor) WebDriverUtils.extractWrappedDriver(element); + + @Language("JavaScript") + final String script = """ + var iterator = document.evaluate("text()", arguments[0]); + var texts = []; + + let thisNode = iterator.iterateNext(); + + while (thisNode) { + texts.push(thisNode.textContent); + thisNode = iterator.iterateNext(); + } + return texts; + """; + + List nodeTexts; + try + { + nodeTexts = (List) executor.executeScript(script, element); + } + catch (WebDriverException retry) + { + // Script might throw if the document tree is modified during iteration. Retry once. + nodeTexts = (List) executor.executeScript(script, element); + } + + return nodeTexts.stream().map(t -> (String) t).toList(); + } + + /** + * Gets text from the first text node under the specified WebElement. + * + * @see #getTextNodesWithin(WebElement) + */ + public static String getTextNodeWithin(WebElement element) + { + List textChildren = getTextNodesWithin(element); + if (textChildren.isEmpty()) + { + throw new NoSuchElementException("Element does not have any text children: " + element.toString()); + } + return textChildren.get(0); + } + + /** + * {@link WebElement#getText()} matches the browser's rendering, which collapses and trims whitespace. + * If you need the actual text written by the server, the element's {@code textContent} property is unmodified.
+ * Given a WebElement representing the following div: + *
{@code
+     * 
three spaces
+ * }
+ * {@link WebElement#getText()} would return {@code "three spaces"} but this method will retain the extra spaces. + * @param element element to inspect + * @return textContent for the given element + */ + public static String getTextContent(WebElement element) + { + return element.getDomProperty("textContent"); + } +} From 621d5f961028e53c16ddf5198a7a70135de75bbe Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Wed, 23 Apr 2025 14:56:05 -0700 Subject: [PATCH 05/21] Fix IDE warning --- src/org/labkey/test/util/selenium/WebElementUtils.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/org/labkey/test/util/selenium/WebElementUtils.java b/src/org/labkey/test/util/selenium/WebElementUtils.java index d702bc33fc..4845aee4af 100644 --- a/src/org/labkey/test/util/selenium/WebElementUtils.java +++ b/src/org/labkey/test/util/selenium/WebElementUtils.java @@ -6,6 +6,7 @@ import org.openqa.selenium.WebDriverException; import org.openqa.selenium.WebElement; +import java.util.Collections; import java.util.List; public abstract class WebElementUtils @@ -59,7 +60,7 @@ public static List getTextNodesWithin(WebElement element) nodeTexts = (List) executor.executeScript(script, element); } - return nodeTexts.stream().map(t -> (String) t).toList(); + return nodeTexts != null ? nodeTexts.stream().map(t -> (String) t).toList() : Collections.emptyList(); } /** From 6af3d6ca681c15d2a8175ff8a7e07470400a06e2 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Wed, 23 Apr 2025 15:07:11 -0700 Subject: [PATCH 06/21] Trim random names. The server will do this silently --- src/org/labkey/test/util/TestDataGenerator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/labkey/test/util/TestDataGenerator.java b/src/org/labkey/test/util/TestDataGenerator.java index d0528a005c..b624fa2555 100644 --- a/src/org/labkey/test/util/TestDataGenerator.java +++ b/src/org/labkey/test/util/TestDataGenerator.java @@ -523,7 +523,7 @@ public static String randomMultiLineString(int size, @Nullable String exclusion) */ public static String randomName(@NotNull String part, int numStartChars, int numEndChars, String charSet, @Nullable String exclusions) { - return randomString(numStartChars, exclusions, charSet) + part + randomString(numEndChars, exclusions, charSet); + return (randomString(numStartChars, exclusions, charSet) + part + randomString(numEndChars, exclusions, charSet)).trim(); } public static String randomDomainName() From 3bef79cbd7f7e1f25bd2e825df424ce5bb1cf067 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Wed, 23 Apr 2025 15:22:32 -0700 Subject: [PATCH 07/21] Handle NBSP in textContent --- src/org/labkey/test/util/selenium/WebElementUtils.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/org/labkey/test/util/selenium/WebElementUtils.java b/src/org/labkey/test/util/selenium/WebElementUtils.java index 4845aee4af..d6866f73de 100644 --- a/src/org/labkey/test/util/selenium/WebElementUtils.java +++ b/src/org/labkey/test/util/selenium/WebElementUtils.java @@ -9,6 +9,8 @@ import java.util.Collections; import java.util.List; +import static org.labkey.test.Locator.NBSP; + public abstract class WebElementUtils { private WebElementUtils() {} @@ -91,6 +93,6 @@ public static String getTextNodeWithin(WebElement element) */ public static String getTextContent(WebElement element) { - return element.getDomProperty("textContent"); + return element.getDomProperty("textContent").replace(NBSP, " "); } } From 074c855978a699cc9da8b0f9d990c8986b7a7118 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Thu, 24 Apr 2025 16:13:59 -0700 Subject: [PATCH 08/21] Fix some corner cases --- .../test/components/domain/HitSelectionDialog.java | 11 +++-------- .../test/components/ui/grids/ResponsiveGrid.java | 2 +- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/org/labkey/test/components/domain/HitSelectionDialog.java b/src/org/labkey/test/components/domain/HitSelectionDialog.java index 68d61e1c99..50b626c55f 100644 --- a/src/org/labkey/test/components/domain/HitSelectionDialog.java +++ b/src/org/labkey/test/components/domain/HitSelectionDialog.java @@ -1,19 +1,14 @@ package org.labkey.test.components.domain; import org.labkey.test.Locator; -import org.labkey.test.components.Component; -import org.labkey.test.components.WebDriverComponent; import org.labkey.test.components.bootstrap.ModalDialog; -import org.labkey.test.components.html.Input; import org.labkey.test.components.ui.search.FilterExpressionPanel; -import org.labkey.test.pages.LabKeyPage; +import org.labkey.test.util.selenium.WebElementUtils; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import java.util.List; -import static org.labkey.test.components.html.Input.Input; - public class HitSelectionDialog extends ModalDialog { @@ -23,9 +18,9 @@ public HitSelectionDialog(WebDriver driver) super(new ModalDialogFinder(driver)); } - public List getAvailableFields() + public List getAvailableFieldLabels() { - return getWrapper().getTexts(elementCache().findFieldOptions()); + return elementCache().findFieldOptions().stream().map(WebElementUtils::getTextContent).toList(); } public FilterExpressionPanel selectField(String fieldName) diff --git a/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java b/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java index 7639dd875b..70d7aefab9 100644 --- a/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java +++ b/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java @@ -830,7 +830,7 @@ protected Map initColumnsAndIndices() headerCellElements.remove(0); offset = 1; } - fieldLabels = headerCellElements.stream().map(WebElementUtils::getTextContent).toList(); + fieldLabels = headerCellElements.stream().map(el -> WebElementUtils.getTextContent(el).trim()).toList(); indexes = new HashMap<>(); for (int i = 0; i < headerCellElements.size(); i++) { From 8b3e015f925ac069bbe9899989f5c4fbe7200f66 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Thu, 24 Apr 2025 16:50:07 -0700 Subject: [PATCH 09/21] Broader double-space testing --- src/org/labkey/test/util/TestDataGenerator.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/org/labkey/test/util/TestDataGenerator.java b/src/org/labkey/test/util/TestDataGenerator.java index b624fa2555..13223a1d7f 100644 --- a/src/org/labkey/test/util/TestDataGenerator.java +++ b/src/org/labkey/test/util/TestDataGenerator.java @@ -581,7 +581,7 @@ public static String randomFieldName(String part) public static String randomFieldName(String part, @Nullable String exclusion) { - return randomFieldName(part, randomInt(0, 5), randomInt(0, 5), exclusion); + return randomFieldName(part, randomInt(0, 5), randomInt(1, 5), exclusion); } public static String randomFieldName(String part, int numStartChars, int numEndChars) @@ -594,7 +594,7 @@ public static String randomFieldName(@NotNull String part, int numStartChars, in // use the characters that we know are encoded in fieldKeys plus characters that we know clients are using String chars = ALL_ILLEGAL_QUERY_KEY_CHARACTERS + " %()=+-[]_|*`'\":;<>?!@#^"; - String randomFieldName = randomName(part, numStartChars, numEndChars, chars, exclusion); + String randomFieldName = randomName(part + " ", numStartChars, numEndChars, chars, exclusion); TestLogger.log("Generated random field name: " + randomFieldName); return randomFieldName; } From 18d56581f0109afda4ae911ee3acd27b27de6aaf Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Fri, 25 Apr 2025 15:00:49 -0700 Subject: [PATCH 10/21] Handle more double-spaces --- src/org/labkey/test/Locator.java | 82 +++++++++++-------- .../components/react/BaseReactSelect.java | 13 ++- .../react/FilteringReactSelect.java | 2 +- .../domainproperties/EntityTypeDesigner.java | 3 +- .../test/pages/ReactAssayDesignerPage.java | 3 +- src/org/labkey/test/util/TextUtils.java | 30 +++++++ 6 files changed, 92 insertions(+), 41 deletions(-) create mode 100644 src/org/labkey/test/util/TextUtils.java diff --git a/src/org/labkey/test/Locator.java b/src/org/labkey/test/Locator.java index 34c1d94387..2bf353fa6c 100644 --- a/src/org/labkey/test/Locator.java +++ b/src/org/labkey/test/Locator.java @@ -25,6 +25,7 @@ import org.labkey.test.selenium.ReclickingWebElement; import org.labkey.test.selenium.RefindingWebElement; import org.labkey.test.util.TestLogger; +import org.labkey.test.util.TextUtils; import org.labkey.test.util.selenium.WebDriverUtils; import org.openqa.selenium.By; import org.openqa.selenium.InvalidSelectorException; @@ -185,23 +186,30 @@ protected WebDriver getWebDriver(SearchContext context) */ public static WebElement waitForAnyElement(FluentWait wait, final Locator... locators) { - return wait.until(new Function() + try { - @Override - public WebElement apply(SearchContext context) + return wait.until(new Function() { - return findAnyElementOrNull(context, locators); - } + @Override + public WebElement apply(SearchContext context) + { + return findAnyElementOrNull(context, locators); + } - @Override - public String toString() - { - List locDescriptions = new ArrayList<>(); - Arrays.stream(locators).forEach(loc -> locDescriptions.add(loc.getLoggableDescription())); - SearchContext searchContext = extractInputFromFluentWait(wait); - return String.join("\n--OR--\n", locDescriptions) + (searchContext instanceof WebDriver ? "" : "\nIN: " + searchContext.toString()); - } - }); + @Override + public String toString() + { + List locDescriptions = new ArrayList<>(); + Arrays.stream(locators).forEach(loc -> locDescriptions.add(loc.getLoggableDescription())); + SearchContext searchContext = extractInputFromFluentWait(wait); + return String.join("\n--OR--\n", locDescriptions) + (searchContext instanceof WebDriver ? "" : "\nIN: " + searchContext.toString()); + } + }); + } + catch (TimeoutException e) + { + throw new NoSuchElementException(e.getMessage(), e); + } } /** @@ -210,28 +218,34 @@ public String toString() */ public static List waitForElements(FluentWait wait, final Locator... locators) { - return wait.until(new Function>() + try { - @Override - public List apply(SearchContext context) - { - List els = findElements(context, locators); - if (els.size() > 0) - return els; - else - return null; - } - - @Override - public String toString() + return wait.until(new Function>() { - List locDescriptions = new ArrayList<>(); - Arrays.stream(locators).forEach(loc -> locDescriptions.add(loc.getLoggableDescription())); - SearchContext searchContext = extractInputFromFluentWait(wait); - return String.join("\n--OR--\n", locDescriptions) + (searchContext instanceof WebDriver ? "" : "\nIN: " + searchContext.toString()); - } - }); + @Override + public List apply(SearchContext context) + { + List els = findElements(context, locators); + if (!els.isEmpty()) + return els; + else + return null; + } + @Override + public String toString() + { + List locDescriptions = new ArrayList<>(); + Arrays.stream(locators).forEach(loc -> locDescriptions.add(loc.getLoggableDescription())); + SearchContext searchContext = extractInputFromFluentWait(wait); + return String.join("\n--OR--\n", locDescriptions) + (searchContext instanceof WebDriver ? "" : "\nIN: " + searchContext.toString()); + } + }); + } + catch (TimeoutException e) + { + throw new NoSuchElementException(e.getMessage(), e); + } } public static List findElements(SearchContext context, final Locator... locators) @@ -984,7 +998,7 @@ public static String xq(String value) */ private static String ns(String value) { - return value.replaceAll("\\s+", " ").trim(); + return TextUtils.normalizeSpace(value); } public static String cq(String value) diff --git a/src/org/labkey/test/components/react/BaseReactSelect.java b/src/org/labkey/test/components/react/BaseReactSelect.java index 01ee6fc83d..3af1d1c115 100644 --- a/src/org/labkey/test/components/react/BaseReactSelect.java +++ b/src/org/labkey/test/components/react/BaseReactSelect.java @@ -23,6 +23,7 @@ import java.util.Collections; import java.util.List; +import java.util.function.Function; import java.util.stream.Collectors; import static org.labkey.test.WebDriverWrapper.WAIT_FOR_JAVASCRIPT; @@ -365,7 +366,7 @@ public List getOptionElements() * * @return List of strings for the values in the list. */ - public List getOptions() + public List getOptions(Function optionMapper) { boolean alreadyOpened = isExpanded(); @@ -374,16 +375,20 @@ public List getOptions() if (!alreadyOpened) open(); - List selectedItems = Locators.listItems.findElements(getComponentElement()); - List rawItems = getWrapper().getTexts(selectedItems); + List optionElements = Locators.listItems.findElements(getComponentElement()); + List rawItems = optionElements.stream().map(optionMapper).toList(); // If it wasn't open before close it, otherwise leave it in the open state. if (!alreadyOpened) close(); - return rawItems.stream().map(String::trim).collect(Collectors.toList()); + return rawItems; } + public List getOptions() + { + return getOptions(el -> el.getText().trim()); + } public String getName() { diff --git a/src/org/labkey/test/components/react/FilteringReactSelect.java b/src/org/labkey/test/components/react/FilteringReactSelect.java index de8713eceb..b9c6d1f0eb 100644 --- a/src/org/labkey/test/components/react/FilteringReactSelect.java +++ b/src/org/labkey/test/components/react/FilteringReactSelect.java @@ -163,7 +163,7 @@ public FilteringReactSelect filterSelect(String value, Locator elementToWaitFor) return this; } - private List setFilter(String value) + public List setFilter(String value) { open(); elementCache().input.sendKeys(value); diff --git a/src/org/labkey/test/components/ui/domainproperties/EntityTypeDesigner.java b/src/org/labkey/test/components/ui/domainproperties/EntityTypeDesigner.java index c92abe4c4e..18680b1712 100644 --- a/src/org/labkey/test/components/ui/domainproperties/EntityTypeDesigner.java +++ b/src/org/labkey/test/components/ui/domainproperties/EntityTypeDesigner.java @@ -13,6 +13,7 @@ import org.labkey.test.components.html.ValidatingInput; import org.labkey.test.components.react.ReactSelect; import org.labkey.test.params.FieldDefinition; +import org.labkey.test.util.selenium.WebElementUtils; import org.openqa.selenium.NotFoundException; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; @@ -129,7 +130,7 @@ public String getNameExpressionPreview() { getWrapper().mouseOver(elementCache().helpTarget("Naming ")); waitFor(()->elementCache().popover.isDisplayed(), "No tooltip was shown for the Name Expression.", 1_000); - return elementCache().popover.getText(); + return WebElementUtils.getTextContent(Locator.tag("p").index(1).findElement(elementCache().popover)).split(": ", 2)[1]; } public T dismissToolTip() diff --git a/src/org/labkey/test/pages/ReactAssayDesignerPage.java b/src/org/labkey/test/pages/ReactAssayDesignerPage.java index e2aa200093..4c5c66ad44 100644 --- a/src/org/labkey/test/pages/ReactAssayDesignerPage.java +++ b/src/org/labkey/test/pages/ReactAssayDesignerPage.java @@ -29,6 +29,7 @@ import org.labkey.test.components.ui.files.AttachmentCard; import org.labkey.test.pages.assay.plate.PlateTemplateListPage; import org.labkey.test.util.Maps; +import org.labkey.test.util.selenium.WebElementUtils; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.ui.Select; @@ -237,7 +238,7 @@ public HitSelectionDialog clickEditCriteria() public List getHitCriteria() { expandPropertiesPanel(); - return getWrapper().getTexts(elementCache().hitSelectionCriteriaLoc.findElements(elementCache().propertiesPanel)); + return elementCache().hitSelectionCriteriaLoc.findElements(elementCache().propertiesPanel).stream().map(WebElementUtils::getTextContent).toList(); } public ReactAssayDesignerPage setStatus(boolean checked) diff --git a/src/org/labkey/test/util/TextUtils.java b/src/org/labkey/test/util/TextUtils.java new file mode 100644 index 0000000000..b1863a2c78 --- /dev/null +++ b/src/org/labkey/test/util/TextUtils.java @@ -0,0 +1,30 @@ +package org.labkey.test.util; + +import java.util.regex.Pattern; + +public class TextUtils +{ + private static final Pattern NS_PATTERN = Pattern.compile("\\s+"); + + private TextUtils() {} + + /** + * Equivalent to XPath {@code normalize-space()}:
+ * "The normalize-space function strips leading and trailing white-space from a string, replaces sequences of + * whitespace characters by a single space, and returns the resulting string." + */ + public static String normalizeSpace(String value) + { + return NS_PATTERN.matcher(value).replaceAll(" ").trim(); + } + + public static String normalizeSpaceMultiline(String value) + { + String[] lines = value.split("\n"); + for (int i = 0; i < lines.length; i++) + { + lines[i] = normalizeSpace(lines[i]); + } + return String.join("\n", lines); + } +} From 7649975ce7422d8fc262f274d9548fd17cdf675c Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Fri, 25 Apr 2025 16:41:15 -0700 Subject: [PATCH 11/21] Yet more --- .../ui/domainproperties/EntityTypeDesigner.java | 3 +-- .../test/tests/SampleTypeNameExpressionTest.java | 3 ++- src/org/labkey/test/util/TextUtils.java | 11 +++++++++++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/org/labkey/test/components/ui/domainproperties/EntityTypeDesigner.java b/src/org/labkey/test/components/ui/domainproperties/EntityTypeDesigner.java index 18680b1712..c92abe4c4e 100644 --- a/src/org/labkey/test/components/ui/domainproperties/EntityTypeDesigner.java +++ b/src/org/labkey/test/components/ui/domainproperties/EntityTypeDesigner.java @@ -13,7 +13,6 @@ import org.labkey.test.components.html.ValidatingInput; import org.labkey.test.components.react.ReactSelect; import org.labkey.test.params.FieldDefinition; -import org.labkey.test.util.selenium.WebElementUtils; import org.openqa.selenium.NotFoundException; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; @@ -130,7 +129,7 @@ public String getNameExpressionPreview() { getWrapper().mouseOver(elementCache().helpTarget("Naming ")); waitFor(()->elementCache().popover.isDisplayed(), "No tooltip was shown for the Name Expression.", 1_000); - return WebElementUtils.getTextContent(Locator.tag("p").index(1).findElement(elementCache().popover)).split(": ", 2)[1]; + return elementCache().popover.getText(); } public T dismissToolTip() diff --git a/src/org/labkey/test/tests/SampleTypeNameExpressionTest.java b/src/org/labkey/test/tests/SampleTypeNameExpressionTest.java index 85c58808e4..5bff97c621 100644 --- a/src/org/labkey/test/tests/SampleTypeNameExpressionTest.java +++ b/src/org/labkey/test/tests/SampleTypeNameExpressionTest.java @@ -38,6 +38,7 @@ import org.labkey.test.util.PortalHelper; import org.labkey.test.util.SampleTypeHelper; import org.labkey.test.util.TestDataGenerator; +import org.labkey.test.util.TextUtils; import org.labkey.test.util.exp.SampleTypeAPIHelper; import org.openqa.selenium.TimeoutException; import org.openqa.selenium.WebElement; @@ -696,7 +697,7 @@ private String generateExpectedToolTip(@Nullable String expectedPreview) if(expectedPreview != null) { expectedToolTip.append("Example of name that will be generated from the current pattern: "); - expectedToolTip.append(expectedPreview); + expectedToolTip.append(TextUtils.normalizeSpace(expectedPreview)); expectedToolTip.append("\n"); } diff --git a/src/org/labkey/test/util/TextUtils.java b/src/org/labkey/test/util/TextUtils.java index b1863a2c78..5b8a585cfd 100644 --- a/src/org/labkey/test/util/TextUtils.java +++ b/src/org/labkey/test/util/TextUtils.java @@ -1,5 +1,6 @@ package org.labkey.test.util; +import java.util.List; import java.util.regex.Pattern; public class TextUtils @@ -18,6 +19,11 @@ public static String normalizeSpace(String value) return NS_PATTERN.matcher(value).replaceAll(" ").trim(); } + public static List normalizeSpace(List values) + { + return values.stream().map(TextUtils::normalizeSpace).toList(); + } + public static String normalizeSpaceMultiline(String value) { String[] lines = value.split("\n"); @@ -27,4 +33,9 @@ public static String normalizeSpaceMultiline(String value) } return String.join("\n", lines); } + + public static List normalizeSpaceMultiline(List values) + { + return values.stream().map(TextUtils::normalizeSpaceMultiline).toList(); + } } From 230b762482a31c5d5c86bbc1a5a84eaa1f137e71 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Fri, 25 Apr 2025 17:10:19 -0700 Subject: [PATCH 12/21] A few more --- src/org/labkey/test/components/domain/DomainFieldRow.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/org/labkey/test/components/domain/DomainFieldRow.java b/src/org/labkey/test/components/domain/DomainFieldRow.java index 8ec1709fb1..1f24c94165 100644 --- a/src/org/labkey/test/components/domain/DomainFieldRow.java +++ b/src/org/labkey/test/components/domain/DomainFieldRow.java @@ -19,6 +19,7 @@ import org.labkey.test.pages.core.admin.BaseSettingsPage.TIME_FORMAT; import org.labkey.test.params.FieldDefinition; import org.labkey.test.util.LabKeyExpectedConditions; +import org.labkey.test.util.selenium.WebElementUtils; import org.openqa.selenium.ElementNotInteractableException; import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.SearchContext; @@ -1795,8 +1796,8 @@ public WebElement hitSelectionCriteriaButton() public List hitSelectionCriteria() { - return getWrapper().getTexts(Locator.tagWithClass("li", "hit-criteria-renderer__field-value") - .findElements(this)); + return Locator.tagWithClass("li", "hit-criteria-renderer__field-value") + .findElements(this).stream().map(WebElementUtils::getTextContent).toList(); } public RadioButton aliquotOption(ExpSchema.DerivationDataScopeType option) From f3bb3da42a4ada7fb4403d1728c487fab34e7382 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Mon, 28 Apr 2025 17:30:36 -0700 Subject: [PATCH 13/21] Handle quotes in column headers --- build.gradle | 1 + gradle.properties | 7 +++++++ src/org/labkey/test/util/TestDataUtils.java | 21 +++++++++++++++++++++ 3 files changed, 29 insertions(+) diff --git a/build.gradle b/build.gradle index af7a8fe084..fc5eb041e0 100644 --- a/build.gradle +++ b/build.gradle @@ -26,6 +26,7 @@ project.dependencies { implementation("commons-io:commons-io:${commonsIoVersion}") implementation("com.fasterxml.jackson.core:jackson-annotations:${jacksonAnnotationsVersion}") implementation("org.bouncycastle:bcprov-jdk18on:${bouncycastleVersion}") + implementation("org.apache.commons:commons-csv:${apacheCommonsCsvVersion}") //api "org.seleniumhq.selenium:selenium-server:${seleniumVersion}" implementation("org.seleniumhq.selenium:selenium-firefox-driver:${seleniumVersion}") diff --git a/gradle.properties b/gradle.properties index 41a6ecc5e7..834394698c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,10 +1,17 @@ +apacheCommonsCsvVersion=1.14.0 + aspectjVersion=1.9.23 + assertjVersion=3.27.3 + awaitilityVersion=4.3.0 lookfirstSardineVersion=5.13 + jettyVersion=12.0.18 + seleniumVersion=4.27.0 + mockserverNettyVersion=5.15.0 labkeySchemasTestVersion=25.3-SNAPSHOT diff --git a/src/org/labkey/test/util/TestDataUtils.java b/src/org/labkey/test/util/TestDataUtils.java index dc6d164563..0cae145635 100644 --- a/src/org/labkey/test/util/TestDataUtils.java +++ b/src/org/labkey/test/util/TestDataUtils.java @@ -1,10 +1,16 @@ package org.labkey.test.util; +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; +import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.labkey.serverapi.reader.TabLoader; +import org.labkey.test.TestFileUtils; import org.labkey.test.params.FieldDefinition; +import java.io.File; +import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; @@ -233,6 +239,21 @@ private static String toTabular(List> rowMaps, List return builder.toString(); } + public static File writeRowsToTsv(String fileName, List> rows) throws IOException + { + File file = new File(TestFileUtils.getTestTempDir(), fileName); + FileUtils.forceMkdirParent(file); + + try (CSVPrinter printer = new CSVPrinter(new FileWriter(file, StandardCharsets.UTF_8), CSVFormat.TDF)) { + for (List row : rows) + { + printer.printRecord(row); + } + } + + return file; + } + /** * Used to quote values to be written to a TSV file * @see org.labkey.api.data.TSVWriter From bc32a32f360958855906d24a54be1245f395523a Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Tue, 29 Apr 2025 10:41:38 -0700 Subject: [PATCH 14/21] Fix some name/fieldKey confusion --- .../components/ui/grids/DetailTableEdit.java | 46 ++++++++++--------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/src/org/labkey/test/components/ui/grids/DetailTableEdit.java b/src/org/labkey/test/components/ui/grids/DetailTableEdit.java index abd8357051..a7336805dc 100644 --- a/src/org/labkey/test/components/ui/grids/DetailTableEdit.java +++ b/src/org/labkey/test/components/ui/grids/DetailTableEdit.java @@ -1,7 +1,6 @@ package org.labkey.test.components.ui.grids; import org.junit.Assert; -import org.labkey.api.query.QueryKey; import org.labkey.remoteapi.CommandException; import org.labkey.test.BootstrapLocators; import org.labkey.test.Locator; @@ -16,6 +15,7 @@ import org.labkey.test.components.ui.files.FileUploadField; import org.labkey.test.params.FieldDefinition; import org.labkey.test.util.AuditLogHelper; +import org.labkey.test.util.EscapeUtil; import org.openqa.selenium.By; import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.WebDriver; @@ -87,7 +87,7 @@ public DetailTableEdit adjustChangeCounter(int change) public boolean isFieldPresent(String fieldLabel) { - return elementCache().fieldValue(fieldLabel) != null; + return elementCache().valueCellWithLabel(fieldLabel) != null; } /** * Check to see if a field is editable. Could be state dependent, that is it returns false if the field is @@ -99,7 +99,7 @@ public boolean isFieldPresent(String fieldLabel) public boolean isFieldEditable(String fieldLabel) { // TODO Could put a check here to see if a field is loading then return false, or wait. - WebElement fieldValueElement = elementCache().fieldValue(fieldLabel); + WebElement fieldValueElement = elementCache().valueCellWithLabel(fieldLabel); return isEditableField(fieldValueElement); } @@ -117,7 +117,7 @@ private boolean isEditableField(WebElement element) **/ public String getReadOnlyField(String fieldLabel) { - WebElement fieldValueElement = elementCache().fieldValue(fieldLabel); + WebElement fieldValueElement = elementCache().valueCellWithLabel(fieldLabel); return fieldValueElement.findElement(By.xpath("./div/*")).getText(); } @@ -129,7 +129,7 @@ public String getReadOnlyField(String fieldLabel) **/ public String getTextField(String fieldLabel) { - WebElement fieldValueElement = elementCache().fieldValue(fieldLabel); + WebElement fieldValueElement = elementCache().valueCellWithLabel(fieldLabel); WebElement textElement = fieldValueElement.findElement(By.xpath("./div/div/*")); if(textElement.getTagName().equalsIgnoreCase("textarea")) return textElement.getText(); @@ -148,7 +148,7 @@ public DetailTableEdit setTextField(String fieldLabel, String value) { if(isFieldEditable(fieldLabel)) { - WebElement fieldValueElement = elementCache().fieldValue(fieldLabel); + WebElement fieldValueElement = elementCache().valueCellWithLabel(fieldLabel); WebElement editableElement = fieldValueElement.findElement(By.xpath("./div/div/*")); String elementType = editableElement.getTagName().toLowerCase().trim(); @@ -205,7 +205,7 @@ public DetailTableEdit setTextareaByFieldName(String fieldName, String value) public boolean getBooleanField(String fieldLabel) { // The text used in the field label and the value of the name attribute in the checkbox don't always have the same case. - WebElement editableElement = Locator.tag("input").findElement(elementCache().fieldValue(fieldLabel)); + WebElement editableElement = Locator.tag("input").findElement(elementCache().valueCellWithLabel(fieldLabel)); String elementType = editableElement.getDomAttribute("type").toLowerCase().trim(); Assert.assertEquals(String.format("Field '%s' is not a checkbox. Cannot be get true/false value.", fieldLabel), "checkbox", elementType); @@ -223,7 +223,7 @@ public boolean getBooleanField(String fieldLabel) public DetailTableEdit setBooleanField(String fieldLabel, boolean value) { - WebElement fieldValueElement = elementCache().fieldValue(fieldLabel); + WebElement fieldValueElement = elementCache().valueCellWithLabel(fieldLabel); Assert.assertTrue(String.format("Field '%s' is not editable and cannot be set.", fieldLabel), isEditableField(fieldValueElement)); getWrapper().scrollIntoView(fieldValueElement); @@ -387,7 +387,7 @@ public DetailTableEdit clearSelectValue(String fieldLabel, boolean waitForSelect /** * Set a DateTime, Date or Time field. - * @param fieldKey The encoded fieldKey of the field to set. + * @param fieldName The name of the field to set. * @param dateTime Will be used to determine what kind of field is being set and how to set it. If the parameter * is a LocalDateTime object then it is assumed that field is a DateTime field. If the parameter is * a LocalDate object then it is assumed to be a date-only field. And I think you can guess what @@ -395,9 +395,9 @@ public DetailTableEdit clearSelectValue(String fieldLabel, boolean waitForSelect * is typed into the field (no picker is used). * @return A reference to this DetailTableEdit object. */ - public DetailTableEdit setDateTimeField(String fieldKey, Object dateTime) + public DetailTableEdit setDateTimeField(String fieldName, Object dateTime) { - ReactDateTimePicker dateTimePicker = getDateTimePicker(fieldKey); + ReactDateTimePicker dateTimePicker = getDateTimePicker(fieldName); if(dateTime instanceof LocalDateTime localDateTime) { dateTimePicker.select(localDateTime); @@ -424,23 +424,22 @@ else if(dateTime instanceof String setValue) return this; } - public String getDateTimeField(String fieldLabel) + public String getDateTimeField(String fieldName) { - ReactDateTimePicker dateTimePicker = getDateTimePicker(fieldLabel); + ReactDateTimePicker dateTimePicker = getDateTimePicker(fieldName); return dateTimePicker.get(); } - public void clearDateTimeField(String fieldLabel) + public void clearDateTimeField(String fieldName) { - ReactDateTimePicker dateTimePicker = getDateTimePicker(fieldLabel); + ReactDateTimePicker dateTimePicker = getDateTimePicker(fieldName); dateTimePicker.clear(); _changeCounter++; } - private ReactDateTimePicker getDateTimePicker(String fieldLabel) + private ReactDateTimePicker getDateTimePicker(String fieldName) { - return new ReactDateTimePicker.ReactDateTimeInputFinder(getDriver()) - .withInputId(fieldLabel).find(this); + return new ReactDateTimePicker.ReactDateTimeInputFinder(getDriver()).find(elementCache().valueCellWithName(fieldName)); } // For use when the field is of an unknown type, as can occur in fuzz tests @@ -452,7 +451,7 @@ public void setDetails(FieldDefinition field, Object newValue) if (field.getType() == FieldDefinition.ColumnType.TextChoice) setSelectValue(field.getLabel(), (List) newValue); else if (field.getType() == FieldDefinition.ColumnType.Date || field.getType() == FieldDefinition.ColumnType.DateAndTime || field.getType() == FieldDefinition.ColumnType.Time) - setDateTimeField(QueryKey.encodePart(field.getName()), newValue); + setDateTimeField(field.getName(), newValue); else if (field.getType() == FieldDefinition.ColumnType.Boolean) setBooleanField(field.getLabel(), (Boolean) newValue); else @@ -586,14 +585,19 @@ public ElementCache() public WebElement editPanel = Locator.tagWithClass("div", "detail__editing") .findWhenNeeded(this); - public WebElement fieldValue(String label) + public WebElement valueCellWithLabel(String label) { return Locator.tagWithAttribute("td", "data-caption", label).findElementOrNull(editPanel); } + public WebElement valueCellWithName(String fieldName) + { + return Locator.tagWithAttribute("td", "data-fieldkey", EscapeUtil.fieldKeyEncodePart(fieldName)).findElement(editPanel); + } + public FileUploadField fileField(String label) { - return new FileUploadField(fieldValue(label), getDriver()); + return new FileUploadField(valueCellWithLabel(label), getDriver()); } public Locator validationMsg = Locator.tagWithClass("span", "validation-message"); From bfe0276b50817301fba042c4c42926da493b80a2 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Tue, 29 Apr 2025 11:31:44 -0700 Subject: [PATCH 15/21] Remove temporary double-spaces --- src/org/labkey/test/util/TestDataGenerator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/labkey/test/util/TestDataGenerator.java b/src/org/labkey/test/util/TestDataGenerator.java index 13223a1d7f..4aa3a19169 100644 --- a/src/org/labkey/test/util/TestDataGenerator.java +++ b/src/org/labkey/test/util/TestDataGenerator.java @@ -594,7 +594,7 @@ public static String randomFieldName(@NotNull String part, int numStartChars, in // use the characters that we know are encoded in fieldKeys plus characters that we know clients are using String chars = ALL_ILLEGAL_QUERY_KEY_CHARACTERS + " %()=+-[]_|*`'\":;<>?!@#^"; - String randomFieldName = randomName(part + " ", numStartChars, numEndChars, chars, exclusion); + String randomFieldName = randomName(part, numStartChars, numEndChars, chars, exclusion); TestLogger.log("Generated random field name: " + randomFieldName); return randomFieldName; } From 48e42818edc6bd9c9e07a1226232addb9c4ca35e Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Wed, 30 Apr 2025 10:55:26 -0700 Subject: [PATCH 16/21] Fix some name/caption/fieldKey conflation --- src/org/labkey/test/components/ui/grids/DetailTableEdit.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/labkey/test/components/ui/grids/DetailTableEdit.java b/src/org/labkey/test/components/ui/grids/DetailTableEdit.java index a7336805dc..dc7eb61dec 100644 --- a/src/org/labkey/test/components/ui/grids/DetailTableEdit.java +++ b/src/org/labkey/test/components/ui/grids/DetailTableEdit.java @@ -592,7 +592,7 @@ public WebElement valueCellWithLabel(String label) public WebElement valueCellWithName(String fieldName) { - return Locator.tagWithAttribute("td", "data-fieldkey", EscapeUtil.fieldKeyEncodePart(fieldName)).findElement(editPanel); + return Locator.tagWithAttribute("td", "data-fieldkey", EscapeUtil.fieldKeyEncodePart(fieldName).toLowerCase()).findElement(editPanel); } public FileUploadField fileField(String label) From 0a2029757778bb10c31fbea20350dfa6ec8042cd Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Wed, 30 Apr 2025 15:11:38 -0700 Subject: [PATCH 17/21] More name/caption/fieldKey conflation --- .../ui/entities/EntityBulkUpdateDialog.java | 5 +- .../labkey/test/params/FieldDefinition.java | 11 ++- .../test/util/DeferredErrorCollector.java | 3 + src/org/labkey/test/util/TestDataUtils.java | 79 ++++++++----------- 4 files changed, 45 insertions(+), 53 deletions(-) diff --git a/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java b/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java index aa057d495d..9173097570 100644 --- a/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java +++ b/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java @@ -212,9 +212,8 @@ public List getFieldNames() { List labels = Locator.tagWithClass("label", "control-label").withAttribute("for") .waitForElements(elementCache(), 2_000); - List columns = new ArrayList<>(); - labels.forEach(a -> columns.add(a.getDomAttribute("for"))); - return columns; + + return labels.stream().map(a -> EscapeUtil.fieldKeyDecodePart(a.getDomAttribute("for"))).toList(); } public EntityBulkUpdateDialog waitForFieldsToBe(List expectedFieldNames, int waitMilliseconds) diff --git a/src/org/labkey/test/params/FieldDefinition.java b/src/org/labkey/test/params/FieldDefinition.java index c80a01b295..7c6717c9ee 100644 --- a/src/org/labkey/test/params/FieldDefinition.java +++ b/src/org/labkey/test/params/FieldDefinition.java @@ -16,6 +16,7 @@ package org.labkey.test.params; import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.json.JSONArray; import org.json.JSONObject; @@ -31,6 +32,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import static org.labkey.test.util.TestDataGenerator.DOMAIN_SPECIAL_STRING; @@ -60,7 +62,7 @@ public class FieldDefinition extends PropertyDescriptor * @param name field name * @param type field type */ - public FieldDefinition(String name, ColumnType type) + public FieldDefinition(@NotNull String name, @NotNull ColumnType type) { setName(name); setType(type); @@ -91,7 +93,7 @@ public static String labelFromName(String name) if (name == null) return null; - if (name.length() == 0) + if (name.isEmpty()) return name; StringBuilder buf = new StringBuilder(name.length() + 10); @@ -121,6 +123,11 @@ else if (Character.isUpperCase(c) && Character.isLowerCase(chars[i - 1])) return buf.toString(); } + public String getEffectiveLabel() + { + return Objects.requireNonNullElseGet(getLabel(), () -> labelFromName(getName())); + } + @Override public Map getAllProperties() { diff --git a/src/org/labkey/test/util/DeferredErrorCollector.java b/src/org/labkey/test/util/DeferredErrorCollector.java index 1fc34cc028..67fde8d6dc 100644 --- a/src/org/labkey/test/util/DeferredErrorCollector.java +++ b/src/org/labkey/test/util/DeferredErrorCollector.java @@ -1,6 +1,8 @@ package org.labkey.test.util; import org.apache.commons.lang3.StringUtils; +import org.assertj.core.api.Assertions; +import org.assertj.core.api.ObjectAssert; import org.awaitility.core.ConditionTimeoutException; import org.hamcrest.Matcher; import org.hamcrest.MatcherAssert; @@ -15,6 +17,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.function.Consumer; import static org.awaitility.Awaitility.await; diff --git a/src/org/labkey/test/util/TestDataUtils.java b/src/org/labkey/test/util/TestDataUtils.java index 0cae145635..5bbad1a8dd 100644 --- a/src/org/labkey/test/util/TestDataUtils.java +++ b/src/org/labkey/test/util/TestDataUtils.java @@ -13,6 +13,7 @@ import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; +import java.io.StringWriter; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; @@ -150,13 +151,13 @@ public static List> rowMapsFromCsv(String tsvString) throws public static String tsvStringFromRowMaps(List> rowMaps, List columns, boolean includeHeaders) { - return toTabular(rowMaps, columns, '\t', includeHeaders); + return writeRowsToString(rowListsFromMaps(rowMaps, columns, includeHeaders, true), CSVFormat.TDF); } public static String csvStringFromRowMaps(List> rowMaps, List columns, boolean includeHeaders) { - return toTabular(rowMaps, columns, ',', includeHeaders); + return writeRowsToString(rowListsFromMaps(rowMaps, columns, includeHeaders, true), CSVFormat.DEFAULT); } @@ -170,7 +171,6 @@ public static List> rowListsFromMaps(List> rowM * @param rowMaps Source data * @param columns keys contained in each map, will copy values associated with them to the resulting list * @return A List> containing values - * @throws IOException */ public static List> rowListsFromMaps(List> rowMaps, List columns, boolean includeHeaders, boolean preserveEmptyValues) { @@ -178,80 +178,63 @@ public static List> rowListsFromMaps(List> rowM if (includeHeaders) { - List headers = new ArrayList<>(); - for(String col : columns) - headers.add(col); + List headers = new ArrayList<>(columns); lists.add(headers); } - for (int i=0; i rowMap : rowMaps) { List rowList = new ArrayList<>(); - var rowMap = rowMaps.get(i); - for(String column : columns) + for (String column : columns) { - var value = (String) rowMap.get(column); - if (value == null && preserveEmptyValues) - rowList.add(""); + var value = rowMap.getOrDefault(column, preserveEmptyValues ? "" : null); + if (value == null) + throw new IllegalArgumentException("Missing value for column '" + column + "' in row: " + rowMap); else - rowList.add(value); + rowList.add(value.toString()); } lists.add(rowList); } return lists; } - /** - * Convert a list of Map> to tabluar (tsv, csv) format - * (assumes the rowMaps all share the same keyset/schema) - * can be used to generate edit-grid paste data, if delimiter is \t and includeHeaders is false - * - * @param rowMaps data to be written into tabular format - * @param columns the fields (in order) from the rowMaps to include in tabular output - * @param delimiter comma [,] for csv tab [\t] for tsv - * @param includeHeaders whether to write the keys as column names on the first line of the output string - * @return - */ - private static String toTabular(List> rowMaps, List columns, - char delimiter, boolean includeHeaders) + public static File writeRowsToTsv(String fileName, List> rows) throws IOException { - StringBuilder builder = new StringBuilder(); - TsvQuoter q = new TsvQuoter(delimiter); - - if (includeHeaders) - { - builder.append(String.join(String.valueOf(delimiter), columns.stream().map(q::quoteValue).toList())); - builder.append("\n"); - } + File file = new File(TestFileUtils.getTestTempDir(), fileName); + FileUtils.forceMkdirParent(file); - for (Map row : rowMaps) - { - List values = new ArrayList<>(); - for (String name : columns) + try (CSVPrinter printer = new CSVPrinter(new FileWriter(file, StandardCharsets.UTF_8), CSVFormat.TDF)) { + for (List row : rows) { - String value = q.quoteValue(row.get(name)); - values.add(value); + printer.printRecord(row); } - builder.append(String.join(String.valueOf(delimiter), values)); - builder.append("\n"); } - return builder.toString(); + + return file; } - public static File writeRowsToTsv(String fileName, List> rows) throws IOException + public static String writeRowsToTsvString(List> rows) throws IOException { - File file = new File(TestFileUtils.getTestTempDir(), fileName); - FileUtils.forceMkdirParent(file); + return writeRowsToString(rows, CSVFormat.TDF); + } - try (CSVPrinter printer = new CSVPrinter(new FileWriter(file, StandardCharsets.UTF_8), CSVFormat.TDF)) { + public static String writeRowsToString(List> rows, CSVFormat format) + { + StringWriter stringWriter = new StringWriter(); + + try (CSVPrinter printer = new CSVPrinter(stringWriter, format)) { for (List row : rows) { printer.printRecord(row); } } + catch (IOException e) + { + throw new RuntimeException(e); + } - return file; + return stringWriter.toString(); } /** From a4de83ecb5fe95c88a4e6a3d8c7dd66d9f1b6931 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Wed, 30 Apr 2025 17:26:16 -0700 Subject: [PATCH 18/21] Fix field key decoding --- .../ui/entities/EntityBulkUpdateDialog.java | 1 + src/org/labkey/test/util/EscapeUtil.java | 25 ++++++------------- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java b/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java index 9173097570..f71f51469f 100644 --- a/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java +++ b/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java @@ -44,6 +44,7 @@ public EntityBulkUpdateDialog(WebDriver driver, UpdatingComponent updatingCompon { super(new ModalDialogFinder(driver).withTitle("Update ")); _updatingComponent = updatingComponent; + getWrapper().mouseOver(elementCache().title); // avoid accidentally triggering tooltips } /** diff --git a/src/org/labkey/test/util/EscapeUtil.java b/src/org/labkey/test/util/EscapeUtil.java index 9e053a709e..82a67be918 100644 --- a/src/org/labkey/test/util/EscapeUtil.java +++ b/src/org/labkey/test/util/EscapeUtil.java @@ -119,28 +119,17 @@ public static String decodeUriPath(String path) return URIUtil.decodePath(path); } - public static String fieldKeyEncodePart(String str) + private static final String[] ILLEGAL = {"$", "/", "&", "}", "~", ",", "."}; + private static final String[] REPLACEMENT = {"$D", "$S", "$A", "$B", "$T", "$C", "$P"}; + + static public String fieldKeyEncodePart(String str) { - str = StringUtils.replace(str, "$", "$D"); - str = StringUtils.replace(str, "/", "$S"); - str = StringUtils.replace(str, "&", "$A"); - str = StringUtils.replace(str, "}", "$B"); - str = StringUtils.replace(str, "~", "$T"); - str = StringUtils.replace(str, ",", "$C"); - str = StringUtils.replace(str, ".", "$P"); - return str; + return StringUtils.replaceEach(str, ILLEGAL, REPLACEMENT); } - public static String fieldKeyDecodePart(String str) + static public String fieldKeyDecodePart(String str) { - str = StringUtils.replace(str, "$C", ","); - str = StringUtils.replace(str, "$T", "~"); - str = StringUtils.replace(str, "$B", "}"); - str = StringUtils.replace(str, "$A", "&"); - str = StringUtils.replace(str, "$S", "/"); - str = StringUtils.replace(str, "$D", "$"); - str = StringUtils.replace(str, "$P", "."); - return str; + return StringUtils.replaceEach(str, REPLACEMENT, ILLEGAL); } public static String getTextChoiceValidatorExpression(List options) From e274be602421f944684897a89af624fdc2578e4f Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Thu, 1 May 2025 10:27:25 -0700 Subject: [PATCH 19/21] Last couple of errors --- src/org/labkey/test/util/TestDataUtils.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/org/labkey/test/util/TestDataUtils.java b/src/org/labkey/test/util/TestDataUtils.java index 5bbad1a8dd..c6e9886e3e 100644 --- a/src/org/labkey/test/util/TestDataUtils.java +++ b/src/org/labkey/test/util/TestDataUtils.java @@ -188,11 +188,15 @@ public static List> rowListsFromMaps(List> rowM List rowList = new ArrayList<>(); for (String column : columns) { - var value = rowMap.getOrDefault(column, preserveEmptyValues ? "" : null); + var value = rowMap.get(column); if (value == null) - throw new IllegalArgumentException("Missing value for column '" + column + "' in row: " + rowMap); - else - rowList.add(value.toString()); + { + if (preserveEmptyValues) + value = ""; + else + throw new IllegalArgumentException("Missing value for column '" + column + "' in row: " + rowMap); + } + rowList.add(value.toString()); } lists.add(rowList); } From cfd0af57e2b8bfc864db1c4c9dd88388581f815b Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Thu, 1 May 2025 16:41:56 -0700 Subject: [PATCH 20/21] Fix a couple of intermittent failures while I'm in here --- .../test/components/ui/grids/TabbedGridPanel.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/org/labkey/test/components/ui/grids/TabbedGridPanel.java b/src/org/labkey/test/components/ui/grids/TabbedGridPanel.java index 8c24c3f042..beb2d0cbe6 100644 --- a/src/org/labkey/test/components/ui/grids/TabbedGridPanel.java +++ b/src/org/labkey/test/components/ui/grids/TabbedGridPanel.java @@ -9,6 +9,7 @@ import org.openqa.selenium.WebElement; import org.openqa.selenium.support.ui.ExpectedConditions; +import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -47,7 +48,15 @@ public WebDriver getDriver() public List getTabs() { - return getWrapper().getTexts(elementCache().navTabs()); + List tabElements = elementCache().navTabs(); + List tabLabels = new ArrayList<>(); + //noinspection ResultOfMethodCallIgnored + WebDriverWrapper.waitFor(() -> { + tabLabels.clear(); + tabLabels.addAll(tabElements.stream().map(WebElement::getText).toList()); + return tabLabels.stream().noneMatch(String::isBlank); + }, 5_000); + return tabLabels; } public List getTabsWithoutCounts() From eb638125e7f4412e5fbd4eb1cee1afcbce01eb7d Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Fri, 2 May 2025 08:31:59 -0700 Subject: [PATCH 21/21] Remove some accidental changes --- src/org/labkey/test/util/DeferredErrorCollector.java | 3 --- src/org/labkey/test/util/TestDataGenerator.java | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/org/labkey/test/util/DeferredErrorCollector.java b/src/org/labkey/test/util/DeferredErrorCollector.java index 67fde8d6dc..1fc34cc028 100644 --- a/src/org/labkey/test/util/DeferredErrorCollector.java +++ b/src/org/labkey/test/util/DeferredErrorCollector.java @@ -1,8 +1,6 @@ package org.labkey.test.util; import org.apache.commons.lang3.StringUtils; -import org.assertj.core.api.Assertions; -import org.assertj.core.api.ObjectAssert; import org.awaitility.core.ConditionTimeoutException; import org.hamcrest.Matcher; import org.hamcrest.MatcherAssert; @@ -17,7 +15,6 @@ import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.function.Consumer; import static org.awaitility.Awaitility.await; diff --git a/src/org/labkey/test/util/TestDataGenerator.java b/src/org/labkey/test/util/TestDataGenerator.java index 4aa3a19169..b624fa2555 100644 --- a/src/org/labkey/test/util/TestDataGenerator.java +++ b/src/org/labkey/test/util/TestDataGenerator.java @@ -581,7 +581,7 @@ public static String randomFieldName(String part) public static String randomFieldName(String part, @Nullable String exclusion) { - return randomFieldName(part, randomInt(0, 5), randomInt(1, 5), exclusion); + return randomFieldName(part, randomInt(0, 5), randomInt(0, 5), exclusion); } public static String randomFieldName(String part, int numStartChars, int numEndChars)