From 04a40f513b2a773c194d1588991db9d95bea7752 Mon Sep 17 00:00:00 2001 From: Trey Chadick Date: Wed, 13 Aug 2025 08:40:30 -0700 Subject: [PATCH 1/7] 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 2/7] 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 3/7] 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 4/7] 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 5/7] 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 6/7] 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 7/7] 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);