diff --git a/src/org/labkey/test/Locator.java b/src/org/labkey/test/Locator.java index 17bd5dc435..badd2c0669 100644 --- a/src/org/labkey/test/Locator.java +++ b/src/org/labkey/test/Locator.java @@ -606,6 +606,11 @@ public static XPathLocator name(String name) return tag("*").withAttribute("name", name); } + public static XPathLocator nameContaining(String name) + { + return tag("*").withAttributeContaining("name", name); + } + public static CssLocator css(String selector) { return new CssLocator(selector); diff --git a/src/org/labkey/test/WebDriverWrapper.java b/src/org/labkey/test/WebDriverWrapper.java index b51b78bfc1..19a4f69640 100644 --- a/src/org/labkey/test/WebDriverWrapper.java +++ b/src/org/labkey/test/WebDriverWrapper.java @@ -4059,6 +4059,13 @@ public void selectOptionByText(Locator locator, String text) public void selectOptionByText(WebElement selectElement, String value) { + if(Boolean.parseBoolean(selectElement.getAttribute("multiple"))) { + List elems = selectElement.findElements(Locator.tag("option")); + elems.forEach(element->{ + if(value.contains(element.getAttribute("value")) ^ element.isSelected()) element.click(); + }); + return; + } Select select = new Select(selectElement); select.selectByVisibleText(value); } diff --git a/src/org/labkey/test/components/domain/DomainFieldRow.java b/src/org/labkey/test/components/domain/DomainFieldRow.java index b5798c5e8c..dc1ea4f328 100644 --- a/src/org/labkey/test/components/domain/DomainFieldRow.java +++ b/src/org/labkey/test/components/domain/DomainFieldRow.java @@ -767,6 +767,11 @@ public DomainFieldRow clickRemoveOntologyConcept() // behind the scenes. Because of that the validator aspect of the TextChoice field is hidden from the user (just like // it is in the product). + public void setAllowMultipleSelections(Boolean allowMultipleSelections) + { + elementCache().allowMultipleSelectionsCheckbox.set(allowMultipleSelections); + } + /** * Set the list of allowed values for a TextChoice field. * @@ -1702,6 +1707,10 @@ protected class ElementCache extends WebDriverComponent.ElementCache public final WebElement domainWarningIcon = Locator.tagWithClass("span", "domain-warning-icon") .findWhenNeeded(this); + // text choice field option + public final Checkbox allowMultipleSelectionsCheckbox = new Checkbox(Locator.tagWithClass("input", "domain-text-choice-multi") + .refindWhenNeeded(this).withTimeout(WAIT_FOR_JAVASCRIPT)); + // lookup field options public final Select lookupContainerSelect = SelectWrapper.Select(Locator.name("domainpropertiesrow-lookupContainer")) .findWhenNeeded(this); diff --git a/src/org/labkey/test/components/domain/DomainFormPanel.java b/src/org/labkey/test/components/domain/DomainFormPanel.java index fb4b639f7c..c86da34ddd 100644 --- a/src/org/labkey/test/components/domain/DomainFormPanel.java +++ b/src/org/labkey/test/components/domain/DomainFormPanel.java @@ -236,6 +236,10 @@ else if (validator instanceof FieldDefinition.TextChoiceValidator textChoiceVali throw new IllegalArgumentException("TextChoice fields cannot have additional validators."); } fieldRow.setTextChoiceValues(textChoiceValidator.getValues()); + if(textChoiceValidator.getMultipleSelections()) + { + fieldRow.setAllowMultipleSelections(textChoiceValidator.getMultipleSelections()); + } } else { diff --git a/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java b/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java index a5edaae738..7c9309caba 100644 --- a/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java +++ b/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java @@ -124,6 +124,19 @@ public EntityBulkUpdateDialog setSelectionField(CharSequence fieldIdentifier, Li return this; } + /** + * Clear the field (fieldIdentifier). + * + * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey}) + * @return this component + */ + public EntityBulkUpdateDialog clearSelection(CharSequence fieldIdentifier) + { + FilteringReactSelect reactSelect = enableSelectionField(fieldIdentifier); + reactSelect.clearSelection(); + return this; + } + /** * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey}) * @param selectValue value to select diff --git a/src/org/labkey/test/components/ui/grids/EditableGrid.java b/src/org/labkey/test/components/ui/grids/EditableGrid.java index 04aba55749..ad279022a0 100644 --- a/src/org/labkey/test/components/ui/grids/EditableGrid.java +++ b/src/org/labkey/test/components/ui/grids/EditableGrid.java @@ -507,11 +507,14 @@ public WebElement setCellValue(int row, CharSequence columnIdentifier, Object va if (value instanceof List) { + // If this is a list assume that it will need a lookup. List values = (List) value; ReactSelect lookupSelect = elementCache().lookupSelect(gridCell); + lookupSelect.clearSelection(); + lookupSelect.open(); for (String _value : values) diff --git a/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java b/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java index e8ee4d0fb9..6ea369501a 100644 --- a/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java +++ b/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java @@ -40,6 +40,12 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.assertEquals; +import static org.labkey.remoteapi.query.Filter.Operator.CONTAINS_ALL; +import static org.labkey.remoteapi.query.Filter.Operator.CONTAINS_ANY; +import static org.labkey.remoteapi.query.Filter.Operator.CONTAINS_EXACTLY; +import static org.labkey.remoteapi.query.Filter.Operator.CONTAINS_NONE; +import static org.labkey.remoteapi.query.Filter.Operator.DOES_NOT_CONTAIN_EXACTLY; +import static org.labkey.remoteapi.query.Filter.Operator.IN; import static org.labkey.test.WebDriverWrapper.waitFor; public class ResponsiveGrid> extends WebDriverComponent.ElementCache> implements UpdatingComponent @@ -234,15 +240,18 @@ public String filterColumnExpectingError(CharSequence columnIdentifier, Filter.O private GridFilterModal initFilterColumn(CharSequence columnIdentifier, Filter.Operator operator, Object value) { + List listOperators = List.of(IN, CONTAINS_ALL, CONTAINS_ANY, CONTAINS_EXACTLY, CONTAINS_NONE, + DOES_NOT_CONTAIN_EXACTLY); clickColumnMenuItem(columnIdentifier, "Filter...", false); GridFilterModal filterModal = new GridFilterModal(getDriver(), this); if (operator != null) { - if (operator.equals(Filter.Operator.IN) && value instanceof List) + if (listOperators.contains(operator) && value instanceof List) { List values = (List) value; filterModal.selectFacetTab().selectValue(values.get(0)); filterModal.selectFacetTab().checkValues(values.toArray(String[]::new)); + filterModal.selectFacetTab().selectFilter(operator.getDisplayValue()); } else filterModal.selectExpressionTab().setFilter(new FilterExpressionPanel.Expression(operator, value)); diff --git a/src/org/labkey/test/components/ui/search/FilterFacetedPanel.java b/src/org/labkey/test/components/ui/search/FilterFacetedPanel.java index 90a8c78713..1c014b7dbd 100644 --- a/src/org/labkey/test/components/ui/search/FilterFacetedPanel.java +++ b/src/org/labkey/test/components/ui/search/FilterFacetedPanel.java @@ -6,6 +6,7 @@ import org.labkey.test.components.WebDriverComponent; import org.labkey.test.components.html.Checkbox; import org.labkey.test.components.html.Input; +import org.labkey.test.components.react.ReactSelect; import org.labkey.test.components.ui.FilterStatusValue; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; @@ -48,6 +49,15 @@ public void selectValue(String value) elementCache().findCheckboxLabel(value).click(); } + /** + * Select a single facet value by clicking its label. Should replace all existing selections. + * @param value desired value + */ + public void selectFilter(String value) + { + elementCache().filterTypeSelects.select(value); + } + /** * Check single facet value by label to see if it is checked or not. * @param value desired value @@ -123,6 +133,8 @@ protected class ElementCache extends Component.ElementCache { protected final Input filterInput = Input(Locator.id("filter-faceted__typeahead-input"), getDriver()).findWhenNeeded(this); + protected final ReactSelect filterTypeSelects = + new ReactSelect.ReactSelectFinder(getDriver()).index(0).findWhenNeeded(this); protected final WebElement checkboxSection = Locator.byClass("labkey-wizard-pills").index(0).refindWhenNeeded(this); protected final Locator.XPathLocator checkboxLabelLoc diff --git a/src/org/labkey/test/pages/DatasetInsertPage.java b/src/org/labkey/test/pages/DatasetInsertPage.java index 4651210bdf..4a922f85c7 100644 --- a/src/org/labkey/test/pages/DatasetInsertPage.java +++ b/src/org/labkey/test/pages/DatasetInsertPage.java @@ -82,7 +82,7 @@ private void tryInsert(Map values) { for (Map.Entry entry : values.entrySet()) { - WebElement fieldInput = Locator.name(EscapeUtil.getFormFieldName(entry.getKey())).findElement(getDriver()); + WebElement fieldInput = Locator.nameContaining(EscapeUtil.getFormFieldName(entry.getKey())).findElement(getDriver()); String type = fieldInput.getAttribute("type"); switch (type) { diff --git a/src/org/labkey/test/pages/query/UpdateQueryRowPage.java b/src/org/labkey/test/pages/query/UpdateQueryRowPage.java index dcb2efa47e..0f16d59a0e 100644 --- a/src/org/labkey/test/pages/query/UpdateQueryRowPage.java +++ b/src/org/labkey/test/pages/query/UpdateQueryRowPage.java @@ -15,7 +15,6 @@ import org.labkey.test.util.EscapeUtil; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; - import java.io.File; import java.util.HashMap; import java.util.Map; @@ -99,7 +98,7 @@ public UpdateQueryRowPage setField(String fieldName, String value) WebElement field = elementCache().findField(fieldName); if (field.getTagName().equals("select")) { - setField(fieldName, OptionSelect.SelectOption.textOption(value)); + selectOptionByText(field, value); } else { @@ -186,7 +185,7 @@ WebElement findField(String name) { if (!fieldMap.containsKey(name)) { - fieldMap.put(name, Locator.name(EscapeUtil.getFormFieldName(name)).findElement(this)); + fieldMap.put(name, Locator.nameContaining(EscapeUtil.getFormFieldName(name)).findElement(this)); } return fieldMap.get(name); } diff --git a/src/org/labkey/test/params/FieldDefinition.java b/src/org/labkey/test/params/FieldDefinition.java index 9f2b5f0900..0e2fa00b88 100644 --- a/src/org/labkey/test/params/FieldDefinition.java +++ b/src/org/labkey/test/params/FieldDefinition.java @@ -475,6 +475,13 @@ public FieldDefinition setTextChoiceValues(List values) return this; } + public FieldDefinition setMultiChoiceValues(List values) + { + Assert.assertEquals("Invalid field type for text choice values.", ColumnType.TextChoice, getType()); + setValidators(List.of(new FieldDefinition.TextChoiceValidator(values).setMultipleSelections())); + return this; + } + public ExpSchema.DerivationDataScopeType getAliquotOption() { return _aliquotOption; @@ -1112,6 +1119,8 @@ public static class TextChoiceValidator extends FieldValidator _values; + private Boolean multipleSelections = false; + public TextChoiceValidator(List values) { // The TextChoice validator only has a name and no description or message. @@ -1143,6 +1152,17 @@ public List getValues() return _values; } + public TextChoiceValidator setMultipleSelections() + { + this.multipleSelections = true; + return this; + } + + public Boolean getMultipleSelections() + { + return this.multipleSelections; + } + } } diff --git a/src/org/labkey/test/tests/list/ListTest.java b/src/org/labkey/test/tests/list/ListTest.java index 41f8992f59..d942a7e643 100644 --- a/src/org/labkey/test/tests/list/ListTest.java +++ b/src/org/labkey/test/tests/list/ListTest.java @@ -84,6 +84,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -1688,6 +1689,42 @@ public void testAutoIncrementKeyEncoded() _listHelper.deleteList(); } + @Test + public void testMultiChoiceValues() + { + // setup a list with an auto-increment key and multi text choice field + String encodedListName = "multiChoiceList"; + String keyName = "'>'"; + String columnName = "MultiChoiceField"; + List tcValues = List.of("~`!@#$%^&*()_+=[]{}\\|';:\"<>?,./", "1", "2"); + _listHelper.createList(PROJECT_VERIFY, encodedListName, keyName, col(columnName, ColumnType.TextChoice) + .setMultiChoiceValues(tcValues)); + _listHelper.goToList(encodedListName); + + DataRegionTable table = new DataRegionTable("query", getDriver()); + table.clickInsertNewRow(); + String valuesToChoose = tcValues.subList(1, 3).stream() + .sorted() + .collect(Collectors.joining(" ")); + Locator loc = Locator.nameContaining(EscapeUtil.getFormFieldName(columnName)); + selectOptionByText(loc, valuesToChoose); + + clickButton("Submit"); + checker().verifyEquals("Multi choice value not as expected", valuesToChoose, table.getDataAsText(0, columnName)); + + table.clickEditRow(0); + valuesToChoose = tcValues.subList(1, 3).stream() + .sorted() + .collect(Collectors.joining(" ")); + selectOptionByText(loc, valuesToChoose); + clickButton("Submit"); + + // verify the multi choice value is persisted + checker().verifyEquals("Multi choice value not as expected", valuesToChoose, table.getDataAsText(0, columnName)); + + _listHelper.deleteList(); + } + private List getQueryFormFieldNames() { return Locator.tag("input").attributeStartsWith("name", "quf_") diff --git a/src/org/labkey/test/util/TestDataGenerator.java b/src/org/labkey/test/util/TestDataGenerator.java index 854b2d2952..8b9431a485 100644 --- a/src/org/labkey/test/util/TestDataGenerator.java +++ b/src/org/labkey/test/util/TestDataGenerator.java @@ -60,6 +60,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Random; import java.util.Set; import java.util.concurrent.ThreadLocalRandom; import java.util.function.Function; @@ -956,6 +957,12 @@ public ImportDataResponse importRows(Connection cn, List> ro return getQueryHelper(cn).importData(TestDataUtils.stringFromRows(TestDataUtils.rowListsFromMaps(rows)), lookupByAlternateKey); } + public static List shuffleSelect(List allFields) + { + int randomSize = new Random().nextInt(allFields.size()) + 1; + return shuffleSelect(allFields, randomSize); + } + public static List shuffleSelect(List allFields, int selectCount) { List shuffled = new ArrayList<>(allFields);