From 27a42c540478a6902049242ab95a6c6eedd5a92e Mon Sep 17 00:00:00 2001 From: Trey Chadick Date: Thu, 11 Sep 2025 12:41:41 -0700 Subject: [PATCH 1/5] Make name validity check focus on the random portion (#2679) - Make name validity check focus on the random portion - Create `RandomName` record for randomly generated names --- src/org/labkey/test/params/ContainerInfo.java | 2 +- src/org/labkey/test/util/RandomName.java | 26 ++++++++++ .../labkey/test/util/TestDataGenerator.java | 48 +++++++++++-------- 3 files changed, 55 insertions(+), 21 deletions(-) create mode 100644 src/org/labkey/test/util/RandomName.java diff --git a/src/org/labkey/test/params/ContainerInfo.java b/src/org/labkey/test/params/ContainerInfo.java index 56def804c6..13a6227019 100644 --- a/src/org/labkey/test/params/ContainerInfo.java +++ b/src/org/labkey/test/params/ContainerInfo.java @@ -37,7 +37,7 @@ protected ContainerInfo(String name, ContainerInfo parentContainer, String folde { if (TestProperties.isTestRunningOnTeamCity()) { - String name = TestDataGenerator.randomName(folderName, TestDataGenerator.randomInt(0, 5), 5, RANDOM_CHARSET, null); + String name = TestDataGenerator.randomName(folderName, TestDataGenerator.randomInt(0, 5), 5, RANDOM_CHARSET, null).name(); if (name.startsWith("@")) { // Folder name may not begin with '@' diff --git a/src/org/labkey/test/util/RandomName.java b/src/org/labkey/test/util/RandomName.java new file mode 100644 index 0000000000..e45af8c0ad --- /dev/null +++ b/src/org/labkey/test/util/RandomName.java @@ -0,0 +1,26 @@ +package org.labkey.test.util; + +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +/** + * Record for a randomly generated name + * + * @param part The test-provided portion of the name + * @param name The full, randomly generated name + */ +public record RandomName(String part, String name) +{ + public RandomName(String part, String name) + { + this.part = part == null ? "" : part; // Don't trim + this.name = Objects.requireNonNull(name); + } + + @Override + public @NotNull String toString() + { + return name; + } +} diff --git a/src/org/labkey/test/util/TestDataGenerator.java b/src/org/labkey/test/util/TestDataGenerator.java index e53ac7b0aa..5dd16bfe4f 100644 --- a/src/org/labkey/test/util/TestDataGenerator.java +++ b/src/org/labkey/test/util/TestDataGenerator.java @@ -546,9 +546,9 @@ public static String randomMultiLineString(int size, @Nullable String exclusion) * @param exclusions characters that are to be excluded from the random parts of the name * @return a name with given characters that will be displayed as returned in the UI. */ - public static String randomName(@NotNull String part, int numStartChars, int numEndChars, String charSet, @Nullable String exclusions) + public static RandomName randomName(@NotNull String part, int numStartChars, int numEndChars, String charSet, @Nullable String exclusions) { - return (randomString(numStartChars, exclusions, charSet) + part + randomString(numEndChars, exclusions, charSet)).trim(); + return new RandomName(part, (randomString(numStartChars, exclusions, charSet) + part + randomString(numEndChars, exclusions, charSet)).trim()); } public static String randomDomainName() @@ -563,7 +563,7 @@ public static String randomDomainName(@Nullable String part) public static String randomInvalidDomainName(@Nullable String namePart, int numStartChars, int numEndChars) { - String domainName = randomName(namePart == null ? "" : namePart, numStartChars, numEndChars, ILLEGAL_DOMAIN_NAME_CHARSET, null); + String domainName = randomName(namePart == null ? "" : namePart, numStartChars, numEndChars, ILLEGAL_DOMAIN_NAME_CHARSET, null).name(); TestLogger.log("Generated random invalid domain name: " + domainName); return domainName; } @@ -583,20 +583,20 @@ public static String randomDomainName(@Nullable String namePart, @Nullable Domai */ public static String randomDomainName(@Nullable String namePart, @Nullable Integer numStartChars, @Nullable Integer numEndChars, @Nullable DomainKind domainKind) { - String _namePart = namePart == null ? "" : namePart; + namePart = namePart == null ? "" : namePart; DomainKind _domainKind = domainKind == null ? DomainKind.SampleSet : domainKind; String charSet = ALPHANUMERIC_STRING + DOMAIN_SPECIAL_STRING; int currentTries = 0; - String domainName = randomName(_namePart, getNumChars(numStartChars, 5), getNumChars(numEndChars, 50), charSet, null); - while (isDomainAndFieldNameInvalid(_domainKind, domainName, null)) + RandomName randomName = randomName(namePart, getNumChars(numStartChars, 5), getNumChars(numEndChars, 50), charSet, null); + while (isDomainAndFieldNameInvalid(_domainKind, randomName, null)) { - domainName = randomName(_namePart, getNumChars(numStartChars, 5), getNumChars(numEndChars, 50), charSet, null); + randomName = randomName(namePart, getNumChars(numStartChars, 5), getNumChars(numEndChars, 50), charSet, null); if (++currentTries >= MAX_RANDOM_TRIES) - throw new IllegalStateException("Failed to generate a valid domain name after " + MAX_RANDOM_TRIES + " tries. Last generated name: " + domainName); + throw new IllegalStateException("Failed to generate a valid domain name after " + MAX_RANDOM_TRIES + " tries. Last generated name: " + randomName); } // Multiple spaces in the UI are collapsed into a single space. If we need to test for handling of multiple spaces, we'll not use this generator - domainName = domainName.replaceAll("\\s+", " "); + String domainName = randomName.name().replaceAll("\\s+", " "); TestLogger.log("Generated random domain name for domainKind " + _domainKind + ": " + domainName); return domainName; @@ -632,7 +632,7 @@ public static String randomFieldName(@NotNull String part, @Nullable Integer num + WIDE_PLACEHOLDER + REPEAT_PLACEHOLDER + ALL_CHARS_PLACEHOLDER; int currentTries = 0; - String randomFieldName = randomName(part, getNumChars(numStartChars, 5), getNumChars(numEndChars, 50), chars, exclusion); + RandomName randomFieldName = randomName(part, getNumChars(numStartChars, 5), getNumChars(numEndChars, 50), chars, exclusion); while (isDomainAndFieldNameInvalid(_domainKind, null, randomFieldName)) { randomFieldName = randomName(part, getNumChars(numStartChars, 5), getNumChars(numEndChars, 50), chars, exclusion); @@ -641,12 +641,13 @@ public static String randomFieldName(@NotNull String part, @Nullable Integer num } TestLogger.log("Generated random field name for domainKind " + _domainKind + ": " + randomFieldName); - return randomFieldName; + return randomFieldName.name(); } - private static boolean isDomainAndFieldNameInvalid(DomainKind domainKind, @Nullable String domainName, @Nullable String fieldName) + private static boolean isDomainAndFieldNameInvalid(DomainKind domainKind, @Nullable RandomName domainName, @Nullable RandomName fieldName) { - if (fieldName != null && fieldName.length() > 64 && fieldName.toLowerCase().contains("key")) // Not guaranteed but likely a list key + // TODO: remove when merging to develop + if (fieldName != null && fieldName.name().length() > 64 && fieldName.part().toLowerCase().contains("key")) // Not guaranteed but likely a list key return true; // Issue 53706: List key field name length is limited to 64 characters if (TestProperties.isRemoteNameValidationEnabled()) @@ -659,7 +660,7 @@ private static boolean isDomainAndFieldNameInvalid(DomainKind domainKind, @Nulla } } - private static boolean isNameInvalidRemote(DomainKind domainKind, @Nullable String domainName, @Nullable String fieldName) + private static boolean isNameInvalidRemote(DomainKind domainKind, @Nullable RandomName domainName, @Nullable RandomName fieldName) { SimplePostCommand command = new SimplePostCommand("property", "validateDomainAndFieldNames"); JSONObject domainDesign = new JSONObject(); @@ -696,13 +697,14 @@ private static boolean isNameInvalidRemote(DomainKind domainKind, @Nullable Stri } } - public static boolean isNameInvalidLocal(DomainKind domainKind, @Nullable String domainName, @Nullable String fieldName) + private static final Pattern COLON_NAME_PATTERN = Pattern.compile(":[a-zA-Z]{3}"); // Avoid illegal patterns like ":Date" + private static boolean isNameInvalidLocal(DomainKind domainKind, @Nullable RandomName domainName, @Nullable RandomName fieldName) { if (domainName != null) { - if (!Character.isLetterOrDigit(domainName.charAt(0))) + if (!Character.isLetterOrDigit(domainName.name().charAt(0))) return true; // domain needs to start with alphanumeric char - if (Pattern.matches("(.*\\s--[^ ].*)|(.*\\s-[^- ].*)", domainName)) + if (Pattern.matches("(.*\\s--[^ ].*)|(.*\\s-[^- ].*)", domainName.name())) return true; // domain name must not contain space followed by dash. (command like: Issue 49161) int maxLength = switch (domainKind) @@ -711,14 +713,20 @@ public static boolean isNameInvalidLocal(DomainKind domainKind, @Nullable String case SampleSet -> 100; default -> 200; // Sources, lists, and datasets allow 200 character names }; - if (domainName.length() > maxLength) + if (domainName.name().length() > maxLength) + return true; + if (COLON_NAME_PATTERN.matcher(domainName.name()) + .results().map(mr -> mr.group(0)) + .anyMatch(s -> !domainName.part().contains(s))) // Only check random portion of the name return true; } if (fieldName != null) { - if (fieldName.length() > 200) + if (fieldName.name().length() > 200) return true; - if (Pattern.matches(".*:[a-zA-Z]{3}.*", fieldName)) // Avoid illegal patterns like ":Date" + if (COLON_NAME_PATTERN.matcher(fieldName.name()) + .results().map(mr -> mr.group(0)) + .anyMatch(s -> !fieldName.part().contains(s))) // Only check random portion of the name return true; } From 8dc1eee348f260520084303f86e145da4af26364 Mon Sep 17 00:00:00 2001 From: Trey Chadick Date: Thu, 11 Sep 2025 15:57:18 -0700 Subject: [PATCH 2/5] Workaround Issue 53915: Lineage panel grids don't show source names (#2684) --- .../ui/entities/ParentEntityEditPanel.java | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/org/labkey/test/components/ui/entities/ParentEntityEditPanel.java b/src/org/labkey/test/components/ui/entities/ParentEntityEditPanel.java index 202a677f91..b5b543ec55 100644 --- a/src/org/labkey/test/components/ui/entities/ParentEntityEditPanel.java +++ b/src/org/labkey/test/components/ui/entities/ParentEntityEditPanel.java @@ -1,5 +1,7 @@ package org.labkey.test.components.ui.entities; +import org.awaitility.Awaitility; +import org.hamcrest.CoreMatchers; import org.junit.Assert; import org.labkey.test.BootstrapLocators; import org.labkey.test.Locator; @@ -14,6 +16,7 @@ import org.openqa.selenium.WebElement; import org.openqa.selenium.support.ui.ExpectedConditions; +import java.time.Duration; import java.util.Arrays; import java.util.HashSet; import java.util.List; @@ -185,11 +188,16 @@ public void clickSave(int waitTime) Panel detailsPanel = new Panel.PanelFinder(getDriver()).withTitle(parentType).waitFor(getDriver()); if (!selections.isEmpty()) { - for (String selection : selections) - { - getWrapper().quickWait().until(ExpectedConditions.visibilityOf( - Locator.linkWithText(selection).findWhenNeeded(detailsPanel))); - } + // Just wait for the correct number of parents/sources to appear for now. + Locator.CssLocator rowLocator = Locator.css(".grid-panel tbody tr"); + Awaitility.await("Total " + parentType + " grid rows").atMost(Duration.ofSeconds(2)) + .until(() -> rowLocator.findElements(detailsPanel).size(), CoreMatchers.equalTo(selections.size())); + // Issue 53915: Lineage panel grids don't show IDs or links for parent sequences and molecules in Biologics + // for (String selection : selections) + // { + // getWrapper().quickWait().until(ExpectedConditions.visibilityOf( + // Locator.linkWithText(selection).findWhenNeeded(detailsPanel))); + // } } else { From 360d9a32c795a59677ad83659e42ce5c7e402a21 Mon Sep 17 00:00:00 2001 From: Dan Duffek Date: Fri, 12 Sep 2025 13:37:37 -0700 Subject: [PATCH 3/5] Fixing various LKSM test issues. (#2675) --- .../ui/grids/FieldSelectionDialog.java | 38 +++++++++---------- .../test/selenium/ReclickingWebElement.java | 10 ++--- .../labkey/test/tests/LinkedSchemaTest.java | 5 +-- 3 files changed, 24 insertions(+), 29 deletions(-) diff --git a/src/org/labkey/test/components/ui/grids/FieldSelectionDialog.java b/src/org/labkey/test/components/ui/grids/FieldSelectionDialog.java index c7c01bbcae..994ee13f92 100644 --- a/src/org/labkey/test/components/ui/grids/FieldSelectionDialog.java +++ b/src/org/labkey/test/components/ui/grids/FieldSelectionDialog.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.stream.Collectors; +import static org.labkey.test.util.TextUtils.normalizeSpace; import static org.labkey.test.util.selenium.WebElementUtils.getTextContent; /** @@ -180,13 +181,13 @@ public WebElement getAvailableFieldElement(FieldKey fieldKey) if(iterator.hasNext()) { // If the field is already expanded don't try to expand it. - if(!isFieldKeyExpanded(elementCache().findAvailableField(fieldKey.toString()))) - expandOrCollapseByFieldKey(fieldKey.toString(), true); + if(!isFieldKeyExpanded(elementCache().findAvailableField(fieldKey))) + expandOrCollapseByFieldKey(fieldKey, true); } } - return elementCache().findAvailableField(fieldKey.toString()); + return elementCache().findAvailableField(fieldKey); } /** @@ -195,7 +196,7 @@ public WebElement getAvailableFieldElement(FieldKey fieldKey) * @param fieldKey The data-fieldkey value of the field to expand. * @param expand True to expand false to collapse. */ - private void expandOrCollapseByFieldKey(String fieldKey, boolean expand) + private void expandOrCollapseByFieldKey(FieldKey fieldKey, boolean expand) { WebElement listItem = elementCache().findAvailableField(fieldKey); @@ -433,7 +434,7 @@ public FieldSelectionDialog setFieldLabel(String fieldName, String newFieldLabel */ public FieldSelectionDialog setFieldLabel(FieldKey fieldKey, String newFieldLabel) { - WebElement listItem = elementCache().findSelectedField(fieldKey.toString()); + WebElement listItem = elementCache().findSelectedField(fieldKey); WebElement updateIcon = Locator.tagWithClass("span", "edit-inline-field__toggle").findWhenNeeded(listItem); updateIcon.click(); @@ -453,9 +454,9 @@ public FieldSelectionDialog setFieldLabel(FieldKey fieldKey, String newFieldLabe getWrapper().mouseOver(elementCache().title); // Dismiss tooltip - WebDriverWrapper.waitFor(()->!elementCache().fieldLabelEdit.isDisplayed() && - elementCache().getListItemElement(elementCache().selectedFieldsPanel, newFieldLabel).isDisplayed(), + WebDriverWrapper.waitFor(()->!elementCache().fieldLabelEdit.isDisplayed(), String.format("New field label '%s' is not in the list.", newFieldLabel), 500); + Assert.assertEquals("Label after update", normalizeSpace(newFieldLabel), elementCache().getFieldLabel(fieldKey)); return this; } @@ -503,10 +504,10 @@ private List getSelectedListItems(String fieldLabel) * @param beforeTarget Will the field being moved go before (above) or after (below) the target field. * @return This dialog. */ - public FieldSelectionDialog repositionField(String fieldToMove, String targetField, boolean beforeTarget) + public FieldSelectionDialog repositionField(FieldKey fieldToMove, FieldKey targetField, boolean beforeTarget) { - WebElement elementToMove = elementCache().getListItemElement(elementCache().selectedFieldsPanel, fieldToMove); - WebElement elementTarget = elementCache().getListItemElement(elementCache().selectedFieldsPanel, targetField); + WebElement elementToMove = elementCache().findSelectedField(fieldToMove); + WebElement elementTarget = elementCache().findSelectedField(targetField); int yBefore = elementToMove.getRect().getY(); @@ -633,28 +634,27 @@ protected List getListItemElements(WebElement panel, String fieldLab .findElements(panel); } - // Will get the first list item that matches the fieldLabel. - protected WebElement getListItemElement(WebElement panel, String fieldLabel) + protected String getFieldLabel(FieldKey fieldKey) { - return Locator.tagWithClass("div", "list-group-item") - .withDescendant(Locator.tagWithClass("div", "field-caption").withText(fieldLabel)) - .findElement(panel); + return Locator.tagWithClass("div", "field-caption") + .findElement(findFieldRow(fieldKey, selectedFieldsPanel)) + .getText(); } - protected WebElement findSelectedField(String fieldKey) + protected WebElement findSelectedField(FieldKey fieldKey) { return findFieldRow(fieldKey, selectedFieldsPanel); } - protected WebElement findAvailableField(String fieldKey) + protected WebElement findAvailableField(FieldKey fieldKey) { return findFieldRow(fieldKey, availableFieldsPanel); } - protected WebElement findFieldRow(String fieldKey, WebElement panel) + protected WebElement findFieldRow(FieldKey fieldKey, WebElement panel) { return Locator.tagWithClass("div", "list-group-item") - .withAttributeIgnoreCase("data-fieldkey", fieldKey) + .withAttributeIgnoreCase("data-fieldkey", fieldKey.toString()) .findElement(panel); } diff --git a/src/org/labkey/test/selenium/ReclickingWebElement.java b/src/org/labkey/test/selenium/ReclickingWebElement.java index cf0e34f256..0417cf1c19 100644 --- a/src/org/labkey/test/selenium/ReclickingWebElement.java +++ b/src/org/labkey/test/selenium/ReclickingWebElement.java @@ -15,7 +15,6 @@ */ package org.labkey.test.selenium; -import org.apache.commons.lang3.Strings; import org.apache.commons.lang3.mutable.Mutable; import org.apache.commons.lang3.mutable.MutableObject; import org.jetbrains.annotations.NotNull; @@ -212,18 +211,15 @@ private void revealElement(WebElement el, String shortMessage) if (!blockResolved) { + // Move mouse to corner to dismiss tooltips + new Actions(getDriver()).moveToLocation(0,0).perform(); + Locator.XPathLocator interceptingElLoc = parseInterceptingElementLoc(shortMessage); if (interceptingElLoc != null) { List interceptingElements = interceptingElLoc.findElements(getDriver()); TestLogger.debug("Found %s element(s) matching extracted locator: %s".formatted(interceptingElements.size(), shortMessage)); - if (Strings.CI.containsAny(interceptingElLoc.toString(), "popover", "ws-pre-wrap", "tip")) - { - // Move mouse to corner to dismiss tooltips - new Actions(getDriver()).moveToLocation(0,0).perform(); - } - if (interceptingElements.size() == 1) { //noinspection ResultOfMethodCallIgnored diff --git a/src/org/labkey/test/tests/LinkedSchemaTest.java b/src/org/labkey/test/tests/LinkedSchemaTest.java index fa7ccf7ca3..dbac31103c 100644 --- a/src/org/labkey/test/tests/LinkedSchemaTest.java +++ b/src/org/labkey/test/tests/LinkedSchemaTest.java @@ -44,7 +44,6 @@ import org.labkey.test.util.AuditLogHelper; import org.labkey.test.util.DataRegionTable; import org.labkey.test.util.LogMethod; -import org.labkey.test.util.PortalHelper; import org.labkey.test.util.SchemaHelper; import org.labkey.test.util.TestDataGenerator; import org.labkey.test.util.exp.DataClassAPIHelper; @@ -1057,7 +1056,7 @@ private void createExperiment(String externalProject, String subFolder, String s goToProjectHome(externalProject); clickTab("Experiment"); waitAndClickAndWait(Locator.linkContainingText("Create Run Group")); - setFormElement(Locator.name("name"), "Parent Run Group"); + setFormElement(Locator.name("Name"), "Parent Run Group"); clickButton("Submit"); _containerHelper.createSubfolder(externalProject, subFolder); @@ -1065,7 +1064,7 @@ private void createExperiment(String externalProject, String subFolder, String s // Create a RunGroup in the subfolder. clickTab("Experiment"); waitAndClickAndWait(Locator.linkContainingText("Create Run Group")); - setFormElement(Locator.name("name"), subFolderRunGroup); + setFormElement(Locator.name("Name"), subFolderRunGroup); clickButton("Submit"); } From b175c18235db866b228887a736e4f2d8aea832fc Mon Sep 17 00:00:00 2001 From: Trey Chadick Date: Tue, 16 Sep 2025 11:11:21 -0700 Subject: [PATCH 4/5] Make `TestDataGenerator.isNameInvalidLocal` check for blank names (#2690) --- src/org/labkey/test/util/TestDataGenerator.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/org/labkey/test/util/TestDataGenerator.java b/src/org/labkey/test/util/TestDataGenerator.java index 5dd16bfe4f..aa9ab14106 100644 --- a/src/org/labkey/test/util/TestDataGenerator.java +++ b/src/org/labkey/test/util/TestDataGenerator.java @@ -702,6 +702,8 @@ private static boolean isNameInvalidLocal(DomainKind domainKind, @Nullable Rando { if (domainName != null) { + if (domainName.name().isBlank()) + return true; if (!Character.isLetterOrDigit(domainName.name().charAt(0))) return true; // domain needs to start with alphanumeric char if (Pattern.matches("(.*\\s--[^ ].*)|(.*\\s-[^- ].*)", domainName.name())) @@ -722,6 +724,8 @@ private static boolean isNameInvalidLocal(DomainKind domainKind, @Nullable Rando } if (fieldName != null) { + if (fieldName.name().isBlank()) + return true; if (fieldName.name().length() > 200) return true; if (COLON_NAME_PATTERN.matcher(fieldName.name()) From ae54ab614d7d2af1bef9ffbd1c075fd5c38568ca Mon Sep 17 00:00:00 2001 From: cnathe Date: Wed, 17 Sep 2025 08:24:52 -0500 Subject: [PATCH 5/5] remove TODO after merge to develop --- src/org/labkey/test/util/TestDataGenerator.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/org/labkey/test/util/TestDataGenerator.java b/src/org/labkey/test/util/TestDataGenerator.java index 27b257abd8..21620a4f08 100644 --- a/src/org/labkey/test/util/TestDataGenerator.java +++ b/src/org/labkey/test/util/TestDataGenerator.java @@ -646,10 +646,6 @@ public static String randomFieldName(@NotNull String part, @Nullable Integer num private static boolean isDomainAndFieldNameInvalid(DomainKind domainKind, @Nullable RandomName domainName, @Nullable RandomName fieldName) { - // TODO: remove when merging to develop - if (fieldName != null && fieldName.name().length() > 64 && fieldName.part().toLowerCase().contains("key")) // Not guaranteed but likely a list key - return true; // Issue 53706: List key field name length is limited to 64 characters - if (TestProperties.isRemoteNameValidationEnabled()) { return isNameInvalidRemote(domainKind, domainName, fieldName);