From 3f07f6630c4fa5b2aa496b4f32c029bb77fbb181 Mon Sep 17 00:00:00 2001 From: Trey Chadick Date: Mon, 4 Aug 2025 11:18:03 -0700 Subject: [PATCH 01/17] Switch back to main window after system maintenance (#2598) --- .../test/tests/filecontent/FileContentUploadTest.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/org/labkey/test/tests/filecontent/FileContentUploadTest.java b/src/org/labkey/test/tests/filecontent/FileContentUploadTest.java index e15f49c784..09c50d4fa3 100644 --- a/src/org/labkey/test/tests/filecontent/FileContentUploadTest.java +++ b/src/org/labkey/test/tests/filecontent/FileContentUploadTest.java @@ -353,8 +353,11 @@ public void testCalculateFileRootSize() throws Exception { String calculateFileRootSizeTask = "Calculate file root sizes"; goToAdminConsole().clickSystemMaintenance().runMaintenanceTask(calculateFileRootSizeTask); + Integer initialFileRootSize = getFileRootSize(); + switchToMainWindow(); + goToProjectHome(); File testFile = TestFileUtils.getSampleData("fileTypes/tsv_sample.tsv"); @@ -363,6 +366,9 @@ public void testCalculateFileRootSize() throws Exception goToAdminConsole().clickSystemMaintenance().runMaintenanceTask(calculateFileRootSizeTask); Integer finalFileRootSize = getFileRootSize(); + + switchToMainWindow(); + if (!checker().wrapAssertion(() -> Assertions.assertThat(finalFileRootSize) .as("Crawled file root size").isGreaterThan(initialFileRootSize))) { From c29e28871fd863ada4b86fb34e14e18e4207385d Mon Sep 17 00:00:00 2001 From: Xing Yang <5168106+XingY@users.noreply.github.com> Date: Mon, 4 Aug 2025 12:16:05 -0700 Subject: [PATCH 02/17] Add selenium test for importing attachment as string values (#2597) --- .../test/tests/InlineImagesListTest.java | 59 ++++++++++++++++++- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/src/org/labkey/test/tests/InlineImagesListTest.java b/src/org/labkey/test/tests/InlineImagesListTest.java index 05c82e15d2..056a9b6d85 100644 --- a/src/org/labkey/test/tests/InlineImagesListTest.java +++ b/src/org/labkey/test/tests/InlineImagesListTest.java @@ -28,6 +28,9 @@ import org.labkey.test.TestTimeoutException; import org.labkey.test.WebTestHelper; import org.labkey.test.categories.Daily; +import org.labkey.test.components.list.ManageListsGrid; +import org.labkey.test.pages.ImportDataPage; +import org.labkey.test.pages.list.BeginPage; import org.labkey.test.pages.list.EditListDefinitionPage; import org.labkey.test.params.FieldDefinition; import org.labkey.test.params.FieldDefinition.ColumnType; @@ -35,6 +38,7 @@ import org.labkey.test.util.DataRegionTable; import org.labkey.test.util.ExcelHelper; import org.labkey.test.util.TestDataGenerator; +import org.labkey.test.util.data.TestDataUtils; import org.openqa.selenium.By; import org.openqa.selenium.support.ui.ExpectedConditions; @@ -44,6 +48,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -52,6 +57,9 @@ @BaseWebDriverTest.ClassTimeout(minutes = 5) public class InlineImagesListTest extends BaseWebDriverTest { + private final static String PROJECT_NAME = "Inline Images List Test Project"; + private final static String IMPORT_PROJECT_NAME = "Inline Images List Import Test Project"; + protected final static String LIST_NAME = TestDataGenerator.randomDomainName("InlineImagesList"); protected final static String LIST_KEY_NAME = TestDataGenerator.randomFieldName("Key"); protected final static ColumnType LIST_KEY_TYPE = ColumnType.Integer; @@ -99,7 +107,7 @@ public List getAssociatedModules() @Override protected String getProjectName() { - return "InlineImagesListTestProject"; + return PROJECT_NAME; } @Override @@ -111,7 +119,8 @@ protected BrowserType bestBrowser() @Override public void doCleanup(boolean afterTest) throws TestTimeoutException { - _containerHelper.deleteProject(getProjectName(), afterTest); + _containerHelper.deleteProject(PROJECT_NAME, afterTest); + _containerHelper.deleteProject(IMPORT_PROJECT_NAME, afterTest); } @Before @@ -327,5 +336,51 @@ public final void testList() throws Exception assertTrue("Height of row 2 not in expected range (" + ROW_HEIGHT_SMALL_LBOUND + " to " + ROW_HEIGHT_SMALL_UBOUND + "). Actual height: " + sheet.getRow(2).getHeight(), (sheet.getRow(2).getHeight() > ROW_HEIGHT_SMALL_LBOUND) && (sheet.getRow(2).getHeight() < ROW_HEIGHT_SMALL_UBOUND)); assertTrue("Height of row 3 not in expected range (" + ROW_HEIGHT_TEXT_LBOUND + " to " + ROW_HEIGHT_TEXT_UBOUND + "). Actual height: " + sheet.getRow(3).getHeight(), (sheet.getRow(3).getHeight() > ROW_HEIGHT_TEXT_LBOUND) && (sheet.getRow(3).getHeight() < ROW_HEIGHT_TEXT_UBOUND)); } + + log("Verify list archive export/import can round trip attachment successfully"); + ManageListsGrid listsGrid = BeginPage.beginAt(this, getProjectName()).getGrid(); + listsGrid.checkAllOnPage(); + File listArchive = listsGrid.exportSelectedLists(); + + _containerHelper.createProject(IMPORT_PROJECT_NAME); + + BeginPage.beginAt(this, IMPORT_PROJECT_NAME).importListArchive(listArchive); + _listHelper.goToList(LIST_NAME); + + // Validate that list is imported as expected with attachments + assertElementPresent("Did not find the expected number of icons for images for " + LRG_PNG_FILE.getName(), Locator.xpath("//img[contains(@title, '" + LRG_PNG_FILE.getName() + "')]"), 1); + assertElementPresent("Did not find the expected number of icons for images for " + JPG01_FILE.getName(), Locator.xpath("//img[contains(@title, '" + JPG01_FILE.getName() + "')]"), 1); + assertElementPresent("Did not find the expected number of icons for images for " + PDF_FILE.getName(), Locator.xpath("//img[contains(@src, 'pdf.gif')]"), 1); + assertElementPresent("Did not find the expected text for " + PDF_FILE.getName(), Locator.xpath("//a[contains(text(), '" + PDF_FILE.getName() + "')]"), 1); + + // Issue 53498: reject string attachment values for import + ImportDataPage listImportPage = _listHelper.clickImportData(); + importFilePathError(listImportPage, "5", "absent.txt"); + importFilePathError(listImportPage, "5", PDF_FILE.getName()); + listImportPage.setCopyPasteMerge(false, true); + importFilePathError(listImportPage, "1", "absent.txt"); + importFilePathError(listImportPage, "1", PDF_FILE.getName()); + listImportPage.setCopyPasteMerge(true, true); + importFilePathError(listImportPage, "1", "absent.txt"); + importFilePathError(listImportPage, "1", PDF_FILE.getName()); + importFilePathError(listImportPage, "5", PDF_FILE.getName()); + } + + private void importFilePathError(ImportDataPage listImportPage, String key, String attachmentValue) + { + String pasteData = TestDataUtils.tsvStringFromRowMaps(List.of(Map.of(LIST_KEY_NAME, key, LIST_ATTACHMENT01_NAME, attachmentValue)), + List.of(LIST_KEY_NAME, LIST_ATTACHMENT01_NAME), true); + log(pasteData); + listImportPage.setText(pasteData); + listImportPage.submitExpectingError(); + try + { + String expectedError = "Row 1: Can't upload '" + attachmentValue + "' to field " + LIST_ATTACHMENT01_NAME + " with type Attachment."; + checker().withScreenshot("import_error").verifyTrue("Invalid attachment error not as expected", isElementPresent(Locator.tagWithClass("div", "labkey-error").withText(expectedError))); + } + catch(NoSuchElementException nse) + { + checker().error("Invalid attachment error not present."); + } } } From 637dc78d0a6a21d5b2a49e6898997796ab4cfcc8 Mon Sep 17 00:00:00 2001 From: Trey Chadick Date: Mon, 4 Aug 2025 16:53:19 -0700 Subject: [PATCH 03/17] Add more informative errors for null ElementCache (#2600) --- src/org/labkey/test/components/Component.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/org/labkey/test/components/Component.java b/src/org/labkey/test/components/Component.java index 121cf23249..386ed6f99a 100644 --- a/src/org/labkey/test/components/Component.java +++ b/src/org/labkey/test/components/Component.java @@ -16,6 +16,7 @@ package org.labkey.test.components; import org.apache.commons.lang3.NotImplementedException; +import org.jetbrains.annotations.NotNull; import org.labkey.test.Locator; import org.labkey.test.selenium.RefindingWebElement; import org.labkey.test.util.TestLogger; @@ -27,6 +28,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.function.Function; @@ -43,13 +45,13 @@ public String toString() } @Override - public WebElement findElement(By by) + public @NotNull WebElement findElement(@NotNull By by) { return getComponentElement().findElement(by); } @Override - public List findElements(By by) + public @NotNull List findElements(@NotNull By by) { return getComponentElement().findElements(by); } @@ -69,8 +71,9 @@ protected EC elementCache() // Pass if element doesn't exist. Might be checking if component is visible. } - _elementCache = newElementCache(); + _elementCache = Objects.requireNonNull(newElementCache()); waitForReady(); + Objects.requireNonNull(_elementCache, "waitForReady() cleared the element cache"); } return _elementCache; } @@ -103,13 +106,13 @@ protected ElementCache() } @Override - public List findElements(By by) + public @NotNull List findElements(@NotNull By by) { return getComponentElement().findElements(by); } @Override - public WebElement findElement(By by) + public @NotNull WebElement findElement(@NotNull By by) { return getComponentElement().findElement(by); } From ace1198a46f0ad6856981dae988be4e3151937da Mon Sep 17 00:00:00 2001 From: Trey Chadick Date: Wed, 6 Aug 2025 17:10:30 -0700 Subject: [PATCH 04/17] Update EditableGrid and EditableGridTest to handle long field names (#2605) --- .../components/ui/grids/EditableGrid.java | 70 +++++++------------ .../tests/component/EditableGridTest.java | 13 ++-- 2 files changed, 32 insertions(+), 51 deletions(-) diff --git a/src/org/labkey/test/components/ui/grids/EditableGrid.java b/src/org/labkey/test/components/ui/grids/EditableGrid.java index aa4c8633d8..5f8dea57da 100644 --- a/src/org/labkey/test/components/ui/grids/EditableGrid.java +++ b/src/org/labkey/test/components/ui/grids/EditableGrid.java @@ -18,6 +18,7 @@ import org.labkey.test.components.ui.grids.FieldReferenceManager.FieldReference; import org.labkey.test.params.FieldDefinition; import org.labkey.test.params.FieldKey; +import org.labkey.test.util.CachingSupplier; import org.labkey.test.util.selenium.ScrollUtils; import org.labkey.test.util.selenium.WebElementUtils; import org.openqa.selenium.By; @@ -151,55 +152,29 @@ public EditableGrid removeColumn(CharSequence columnIdentifier) private boolean hasSelectColumn() { - return elementCache().selectColumn.isDisplayed(); + return elementCache().hasSelectColumn.get(); } public EditableGrid selectRow(int index, boolean checked) { - if (hasSelectColumn()) - { - WebElement checkBox = Locator.css("td > input[type=checkbox]").findElement(getRow(index)); - getWrapper().setCheckbox(checkBox, checked); - } - else - { - throw new NoSuchElementException("There is no select checkbox for row " + index); - } + elementCache().getCheckbox(index).set(checked); return this; } public boolean isRowSelected(int index) { - if (hasSelectColumn()) - { - WebElement checkBox = Locator.css("td > input[type=checkbox]").findElement(getRow(index)); - return checkBox.isSelected(); - } - else - { - throw new NoSuchElementException("There is no select checkbox for row " + index); - } + return elementCache().getCheckbox(index).isSelected(); } public EditableGrid selectAll(boolean checked) { - if (hasSelectColumn()) - { - getWrapper().setCheckbox(elementCache().selectColumn, checked); - } - else - { - throw new NoSuchElementException("There is no select checkbox for all rows."); - } + elementCache().selectAllCheckbox.set(checked); return this; } public boolean areAllRowsSelected() { - if (hasSelectColumn()) - return new Checkbox(elementCache().selectColumn).isSelected(); - else - throw new NoSuchElementException("There is no select checkbox for all rows."); + return elementCache().selectAllCheckbox.isSelected(); } /** @@ -212,14 +187,14 @@ public boolean areAllRowsSelected() public EditableGrid shiftSelectRange(int start, int end) { if (!hasSelectColumn()) - throw new NoSuchElementException("there is no select checkbox for all rows"); + throw new NoSuchElementException("there is no selection column for grid"); var checkBoxes = Locator.tag("tr").child("td") .child(Locator.tagWithAttribute("input", "type", "checkbox")) .findElements(elementCache().table); - getWrapper().scrollIntoView(checkBoxes.get(0), true); // bring as much of the grid into view as possible + checkBoxes.get(start).click(); + getWrapper().scrollIntoView(checkBoxes.get(end), true); // Actions.click() doesn't scroll new Actions(getDriver()) - .click(checkBoxes.get(start)) .keyDown(Keys.SHIFT) .click(checkBoxes.get(end)) .keyUp(Keys.SHIFT) @@ -227,11 +202,6 @@ public EditableGrid shiftSelectRange(int start, int end) return this; } - private List getRows() - { - return Locators.rows.findElements(elementCache().table); - } - /** * @param columnIdentifiers fieldKeys, names, or labels of columns * @return grid data for the specified columns, keyed by column label @@ -291,7 +261,7 @@ private List> getGridData(Function cells = row.findElements(By.tagName("td")); Map rowMap = new LinkedHashMap<>(includedColHeaders.size()); @@ -337,7 +307,7 @@ public List getColumnData(CharSequence columnIdentifier) private WebElement getRow(int index) { - return getRows().get(index); + return elementCache().getRows().get(index); } /** @@ -387,7 +357,7 @@ public boolean isCellReadOnly(int row, CharSequence columnIdentifier) public int getRowCount() { - return getRows().size(); + return elementCache().getRows().size(); } /** @@ -657,6 +627,7 @@ public WebElement activateCellUsingDoubleClick(int row, CharSequence columnIdent // Account for the cell already being active. if(!textArea.isDisplayed()) { + getWrapper().scrollIntoView(gridCell); getWrapper().doubleClick(gridCell); waitFor(textArea::isDisplayed, String.format("Table cell for row %d and column '%s' was not activated.", row, columnIdentifier), 1_000); @@ -1086,7 +1057,7 @@ private boolean areAllInSelection() List columns = getColumnLabels(); int selectIndexOffset = hasSelectColumn() ? 1 : 0; WebElement indexCell = getCell(0, columns.get(1 + selectIndexOffset)); - WebElement endCell = getCell(getRows().size()-1, columns.get(columns.size()-1)); + WebElement endCell = getCell(elementCache().getRows().size()-1, columns.get(columns.size()-1)); return (isInSelection(indexCell) && isInSelection(endCell)); } @@ -1219,7 +1190,13 @@ protected class ElementCache extends Component.ElementCache final WebElement deleteRowsBtn = Locator.byClass("bulk-remove-button").findWhenNeeded(topControls); final ExportMenu exportMenu = ExportMenu.finder(getDriver()).findWhenNeeded(topControls); final WebElement table = Locator.byClass("table-cellular").findWhenNeeded(this); - private final WebElement selectColumn = Locator.xpath("//th/input[@type='checkbox']").findWhenNeeded(table); + private final Checkbox selectAllCheckbox = new Checkbox(Locator.xpath("//th/input[@type='checkbox']").findWhenNeeded(table)); + private final CachingSupplier hasSelectColumn = new CachingSupplier<>(selectAllCheckbox::isDisplayed); + + Checkbox getCheckbox(int rowIndex) + { + return new Checkbox(Locator.css("td > input[type=checkbox]").findElement(getRow(rowIndex))); + } protected WebElement getColumnHeaderCell(CharSequence columnIdentifier) { @@ -1292,6 +1269,11 @@ public ReactDateTimePicker datePicker() final WebElement addRowsPanel = Locator.byClass("editable-grid__controls").findWhenNeeded(this); final Input addCountInput = Input.Input(Locator.name("addCount"), getDriver()).findWhenNeeded(addRowsPanel); final WebElement addRowsButton = Locator.byClass("btn-primary").findWhenNeeded(addRowsPanel); + + List getRows() + { + return Locators.rows.findElements(table); + } } protected abstract static class Locators diff --git a/src/org/labkey/test/tests/component/EditableGridTest.java b/src/org/labkey/test/tests/component/EditableGridTest.java index ac04dc7f9e..391ef10d67 100644 --- a/src/org/labkey/test/tests/component/EditableGridTest.java +++ b/src/org/labkey/test/tests/component/EditableGridTest.java @@ -36,6 +36,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.stream.Collectors; import static org.hamcrest.CoreMatchers.everyItem; @@ -1102,7 +1103,7 @@ public void testShiftArrowSelectHorizontal() checker().verifyTrue(String.format("The expected cell on row %d and column %s is not selected after hitting .", gridRow, PASTE_2), - endCell.getAttribute("class").toLowerCase().contains("cell-selected")); + Objects.requireNonNullElse(endCell.getAttribute("class"), "").toLowerCase().contains("cell-selected")); checker().screenShotIfNewError("TAB_ERROR"); } @@ -1252,8 +1253,8 @@ private void checkSelectedStyle(EditableGrid editableGrid, for(int rowIndex = startRow; rowIndex <= endRow; rowIndex++) { WebElement gridCell = Locator.tag("div").findElement(editableGrid.getCell(rowIndex, columnNames.get(colIndex))); - checker().verifyTrue(String.format("Cell (%s, %d) is not selected.",columnNames.get(colIndex), rowIndex), - gridCell.getAttribute("class").toLowerCase().contains("cell-selection")); + checker().verifyTrue(String.format("Cell (%s, %d) is not selected.", columnNames.get(colIndex), rowIndex), + Objects.requireNonNullElse(gridCell.getAttribute("class"), "").toLowerCase().contains("cell-selection")); } } @@ -1398,9 +1399,8 @@ public void testPasteCellValidation() testGrid.addRows(3); log("Pasting invalid values"); - testGrid.selectCell(0, STR_FIELD); + testGrid.pasteFromCell(0, STR_FIELD, rowsToString(clipRows), false); - actionPaste(null, rowsToString(clipRows)); List> expectedCellWarnings = List.of( Arrays.asList(null, null, null, null, null, null, null, null, null, null, null, null, null, null), Arrays.asList(null, REQ_STR_FIELD.getLabel() + " is required.", null, REQ_INT_FIELD.getLabel() + " is required.", null, REQ_DATETIME_FIELD.getLabel() + " is required.", null, REQ_TIME_FIELD.getLabel() + " is required.", null, null, null, REQ_TEXTCHOICE_FIELD.getLabel() + " is required.", null, REQ_LOOKUP_FIELD.getLabel() + " is required."), @@ -1511,8 +1511,7 @@ public void testFillCellValidation() testGrid.addRows(3); log("Start with pasting invalid values, so we can fill down invalid values for dropdowns and data/time inputs"); - testGrid.selectCell(0, STR_FIELD); - actionPaste(null, rowsToString(clipRows)); + testGrid.pasteFromCell(0, STR_FIELD, rowsToString(clipRows), false); // Scroll one column to the right into view, this will help ensure the REQ_LOOKUP_FIELD is within the viewport. var index = testGrid.getColumnLabels().indexOf(REQ_LOOKUP_FIELD.getLabel() + " *") + 1; From 8a64dacf29aee88895932a54a22a76ee2a81d01e Mon Sep 17 00:00:00 2001 From: Trey Chadick Date: Wed, 6 Aug 2025 17:11:04 -0700 Subject: [PATCH 05/17] Handle double spaces in some error messages (#2607) - Fix `EditableGridTest` and `ListLookupTest` to handle adjacent spaces in field names - Add `FieldInfo.getUiLabel` - Make `FieldInfo._namePart` final --- src/org/labkey/test/params/FieldInfo.java | 39 +++++++++++-------- .../tests/component/EditableGridTest.java | 7 ++-- .../test/tests/list/ListLookupTest.java | 22 ++++++++--- src/org/labkey/test/util/TextUtils.java | 5 ++- 4 files changed, 47 insertions(+), 26 deletions(-) diff --git a/src/org/labkey/test/params/FieldInfo.java b/src/org/labkey/test/params/FieldInfo.java index f962aa8241..073eaa5ca4 100644 --- a/src/org/labkey/test/params/FieldInfo.java +++ b/src/org/labkey/test/params/FieldInfo.java @@ -3,8 +3,10 @@ import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.labkey.test.params.FieldDefinition.ColumnType; +import org.labkey.test.util.CachingSupplier; import org.labkey.test.util.EscapeUtil; import org.labkey.test.util.TestDataGenerator; +import org.labkey.test.util.TextUtils; import java.util.Objects; import java.util.function.Consumer; @@ -17,22 +19,25 @@ public class FieldInfo implements CharSequence, WrapsFieldKey { private final FieldKey _fieldKey; - private final String _label; + private final String _rawLabel; private final ColumnType _columnType; private final Consumer _fieldDefinitionMutator; - private String _namePart; // used for random field generation to track the name part used + private final String _namePart; // used for random field generation to track the name part used + private final CachingSupplier _label = new CachingSupplier<>(() -> Objects.requireNonNullElseGet(getRawLabel(), () -> FieldDefinition.labelFromName(getName()))); + private final CachingSupplier _uiLabel = new CachingSupplier<>(() -> TextUtils.normalizeSpace(getLabel())); - private FieldInfo(FieldKey fieldKey, String label, ColumnType columnType, Consumer fieldDefinitionMutator) + private FieldInfo(FieldKey fieldKey, String label, ColumnType columnType, Consumer fieldDefinitionMutator, String namePart) { _fieldKey = fieldKey; - _label = label; + _rawLabel = label; _columnType = Objects.requireNonNullElse(columnType, ColumnType.String); _fieldDefinitionMutator = fieldDefinitionMutator; + _namePart = namePart; } public FieldInfo(String name, String label, ColumnType columnType) { - this(FieldKey.fromParts(name.trim()), label, columnType, null); + this(FieldKey.fromParts(name.trim()), label, columnType, null, name); } public FieldInfo(String name, String label) @@ -55,9 +60,7 @@ public FieldInfo(String name) */ public static FieldInfo random(String namePart, ColumnType columnType) { - FieldInfo field = new FieldInfo(TestDataGenerator.randomFieldName(namePart), columnType); - field.setNamePart(namePart); - return field; + return new FieldInfo(FieldKey.fromParts(TestDataGenerator.randomFieldName(namePart)), null, columnType, null, namePart); } /** @@ -82,19 +85,28 @@ public FieldInfo customizeFieldDefinition(Consumer fieldDefinit { throw new IllegalArgumentException("FieldDefinition customizer should not modify field label"); } - return new FieldInfo(_fieldKey, _label, _columnType, fieldDefinitionMutator); + return new FieldInfo(_fieldKey, _rawLabel, _columnType, fieldDefinitionMutator, _namePart); } @Contract(pure = true) protected String getRawLabel() { - return _label; + return _rawLabel; } @Contract(pure = true) public String getLabel() { - return Objects.requireNonNullElseGet(getRawLabel(), () -> FieldDefinition.labelFromName(_fieldKey.getName())); + return _label.get(); + } + + /** + * Get field label as it appears when rendered in browser + */ + @Contract(pure = true) + public String getUiLabel() + { + return _uiLabel.get(); } @Override @@ -175,11 +187,6 @@ private FieldDefinition getFieldDefinition(ColumnType columnType) return fieldDefinition; } - private void setNamePart(String namePart) - { - _namePart = namePart; - } - @Override public int length() { diff --git a/src/org/labkey/test/tests/component/EditableGridTest.java b/src/org/labkey/test/tests/component/EditableGridTest.java index 92a7cc60fb..d5cd5b240e 100644 --- a/src/org/labkey/test/tests/component/EditableGridTest.java +++ b/src/org/labkey/test/tests/component/EditableGridTest.java @@ -21,6 +21,7 @@ import org.labkey.test.params.experiment.SampleTypeDefinition; import org.labkey.test.params.list.IntListDefinition; import org.labkey.test.params.list.ListDefinition; +import org.labkey.test.util.TextUtils; import org.openqa.selenium.Dimension; import org.openqa.selenium.Keys; import org.openqa.selenium.WebElement; @@ -1341,11 +1342,11 @@ public void testInputCellValidation() log("Input empty string for required field should trigger cell warning."); testGrid.setCellValue(1, REQ_STR_FIELD, " "); checker().verifyEquals("Cell warning status not as expected at row " + 1 + " for col " + REQ_STR_FIELD.getLabel(), true, testGrid.hasCellError(1, REQ_STR_FIELD)); - checker().verifyEquals("Cell warning msg not as expected at row " + 1 + " for col " + REQ_STR_FIELD.getLabel(), REQ_STR_FIELD.getLabel() + " is required.", testGrid.getCellPopoverText(1, REQ_STR_FIELD)); + checker().verifyEquals("Cell warning msg not as expected at row " + 1 + " for col " + REQ_STR_FIELD.getLabel(), REQ_STR_FIELD.getUiLabel() + " is required.", testGrid.getCellPopoverText(1, REQ_STR_FIELD)); mouseOver(testGrid.getCell(0, "Row")); // dismiss warning popup testGrid.setCellValue(1, REQ_INT_FIELD, " "); checker().verifyEquals("Cell warning status not as expected at row " + 1 + " for col " + REQ_INT_FIELD.getLabel(), true, testGrid.hasCellError(1, REQ_INT_FIELD)); - checker().verifyEquals("Cell warning msg not as expected at row " + 1 + " for col " + REQ_INT_FIELD.getLabel(), REQ_INT_FIELD.getLabel() + " is required.", testGrid.getCellPopoverText(1, REQ_INT_FIELD)); + checker().verifyEquals("Cell warning msg not as expected at row " + 1 + " for col " + REQ_INT_FIELD.getLabel(), REQ_INT_FIELD.getUiLabel() + " is required.", testGrid.getCellPopoverText(1, REQ_INT_FIELD)); log("Correct values should remove cell warning, keep entering wrong values should update warning"); mouseOver(testGrid.getCell(0, "Row")); // dismiss warning popup @@ -1496,7 +1497,7 @@ private void verifyCellWarning(EditableGrid testGrid, List expectedWarni checker().verifyEquals("Cell warning status not as expected at row " + rowId + " for col " + field.getLabel(), !StringUtils.isEmpty(expectedWarning), testGrid.hasCellError(rowId, field)); if (!StringUtils.isEmpty(expectedWarning)) - checker().verifyEquals("Cell warning msg not as expected at row " + rowId + " for col " + field.getLabel(), expectedWarning, testGrid.getCellPopoverText(rowId, field)); + checker().verifyEquals("Cell warning msg not as expected at row " + rowId + " for col " + field.getLabel(), TextUtils.normalizeSpace(expectedWarning), testGrid.getCellPopoverText(rowId, field)); } } diff --git a/src/org/labkey/test/tests/list/ListLookupTest.java b/src/org/labkey/test/tests/list/ListLookupTest.java index 056a1a0115..1e36e7b33e 100644 --- a/src/org/labkey/test/tests/list/ListLookupTest.java +++ b/src/org/labkey/test/tests/list/ListLookupTest.java @@ -26,6 +26,8 @@ import java.util.Map; import static org.junit.Assert.assertEquals; +import static org.labkey.test.params.FieldDefinition.labelFromName; +import static org.labkey.test.util.TextUtils.normalizeSpace; // Issue 52098, Issue 49422 @Category({Daily.class, Data.class, Hosting.class}) @@ -122,7 +124,8 @@ public void testWithoutValidatorOrAlternateKeys() throws IOException, CommandExc String error = importDataPage .setText(bulkData) .submitExpectingError(); - checker().withScreenshot().verifyEquals("Error message for invalid primary key not as expected", "Could not convert value 'noneSuch' (String) for Integer field '" + lookFromLookupFieldName + "'", error); + checker().withScreenshot().verifyEquals("Error message for invalid primary key not as expected", + "Could not convert value 'noneSuch' (String) for Integer field '" + normalizeSpace(lookFromLookupFieldName) + "'", error); } @Test @@ -168,7 +171,8 @@ public void testWithoutValidatorWithAlternateKeys() throws IOException, CommandE .setText(bulkData) .setImportLookupByAlternateKey(true) .submitExpectingError(); - checker().withScreenshot().verifyEquals("Error message after supplying invalid alternate key not as expected", "Value 'NotAValue' not found for field " + lookFromLookupFieldName + " in the current context.", error); + checker().withScreenshot().verifyEquals("Error message after supplying invalid alternate key not as expected", + "Value 'NotAValue' not found for field " + normalizeSpace(lookFromLookupFieldName) + " in the current context.", error); } @Test @@ -196,13 +200,16 @@ public void testWithLookupValidatorWithoutAlternateKeys() throws IOException, Co String error = importDataPage .setText(tsvFromColumn(List.of(lookFromLookupFieldName, "1000"))) .submitExpectingError(); - checker().withScreenshot().verifyEquals("Error message for invalid primary key value not as expected", "Value '1000' was not present in lookup target 'lists." + lookToListName + "' for field '" + FieldDefinition.labelFromName(lookFromLookupFieldName) + "'", error); + checker().withScreenshot().verifyEquals("Error message for invalid primary key value not as expected", + "Value '1000' was not present in lookup target 'lists." + normalizeSpace(lookToListName) + + "' for field '" + normalizeSpace(labelFromName(lookFromLookupFieldName)) + "'", error); log("With lookup validation on, import data and provide an invalid primary key of type string."); error = importDataPage .setText(tsvFromColumn(List.of(lookFromLookupFieldName, "Look"))) .submitExpectingError(); - checker().withScreenshot().verifyEquals("Error message for invalid primary key type not as expected", "Could not convert value 'Look' (String) for Integer field '" + lookFromLookupFieldName + "'", error); + checker().withScreenshot().verifyEquals("Error message for invalid primary key type not as expected", + "Could not convert value 'Look' (String) for Integer field '" + normalizeSpace(lookFromLookupFieldName) + "'", error); } @Test @@ -246,14 +253,17 @@ public void testWithLookupValidatorAndAlternateKeys() throws IOException, Comman .setText(bulkData) .setImportLookupByAlternateKey(true) .submitExpectingError(); - checker().withScreenshot().verifyEquals("Error message for invalid string alternate key not as expected", "Value 'Invalid' not found for field " + lookFromLookupFieldName + " in the current context.", error); + checker().withScreenshot().verifyEquals("Error message for invalid string alternate key not as expected", + "Value 'Invalid' not found for field " + normalizeSpace(lookFromLookupFieldName) + " in the current context.", error); bulkData = tsvFromColumn(List.of(lookFromLookupFieldName, "1234")); error = importDataPage .setText(bulkData) .setImportLookupByAlternateKey(true) .submitExpectingError(); - checker().withScreenshot().verifyEquals("Error message for invalid number-like alternate key not as expected", "Value '1234' was not present in lookup target 'lists." + lookToListName + "' for field '" + FieldDefinition.labelFromName(lookFromLookupFieldName) + "'", error); + checker().withScreenshot().verifyEquals("Error message for invalid number-like alternate key not as expected", + "Value '1234' was not present in lookup target 'lists." + normalizeSpace(lookToListName) + + "' for field '" + normalizeSpace(labelFromName(lookFromLookupFieldName)) + "'", error); } diff --git a/src/org/labkey/test/util/TextUtils.java b/src/org/labkey/test/util/TextUtils.java index 5b8a585cfd..97ec58700e 100644 --- a/src/org/labkey/test/util/TextUtils.java +++ b/src/org/labkey/test/util/TextUtils.java @@ -16,7 +16,10 @@ private TextUtils() {} */ public static String normalizeSpace(String value) { - return NS_PATTERN.matcher(value).replaceAll(" ").trim(); + if (value == null) + return value; + else + return NS_PATTERN.matcher(value).replaceAll(" ").trim(); } public static List normalizeSpace(List values) From eb13dfc971eb46572583bc3ef1da612585d314c4 Mon Sep 17 00:00:00 2001 From: Trey Chadick Date: Fri, 8 Aug 2025 08:06:04 -0700 Subject: [PATCH 06/17] More helpers for tricky characters (#2612) --- src/org/labkey/test/WebDriverWrapper.java | 10 ++++------ src/org/labkey/test/params/FieldInfo.java | 12 ++++++++++-- src/org/labkey/test/util/TextSearcher.java | 11 +++-------- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/org/labkey/test/WebDriverWrapper.java b/src/org/labkey/test/WebDriverWrapper.java index 28cca5bc2f..fe0077f074 100644 --- a/src/org/labkey/test/WebDriverWrapper.java +++ b/src/org/labkey/test/WebDriverWrapper.java @@ -1631,14 +1631,12 @@ public boolean isTextPresent(String... texts) if (htmlSource == null || !htmlSource.contains(text)) present.setFalse(); - return present.getValue(); + return present.get(); }; TextSearcher searcher = new TextSearcher(this); - searcher.setSearchTransformer(TextSearcher.TextTransformers.IDENTITY); - searcher.setSourceTransformer(TextSearcher.TextTransformers.IDENTITY); searcher.searchForTexts(handler, Arrays.asList(texts)); - return present.getValue(); + return present.get(); } public List getTextOrder(TextSearcher searcher, String... texts) @@ -1729,12 +1727,12 @@ public boolean isAnyTextPresent(String... texts) if (htmlSource.contains(text)) found.setTrue(); - return !found.getValue(); // stop searching if any value is found + return !found.get(); // stop searching if any value is found }; TextSearcher searcher = new TextSearcher(this); searcher.searchForTexts(handler, Arrays.asList(texts)); - return found.getValue(); + return found.get(); } /** diff --git a/src/org/labkey/test/params/FieldInfo.java b/src/org/labkey/test/params/FieldInfo.java index 073eaa5ca4..22077fbf95 100644 --- a/src/org/labkey/test/params/FieldInfo.java +++ b/src/org/labkey/test/params/FieldInfo.java @@ -24,7 +24,6 @@ public class FieldInfo implements CharSequence, WrapsFieldKey private final Consumer _fieldDefinitionMutator; private final String _namePart; // used for random field generation to track the name part used private final CachingSupplier _label = new CachingSupplier<>(() -> Objects.requireNonNullElseGet(getRawLabel(), () -> FieldDefinition.labelFromName(getName()))); - private final CachingSupplier _uiLabel = new CachingSupplier<>(() -> TextUtils.normalizeSpace(getLabel())); private FieldInfo(FieldKey fieldKey, String label, ColumnType columnType, Consumer fieldDefinitionMutator, String namePart) { @@ -106,7 +105,7 @@ public String getLabel() @Contract(pure = true) public String getUiLabel() { - return _uiLabel.get(); + return TextUtils.normalizeSpace(getLabel()); } @Override @@ -122,6 +121,15 @@ public String getName() return _fieldKey.getName(); } + /** + * Get column name quoted for use in queries and calculated field expressions + */ + @Contract(pure = true) + public String getSqlName() + { + return EscapeUtil.getSqlQuotedValue(_fieldKey.getName()); + } + /** * Get name escaped for use in sample or source name expressions */ diff --git a/src/org/labkey/test/util/TextSearcher.java b/src/org/labkey/test/util/TextSearcher.java index f52763f2e3..3ed4a79f06 100644 --- a/src/org/labkey/test/util/TextSearcher.java +++ b/src/org/labkey/test/util/TextSearcher.java @@ -22,6 +22,7 @@ import java.io.File; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.function.Function; import java.util.function.Supplier; @@ -59,10 +60,7 @@ public TextSearcher(final WebDriverWrapper test) public final TextSearcher setSourceTransformer(Function sourceTransformer) { - if (sourceTransformer == null) - this.sourceTransformer = TextTransformers.IDENTITY; - else - this.sourceTransformer = sourceTransformer; + this.sourceTransformer = Objects.requireNonNullElse(sourceTransformer, TextTransformers.IDENTITY); return this; } @@ -73,10 +71,7 @@ public final TextSearcher clearSourceTransformer() public final TextSearcher setSearchTransformer(Function searchTransformer) { - if (searchTransformer == null) - this.searchTransformer = TextTransformers.IDENTITY; - else - this.searchTransformer = searchTransformer; + this.searchTransformer = Objects.requireNonNullElse(searchTransformer, TextTransformers.IDENTITY); return this; } From 06681d77d386ebfa0ea51d25555808f84efdb63b Mon Sep 17 00:00:00 2001 From: Trey Chadick Date: Fri, 8 Aug 2025 17:11:01 -0700 Subject: [PATCH 07/17] Fix blocked clicks for editable grid operations (#2613) --- src/org/labkey/test/components/ui/Pager.java | 33 +++++++++++++------ .../components/ui/grids/EditableGrid.java | 3 +- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/org/labkey/test/components/ui/Pager.java b/src/org/labkey/test/components/ui/Pager.java index dd0740a53b..f364bed03a 100644 --- a/src/org/labkey/test/components/ui/Pager.java +++ b/src/org/labkey/test/components/ui/Pager.java @@ -63,28 +63,41 @@ public int getCurrentPage() // only works on GridPanel public Pager selectPageSize(String pageSize) // only works on GridPanel { - int currentPageSize = getPageSize(); - if(currentPageSize != Integer.parseInt(pageSize)) - { - _pagedComponent.doAndWaitForUpdate(() -> elementCache().jumpToDropdown.clickSubMenu(false, pageSize)); - } + pageSize(pageSize); return this; } public int getPageSize() // only works on GridPanel { - // Changing the jumpToDropdown button from the deprecated DropdownButtonGroup class to a MultiMenu type has changed - // the way that various text from the control is gathered. Getting the current page size now requires that the dropdown + return pageSize(null); + } + + /** + * Sets the page size if required (pageSize is specified and doesn't match the current page size) + * @return returns the initial page size + */ + private int pageSize(String pageSize) // only works on GridPanel + { + // Getting the current page size requires that the dropdown // be expanded and the selected page size found in the list. elementCache().jumpToDropdown.expand(); // Find the selected li element in the page size list WebElement activeLi = Locator.byClass("active").findElement(elementCache().jumpToDropdown); - int size = Integer.parseInt(activeLi.getText()); - elementCache().jumpToDropdown.collapse(); + int initialSize = Integer.parseInt(activeLi.getText()); + + if (pageSize != null && initialSize != Integer.parseInt(pageSize)) + { + _pagedComponent.doAndWaitForUpdate(() -> elementCache().jumpToDropdown.clickSubMenu(false, pageSize)); + } + else + { + // Tooltip sometimes blocks button. Click active option to dismiss menu. + activeLi.click(); + } - return size; + return initialSize; } /** diff --git a/src/org/labkey/test/components/ui/grids/EditableGrid.java b/src/org/labkey/test/components/ui/grids/EditableGrid.java index 5f8dea57da..db6ce743cf 100644 --- a/src/org/labkey/test/components/ui/grids/EditableGrid.java +++ b/src/org/labkey/test/components/ui/grids/EditableGrid.java @@ -192,8 +192,9 @@ public EditableGrid shiftSelectRange(int start, int end) var checkBoxes = Locator.tag("tr").child("td") .child(Locator.tagWithAttribute("input", "type", "checkbox")) .findElements(elementCache().table); + getWrapper().scrollIntoView(checkBoxes.get(start)); // Make sure the header isn't in the way checkBoxes.get(start).click(); - getWrapper().scrollIntoView(checkBoxes.get(end), true); // Actions.click() doesn't scroll + getWrapper().scrollIntoView(checkBoxes.get(end)); // Actions.click() doesn't scroll new Actions(getDriver()) .keyDown(Keys.SHIFT) .click(checkBoxes.get(end)) From 06c4adc19359ee66e4fb483e0a1903fde994d7d4 Mon Sep 17 00:00:00 2001 From: Nick Kerr Date: Mon, 11 Aug 2025 10:31:04 -0700 Subject: [PATCH 08/17] AuditLogHelper: return null instead of index error (#2610) --- .../labkey/test/tests/DomainDesignerTest.java | 2 +- .../test/tests/TextChoiceSampleTypeTest.java | 20 ++-- src/org/labkey/test/util/AuditLogHelper.java | 109 +++++++++--------- 3 files changed, 65 insertions(+), 66 deletions(-) diff --git a/src/org/labkey/test/tests/DomainDesignerTest.java b/src/org/labkey/test/tests/DomainDesignerTest.java index 8e4cd16241..479bea906b 100644 --- a/src/org/labkey/test/tests/DomainDesignerTest.java +++ b/src/org/labkey/test/tests/DomainDesignerTest.java @@ -282,7 +282,7 @@ public void testInvalidLookupDomainField() throws IOException, CommandException .clickSave(); AuditLogHelper.DetailedAuditEventRow expectedDomainEvent = new AuditLogHelper.DetailedAuditEventRow(null, listName, null, - "The name of the list domain 'InvalidLookUpNameList' was changed to 'InvalidLookUpNameList_edited'. The descriptor of domain InvalidLookUpNameList_edited was updated.", + String.format("The name of the list domain '%s' was changed to '%s'. The descriptor of domain %s was updated.", listName, editedListName, editedListName), "", null, null, "Name: " + listName + " > " + editedListName); boolean pass = _auditLogHelper.validateLastDomainAuditEvents(editedListName, getProjectName(), expectedDomainEvent, Collections.emptyMap()); checker().verifyTrue("The comment logged for the list renaming was not as expected", pass); diff --git a/src/org/labkey/test/tests/TextChoiceSampleTypeTest.java b/src/org/labkey/test/tests/TextChoiceSampleTypeTest.java index 6cf91f37fe..06ef616241 100644 --- a/src/org/labkey/test/tests/TextChoiceSampleTypeTest.java +++ b/src/org/labkey/test/tests/TextChoiceSampleTypeTest.java @@ -29,10 +29,8 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.stream.Collectors; @Category({Daily.class}) @@ -330,10 +328,6 @@ public void testUpdatingAndDeletingValuesInSampleType() throws IOException, Comm TestDataGenerator dataGenerator = createSampleType(sampleTypeName, namePrefix, textChoiceFieldName, expectedUnLockedValues); - // Add the list of the event ids to an ignore list so future tests don't look at them again. - Set ignoreIds = new HashSet<>(); - ignoreIds.addAll(_auditLogHelper.getDomainEventIds(getProjectName(), sampleTypeName, null)); - log("Create some samples that have TextChoice values set."); // Only assign a few of the values to samples (i.e. lock them). @@ -347,7 +341,7 @@ public void testUpdatingAndDeletingValuesInSampleType() throws IOException, Comm Map samplesWithTC = new HashMap<>(); int index = 0; - for(int i = 1; i <= 20; i++) + for (int i = 1; i <= 20; i++) { String sampleName = String.format("%s%d", namePrefix, i); @@ -357,10 +351,10 @@ public void testUpdatingAndDeletingValuesInSampleType() throws IOException, Comm String strFieldValue; // Give a TextChoice value to every other sample. - if(i%2 == 0) + if (i%2 == 0) { - if(index >= expectedLockedValues.size()) + if (index >= expectedLockedValues.size()) index = 0; String tcValue = expectedLockedValues.get(index); @@ -413,7 +407,7 @@ public void testUpdatingAndDeletingValuesInSampleType() throws IOException, Comm value = expectedUnLockedValues.get(0); fieldRow.selectTextChoiceValue(value); - if(checker().verifyTrue(String.format("Delete button is not enabled for value '%s', it should be.", value), + if (checker().verifyTrue(String.format("Delete button is not enabled for value '%s', it should be.", value), fieldRow.isTextChoiceDeleteButtonEnabled())) { fieldRow.deleteTextChoiceValue(value); @@ -474,13 +468,13 @@ public void testUpdatingAndDeletingValuesInSampleType() throws IOException, Comm // Construct a list of samples that have TextChoice set and what they are expected to be. List> expectedSamples = new ArrayList<>(); - for(Map.Entry entry : samplesWithTC.entrySet()) + for (Map.Entry entry : samplesWithTC.entrySet()) { String sampleId = entry.getKey(); // Need to special case for the TC value that was just updated. String sampleValue; - if(entry.getValue().equals(valueToUpdate)) + if (entry.getValue().equals(valueToUpdate)) { sampleValue = updatedValue; } @@ -618,7 +612,7 @@ public void testSetTextChoiceValueForSample() throws IOException, CommandExcepti List availableSamples = new ArrayList<>(); - for(int i = 1; i <= 5; i++) + for (int i = 1; i <= 5; i++) { Map sample = new HashMap<>(); String sampleName = String.format("%s%d", namePrefix, i); diff --git a/src/org/labkey/test/util/AuditLogHelper.java b/src/org/labkey/test/util/AuditLogHelper.java index 8914ff5190..73fb555877 100644 --- a/src/org/labkey/test/util/AuditLogHelper.java +++ b/src/org/labkey/test/util/AuditLogHelper.java @@ -1,6 +1,7 @@ package org.labkey.test.util; import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.json.JSONException; import org.labkey.remoteapi.CommandException; @@ -165,10 +166,12 @@ public void checkTimelineAuditEventDiffCount(String containerPath, List { checkAuditEventDiffCount(containerPath, getAuditEventNameFromURL(), expectedDiffCounts); } + public void checkAuditEventDiffCount(String containerPath, AuditEvent auditEventName, List expectedDiffCounts) throws IOException, CommandException { checkAuditEventDiffCount(containerPath, auditEventName, Collections.emptyList(), expectedDiffCounts); } + public void checkAuditEventDiffCount(String containerPath, AuditEvent auditEventName, List filters, List expectedDiffCounts) throws IOException, CommandException { Integer maxRows = expectedDiffCounts.size(); @@ -287,12 +290,13 @@ public boolean validateDomainPropertiesAuditLog(String domainName, Integer domai return true; Map actualAuditDetails = getDomainPropertyEvents(domainName, domainEventId); boolean pass = true; - if (expectedAuditDetails.keySet().size() != actualAuditDetails.keySet().size()) + if (expectedAuditDetails.size() != actualAuditDetails.size()) { pass = false; - TestLogger.log("Number of DomainPropertyAuditEvent events not as expected."); + TestLogger.log(String.format("Number of DomainPropertyAuditEvent events not as expected. Expected %d, Actual %d.", expectedAuditDetails.size(), actualAuditDetails.size())); } - for (String key : expectedAuditDetails.keySet()) + + for (String key : expectedAuditDetails.keySet()) { DetailedAuditEventRow expectedAuditDetail = expectedAuditDetails.get(key); DetailedAuditEventRow actualAuditDetail = actualAuditDetails.get(key); @@ -311,13 +315,19 @@ public boolean validateDomainPropertiesAuditLog(String domainName, Integer domai public boolean validateLastDomainAuditEvents(String domainName, String projectName, DetailedAuditEventRow expectedDomainEvent, Map expectedDomainPropertyEvents) { DetailedAuditEventRow latestDomainEvent = getLastDomainEvent(projectName, domainName); + if (latestDomainEvent == null) + { + TestLogger.log(String.format("No DomainAuditEvent found for domain '%s' in project '%s'.", domainName, projectName)); + return false; + } + boolean pass = validateDetailAuditLog(expectedDomainEvent, latestDomainEvent); return pass && validateDomainPropertiesAuditLog(domainName, latestDomainEvent.rowId, expectedDomainPropertyEvents); } public List getDomainEventIds(String projectName, String domainName, @Nullable Collection ignoreIds) { - List domainAuditEventAllRows = getDomainEventLog(projectName, domainName, ignoreIds); + List domainAuditEventAllRows = getDomainAuditEventLog(projectName, domainName, ignoreIds, null); List domainEventIds = new ArrayList<>(); domainAuditEventAllRows.forEach((event)->domainEventIds.add(event.rowId)); @@ -327,14 +337,20 @@ public List getDomainEventIds(String projectName, String domainName, @N return domainEventIds; } - public DetailedAuditEventRow getLastDomainEvent(String projectName, String domainName) + public @Nullable DetailedAuditEventRow getLastDomainEvent(String projectName, String domainName) { - return getDomainEventLog(projectName, domainName, null).get(0); + List eventLog = getDomainAuditEventLog(projectName, domainName, null, 1); + if (eventLog.isEmpty()) + return null; + return eventLog.get(0); } - public Integer getLastDomainEventId(String projectName, String domainName) + public @Nullable Integer getLastDomainEventId(String projectName, String domainName) { - return getLastDomainEvent(projectName, domainName).rowId; + DetailedAuditEventRow event = getLastDomainEvent(projectName, domainName); + if (event == null) + return null; + return event.rowId; } public static List propertyAuditColumns = List.of("type", "comment", "usercomment", "oldvalues", "newvalues", "datachanges"); @@ -363,8 +379,11 @@ public String getLogString() } } - public Map getDomainPropertyEvents(String domainName, Integer domainEventId) + public @NotNull Map getDomainPropertyEvents(String domainName, Integer domainEventId) { + if (domainEventId == null) + return Collections.emptyMap(); + List> allRows = getDomainPropertyEventLog(domainName, Collections.singletonList(domainEventId)); Map domainPropEventComments = new HashMap<>(); allRows.forEach((event)->{ @@ -378,8 +397,8 @@ public Map getDomainPropertyEvents(String domainN String dataChanges = getLogColumnDisplayValue(event, "dataChanges"); domainPropEventComments.put(propertyName, new DetailedAuditEventRow(rowId, propertyName, action, comment, userComment, oldValue, newValue, dataChanges)); }); - return domainPropEventComments; + return domainPropEventComments; } public Map getLastDomainPropertyEvents(String projectName, String domainName) @@ -395,18 +414,14 @@ public List getLastDomainPropertyValues(String projectName, String domai public List getDomainEventComments(String projectName, String domainName, @Nullable Collection ignoreIds) { - List domainAuditEventAllRows = getDomainEventLog(projectName, domainName, ignoreIds); - - List domainEventComments = new ArrayList<>(); - domainAuditEventAllRows.forEach((event)->domainEventComments.add(event.comment)); - return domainEventComments; + return getDomainAuditEventLog(projectName, domainName, ignoreIds, null).stream().map(event -> event.comment).toList(); } public Set getDomainEventIdsFromPropertyEvents(List> domainPropertyEventRows) { Set domainEventIds = new HashSet<>(); - for(Map row : domainPropertyEventRows) + for (Map row : domainPropertyEventRows) { domainEventIds.add(getLogColumnIntValue(row, "domaineventid")); } @@ -414,48 +429,43 @@ public Set getDomainEventIdsFromPropertyEvents(List return domainEventIds; } - private List getDomainEventLog(String projectName, String domainName, @Nullable Collection ignoreIds) + private List getDomainAuditEventLog(String projectName, String domainName, @Nullable Collection ignoreIds, @Nullable Integer maxRows) { TestLogger.log("Get a list of the Domain Events for project '" + projectName + "'. "); + domainName = domainName.trim(); Connection cn = WebTestHelper.getRemoteApiConnection(); SelectRowsCommand cmd = new SelectRowsCommand("auditLog", "DomainAuditEvent"); cmd.setRequiredVersion(9.1); cmd.setColumns(Arrays.asList("rowid", "domainuri", "domainname", "comment", "usercomment", "oldvalues", "newvalues", "datachanges")); cmd.addFilter("projectid/DisplayName", projectName, Filter.Operator.EQUAL); - if(null != ignoreIds) + cmd.addFilter("domainname", domainName, Filter.Operator.EQUAL); + if (null != ignoreIds) { - StringBuilder stringBuilder = new StringBuilder(); - ignoreIds.forEach((id)->{ - if(!stringBuilder.isEmpty()) - stringBuilder.append(";"); - stringBuilder.append(id); - }); - cmd.addFilter("rowId", stringBuilder, Filter.Operator.NOT_IN); + String rowIds = StringUtils.join(ignoreIds, ";"); + cmd.addFilter("rowId", rowIds, Filter.Operator.NOT_IN); } cmd.setContainerFilter(ContainerFilter.AllFolders); - cmd.setSorts(Arrays.asList(new Sort("RowId", Sort.Direction.DESCENDING))); + cmd.setSorts(List.of(new Sort("RowId", Sort.Direction.DESCENDING))); + + if (maxRows != null) + cmd.setMaxRows(maxRows); List> domainAuditEventAllRows = executeSelectCommand(cn, cmd); - TestLogger.log("Number of 'Domain Event' log entries for '" + projectName + "': " + domainAuditEventAllRows.size()); + TestLogger.log(String.format("Number of Domain Event log entries for domain '%s' in '%s': %d", domainName, projectName, domainAuditEventAllRows.size())); - TestLogger.log("Filter the list to look only at '" + domainName + "'."); List domainAuditEventRows = new ArrayList<>(); - for(Map row : domainAuditEventAllRows) + for (Map row : domainAuditEventAllRows) { - String domainName_ = getLogColumnValue(row, "domainname"); - - if(domainName_.trim().equalsIgnoreCase(domainName.trim())) - { - Integer rowId = getLogColumnIntValue(row, "rowid"); - String comment = getLogColumnValue(row, "comment"); - String userComment = getLogColumnValue(row, "usercomment"); - String oldValue = getLogColumnValue(row, "oldvalues"); - String newValue = getLogColumnValue(row, "newvalues"); - String dataChanges = getLogColumnDisplayValue(row, "dataChanges"); - domainAuditEventRows.add(new DetailedAuditEventRow(rowId, domainName, null, comment, userComment, oldValue, newValue, dataChanges)); - } + String eventDomainName = getLogColumnValue(row, "domainname"); + Integer rowId = getLogColumnIntValue(row, "rowid"); + String comment = getLogColumnValue(row, "comment"); + String userComment = getLogColumnValue(row, "usercomment"); + String oldValue = getLogColumnValue(row, "oldvalues"); + String newValue = getLogColumnValue(row, "newvalues"); + String dataChanges = getLogColumnDisplayValue(row, "dataChanges"); + domainAuditEventRows.add(new DetailedAuditEventRow(rowId, eventDomainName, null, comment, userComment, oldValue, newValue, dataChanges)); } return domainAuditEventRows; @@ -469,15 +479,10 @@ private List> getDomainPropertyEventLog(String domainName, @ cmd.setColumns(Arrays.asList("Created", "CreatedBy", "ImpersonatedBy", "propertyname", "action", "domainname", "domaineventid", "Comment", "UserComment", "oldvalues", "newvalues", "datachanges")); cmd.addFilter("domainname", domainName, Filter.Operator.EQUAL); - if(null != eventIds) + if (null != eventIds) { - StringBuilder stringBuilder = new StringBuilder(); - eventIds.forEach((id)->{ - if(!stringBuilder.isEmpty()) - stringBuilder.append(";"); - stringBuilder.append(id); - }); - cmd.addFilter("domaineventid/rowid", stringBuilder, Filter.Operator.IN); + String rowIds = StringUtils.join(eventIds, ";"); + cmd.addFilter("domaineventid/rowid", rowIds, Filter.Operator.IN); } cmd.setContainerFilter(ContainerFilter.AllFolders); @@ -494,7 +499,7 @@ private List> executeSelectCommand(Connection cn, SelectRows TestLogger.log("Number of rows: " + response.getRowCount()); rowsReturned.addAll(response.getRows()); } - catch(IOException | CommandException ex) + catch (IOException | CommandException ex) { // Just fail here, don't toss the exception up the stack. fail("There was a command exception when getting the log: " + ex); @@ -551,7 +556,7 @@ private String getLogColumnValue(Map rowEntry, String columnName return null; return value.toString(); } - catch(JSONException je) + catch (JSONException je) { // Just fail here, don't toss the exception up the stack. throw new IllegalArgumentException(je); @@ -577,7 +582,7 @@ private Integer getLogColumnIntValue(Map rowEntry, String column return null; return parseInt(strVal); } - catch(JSONException je) + catch (JSONException je) { // Just fail here, don't toss the exception up the stack. throw new IllegalArgumentException(je); From 435a4986893b1c77d3ccb50c9e953bddf7a17c6b Mon Sep 17 00:00:00 2001 From: Trey Chadick Date: Mon, 11 Aug 2025 12:29:19 -0700 Subject: [PATCH 09/17] Wait for system maintenance page to load when rerunning the task (#2615) --- src/org/labkey/test/WebDriverWrapper.java | 24 +++++++++++++++++++ .../admin/ConfigureSystemMaintenancePage.java | 5 ++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/org/labkey/test/WebDriverWrapper.java b/src/org/labkey/test/WebDriverWrapper.java index fe0077f074..83bd5dd672 100644 --- a/src/org/labkey/test/WebDriverWrapper.java +++ b/src/org/labkey/test/WebDriverWrapper.java @@ -75,6 +75,7 @@ import org.openqa.selenium.Keys; import org.openqa.selenium.NoAlertPresentException; import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.NoSuchWindowException; import org.openqa.selenium.ScriptTimeoutException; import org.openqa.selenium.SearchContext; import org.openqa.selenium.StaleElementReferenceException; @@ -2132,6 +2133,29 @@ public long doAndMaybeWaitForPageToLoad(int msWait, Supplier action) return loadTimer.elapsed().toMillis(); } + public long doAndWaitForWindow(Runnable action, String windowName) + { + return doAndMaybeWaitForPageToLoad(10_000, () -> { + String initialWindow = getDriver().getWindowHandle(); + boolean targetWindowExists; + try + { + getDriver().switchTo().window(windowName); + getDriver().switchTo().window(initialWindow); + targetWindowExists = true; + } + catch (NoSuchWindowException e) + { + targetWindowExists = false; + } + + action.run(); + + getDriver().switchTo().window(windowName); + return targetWindowExists; + }); + } + public long doAndAcceptUnloadAlert(Runnable func, String partialAlertText) { return doAndWaitForPageToLoad(() -> diff --git a/src/org/labkey/test/pages/admin/ConfigureSystemMaintenancePage.java b/src/org/labkey/test/pages/admin/ConfigureSystemMaintenancePage.java index 15961fa834..abf65023eb 100644 --- a/src/org/labkey/test/pages/admin/ConfigureSystemMaintenancePage.java +++ b/src/org/labkey/test/pages/admin/ConfigureSystemMaintenancePage.java @@ -26,9 +26,8 @@ public static ConfigureSystemMaintenancePage beginAt(WebDriverWrapper webDriverW */ public PipelineStatusDetailsPage runMaintenanceTask(String description) { - click(Locator.tagWithAttribute("input", "type", "checkbox") - .followingSibling("a").withText(description)); - getDriver().switchTo().window("systemMaintenance"); + doAndWaitForWindow(() -> click(Locator.tagWithAttribute("input", "type", "checkbox") + .followingSibling("a").withText(description)), "systemMaintenance"); PipelineStatusDetailsPage pipelineStatusDetailsPage = new PipelineStatusDetailsPage(getDriver()); pipelineStatusDetailsPage.waitForComplete(); From ffd5c3d5e7bdec04034f225aa63c304c72b1d315 Mon Sep 17 00:00:00 2001 From: Trey Chadick Date: Mon, 11 Aug 2025 15:47:22 -0700 Subject: [PATCH 10/17] Wait for panel visibility in ShowAdminPage (#2620) --- .../pages/ConfigureReportsAndScriptsPage.java | 5 +- .../test/pages/core/admin/ShowAdminPage.java | 193 +++++++----------- 2 files changed, 73 insertions(+), 125 deletions(-) diff --git a/src/org/labkey/test/pages/ConfigureReportsAndScriptsPage.java b/src/org/labkey/test/pages/ConfigureReportsAndScriptsPage.java index 2f58e15881..6d65f16f46 100644 --- a/src/org/labkey/test/pages/ConfigureReportsAndScriptsPage.java +++ b/src/org/labkey/test/pages/ConfigureReportsAndScriptsPage.java @@ -27,6 +27,7 @@ import org.labkey.test.util.LogMethod; import org.labkey.test.util.LoggedParam; import org.labkey.test.util.TestLogger; +import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import java.io.File; @@ -45,7 +46,7 @@ public class ConfigureReportsAndScriptsPage extends LabKeyPage private static final String DEFAULT_ENGINE = "Mozilla Rhino"; private static final String EDIT_WINDOW_TITLE = "Edit Engine Configuration"; - public ConfigureReportsAndScriptsPage(WebDriverWrapper test) + public ConfigureReportsAndScriptsPage(WebDriver test) { super(test); waitForEnginesGrid(); @@ -54,7 +55,7 @@ public ConfigureReportsAndScriptsPage(WebDriverWrapper test) public static ConfigureReportsAndScriptsPage beginAt(WebDriverWrapper driver) { driver.beginAt(WebTestHelper.buildURL("core", "configureReportsAndScripts")); - return new ConfigureReportsAndScriptsPage(driver); + return new ConfigureReportsAndScriptsPage(driver.getDriver()); } public void waitForEnginesGrid() diff --git a/src/org/labkey/test/pages/core/admin/ShowAdminPage.java b/src/org/labkey/test/pages/core/admin/ShowAdminPage.java index 11af29747b..0df5e6ea13 100644 --- a/src/org/labkey/test/pages/core/admin/ShowAdminPage.java +++ b/src/org/labkey/test/pages/core/admin/ShowAdminPage.java @@ -28,10 +28,11 @@ import org.labkey.test.util.OptionalFeatureHelper; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.ui.ExpectedConditions; import java.util.List; +import java.util.function.Function; -// TODO: Missing lots of functionality public class ShowAdminPage extends LabKeyPage { public ShowAdminPage(WebDriver driver) @@ -48,104 +49,105 @@ public static ShowAdminPage beginAt(WebDriverWrapper driver) public ShowAdminPage goToServerInformationSection() { elementCache().sectionServerInfo.click(); + shortWait().until(ExpectedConditions.visibilityOf(elementCache().serverInfoPanel)); return this; } public ShowAdminPage goToSettingsSection() { elementCache().sectionSettingsLinks.click(); + shortWait().until(ExpectedConditions.visibilityOf(elementCache().settingsPanel)); return this; } public ShowAdminPage goToModuleInformationSection() { elementCache().sectionModuleInfo.click(); + shortWait().until(ExpectedConditions.visibilityOf(elementCache().moduleInfoPanel)); return this; } - public ShowAdminPage goToActiveUsersSection() + public ShowAdminPage goToRecentUsersSection() { elementCache().sectionActiveUsers.click(); + shortWait().until(ExpectedConditions.visibilityOf(elementCache().recentUsersPanel)); return this; } - public List getActiveUsers() + public void clickSettingsLink(String settingsLink) { - goToActiveUsersSection(); - return getTexts(elementCache().findActiveUsers()); + goToSettingsSection(); + clickAndWait(Locator.linkWithText(settingsLink).findElement(elementCache().settingsPanel)); + } + + public T clickSettingsLink(String settingsLink, Function pageFactory) + { + clickSettingsLink(settingsLink); + return pageFactory.apply(getDriver()); + } + + public List getRecentUsers() + { + goToRecentUsersSection(); + return getTexts(elementCache().findRecentUsers()); } public String getServerGUID() { goToServerInformationSection(); - return elementCache().findServerGUID().getText(); + return elementCache().serverGuidEl.getText(); } public void clickAnalyticsSettings() { - goToSettingsSection(); - clickAndWait(elementCache().analyticsSettingsLink); + clickSettingsLink("analytics settings"); } public void clickAllowedExternalRedirectHosts() { - goToSettingsSection(); - clickAndWait(elementCache().externalRedirectHostLink); + clickSettingsLink("allowed external redirect hosts"); Locator.waitForAnyElement(shortWait(), Locator.tagWithText("span", "Done"), Locator.tagWithText("span", "Save")); } public ShowAuditLogPage clickAuditLog() { - goToSettingsSection(); - clickAndWait(elementCache().auditLogLink); - return new ShowAuditLogPage(getDriver()); + return clickSettingsLink("audit log", ShowAuditLogPage::new); } public ExternalSourcesPage clickAllowedExternalResourceHosts() { - goToSettingsSection(); - clickAndWait(elementCache().externalResourceHostsLink); - return new ExternalSourcesPage(getDriver()); + return clickSettingsLink("allowed external resource hosts", ExternalSourcesPage::new); } public AllowedFileExtensionAdminPage clickAllowedFileExtensions() { - goToSettingsSection(); - clickAndWait(elementCache().allowedFileExtensionLink); - return new AllowedFileExtensionAdminPage(getDriver()); + return clickSettingsLink("allowed file extensions", AllowedFileExtensionAdminPage::new); } public void clickAuditLogMaintenance() { - goToSettingsSection(); - clickAndWait(elementCache().auditLogMaintenanceLink); + clickSettingsLink("Audit Log Maintenance"); } + public LoginConfigurePage clickAuthentication() { - goToSettingsSection(); - clickAndWait(elementCache().authenticationLink); - return new LoginConfigurePage(getDriver()); + return clickSettingsLink("authentication", LoginConfigurePage::new); } public void clickConfigurePageElements() { - goToSettingsSection(); - clickAndWait(elementCache().configurePageElements); + clickSettingsLink("configure page elements"); Locator.waitForAnyElement(shortWait(), Locator.tagWithText("span", "Done"), Locator.tagWithText("span", "Save")); } public ComplianceSettingsAccountsPage clickComplianceSettings() { - goToSettingsSection(); - clickAndWait(elementCache().complianceSettings); - return new ComplianceSettingsAccountsPage(getDriver()); + return clickSettingsLink("Compliance Settings", ComplianceSettingsAccountsPage::new); } public DomainDesignerPage clickChangeUserProperties() { - goToSettingsSection(); - clickAndWait(elementCache().changeUserPropertiesLink); - return new DomainDesignerPage(getDriver()); + return clickSettingsLink("change user properties", DomainDesignerPage::new); } public void clickDeprecatedFeatures() @@ -156,130 +158,103 @@ public void clickDeprecatedFeatures() public void clickEmailCustomization() { - goToSettingsSection(); - clickAndWait(elementCache().emailCustomizationLink); + clickSettingsLink("email customization"); } public void clickNotificationServiceAdmin() { - goToSettingsSection(); - clickAndWait(elementCache().notificationServiceAdminLink); + clickSettingsLink("notification service admin"); } public ConfigureFileSystemAccessPage clickFiles() { - goToSettingsSection(); - clickAndWait(elementCache().filesLink); - return new ConfigureFileSystemAccessPage(getDriver()); + return clickSettingsLink("files", ConfigureFileSystemAccessPage::new); } public void clickFullTextSearch() { - goToSettingsSection(); - clickAndWait(elementCache().fullTextSearchLink); + clickSettingsLink("full-text search"); } public FolderTypePages clickFolderType() { - goToSettingsSection(); - clickAndWait(elementCache().folderTypeLink); - return new FolderTypePages(getDriver()); + return clickSettingsLink("folder types", FolderTypePages::new); } public SiteValidationPage clickSiteValidation() { - goToSettingsSection(); - clickAndWait(elementCache().siteValidationLink); - return new SiteValidationPage(getDriver()); + return clickSettingsLink("site validation", SiteValidationPage::new); } public LookAndFeelSettingsPage clickLookAndFeelSettings() { - goToSettingsSection(); - clickAndWait(elementCache().lookAndFeelSettingsLink); - return new LookAndFeelSettingsPage(getDriver()); + return clickSettingsLink("look and feel settings", LookAndFeelSettingsPage::new); } public void clickMasterPatientIndex() { - goToSettingsSection(); - clickAndWait(elementCache().masterPatientIndex); + clickSettingsLink("Master Patient Index"); } public void clickProfiler() { - goToSettingsSection(); - clickAndWait(elementCache().profilerLink); + clickSettingsLink("profiler"); } public void clickRunningThreads() { - goToSettingsSection(); - clickAndWait(elementCache().runningThreadsLink); + clickSettingsLink("running threads"); } public CustomizeSitePage clickSiteSettings() { - goToSettingsSection(); - clickAndWait(elementCache().siteSettingsLink); - return new CustomizeSitePage(getDriver()); + return clickSettingsLink("site settings", CustomizeSitePage::new); } public void clickSiteWideTerms() { - goToSettingsSection(); - clickAndWait(elementCache().siteWideTermsLink); + clickSettingsLink("site-wide terms of use"); } public ConfigureSystemMaintenancePage clickSystemMaintenance() { - goToSettingsSection(); - clickAndWait(elementCache().systemMaintenanceLink); - return new ConfigureSystemMaintenancePage(getDriver()); + return clickSettingsLink("system maintenance", ConfigureSystemMaintenancePage::new); } public void clickSystemProperties() { - goToSettingsSection(); - clickAndWait(elementCache().systemPropertiesLink); + clickSettingsLink("system properties"); } public ConfigureReportsAndScriptsPage clickViewsAndScripting() { - goToSettingsSection(); - clickAndWait(elementCache().viewsAndScriptingLink); - return new ConfigureReportsAndScriptsPage(this); + return clickSettingsLink("views and scripting", ConfigureReportsAndScriptsPage::new); } public void clickCredits() { - goToSettingsSection(); - clickAndWait(elementCache().creditsLink); + clickSettingsLink("credits"); } public void clickViewPrimarySiteLogFile() { - goToSettingsSection(); - clickAndWait(elementCache().viewPrimarySiteLogFileLink); + clickSettingsLink("view primary site log file"); } public void clickPostgresActivity() { - goToSettingsSection(); - clickAndWait(elementCache().postgresActivityLink); + clickSettingsLink("postgres activity"); } public void clickPostgresLocks() { - goToSettingsSection(); - clickAndWait(elementCache().postgresLocksLink); + clickSettingsLink("postgres locks"); } public List getAllAdminConsoleLinks() { goToSettingsSection(); - WebElement adminLinksContainer = Locator.id("links").findElement(getDriver()); - return Locator.tag("a").findElements(adminLinksContainer); + return Locator.tag("a").findElements(elementCache().settingsPanel); } @Override @@ -288,52 +263,24 @@ protected ElementCache newElementCache() return new ElementCache(); } - protected class ElementCache extends LabKeyPage.ElementCache - { - protected WebElement sectionServerInfo = Locator.linkWithText("Server Information").findWhenNeeded(this); - protected WebElement sectionSettingsLinks = Locator.linkWithText("Settings").findWhenNeeded(this); - protected WebElement sectionModuleInfo = Locator.linkWithText("Module Information").findWhenNeeded(this); - protected WebElement sectionActiveUsers = Locator.linkWithText("Active Users").findWhenNeeded(this); - - protected WebElement analyticsSettingsLink = Locator.linkWithText("analytics settings").findWhenNeeded(this); - protected WebElement externalRedirectHostLink = Locator.linkWithText("allowed external redirect hosts").findWhenNeeded(this); - protected WebElement externalResourceHostsLink = Locator.linkWithText("allowed external resource hosts").findWhenNeeded(this); - protected WebElement allowedFileExtensionLink = Locator.linkWithText("allowed file extensions").findWhenNeeded(this); - protected WebElement auditLogLink = Locator.linkWithText("audit log").findWhenNeeded(this); - protected WebElement auditLogMaintenanceLink = Locator.linkWithText("Audit Log Maintenance").findWhenNeeded(this); - protected WebElement authenticationLink = Locator.linkWithText("authentication").findWhenNeeded(this); - protected WebElement configurePageElements = Locator.linkWithText("configure page elements").findWhenNeeded(this); - protected WebElement complianceSettings = Locator.linkWithText("Compliance Settings").findWhenNeeded(this); - protected WebElement changeUserPropertiesLink = Locator.linkWithText("change user properties").findWhenNeeded(this); - protected WebElement emailCustomizationLink = Locator.linkWithText("email customization").findWhenNeeded(this); - protected WebElement notificationServiceAdminLink = Locator.linkWithText("notification service admin").findWhenNeeded(this); - protected WebElement filesLink = Locator.linkWithText("files").findWhenNeeded(this); - protected WebElement fullTextSearchLink = Locator.linkWithText("full-text search").findWhenNeeded(this); - protected WebElement folderTypeLink = Locator.linkWithText("folder types").findWhenNeeded(this); - protected WebElement siteValidationLink = Locator.linkWithText("site validation").findWhenNeeded(this); - protected WebElement lookAndFeelSettingsLink = Locator.linkWithText("look and feel settings").findWhenNeeded(this); - protected WebElement masterPatientIndex = Locator.linkWithText("Master Patient Index").findWhenNeeded(this); - protected WebElement profilerLink = Locator.linkWithText("profiler").findWhenNeeded(this); - protected WebElement runningThreadsLink = Locator.linkWithText("running threads").findWhenNeeded(this); - protected WebElement siteSettingsLink = Locator.linkWithText("site settings").findWhenNeeded(this); - protected WebElement siteWideTermsLink = Locator.linkContainingText("site-wide terms of use").findWhenNeeded(this); - protected WebElement systemMaintenanceLink = Locator.linkWithText("system maintenance").findWhenNeeded(this); - protected WebElement systemPropertiesLink = Locator.linkContainingText("system properties").findWhenNeeded(this); - protected WebElement viewsAndScriptingLink = Locator.linkWithText("views and scripting").findWhenNeeded(this); - protected WebElement creditsLink = Locator.linkWithText("credits").findWhenNeeded(this); - protected WebElement viewPrimarySiteLogFileLink = Locator.linkWithText("view primary site log file").findWhenNeeded(this); - - protected WebElement postgresActivityLink = Locator.linkWithText("postgres activity").findWhenNeeded(this); - protected WebElement postgresLocksLink = Locator.linkWithText("postgres locks").findWhenNeeded(this); - - protected List findActiveUsers() - { - return Locator.tagWithName("table", "activeUsers").append(Locator.tag("td").position(1)).findElements(this); - } + protected class ElementCache extends LabKeyPage.ElementCache + { + private final WebElement adminNavPanel = Locator.id("lk-admin-nav").findWhenNeeded(this); + private final WebElement sectionServerInfo = Locator.linkWithText("Server Information").findWhenNeeded(adminNavPanel); + private final WebElement sectionSettingsLinks = Locator.linkWithText("Settings").findWhenNeeded(adminNavPanel); + private final WebElement sectionModuleInfo = Locator.linkWithText("Module Information").findWhenNeeded(adminNavPanel); + private final WebElement sectionActiveUsers = Locator.linkWithText("Active Users").findWhenNeeded(adminNavPanel); - protected WebElement findServerGUID() + private final WebElement serverInfoPanel = Locator.id("info").withClass("lk-admin-section").findWhenNeeded(this); + private final WebElement settingsPanel = Locator.id("links").withClass("lk-admin-section").findWhenNeeded(this); + private final WebElement moduleInfoPanel = Locator.id("modules").withClass("lk-admin-section").findWhenNeeded(this); + private final WebElement recentUsersPanel = Locator.id("users").withClass("lk-admin-section").findWhenNeeded(this); + + private List findRecentUsers() { - return Locator.tagWithText("td", "Server GUID").followingSibling("td").findElement(this); + return Locator.tagWithName("table", "activeUsers").append(Locator.tag("td").position(1)).findElements(recentUsersPanel); } + + private final WebElement serverGuidEl = Locator.tagWithText("td", "Server GUID").followingSibling("td").findWhenNeeded(serverInfoPanel); } } From 04a40f513b2a773c194d1588991db9d95bea7752 Mon Sep 17 00:00:00 2001 From: Trey Chadick Date: Wed, 13 Aug 2025 08:40:30 -0700 Subject: [PATCH 11/17] Add support for generating random text choice data (#2616) --- .../labkey/test/util/TestDataGenerator.java | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/src/org/labkey/test/util/TestDataGenerator.java b/src/org/labkey/test/util/TestDataGenerator.java index c4a031c157..1128cce0bc 100644 --- a/src/org/labkey/test/util/TestDataGenerator.java +++ b/src/org/labkey/test/util/TestDataGenerator.java @@ -411,7 +411,7 @@ private Map generateRow() { if (!_dataSuppliers.containsKey(columnName)) { - _dataSuppliers.put(columnName, getDefaultDataSupplier(_columns.get(columnName).getRangeURI())); + _dataSuppliers.put(columnName, getDefaultDataSupplier(_columns.get(columnName))); } if (_autoGeneratedFields.contains(columnName)) @@ -425,29 +425,47 @@ private Map generateRow() return newRow; } - private Supplier getDefaultDataSupplier(String columnType) + private Supplier getDefaultDataSupplier(PropertyDescriptor propertyDescriptor) { + Map allProperties = Objects.requireNonNullElse(propertyDescriptor.getAllProperties(), Collections.emptyMap()); + String columnType = (String) allProperties.get("conceptURI"); + if (columnType == null) columnType = propertyDescriptor.getRangeURI(); + switch (columnType.substring(columnType.indexOf('#') + 1).toLowerCase()) { case "string": - return ()-> randomString(20, _excludedChars, _alphaNumericStr ? ALPHANUMERIC_STRING : CHARSET_STRING); + return () -> randomString(20, _excludedChars, _alphaNumericStr ? ALPHANUMERIC_STRING : CHARSET_STRING); case "int": - return ()-> randomInt(0, 20); + return () -> randomInt(0, 20); case "float": - return ()-> randomFloat(0, 20); + return () -> randomFloat(0, 20); case "double": - return ()-> randomDouble(0, 20); + return () -> randomDouble(0, 20); case "boolean": return this::randomBoolean; case "date": case "datetime": - return ()-> randomDateString(DateUtils.addWeeks(new Date(), -39), new Date()); + return () -> randomDateString(DateUtils.addWeeks(new Date(), -39), new Date()); case "time": - return ()-> + return () -> randomInt(0, 23) + ":" + // hour StringUtils.leftPad(String.valueOf(randomInt(0, 59)), 2, "0") + ":" + // minute StringUtils.leftPad(String.valueOf(randomInt(0, 59)), 2, "0") + "." + // second StringUtils.leftPad(String.valueOf(randomInt(0, 999)), 3, "0"); // millisecond + case "textchoice": + List textChoices = ((FieldDefinition) propertyDescriptor) + .getValidators().stream() + .map(v -> { + if (v instanceof FieldDefinition.TextChoiceValidator tcv && !tcv.getValues().isEmpty()) + return tcv.getValues(); + else + return null; + }) + .filter(Objects::nonNull) + .findFirst() + .orElseThrow(() -> new IllegalStateException("No choices defined for textChoice field : " + propertyDescriptor.getName())); + + return () -> randomChoice(textChoices); default: throw new IllegalArgumentException("ColumnType " + columnType + " isn't implemented yet"); } @@ -595,7 +613,7 @@ public static String randomFieldName(@NotNull String part, int numStartChars, in return randomFieldName; } - public static String randomChoice(List choices) + public static T randomChoice(List choices) { return choices.get(randomInt(0, choices.size() - 1)); } From 7c88338e07c22565cdcdc90da5264dc5b67b97b3 Mon Sep 17 00:00:00 2001 From: Trey Chadick Date: Wed, 13 Aug 2025 08:40:56 -0700 Subject: [PATCH 12/17] Fix WebDriverWrapper.isTextPresent usages (#2618) --- src/org/labkey/test/LabKeySiteWrapper.java | 2 +- src/org/labkey/test/WebDriverWrapper.java | 58 +++++++------------ .../announcements/ModeratorReviewTest.java | 2 +- src/org/labkey/test/util/EscapeUtil.java | 1 + src/org/labkey/test/util/TextSearcher.java | 53 ++++++++++++++++- 5 files changed, 74 insertions(+), 42 deletions(-) diff --git a/src/org/labkey/test/LabKeySiteWrapper.java b/src/org/labkey/test/LabKeySiteWrapper.java index 03f757301c..de4a1135f7 100644 --- a/src/org/labkey/test/LabKeySiteWrapper.java +++ b/src/org/labkey/test/LabKeySiteWrapper.java @@ -431,7 +431,7 @@ public void signInShouldFail(String email, String password, String... expectedMe errorText = getText(Locator.tagWithClass("div", "auth-form-body").childTag("p")); } - List missingErrors = getMissingTexts(new TextSearcher(errorText), expectedMessages); + List missingErrors = new TextSearcher(errorText).getMissingTexts(expectedMessages); assertTrue(String.format("Wrong errors.\nExpected: ['%s']\nActual: '%s'", String.join("',\n'", expectedMessages), errorText), missingErrors.isEmpty()); } diff --git a/src/org/labkey/test/WebDriverWrapper.java b/src/org/labkey/test/WebDriverWrapper.java index 83bd5dd672..ea97863924 100644 --- a/src/org/labkey/test/WebDriverWrapper.java +++ b/src/org/labkey/test/WebDriverWrapper.java @@ -18,7 +18,6 @@ import org.apache.commons.collections4.MultiValuedMap; import org.apache.commons.collections4.multimap.HashSetValuedHashMap; import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.mutable.MutableBoolean; import org.apache.commons.lang3.mutable.MutableInt; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; @@ -61,6 +60,7 @@ import org.labkey.test.util.RelativeUrl; import org.labkey.test.util.TestLogger; import org.labkey.test.util.TextSearcher; +import org.labkey.test.util.TextSearcher.TextTransformers; import org.labkey.test.util.Timer; import org.labkey.test.util.selenium.ScrollUtils; import org.labkey.test.util.selenium.WebDriverUtils; @@ -1615,29 +1615,27 @@ public void assertLabKeyErrorPresent() fail("No errors found"); } - public static String encodeText(String unencodedText) + /** + * Check whether all the specified text is present on the page + * @param texts un-encoded text to search for + * @return true if all the specified texts are present on the page + */ + public boolean isTextPresent(String... texts) { - return unencodedText - .replace("&", "&") - .replace("<", "<") - .replace(">", ">"); + return new TextSearcher(this).areAllTextsPresent(texts); } - public boolean isTextPresent(String... texts) + /** + * Check whether the HTML-encoded text is in the page source + * @param htmlFragments encoded html fragments to search for + * @return true if all the specified texts are present on the page + */ + public boolean isHtmlPresent(String... htmlFragments) { - final MutableBoolean present = new MutableBoolean(true); - - TextSearcher.TextHandler handler = (htmlSource, text) -> { - // Not found... stop enumerating and return false - if (htmlSource == null || !htmlSource.contains(text)) - present.setFalse(); - - return present.get(); - }; TextSearcher searcher = new TextSearcher(this); - searcher.searchForTexts(handler, Arrays.asList(texts)); + searcher.setSearchTransformer(TextTransformers.IDENTITY); - return present.get(); + return searcher.areAllTextsPresent(htmlFragments); } public List getTextOrder(TextSearcher searcher, String... texts) @@ -1660,11 +1658,6 @@ public List getTextOrder(TextSearcher searcher, String... texts) return orderedTexts; } - public List getMissingTexts(TextSearcher searcher, String... texts) - { - return searcher.getMissingTexts(Arrays.asList(texts)); - } - public String getText(Locator elementLocator) { WebElement el = elementLocator.findElement(getDriver()); @@ -1693,7 +1686,7 @@ public void assertTextPresent(String... texts) public void assertTextPresent(TextSearcher searcher, String... texts) { - List missingTexts = getMissingTexts(searcher, texts); + List missingTexts = searcher.getMissingTexts(texts); if (!missingTexts.isEmpty()) { @@ -1710,7 +1703,7 @@ public void assertTextPresentCaseInsensitive(String... texts) { TextSearcher searcher = new TextSearcher(this); - searcher.setSearchTransformer((text) -> encodeText(text).toLowerCase()); + searcher.setSearchTransformer(TextTransformers.ENCODE_HTML.andThen(String::toLowerCase)); searcher.setSourceTransformer(String::toLowerCase); @@ -1722,18 +1715,7 @@ public void assertTextPresentCaseInsensitive(String... texts) */ public boolean isAnyTextPresent(String... texts) { - final MutableBoolean found = new MutableBoolean(false); - - TextSearcher.TextHandler handler = (htmlSource, text) -> { - if (htmlSource.contains(text)) - found.setTrue(); - - return !found.get(); // stop searching if any value is found - }; - TextSearcher searcher = new TextSearcher(this); - searcher.searchForTexts(handler, Arrays.asList(texts)); - - return found.get(); + return new TextSearcher(this).isAnyTextPresent(texts); } /** @@ -1893,7 +1875,7 @@ public static void assertTextNotPresent(TextSearcher searcher, String... texts) public void assertTextNotPresent(String... texts) { TextSearcher searcher = new TextSearcher(this); - searcher.setSearchTransformer((text) -> encodeText(text).replace(" ", " ")); + searcher.setSearchTransformer(TextTransformers.ENCODE_HTML.andThen(t -> t.replace(" ", " "))); assertTextNotPresent(searcher, texts); } diff --git a/src/org/labkey/test/tests/announcements/ModeratorReviewTest.java b/src/org/labkey/test/tests/announcements/ModeratorReviewTest.java index d79161b04e..70fdb06842 100644 --- a/src/org/labkey/test/tests/announcements/ModeratorReviewTest.java +++ b/src/org/labkey/test/tests/announcements/ModeratorReviewTest.java @@ -163,7 +163,7 @@ private String addResponse(String user, String title, boolean expectAutoApproved // commonmark-java auto-linking turns all email addresses into mailto: links String formattedResponse = replaceEmailAddressesWithMailToLinks(response); - boolean responseAdded = isTextPresent(formattedResponse); + boolean responseAdded = isHtmlPresent(formattedResponse); if (expectAutoApproved && !responseAdded) { checker().fatal().error(String.format("Expected response '%s' was not present on the thread page.", formattedResponse)); diff --git a/src/org/labkey/test/util/EscapeUtil.java b/src/org/labkey/test/util/EscapeUtil.java index 8e7241219e..ff1a098c6c 100644 --- a/src/org/labkey/test/util/EscapeUtil.java +++ b/src/org/labkey/test/util/EscapeUtil.java @@ -150,4 +150,5 @@ public static String escapeForNameExpression(String name) { return nameExpressionNeedsEscaping.matcher(name).replaceAll("\\\\$1"); } + } diff --git a/src/org/labkey/test/util/TextSearcher.java b/src/org/labkey/test/util/TextSearcher.java index 3ed4a79f06..a36368ba1b 100644 --- a/src/org/labkey/test/util/TextSearcher.java +++ b/src/org/labkey/test/util/TextSearcher.java @@ -15,12 +15,13 @@ */ package org.labkey.test.util; -import org.labkey.test.BaseWebDriverTest; +import org.apache.commons.lang3.mutable.MutableBoolean; import org.labkey.test.TestFileUtils; import org.labkey.test.WebDriverWrapper; import java.io.File; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.function.Function; @@ -98,6 +99,11 @@ public final void searchForTexts(TextHandler textHandler, List texts) } } + public List getMissingTexts(String... texts) + { + return getMissingTexts(Arrays.asList(texts)); + } + public List getMissingTexts(List texts) { final List missingTexts = new ArrayList<>(); @@ -113,6 +119,46 @@ public List getMissingTexts(List texts) return missingTexts; } + /** + * Checks whether any of the texts are present + * @return true if any of the specified text is found + */ + public boolean isAnyTextPresent(String... texts) + { + final MutableBoolean found = new MutableBoolean(false); + + TextSearcher.TextHandler handler = (htmlSource, text) -> { + if (htmlSource.contains(text)) + found.setTrue(); + + return !found.get(); // stop searching if any value is found + }; + searchForTexts(handler, Arrays.asList(texts)); + + return found.get(); + } + + /** + * Checks whether all the specified texts are present + * @return true if all the specified texts are present on the page + */ + public boolean areAllTextsPresent(String... texts) + { + final MutableBoolean present = new MutableBoolean(true); + + TextSearcher.TextHandler handler = (htmlSource, text) -> { + // Not found... stop enumerating and return false + if (htmlSource == null || !htmlSource.contains(text)) + present.setFalse(); + + return present.get(); + }; + + searchForTexts(handler, Arrays.asList(texts)); + + return present.get(); + } + /** * @return source text from the last search attempt */ @@ -129,7 +175,10 @@ public interface TextHandler public static abstract class TextTransformers { - public static final Function ENCODE_HTML = BaseWebDriverTest::encodeText; + public static final Function ENCODE_HTML = t -> t + .replace("&", "&") + .replace("<", "<") + .replace(">", ">"); public static final Function IDENTITY = text -> text; //Inserts spaces between camel-cased words From 976df8587fc96bc26d3c546e011eb5768f17e7eb Mon Sep 17 00:00:00 2001 From: Trey Chadick Date: Wed, 13 Aug 2025 08:42:21 -0700 Subject: [PATCH 13/17] Update 'doAndWaitForWindow' to actually wait for the window (#2623) --- src/org/labkey/test/WebDriverWrapper.java | 4 +- .../test/util/LabKeyExpectedConditions.java | 100 ++++++------------ 2 files changed, 35 insertions(+), 69 deletions(-) diff --git a/src/org/labkey/test/WebDriverWrapper.java b/src/org/labkey/test/WebDriverWrapper.java index ea97863924..ec09a9d74c 100644 --- a/src/org/labkey/test/WebDriverWrapper.java +++ b/src/org/labkey/test/WebDriverWrapper.java @@ -167,6 +167,7 @@ import static org.labkey.test.TestProperties.isWebDriverLoggingEnabled; import static org.labkey.test.WebTestHelper.makeRelativeUrl; import static org.labkey.test.components.html.RadioButton.RadioButton; +import static org.labkey.test.util.LabKeyExpectedConditions.windowIsPresent; import static org.openqa.selenium.chrome.ChromeDriverService.CHROME_DRIVER_LOG_PROPERTY; import static org.openqa.selenium.chrome.ChromeDriverService.CHROME_DRIVER_VERBOSE_LOG_PROPERTY; import static org.openqa.selenium.firefox.GeckoDriverService.GECKO_DRIVER_LOG_PROPERTY; @@ -2133,7 +2134,8 @@ public long doAndWaitForWindow(Runnable action, String windowName) action.run(); - getDriver().switchTo().window(windowName); + new WebDriverWait(getDriver(), Duration.ofSeconds(5)).until(windowIsPresent(windowName)); + return targetWindowExists; }); } diff --git a/src/org/labkey/test/util/LabKeyExpectedConditions.java b/src/org/labkey/test/util/LabKeyExpectedConditions.java index fc87aedf33..89a0960d32 100644 --- a/src/org/labkey/test/util/LabKeyExpectedConditions.java +++ b/src/org/labkey/test/util/LabKeyExpectedConditions.java @@ -21,8 +21,8 @@ import org.openqa.selenium.Dimension; import org.openqa.selenium.JavascriptExecutor; import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.NoSuchWindowException; import org.openqa.selenium.Point; -import org.openqa.selenium.SearchContext; import org.openqa.selenium.StaleElementReferenceException; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; @@ -33,6 +33,7 @@ import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.function.Function; public class LabKeyExpectedConditions @@ -42,32 +43,6 @@ private LabKeyExpectedConditions() // Utility class } - /** - * An expectation for checking child WebElement as a part of parent element to be present - * - * @param context SearchContext to find element within - * @param childLocator used to find child element. For example Locator.xpath("./tr/td") - * @return subelement - */ - public static ExpectedCondition presenceOfNestedElementLocatedBy(final SearchContext context, final By childLocator) - { - return new ExpectedCondition<>() - { - - @Override - public WebElement apply(WebDriver webDriver) - { - return context.findElement(childLocator); - } - - @Override - public String toString() - { - return String.format("visibility of element located by %s", childLocator); - } - }; - } - /** * An expectation for checking that an element has stopped moving * @@ -101,7 +76,7 @@ public String toString() /** * Another expectation for checking that an element has stopped moving * - * @param el the element who's position changes + * @param el the element whose position changes * @return the element when animation is complete */ public static ExpectedCondition animationIsDone(final WebElement el) { @@ -165,7 +140,7 @@ public WebElement apply(WebDriver driver) return null; } - if (el.isEnabled() && !el.getAttribute("class").contains("disabled")) + if (el.isEnabled() && !Objects.requireNonNullElse(el.getAttribute("class"), "").contains("disabled")) return el; else return null; @@ -202,43 +177,7 @@ public Boolean apply(WebDriver ignored) @Override public String toString() { - return staleCheck.toString() + " after clicking"; - } - }; - } - - /** - * Wraps {@link ExpectedConditions#visibilityOfAllElements(WebElement...)} - * This expectations accounts for the behavior of LabKey WebElement wrappers, which will throw if you attempt to - * inspect them before the element has appeared. - * - * @param elements list of WebElements - * @return the list of WebElements once they are located - * @see org.labkey.test.selenium.LazyWebElement - */ - public static ExpectedCondition> visibilityOfAllElements(WebElement... elements) - { - return new ExpectedCondition<>() - { - final ExpectedCondition> wrapped = ExpectedConditions.visibilityOfAllElements(elements); - - @Override - public List apply(WebDriver driver) - { - try - { - return wrapped.apply(driver); - } - catch (StaleElementReferenceException | NoSuchElementException ignore) - { - return null; - } - } - - @Override - public String toString() - { - return wrapped.toString(); + return staleCheck + " after clicking"; } }; } @@ -246,10 +185,10 @@ public String toString() /** * Wraps {@link ExpectedConditions#stalenessOf(WebElement)} * Firefox occasionally throws "NoSuchElementException: Web element reference not seen before" - * for short lived elements. + * for short-lived elements. * * @param element WebElement that should go stale. - * @return false if the element is still attached to the DOM, true otherwise. + * @return ExpectedCondition that returns false if the element is still attached to the DOM, true otherwise. */ public static ExpectedCondition stalenessOf(WebElement element) { @@ -292,6 +231,31 @@ public static ExpectedCondition visibilityOf(WebElement element, boolean visi : ExpectedConditions.invisibilityOf(element); } + /** + * An expectation for checking whether a window or tab with the give name is present. + * If the window/tab is available it switches the given driver to the specified window/tab. + * @param windowName The name of the window + * @return An expected condition that returns the driver with focus switched to the specified window or tab. + * @see WebDriver.TargetLocator#window(String) + */ + public static ExpectedCondition windowIsPresent(final String windowName) { + return new ExpectedCondition<>() { + @Override + public WebDriver apply(WebDriver driver) { + try { + return driver.switchTo().window(windowName); + } catch (NoSuchWindowException e) { + return null; + } + } + + @Override + public String toString() { + return "window named " + windowName; + } + }; + } + /** * Wraps a 'Wait' to terminate after function return a non-null value. * Normally, 'Wait' expects a non-null, non-false return value From 3cee692deeacfde321360a12742b8f86e9839f84 Mon Sep 17 00:00:00 2001 From: Nick Kerr Date: Fri, 15 Aug 2025 07:35:58 -0700 Subject: [PATCH 14/17] Issue 53699: BufferUnderflow and length validator test case (#2632) --- src/org/labkey/test/tests/ClientAPITest.java | 71 ++++++++++++++------ 1 file changed, 50 insertions(+), 21 deletions(-) diff --git a/src/org/labkey/test/tests/ClientAPITest.java b/src/org/labkey/test/tests/ClientAPITest.java index a9b4549d91..3a6ceea1d1 100644 --- a/src/org/labkey/test/tests/ClientAPITest.java +++ b/src/org/labkey/test/tests/ClientAPITest.java @@ -57,6 +57,7 @@ import org.labkey.test.util.PermissionsHelper; import org.labkey.test.util.PortalHelper; import org.labkey.test.util.StudyHelper; +import org.labkey.test.util.TestDataGenerator; import org.labkey.test.util.data.TestDataUtils; import org.labkey.test.util.UIUserHelper; import org.labkey.test.util.WikiHelper; @@ -77,9 +78,12 @@ import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.endsWith; +import static org.hamcrest.Matchers.startsWith; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.labkey.test.WebTestHelper.getHttpResponse; import static org.labkey.test.params.FieldDefinition.DOMAIN_TRICKY_CHARACTERS; @@ -93,8 +97,10 @@ public class ClientAPITest extends BaseWebDriverTest private static final String PROJECT_NAME = "ClientAPITestProject"; private static final String OTHER_PROJECT = "OtherClientAPITestProject"; // for cross-project query test - protected static final String FOLDER_NAME = "api folder"; + protected static final String API_FOLDER_NAME = "api folder"; + private static final String API_FOLDER_PATH = PROJECT_NAME + "/" + API_FOLDER_NAME; private static final String SUBFOLDER_NAME = "subfolder"; + private static final String SUBFOLDER_PATH = API_FOLDER_PATH + "/" + SUBFOLDER_NAME; private static final String TIME_STUDY_FOLDER = "timeStudyFolder"; private static final String TIME_STUDY_NAME = "timeStudyName"; private static final String VISIT_STUDY_FOLDER = "visitStudyFolder"; @@ -129,10 +135,6 @@ public class ClientAPITest extends BaseWebDriverTest public static final String TEST_DIV_NAME = "testDiv"; - private static final String GRIDTEST_GRIDTITLE = "ClientAPITest Grid Title"; - - private static final int PAGE_SIZE = 4; - public static final String SRC_PREFIX = "Loading XSS")); } + @Test // Issue 53699 + public void testLargeMultibytePayload() + { + for (int i = 0; i < 10; i++) + { + // Arrange + int characterCount = 5_000; + // We had problems fetching the JSON content server-side due to Tomcat's InputBuffer not dealing with + // characters that span multiple "pages" of the byte buffer. Test with different numbers of single-byte + // UTF-8 characters before the 4-byte characters to ensure we get different boundary conditions + String tooManyMultibyteCharacters = "a".repeat(i) + "\uD83D\uDC7E".repeat(characterCount); + + TestDataGenerator dataGenerator = new TestDataGenerator("lists", LIST_NAME, API_FOLDER_PATH) + .addCustomRow(Map.of("FirstName", tooManyMultibyteCharacters, "LastName", "Chaplin", "Age", 42)); + + // Act + // Prior to fix for Issue 53699 this would throw a BufferUnderflowException when processing the JSON payload + Exception exception = assertThrows(Exception.class, dataGenerator::insertRows); + + // Assert + // Prior to fix length validation incorrectly checked for string length rather than character code point length + String expectedPrefix = "Value is too long for column 'FirstName', a maximum length of 4000 is allowed."; + String expectedSuffix = String.format("was %d characters long.", characterCount + i); + + assertThat(exception.getMessage(), startsWith(expectedPrefix)); + assertThat(exception.getMessage(), endsWith(expectedSuffix)); + } + } + @Override public BrowserType bestBrowser() { From 4324a2032a8d41f422a29c4eaad85293703e2d25 Mon Sep 17 00:00:00 2001 From: Trey Chadick Date: Fri, 15 Aug 2025 08:03:44 -0700 Subject: [PATCH 15/17] Fix tab management and random data generation (#2631) --- src/org/labkey/test/WebDriverWrapper.java | 29 ++++---- .../labkey/test/util/TestDataGenerator.java | 72 ++++++++++--------- 2 files changed, 56 insertions(+), 45 deletions(-) diff --git a/src/org/labkey/test/WebDriverWrapper.java b/src/org/labkey/test/WebDriverWrapper.java index ec09a9d74c..7a2f817cb0 100644 --- a/src/org/labkey/test/WebDriverWrapper.java +++ b/src/org/labkey/test/WebDriverWrapper.java @@ -148,6 +148,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.Logger; @@ -2118,25 +2119,29 @@ public long doAndMaybeWaitForPageToLoad(int msWait, Supplier action) public long doAndWaitForWindow(Runnable action, String windowName) { + String initialWindow = getDriver().getWindowHandle(); + AtomicBoolean targetWindowExists = new AtomicBoolean(false); + try + { + getDriver().switchTo().window(windowName); + targetWindowExists.set(true); + } + catch (NoSuchWindowException e) + { + targetWindowExists.set(false); + } + + // Call doAndMaybeWaitForPageToLoad with target window in focus (if present) + // Then it will correctly detect the page load in that window return doAndMaybeWaitForPageToLoad(10_000, () -> { - String initialWindow = getDriver().getWindowHandle(); - boolean targetWindowExists; - try - { - getDriver().switchTo().window(windowName); + if (targetWindowExists.get()) getDriver().switchTo().window(initialWindow); - targetWindowExists = true; - } - catch (NoSuchWindowException e) - { - targetWindowExists = false; - } action.run(); new WebDriverWait(getDriver(), Duration.ofSeconds(5)).until(windowIsPresent(windowName)); - return targetWindowExists; + return targetWindowExists.get(); }); } diff --git a/src/org/labkey/test/util/TestDataGenerator.java b/src/org/labkey/test/util/TestDataGenerator.java index 1128cce0bc..b1cf92193b 100644 --- a/src/org/labkey/test/util/TestDataGenerator.java +++ b/src/org/labkey/test/util/TestDataGenerator.java @@ -56,6 +56,7 @@ import java.util.Objects; import java.util.Set; import java.util.concurrent.ThreadLocalRandom; +import java.util.function.Function; import java.util.function.Supplier; import java.util.regex.Pattern; @@ -428,46 +429,51 @@ private Map generateRow() private Supplier getDefaultDataSupplier(PropertyDescriptor propertyDescriptor) { Map allProperties = Objects.requireNonNullElse(propertyDescriptor.getAllProperties(), Collections.emptyMap()); - String columnType = (String) allProperties.get("conceptURI"); - if (columnType == null) columnType = propertyDescriptor.getRangeURI(); + Function getType = s -> s == null ? "" : s.substring(s.indexOf('#') + 1).toLowerCase().trim(); + String conceptUriName = getType.apply((String) allProperties.get("conceptURI")); + String rangeUriName = getType.apply(propertyDescriptor.getRangeURI()); - switch (columnType.substring(columnType.indexOf('#') + 1).toLowerCase()) + switch (conceptUriName) { - case "string": - return () -> randomString(20, _excludedChars, _alphaNumericStr ? ALPHANUMERIC_STRING : CHARSET_STRING); - case "int": - return () -> randomInt(0, 20); - case "float": - return () -> randomFloat(0, 20); - case "double": - return () -> randomDouble(0, 20); - case "boolean": - return this::randomBoolean; - case "date": - case "datetime": - return () -> randomDateString(DateUtils.addWeeks(new Date(), -39), new Date()); - case "time": - return () -> - randomInt(0, 23) + ":" + // hour - StringUtils.leftPad(String.valueOf(randomInt(0, 59)), 2, "0") + ":" + // minute - StringUtils.leftPad(String.valueOf(randomInt(0, 59)), 2, "0") + "." + // second - StringUtils.leftPad(String.valueOf(randomInt(0, 999)), 3, "0"); // millisecond case "textchoice": List textChoices = ((FieldDefinition) propertyDescriptor) - .getValidators().stream() - .map(v -> { - if (v instanceof FieldDefinition.TextChoiceValidator tcv && !tcv.getValues().isEmpty()) - return tcv.getValues(); - else - return null; - }) - .filter(Objects::nonNull) - .findFirst() - .orElseThrow(() -> new IllegalStateException("No choices defined for textChoice field : " + propertyDescriptor.getName())); + .getValidators().stream() + .map(v -> { + if (v instanceof FieldDefinition.TextChoiceValidator tcv && !tcv.getValues().isEmpty()) + return tcv.getValues(); + else + return null; + }) + .filter(Objects::nonNull) + .findFirst() + .orElseThrow(() -> new IllegalStateException("No choices defined for textChoice field : " + propertyDescriptor.getName())); return () -> randomChoice(textChoices); default: - throw new IllegalArgumentException("ColumnType " + columnType + " isn't implemented yet"); + switch (rangeUriName) + { + case "string": + return () -> randomString(20, _excludedChars, _alphaNumericStr ? ALPHANUMERIC_STRING : CHARSET_STRING); + case "int": + return () -> randomInt(0, 20); + case "float": + return () -> randomFloat(0, 20); + case "double": + return () -> randomDouble(0, 20); + case "boolean": + return this::randomBoolean; + case "date": + case "datetime": + return () -> randomDateString(DateUtils.addWeeks(new Date(), -39), new Date()); + case "time": + return () -> + randomInt(0, 23) + ":" + // hour + StringUtils.leftPad(String.valueOf(randomInt(0, 59)), 2, "0") + ":" + // minute + StringUtils.leftPad(String.valueOf(randomInt(0, 59)), 2, "0") + "." + // second + StringUtils.leftPad(String.valueOf(randomInt(0, 999)), 3, "0"); // millisecond + default: + throw new IllegalArgumentException("ColumnType " + conceptUriName + " isn't implemented yet"); + } } } From 6e967ad20f20d2ca35ad13b6135f10b6028d439d Mon Sep 17 00:00:00 2001 From: Trey Chadick Date: Fri, 15 Aug 2025 14:50:39 -0700 Subject: [PATCH 16/17] Helper to create dataset categories with retry (#2626) --- .../test/pages/reports/ManageViewsPage.java | 46 ++++++++++++++++++- .../test/tests/SampleTypeLinkToStudyTest.java | 14 +----- 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/src/org/labkey/test/pages/reports/ManageViewsPage.java b/src/org/labkey/test/pages/reports/ManageViewsPage.java index 810b70f5eb..6ed6bbb3a9 100644 --- a/src/org/labkey/test/pages/reports/ManageViewsPage.java +++ b/src/org/labkey/test/pages/reports/ManageViewsPage.java @@ -17,11 +17,17 @@ import org.labkey.test.Locator; import org.labkey.test.components.ChartQueryDialog; +import org.labkey.test.components.ext4.Window; import org.labkey.test.components.html.BootstrapMenu; import org.labkey.test.pages.LabKeyPage; import org.labkey.test.util.LogMethod; +import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; + +import java.time.Duration; import static org.labkey.test.components.ext4.Window.Window; @@ -34,7 +40,7 @@ public ManageViewsPage(WebDriver driver) public void clickAddReport(String reportType) { - BootstrapMenu.find(getDriver(),"Add Report").clickSubMenu(true,reportType); + new BootstrapMenu.BootstrapMenuFinder(getDriver()).withButtonTextContaining("Add Report").find().clickSubMenu(true,reportType); } public ChartQueryDialog clickAddChart() @@ -89,4 +95,42 @@ public void viewReport(String reportName) mouseOver(reportLink); clickAndWait(reportLink); } + + public void createCategory(String categoryName) + { + createCategories(categoryName); + } + + public void createCategories(String... categoryNames) + { + Locator.linkWithText("Manage Categories").findElement(getDriver()).click(); + Window categoryWindow = new Window.WindowFinder(getDriver()).withTitle("Manage Categories").waitFor(); + for (String categoryName : categoryNames) + addCategory(categoryName, categoryWindow); + clickButton("Done", 0); + categoryWindow.waitForClose(); + } + + private void addCategory(String categoryName, Window categoryWindow) + { + int attempt = 0; + while (true) + { + categoryWindow.clickButton("New Category", 0); + WebElement newCategoryField = Locator.input("label").withAttributeContaining("id", "textfield").notHidden().waitForElement(categoryWindow, WAIT_FOR_JAVASCRIPT); + setFormElementJS(newCategoryField, categoryName); + fireEvent(newCategoryField, SeleniumEvent.blur); + new WebDriverWait(getDriver(), Duration.ofSeconds(2)).until(ExpectedConditions.invisibilityOf(newCategoryField)); + try + { + Locator.tagWithText("div", categoryName).waitForElement(categoryWindow, 2_000); + break; + } + catch (NoSuchElementException e) + { + if (++attempt >= 3) + throw e; + } + } + } } diff --git a/src/org/labkey/test/tests/SampleTypeLinkToStudyTest.java b/src/org/labkey/test/tests/SampleTypeLinkToStudyTest.java index 65e877753d..c4e4fc2f8b 100644 --- a/src/org/labkey/test/tests/SampleTypeLinkToStudyTest.java +++ b/src/org/labkey/test/tests/SampleTypeLinkToStudyTest.java @@ -24,12 +24,10 @@ import org.labkey.test.params.FieldDefinition; import org.labkey.test.params.experiment.SampleTypeDefinition; import org.labkey.test.util.DataRegionTable; -import org.labkey.test.util.Ext4Helper; import org.labkey.test.util.PortalHelper; import org.labkey.test.util.SampleTypeHelper; import org.labkey.test.util.StudyHelper; import org.labkey.test.util.TestDataGenerator; -import org.openqa.selenium.WebElement; import java.io.File; import java.io.IOException; @@ -773,17 +771,7 @@ private void createNewVisits(String label, String startRange, String endRange) private void createDatasetCategory(String projectName, String name) { goToProjectHome(projectName); - goToManageViews(); - Locator.linkWithText("Manage Categories").findElement(getDriver()).click(); - _extHelper.waitForExtDialog("Manage Categories"); - Window categoryWindow = new Window.WindowFinder(getDriver()).withTitle("Manage Categories").waitFor(); - categoryWindow.clickButton("New Category", 0); - WebElement newCategoryField = Locator.input("label").withAttributeContaining("id", "textfield").notHidden().waitForElement(getDriver(), WAIT_FOR_JAVASCRIPT); - setFormElementJS(newCategoryField, name); - fireEvent(newCategoryField, SeleniumEvent.blur); - waitForElement(Ext4Helper.Locators.window("Manage Categories").append("//div").withText(name)); - clickButton("Done", 0); - _extHelper.waitForExtDialogToDisappear("Manage Categories"); + goToManageViews().createCategory(name); } private String getCategory(String projectName, String datasetName) From 3a1bb0b32905047779d647e28ad02e40662766e3 Mon Sep 17 00:00:00 2001 From: Dan Duffek Date: Mon, 18 Aug 2025 10:39:22 -0700 Subject: [PATCH 17/17] BackPort sample type test fix (#2636) --- src/org/labkey/test/tests/SampleTypeNameExpressionTest.java | 5 ++++- src/org/labkey/test/tests/SampleTypeRenameTest.java | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/org/labkey/test/tests/SampleTypeNameExpressionTest.java b/src/org/labkey/test/tests/SampleTypeNameExpressionTest.java index 827209e16e..264af5e0fe 100644 --- a/src/org/labkey/test/tests/SampleTypeNameExpressionTest.java +++ b/src/org/labkey/test/tests/SampleTypeNameExpressionTest.java @@ -71,7 +71,10 @@ public class SampleTypeNameExpressionTest extends BaseWebDriverTest { private static final String PROJECT_NAME = "SampleType_Name_Expression_Test"; - private static final String DEFAULT_SAMPLE_PARENT_VALUE = "SS" + TestDataGenerator.randomString(3).replaceAll("[_)]", "."); // '_' is used as delimiter to get batchRandomId and ) is used to close the defaultValue() + + // Issue 53548: Naming Pattern with a default value containing a () or {} cannot be saved. + private static final String DEFAULT_SAMPLE_PARENT_VALUE = "SS" + + EscapeUtil.escapeForNameExpression(TestDataGenerator.randomString(3, "{}()_")); private static final String PARENT_SAMPLE_TYPE = "PS" + DOMAIN_TRICKY_CHARACTERS; private static final String PARENT_SAMPLE_TYPE_INPUT = escapeForNameExpression(PARENT_SAMPLE_TYPE); diff --git a/src/org/labkey/test/tests/SampleTypeRenameTest.java b/src/org/labkey/test/tests/SampleTypeRenameTest.java index f5f8d40a16..e61a745ea0 100644 --- a/src/org/labkey/test/tests/SampleTypeRenameTest.java +++ b/src/org/labkey/test/tests/SampleTypeRenameTest.java @@ -102,6 +102,8 @@ public void testSampleTypeFieldRename() throws IOException, CommandException testDataGenerator.addCustomRow(Map.of(FIELD_INT, intVal++)); testDataGenerator.insertRows(); + SearchAdminAPIHelper.waitForIndexer(); + goToProjectHome(); SampleTypeHelper sampleHelper = new SampleTypeHelper(this); UpdateSampleTypePage updatePage = sampleHelper.goToEditSampleType(sampleTypeName);