From 87afc4cd91e45814d79cdc2bec8da8a2fb9d4565 Mon Sep 17 00:00:00 2001 From: Karl Lum Date: Wed, 3 Sep 2025 14:58:08 -0700 Subject: [PATCH 1/6] Regression for manage views exception (#2657) --- .../test/tests/DataViewsReportOrderingTest.java | 11 +++++++++++ src/org/labkey/test/util/DataRegionTable.java | 7 +++++++ 2 files changed, 18 insertions(+) diff --git a/src/org/labkey/test/tests/DataViewsReportOrderingTest.java b/src/org/labkey/test/tests/DataViewsReportOrderingTest.java index bf6c17c243..e53a35a584 100644 --- a/src/org/labkey/test/tests/DataViewsReportOrderingTest.java +++ b/src/org/labkey/test/tests/DataViewsReportOrderingTest.java @@ -25,6 +25,8 @@ import org.labkey.test.TestTimeoutException; import org.labkey.test.categories.Daily; import org.labkey.test.components.ext4.Window; +import org.labkey.test.pages.reports.ManageViewsPage; +import org.labkey.test.pages.user.ShowUsersPage; import org.labkey.test.util.PortalHelper; import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.WebDriver; @@ -133,6 +135,15 @@ private String[] reverseReports(List reportsOriginalOrder) return reverseReports; } + @Test + public void testRootFolderAccess() + { + // Regression for issue 53630 + ShowUsersPage showUsersPage = goToSiteUsers(); + ManageViewsPage mvp = showUsersPage.getUsersTable().openManageViews(); + mvp.clickAddReport("R Report"); + } + @Override protected BrowserType bestBrowser() { diff --git a/src/org/labkey/test/util/DataRegionTable.java b/src/org/labkey/test/util/DataRegionTable.java index aacbb3d46d..26d73838ff 100644 --- a/src/org/labkey/test/util/DataRegionTable.java +++ b/src/org/labkey/test/util/DataRegionTable.java @@ -40,6 +40,7 @@ import org.labkey.test.pages.ImportDataPage; import org.labkey.test.pages.TimeChartWizard; import org.labkey.test.pages.query.UpdateQueryRowPage; +import org.labkey.test.pages.reports.ManageViewsPage; import org.labkey.test.params.FieldKey; import org.labkey.test.selenium.LazyWebElement; import org.labkey.test.selenium.RefindingWebElement; @@ -166,6 +167,12 @@ public CustomizeView openCustomizeGrid() return getCustomizeView(); } + public ManageViewsPage openManageViews() + { + getViewsMenu().clickSubMenu(false, "Manage Views"); + return new ManageViewsPage(getDriver()); + } + protected DataRegionExportHelper getExportPanel() { if (_exportHelper == null) From 920eefe9638ddd718ec4eb8b92fead14a6fbc34c Mon Sep 17 00:00:00 2001 From: Dan Duffek Date: Thu, 4 Sep 2025 11:54:05 -0700 Subject: [PATCH 2/6] Cherry-pick TestFileUtils.makeLegalFileName from fb_lkbDomainFuzzing in develop. (#2670) --- src/org/labkey/test/TestFileUtils.java | 41 +++++++------------------- 1 file changed, 10 insertions(+), 31 deletions(-) diff --git a/src/org/labkey/test/TestFileUtils.java b/src/org/labkey/test/TestFileUtils.java index 1848c71a7b..a860e0481e 100644 --- a/src/org/labkey/test/TestFileUtils.java +++ b/src/org/labkey/test/TestFileUtils.java @@ -19,6 +19,7 @@ import org.apache.commons.compress.archivers.ArchiveStreamFactory; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.io.FileSystem; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; @@ -27,8 +28,6 @@ import org.apache.pdfbox.Loader; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.text.PDFTextStripper; -import org.apache.poi.xssf.streaming.SXSSFRow; -import org.apache.poi.xssf.streaming.SXSSFWorkbook; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.openpgp.PGPCompressedData; import org.bouncycastle.openpgp.PGPEncryptedDataList; @@ -41,12 +40,10 @@ import org.bouncycastle.openpgp.operator.jcajce.JcePBEDataDecryptorFactoryBuilder; import org.bouncycastle.util.io.Streams; import org.jetbrains.annotations.NotNull; -import org.labkey.serverapi.reader.Readers; import org.openqa.selenium.NotFoundException; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; -import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; @@ -71,6 +68,7 @@ import java.util.List; import java.util.Set; import java.util.TreeSet; +import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.zip.GZIPInputStream; import java.util.zip.ZipEntry; @@ -707,34 +705,15 @@ public static byte[] decrypt(byte[] encrypted, char[] passPhrase) throws IOExcep return Streams.readAll(ld.getInputStream()); } + private static final Pattern badChars = Pattern.compile("[\\\\:/\\[\\]?*|]"); - public static File convertTabularToXlsx(File tabularFile, String delimiter, String sheetName, String xlsxFileName) throws IOException, PGPException + /** + * Determining expected file names for downloaded files that are named according to some + * value that might include characters that are not legal for files + * @see FileSystem#toLegalFileName(String, char) + */ + public static String makeLegalFileName(String candidate) { - File excelFile = new File(getTestTempDir(), xlsxFileName); - FileUtils.forceMkdirParent(excelFile); - - try(SXSSFWorkbook workBook = new SXSSFWorkbook(1000); // holds 1000 rows at a time - BufferedReader br = Readers.getReader(tabularFile); - FileOutputStream out = new FileOutputStream(excelFile)) - { - var sheet = workBook.createSheet(sheetName); - - String currentLine; - int rowNum=0; - - while ((currentLine = br.readLine()) != null) - { - String str[] = currentLine.split(delimiter); - SXSSFRow currentRow = sheet.createRow(rowNum); - for (int i = 0; i < str.length; i++) - { - currentRow.createCell(i).setCellValue(str[i]); - } - rowNum++; - } - workBook.write(out); // flush remaining rows - } - - return excelFile; + return badChars.matcher(candidate).replaceAll("_"); } } From cf3937fdfa3fe31f79dce67416c87c6a50859723 Mon Sep 17 00:00:00 2001 From: Nick Kerr Date: Fri, 5 Sep 2025 10:00:25 -0700 Subject: [PATCH 3/6] FileBrowserHelper.getFileDetailInfo: Support more refined filtering (#2668) --- src/org/labkey/test/AssayAPITest.java | 6 +- src/org/labkey/test/tests/SampleTypeTest.java | 10 +- .../labkey/test/util/FileBrowserHelper.java | 95 ++++++++++++++----- 3 files changed, 78 insertions(+), 33 deletions(-) diff --git a/src/org/labkey/test/AssayAPITest.java b/src/org/labkey/test/AssayAPITest.java index 1ccaa3fcd5..201db65e20 100644 --- a/src/org/labkey/test/AssayAPITest.java +++ b/src/org/labkey/test/AssayAPITest.java @@ -311,8 +311,8 @@ public void testImportRun_dataRows() throws Exception assayHelper.importAssay(assayId, runName, dataRowsInvalidResultFileDirectory, getProjectName(), Collections.singletonMap("RunFileField", CREST_FILE.getName()), Collections.emptyMap(), "DataFileField: Invalid file path: ../"); // valid run file and valid result file - FileBrowserHelper.FileDetailInfo runFileInfo = _fileBrowserHelper.getFileDetailInfo(getProjectName(), CREST_FILE.getName()); - FileBrowserHelper.FileDetailInfo resultFileInfo = _fileBrowserHelper.getFileDetailInfo(getProjectName(), SCREENSHOT_FILE.getName()); + FileBrowserHelper.FileDetailInfo runFileInfo = FileBrowserHelper.getFileDetailInfo(getProjectName(), CREST_FILE.getName()); + FileBrowserHelper.FileDetailInfo resultFileInfo = FileBrowserHelper.getFileDetailInfo(getProjectName(), SCREENSHOT_FILE.getName()); List> scenarios = List.of(new Pair<>(CREST_FILE.getName(), SCREENSHOT_FILE.getName()), new Pair<>(runFileInfo.absoluteFilePath(), resultFileInfo.absoluteFilePath()), new Pair<>(runFileInfo.webDavUrl(), resultFileInfo.webDavUrl()), @@ -388,7 +388,7 @@ public void testGpatSaveBatch() throws Exception goToModule("FileContent"); _fileBrowserHelper.uploadFile(HELP_ICON_FILE); goToManageAssays(); - FileBrowserHelper.FileDetailInfo runFileInfo = _fileBrowserHelper.getFileDetailInfo(getProjectName(), "help.jpg"); + FileBrowserHelper.FileDetailInfo runFileInfo = FileBrowserHelper.getFileDetailInfo(getProjectName(), "help.jpg"); ((APIAssayHelper) _assayHelper).saveBatch(assayName, "Valid absolute path", Collections.singletonMap("RunFileField", runFileInfo.absoluteFilePath()), resultRows, getProjectName(), null); ((APIAssayHelper) _assayHelper).saveBatch(assayName, "Valid webdav full path", Collections.singletonMap("RunFileField", runFileInfo.webDavUrl()), resultRows, getProjectName(), null); ((APIAssayHelper) _assayHelper).saveBatch(assayName, "Valid webdav relative path", Collections.singletonMap("RunFileField", runFileInfo.webDavUrlRelative()), resultRows, getProjectName(), null); diff --git a/src/org/labkey/test/tests/SampleTypeTest.java b/src/org/labkey/test/tests/SampleTypeTest.java index 4962306eb9..a48eb18528 100644 --- a/src/org/labkey/test/tests/SampleTypeTest.java +++ b/src/org/labkey/test/tests/SampleTypeTest.java @@ -1608,9 +1608,9 @@ public void testFilePathOnBulkImport() _fileBrowserHelper.uploadFile(TestFileUtils.getSampleData(testFileHomeName)); _fileBrowserHelper.uploadFile(TestFileUtils.getSampleData(testFileHomeNameB)); _fileBrowserHelper.createFolder(homeFileDirectory); - FileBrowserHelper.FileDetailInfo homeFileInfo = _fileBrowserHelper.getFileDetailInfo(PROJECT_NAME, testFileHomeName); - FileBrowserHelper.FileDetailInfo homeFileBInfo = _fileBrowserHelper.getFileDetailInfo(PROJECT_NAME, testFileHomeNameB); - FileBrowserHelper.FileDetailInfo homeDirInfo = _fileBrowserHelper.getFileDetailInfo(PROJECT_NAME, homeFileDirectory); + FileBrowserHelper.FileDetailInfo homeFileInfo = FileBrowserHelper.getFileDetailInfo(PROJECT_NAME, testFileHomeName); + FileBrowserHelper.FileDetailInfo homeFileBInfo = FileBrowserHelper.getFileDetailInfo(PROJECT_NAME, testFileHomeNameB); + FileBrowserHelper.FileDetailInfo homeDirInfo = FileBrowserHelper.getFileDetailInfo(PROJECT_NAME, homeFileDirectory); String folderContainerPath = PROJECT_NAME + "/" + FOLDER_NAME; String testFileSubName = "sampleType.tsv"; @@ -1619,8 +1619,8 @@ public void testFilePathOnBulkImport() goToModule("FileContent"); _fileBrowserHelper.uploadFile(TestFileUtils.getSampleData(testFileSubName)); _fileBrowserHelper.createFolder(subFileDirectory); - FileBrowserHelper.FileDetailInfo subFileInfo = _fileBrowserHelper.getFileDetailInfo(folderContainerPath, testFileSubName); - FileBrowserHelper.FileDetailInfo subDirInfo = _fileBrowserHelper.getFileDetailInfo(folderContainerPath, subFileDirectory); + FileBrowserHelper.FileDetailInfo subFileInfo = FileBrowserHelper.getFileDetailInfo(folderContainerPath, testFileSubName); + FileBrowserHelper.FileDetailInfo subDirInfo = FileBrowserHelper.getFileDetailInfo(folderContainerPath, subFileDirectory); goToProjectHome(); clickAndWait(Locator.linkWithText(sampleTypeNameHome)); diff --git a/src/org/labkey/test/util/FileBrowserHelper.java b/src/org/labkey/test/util/FileBrowserHelper.java index b4ac3b149e..f4fee662d8 100644 --- a/src/org/labkey/test/util/FileBrowserHelper.java +++ b/src/org/labkey/test/util/FileBrowserHelper.java @@ -18,13 +18,12 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.mutable.Mutable; import org.apache.commons.lang3.mutable.MutableObject; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.junit.Assert; import org.labkey.remoteapi.CommandException; -import org.labkey.remoteapi.Connection; import org.labkey.remoteapi.query.Filter; import org.labkey.remoteapi.query.SelectRowsCommand; -import org.labkey.remoteapi.query.SelectRowsResponse; import org.labkey.test.BaseWebDriverTest; import org.labkey.test.Locator; import org.labkey.test.SortDirection; @@ -51,9 +50,11 @@ import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Map; +import static java.util.Collections.emptyList; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; @@ -880,56 +881,100 @@ public void openFolderTree() } } - private String stringOrNull(Object value) + private static String stringOrNull(Object value) { if (value == null) return null; return (String) value; } - public record FileDetailInfo(String fileName, String absoluteFilePath, String dataFileUrl, String webDavUrl, String webDavUrlRelative) + public record FileDetailInfo(String fileName, String absoluteFilePath, String dataFileUrl, String relativeFolder, String webDavUrl, String webDavUrlRelative) { } - public FileDetailInfo getFileDetailInfo(String containerPath, String fileName) + /** + * Queries the "exp"."files" table to gather metadata about a specific file. + * + * @param containerPath The container path in which to make the request. + * @param fileName The name of the file to find. + */ + public static @Nullable FileDetailInfo getFileDetailInfo(String containerPath, String fileName) + { + return getFileDetailInfo(containerPath, fileName, null); + } + + /** + * Queries the "exp"."files" table to gather metadata about a specific file. Optionally, a relativeFolder can be + * supplied to match against the "RelativeFolder" column. This is useful when looking for files in a specific + * subdirectory. + * + * @param containerPath The container path in which to make the request. + * @param fileName The name of the file to find. + * @param relativeFolder The expected value of the "RelativeFolder" column. If null, the column is not checked. + */ + public static @Nullable FileDetailInfo getFileDetailInfo(String containerPath, String fileName, @Nullable String relativeFolder) + { + var fileInfos = getFileDetailInfos(containerPath, List.of(new Filter("Name", fileName))); + + // "RelativeFolder" is not a filterable column on the query table + if (relativeFolder != null) + fileInfos = fileInfos.stream().filter(f -> relativeFolder.equals(f.relativeFolder)).toList(); + + if (fileInfos.size() > 1) + { + String message = String.format("%d files found with name \"%s\"", fileInfos.size(), fileName); + if (relativeFolder != null) + message += String.format(" in relative folder \"%s\"", relativeFolder); + message += ". Expected 0 or 1 files with that name."; + + throw new AssertionError(message); + } + + return fileInfos.isEmpty() ? null : fileInfos.get(0); + } + + private static @NotNull List getFileDetailInfos(String containerPath, @Nullable Collection filters) { - List filePathColumns = List.of("AbsoluteFilePath", "FileExists", "DataFileUrl", "WebDavUrl", "WebDavUrlRelative"); try { - Connection cn = WebTestHelper.getRemoteApiConnection(); - SelectRowsCommand cmd = new SelectRowsCommand("exp", "files"); - cmd.addFilter("Name", fileName, Filter.Operator.EQUAL); - cmd.setColumns(filePathColumns); - SelectRowsResponse response = cmd.execute(cn, "/" + containerPath); + var cmd = new SelectRowsCommand("exp", "files"); + cmd.setColumns(List.of("AbsoluteFilePath", "DataFileUrl", "FileExists", "Name", "RelativeFolder", "WebDavUrl", "WebDavUrlRelative")); + + if (filters != null) + { + for (var filter : filters) + cmd.addFilter(filter); + } + + var response = cmd.execute(WebTestHelper.getRemoteApiConnection(), "/" + containerPath); - for (Map row: response.getRows()) + var files = new ArrayList(); + for (var row : response.getRows()) { if (!(Boolean) row.get("FileExists")) continue; - Object absoluteFilePath = row.get("AbsoluteFilePath"); - Object dataFileUrl = row.get("DataFileUrl"); - Object webDavUrl = row.get("WebDavUrl"); - Object webDavUrlRelative = row.get("WebDavUrlRelative"); - return new FileDetailInfo(fileName, stringOrNull(absoluteFilePath), stringOrNull(dataFileUrl), stringOrNull(webDavUrl), stringOrNull(webDavUrlRelative)); + String fileName = stringOrNull(row.get("Name")); + String absoluteFilePath = stringOrNull(row.get("AbsoluteFilePath")); + String dataFileUrl = stringOrNull(row.get("DataFileUrl")); + String relativeFolder = stringOrNull(row.get("RelativeFolder")); + String webDavUrl = stringOrNull(row.get("WebDavUrl")); + String webDavUrlRelative = stringOrNull(row.get("WebDavUrlRelative")); + files.add(new FileDetailInfo(fileName, absoluteFilePath, dataFileUrl, relativeFolder, webDavUrl, webDavUrlRelative)); } + + return files; } catch (CommandException ce) { - if (ce.getStatusCode() == 404) - { - return null; - } - else - { + if (ce.getStatusCode() != 404) throw new RuntimeException(ce); - } } catch (IOException ioe) { throw new RuntimeException(ioe); } - return null; + return emptyList(); } // See PageFlowUtil.encodeURIComponent() From 6f1b477a6b06f0a0649ff33f18f91e0f74651460 Mon Sep 17 00:00:00 2001 From: Trey Chadick Date: Fri, 5 Sep 2025 10:19:16 -0700 Subject: [PATCH 4/6] Regression test for 53629: NavMenu doesn't open in new tab correctly (#2673) --- src/org/labkey/test/WebDriverWrapper.java | 8 +++++ src/org/labkey/test/tests/DataRegionTest.java | 32 +++++++++++++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/org/labkey/test/WebDriverWrapper.java b/src/org/labkey/test/WebDriverWrapper.java index 7a2f817cb0..e60c933dcc 100644 --- a/src/org/labkey/test/WebDriverWrapper.java +++ b/src/org/labkey/test/WebDriverWrapper.java @@ -2117,6 +2117,14 @@ public long doAndMaybeWaitForPageToLoad(int msWait, Supplier action) return loadTimer.elapsed().toMillis(); } + public void doAndWaitForNewWindow(Runnable action) + { + Set windows = getDriver().getWindowHandles(); + action.run(); + switchToWindow(windows.size()); + waitForDocument(); + } + public long doAndWaitForWindow(Runnable action, String windowName) { String initialWindow = getDriver().getWindowHandle(); diff --git a/src/org/labkey/test/tests/DataRegionTest.java b/src/org/labkey/test/tests/DataRegionTest.java index f7e3bb1106..02fe1a2220 100644 --- a/src/org/labkey/test/tests/DataRegionTest.java +++ b/src/org/labkey/test/tests/DataRegionTest.java @@ -17,19 +17,23 @@ package org.labkey.test.tests; import org.apache.commons.lang3.tuple.Pair; +import org.assertj.core.api.Assertions; import org.junit.Test; import org.junit.experimental.categories.Category; import org.labkey.test.BaseWebDriverTest; import org.labkey.test.Locator; import org.labkey.test.categories.Daily; import org.labkey.test.categories.Data; +import org.labkey.test.components.ChartTypeDialog; import org.labkey.test.params.FieldDefinition; import org.labkey.test.params.FieldDefinition.ColumnType; import org.labkey.test.util.DataRegionExportHelper; import org.labkey.test.util.DataRegionTable; import org.labkey.test.util.EscapeUtil; +import org.labkey.test.util.selenium.WebDriverUtils; import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.WebElement; +import org.openqa.selenium.interactions.Actions; import java.net.MalformedURLException; import java.net.URL; @@ -141,12 +145,34 @@ public void testSteps() throws Exception clickAndWait(Locator.linkWithText(LIST_NAME)); URL url = getURL(); dataRegionTest(url, INJECT_CHARS_1); - dataRegionTest(url, INJECT_CHARS_2); + DataRegionTable dataRegionTable = dataRegionTest(url, INJECT_CHARS_2); + testOpenMenuItemInNewTab(dataRegionTable); exportLoggingTest(); testQWPDemoPage(); } + /** + * Regression coverage for Issue 53629: NavMenu doesn't open in new tab correctly + */ + private void testOpenMenuItemInNewTab(DataRegionTable dataRegionTable) + { + WebElement createChartMenuItem = dataRegionTable + .getReportMenu() + .openMenuTo("Create Chart"); + doAndWaitForNewWindow(() -> + new Actions(getDriver()) + .keyDown(WebDriverUtils.MODIFIER_KEY) + .click(createChartMenuItem) + .perform()); + ChartTypeDialog chartTypeDialog = new ChartTypeDialog(getDriver()); + Assertions.assertThat(chartTypeDialog.getColumnList()) + .as("List should be pre-selected for chart") + .containsExactlyInAnyOrder(LIST_KEY_NAME, NAME_COLUMN.getName(), HEX_COLUMN.getName()); + getDriver().close(); + switchToMainWindow(); + } + @Override protected List> getTabSignalsPairs() { @@ -188,7 +214,7 @@ private void createList() _listHelper.uploadData(LIST_DATA); } - private void dataRegionTest(URL url, String dataRegionName) throws MalformedURLException + private DataRegionTable dataRegionTest(URL url, String dataRegionName) throws MalformedURLException { log("** Beginning test for dataRegionName: " + dataRegionName); @@ -281,6 +307,8 @@ private void dataRegionTest(URL url, String dataRegionName) throws MalformedURLE table.rowSelector().showAll(); assertEquals(15, table.getDataRowCount()); + + return table; } private void enableComplianceIfInstalled() From 0d1ba002c228a88a2c05bebfec9bf241a35cc066 Mon Sep 17 00:00:00 2001 From: Trey Chadick Date: Mon, 8 Sep 2025 14:17:16 -0700 Subject: [PATCH 5/6] Improve synchronization of ParentEntityEditPanel (#2669) --- src/org/labkey/test/Locator.java | 9 +++ .../test/components/bootstrap/Panel.java | 36 +++++++---- .../ui/entities/ParentEntityEditPanel.java | 59 +++++++++---------- 3 files changed, 61 insertions(+), 43 deletions(-) diff --git a/src/org/labkey/test/Locator.java b/src/org/labkey/test/Locator.java index f838d7832f..17bd5dc435 100644 --- a/src/org/labkey/test/Locator.java +++ b/src/org/labkey/test/Locator.java @@ -19,6 +19,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.mutable.MutableObject; import org.intellij.lang.annotations.Language; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.labkey.test.selenium.LazyWebElement; @@ -373,11 +374,13 @@ protected static T extractInputFromFluentWait(FluentWait wait) return wrappedContext.getValue(); } + @Contract(pure = true) public LazyWebElement findWhenNeeded(SearchContext context) { return new LazyWebElement<>(this, context); } + @Contract(pure = true) public RefindingWebElement refindWhenNeeded(SearchContext context) { return new RefindingWebElement(this, context); @@ -391,11 +394,13 @@ public WebElement findElement(SearchContext context) new NoSuchElementException("Unable to find element: " + getFindDescription(context))); } + @Contract(pure = true) public WebElement findElementOrNull(SearchContext context) { return findOptionalElement(context).orElse(null); } + @Contract(pure = true) public Optional findOptionalElement(SearchContext context) { List elements = findElements(context); @@ -404,6 +409,7 @@ public Optional findOptionalElement(SearchContext context) return Optional.of(elements.get(0)); } + @Contract(pure = true) @Override public List findElements(SearchContext context) { @@ -454,11 +460,13 @@ public List findElements(SearchContext context) } } + @Contract(pure = true) public boolean existsIn(SearchContext context) { return findElementOrNull(context) != null; } + @Contract(pure = true) public boolean isDisplayed(SearchContext context) { WebElement element = findElementOrNull(context); @@ -471,6 +479,7 @@ public boolean isDisplayed(SearchContext context) * @param context Search context. * @return True if there are any elements visible, false otherwise. */ + @Contract(pure = true) public boolean areAnyVisible(SearchContext context) { List elements = findElements(context); diff --git a/src/org/labkey/test/components/bootstrap/Panel.java b/src/org/labkey/test/components/bootstrap/Panel.java index 6daee505ec..2b8a74edff 100644 --- a/src/org/labkey/test/components/bootstrap/Panel.java +++ b/src/org/labkey/test/components/bootstrap/Panel.java @@ -71,26 +71,20 @@ public class ElementCache extends Component.ElementCache protected final WebElement panelBody = Locator.byClass("panel-body").findWhenNeeded(this); } - public static class PanelFinder extends WebDriverComponentFinder, PanelFinder> + protected static abstract class AbstractPanelFinder, F extends AbstractPanelFinder> extends WebDriverComponentFinder { - private final Locator.XPathLocator _baseLocator = Locator.tagWithClass("div", "panel-default"); + private final Locator.XPathLocator _baseLocator = Locator.tagWithClass("div", "panel"); private String _title = null; - public PanelFinder(WebDriver driver) + public AbstractPanelFinder(WebDriver driver) { super(driver); } - public PanelFinder withTitle(String title) + public F withTitle(String title) { _title = title; - return this; - } - - @Override - protected Panel construct(WebElement el, WebDriver driver) - { - return new PanelImpl(el, driver); + return getThis(); } @Override @@ -103,6 +97,26 @@ protected Locator locator() return _baseLocator; } } + + public static class PanelFinder extends AbstractPanelFinder, PanelFinder> + { + public PanelFinder(WebDriver driver) + { + super(driver); + } + + @Override + protected PanelFinder getThis() + { + return this; + } + + @Override + protected Panel construct(WebElement el, WebDriver driver) + { + return new PanelImpl(el, driver); + } + } } class PanelImpl extends Panel diff --git a/src/org/labkey/test/components/ui/entities/ParentEntityEditPanel.java b/src/org/labkey/test/components/ui/entities/ParentEntityEditPanel.java index 34efbee7fa..202a677f91 100644 --- a/src/org/labkey/test/components/ui/entities/ParentEntityEditPanel.java +++ b/src/org/labkey/test/components/ui/entities/ParentEntityEditPanel.java @@ -4,8 +4,7 @@ import org.labkey.test.BootstrapLocators; import org.labkey.test.Locator; import org.labkey.test.WebDriverWrapper; -import org.labkey.test.components.Component; -import org.labkey.test.components.WebDriverComponent; +import org.labkey.test.components.bootstrap.Panel; import org.labkey.test.components.react.BaseReactSelect; import org.labkey.test.components.react.FilteringReactSelect; import org.labkey.test.components.react.ReactSelect; @@ -13,9 +12,12 @@ import org.openqa.selenium.StaleElementReferenceException; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.ui.ExpectedConditions; import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** *

@@ -25,11 +27,8 @@ *

* @see ParentEntityEditPanel.tsx */ -public class ParentEntityEditPanel extends WebDriverComponent +public class ParentEntityEditPanel extends Panel { - private final WebDriver driver; - private final WebElement editingDiv; - /** * Constructor for the panel. * @@ -38,20 +37,7 @@ public class ParentEntityEditPanel extends WebDriverComponentelementCache().saveButton.isEnabled(), "Save button is not enabled.", 2_500); + String parentType = getTitle().split(" ", 2)[1].trim(); // Trim "Editing" from the title + Set selections = new HashSet<>(); + getAllParents().stream().map(BaseReactSelect::getSelections).forEach(selections::addAll); + // The wait time is used here to validate the panel exits edit mode. clickButtonWaitForPanel(elementCache().saveButton, waitTime); @@ -192,6 +182,20 @@ public void clickSave(int waitTime) WebDriverWrapper.waitFor(()->!progressbar.isDisplayed(), "It looks like an update took too long.", 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))); + } + } + else + { + getWrapper().quickWait().until(ExpectedConditions.visibilityOf( + Locator.tag("td").containing("has been set for this").findWhenNeeded(detailsPanel))); // e.g. "No source parent type has been set for this source." + } } /** @@ -467,11 +471,12 @@ public boolean hasParentInputError() /** * Simple finder for this panel. */ - public static class ParentEntityEditPanelFinder extends WebDriverComponentFinder + public static class ParentEntityEditPanelFinder extends AbstractPanelFinder { public ParentEntityEditPanelFinder(WebDriver driver) { super(driver); + withTitle("Editing"); } @Override @@ -479,16 +484,6 @@ protected ParentEntityEditPanel construct(WebElement element, WebDriver driver) { return new ParentEntityEditPanel(element, driver); } - - @Override - protected Locator locator() - { - return Locator - .tagContainingText("div", "Editing") - .withClass("panel-heading") - .parent() - .child(Locator.tagWithClass("div", "panel-body")); - } } public static class DataClassAddParentEntityPanelFinder extends WebDriverComponentFinder @@ -517,7 +512,7 @@ protected ElementCache newElementCache() return new ElementCache(); } - protected class ElementCache extends Component.ElementCache + protected class ElementCache extends Panel.ElementCache { final WebElement saveButton = Locator.byClass("btn-success").withText("Save").findWhenNeeded(this); final WebElement cancelButton = Locator.byClass("btn-default").withText("Cancel").refindWhenNeeded(this); From 399c044adb6899fbac2f50249ade18ea27c19bf5 Mon Sep 17 00:00:00 2001 From: Trey Chadick Date: Mon, 8 Sep 2025 15:03:01 -0700 Subject: [PATCH 6/6] Avoid known issues with random name generation (#2674) - Avoid creating long list key field names - Stop using the server to validate random field and domain names --- src/org/labkey/test/TestProperties.java | 5 ++ .../labkey/test/tests/DomainDesignerTest.java | 2 +- .../test/tests/query/QueryLookupTest.java | 2 +- src/org/labkey/test/util/DomainUtils.java | 17 ++++++ .../labkey/test/util/TestDataGenerator.java | 60 ++++++++++++++++--- 5 files changed, 77 insertions(+), 9 deletions(-) diff --git a/src/org/labkey/test/TestProperties.java b/src/org/labkey/test/TestProperties.java index 83d56286fc..88fb9ad949 100644 --- a/src/org/labkey/test/TestProperties.java +++ b/src/org/labkey/test/TestProperties.java @@ -257,6 +257,11 @@ public static boolean isTrialServer() return getBooleanProperty("webtest.server.trial", false); } + public static boolean isRemoteNameValidationEnabled() + { + return getBooleanProperty("webtest.remote.domain.validation", false); + } + public static boolean isCheckerFatal() { return "true".equals(System.getProperty("webtest.checker.fatal")); diff --git a/src/org/labkey/test/tests/DomainDesignerTest.java b/src/org/labkey/test/tests/DomainDesignerTest.java index 040e53438b..a8e2e78a7c 100644 --- a/src/org/labkey/test/tests/DomainDesignerTest.java +++ b/src/org/labkey/test/tests/DomainDesignerTest.java @@ -316,7 +316,7 @@ public void testInvalidLookupDomainField() throws IOException, CommandException public void testInvalidSampleFieldFromDelete() throws Exception { String listName = TestDataGenerator.randomDomainName("Sample Lookups List", DomainUtils.DomainKind.IntList); - String listKey = TestDataGenerator.randomFieldName("Id", null, 10, null, DomainUtils.DomainKind.IntList); + String listKey = DomainUtils.DomainKind.IntList.randomFieldName("Key"); String sampleType1 = TestDataGenerator.randomDomainName("Sample Type 1"); String sampleType2 = TestDataGenerator.randomDomainName("Sample Type 2"); SampleTypeAPIHelper.createEmptySampleType(getProjectName(), new SampleTypeDefinition(sampleType1)); diff --git a/src/org/labkey/test/tests/query/QueryLookupTest.java b/src/org/labkey/test/tests/query/QueryLookupTest.java index dec685a921..0e1aad74b3 100644 --- a/src/org/labkey/test/tests/query/QueryLookupTest.java +++ b/src/org/labkey/test/tests/query/QueryLookupTest.java @@ -29,7 +29,7 @@ public class QueryLookupTest extends BaseWebDriverTest private static final String PROJECT_NAME = "QueryLookupTest" + TRICKY_CHARACTERS_FOR_PROJECT_NAMES; private static final String LIST_NAME = "l&ist q"; - private static final FieldInfo NAME_COLUMN = FieldInfo.random("Name", FieldDefinition.ColumnType.String, DomainUtils.DomainKind.VarList); + private static final FieldInfo NAME_COLUMN = FieldInfo.random("Key", FieldDefinition.ColumnType.String, DomainUtils.DomainKind.VarList); private static final FieldInfo TSHIRT_COLUMN = FieldInfo.random("TShirt", FieldDefinition.ColumnType.String, DomainUtils.DomainKind.VarList); @Override diff --git a/src/org/labkey/test/util/DomainUtils.java b/src/org/labkey/test/util/DomainUtils.java index 9266495c22..790cd2afa3 100644 --- a/src/org/labkey/test/util/DomainUtils.java +++ b/src/org/labkey/test/util/DomainUtils.java @@ -6,6 +6,8 @@ import org.labkey.remoteapi.domain.DropDomainCommand; import org.labkey.remoteapi.domain.GetDomainDetailsCommand; import org.labkey.test.WebTestHelper; +import org.labkey.test.params.FieldDefinition; +import org.labkey.test.params.FieldInfo; import java.io.IOException; @@ -108,5 +110,20 @@ public String randomName(String namePart) { return TestDataGenerator.randomDomainName(namePart, this); } + + public String randomFieldName(String namePart) + { + return randomField(namePart).getName(); + } + + public FieldInfo randomField(String namePart) + { + return randomField(namePart, null); + } + + public FieldInfo randomField(String namePart, FieldDefinition.ColumnType columnType) + { + return FieldInfo.random(namePart, columnType, this); + } } } diff --git a/src/org/labkey/test/util/TestDataGenerator.java b/src/org/labkey/test/util/TestDataGenerator.java index 27f0f41cfb..e53ac7b0aa 100644 --- a/src/org/labkey/test/util/TestDataGenerator.java +++ b/src/org/labkey/test/util/TestDataGenerator.java @@ -37,8 +37,10 @@ import org.labkey.remoteapi.query.SelectRowsResponse; import org.labkey.remoteapi.query.Sort; import org.labkey.serverapi.reader.TabLoader; +import org.labkey.test.TestProperties; import org.labkey.test.WebTestHelper; import org.labkey.test.params.FieldDefinition; +import org.labkey.test.util.DomainUtils.DomainKind; import org.labkey.test.util.data.ColumnNameMapper; import org.labkey.test.util.data.TestDataUtils; import org.labkey.test.util.query.QueryApiHelper; @@ -566,7 +568,7 @@ public static String randomInvalidDomainName(@Nullable String namePart, int numS return domainName; } - public static String randomDomainName(@Nullable String namePart, @Nullable DomainUtils.DomainKind domainKind) + public static String randomDomainName(@Nullable String namePart, @Nullable DomainKind domainKind) { return randomDomainName(namePart, null, null, domainKind); } @@ -579,10 +581,10 @@ public static String randomDomainName(@Nullable String namePart, @Nullable Domai * @param numEndChars Number of random characters at end of name * @return name containing the given name part and appended random characters that should be a valid domain name */ - public static String randomDomainName(@Nullable String namePart, @Nullable Integer numStartChars, @Nullable Integer numEndChars, @Nullable DomainUtils.DomainKind domainKind) + public static String randomDomainName(@Nullable String namePart, @Nullable Integer numStartChars, @Nullable Integer numEndChars, @Nullable DomainKind domainKind) { String _namePart = namePart == null ? "" : namePart; - DomainUtils.DomainKind _domainKind = domainKind == null ? DomainUtils.DomainKind.SampleSet : domainKind; + 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); @@ -615,14 +617,14 @@ public static String randomFieldName(String part, @Nullable String exclusion) return randomFieldName(part, exclusion, null); } - public static String randomFieldName(String part, @Nullable String exclusion, DomainUtils.DomainKind domainKind) + public static String randomFieldName(String part, @Nullable String exclusion, DomainKind domainKind) { return randomFieldName(part, null, null, exclusion, domainKind); } - public static String randomFieldName(@NotNull String part, @Nullable Integer numStartChars, @Nullable Integer numEndChars, @Nullable String exclusion, @Nullable DomainUtils.DomainKind domainKind) + public static String randomFieldName(@NotNull String part, @Nullable Integer numStartChars, @Nullable Integer numEndChars, @Nullable String exclusion, @Nullable DomainKind domainKind) { - DomainUtils.DomainKind _domainKind = domainKind == null ? DomainUtils.DomainKind.SampleSet : domainKind; + DomainKind _domainKind = domainKind == null ? DomainKind.SampleSet : domainKind; // use the characters that we know are encoded in fieldKeys plus characters that we know clients are using // Issue 53197: Field name with double byte character can cause client side exception in Firefox when trying to customize grid view. @@ -642,7 +644,22 @@ public static String randomFieldName(@NotNull String part, @Nullable Integer num return randomFieldName; } - private static boolean isDomainAndFieldNameInvalid(DomainUtils.DomainKind domainKind, @Nullable String domainName, @Nullable String fieldName) + private static boolean isDomainAndFieldNameInvalid(DomainKind domainKind, @Nullable String domainName, @Nullable String fieldName) + { + if (fieldName != null && fieldName.length() > 64 && fieldName.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); + } + else + { + return isNameInvalidLocal(domainKind, domainName, fieldName); + } + } + + private static boolean isNameInvalidRemote(DomainKind domainKind, @Nullable String domainName, @Nullable String fieldName) { SimplePostCommand command = new SimplePostCommand("property", "validateDomainAndFieldNames"); JSONObject domainDesign = new JSONObject(); @@ -679,6 +696,35 @@ private static boolean isDomainAndFieldNameInvalid(DomainUtils.DomainKind domain } } + public static boolean isNameInvalidLocal(DomainKind domainKind, @Nullable String domainName, @Nullable String fieldName) + { + if (domainName != null) + { + if (!Character.isLetterOrDigit(domainName.charAt(0))) + return true; // domain needs to start with alphanumeric char + if (Pattern.matches("(.*\\s--[^ ].*)|(.*\\s-[^- ].*)", domainName)) + return true; // domain name must not contain space followed by dash. (command like: Issue 49161) + + int maxLength = switch (domainKind) + { + case Assay -> 200 - 13; // Make room for "{$domainName} Batch Fields" domain + case SampleSet -> 100; + default -> 200; // Sources, lists, and datasets allow 200 character names + }; + if (domainName.length() > maxLength) + return true; + } + if (fieldName != null) + { + if (fieldName.length() > 200) + return true; + if (Pattern.matches(".*:[a-zA-Z]{3}.*", fieldName)) // Avoid illegal patterns like ":Date" + return true; + } + + return false; + } + public static T randomChoice(List choices) { return choices.get(randomInt(0, choices.size() - 1));