From 333c490722ce03479f498bb2e50acc8482811c19 Mon Sep 17 00:00:00 2001 From: Daria Bodiakova <70635654+DariaBod@users.noreply.github.com> Date: Fri, 23 Jan 2026 13:40:28 -0800 Subject: [PATCH 01/21] initial commit for multichoice value --- .../components/domain/DomainFieldRow.java | 5 +++ .../components/domain/DomainFormPanel.java | 10 +++++ .../components/ui/grids/ResponsiveGrid.java | 11 ++++- .../ui/search/FilterFacetedPanel.java | 12 +++++ .../labkey/test/params/FieldDefinition.java | 45 +++++++++++++++++++ 5 files changed, 82 insertions(+), 1 deletion(-) diff --git a/src/org/labkey/test/components/domain/DomainFieldRow.java b/src/org/labkey/test/components/domain/DomainFieldRow.java index 8c36831e33..cbb05220bf 100644 --- a/src/org/labkey/test/components/domain/DomainFieldRow.java +++ b/src/org/labkey/test/components/domain/DomainFieldRow.java @@ -935,6 +935,11 @@ public String updateLockedTextChoiceValue(String originalValue, String newValue) return alert.getText(); } + public void updateMultiChoiceValue(String originalValue, String newValue) + { + updateValue(originalValue, newValue, true); + } + /** * Select a TextChoice value and check if the edit field is enabled. * diff --git a/src/org/labkey/test/components/domain/DomainFormPanel.java b/src/org/labkey/test/components/domain/DomainFormPanel.java index bc596f1e50..145237c0f2 100644 --- a/src/org/labkey/test/components/domain/DomainFormPanel.java +++ b/src/org/labkey/test/components/domain/DomainFormPanel.java @@ -235,6 +235,16 @@ else if (validator instanceof FieldDefinition.TextChoiceValidator textChoiceVali } fieldRow.setTextChoiceValues(textChoiceValidator.getValues()); } + else if (validator instanceof FieldDefinition.MultiChoiceValidator textChoiceValidator) + { + if(!fieldRow.isExpanded()) fieldRow.expand(); + // TextChoice is a field type; implemented using a special validator. TextChoice field cannot have other validators. + if (validators.size() > 1) + { + throw new IllegalArgumentException("TextChoice fields cannot have additional validators."); + } + fieldRow.setTextChoiceValues(textChoiceValidator.getValues()); + } else { throw new IllegalArgumentException("Validator not supported: " + validator.getClass().getName()); 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/params/FieldDefinition.java b/src/org/labkey/test/params/FieldDefinition.java index 78dbb89f17..acbfde8882 100644 --- a/src/org/labkey/test/params/FieldDefinition.java +++ b/src/org/labkey/test/params/FieldDefinition.java @@ -470,6 +470,13 @@ public FieldDefinition setTextChoiceValues(List values) return this; } + public FieldDefinition setMultiChoiceValues(List values) + { + Assert.assertEquals("Invalid field type for text choice values.", ColumnType.MultiChoice, getType()); + setValidators(List.of(new FieldDefinition.MultiChoiceValidator(values))); + return this; + } + public ExpSchema.DerivationDataScopeType getAliquotOption() { return _aliquotOption; @@ -606,6 +613,7 @@ public boolean isMeasureByDefault() ColumnType Sample = new ColumnTypeImpl("Sample", "int", "http://www.labkey.org/exp/xml#sample", new IntLookup( "exp", "Materials")); ColumnType Barcode = new ColumnTypeImpl("Unique ID", "string", "http://www.labkey.org/types#storageUniqueId", null); ColumnType TextChoice = new ColumnTypeImpl("Text Choice", "string", "http://www.labkey.org/types#textChoice", null); + ColumnType MultiChoice = new ColumnTypeImpl("Multi Choice", "string", "http://www.labkey.org/types#multiChoice", null); ColumnType SMILES = new ColumnTypeImpl("SMILES", "string", "http://www.labkey.org/exp/xml#smiles", null); ColumnType Calculation = new ColumnTypeImpl("Calculation", null, "http://www.labkey.org/exp/xml#calculated", null); /** @@ -1140,6 +1148,43 @@ public List getValues() } + public static class MultiChoiceValidator extends FieldValidator + { + private final List _values; + + public MultiChoiceValidator(List values) + { + // The TextChoice validator only has a name and no description or message. + // And the name is generated (not user defined). + super("Text Choice Validator", "", ""); + _values = Collections.unmodifiableList(values); + } + + @Override + protected MultiChoiceValidator getThis() + { + return this; + } + + @Override + protected String getType() + { + return "MultiChoice"; + } + + @Override + protected String getExpression() + { + return EscapeUtil.getTextChoiceValidatorExpression(_values); + } + + public List getValues() + { + return _values; + } + + } + } class ColumnTypeImpl implements FieldDefinition.ColumnType From fabe096a43a10bb6549df6fc72df8f4948c933d2 Mon Sep 17 00:00:00 2001 From: Daria Bodiakova <70635654+DariaBod@users.noreply.github.com> Date: Mon, 26 Jan 2026 15:19:27 -0800 Subject: [PATCH 02/21] some changes --- .../components/domain/DomainFieldRow.java | 15 ++++--- .../components/domain/DomainFormPanel.java | 10 +---- .../labkey/test/params/FieldDefinition.java | 41 ++++--------------- 3 files changed, 20 insertions(+), 46 deletions(-) diff --git a/src/org/labkey/test/components/domain/DomainFieldRow.java b/src/org/labkey/test/components/domain/DomainFieldRow.java index cbb05220bf..c066bbf3cd 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. * @@ -780,6 +785,7 @@ public DomainFieldRow setTextChoiceValues(List values) TextChoiceValueDialog addValuesDialog = new TextChoiceValueDialog(this); addValuesDialog.addValues(values); + return addValuesDialog.clickApply(); } @@ -935,11 +941,6 @@ public String updateLockedTextChoiceValue(String originalValue, String newValue) return alert.getText(); } - public void updateMultiChoiceValue(String originalValue, String newValue) - { - updateValue(originalValue, newValue, true); - } - /** * Select a TextChoice value and check if the edit field is enabled. * @@ -1707,6 +1708,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.inputById("domainpropertiesrow-textChoiceAllowMulti-0-0") + .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 145237c0f2..5ba00274a9 100644 --- a/src/org/labkey/test/components/domain/DomainFormPanel.java +++ b/src/org/labkey/test/components/domain/DomainFormPanel.java @@ -234,16 +234,10 @@ else if (validator instanceof FieldDefinition.TextChoiceValidator textChoiceVali throw new IllegalArgumentException("TextChoice fields cannot have additional validators."); } fieldRow.setTextChoiceValues(textChoiceValidator.getValues()); - } - else if (validator instanceof FieldDefinition.MultiChoiceValidator textChoiceValidator) - { - if(!fieldRow.isExpanded()) fieldRow.expand(); - // TextChoice is a field type; implemented using a special validator. TextChoice field cannot have other validators. - if (validators.size() > 1) + if(textChoiceValidator.getMultipleSelections()) { - throw new IllegalArgumentException("TextChoice fields cannot have additional validators."); + fieldRow.setAllowMultipleSelections(textChoiceValidator.getMultipleSelections()); } - fieldRow.setTextChoiceValues(textChoiceValidator.getValues()); } else { diff --git a/src/org/labkey/test/params/FieldDefinition.java b/src/org/labkey/test/params/FieldDefinition.java index acbfde8882..deb72adab1 100644 --- a/src/org/labkey/test/params/FieldDefinition.java +++ b/src/org/labkey/test/params/FieldDefinition.java @@ -472,8 +472,8 @@ public FieldDefinition setTextChoiceValues(List values) public FieldDefinition setMultiChoiceValues(List values) { - Assert.assertEquals("Invalid field type for text choice values.", ColumnType.MultiChoice, getType()); - setValidators(List.of(new FieldDefinition.MultiChoiceValidator(values))); + Assert.assertEquals("Invalid field type for text choice values.", ColumnType.TextChoice, getType()); + setValidators(List.of(new FieldDefinition.TextChoiceValidator(values).setMultipleSelections())); return this; } @@ -613,7 +613,6 @@ public boolean isMeasureByDefault() ColumnType Sample = new ColumnTypeImpl("Sample", "int", "http://www.labkey.org/exp/xml#sample", new IntLookup( "exp", "Materials")); ColumnType Barcode = new ColumnTypeImpl("Unique ID", "string", "http://www.labkey.org/types#storageUniqueId", null); ColumnType TextChoice = new ColumnTypeImpl("Text Choice", "string", "http://www.labkey.org/types#textChoice", null); - ColumnType MultiChoice = new ColumnTypeImpl("Multi Choice", "string", "http://www.labkey.org/types#multiChoice", null); ColumnType SMILES = new ColumnTypeImpl("SMILES", "string", "http://www.labkey.org/exp/xml#smiles", null); ColumnType Calculation = new ColumnTypeImpl("Calculation", null, "http://www.labkey.org/exp/xml#calculated", null); /** @@ -1115,6 +1114,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. @@ -1146,41 +1147,15 @@ public List getValues() return _values; } - } - - public static class MultiChoiceValidator extends FieldValidator - { - private final List _values; - - public MultiChoiceValidator(List values) - { - // The TextChoice validator only has a name and no description or message. - // And the name is generated (not user defined). - super("Text Choice Validator", "", ""); - _values = Collections.unmodifiableList(values); - } - - @Override - protected MultiChoiceValidator getThis() + public TextChoiceValidator setMultipleSelections() { + this.multipleSelections = true; return this; } - @Override - protected String getType() - { - return "MultiChoice"; - } - - @Override - protected String getExpression() - { - return EscapeUtil.getTextChoiceValidatorExpression(_values); - } - - public List getValues() + public Boolean getMultipleSelections() { - return _values; + return this.multipleSelections; } } From 349d08db85ca3998e31a0171123403d9d740e4ee Mon Sep 17 00:00:00 2001 From: Daria Bodiakova <70635654+DariaBod@users.noreply.github.com> Date: Tue, 27 Jan 2026 16:55:02 -0800 Subject: [PATCH 03/21] add tests for: - SMProAssayCreateTest.testAddResultFields - SMSampleCreateTest.testSamplesWithMultiChoice - SMSourceTypeMultiChoiceTest - ListTest.testMultiChoiceValues --- src/org/labkey/test/WebDriverWrapper.java | 8 +++++ .../components/domain/DomainFieldRow.java | 2 +- src/org/labkey/test/tests/list/ListTest.java | 33 +++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/org/labkey/test/WebDriverWrapper.java b/src/org/labkey/test/WebDriverWrapper.java index b51b78bfc1..c571d84a13 100644 --- a/src/org/labkey/test/WebDriverWrapper.java +++ b/src/org/labkey/test/WebDriverWrapper.java @@ -3465,6 +3465,14 @@ public void setFormElement(Locator l, String text) setFormElement(el, text); } + public void setListElement(Locator l, List text) + { + List elems = l.waitForElements(new WebDriverWait(getDriver(), Duration.ofMillis(WAIT_FOR_JAVASCRIPT))); + elems.forEach(element->{ + if(text.contains(element.getAttribute("value")) ^ element.isSelected()) element.click(); + }); + } + /** * Clears and sets the text of the specified input element. * Warning: Clear unfocuses the element which causes some inputs to disappear. diff --git a/src/org/labkey/test/components/domain/DomainFieldRow.java b/src/org/labkey/test/components/domain/DomainFieldRow.java index c066bbf3cd..fff227aefc 100644 --- a/src/org/labkey/test/components/domain/DomainFieldRow.java +++ b/src/org/labkey/test/components/domain/DomainFieldRow.java @@ -1709,7 +1709,7 @@ protected class ElementCache extends WebDriverComponent.ElementCache .findWhenNeeded(this); // text choice field option - public final Checkbox allowMultipleSelectionsCheckbox = new Checkbox(Locator.inputById("domainpropertiesrow-textChoiceAllowMulti-0-0") + public final Checkbox allowMultipleSelectionsCheckbox = new Checkbox(Locator.tagWithClass("input", "domain-text-choice-multi") .refindWhenNeeded(this).withTimeout(WAIT_FOR_JAVASCRIPT)); // lookup field options diff --git a/src/org/labkey/test/tests/list/ListTest.java b/src/org/labkey/test/tests/list/ListTest.java index 41f8992f59..7921b0b0b9 100644 --- a/src/org/labkey/test/tests/list/ListTest.java +++ b/src/org/labkey/test/tests/list/ListTest.java @@ -1688,6 +1688,39 @@ public void testAutoIncrementKeyEncoded() _listHelper.deleteList(); } + @Test + public void testMultiChoiceValues() + { + // setup a list with an auto-increment key that we need to make sure is encoded in the form input + String encodedListName = "multiChoiceList"; + String keyName = "'>'"; + List tcValues = List.of("0", "1", "2"); + _listHelper.createList(PROJECT_VERIFY, encodedListName, keyName, col("MCF", ColumnType.TextChoice) + .setMultiChoiceValues(tcValues)); + _listHelper.goToList(encodedListName); + + DataRegionTable table = new DataRegionTable("query", getDriver()); + table.clickInsertNewRow(); + List valuesToChoose = List.of("0", "1"); + Locator loc = Locator.tag("select").append(Locator.tag("option")); + setListElement(loc, valuesToChoose); + + clickButton("Submit"); + table = new DataRegionTable("query", getDriver()); + checker().verifyEquals("Name value not as expected", String.join(" ", valuesToChoose), table.getDataAsText(0, "MCF")); + + table.clickEditRow(0); + valuesToChoose = List.of("1", "2"); + setListElement(loc, valuesToChoose); + clickButton("Submit"); + + // verify the name value is persisted + table = new DataRegionTable("query", getDriver()); + checker().verifyEquals("Name value not as expected", String.join(" ", valuesToChoose), table.getDataAsText(0, "MCF")); + + _listHelper.deleteList(); + } + private List getQueryFormFieldNames() { return Locator.tag("input").attributeStartsWith("name", "quf_") From fd9d70040f697c7191c0e0559299f5de747f213b Mon Sep 17 00:00:00 2001 From: Daria Bodiakova <70635654+DariaBod@users.noreply.github.com> Date: Thu, 29 Jan 2026 11:39:00 -0800 Subject: [PATCH 04/21] new changes for test --- src/org/labkey/test/Locator.java | 5 ++++ src/org/labkey/test/WebDriverWrapper.java | 15 +++++----- .../components/domain/DomainFieldRow.java | 1 - .../components/ui/grids/EditableGrid.java | 3 ++ .../labkey/test/pages/DatasetInsertPage.java | 4 ++- .../test/pages/query/UpdateQueryRowPage.java | 5 ++-- src/org/labkey/test/tests/list/ListTest.java | 30 +++++++++++-------- 7 files changed, 38 insertions(+), 25 deletions(-) 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 c571d84a13..19a4f69640 100644 --- a/src/org/labkey/test/WebDriverWrapper.java +++ b/src/org/labkey/test/WebDriverWrapper.java @@ -3465,14 +3465,6 @@ public void setFormElement(Locator l, String text) setFormElement(el, text); } - public void setListElement(Locator l, List text) - { - List elems = l.waitForElements(new WebDriverWait(getDriver(), Duration.ofMillis(WAIT_FOR_JAVASCRIPT))); - elems.forEach(element->{ - if(text.contains(element.getAttribute("value")) ^ element.isSelected()) element.click(); - }); - } - /** * Clears and sets the text of the specified input element. * Warning: Clear unfocuses the element which causes some inputs to disappear. @@ -4067,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 fff227aefc..ef83ced7cb 100644 --- a/src/org/labkey/test/components/domain/DomainFieldRow.java +++ b/src/org/labkey/test/components/domain/DomainFieldRow.java @@ -785,7 +785,6 @@ public DomainFieldRow setTextChoiceValues(List values) TextChoiceValueDialog addValuesDialog = new TextChoiceValueDialog(this); addValuesDialog.addValues(values); - return addValuesDialog.clickApply(); } 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/pages/DatasetInsertPage.java b/src/org/labkey/test/pages/DatasetInsertPage.java index 4651210bdf..33a517ecdc 100644 --- a/src/org/labkey/test/pages/DatasetInsertPage.java +++ b/src/org/labkey/test/pages/DatasetInsertPage.java @@ -23,6 +23,8 @@ import org.openqa.selenium.WebElement; import java.util.Map; +import java.util.Objects; +import java.util.Optional; import static org.labkey.test.util.EscapeUtil.FORM_FIELD_PREFIX; @@ -82,7 +84,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..c2bdd1a975 100644 --- a/src/org/labkey/test/pages/query/UpdateQueryRowPage.java +++ b/src/org/labkey/test/pages/query/UpdateQueryRowPage.java @@ -18,6 +18,7 @@ import java.io.File; import java.util.HashMap; +import java.util.List; import java.util.Map; public class UpdateQueryRowPage extends LabKeyPage @@ -99,7 +100,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 +187,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/tests/list/ListTest.java b/src/org/labkey/test/tests/list/ListTest.java index 7921b0b0b9..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; @@ -1691,32 +1692,35 @@ public void testAutoIncrementKeyEncoded() @Test public void testMultiChoiceValues() { - // setup a list with an auto-increment key that we need to make sure is encoded in the form input + // setup a list with an auto-increment key and multi text choice field String encodedListName = "multiChoiceList"; String keyName = "'>'"; - List tcValues = List.of("0", "1", "2"); - _listHelper.createList(PROJECT_VERIFY, encodedListName, keyName, col("MCF", ColumnType.TextChoice) + 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(); - List valuesToChoose = List.of("0", "1"); - Locator loc = Locator.tag("select").append(Locator.tag("option")); - setListElement(loc, valuesToChoose); + String valuesToChoose = tcValues.subList(1, 3).stream() + .sorted() + .collect(Collectors.joining(" ")); + Locator loc = Locator.nameContaining(EscapeUtil.getFormFieldName(columnName)); + selectOptionByText(loc, valuesToChoose); clickButton("Submit"); - table = new DataRegionTable("query", getDriver()); - checker().verifyEquals("Name value not as expected", String.join(" ", valuesToChoose), table.getDataAsText(0, "MCF")); + checker().verifyEquals("Multi choice value not as expected", valuesToChoose, table.getDataAsText(0, columnName)); table.clickEditRow(0); - valuesToChoose = List.of("1", "2"); - setListElement(loc, valuesToChoose); + valuesToChoose = tcValues.subList(1, 3).stream() + .sorted() + .collect(Collectors.joining(" ")); + selectOptionByText(loc, valuesToChoose); clickButton("Submit"); - // verify the name value is persisted - table = new DataRegionTable("query", getDriver()); - checker().verifyEquals("Name value not as expected", String.join(" ", valuesToChoose), table.getDataAsText(0, "MCF")); + // verify the multi choice value is persisted + checker().verifyEquals("Multi choice value not as expected", valuesToChoose, table.getDataAsText(0, columnName)); _listHelper.deleteList(); } From f349d82ba04436eb6077c27a54423406a7c4c085 Mon Sep 17 00:00:00 2001 From: Daria Bodiakova <70635654+DariaBod@users.noreply.github.com> Date: Mon, 2 Feb 2026 17:00:59 -0800 Subject: [PATCH 05/21] add test testMultiChoiceUpdateFromFile(), testMultiChoiceEditInGridDrag() fix test testMultiChoiceEditInBulk() --- .../ui/entities/EntityBulkUpdateDialog.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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 From e5ec9dbc73f9ac324c0f45a36945eca9cea2e702 Mon Sep 17 00:00:00 2001 From: Daria Bodiakova <70635654+DariaBod@users.noreply.github.com> Date: Thu, 5 Feb 2026 11:13:25 -0800 Subject: [PATCH 06/21] -delete unused imports --- src/org/labkey/test/pages/DatasetInsertPage.java | 2 -- src/org/labkey/test/pages/query/UpdateQueryRowPage.java | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/org/labkey/test/pages/DatasetInsertPage.java b/src/org/labkey/test/pages/DatasetInsertPage.java index 33a517ecdc..4a922f85c7 100644 --- a/src/org/labkey/test/pages/DatasetInsertPage.java +++ b/src/org/labkey/test/pages/DatasetInsertPage.java @@ -23,8 +23,6 @@ import org.openqa.selenium.WebElement; import java.util.Map; -import java.util.Objects; -import java.util.Optional; import static org.labkey.test.util.EscapeUtil.FORM_FIELD_PREFIX; diff --git a/src/org/labkey/test/pages/query/UpdateQueryRowPage.java b/src/org/labkey/test/pages/query/UpdateQueryRowPage.java index c2bdd1a975..ce442e3428 100644 --- a/src/org/labkey/test/pages/query/UpdateQueryRowPage.java +++ b/src/org/labkey/test/pages/query/UpdateQueryRowPage.java @@ -15,10 +15,9 @@ 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.List; import java.util.Map; public class UpdateQueryRowPage extends LabKeyPage From 33683e091b91dff5dc9df449103c0eb5343ed782 Mon Sep 17 00:00:00 2001 From: Daria Bodiakova <70635654+DariaBod@users.noreply.github.com> Date: Thu, 5 Feb 2026 11:25:27 -0800 Subject: [PATCH 07/21] -delete unexpected symbol --- src/org/labkey/test/pages/query/UpdateQueryRowPage.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/org/labkey/test/pages/query/UpdateQueryRowPage.java b/src/org/labkey/test/pages/query/UpdateQueryRowPage.java index ce442e3428..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; From 9a5773a0448375293c8d0fc7fd82d77cb1451bd8 Mon Sep 17 00:00:00 2001 From: Daria Bodiakova <70635654+DariaBod@users.noreply.github.com> Date: Thu, 5 Feb 2026 14:58:33 -0800 Subject: [PATCH 08/21] -some code style fixes -add new shuffleSelect -replace my method with shuffleSelect --- src/org/labkey/test/util/TestDataGenerator.java | 7 +++++++ 1 file changed, 7 insertions(+) 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); From 41e1c78ba789b0ef30197ec7cb9b5cb71676e105 Mon Sep 17 00:00:00 2001 From: Daria Bodiakova <70635654+DariaBod@users.noreply.github.com> Date: Tue, 10 Feb 2026 16:58:32 -0800 Subject: [PATCH 09/21] Apply suggestion from @labkey-tchad Co-authored-by: Trey Chadick --- src/org/labkey/test/pages/query/UpdateQueryRowPage.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/org/labkey/test/pages/query/UpdateQueryRowPage.java b/src/org/labkey/test/pages/query/UpdateQueryRowPage.java index 0f16d59a0e..8ce582e57b 100644 --- a/src/org/labkey/test/pages/query/UpdateQueryRowPage.java +++ b/src/org/labkey/test/pages/query/UpdateQueryRowPage.java @@ -185,7 +185,8 @@ WebElement findField(String name) { if (!fieldMap.containsKey(name)) { - fieldMap.put(name, Locator.nameContaining(EscapeUtil.getFormFieldName(name)).findElement(this)); + // Multi-value text choice fields are prepended with "[]" + fieldMap.put(name, Locator.tag("*").attributeStartsWith("name", EscapeUtil.getFormFieldName(name)).findElement(this)); } return fieldMap.get(name); } From 36a970adeff32a6c7c6f80cdb98c95b328f947c3 Mon Sep 17 00:00:00 2001 From: Cory Nathe Date: Thu, 5 Feb 2026 14:24:58 -0600 Subject: [PATCH 10/21] Provide warnings for unknown fields for cross sample type import (#2862) - Test fixes for change from quoted values to bold in error messages --- src/org/labkey/test/LabKeySiteWrapper.java | 14 ++++++++++---- .../labkey/test/tests/InlineImagesListTest.java | 4 ++-- src/org/labkey/test/tests/SampleTypeTest.java | 16 ++++++++-------- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/org/labkey/test/LabKeySiteWrapper.java b/src/org/labkey/test/LabKeySiteWrapper.java index ee5863824e..1272e53c8e 100644 --- a/src/org/labkey/test/LabKeySiteWrapper.java +++ b/src/org/labkey/test/LabKeySiteWrapper.java @@ -1721,23 +1721,29 @@ protected void deletePipelineJob(@LoggedParam String jobDescription, @LoggedPara public String getConversionErrorMessage(Object value, String fieldName, Class targetClass) { - return getConversionErrorMessage(value, fieldName, targetClass, true); + return getConversionErrorMessage(value, fieldName, targetClass, true, false); } // Note: Keep in sync with ConvertHelper.getStandardConversionErrorMessage() // Example: "Could not convert value '2.34' (Double) for Boolean field 'Medical History.Dep Diagnosed in Last 18 Months'" - public String getConversionErrorMessage(Object value, String fieldName, Class targetClass, boolean useUSDateParsing) + public String getConversionErrorMessage(Object value, String fieldName, Class targetClass, boolean useUSDateParsing, boolean withoutSingleQuotes) { + String errorMessage; String fieldType = targetClass.getSimpleName(); + String quote = withoutSingleQuotes ? "" : "'"; // Issue 50768: Need a better error message if date value is not in the expected format. if (fieldType.equalsIgnoreCase("date") || fieldType.equalsIgnoreCase("datetime") || fieldType.equalsIgnoreCase("timestamp")) { String parsingMode = useUSDateParsing ? "U.S. date parsing (MDY)" : "Non-U.S. date parsing (DMY)"; - return "'" + value + "' is not a valid " + fieldType + " for " + fieldName + " using " + parsingMode; + errorMessage = quote + value + quote + " is not a valid " + fieldType + " for " + fieldName + " using " + parsingMode; + } + else + { + errorMessage = "Could not convert value " + quote + value + quote + " (" + value.getClass().getSimpleName() + ") for " + fieldType + " field " + quote + fieldName + quote; } - return "Could not convert value '" + value + "' (" + value.getClass().getSimpleName() + ") for " + fieldType + " field '" + fieldName + "'" ; + return errorMessage; } private ProductKey getProductConfiguration() throws IOException, CommandException diff --git a/src/org/labkey/test/tests/InlineImagesListTest.java b/src/org/labkey/test/tests/InlineImagesListTest.java index fab04e0bb9..07c9d9ddcc 100644 --- a/src/org/labkey/test/tests/InlineImagesListTest.java +++ b/src/org/labkey/test/tests/InlineImagesListTest.java @@ -372,7 +372,7 @@ public final void testList() throws Exception importFilePathError(listImportPage, "1", PDF_FILE.getName()); importFilePathError(listImportPage, "5", PDF_FILE.getName()); - String attachmentError = "Can't upload '%s' to field %s with type Attachment."; + String attachmentError = "Cannot upload '%s' to Attachment type field '%s'."; String attachmentPdfError = String.format(attachmentError, PDF_FILE.getName(), LIST_ATTACHMENT01_NAME); String attachmentAbsentError = String.format(attachmentError, "Absent.txt", LIST_ATTACHMENT01_NAME); verifyQueryAPI("lists", LIST_NAME, Map.of(LIST_KEY_NAME, 5, LIST_ATTACHMENT01_NAME, PDF_FILE.getName()), true, "Row 1: " + attachmentPdfError); @@ -397,7 +397,7 @@ private void importFilePathError(ImportDataPage listImportPage, String key, Stri listImportPage.submitExpectingError(); try { - String expectedError = "Row 1: Can't upload '" + attachmentValue + "' to field " + LIST_ATTACHMENT01_NAME + " with type Attachment."; + String expectedError = "Row 1: Cannot upload '" + attachmentValue + "' to Attachment type field '" + LIST_ATTACHMENT01_NAME + "'."; checker().withScreenshot("import_error").verifyTrue("Invalid attachment error not as expected", isElementPresent(Locator.tagWithClass("div", "labkey-error").withText(expectedError))); } catch(NoSuchElementException nse) diff --git a/src/org/labkey/test/tests/SampleTypeTest.java b/src/org/labkey/test/tests/SampleTypeTest.java index 4d84069379..b3dd2414aa 100644 --- a/src/org/labkey/test/tests/SampleTypeTest.java +++ b/src/org/labkey/test/tests/SampleTypeTest.java @@ -1883,11 +1883,11 @@ public void testAmountsAndUnitsWithDisplayUnit() log("verify error when inserting a row with an amount but no unit"); sampleHelper.insertRow(Map.of("Name", "AU-ERR-1", "StoredAmount", "0.0")); - assertTextPresent("No Units value provided for Amount 0.0."); + assertTextPresent("No 'Units' value provided for Amount '0.0'."); clickButton("Cancel"); log("verify error when inserting a row with a unit but no amount"); sampleHelper.insertRow(Map.of("Name", "AU-ERR-2", "Units", "mL")); - assertTextPresent("No Amount value provided for Units mL."); + assertTextPresent("No 'Amount' value provided for Units 'mL'."); clickButton("Cancel"); log("verify error when inserting a row with incompatible units"); @@ -1962,11 +1962,11 @@ public void testAmountsAndUnitsWithoutDisplayUnit() log("verify that inserting a row with an amount or unit requires both fields to be filled in"); // insert row with amount but not unit (error expected) sampleHelper.insertRow(Map.of("Name", "AU-ERR-1", "StoredAmount", "5.0")); - assertTextPresent("No Units value provided for Amount 5.0."); + assertTextPresent("No 'Units' value provided for Amount '5.0'."); clickButton("Cancel"); // insert row with unit but not amount (error expected) sampleHelper.insertRow(Map.of("Name", "AU-ERR-2", "Units", "mg")); - assertTextPresent("No Amount value provided for Units mg."); + assertTextPresent("No 'Amount' value provided for Units 'mg'."); clickButton("Cancel"); // insert row with both amount and unit (success) sampleHelper.insertRow(Map.of("Name", "AU-SUCCESS-1", "StoredAmount", "5.0", "Units", "mg")); @@ -1975,11 +1975,11 @@ public void testAmountsAndUnitsWithoutDisplayUnit() log("verify that updating a row with an amount or unit requires both fields to be filled in"); // update row with amount but not unit (error expected) sampleHelper.updateRow(0, Map.of("Units", "")); - assertTextPresent("No Units value provided for Amount 5.0."); + assertTextPresent("No 'Units' value provided for Amount '5.0'."); clickButton("Cancel"); // update row with unit but not amount (error expected) sampleHelper.updateRow(0, Map.of("StoredAmount", "")); - assertTextPresent("No Amount value provided for Units mg."); + assertTextPresent("No 'Amount' value provided for Units 'mg'."); clickButton("Cancel"); // update row with both amount and unit (success) sampleHelper.updateRow(0, Map.of("StoredAmount", "10.0123", "Units", "g")); @@ -1988,11 +1988,11 @@ public void testAmountsAndUnitsWithoutDisplayUnit() log("verify that bulk import with an amount or unit requires both fields to be filled in"); // bulk import with amount but not unit (error expected) sampleHelper.bulkImportExpectingError(List.of(Map.of("Name", "AU-BULK-ERR-1", "StoredAmount", "0")), SampleTypeHelper.IMPORT_OPTION); - assertTextPresent("A Units value must be provided when Amounts are provided"); + assertTextPresent("A 'Units' value must be provided when 'Amounts' are provided"); clickButton("Cancel"); // bulk import with unit but not amount (error expected) sampleHelper.bulkImportExpectingError(List.of(Map.of("Name", "AU-BULK-ERR-2", "Units", "mL")), SampleTypeHelper.IMPORT_OPTION); - assertTextPresent("An Amount value must be provided when Units are provided."); + assertTextPresent("An 'Amount' value must be provided when 'Units' are provided."); clickButton("Cancel"); // bulk import with both amount and unit (success expected) sampleHelper.bulkImport(List.of(Map.of("Name", "AU-BULK-SUCCESS-1", "StoredAmount", "0", "Units", "L"))); From bac1f69324022a61c48a0c71344c53cbefcc49f9 Mon Sep 17 00:00:00 2001 From: Daria Bodiakova <70635654+DariaBod@users.noreply.github.com> Date: Fri, 6 Feb 2026 16:46:42 -0800 Subject: [PATCH 11/21] Dismiss popover for responsive grid (#2874) --- .../test/components/ui/grids/ResponsiveGrid.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java b/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java index 6ea369501a..9ddbf90459 100644 --- a/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java +++ b/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java @@ -337,6 +337,7 @@ public void clickColumnMenuItem(CharSequence columnIdentifier, String menuText, WebElement menu = Locator.css("ul.grid-header-cell__dropdown-menu.open").findWhenNeeded(getDriver()); WebElement menuItem = Locator.css("li > a").containing(menuText).findWhenNeeded(menu); waitFor(menuItem::isDisplayed, 1000); + dismissPopover(); if (waitForUpdate) doAndWaitForUpdate(menuItem::click); else @@ -809,6 +810,17 @@ public Optional getGridEmptyMessage() return msg; } + public void dismissPopover() + { + getWrapper().mouseOut(); + Locators.popover.findOptionalElement(getDriver()).ifPresent(popover -> { + getWrapper().mouseOver(popover); + getWrapper().mouseOut(); + getWrapper().mouseOver(elementCache().getGridHeaderManager().getColumnHeader(0).getElement()); + getWrapper().shortWait().until(ExpectedConditions.invisibilityOf(popover)); + }); + } + public List getHeaders() { return Collections.unmodifiableList(elementCache().findHeaders()); @@ -980,6 +992,7 @@ static public Locator.XPathLocator responsiveGridByBaseId(String baseGridId) static final Locator emptyGrid = Locator.css("tbody tr.grid-empty"); static final Locator spinner = Locator.byClass("fa-spinner"); static final Locator headerCells = Locator.tagWithClass("th", "grid-header-cell"); + static final Locator popover = Locator.byClass("popover"); static public Locator.XPathLocator headerCellBody(String label) { return Locator.tagWithClass("div", "grid-header-cell__body") From c44cbd68192a26ab825d45c1616594bdfd6be1a4 Mon Sep 17 00:00:00 2001 From: Cory Nathe Date: Mon, 9 Feb 2026 09:02:45 -0600 Subject: [PATCH 12/21] Misc fixes for error message single quote change (#2876) getConversionErrorMessage() fix to message case for invalid date/timestamp now quoting field name for consistency --- src/org/labkey/test/LabKeySiteWrapper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/labkey/test/LabKeySiteWrapper.java b/src/org/labkey/test/LabKeySiteWrapper.java index 1272e53c8e..d7af9d257f 100644 --- a/src/org/labkey/test/LabKeySiteWrapper.java +++ b/src/org/labkey/test/LabKeySiteWrapper.java @@ -1736,7 +1736,7 @@ public String getConversionErrorMessage(Object value, String fieldName, Class if (fieldType.equalsIgnoreCase("date") || fieldType.equalsIgnoreCase("datetime") || fieldType.equalsIgnoreCase("timestamp")) { String parsingMode = useUSDateParsing ? "U.S. date parsing (MDY)" : "Non-U.S. date parsing (DMY)"; - errorMessage = quote + value + quote + " is not a valid " + fieldType + " for " + fieldName + " using " + parsingMode; + errorMessage = quote + value + quote + " is not a valid " + fieldType + " for " + quote + fieldName + quote + " using " + parsingMode; } else { From c60c514afbf75dff2b522719c6dff39cddb89992 Mon Sep 17 00:00:00 2001 From: Daria Bodiakova <70635654+DariaBod@users.noreply.github.com> Date: Mon, 9 Feb 2026 11:50:43 -0800 Subject: [PATCH 13/21] Fix expected error messages in tests (#2875) --- .../test/tests/SampleTypeLineageTest.java | 3 ++- .../test/tests/list/ListDateAndTimeTest.java | 21 +++++++++++-------- .../labkey/test/tests/nab/NabAssayTest.java | 3 ++- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/org/labkey/test/tests/SampleTypeLineageTest.java b/src/org/labkey/test/tests/SampleTypeLineageTest.java index 6fd2bf19d9..574dfa68fb 100644 --- a/src/org/labkey/test/tests/SampleTypeLineageTest.java +++ b/src/org/labkey/test/tests/SampleTypeLineageTest.java @@ -30,6 +30,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -311,7 +312,7 @@ at ParentFolder_SampleType in the sub folder it should see one sample (the deriv log("Again check that data validation works as expected."); checker().verifyTrue("Expected error message 'is not a valid Date' is not present.", - isTextPresent("'BadDate' is not a valid Date for DateCol ")); + isTextPresent(getConversionErrorMessage("BadDate", "DateCol", Date.class))); setFormElement(Locator.name("Output Sample 1_DateCol"), "1/1/2007"); clickButton("Submit"); diff --git a/src/org/labkey/test/tests/list/ListDateAndTimeTest.java b/src/org/labkey/test/tests/list/ListDateAndTimeTest.java index c0c7aea7ba..393bd073da 100644 --- a/src/org/labkey/test/tests/list/ListDateAndTimeTest.java +++ b/src/org/labkey/test/tests/list/ListDateAndTimeTest.java @@ -3,6 +3,7 @@ import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; import org.jetbrains.annotations.Nullable; +import org.joda.time.DateTime; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; @@ -33,6 +34,8 @@ import java.io.File; import java.io.IOException; +import java.sql.Time; +import java.sql.Timestamp; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; @@ -41,6 +44,8 @@ import java.util.List; import java.util.Map; +import static org.labkey.test.util.samplemanagement.SMTestUtils.COL_DATETIME_NAME; + @Category({Daily.class, Data.class, Hosting.class}) public class ListDateAndTimeTest extends BaseWebDriverTest { @@ -1022,8 +1027,6 @@ public void testInvalidDateAndTimeInsert() log("Validate adding entries in bulk will give a meaningful error with a bad format."); - String expectedDateErrorMsgFormat = "'%s' is not a valid %s for %s using U.S. date parsing (MDY)"; - String expectedTimeErrorMsgFormat = "Could not convert value '%s' (String) for %s field '%s'"; String badDate = "45/93/2001"; String nonLeapDay = "2/29/2023"; String badTime = "26:abc:604"; @@ -1037,7 +1040,7 @@ public void testInvalidDateAndTimeInsert() checker().withScreenshot() .verifyEquals("Error message for a bad date value not as expected.", - String.format(expectedDateErrorMsgFormat, badDate, "Date", dateCol), actualErrorMsg); + getConversionErrorMessage(badDate, dateCol, Date.class), actualErrorMsg); // Not a leap year. bulkImportText = String.format("%s\t\n%s", dateCol, nonLeapDay); @@ -1046,7 +1049,7 @@ public void testInvalidDateAndTimeInsert() checker().withScreenshot() .verifyEquals("Error message for invalid leap day not as expected.", - String.format(expectedDateErrorMsgFormat, nonLeapDay, "Date", dateCol), actualErrorMsg); + getConversionErrorMessage(nonLeapDay, dateCol, Date.class), actualErrorMsg); bulkImportText = String.format("%s\t\n%s", timeCol, badTime); importPage.setText(bulkImportText); @@ -1054,7 +1057,7 @@ public void testInvalidDateAndTimeInsert() checker().withScreenshot() .verifyEquals("Error message for a bad time value not as expected.", - String.format(expectedTimeErrorMsgFormat, badTime, "Time", timeCol), actualErrorMsg); + getConversionErrorMessage(badTime, timeCol, Time.class), actualErrorMsg); bulkImportText = String.format("%s\t\n%s", dateTimeCol, badDateTime); importPage.setText(bulkImportText); @@ -1062,7 +1065,7 @@ public void testInvalidDateAndTimeInsert() checker().withScreenshot() .verifyEquals("Error message for a bad DateTime value not as expected.", - String.format(expectedDateErrorMsgFormat, badDateTime, "Timestamp", dateTimeCol), actualErrorMsg); + getConversionErrorMessage(badDateTime, dateTimeCol, Timestamp.class), actualErrorMsg); File excelDateTimeFile = TestFileUtils.getSampleData("lists/Bad_Date_And_Time_Values.xlsx"); importPage = importPage.selectUpload(); @@ -1071,9 +1074,9 @@ public void testInvalidDateAndTimeInsert() checker().withScreenshot() .verifyTrue("Error message for bad file import not as expected.", - actualErrorMsg.contains(String.format(expectedTimeErrorMsgFormat, badTime, "Time", timeCol)) && - actualErrorMsg.contains(String.format(expectedDateErrorMsgFormat, nonLeapDay, "Date", dateCol)) && - actualErrorMsg.contains(String.format(expectedDateErrorMsgFormat, badDateTime, "Timestamp", dateTimeCol)) + actualErrorMsg.contains(getConversionErrorMessage(badTime, timeCol, Time.class)) && + actualErrorMsg.contains(getConversionErrorMessage(nonLeapDay, dateCol, Date.class)) && + actualErrorMsg.contains(getConversionErrorMessage(badDateTime, dateTimeCol, Timestamp.class)) ); _listHelper.beginAtList(getProjectName(), listName); diff --git a/src/org/labkey/test/tests/nab/NabAssayTest.java b/src/org/labkey/test/tests/nab/NabAssayTest.java index 1e23d8c42e..f02ae5276c 100644 --- a/src/org/labkey/test/tests/nab/NabAssayTest.java +++ b/src/org/labkey/test/tests/nab/NabAssayTest.java @@ -54,6 +54,7 @@ import java.io.File; import java.util.ArrayList; import java.util.Arrays; +import java.util.Date; import java.util.List; import java.util.Map; @@ -292,7 +293,7 @@ public void runUITests() runFile(TEST_ASSAY_NAB_FILE2). build()).doImport(); - assertElementPresent(Locators.labkeyError.containing("'bad-date' is not a valid Date for Date using U.S. date parsing (MDY)."), 1); + assertElementPresent(Locators.labkeyError.containing(getConversionErrorMessage("bad-date", "Date", Date.class)), 1); // These dates are SQL Server specific // assertElementPresent(Locators.labkeyError.containing("Only dates between January 1, 1753 and December 31, 9999 are accepted."), 1); assertElementPresent(Locators.labkeyError.containing("Only dates between "), 1); From fe04f1fc230dfb445d8127eb1e83585e24cc937a Mon Sep 17 00:00:00 2001 From: Nick Kerr Date: Mon, 9 Feb 2026 14:16:11 -0800 Subject: [PATCH 14/21] Fix build: remove import in ListDateAndTimeTest (#2879) --- src/org/labkey/test/tests/list/ListDateAndTimeTest.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/org/labkey/test/tests/list/ListDateAndTimeTest.java b/src/org/labkey/test/tests/list/ListDateAndTimeTest.java index 393bd073da..613b65510b 100644 --- a/src/org/labkey/test/tests/list/ListDateAndTimeTest.java +++ b/src/org/labkey/test/tests/list/ListDateAndTimeTest.java @@ -3,7 +3,6 @@ import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; import org.jetbrains.annotations.Nullable; -import org.joda.time.DateTime; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; @@ -44,8 +43,6 @@ import java.util.List; import java.util.Map; -import static org.labkey.test.util.samplemanagement.SMTestUtils.COL_DATETIME_NAME; - @Category({Daily.class, Data.class, Hosting.class}) public class ListDateAndTimeTest extends BaseWebDriverTest { From 0435138dffcfb19719fda2aa605efbcfb2e69a92 Mon Sep 17 00:00:00 2001 From: Dan Duffek Date: Tue, 10 Feb 2026 05:48:01 -0800 Subject: [PATCH 15/21] Updating SampleFinder Test Component (#2877) --- src/org/labkey/test/components/ui/search/SampleFinder.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/org/labkey/test/components/ui/search/SampleFinder.java b/src/org/labkey/test/components/ui/search/SampleFinder.java index 905777a5b0..9e02bb9451 100644 --- a/src/org/labkey/test/components/ui/search/SampleFinder.java +++ b/src/org/labkey/test/components/ui/search/SampleFinder.java @@ -109,6 +109,11 @@ public void removeSearchCard(String queryName) elementCache().findFilterCard(queryName).clickRemove(); } + public EntityFieldFilterModal editSearchCard(String queryName) + { + return elementCache().findFilterCard(queryName).clickEdit(); + } + /** * Reset the sample finder to its initial state, with no search criteria */ From b4d52e09b48c6e1e5c53d65bf7d76d9f0c5bef4f Mon Sep 17 00:00:00 2001 From: Daria Bodiakova <70635654+DariaBod@users.noreply.github.com> Date: Tue, 10 Feb 2026 17:03:54 -0800 Subject: [PATCH 16/21] fix comments about selectFilter javadoc and initFilterColumn method --- .../components/ui/grids/ResponsiveGrid.java | 22 +++++++++++++------ .../ui/search/FilterFacetedPanel.java | 19 ++++++++++++---- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java b/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java index 9ddbf90459..1f79698df5 100644 --- a/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java +++ b/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java @@ -17,6 +17,7 @@ import org.labkey.test.components.react.ReactCheckBox; import org.labkey.test.components.ui.grids.FieldReferenceManager.FieldReference; import org.labkey.test.components.ui.search.FilterExpressionPanel; +import org.labkey.test.components.ui.search.FilterFacetedPanel; import org.labkey.test.params.FieldKey; import org.labkey.test.util.selenium.WebElementUtils; import org.openqa.selenium.Keys; @@ -46,6 +47,8 @@ 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.remoteapi.query.Filter.Operator.IS_EMPTY; +import static org.labkey.remoteapi.query.Filter.Operator.IS_NOT_EMPTY; import static org.labkey.test.WebDriverWrapper.waitFor; public class ResponsiveGrid> extends WebDriverComponent.ElementCache> implements UpdatingComponent @@ -241,7 +244,7 @@ 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); + DOES_NOT_CONTAIN_EXACTLY, IS_EMPTY, IS_NOT_EMPTY); clickColumnMenuItem(columnIdentifier, "Filter...", false); GridFilterModal filterModal = new GridFilterModal(getDriver(), this); if (operator != null) @@ -249,9 +252,13 @@ private GridFilterModal initFilterColumn(CharSequence columnIdentifier, Filter.O 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()); + FilterFacetedPanel filterPanel = filterModal.selectFacetTab(); + filterPanel.selectValue(values.get(0)); + filterPanel.checkValues(values.toArray(String[]::new)); + if (filterPanel.isFiltersPresented()) + { + filterPanel.selectFilter(operator); + } } else filterModal.selectExpressionTab().setFilter(new FilterExpressionPanel.Expression(operator, value)); @@ -394,15 +401,16 @@ public T selectRow(int index, boolean checked) /** * Finds the first row with the specified texts in the specified columns, and sets its checkbox - * @param partialMap key-column (fieldKey, name, or label), value-text in that column - * @param checked the desired checkbox state + * + * @param partialMap key-column (fieldKey, name, or label), value-text in that column + * @param checked the desired checkbox state * @return this grid */ public T selectRow(Map partialMap, boolean checked) { GridRow row = getRow(partialMap); selectRowAndVerifyCheckedCounts(row, checked); - getWrapper().log("Row described by map ["+partialMap+"] selection state set to + ["+row.isSelected()+"]"); + getWrapper().log("Row described by map [" + partialMap + "] selection state set to + [" + row.isSelected() + "]"); return getThis(); } diff --git a/src/org/labkey/test/components/ui/search/FilterFacetedPanel.java b/src/org/labkey/test/components/ui/search/FilterFacetedPanel.java index 1c014b7dbd..d3bd25a609 100644 --- a/src/org/labkey/test/components/ui/search/FilterFacetedPanel.java +++ b/src/org/labkey/test/components/ui/search/FilterFacetedPanel.java @@ -8,6 +8,7 @@ import org.labkey.test.components.html.Input; import org.labkey.test.components.react.ReactSelect; import org.labkey.test.components.ui.FilterStatusValue; +import org.labkey.remoteapi.query.Filter; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.ui.ExpectedConditions; @@ -15,7 +16,9 @@ import java.util.List; import java.util.stream.Collectors; +import static org.labkey.test.WebDriverWrapper.waitFor; import static org.labkey.test.components.html.Input.Input; +import static org.labkey.test.util.samplemanagement.SMTestUtils.isVisible; public class FilterFacetedPanel extends WebDriverComponent { @@ -50,12 +53,20 @@ public void selectValue(String value) } /** - * Select a single facet value by clicking its label. Should replace all existing selections. - * @param value desired value + * Check that filter choosing option exists on the page. + */ + public boolean isFiltersPresented() + { + return waitFor(() -> isVisible(elementCache().filterTypeSelects), 1000); + } + + /** + * Select a filer by clicking its label. Right now this method relevant only for multi-value text choice. + * @param operator desired filter value */ - public void selectFilter(String value) + public void selectFilter(Filter.Operator operator) { - elementCache().filterTypeSelects.select(value); + elementCache().filterTypeSelects.select(operator.getDisplayValue()); } /** From 6648cf10385f4504b6d501eabe09240e4d72279a Mon Sep 17 00:00:00 2001 From: Daria Bodiakova <70635654+DariaBod@users.noreply.github.com> Date: Fri, 13 Feb 2026 09:35:03 -0800 Subject: [PATCH 17/21] Apply suggestions from code review Co-authored-by: Trey Chadick --- src/org/labkey/test/tests/list/ListTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/org/labkey/test/tests/list/ListTest.java b/src/org/labkey/test/tests/list/ListTest.java index d942a7e643..2e37d09e11 100644 --- a/src/org/labkey/test/tests/list/ListTest.java +++ b/src/org/labkey/test/tests/list/ListTest.java @@ -1692,6 +1692,7 @@ public void testAutoIncrementKeyEncoded() @Test public void testMultiChoiceValues() { + Assume.assumeTrue("Multi-choice text fields are only supported on PostgreSQL", WebTestHelper.getDatabaseType() == WebTestHelper.DatabaseType.PostgreSQL); // setup a list with an auto-increment key and multi text choice field String encodedListName = "multiChoiceList"; String keyName = "'>'"; From d1751b34f9ae8b00e92f1005cefa44359b9c6b22 Mon Sep 17 00:00:00 2001 From: Daria Bodiakova <70635654+DariaBod@users.noreply.github.com> Date: Fri, 13 Feb 2026 09:41:03 -0800 Subject: [PATCH 18/21] Apply suggestions from code review Co-authored-by: Trey Chadick --- src/org/labkey/test/tests/list/ListTest.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/org/labkey/test/tests/list/ListTest.java b/src/org/labkey/test/tests/list/ListTest.java index 2e37d09e11..54132a65db 100644 --- a/src/org/labkey/test/tests/list/ListTest.java +++ b/src/org/labkey/test/tests/list/ListTest.java @@ -1694,9 +1694,9 @@ public void testMultiChoiceValues() { Assume.assumeTrue("Multi-choice text fields are only supported on PostgreSQL", WebTestHelper.getDatabaseType() == WebTestHelper.DatabaseType.PostgreSQL); // setup a list with an auto-increment key and multi text choice field - String encodedListName = "multiChoiceList"; - String keyName = "'>'"; - String columnName = "MultiChoiceField"; + String encodedListName = TestDataGenerator.randomDomainName("multiChoiceList", DomainUtils.DomainKind.IntList); + String keyName = TestDataGenerator.randomFieldName("'>'"); + String columnName = TestDataGenerator.randomFieldName("MultiChoiceField"); List tcValues = List.of("~`!@#$%^&*()_+=[]{}\\|';:\"<>?,./", "1", "2"); _listHelper.createList(PROJECT_VERIFY, encodedListName, keyName, col(columnName, ColumnType.TextChoice) .setMultiChoiceValues(tcValues)); @@ -1711,7 +1711,7 @@ public void testMultiChoiceValues() selectOptionByText(loc, valuesToChoose); clickButton("Submit"); - checker().verifyEquals("Multi choice value not as expected", valuesToChoose, table.getDataAsText(0, columnName)); + checker().withScreenshot().verifyEquals("Multi choice value not as expected", valuesToChoose, table.getDataAsText(0, columnName)); table.clickEditRow(0); valuesToChoose = tcValues.subList(1, 3).stream() @@ -1721,7 +1721,7 @@ public void testMultiChoiceValues() clickButton("Submit"); // verify the multi choice value is persisted - checker().verifyEquals("Multi choice value not as expected", valuesToChoose, table.getDataAsText(0, columnName)); + checker().withScreenshot().verifyEquals("Multi choice value not as expected", valuesToChoose, table.getDataAsText(0, columnName)); _listHelper.deleteList(); } From 187e9b40424b5aac24bc2e48486442f25ec0df5f Mon Sep 17 00:00:00 2001 From: Susan Hert Date: Tue, 10 Feb 2026 07:22:11 -0800 Subject: [PATCH 19/21] Move getExpectedAuditDataChange method to AuditLogHelper. (#2880) --- src/org/labkey/test/util/AuditLogHelper.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/org/labkey/test/util/AuditLogHelper.java b/src/org/labkey/test/util/AuditLogHelper.java index 89bf6e0010..7509898328 100644 --- a/src/org/labkey/test/util/AuditLogHelper.java +++ b/src/org/labkey/test/util/AuditLogHelper.java @@ -293,6 +293,17 @@ public Map getTransactionAuditLogDetails(Integer transactionAudi return new JSONObject(detailJSON).toMap(); } + public static String getExpectedAuditDataChange(String field, Object oldValue, Object newValue) + { + String dataChangeString = field + ": "; + if (oldValue != null) + dataChangeString += oldValue; + if (newValue != null) + dataChangeString += " > " + newValue; + return dataChangeString; + } + + public void checkLastTransactionAuditLogDetails(String containerPath, Map expectedDetails) { Integer transactionAuditId = getLastTransactionId(containerPath); From 7c72826c27f249268b55cc8c86808fb9e5783225 Mon Sep 17 00:00:00 2001 From: Josh Eckels Date: Wed, 4 Feb 2026 10:25:10 -0800 Subject: [PATCH 20/21] Make CSP error logging validation more robust (#2868) --- .../labkey/test/tests/wiki/WikiCspTest.java | 31 ++++++++++++++----- .../test/util/core/admin/CspConfigHelper.java | 5 +++ 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/org/labkey/test/tests/wiki/WikiCspTest.java b/src/org/labkey/test/tests/wiki/WikiCspTest.java index e54819b6d8..5f6976008a 100644 --- a/src/org/labkey/test/tests/wiki/WikiCspTest.java +++ b/src/org/labkey/test/tests/wiki/WikiCspTest.java @@ -13,6 +13,7 @@ import org.labkey.test.util.CspLogUtil; import org.labkey.test.util.TextSearcher; import org.labkey.test.util.WikiHelper; +import org.labkey.test.util.core.admin.CspConfigHelper; import java.util.Arrays; import java.util.List; @@ -21,7 +22,7 @@ @BaseWebDriverTest.ClassTimeout(minutes = 2) public class WikiCspTest extends BaseWebDriverTest { - private static final String PROJECT_NAME = TRICKY_CHARACTERS_FOR_PROJECT_NAMES + "WikiCspTest"; + private static final String PROJECT_NAME = TRICKY_CHARACTERS_FOR_PROJECT_NAMES; private static final String WIKI_PAGE_TITLE = "TOC_with_inline"; private static final String WIKI_PAGE_BODY = // Issue 52483: HTML substitution patterns can throw errors during wiki validation @@ -53,6 +54,17 @@ private void doSetup() _containerHelper.createProject(PROJECT_NAME, null); _containerHelper.enableModules(Arrays.asList("Wiki")); goToProjectHome(); + CspConfigHelper.debugCspWarnings(); // Ensure that CSP violation logs aren't suppressed by de-duping efforts + } + + @Override + protected void doCleanup(boolean afterTest) + { + super.doCleanup(afterTest); + if (afterTest) + { + CspConfigHelper.infoCspWarnings(); + } } @Override @@ -76,13 +88,18 @@ public void testCspChecks() waitForText("Click me"); - try + waitFor(() -> { - CspLogUtil.checkNewCspWarnings(getArtifactCollector()); - } - catch (CspLogUtil.CspWarningDetectedException ignore) {} - - goToAdminConsole().goToSettingsSection(); + try + { + CspLogUtil.checkNewCspWarnings(getArtifactCollector()); + return false; + } + catch (CspLogUtil.CspWarningDetectedException ignore) + { + return true; + } + }, "Should have triggered a CSP error", WAIT_FOR_PAGE); SiteValidationPage validationPage = goToAdminConsole().clickSiteValidation(); validationPage.setAllValidators(false); diff --git a/src/org/labkey/test/util/core/admin/CspConfigHelper.java b/src/org/labkey/test/util/core/admin/CspConfigHelper.java index a8ec0125ed..55f4546e2b 100644 --- a/src/org/labkey/test/util/core/admin/CspConfigHelper.java +++ b/src/org/labkey/test/util/core/admin/CspConfigHelper.java @@ -75,6 +75,11 @@ public static void debugCspWarnings() Log4jUtils.setLogLevel("org.labkey.core.admin.AdminController.ContentSecurityPolicyReportAction", ManagerPage.LoggingLevel.DEBUG); } + public static void infoCspWarnings() + { + Log4jUtils.setLogLevel("org.labkey.core.admin.AdminController.ContentSecurityPolicyReportAction", ManagerPage.LoggingLevel.INFO); + } + public static class AllowedHost { private final Directive _directive; From 87c688b90c363639322dd669dc03767e24007db2 Mon Sep 17 00:00:00 2001 From: Daria Bodiakova <70635654+DariaBod@users.noreply.github.com> Date: Fri, 13 Feb 2026 17:30:19 -0800 Subject: [PATCH 21/21] fix all pr comments --- src/org/labkey/test/Locator.java | 5 -- src/org/labkey/test/WebDriverWrapper.java | 7 --- .../components/domain/DomainFormPanel.java | 11 ++++- .../components/ui/grids/ResponsiveGrid.java | 8 +++- .../labkey/test/pages/DatasetInsertPage.java | 2 +- .../test/pages/query/UpdateQueryRowPage.java | 2 - .../labkey/test/params/FieldDefinition.java | 47 +++++++++++++++---- src/org/labkey/test/tests/list/ListTest.java | 34 +++++++------- .../labkey/test/util/data/TestDataUtils.java | 15 ++++++ 9 files changed, 87 insertions(+), 44 deletions(-) diff --git a/src/org/labkey/test/Locator.java b/src/org/labkey/test/Locator.java index badd2c0669..17bd5dc435 100644 --- a/src/org/labkey/test/Locator.java +++ b/src/org/labkey/test/Locator.java @@ -606,11 +606,6 @@ 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 9d9247318b..d251eb47f0 100644 --- a/src/org/labkey/test/WebDriverWrapper.java +++ b/src/org/labkey/test/WebDriverWrapper.java @@ -4059,13 +4059,6 @@ 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/DomainFormPanel.java b/src/org/labkey/test/components/domain/DomainFormPanel.java index c86da34ddd..f1d66a4729 100644 --- a/src/org/labkey/test/components/domain/DomainFormPanel.java +++ b/src/org/labkey/test/components/domain/DomainFormPanel.java @@ -236,10 +236,17 @@ 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(false); + } + else if (validator instanceof FieldDefinition.MultiValueTextChoiceValidator multiValueTextChoiceValidator) + { + // MultiValueTextChoice is a field type; implemented using a special validator. TextChoice field cannot have other validators. + if (validators.size() > 1) { - fieldRow.setAllowMultipleSelections(textChoiceValidator.getMultipleSelections()); + throw new IllegalArgumentException("TextChoice fields cannot have additional validators."); } + fieldRow.setTextChoiceValues(multiValueTextChoiceValidator.getValues()); + fieldRow.setAllowMultipleSelections(true); } else { diff --git a/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java b/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java index 1f79698df5..c50ba05ab2 100644 --- a/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java +++ b/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java @@ -244,7 +244,7 @@ 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, IS_EMPTY, IS_NOT_EMPTY); + DOES_NOT_CONTAIN_EXACTLY); clickColumnMenuItem(columnIdentifier, "Filter...", false); GridFilterModal filterModal = new GridFilterModal(getDriver(), this); if (operator != null) @@ -260,8 +260,14 @@ private GridFilterModal initFilterColumn(CharSequence columnIdentifier, Filter.O filterPanel.selectFilter(operator); } } + else if (value == null) + { + filterModal.selectFacetTab().selectFilter(operator); + } else + { filterModal.selectExpressionTab().setFilter(new FilterExpressionPanel.Expression(operator, value)); + } } return filterModal; } diff --git a/src/org/labkey/test/pages/DatasetInsertPage.java b/src/org/labkey/test/pages/DatasetInsertPage.java index 4a922f85c7..70e7f2434c 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.nameContaining(EscapeUtil.getFormFieldName(entry.getKey())).findElement(getDriver()); + WebElement fieldInput = Locator.tag("*").attributeEndsWith("name", 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 ff97300800..ca383a6f53 100644 --- a/src/org/labkey/test/pages/query/UpdateQueryRowPage.java +++ b/src/org/labkey/test/pages/query/UpdateQueryRowPage.java @@ -192,8 +192,6 @@ WebElement findField(String name, boolean multiValue) { if (!fieldMap.containsKey(name)) { - // Multi-value text choice fields are prepended with "[]" - fieldMap.put(name, Locator.tag("*").attributeStartsWith("name", EscapeUtil.getFormFieldName(name)).findElement(this)); fieldMap.put(name, Locator.name(EscapeUtil.getFormFieldName(name, multiValue)).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 0e2fa00b88..3f426ef2a7 100644 --- a/src/org/labkey/test/params/FieldDefinition.java +++ b/src/org/labkey/test/params/FieldDefinition.java @@ -477,8 +477,8 @@ public FieldDefinition setTextChoiceValues(List values) 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())); + Assert.assertEquals("Invalid field type for text choice values.", ColumnType.MultiValueTextChoice, getType()); + setValidators(List.of(new FieldDefinition.MultiValueTextChoiceValidator(values))); return this; } @@ -618,6 +618,7 @@ public boolean isMeasureByDefault() ColumnType Sample = new ColumnTypeImpl("Sample", "int", "http://www.labkey.org/exp/xml#sample", new IntLookup( "exp", "Materials")); ColumnType Barcode = new ColumnTypeImpl("Unique ID", "string", "http://www.labkey.org/types#storageUniqueId", null); ColumnType TextChoice = new ColumnTypeImpl("Text Choice", "string", "http://www.labkey.org/types#textChoice", null); + ColumnType MultiValueTextChoice = new ColumnTypeImpl("Text Choice", "string", "http://cpas.fhcrc.org/exp/xml#multiChoice", null); ColumnType SMILES = new ColumnTypeImpl("SMILES", "string", "http://www.labkey.org/exp/xml#smiles", null); ColumnType Calculation = new ColumnTypeImpl("Calculation", null, "http://www.labkey.org/exp/xml#calculated", null); /** @@ -1119,8 +1120,6 @@ 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. @@ -1152,15 +1151,47 @@ public List getValues() return _values; } - public TextChoiceValidator setMultipleSelections() + } + + /** + * TextChoice is implemented using a validator, however it is more 'limited' in scope. The user does not name a TextChoice + * validator or add a description or error message. A TextChoice is a lot like a look-up field, but it is not linked + * to an external data source. The user only provides the list of (string) values that the field will display in the dropdown. + * Another difference is that there can only be one TextChoice on a field, whereas you can have multiple validators on a field. + */ + public static class MultiValueTextChoiceValidator extends FieldValidator + { + private final List _values; + + public MultiValueTextChoiceValidator(List values) + { + // The TextChoice validator only has a name and no description or message. + // And the name is generated (not user defined). + super("Text Choice Validator", "", ""); + _values = Collections.unmodifiableList(values); + } + + @Override + protected MultiValueTextChoiceValidator getThis() { - this.multipleSelections = true; return this; } - public Boolean getMultipleSelections() + @Override + protected String getType() { - return this.multipleSelections; + return "TextChoice"; + } + + @Override + protected String getExpression() + { + return EscapeUtil.getTextChoiceValidatorExpression(_values); + } + + public List getValues() + { + return _values; } } diff --git a/src/org/labkey/test/tests/list/ListTest.java b/src/org/labkey/test/tests/list/ListTest.java index 54132a65db..1b47a050e1 100644 --- a/src/org/labkey/test/tests/list/ListTest.java +++ b/src/org/labkey/test/tests/list/ListTest.java @@ -18,6 +18,7 @@ import org.hamcrest.CoreMatchers; import org.hamcrest.MatcherAssert; +import org.junit.Assume; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -84,7 +85,6 @@ 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; @@ -1698,30 +1698,28 @@ public void testMultiChoiceValues() String keyName = TestDataGenerator.randomFieldName("'>'"); String columnName = TestDataGenerator.randomFieldName("MultiChoiceField"); List tcValues = List.of("~`!@#$%^&*()_+=[]{}\\|';:\"<>?,./", "1", "2"); - _listHelper.createList(PROJECT_VERIFY, encodedListName, keyName, col(columnName, ColumnType.TextChoice) + _listHelper.createList(PROJECT_VERIFY, encodedListName, keyName, col(columnName, ColumnType.MultiValueTextChoice) .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().withScreenshot().verifyEquals("Multi choice value not as expected", valuesToChoose, table.getDataAsText(0, columnName)); + UpdateQueryRowPage insertNewRow = table.clickInsertNewRow(); + List valuesToChoose = tcValues.subList(1, 3); + valuesToChoose.forEach(value->{ + insertNewRow.setField(columnName, value); + }); + insertNewRow.submit(); + checker().withScreenshot().verifyEquals("Multi choice value not as expected", String.join(" ", valuesToChoose), table.getDataAsText(0, columnName)); - table.clickEditRow(0); - valuesToChoose = tcValues.subList(1, 3).stream() - .sorted() - .collect(Collectors.joining(" ")); - selectOptionByText(loc, valuesToChoose); - clickButton("Submit"); + UpdateQueryRowPage editRow = table.clickEditRow(0); + valuesToChoose = tcValues.subList(1, 3); + valuesToChoose.forEach(value->{ + editRow.setField(columnName, value); + }); + editRow.submit(); // verify the multi choice value is persisted - checker().withScreenshot().verifyEquals("Multi choice value not as expected", valuesToChoose, table.getDataAsText(0, columnName)); + checker().withScreenshot().verifyEquals("Multi choice value not as expected", String.join(" ", valuesToChoose), table.getDataAsText(0, columnName)); _listHelper.deleteList(); } diff --git a/src/org/labkey/test/util/data/TestDataUtils.java b/src/org/labkey/test/util/data/TestDataUtils.java index 3eb899cb29..b7ed252c1a 100644 --- a/src/org/labkey/test/util/data/TestDataUtils.java +++ b/src/org/labkey/test/util/data/TestDataUtils.java @@ -24,6 +24,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.Reader; +import java.io.StringReader; import java.io.StringWriter; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -577,6 +578,20 @@ public static List> readRowsFromFile(File file, CSVFormat format) t } } + public static List parseMultiValueText(String multiValueString) throws IOException + { + CSVFormat format = CSVFormat.RFC4180.builder() + .setIgnoreSurroundingSpaces(true).get(); + try (CSVParser parser = format.parse(new StringReader(multiValueString))) + { + List records = parser.getRecords(); + List> list = records.stream().map(CSVRecord::toList).toList(); + if (list.size() != 1) + throw new IllegalArgumentException("Invalid multi-value text string: " + multiValueString); + return list.getFirst(); + } + } + public static String stringFromRows(List> rows, CSVFormat format) { StringWriter stringWriter = new StringWriter();