diff --git a/src/org/labkey/remoteapi/issues/IssueModel.java b/src/org/labkey/remoteapi/issues/IssueModel.java index c5bd31648f..f5ef9ec5a4 100644 --- a/src/org/labkey/remoteapi/issues/IssueModel.java +++ b/src/org/labkey/remoteapi/issues/IssueModel.java @@ -1,6 +1,7 @@ package org.labkey.remoteapi.issues; import org.json.JSONObject; +import org.json.JSONParserConfiguration; import org.labkey.api.collections.CaseInsensitiveHashMap; import java.io.File; @@ -27,7 +28,8 @@ public IssueModel setProperties(Map properties) public JSONObject toJSON() { - var json = new JSONObject(_properties); + // Ensure that null values are sent instead of omitted + var json = new JSONObject(_properties, new JSONParserConfiguration().withUseNativeNulls(true)); // handle attachments if (!_attachments.isEmpty()) 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/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/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("_"); } } 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/WebDriverWrapper.java b/src/org/labkey/test/WebDriverWrapper.java index 1aa4c3b1f9..afb2f26a15 100644 --- a/src/org/labkey/test/WebDriverWrapper.java +++ b/src/org/labkey/test/WebDriverWrapper.java @@ -2129,6 +2129,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/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..b5b543ec55 100644 --- a/src/org/labkey/test/components/ui/entities/ParentEntityEditPanel.java +++ b/src/org/labkey/test/components/ui/entities/ParentEntityEditPanel.java @@ -1,11 +1,12 @@ 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; 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 +14,13 @@ import org.openqa.selenium.StaleElementReferenceException; import org.openqa.selenium.WebDriver; 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; +import java.util.Set; /** *

@@ -25,11 +30,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 +40,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 +185,25 @@ 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()) + { + // 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 + { + 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 +479,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 +492,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 +520,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); 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/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/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/DataRegionTest.java b/src/org/labkey/test/tests/DataRegionTest.java index add7f0d02e..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; @@ -39,6 +43,8 @@ import java.util.Map; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.labkey.test.params.FieldDefinition.DOMAIN_TRICKY_CHARACTERS; @Category({Daily.class, Data.class}) @@ -139,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() { @@ -186,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); @@ -199,13 +227,13 @@ private void dataRegionTest(URL url, String dataRegionName) throws MalformedURLE assertEquals(TOTAL_ROWS, table.getDataRowCount()); assertEquals("aqua", table.getDataAsText(0, "Name")); assertEquals("#FFFF00", table.getDataAsText(15, "Hex")); - assertEquals(false, table.getPagingWidget().hasPagingButton(true)); - assertEquals(false, table.getPagingWidget().hasPagingButton(false)); + assertFalse(table.getPagingWidget().hasPagingButton(true)); + assertFalse(table.getPagingWidget().hasPagingButton(false)); log("Test 3 per page"); table.setMaxRows(3); - assertEquals(true, table.getPagingWidget().hasPagingButton(true)); - assertEquals(true, table.getPagingWidget().hasPagingButton(false)); + assertTrue(table.getPagingWidget().hasPagingButton(true)); + assertTrue(table.getPagingWidget().hasPagingButton(false)); table.getPagingWidget().viewPagingOptions(); assertElementPresent(Locator.linkWithText("3 per page").notHidden().append(Locator.tagWithClass("i", "fa-check-square-o"))); assertElementPresent(Locator.linkWithText("20 per page").notHidden()); @@ -221,40 +249,40 @@ private void dataRegionTest(URL url, String dataRegionName) throws MalformedURLE table.assertPaginationText(1, 5, 16); assertEquals(5, table.getDataRowCount()); assertEquals("aqua", table.getDataAsText(0, "Name")); - assertEquals(false, table.getPagingWidget().menuOptionEnabled("Show first", "Show first")); - assertEquals(true, table.getPagingWidget().menuOptionEnabled("Show last", "Show last")); - assertEquals(false, table.getPagingWidget().pagingButtonEnabled(true)); - assertEquals(true, table.getPagingWidget().pagingButtonEnabled(false)); + assertFalse(table.getPagingWidget().menuOptionEnabled("Show first", "Show first")); + assertTrue(table.getPagingWidget().menuOptionEnabled("Show last", "Show last")); + assertFalse(table.getPagingWidget().pagingButtonEnabled(true)); + assertTrue(table.getPagingWidget().pagingButtonEnabled(false)); log("Next Page"); table.pageNext(); table.assertPaginationText(6, 10, 16); assertEquals(5, table.getDataRowCount()); assertEquals("grey", table.getDataAsText(0, "Name")); - assertEquals(true, table.getPagingWidget().menuOptionEnabled("Show first", "Show first")); - assertEquals(true, table.getPagingWidget().menuOptionEnabled("Show last", "Show last")); - assertEquals(true, table.getPagingWidget().pagingButtonEnabled(true)); - assertEquals(true, table.getPagingWidget().pagingButtonEnabled(false)); + assertTrue(table.getPagingWidget().menuOptionEnabled("Show first", "Show first")); + assertTrue(table.getPagingWidget().menuOptionEnabled("Show last", "Show last")); + assertTrue(table.getPagingWidget().pagingButtonEnabled(true)); + assertTrue(table.getPagingWidget().pagingButtonEnabled(false)); log("Last Page"); table.pageLast(); table.assertPaginationText(16, 16, 16); assertEquals(1, table.getDataRowCount()); assertEquals("yellow", table.getDataAsText(0, "Name")); - assertEquals(true, table.getPagingWidget().menuOptionEnabled("Show first", "Show first")); - assertEquals(false, table.getPagingWidget().menuOptionEnabled("Show last", "Show last")); - assertEquals(true, table.getPagingWidget().pagingButtonEnabled(true)); - assertEquals(false, table.getPagingWidget().pagingButtonEnabled(false)); + assertTrue(table.getPagingWidget().menuOptionEnabled("Show first", "Show first")); + assertFalse(table.getPagingWidget().menuOptionEnabled("Show last", "Show last")); + assertTrue(table.getPagingWidget().pagingButtonEnabled(true)); + assertFalse(table.getPagingWidget().pagingButtonEnabled(false)); log("Previous Page"); table.pagePrev(); table.assertPaginationText(11, 15, 16); assertEquals(5, table.getDataRowCount()); assertEquals("purple", table.getDataAsText(0, "Name")); - assertEquals(true, table.getPagingWidget().menuOptionEnabled("Show first", "Show first")); - assertEquals(true, table.getPagingWidget().menuOptionEnabled("Show last", "Show last")); - assertEquals(true, table.getPagingWidget().pagingButtonEnabled(true)); - assertEquals(true, table.getPagingWidget().pagingButtonEnabled(false)); + assertTrue(table.getPagingWidget().menuOptionEnabled("Show first", "Show first")); + assertTrue(table.getPagingWidget().menuOptionEnabled("Show last", "Show last")); + assertTrue(table.getPagingWidget().pagingButtonEnabled(true)); + assertTrue(table.getPagingWidget().pagingButtonEnabled(false)); log("Setting a filter should go back to first page"); table.setFilter(NAME_COLUMN.getName(), "Does Not Equal", "aqua"); @@ -266,7 +294,7 @@ private void dataRegionTest(URL url, String dataRegionName) throws MalformedURLE Locator.XPathLocator selectionPart = Locator.tagWithAttribute("div", "data-msgpart", "selection"); waitForElement(selectionPart); WebElement msgDiv = selectionPart.findElement(getDriver()); - assertEquals(true, msgDiv.getText().contains("Selected 5 of 15 rows.")); + assertTrue(msgDiv.getText().contains("Selected 5 of 15 rows.")); assertElementPresent(selectionPart.append(Locator.tagWithClass("span", "select-all"))); assertElementPresent(selectionPart.append(Locator.tagWithClass("span", "select-none"))); @@ -279,12 +307,14 @@ private void dataRegionTest(URL url, String dataRegionName) throws MalformedURLE table.rowSelector().showAll(); assertEquals(15, table.getDataRowCount()); + + return table; } private void enableComplianceIfInstalled() { // Make sure it works with Compliance on (which enables Elec Sign control) - // Have to do what enableModule does in order to check if it's installed + // Have to do what enableModule does to check if it's installed goToFolderManagement(); clickAndWait(Locator.linkWithText("Folder Type")); 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/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/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"); } 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/tests/issues/IssueAPITest.java b/src/org/labkey/test/tests/issues/IssueAPITest.java index 8422cef180..46190084fc 100644 --- a/src/org/labkey/test/tests/issues/IssueAPITest.java +++ b/src/org/labkey/test/tests/issues/IssueAPITest.java @@ -12,9 +12,12 @@ import org.labkey.remoteapi.issues.IssueResponseModel; import org.labkey.remoteapi.issues.IssuesCommand; import org.labkey.test.BaseWebDriverTest; +import org.labkey.test.Locator; import org.labkey.test.TestFileUtils; import org.labkey.test.categories.Daily; import org.labkey.test.categories.Issues; +import org.labkey.test.pages.issues.IssuesAdminPage; +import org.labkey.test.params.FieldDefinition; import org.labkey.test.util.APIUserHelper; import org.labkey.test.util.ApiPermissionsHelper; import org.labkey.test.util.IssuesHelper; @@ -24,7 +27,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.stream.Collectors; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.MatcherAssert.assertThat; @@ -36,9 +38,11 @@ @Category({Issues.class, Daily.class}) public class IssueAPITest extends BaseWebDriverTest { + public static final String CUSTOM_REQUIRED_FIELD = "CustomField"; IssuesHelper _issuesHelper = new IssuesHelper(this); APIUserHelper _userHelper = new APIUserHelper(this); static String ISSUES = "issues"; + static String ISSUES_WITH_REQUIRED = "required"; static Integer TEST_USER_ID; static String TEST_USER_NAME; static String TEST_USER_DISPLAY_NAME; @@ -60,6 +64,12 @@ private void doSetup() _containerHelper.createProject(getProjectName(), null); _issuesHelper.createNewIssuesList(ISSUES, getContainerHelper()); + _issuesHelper.createNewIssuesList(ISSUES_WITH_REQUIRED, getContainerHelper()); + clickAndWait(Locator.linkWithText(ISSUES_WITH_REQUIRED)); + IssuesAdminPage adminPage = _issuesHelper.goToAdmin(); + adminPage.getFieldsPanel().addField(new FieldDefinition(CUSTOM_REQUIRED_FIELD, FieldDefinition.ColumnType.String).setRequired(true)); + adminPage.clickSave(); + TEST_USER_NAME = getUsername(); TEST_USER_ID = _userHelper.getUserId(TEST_USER_NAME); TEST_USER_DISPLAY_NAME = _userHelper.getDisplayNameForEmail(TEST_USER_NAME); @@ -98,6 +108,76 @@ public void testInsertAnIssue() throws Exception assertEquals("expect a single attachment", List.of(testFile.getName()), attachments); } + @Test + public void testUpdateAnIssueWithRequiredField() throws Exception + { + // First try inserting without a required custom field + IssueModel testIssue = basicIssueModel("Custom field test issue", "Let's see if we can update this"); + testIssue.setIssueDefName(ISSUES_WITH_REQUIRED); + IssuesCommand cmd = new IssuesCommand(); + cmd.setIssues(List.of(testIssue)); + final String expectedError = "Missing value for required property: CustomField"; + try + { + cmd.execute(createDefaultConnection(), getProjectName()); + fail("Should have thrown an exception because the required field was not set"); + } + catch (CommandException expectedFailure) + { + assertEquals(expectedError, expectedFailure.getMessage()); + } + + // Retry, supplying a value + String customFieldValue = "test value"; + testIssue.setProp(CUSTOM_REQUIRED_FIELD, customFieldValue); + IssueResponse response = cmd.execute(createDefaultConnection(), getProjectName()); + assertEquals(1, response.getIssueIds().size()); + + // Update without including the custom field, ensuring that the old value is retained + IssueModel toUpdate = new IssueModel(); + toUpdate.setIssueId(response.getIssueIds().get(0)); + final String updatedTitle = "Updated custom field test issue"; + toUpdate.setTitle(updatedTitle); + toUpdate.setIssueDefName(ISSUES_WITH_REQUIRED); + toUpdate.setAction(IssueModel.IssueAction.update); + IssueResponse updateResponse = new IssuesCommand(List.of(toUpdate)).execute(createDefaultConnection(), getProjectName()); + assertEquals(1, updateResponse.getIssueIds().size()); + + IssueResponseModel updatedIssue = getIssueResponse(updateResponse.getIssueIds().get(0)); + assertEquals(updatedTitle, updatedIssue.getTitle()); + assertEquals(customFieldValue, updatedIssue.getProperties(CUSTOM_REQUIRED_FIELD)); + + // Update with a new value for the custom field + toUpdate = new IssueModel(); + toUpdate.setIssueId(response.getIssueIds().get(0)); + toUpdate.setIssueDefName(ISSUES_WITH_REQUIRED); + toUpdate.setAction(IssueModel.IssueAction.update); + final String newCustomFieldValue = "new value"; + toUpdate.setProp(CUSTOM_REQUIRED_FIELD, newCustomFieldValue); + updateResponse = new IssuesCommand(List.of(toUpdate)).execute(createDefaultConnection(), getProjectName()); + assertEquals(1, updateResponse.getIssueIds().size()); + + updatedIssue = getIssueResponse(updateResponse.getIssueIds().get(0)); + assertEquals(updatedTitle, updatedIssue.getTitle()); + assertEquals(newCustomFieldValue, updatedIssue.getProperties(CUSTOM_REQUIRED_FIELD)); + + // Attempt an update setting the value to null, which should fail + toUpdate = new IssueModel(); + toUpdate.setIssueId(response.getIssueIds().get(0)); + toUpdate.setIssueDefName(ISSUES_WITH_REQUIRED); + toUpdate.setAction(IssueModel.IssueAction.update); + toUpdate.setProp(CUSTOM_REQUIRED_FIELD, null); + try + { + new IssuesCommand(List.of(toUpdate)).execute(createDefaultConnection(), getProjectName()); + fail("Should have thrown an exception because the required field was set to null"); + } + catch (CommandException expectedFailure) + { + assertEquals(expectedError, expectedFailure.getMessage()); + } + } + @Test public void testUpdateAnIssue() throws Exception { @@ -107,7 +187,9 @@ public void testUpdateAnIssue() throws Exception String title = "Updated issue test issue"; Integer updatedPri = 4; IssueModel originalIssue = basicIssueModel(originalTitle, originalComment); - IssueModel updateIssue = basicIssueModel(title, comment) + IssueModel updateIssue = new IssueModel() + .setTitle(title) + .setComment(comment) .setAction(IssueModel.IssueAction.update) .setPriority(updatedPri); @@ -136,7 +218,9 @@ public void testResolveAnIssue() throws Exception Integer updatedPri = 4; IssueModel originalIssue = basicIssueModel(originalTitle, originalComment); - IssueModel resolveIssue = basicIssueModel(newTitle, newComment) + IssueModel resolveIssue = new IssueModel() + .setTitle(newTitle) + .setComment(newComment) .setAction(IssueModel.IssueAction.resolve) .setResolution("Fixed") .setPriority(updatedPri); @@ -165,7 +249,9 @@ public void testCloseAnIssue() throws Exception String newTitle = "Closed test issue"; Integer updatedPri = 4; IssueModel originalIssue = basicIssueModel(originalTitle, originalComment); - IssueModel closeIssue = basicIssueModel(newTitle, newComment) + IssueModel closeIssue = new IssueModel() + .setTitle(newTitle) + .setComment(newComment) .setAction(IssueModel.IssueAction.close) .setPriority(updatedPri); @@ -187,7 +273,8 @@ public void testAssignAnIssue() throws Exception { String title = "Assign Test Issue"; IssueModel originalIssue = basicIssueModel(title, "Gonna assign this"); - IssueModel updateIssue = basicIssueModel(null, "assigned now") + IssueModel updateIssue = new IssueModel() + .setComment("assigned now") .setAction(IssueModel.IssueAction.update) .setAssignedTo(TEST_BUDDY_ID); @@ -208,9 +295,9 @@ public void testAssignAnIssue() throws Exception public void testReopenAnIssue() throws Exception { IssueModel originalIssue = basicIssueModel("Reopen test issue", "Gonna close and reopen this"); - IssueModel closeIssue = basicIssueModel(null, null) + IssueModel closeIssue = new IssueModel() .setAction(IssueModel.IssueAction.close); - IssueModel reopenIssue = basicIssueModel(null, null) + IssueModel reopenIssue = new IssueModel() .setAction(IssueModel.IssueAction.reopen); // insert @@ -267,10 +354,11 @@ public void testResolveAClosedIssue() throws Exception var issueId = doIssueAction(issue); - IssueModel close = basicIssueModel(null, null).setAction(IssueModel.IssueAction.close) - .setIssueId(issueId); - IssueModel resolve = basicIssueModel(null, null).setAction(IssueModel.IssueAction.resolve) + IssueModel close = new IssueModel().setAction(IssueModel.IssueAction.close) .setIssueId(issueId); + IssueModel resolve = new IssueModel().setAction(IssueModel.IssueAction.resolve) + .setIssueId(issueId). + setAssignedTo(TEST_USER_ID); doIssueAction(close); var closedResponse = getIssueResponse(issueId); @@ -303,7 +391,7 @@ public void testUpdateMultipleIssues() throws Exception insertCmd.setIssues(issues); var insertResponse = insertCmd.execute(createDefaultConnection(), getProjectName()); List issueIds = insertResponse.getIssueIds(); - var distinctIds = issueIds.stream().distinct().collect(Collectors.toList()); + var distinctIds = issueIds.stream().distinct().toList(); // ensure we got as many issueIDs back as we sent issues in assertEquals("Expect to get as many issueIds as there are inputs", issues.size(), issueIds.size()); // make sure we didn't get a list of IDs with duplicates diff --git a/src/org/labkey/test/tests/list/ListTest.java b/src/org/labkey/test/tests/list/ListTest.java index e03ce490f5..73862cf52a 100644 --- a/src/org/labkey/test/tests/list/ListTest.java +++ b/src/org/labkey/test/tests/list/ListTest.java @@ -53,6 +53,7 @@ import org.labkey.test.params.FieldDefinition; import org.labkey.test.params.FieldDefinition.StringLookup; import org.labkey.test.params.FieldKey; +import org.labkey.test.params.list.IntListDefinition; import org.labkey.test.params.list.VarListDefinition; import org.labkey.test.tests.AuditLogTest; import org.labkey.test.util.AbstractDataRegionExportOrSignHelper.ColumnHeaderType; @@ -98,6 +99,7 @@ public class ListTest extends BaseWebDriverTest protected final static String PROJECT_VERIFY = "ListVerifyProject" ;//+ TRICKY_CHARACTERS_FOR_PROJECT_NAMES; private final static String PROJECT_OTHER = "OtherListVerifyProject"; protected final static String LIST_NAME_COLORS = "A_Colors_" + DOMAIN_TRICKY_CHARACTERS; + protected final static String LIST_NAME_HTML_KEY = "A_HtmlKey_" + DOMAIN_TRICKY_CHARACTERS; protected final static ColumnType LIST_KEY_TYPE = ColumnType.String; protected final static String LIST_KEY_NAME = "Key"; @@ -261,15 +263,33 @@ protected void setUpListFinish() .submit(); } + /** Issue 53796: 25.3 -> 25.7: DataRegion.getChecked() incorrectly HTML encodes the results */ + @Test + public void testKeyWithHtmlCharacters() + { + _listHelper.createList(getProjectName(), LIST_NAME_HTML_KEY, new FieldDefinition(LIST_KEY_NAME2, LIST_KEY_TYPE)); + ImportDataPage importDataPage = _listHelper.clickImportData(); + String value = "<>ThisIsTheKeyValueWithHtmlCharacters"; + importDataPage.setText(LIST_KEY_NAME2_BULK + "\n" + value); + importDataPage.submit(); + assertTextPresent(value); + final DataRegionTable dt = DataRegion(getDriver()).withName("query").find(); + dt.checkAllOnPage(); + @SuppressWarnings("unchecked") List checked = (List)executeScript("return LABKEY.DataRegions.query.getChecked()"); + assertEquals(Arrays.asList(value), checked); + dt.deleteSelectedRows(); + assertTextNotPresent(value); + } + @LogMethod - protected void setUpList(String projectName) + protected void setUpList() { // TODO: Break this up into explicit test cases and remove redundant test coverage. // But at least now it's only called from the one test case that relies on this list, testCustomViews(). // Previously it was called from the @BeforeClass method, even though none of the other test cases use this list. log("Add list -- " + LIST_NAME_COLORS); - _listHelper.createList(projectName, LIST_NAME_COLORS, new FieldDefinition(LIST_KEY_NAME2, LIST_KEY_TYPE), _listColFake, + _listHelper.createList(getProjectName(), LIST_NAME_COLORS, new FieldDefinition(LIST_KEY_NAME2, LIST_KEY_TYPE), _listColFake, _listColMonth, _listColTone); log("Add description and test edit"); @@ -308,13 +328,13 @@ protected void setUpList(String projectName) log("Test check/uncheck of checkboxes"); // Second row (Green) assertEquals(1, table.getRowIndex(TEST_DATA[TD_COLOR][1])); - clickAndWait(table.updateLink(1)); + table.clickEditRow(1); setFormElement(Locator.name("quf_" + _listColMonth.getName()), VALID_MONTHS[1]); // Has a funny format -- need to post converted date checkCheckbox(Locator.checkboxByName("quf_JewelTone")); clickButton("Submit"); // Third row (Red) assertEquals(2, table.getRowIndex(TEST_DATA[TD_COLOR][2])); - clickAndWait(table.updateLink(2)); + table.clickEditRow(2); setFormElement(Locator.name("quf_" + _listColMonth.getName()), VALID_MONTHS[2]); // Has a funny format -- need to post converted date uncheckCheckbox(Locator.checkboxByName("quf_JewelTone")); clickButton("Submit"); @@ -580,7 +600,7 @@ public void testCreateListWithBOMFile() public void testCustomViews() { goToProjectHome(); - setUpList(getProjectName()); + setUpList(); goToProjectHome(); waitAndClickAndWait(Locator.linkWithText(LIST_NAME_COLORS)); @@ -1064,7 +1084,7 @@ public void listSelfJoinTest() clickButton("Edit"); assertTextPresent("dummy one"); clickButton("Cancel"); - clickAndWait(regionTable.updateLink(0)); + regionTable.clickEditRow(0); assertTextPresent("dummy one"); clickButton("Cancel"); } 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/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) 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/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() 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 27f0f41cfb..aa9ab14106 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; @@ -544,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() @@ -561,12 +563,12 @@ 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; } - 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,22 +581,22 @@ 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; + 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; @@ -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. @@ -630,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); @@ -639,10 +641,26 @@ 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(DomainUtils.DomainKind domainKind, @Nullable String domainName, @Nullable String fieldName) + 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); + } + else + { + return isNameInvalidLocal(domainKind, domainName, fieldName); + } + } + + private static boolean isNameInvalidRemote(DomainKind domainKind, @Nullable RandomName domainName, @Nullable RandomName fieldName) { SimplePostCommand command = new SimplePostCommand("property", "validateDomainAndFieldNames"); JSONObject domainDesign = new JSONObject(); @@ -679,6 +697,46 @@ private static boolean isDomainAndFieldNameInvalid(DomainUtils.DomainKind domain } } + 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 (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())) + 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.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.name().isBlank()) + return true; + if (fieldName.name().length() > 200) + return true; + 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; + } + + return false; + } + public static T randomChoice(List choices) { return choices.get(randomInt(0, choices.size() - 1));