From 836866329797d7b799117b19ef95180d272e11e4 Mon Sep 17 00:00:00 2001 From: labkey-susanh Date: Sun, 26 Apr 2026 20:05:31 -0700 Subject: [PATCH 1/6] Update locator for bulk edit row to accommodate row with multiple inputs (e.g., StoredAmount and Units) --- .../test/components/ui/entities/EntityBulkUpdateDialog.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java b/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java index c358b7d65c..22ee798f7c 100644 --- a/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java +++ b/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java @@ -457,7 +457,7 @@ public WebElement formRow(CharSequence fieldIdentifier) { String fieldKey = FieldKey.fromName(fieldIdentifier).toString(); return Locator.tagWithClass("div", "row") - .withChild(Locator.tagWithAttribute("label", "for", fieldKey)) + .withDescendant(Locator.tagWithAttribute("label", "for", fieldKey)) .waitForElement(this, WAIT_TIMEOUT); } From 08d747bfbfd742772bfe5c25c81952f3c554b9d7 Mon Sep 17 00:00:00 2001 From: cnathe Date: Tue, 28 Apr 2026 09:03:27 -0500 Subject: [PATCH 2/6] Update EntityBulkInsertDialog and EntityBulkUpdateDialog amount/units setters to use helpers in abstract EntityBulkDialog --- .../ui/entities/EntityBulkDialog.java | 217 ++++++++++++++++++ .../ui/entities/EntityBulkInsertDialog.java | 86 +------ .../ui/entities/EntityBulkUpdateDialog.java | 80 +------ 3 files changed, 232 insertions(+), 151 deletions(-) create mode 100644 src/org/labkey/test/components/ui/entities/EntityBulkDialog.java diff --git a/src/org/labkey/test/components/ui/entities/EntityBulkDialog.java b/src/org/labkey/test/components/ui/entities/EntityBulkDialog.java new file mode 100644 index 0000000000..6d7e399c23 --- /dev/null +++ b/src/org/labkey/test/components/ui/entities/EntityBulkDialog.java @@ -0,0 +1,217 @@ +package org.labkey.test.components.ui.entities; + +import org.apache.commons.lang3.StringUtils; +import org.labkey.api.util.Pair; +import org.labkey.test.Locator; +import org.labkey.test.components.bootstrap.ModalDialog; +import org.labkey.test.components.html.Checkbox; +import org.labkey.test.components.html.Input; +import org.labkey.test.components.react.FilteringReactSelect; +import org.labkey.test.components.react.ReactDateTimePicker; +import org.labkey.test.components.react.ReactSelect; +import org.labkey.test.components.react.ToggleButton; +import org.labkey.test.components.ui.files.FileAttachmentContainer; +import org.labkey.test.params.FieldKey; +import org.openqa.selenium.WebElement; + +import java.util.HashMap; +import java.util.Map; + +import static org.labkey.test.WebDriverWrapper.WAIT_FOR_JAVASCRIPT; + +/** + * Abstract base for {@link EntityBulkInsertDialog} and {@link EntityBulkUpdateDialog}. + */ +public abstract class EntityBulkDialog extends ModalDialog +{ + protected int _changeCounter = 0; + + protected EntityBulkDialog(ModalDialogFinder finder) + { + super(finder); + } + + /** + * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey}) + * @return current value of the specified field + */ + public String getTextArea(CharSequence fieldIdentifier) + { + return elementCache().textArea(fieldIdentifier).get(); + } + + /** + * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey}) + * @return current value of the specified field + */ + public String getNumericField(CharSequence fieldIdentifier) + { + return elementCache().textInput(fieldIdentifier).get(); + } + + /** + * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey}) + * @return current value of the specified field + */ + public boolean getBooleanField(CharSequence fieldIdentifier) + { + return elementCache().checkBox(fieldIdentifier).get(); + } + + public String getFieldValue(WebElement input) + { + String value = input.getText(); + if (StringUtils.isEmpty(value)) + value = input.getAttribute("value"); + if (StringUtils.isEmpty(value)) + value = input.getAttribute("placeholder"); + if (StringUtils.isEmpty(value) && "checkbox".equals(input.getAttribute("type"))) + value = input.getAttribute("title"); + return value; + } + + protected String getValueForReactSelect(ReactSelect reactSelect) + { + if (!reactSelect.getSelections().isEmpty()) + { + return reactSelect.getSelections().get(0); + } + else + { + return ""; + } + } + + private WebElement getAmountUnitsRow() + { + String fieldLabel = "Amount and Units"; + return elementCache().formRowByControlLabel(fieldLabel); + } + + public Pair getAmountUnit() + { + String amountVal = getFieldValue(getAmountInput()); + String unitVal = Locator.tagWithClass("div", "select-input__value-container").withoutAttribute("type", "hidden").findElement(getAmountUnitsRow()).getText(); + return new Pair<>(amountVal, unitVal); + } + + public Pair getAmountUnitValue() + { + enableAmountUnit(); + String amountVal = getWrapper().getFormElement(getAmountInput()); + String unitVal = getValueForReactSelect(getAmountUnitSelect()); + return new Pair<>(amountVal, unitVal); + } + + public void enableAmountUnit() + { + ToggleButton toggle = new ToggleButton.ToggleButtonFinder(getDriver()).findOrNull(getAmountUnitsRow()); + if (toggle != null && !toggle.isOn()) + { + toggle.set(true); + _changeCounter++; + } + } + + public void disableAmountUnit() + { + ToggleButton toggle = new ToggleButton.ToggleButtonFinder(getDriver()).findOrNull(getAmountUnitsRow()); + if(toggle != null && toggle.isOn()) + { + toggle.set(false); + _changeCounter--; + } + } + + private WebElement getAmountInput() + { + return elementCache().amountInputLoc.findElement(getAmountUnitsRow()); + } + + public ReactSelect getAmountUnitSelect() + { + enableAmountUnit(); + return new ReactSelect(getAmountUnitsRow(), getDriver()); + } + + public void setAmountUnit(String amount, String unit) + { + enableAmountUnit(); + + if (amount != null) + getWrapper().setFormElement(getAmountInput(), amount); + if (unit != null) + { + ReactSelect reactSelect = getAmountUnitSelect(); + if (!unit.isEmpty()) + reactSelect.select(unit); + else + reactSelect.clearSelection(); + } + + if (amount != null && unit != null) + _changeCounter++; + } + + @Override + protected abstract ElementCache newElementCache(); + + @Override + protected ElementCache elementCache() + { + return (ElementCache) super.elementCache(); + } + + protected abstract class ElementCache extends ModalDialog.ElementCache + { + protected final Locator textInputLoc = Locator.tagWithAttribute("input", "type", "text"); + protected final Locator checkBoxLoc = Locator.tagWithAttribute("input", "type", "checkbox"); + protected final Locator.XPathLocator amountInputLoc = Locator.tag("input").withAttribute("aria-label", "Amount"); + + protected final Map _rows = new HashMap<>(); + + /** + * Returns the form row div that contains the controls for the given field. + */ + public abstract WebElement formRow(CharSequence fieldIdentifier); + + // For composite fields (e.g. StoredAmount + Units) that render a
label instead of