diff --git a/src/org/labkey/test/BaseWebDriverTest.java b/src/org/labkey/test/BaseWebDriverTest.java
index cc9345dbd9..85887514a5 100644
--- a/src/org/labkey/test/BaseWebDriverTest.java
+++ b/src/org/labkey/test/BaseWebDriverTest.java
@@ -40,7 +40,7 @@
import org.junit.runners.model.MultipleFailureException;
import org.junit.runners.model.Statement;
import org.junit.runners.model.TestTimedOutException;
-import org.labkey.api.query.QueryKey;
+import org.labkey.api.query.FieldKey;
import org.labkey.junit.rules.TestWatcher;
import org.labkey.remoteapi.CommandException;
import org.labkey.remoteapi.CommandResponse;
@@ -218,7 +218,7 @@ public abstract class BaseWebDriverTest extends LabKeySiteWrapper implements Cle
public static final double DELTA = 10E-10;
- public static final String[] ILLEGAL_QUERY_KEY_CHARACTERS = QueryKey.ILLEGAL;
+ public static final String[] ILLEGAL_QUERY_KEY_CHARACTERS = FieldKey.ILLEGAL;
public static final String ALL_ILLEGAL_QUERY_KEY_CHARACTERS = StringUtils.join(ILLEGAL_QUERY_KEY_CHARACTERS, "");
// See TSVWriter.shouldQuote. Generally we are not able to use the tab and new line characters when creating field names in the UI, but including here for completeness
public static final String[] TRICKY_IMPORT_FIELD_CHARACTERS = {"\\", "\"", "\\t", ",", "\\n", "\\r"};
diff --git a/src/org/labkey/test/TestFileUtils.java b/src/org/labkey/test/TestFileUtils.java
index fe93c8bfaa..fcbed755cd 100644
--- a/src/org/labkey/test/TestFileUtils.java
+++ b/src/org/labkey/test/TestFileUtils.java
@@ -48,6 +48,7 @@
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
+import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
@@ -55,6 +56,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
@@ -470,7 +472,12 @@ public static File writeTempFile(String name, String contents) throws IOExceptio
*/
public static File writeFile(File file, String contents) throws IOException
{
- try (Writer writer = PrintWriters.getPrintWriter(file))
+ return writeFile(file, contents, false);
+ }
+
+ public static File writeFile(File file, String contents, boolean append) throws IOException
+ {
+ try (Writer writer = new OutputStreamWriter(new FileOutputStream(file, append), StandardCharsets.UTF_8))
{
writer.write(contents);
return file;
@@ -578,10 +585,10 @@ private static List unTar(final File inputFile, final File outputDir) thro
{
final List untaredFiles = new ArrayList<>();
try (InputStream is = new FileInputStream(inputFile);
- TarArchiveInputStream inputStream = (TarArchiveInputStream) new ArchiveStreamFactory().createArchiveInputStream("tar", is))
+ TarArchiveInputStream inputStream = new ArchiveStreamFactory().createArchiveInputStream("tar", is))
{
TarArchiveEntry entry;
- while ((entry = (TarArchiveEntry) inputStream.getNextEntry()) != null)
+ while ((entry = inputStream.getNextEntry()) != null)
{
final File outputFile = new File(outputDir, entry.getName());
if (entry.isDirectory())
@@ -598,7 +605,7 @@ private static List unTar(final File inputFile, final File outputDir) thro
{
try (OutputStream outputFileStream = new FileOutputStream(outputFile))
{
- org.apache.commons.compress.utils.IOUtils.copy(inputStream, outputFileStream);
+ IOUtils.copy(inputStream, outputFileStream);
}
}
untaredFiles.add(outputFile);
diff --git a/src/org/labkey/test/WebDriverWrapper.java b/src/org/labkey/test/WebDriverWrapper.java
index e894e92d18..f5dbd43307 100644
--- a/src/org/labkey/test/WebDriverWrapper.java
+++ b/src/org/labkey/test/WebDriverWrapper.java
@@ -3434,7 +3434,7 @@ public void setFormElement(Locator l, String text)
*/
public void setFormElement(WebElement el, String text)
{
- String inputType = el.getAttribute("type");
+ String inputType = el.getDomAttribute("type");
if ("file".equals(inputType))
{
diff --git a/src/org/labkey/test/components/ManageSampleStatusesPanel.java b/src/org/labkey/test/components/ManageSampleStatusesPanel.java
index 31d889a0a4..3777ad8495 100644
--- a/src/org/labkey/test/components/ManageSampleStatusesPanel.java
+++ b/src/org/labkey/test/components/ManageSampleStatusesPanel.java
@@ -158,7 +158,7 @@ public boolean isLocked()
public List getStatusNames()
{
return elementCache().statusItems
- .findElements(this)
+ .waitForElements(this, 2_000)
.stream()
.map(WebElement::getText)
.collect(Collectors.toList());
diff --git a/src/org/labkey/test/components/react/Tabs.java b/src/org/labkey/test/components/react/Tabs.java
index 3336a8c9c2..2d0e5c51c5 100644
--- a/src/org/labkey/test/components/react/Tabs.java
+++ b/src/org/labkey/test/components/react/Tabs.java
@@ -53,9 +53,14 @@ public WebDriver getDriver()
return _driver;
}
+ public WebElement findTab(String tabText)
+ {
+ return elementCache().findTab(tabText);
+ }
+
public WebElement findPanelForTab(String tabText)
{
- return elementCache().findTabPanel(tabText);
+ return elementCache().findTabPanel(elementCache().findTab(tabText));
}
public WebElement selectTab(String tabText)
@@ -63,14 +68,19 @@ public WebElement selectTab(String tabText)
WebElement tab = elementCache().findTab(tabText);
getWrapper().scrollIntoView(tab);
tab.click();
- WebElement panel = findPanelForTab(tabText);
+ WebElement panel = elementCache().findTabPanel(tab);
getWrapper().shortWait().until(ExpectedConditions.visibilityOf(panel));
return panel;
}
+ public WebElement findPanelForActiveTab()
+ {
+ return elementCache().findTabPanel(elementCache().findSelectedTab());
+ }
+
public boolean isTabSelected(String tabText)
{
- return Boolean.valueOf(elementCache().findTab(tabText).getAttribute("aria-selected"));
+ return Boolean.valueOf(elementCache().findTab(tabText).getDomAttribute("aria-selected"));
}
public List getTabText()
@@ -80,6 +90,16 @@ public List getTabText()
.stream().map(WebElement::getText).toList();
}
+ public String getSelectedTabText()
+ {
+ return elementCache().findSelectedTab().getText();
+ }
+
+ public String getSelectedTabKey()
+ {
+ return elementCache().findSelectedTab().getDomAttribute("data-event-key");
+ }
+
@Override
protected ElementCache newElementCache()
{
@@ -102,6 +122,11 @@ public ElementCache()
}
}
+ protected WebElement findSelectedTab()
+ {
+ return tabLoc.withAttribute("aria-selected", "true").findElement(this);
+ }
+
List findAllTabs()
{
if (tabs.isEmpty())
@@ -125,7 +150,7 @@ WebElement findTab(String tabText)
catch (NoSuchElementException ex)
{
throw new NoSuchElementException(String.format("'%s' not among available tabs: %s",
- tabText, getWrapper().getTexts(findAllTabs())), ex);
+ tabText, getWrapper().getTexts(findAllTabs())), ex);
}
tabMap.put(tabText, tabEl);
}
@@ -133,9 +158,9 @@ WebElement findTab(String tabText)
}
// Tab panels can be updated and changed when flipping between tabs. Don't persist the panel element find it each time.
- WebElement findTabPanel(String tabText)
+ WebElement findTabPanel(WebElement tabElement)
{
- String panelId = findTab(tabText).getAttribute("aria-controls");
+ String panelId = tabElement.getDomAttribute("aria-controls");
WebElement panelEl;
try
{
@@ -143,7 +168,7 @@ WebElement findTabPanel(String tabText)
}
catch (NoSuchElementException ex)
{
- throw new NoSuchElementException("Panel not found for tab : " + tabText, ex);
+ throw new NoSuchElementException("Panel not found for tab : " + tabElement.getText(), ex);
}
return panelEl;
diff --git a/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java b/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java
index d2bdff4ce9..49b7c87ad3 100644
--- a/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java
+++ b/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java
@@ -1,6 +1,5 @@
package org.labkey.test.components.ui.entities;
-import org.jetbrains.annotations.Nullable;
import org.labkey.test.BootstrapLocators;
import org.labkey.test.Locator;
import org.labkey.test.WebDriverWrapper;
@@ -10,18 +9,24 @@
import org.labkey.test.components.html.RadioButton;
import org.labkey.test.components.react.FilteringReactSelect;
import org.labkey.test.components.react.ReactDateTimePicker;
+import org.labkey.test.components.ui.files.FileAttachmentContainer;
import org.labkey.test.params.FieldDefinition;
-import org.labkey.test.util.EscapeUtil;
+import org.labkey.test.params.FieldKey;
import org.openqa.selenium.StaleElementReferenceException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedConditions;
+import java.io.File;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
+/**
+ * `fieldIdentifier` arguments accept field names or {@link FieldKey}s
+ */
public class EntityBulkInsertDialog extends ModalDialog
{
public EntityBulkInsertDialog(WebDriver driver)
@@ -29,7 +34,7 @@ public EntityBulkInsertDialog(WebDriver driver)
this(new ModalDialogFinder(driver).withTitle("Bulk Add"));
}
- private EntityBulkInsertDialog(ModalDialogFinder finder)
+ protected EntityBulkInsertDialog(ModalDialogFinder finder)
{
super(finder);
}
@@ -91,11 +96,11 @@ public String getCreationTypeSelected()
{
if(elementCache().derivativesOption.isChecked())
{
- option = elementCache().derivativesOption.getComponentElement().getAttribute("value");
+ option = elementCache().derivativesOption.getComponentElement().getDomAttribute("value");
}
else
{
- option = elementCache().poolOption.getComponentElement().getAttribute("value");
+ option = elementCache().poolOption.getComponentElement().getDomAttribute("value");
}
}
@@ -104,12 +109,7 @@ public String getCreationTypeSelected()
public EntityBulkInsertDialog setQuantity(int quantity)
{
- return setQuantity(Integer.toString(quantity));
- }
-
- public EntityBulkInsertDialog setQuantity(String quantity)
- {
- getWrapper().setFormElement(elementCache().quantity, quantity);
+ getWrapper().setFormElement(elementCache().quantity, Integer.toString(quantity));
return this;
}
@@ -161,83 +161,129 @@ public String getDescription()
return getWrapper().getFormElement(elementCache().description);
}
- public EntityBulkInsertDialog setTextField(String fieldKey, String value)
+ /**
+ * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey})
+ * @param value value to set
+ * @return this component
+ */
+ public EntityBulkInsertDialog setTextArea(CharSequence fieldIdentifier, String value)
+ {
+ elementCache().textArea(fieldIdentifier).set(value);
+ return this;
+ }
+
+ /**
+ * @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})
+ * @param value value to set
+ * @return this component
+ */
+ public EntityBulkInsertDialog setTextField(CharSequence fieldIdentifier, String value)
{
- elementCache().textInput(fieldKey).set(value);
+ elementCache().textInput(fieldIdentifier).set(value);
return this;
}
- public String getTextField(String fieldKey)
+ /**
+ * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey})
+ * @return current value of the specified field
+ */
+ public String getTextField(CharSequence fieldIdentifier)
{
- return elementCache().textInput(fieldKey).get();
+ return elementCache().textInput(fieldIdentifier).get();
}
- public EntityBulkInsertDialog setNumericField(String fieldKey, String value)
+ /**
+ * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey})
+ * @param value value to set
+ * @return this component
+ */
+ public EntityBulkInsertDialog setNumericField(CharSequence fieldIdentifier, String value)
{
- elementCache().numericInput(fieldKey).set(value);
+ elementCache().numericInput(fieldIdentifier).set(value);
return this;
}
- public String getNumericField(String fieldKey)
+ /**
+ * @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().numericInput(fieldKey).get();
+ return elementCache().numericInput(fieldIdentifier).get();
}
- public EntityBulkInsertDialog setSelectionField(String fieldCaption, List selectValues)
+ /**
+ * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey})
+ * @param selectValues values to select
+ * @return this component
+ */
+ public EntityBulkInsertDialog setSelectionField(CharSequence fieldIdentifier, List selectValues)
{
- FilteringReactSelect reactSelect = elementCache().selectionField(fieldCaption);
+ FilteringReactSelect reactSelect = elementCache().selectionField(fieldIdentifier);
selectValues.forEach(reactSelect::filterSelect);
return this;
}
- public List getSelectionField(String fieldCaption)
+ /**
+ * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey})
+ * @return current value of the specified field
+ */
+ public List getSelectionField(CharSequence fieldIdentifier)
{
- FilteringReactSelect reactSelect = elementCache().selectionField(fieldCaption);
+ FilteringReactSelect reactSelect = elementCache().selectionField(fieldIdentifier);
return reactSelect.getSelections();
}
/**
* Clear the value(s) from a field that is a drop down selection field.
*
- * @param fieldCaption Caption for the field.
+ * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey})
* @return This insert dialog.
*/
- public EntityBulkInsertDialog clearSelectionField(String fieldCaption)
+ public EntityBulkInsertDialog clearSelectionField(CharSequence fieldIdentifier)
{
- FilteringReactSelect reactSelect = elementCache().selectionField(fieldCaption);
+ FilteringReactSelect reactSelect = elementCache().selectionField(fieldIdentifier);
reactSelect.clearSelection();
return this;
}
- public EntityBulkInsertDialog setFieldWithId(String id, String value)
- {
- getWrapper().setFormElement(Locator.tagWithId("input", id), value);
- return this;
- }
-
- public String getFieldWithId(String id)
+ // For use when the field is of an unknown type, as can occur in fuzz tests
+ public void setValue(FieldDefinition field, Object newValue)
{
- return getWrapper().getFormElement(Locator.tagWithId("input", id));
+ if (field.getType() == FieldDefinition.ColumnType.TextChoice || field.getLookup() != null)
+ setSelectionField(field.getName(), newValue instanceof String ? List.of((String) newValue) : (List) newValue);
+ else if (field.getType() == FieldDefinition.ColumnType.Integer || field.getType() == FieldDefinition.ColumnType.Decimal || field.getType() == FieldDefinition.ColumnType.Double)
+ setNumericField(field.getName(), String.valueOf(newValue));
+ else if (field.getType() == FieldDefinition.ColumnType.Date || field.getType() == FieldDefinition.ColumnType.DateAndTime || field.getType() == FieldDefinition.ColumnType.Time)
+ setDateTimeField(field.getName(), newValue);
+ else if (field.getType() == FieldDefinition.ColumnType.Boolean)
+ setBooleanField(field.getName(), (Boolean) newValue);
+ else if (field.getType() == FieldDefinition.ColumnType.MultiLine)
+ setTextArea(field.getName(), (String) newValue);
+ else if (field.getType() == FieldDefinition.ColumnType.File)
+ attachFile(field.getName(), (File) newValue);
+ else
+ setTextField(field.getName(), (String) newValue);
}
public void setInsertFieldValues(List fields, Map data)
{
for (FieldDefinition field : fields)
{
- String fieldKey = EscapeUtil.fieldKeyEncodePart(field.getName());
- Object value = data.get(field.getLabel() != null ? field.getLabel() : FieldDefinition.labelFromName(field.getName()));
+ Object value = data.get(field.getEffectiveLabel());
if (value == null)
continue;
- if (field.getType() == FieldDefinition.ColumnType.Boolean)
- setBooleanField(fieldKey, (Boolean) value);
- else if (field.getType() == FieldDefinition.ColumnType.Integer || field.getType() == FieldDefinition.ColumnType.Decimal || field.getType() == FieldDefinition.ColumnType.Double)
- setNumericField(fieldKey, String.valueOf(value));
- else if (field.getType() == FieldDefinition.ColumnType.Date || field.getType() == FieldDefinition.ColumnType.DateAndTime || field.getType() == FieldDefinition.ColumnType.Time)
- setDateTimeField(fieldKey, value);
- else if (field.getType() == FieldDefinition.ColumnType.TextChoice)
- setSelectionField(field.getLabel(), (List) value);
- else
- setTextField(fieldKey, (String) value);
+
+ setValue(field, value);
}
}
@@ -246,32 +292,66 @@ else if (field.getType() == FieldDefinition.ColumnType.TextChoice)
* object to use the picker to set the field. If a text value is passed in it is used as a literal and jut typed
* into the textbox.
*
- * @param fieldKey Field to update.
+ * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey})
* @param dateTime A LocalDateTime, LocalDate, LocalTime or String.
* @return A reference to this page.
*/
- public EntityBulkInsertDialog setDateTimeField(String fieldKey, Object dateTime)
+ public EntityBulkInsertDialog setDateTimeField(CharSequence fieldIdentifier, Object dateTime)
{
- ReactDateTimePicker dateTimePicker = elementCache().dateInput(fieldKey);
+ ReactDateTimePicker dateTimePicker = elementCache().dateInput(fieldIdentifier);
dateTimePicker.select(dateTime);
return this;
}
- public String getDateTimeField(String fieldKey)
+ /**
+ * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey})
+ * @return current value of the specified field
+ */
+ public String getDateTimeField(CharSequence fieldIdentifier)
{
- return elementCache().dateInput(fieldKey).get();
+ return elementCache().dateInput(fieldIdentifier).get();
}
- public EntityBulkInsertDialog setBooleanField(String fieldKey, boolean checked)
+ /**
+ * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey})
+ * @param checked value to set
+ * @return this component
+ */
+ public EntityBulkInsertDialog setBooleanField(CharSequence fieldIdentifier, boolean checked)
{
- Checkbox box = elementCache().checkBox(fieldKey);
+ Checkbox box = elementCache().checkBox(fieldIdentifier);
box.set(checked);
return this;
}
- public boolean getBooleanField(String fieldKey)
+ /**
+ * @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();
+ }
+
+ /**
+ * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey})
+ * @param file file to attach
+ * @return this component
+ */
+ public EntityBulkInsertDialog attachFile(CharSequence fieldIdentifier, File file)
{
- return elementCache().checkBox(fieldKey).get();
+ elementCache().fileUploadField(fieldIdentifier).attachFile(file);
+ return this;
+ }
+
+ /**
+ * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey})
+ * @return this component
+ */
+ public EntityBulkInsertDialog removeFile(CharSequence fieldIdentifier)
+ {
+ elementCache().fileUploadField(fieldIdentifier).removeFile();
+ return this;
}
/**
@@ -360,6 +440,16 @@ protected void waitForReady()
getWrapper().shortWait().until(ExpectedConditions.elementToBeClickable( elementCache().addRowsButton ));
}
+ /**
+ * File upload fields append "-fileUpload" to the field's fieldKey
+ * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey})
+ * @return FieldKey with expected suffix
+ */
+ public static FieldKey fileUploadFieldKey(CharSequence fieldIdentifier)
+ {
+ return FieldKey.fromFieldKey(FieldKey.fromName(fieldIdentifier) + "-fileUpload");
+ }
+
@Override
protected ElementCache newElementCache()
{
@@ -374,42 +464,57 @@ protected ElementCache elementCache()
protected class ElementCache extends ModalDialog.ElementCache
{
- public Locator validationMessage = Locator.tagWithClass("span", "validation-message");
+ public final Locator validationMessage = Locator.tagWithClass("span", "validation-message");
+
+ private final Map _rows = new HashMap<>();
- public WebElement formRow(String fieldKey)
+ public WebElement formRow(CharSequence fieldIdentifier)
{
- return Locator.tagWithClass("div", "row")
- .withChild(Locator.tagWithAttribute("label", "for", fieldKey))
- .findElement(this);
+ String fieldKey = FieldKey.fromName(fieldIdentifier).toString();
+ return _rows.computeIfAbsent(fieldKey, fk ->
+ Locator.tagWithClass("div", "row")
+ // TODO: Shouldn't need to be case-insensitive. Parent/source lookups have weird casing
+ .withChild(Locator.tagWithAttributeIgnoreCase("label", "for", fieldKey))
+ .findElement(this));
}
- public FilteringReactSelect selectionField(String fieldCaption)
+ public FilteringReactSelect selectionField(CharSequence fieldIdentifier)
{
- return FilteringReactSelect.finder(getDriver()).followingLabelWithSpan(fieldCaption).find(this);
+ return new FilteringReactSelect(formRow(fieldIdentifier), getDriver());
}
- public Checkbox checkBox(String fieldKey)
+ public Checkbox checkBox(CharSequence fieldIdentifier)
{
- WebElement row = elementCache().formRow(fieldKey);
+ WebElement row = formRow(fieldIdentifier);
return new Checkbox(checkBoxLoc.findElement(row));
}
- public Input textInput(String fieldKey)
+ public Input textInput(CharSequence fieldIdentifier)
{
- WebElement inputEl = textInputLoc.findElement(elementCache().formRow(fieldKey));
+ WebElement inputEl = textInputLoc.findElement(formRow(fieldIdentifier));
return new Input(inputEl, getDriver());
}
- public Input numericInput(String fieldKey)
+ public Input textArea(CharSequence fieldIdentifier)
{
- WebElement inputEl = numberInputLoc.findElement(formRow(fieldKey));
+ WebElement inputEl = Locator.tag("textarea").findElement(formRow(fieldIdentifier));
return new Input(inputEl, getDriver());
}
- public ReactDateTimePicker dateInput(String fieldKey)
+ public Input numericInput(CharSequence fieldIdentifier)
+ {
+ WebElement inputEl = numberInputLoc.findElement(formRow(fieldIdentifier));
+ return new Input(inputEl, getDriver());
+ }
+
+ public ReactDateTimePicker dateInput(CharSequence fieldIdentifier)
+ {
+ return new ReactDateTimePicker.ReactDateTimeInputFinder(getDriver()).find(formRow(fieldIdentifier));
+ }
+
+ public FileAttachmentContainer fileUploadField(CharSequence fieldIdentifier)
{
- return new ReactDateTimePicker.ReactDateTimeInputFinder(getDriver())
- .withInputId(fieldKey).find(formRow(fieldKey));
+ return new FileAttachmentContainer(formRow(fileUploadFieldKey(fieldIdentifier)), getDriver());
}
public List fieldLabels()
diff --git a/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java b/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java
index 090c8207f5..14716f277d 100644
--- a/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java
+++ b/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java
@@ -1,5 +1,6 @@
package org.labkey.test.components.ui.entities;
+import org.jetbrains.annotations.NotNull;
import org.labkey.remoteapi.CommandException;
import org.labkey.test.BootstrapLocators;
import org.labkey.test.Locator;
@@ -15,20 +16,22 @@
import org.labkey.test.components.react.ToggleButton;
import org.labkey.test.components.ui.files.FileAttachmentContainer;
import org.labkey.test.params.FieldDefinition;
+import org.labkey.test.params.FieldKey;
import org.labkey.test.util.AuditLogHelper;
-import org.labkey.test.util.EscapeUtil;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
+import java.io.File;
import java.io.IOException;
import java.time.Duration;
-import java.util.ArrayList;
import java.util.List;
/**
- * Automates product component src/components/forms/QueryInfoForms, with BulkUpdateForm.d.ts
+ * Automates product component src/components/forms/QueryInfoForms, with BulkUpdateForm.d.ts
+ *
+ * `fieldIdentifier` arguments accept field names or {@link FieldKey}s
*/
public class EntityBulkUpdateDialog extends ModalDialog
{
@@ -58,18 +61,26 @@ public EntityBulkUpdateDialog adjustChangeCounter(int change)
return this;
}
- // enable/disable field editable state
-
- public boolean isFieldEnabled(String fieldKey)
+ /**
+ * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey})
+ */
+ public boolean isFieldEnabled(CharSequence fieldIdentifier)
{
- return elementCache().getToggle(fieldKey).isOn();
+ return elementCache().getToggle(fieldIdentifier).isOn();
}
- public EntityBulkUpdateDialog setEditableState(String fieldKey, boolean enable)
+ /**
+ * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey})
+ */
+ public EntityBulkUpdateDialog setEditableState(CharSequence fieldIdentifier, boolean enable)
{
- elementCache().getToggle(fieldKey).set(enable);
- if (enable) _changeCounter++;
- else _changeCounter--;
+ ToggleButton toggle = elementCache().getToggle(fieldIdentifier);
+ if (toggle.isOn() != enable)
+ {
+ toggle.set(enable);
+ if (enable) _changeCounter++;
+ else _changeCounter--;
+ }
return this;
}
@@ -82,121 +93,212 @@ private WebDriverWait waiter()
public void setValue(FieldDefinition field, Object newValue)
{
if (field.getType() == FieldDefinition.ColumnType.TextChoice || field.getLookup() != null)
- setSelectionField(EscapeUtil.fieldKeyEncodePart(field.getName()), newValue instanceof String ? List.of((String) newValue) : (List) newValue);
+ setSelectionField(field.getName(), newValue instanceof String ? List.of((String) newValue) : (List) newValue);
else if (field.getType() == FieldDefinition.ColumnType.Integer || field.getType() == FieldDefinition.ColumnType.Decimal || field.getType() == FieldDefinition.ColumnType.Double)
- setNumericField(EscapeUtil.fieldKeyEncodePart(field.getName()), String.valueOf(newValue));
+ setNumericField(field.getName(), String.valueOf(newValue));
else if (field.getType() == FieldDefinition.ColumnType.Date || field.getType() == FieldDefinition.ColumnType.DateAndTime || field.getType() == FieldDefinition.ColumnType.Time)
- setDateField(EscapeUtil.fieldKeyEncodePart(field.getName()), (String) newValue);
+ setDateField(field.getName(), (String) newValue);
else if (field.getType() == FieldDefinition.ColumnType.Boolean)
- setBooleanField(EscapeUtil.fieldKeyEncodePart(field.getName()), (Boolean) newValue);
+ setBooleanField(field.getName(), (Boolean) newValue);
else if (field.getType() == FieldDefinition.ColumnType.MultiLine)
- setTextArea(EscapeUtil.fieldKeyEncodePart(field.getName()), (String) newValue);
+ setTextArea(field.getName(), (String) newValue);
else
- setTextField(EscapeUtil.fieldKeyEncodePart(field.getName()), (String) newValue);
+ setTextField(field.getName(), (String) newValue);
}
// interact with selection fields
- public EntityBulkUpdateDialog setSelectionField(String fieldKey, List selectValues)
+ /**
+ * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey})
+ * @param selectValues value to select
+ * @return this component
+ */
+ public EntityBulkUpdateDialog setSelectionField(CharSequence fieldIdentifier, List selectValues)
{
- setEditableState(fieldKey, true);
- FilteringReactSelect reactSelect = elementCache().getSelect(fieldKey);
- WebDriverWrapper.waitFor(reactSelect::isEnabled,
- "the ["+fieldKey+"] reactSelect did not become enabled in time", WAIT_TIMEOUT);
+ FilteringReactSelect reactSelect = enableSelectionField(fieldIdentifier);
selectValues.forEach(reactSelect::filterSelect);
return this;
}
- public List getSelectionOptions(String fieldKey)
+ /**
+ * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey})
+ * @param selectValue value to select
+ * @return this component
+ */
+ public EntityBulkUpdateDialog setSelectionField(CharSequence fieldIdentifier, String selectValue)
{
- return enableAndWait(fieldKey, elementCache().getSelect(fieldKey)).getOptions();
+ return setSelectionField(fieldIdentifier, List.of(selectValue));
}
- public List getSelectionFieldValues(String fieldKey)
+ /**
+ * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey})
+ * @return available options for the specified field
+ */
+ public List getSelectionOptions(CharSequence fieldIdentifier)
{
- return enableAndWait(fieldKey, elementCache().getSelect(fieldKey)).getSelections();
+ return enableSelectionField(fieldIdentifier).getOptions();
}
- public EntityBulkUpdateDialog setTextArea(String fieldKey, String text)
+ /**
+ * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey})
+ * @return selected options for the specified field
+ */
+ public List getSelectionFieldValues(CharSequence fieldIdentifier)
{
- enableAndWait(fieldKey, elementCache().textArea(fieldKey)).set(text);
- return this;
+ return enableSelectionField(fieldIdentifier).getSelections();
}
- public String getTextArea(String fieldKey)
+ private @NotNull FilteringReactSelect enableSelectionField(CharSequence fieldIdentifier)
{
- return elementCache().textArea(fieldKey).get();
+ setEditableState(fieldIdentifier, true);
+ FilteringReactSelect reactSelect = elementCache().getSelect(fieldIdentifier);
+ WebDriverWrapper.waitFor(reactSelect::isEnabled,
+ "the ["+ fieldIdentifier +"] reactSelect did not become enabled in time", WAIT_TIMEOUT);
+ return reactSelect;
}
- // get/set text fields with ID
+ /**
+ * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey})
+ * @param value value to set
+ * @return this component
+ */
+ public EntityBulkUpdateDialog setTextArea(CharSequence fieldIdentifier, String value)
+ {
+ enableAndWait(fieldIdentifier, elementCache().textArea(fieldIdentifier)).set(value);
+ return this;
+ }
+
+ /**
+ * @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();
+ }
- public EntityBulkUpdateDialog setTextField(String fieldKey, String value)
+ /**
+ * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey})
+ * @param value value to set
+ * @return this component
+ */
+ public EntityBulkUpdateDialog setTextField(CharSequence fieldIdentifier, String value)
{
- enableAndWait(fieldKey, elementCache().textInput(fieldKey)).set(value);
+ enableAndWait(fieldIdentifier, elementCache().textInput(fieldIdentifier)).set(value);
return this;
}
- public String getTextField(String fieldKey)
+ /**
+ * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey})
+ * @return current value of the specified field
+ */
+ public String getTextField(CharSequence fieldIdentifier)
{
- return enableAndWait(fieldKey, elementCache().textInput(fieldKey)).get();
+ return enableAndWait(fieldIdentifier, elementCache().textInput(fieldIdentifier)).get();
}
- public EntityBulkUpdateDialog setNumericField(String fieldKey, String value)
+ /**
+ * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey})
+ * @param value value to set
+ * @return this component
+ */
+ public EntityBulkUpdateDialog setNumericField(CharSequence fieldIdentifier, String value)
{
- enableAndWait(fieldKey, elementCache().numericInput(fieldKey)).set(value);
+ enableAndWait(fieldIdentifier, elementCache().numericInput(fieldIdentifier)).set(value);
return this;
}
- public String getNumericField(String fieldKey)
+ /**
+ * @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().numericInput(fieldKey).get();
+ return elementCache().numericInput(fieldIdentifier).get();
}
- public EntityBulkUpdateDialog setDateField(String fieldKey, String dateString)
+ /**
+ * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey})
+ * @param dateString string representation of date to set
+ * @return this component
+ */
+ public EntityBulkUpdateDialog setDateField(CharSequence fieldIdentifier, String dateString)
{
- enableAndWait(fieldKey, elementCache().dateInput(fieldKey)).set(dateString);
+ enableAndWait(fieldIdentifier, elementCache().dateInput(fieldIdentifier)).set(dateString);
return this;
}
- public String getDateField(String fieldKey)
+ /**
+ * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey})
+ * @return current value of the specified field
+ */
+ public String getDateField(CharSequence fieldIdentifier)
{
- return elementCache().dateInput(fieldKey).get();
+ return elementCache().dateInput(fieldIdentifier).get();
}
- public FileAttachmentContainer getFileField(String fieldKey)
+ /**
+ * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey})
+ * @return file attachment component
+ */
+ public FileAttachmentContainer getFileField(CharSequence fieldIdentifier)
{
- return elementCache().fileUploadField(fieldKey);
+ fieldIdentifier = EntityBulkInsertDialog.fileUploadFieldKey(fieldIdentifier);
+ return enableAndWait(fieldIdentifier, elementCache().fileUploadField(fieldIdentifier));
}
- public EntityBulkUpdateDialog removeFile(String fieldKey)
+ /**
+ * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey})
+ * @param file file to attach
+ * @return this component
+ */
+ public EntityBulkUpdateDialog attachFile(CharSequence fieldIdentifier, File file)
{
- getFileField(fieldKey).removeFile();
- _changeCounter++;
+ getFileField(fieldIdentifier).attachFile(file);
return this;
}
- public EntityBulkUpdateDialog setBooleanField(String fieldKey, boolean checked)
+ /**
+ * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey})
+ * @return this component
+ */
+ public EntityBulkUpdateDialog removeFile(CharSequence fieldIdentifier)
{
- enableAndWait(fieldKey, getCheckBox(fieldKey)).set(checked);
+ getFileField(fieldIdentifier).removeFile();
return this;
}
- private > T enableAndWait(String fieldKey, T formItem)
+ /**
+ * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey})
+ * @param checked value to set
+ * @return this component
+ */
+ public EntityBulkUpdateDialog setBooleanField(CharSequence fieldIdentifier, boolean checked)
{
- setEditableState(fieldKey, true);
+ enableAndWait(fieldIdentifier, getCheckBox(fieldIdentifier)).set(checked);
+ return this;
+ }
+
+ private > T enableAndWait(CharSequence fieldIdentifier, T formItem)
+ {
+ setEditableState(fieldIdentifier, true);
// "Clickable" means visible and enabled
waiter().until(ExpectedConditions.elementToBeClickable(formItem.getComponentElement()));
return formItem;
}
- public boolean getBooleanField(String fieldKey)
+ /**
+ * @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 getCheckBox(fieldKey).get();
+ return getCheckBox(fieldIdentifier).get();
}
- private Checkbox getCheckBox(String fieldKey)
+ private Checkbox getCheckBox(CharSequence fieldIdentifier)
{
- WebElement row = elementCache().formRow(fieldKey);
+ WebElement row = elementCache().formRow(fieldIdentifier);
return new Checkbox(elementCache().checkBoxLoc.findElement(row));
}
@@ -215,7 +317,7 @@ public List getFieldNames()
List labels = Locator.tagWithClass("label", "control-label").withAttribute("for")
.waitForElements(elementCache(), 2_000);
- return labels.stream().map(a -> EscapeUtil.fieldKeyDecodePart(a.getDomAttribute("for"))).toList();
+ return labels.stream().map(a -> FieldKey.fromFieldKey(a.getDomAttribute("for")).getName()).toList();
}
public EntityBulkUpdateDialog waitForFieldsToBe(List expectedFieldNames, int waitMilliseconds)
@@ -321,50 +423,50 @@ protected ElementCache elementCache()
protected class ElementCache extends ModalDialog.ElementCache
{
- public WebElement formRow(String fieldKey)
+ public WebElement formRow(CharSequence fieldIdentifier)
{
+ String fieldKey = FieldKey.fromName(fieldIdentifier).toString();
return Locator.tagWithClass("div", "row")
.withChild(Locator.tagWithAttribute("label", "for", fieldKey))
.waitForElement(this, WAIT_TIMEOUT);
}
- public ToggleButton getToggle(String fieldKey)
+ public ToggleButton getToggle(CharSequence fieldIdentifier)
{
- return new ToggleButton.ToggleButtonFinder(getDriver()).waitFor(formRow(fieldKey));
+ return new ToggleButton.ToggleButtonFinder(getDriver()).waitFor(formRow(fieldIdentifier));
}
- public FilteringReactSelect getSelect(String fieldKey)
+ public FilteringReactSelect getSelect(CharSequence fieldIdentifier)
{
- return FilteringReactSelect.finder(getDriver()).withNamedInput(fieldKey).refindWhenNeeded(this);
+ return new FilteringReactSelect(formRow(fieldIdentifier), getDriver());
}
- public Input textInput(String fieldKey)
+ public Input textInput(CharSequence fieldIdentifier)
{
- WebElement inputEl = textInputLoc.waitForElement(formRow(fieldKey), WAIT_TIMEOUT);
+ WebElement inputEl = textInputLoc.waitForElement(formRow(fieldIdentifier), WAIT_TIMEOUT);
return new Input(inputEl, getDriver());
}
- public Input textArea(String fieldKey)
+ public Input textArea(CharSequence fieldIdentifier)
{
- WebElement inputEl = Locator.textarea(fieldKey).waitForElement(formRow(fieldKey), WAIT_TIMEOUT);
+ WebElement inputEl = Locator.tag("textarea").waitForElement(formRow(fieldIdentifier), WAIT_TIMEOUT);
return new Input(inputEl, getDriver());
}
- public Input numericInput(String fieldKey)
+ public Input numericInput(CharSequence fieldIdentifier)
{
- WebElement inputEl = numberInputLoc.waitForElement(formRow(fieldKey), WAIT_TIMEOUT);
+ WebElement inputEl = numberInputLoc.waitForElement(formRow(fieldIdentifier), WAIT_TIMEOUT);
return new Input(inputEl, getDriver());
}
- public ReactDateTimePicker dateInput(String fieldKey)
+ public ReactDateTimePicker dateInput(CharSequence fieldIdentifier)
{
- return new ReactDateTimePicker.ReactDateTimeInputFinder(getDriver())
- .withInputId(fieldKey).waitFor(formRow(fieldKey));
+ return new ReactDateTimePicker.ReactDateTimeInputFinder(getDriver()).waitFor(formRow(fieldIdentifier));
}
- public FileAttachmentContainer fileUploadField(String fieldKey)
+ public FileAttachmentContainer fileUploadField(CharSequence fieldIdentifier)
{
- return new FileAttachmentContainer(formRow(fieldKey), getDriver());
+ return new FileAttachmentContainer(formRow(fieldIdentifier), getDriver());
}
final Locator textInputLoc = Locator.tagWithAttribute("input", "type", "text");
diff --git a/src/org/labkey/test/components/ui/entities/EntityInsertPanel.java b/src/org/labkey/test/components/ui/entities/EntityInsertPanel.java
index 4852013037..cb80a53b0b 100644
--- a/src/org/labkey/test/components/ui/entities/EntityInsertPanel.java
+++ b/src/org/labkey/test/components/ui/entities/EntityInsertPanel.java
@@ -66,16 +66,20 @@ public boolean isAddParentMenuEnabled()
return Locator.buttonContainingText("Add Parent").findElement(this).isEnabled();
}
+ /**
+ * Get 'Add Parent' menu for passive inspection. Use {@link EntityInsertPanel#addParent(String)} to actually add
+ * parents to the entity
+ */
public MultiMenu getAddParentMenu()
{
- return elementCache().addParent;
+ return new ReadOnlyMenu(elementCache().addParent, "Parent");
}
public EntityInsertPanel addParent(String parentType)
{
Assert.assertTrue("Add Parent menu not present", isAddParentMenuPresent());
getWrapper().shortWait().until(ExpectedConditions.elementToBeClickable(elementCache().addParent.getComponentElement()));
- getAddParentMenu().doMenuAction(parentType);
+ getEditableGrid().doAndWaitForColumnUpdate(() -> elementCache().addParent.doMenuAction(parentType));
return this;
}
@@ -89,23 +93,31 @@ public boolean isAddSourceMenuEnabled()
return Locator.buttonContainingText("Add Source").findElement(this).isEnabled();
}
+ /**
+ * Get 'Add Source' menu for passive inspection. Use {@link EntityInsertPanel#addSource(String)} to actually add
+ * sources to the entity
+ */
public MultiMenu getAddSourceMenu()
{
- return elementCache().addSource;
+ return new ReadOnlyMenu(elementCache().addSource, "Source");
}
public EntityInsertPanel addSource(String sourceType)
{
Assert.assertTrue("Add Source menu not present", isAddSourceMenuPresent());
getWrapper().shortWait().until(ExpectedConditions.elementToBeClickable(elementCache().addSource.getComponentElement()));
- getAddSourceMenu().doMenuAction(sourceType);
+ getEditableGrid().doAndWaitForColumnUpdate(() -> elementCache().addSource.doMenuAction(sourceType));
return this;
}
- public EntityInsertPanel removeColumn(String columnName)
+ /**
+ * @param columnIdentifier fieldKey, name, or label
+ * @return this component
+ */
+ public EntityInsertPanel removeColumn(CharSequence columnIdentifier)
{
showGrid();
- elementCache().grid.removeColumn(columnName);
+ elementCache().grid.removeColumn(columnIdentifier);
return this;
}
@@ -205,7 +217,7 @@ public List getColumnHeaders()
public List
*
* @param row Index of the row (0 based).
- * @param fieldLabel Label of the column to update.
+ * @param columnIdentifier fieldKey, name, or label of column
* @param value If the cell is a lookup, value should be List.of(value(s)). To use the date picker pass a 'Date', 'LocalDate', or 'LocalDateTime'
* @return cell WebElement
*/
- public WebElement setCellValue(int row, String fieldLabel, Object value)
+ public WebElement setCellValue(int row, CharSequence columnIdentifier, Object value)
{
- return setCellValue(row, fieldLabel, value, true, false);
+ return setCellValue(row, columnIdentifier, value, true, false);
}
/**
@@ -395,14 +441,14 @@ public WebElement setCellValue(int row, String fieldLabel, Object value)
*
*
* @param row Index of the row (0 based).
- * @param fieldLabel Label of the column to update.
+ * @param columnIdentifier fieldKey, name, or label of column
* @param value Optional value to type in to filter the options shown
* @param index The index of the option to select for the lookup
* @return cell WebElement
*/
- public WebElement setCellValueForLookup(int row, String fieldLabel, @Nullable String value, int index)
+ public WebElement setCellValueForLookup(int row, CharSequence columnIdentifier, @Nullable String value, int index)
{
- WebElement gridCell = selectCell(row, fieldLabel);
+ WebElement gridCell = selectCell(row, columnIdentifier);
ReactSelect lookupSelect = elementCache().lookupSelect(gridCell);
@@ -412,7 +458,7 @@ public WebElement setCellValueForLookup(int row, String fieldLabel, @Nullable St
List elements = lookupSelect.getOptionElements();
if (elements.size() < index)
- throw new NotFoundException("Could not select option at index " + index + " in lookup for " + fieldLabel + ". Only " + elements.size() + " options found.");
+ throw new NotFoundException("Could not select option at index " + index + " in lookup for " + columnIdentifier + ". Only " + elements.size() + " options found.");
elements.get(index).click();
return gridCell;
}
@@ -427,13 +473,13 @@ public WebElement setCellValueForLookup(int row, String fieldLabel, @Nullable St
*
*
* @param row Index of the row (0 based).
- * @param fieldLabel Label of the column to update.
+ * @param columnIdentifier fieldKey, name, or label of column
* @param value If the cell is a lookup, value should be List.of(value(s)). To use the date picker pass a 'Date', 'LocalDate', or 'LocalDateTime'
* @param checkContains Check to see if the value passed in is contained in the value shown in the grid after the edit.
* Will be true most of the time but can be false if the field has formatting that may alter the value passed in like date values.
* @return cell WebElement
*/
- public WebElement setCellValue(int row, String fieldLabel, Object value, boolean checkContains, boolean centerSelectedCell)
+ public WebElement setCellValue(int row, CharSequence columnIdentifier, Object value, boolean checkContains, boolean centerSelectedCell)
{
// Normalize date values
if (value instanceof Date date)
@@ -442,9 +488,9 @@ public WebElement setCellValue(int row, String fieldLabel, Object value, boolean
}
if (centerSelectedCell)
- ScrollUtils.scrollIntoView(getCell(row, fieldLabel), center, center);
+ ScrollUtils.scrollIntoView(getCell(row, columnIdentifier), center, center);
- WebElement gridCell = selectCell(row, fieldLabel);
+ WebElement gridCell = selectCell(row, columnIdentifier);
if (value instanceof List)
{
@@ -535,10 +581,9 @@ public void setEntityData(List >data, List
{
Map rowData = data.get(i);
for (FieldDefinition field : fields) {
- String key = field.getLabel() != null ? field.getLabel() : field.getName();
- Object value = rowData.get(key);
+ Object value = rowData.get(field.getEffectiveLabel());
if (value != null)
- setCellValue(i, key, value);
+ setCellValue(i, field.getName(), value);
}
}
}
@@ -548,8 +593,8 @@ public EditableGrid setRecordValues(List> rowValues)
for (int i = 0; i < rowValues.size(); i++)
{
Map columnValues = rowValues.get(i);
- for(String fieldLabel : columnValues.keySet())
- setCellValue(i, fieldLabel, columnValues.get(fieldLabel), true, true);
+ for(String fieldIdentifier : columnValues.keySet())
+ setCellValue(i, fieldIdentifier, columnValues.get(fieldIdentifier), true, true);
}
return this;
}
@@ -559,16 +604,16 @@ public EditableGrid setRecordValues(List> rowValues)
* Use '\n' for a new line.
*
* @param row Row to update.
- * @param fieldLabel Column label of the multi-line field.
+ * @param columnIdentifier fieldKey, name, or label of column
* @param value The value to set.
*/
- public void setMultiLineCellValue(int row, String fieldLabel, String value)
+ public void setMultiLineCellValue(int row, CharSequence columnIdentifier, String value)
{
- WebElement gridCell = getCell(row, fieldLabel);
+ WebElement gridCell = getCell(row, columnIdentifier);
String beforeText = gridCell.getText();
- WebElement textArea = activateCellUsingDoubleClick(row, fieldLabel);
+ WebElement textArea = activateCellUsingDoubleClick(row, columnIdentifier);
textArea.sendKeys(value, Keys.RETURN); // Add the RETURN to close the inputCell.
@@ -585,12 +630,12 @@ public void setMultiLineCellValue(int row, String fieldLabel, String value)
* Double-clicking a cell that is "text" value field will activate it and present a textArea for editing the value.
* This will return the textArea WebElement that can be used to set the field.
* @param row Row to be edited.
- * @param fieldLabel Column label of the field.
+ * @param columnIdentifier fieldKey, name, or label of column
* @return The TextArea component that can be used to edit the field.
*/
- public WebElement activateCellUsingDoubleClick(int row, String fieldLabel)
+ public WebElement activateCellUsingDoubleClick(int row, CharSequence columnIdentifier)
{
- WebElement gridCell = getCell(row, fieldLabel);
+ WebElement gridCell = getCell(row, columnIdentifier);
WebElement textArea = Locator.tag("textarea").refindWhenNeeded(gridCell);
// Account for the cell already being active.
@@ -598,7 +643,7 @@ public WebElement activateCellUsingDoubleClick(int row, String fieldLabel)
{
getWrapper().doubleClick(gridCell);
waitFor(textArea::isDisplayed,
- String.format("Table cell for row %d and column '%s' was not activated.", row, fieldLabel), 1_000);
+ String.format("Table cell for row %d and column '%s' was not activated.", row, columnIdentifier), 1_000);
}
return textArea;
}
@@ -606,12 +651,12 @@ public WebElement activateCellUsingDoubleClick(int row, String fieldLabel)
/**
* Creates a value in a select that allows the user to insert/create a value, vs. selecting from an existing/populated set
* @param row the row
- * @param fieldLabel label of the column
+ * @param columnIdentifier fieldKey, name, or label of column
* @param value value to insert
*/
- public void setNewSelectValue(int row, String fieldLabel, String value)
+ public void setNewSelectValue(int row, CharSequence columnIdentifier, String value)
{
- WebElement gridCell = selectCell(row, fieldLabel);
+ WebElement gridCell = selectCell(row, columnIdentifier);
ReactSelect createSelect = elementCache().lookupSelect(gridCell);
@@ -619,26 +664,26 @@ public void setNewSelectValue(int row, String fieldLabel, String value)
}
/**
- * Search for a row and then clear the given cell (fieldLabelToClear) on the row.
+ * Search for a row and then clear the given cell (columnToClear) on the row.
*
- * @param fieldLabelToSearch Column to search.
+ * @param columnToSearch Column to search.
* @param valueToSearch Value in the column to search for.
- * @param fieldLabelToClear Column to clear.
+ * @param columnToClear Column to clear.
*/
- public void clearCellValue(String fieldLabelToSearch, String valueToSearch, String fieldLabelToClear)
+ public void clearCellValue(CharSequence columnToSearch, String valueToSearch, CharSequence columnToClear)
{
- clearCellValue(getRowIndex(fieldLabelToSearch, valueToSearch), fieldLabelToClear);
+ clearCellValue(getRowIndex(columnToSearch, valueToSearch), columnToClear);
}
/**
- * Clear the cell (fieldLabel) in the row.
+ * Clear the cell (columnIdentifier) in the row.
*
* @param row Row of the cell to clear.
- * @param fieldLabel Column of the cell to clear.
+ * @param columnIdentifier fieldKey, name, or label of column
*/
- public void clearCellValue(int row, String fieldLabel)
+ public void clearCellValue(int row, CharSequence columnIdentifier)
{
- selectCell(row, fieldLabel);
+ selectCell(row, columnIdentifier);
new Actions(getDriver()).sendKeys(Keys.DELETE).perform();
}
@@ -646,12 +691,12 @@ public void clearCellValue(int row, String fieldLabel)
* For a given row get the value in the given column.
*
* @param row The row index (0 based).
- * @param fieldLabel The label of the column to get the value for.
+ * @param columnIdentifier fieldKey, name, or label of column
* @return The string value of the {@link WebElement} that is the cell.
*/
- public String getCellValue(int row, String fieldLabel)
+ public String getCellValue(int row, CharSequence columnIdentifier)
{
- return getCellValue(getCell(row, fieldLabel));
+ return getCellValue(getCell(row, columnIdentifier));
}
private String getCellValue(WebElement cell)
@@ -675,12 +720,12 @@ public EditableGrid dismissDropdownList()
* For the given row get the values displayed in the dropdown list for the given column.
*
* @param row The 0 based row index.
- * @param fieldLabel The label of the column.
+ * @param columnIdentifier fieldKey, name, or label of column
* @return A list of strings from the dropdown list. If the cell does not have a dropdown then an empty list is returned.
*/
- public List getDropdownListForCell(int row, String fieldLabel)
+ public List getDropdownListForCell(int row, CharSequence columnIdentifier)
{
- return getFilteredDropdownListForCell(row, fieldLabel, null);
+ return getFilteredDropdownListForCell(row, columnIdentifier, null);
}
/**
@@ -688,14 +733,14 @@ public List getDropdownListForCell(int row, String fieldLabel)
* If this cell is not a lookup cell, does not have a dropdown, the text will not be entered and an empty list will be returned.
*
* @param row A 0 based index containing the cell.
- * @param fieldLabel The column of the cell.
+ * @param columnIdentifier fieldKey, name, or label of column
* @param filterText The text to type into the cell. If the value is null it will not filter the list.
* @return A list values shown in the dropdown list after the text has been entered.
*/
- public List getFilteredDropdownListForCell(int row, String fieldLabel, @Nullable String filterText)
+ public List getFilteredDropdownListForCell(int row, CharSequence columnIdentifier, @Nullable String filterText)
{
- WebElement gridCell = selectCell(row, fieldLabel);
+ WebElement gridCell = selectCell(row, columnIdentifier);
ReactSelect lookupSelect = elementCache().lookupSelect(gridCell);
@@ -715,28 +760,28 @@ public List getFilteredDropdownListForCell(int row, String fieldLabel, @
* Pastes delimited text to the grid, via a single target. The component is clever enough to target
* text into cells based on text delimiters; thus we can paste a square of data into the grid.
* @param row index of the target cell
- * @param fieldLabel column of the target cell
+ * @param columnIdentifier fieldKey, name, or label of column
* @param pasteText tab-delimited or csv or excel data
* @return A Reference to this editableGrid object.
*/
- public EditableGrid pasteFromCell(int row, String fieldLabel, String pasteText)
+ public EditableGrid pasteFromCell(int row, CharSequence columnIdentifier, String pasteText)
{
- return pasteFromCell(row, fieldLabel, pasteText, false);
+ return pasteFromCell(row, columnIdentifier, pasteText, false);
}
/**
* Pastes delimited text to the grid, via a single target. The component is clever enough to target
* text into cells based on text delimiters; thus we can paste a square of data into the grid.
* @param row index of the target cell
- * @param fieldLabel column of the target cell
+ * @param columnIdentifier fieldKey, name, or label of column
* @param pasteText tab-delimited or csv or excel data
* @param validate whether to await/confirm the presence of pasted text before resuming
* @return A Reference to this editableGrid object.
*/
- public EditableGrid pasteFromCell(int row, String fieldLabel, String pasteText, boolean validate)
+ public EditableGrid pasteFromCell(int row, CharSequence columnIdentifier, String pasteText, boolean validate)
{
int initialRowCount = getRowCount();
- WebElement gridCell = getCell(row, fieldLabel);
+ WebElement gridCell = getCell(row, columnIdentifier);
String indexValue = gridCell.getText();
selectCell(gridCell);
@@ -775,17 +820,16 @@ protected void waitForAnyPasteContent(String pasteContent)
public void waitForPasteContent(String pasteContent)
{
// split pasteContent into its parts
- var contentParts = pasteContent.replace("\n", "\t").split("\t");
+ var contentParts = pasteContent.split("\\s*[\n\t]\\s*");
// filter out empty and space-only values
var filteredParts = Arrays.stream(contentParts)
- .filter(a-> !a.isEmpty() && !a.equals(" "))
+ .filter(a-> !a.isBlank())
.map(str -> {
if (str.startsWith("\"") && str.endsWith("\""))
{
// reverse TsvQuoter.quote
str = str.replaceAll("\"\"", "\"");
- str = str.substring(1);
- str = str.substring(0, str.length() - 1);
+ str = str.substring(1, str.length() - 1); // remove surrounding quotes
}
return str;
})
@@ -815,12 +859,12 @@ public List getSelectedCells()
* the grid should produce an error/alert.
* @param pasteText The text to paste
* @param startRowIndex index of the starting row
- * @param startColumn text of the starting cell
+ * @param startColumn fieldKey, name, or label of the starting cell
* @param endRowIndex index of the ending row
- * @param endColumn text of the ending cell
+ * @param endColumn fieldKey, name, or label of the ending cell
* @return the current grid instance
*/
- public EditableGrid pasteMultipleCells(String pasteText, int startRowIndex, String startColumn, int endRowIndex, String endColumn)
+ public EditableGrid pasteMultipleCells(String pasteText, int startRowIndex, CharSequence startColumn, int endRowIndex, CharSequence endColumn)
{
WebElement startCell = getCell(startRowIndex, startColumn);
WebElement endCell = getCell(endRowIndex, endColumn);
@@ -832,12 +876,12 @@ public EditableGrid pasteMultipleCells(String pasteText, int startRowIndex, Stri
/**
* Copies text from the grid, b
* @param startRowIndex Index of the top-left cell's row
- * @param startColumn Column label of the top-left cell
+ * @param startColumn fieldKey, name, or label of the top-left cell
* @param endRowIndex Index of the bottom-right cell's row
- * @param endColumn Column label of the bottom-right cell
+ * @param endColumn fieldKey, name, or label of the bottom-right cell
* @return the text contained in the prescribed selection
*/
- public String copyCellRange(int startRowIndex, String startColumn, int endRowIndex, String endColumn) throws IOException, UnsupportedFlavorException
+ public String copyCellRange(int startRowIndex, CharSequence startColumn, int endRowIndex, CharSequence endColumn) throws IOException, UnsupportedFlavorException
{
WebElement startCell = getCell(startRowIndex, startColumn);
WebElement endCell = getCell(endRowIndex, endColumn);
@@ -935,10 +979,10 @@ private void selectAllCells()
"the expected cells did not become selected", 3000);
}
- public WebElement selectCell(int row, String fieldLabel)
+ public WebElement selectCell(int row, CharSequence columnIdentifier)
{
// Get a reference to the cell.
- WebElement gridCell = getCell(row, fieldLabel);
+ WebElement gridCell = getCell(row, columnIdentifier);
// Select the cell.
selectCell(gridCell);
@@ -992,14 +1036,14 @@ public boolean isCellSelected(WebElement cell)
// div will not have the cell-selected in the class attribute.
return Locator.tagWithClass("div", "cellular-display")
.findElement(cell)
- .getAttribute("class").contains("cell-selected");
+ .getDomAttribute("class").contains("cell-selected");
}
catch (NoSuchElementException nse)
{
// If the cell is an open/active reactSelect the class attribute is different.
return Locator.tagWithClass("div", "select-input__control")
.findElement(cell)
- .getAttribute("class").contains("select-input__control--is-focused");
+ .getDomAttribute("class").contains("select-input__control--is-focused");
}
}
@@ -1013,7 +1057,7 @@ private boolean isInSelection(WebElement cell) // 'in selection' shows as blue
// Should not need to add code for a reactSelect here. A selection involves clicking/dragging, which closes the reactSelect.
return Locator.tagWithClass("div", "cellular-display")
.findElement(cell)
- .getAttribute("class").contains("cell-selection");
+ .getDomAttribute("class").contains("cell-selection");
}
/**
@@ -1030,9 +1074,9 @@ private boolean areAllInSelection()
return (isInSelection(indexCell) && isInSelection(endCell));
}
- public boolean hasCellError(int row, String fieldLabel)
+ public boolean hasCellError(int row, CharSequence columnIdentifier)
{
- WebElement gridCell = getCell(row, fieldLabel);
+ WebElement gridCell = getCell(row, columnIdentifier);
return cellHasError(gridCell);
}
@@ -1041,18 +1085,28 @@ private boolean cellHasError(WebElement cell)
return Locator.tagWithClass("div", "cell-error").existsIn(cell);
}
- public String getCellError(int row, String fieldLabel)
+ /**
+ * @param row row index
+ * @param columnIdentifier fieldKey, name, or label of column
+ * @return error text in the specified cell or 'null' if there is no error
+ */
+ public String getCellError(int row, CharSequence columnIdentifier)
{
- WebElement gridCell = getCell(row, fieldLabel);
+ WebElement gridCell = getCell(row, columnIdentifier);
if (cellHasError(gridCell))
return Locator.tagWithClass("div", "cell-error").findElement(gridCell).getText();
return null;
}
- public String getCellPopoverText(int row, String fieldLabel)
+ /**
+ * @param row row index
+ * @param columnIdentifier fieldKey, name, or label of column
+ * @return popover text when mousing over the specified cell or 'null' if there is none
+ */
+ public String getCellPopoverText(int row, CharSequence columnIdentifier)
{
- WebElement cellDiv = Locator.tagWithClass("div", "cellular-display").findElement(getCell(row, fieldLabel));
+ WebElement cellDiv = Locator.tagWithClass("div", "cellular-display").findElement(getCell(row, columnIdentifier));
getWrapper().mouseOver(cellDiv); // cause the tooltip to be present
if (WebDriverWrapper.waitFor(()-> null != Locator.byClass("popover").findElementOrNull(getDriver()), 1000))
{
@@ -1086,12 +1140,12 @@ public void setAddRows(int count)
public void addRows(int count)
{
setAddRows(count);
- doAndWaitForUpdate(() -> {
+ doAndWaitForRowCountUpdate(() -> {
elementCache().addRowsButton.click();
});
}
- private void doAndWaitForUpdate(Runnable func)
+ private void doAndWaitForRowCountUpdate(Runnable func)
{
int initialCount = getRowCount();
@@ -1100,6 +1154,21 @@ private void doAndWaitForUpdate(Runnable func)
waitFor(() -> getRowCount() != initialCount, "Failed to add/remove rows", 5_000);
}
+ /**
+ * Wait for column count to change after the provided action
+ */
+ public void doAndWaitForColumnUpdate(Runnable func)
+ {
+ int initialCount = elementCache().findHeaders().size();
+
+ func.run();
+
+ waitFor(() -> {
+ clearElementCache();
+ return elementCache().findHeaders().size() != initialCount;
+ }, "Failed to add/remove column", 5_000);
+ }
+
@Override
protected ElementCache newElementCache()
{
@@ -1116,73 +1185,54 @@ protected class ElementCache extends Component>.ElementCache
final WebElement table = Locator.byClass("table-cellular").findWhenNeeded(this);
private final WebElement selectColumn = Locator.xpath("//th/input[@type='checkbox']").findWhenNeeded(table);
- public WebElement getGridCellHeader(String label)
+ protected WebElement getColumnHeaderCell(CharSequence columnIdentifier)
{
- return Locator.byClass("grid-header-cell").withDescendant(Locator.tagContainingText("span", label)).findWhenNeeded(table);
+ return findColumnHeader(columnIdentifier).getElement();
}
- private List fieldLabels = new ArrayList<>();
-
- public List getColumnLabels()
+ private FieldReferenceManager _fieldReferenceManager;
+ protected FieldReferenceManager getGridHeaderManager()
{
- // If the number of header cells is not equal to the list of fieldLabels the columns have been modified since
- // the last call to getColumnLabels so get the column labels again.
- List headerCells = Locators.headerCells.waitForElements(table, WAIT_FOR_JAVASCRIPT);
- if (fieldLabels.size() != headerCells.size())
+ if (_fieldReferenceManager == null)
{
- fieldLabels = new ArrayList<>();
- }
+ List columnHeaders = new ArrayList<>();
+ List headerCellElements = Locators.headerCells.waitForElements(table, WAIT_FOR_JAVASCRIPT);
+ int domIndex = 0;
- if (fieldLabels.isEmpty())
- {
- for (WebElement el : headerCells)
- {
- fieldLabels.add(getLabelFromHeaderCell(el));
- }
-
- int rowNumberColumn = 0;
if (hasSelectColumn())
{
- fieldLabels.set(0, SELECT_COLUMN_HEADER);
- rowNumberColumn = 1;
+ columnHeaders.add(new EditableGridColumnHeader(headerCellElements.get(0), domIndex, SELECT_COLUMN_LABEL_PLACEHOLDER));
+ domIndex++;
}
- if (fieldLabels.get(rowNumberColumn).trim().isEmpty())
+ for (; domIndex < headerCellElements.size(); domIndex++)
{
- fieldLabels.set(rowNumberColumn, ROW_NUMBER_COLUMN_HEADER);
+ columnHeaders.add(new EditableGridColumnHeader(headerCellElements.get(domIndex), domIndex));
}
+
+ _fieldReferenceManager = new FieldReferenceManager(columnHeaders);
}
+ return _fieldReferenceManager;
+ }
- return fieldLabels;
+ protected List findHeaders()
+ {
+ return getGridHeaderManager().getColumnHeaders();
}
- /**
- * Extract label from header cell. Editable grid header cells have several different layouts. What they have in
- * common is that the label is the first text node in the cell, possibly within a <span>
- */
- private String getLabelFromHeaderCell(WebElement el)
+ protected FieldReference findColumnHeader(CharSequence columnIdentifier)
{
- // Use text nodes to ignore browser whitespace formatting
- List textNodes = WebElementUtils.getTextNodesWithin(el);
- if (textNodes.isEmpty())
- {
- List children = Locator.xpath("./*").findElements(el);
- if (children.isEmpty())
- {
- return ""; // probably the selection checkbox column
- }
- else
- {
- // Depth-first search until we find some text
- return getLabelFromHeaderCell(children.get(0));
- }
- }
- else
- {
- boolean required = Locator.byClass("required-symbol").existsIn(el);
- String label = textNodes.get(0).trim(); // trim trailing NBSP
- return label + (required ? " *" : ""); // re-add required asterisk for tests that expect it
- }
+ return getGridHeaderManager().findFieldReference(columnIdentifier);
+ }
+
+ protected int getColumnIndex(CharSequence columnIdentifier)
+ {
+ return findColumnHeader(columnIdentifier).getDomIndex();
+ }
+
+ protected List getColumnLabels()
+ {
+ return findHeaders().stream().map(FieldReference::getLabel).collect(Collectors.toList());
}
public WebElement inputCell()
@@ -1244,4 +1294,60 @@ protected Locator locator()
return _locator;
}
}
+
+ protected static class EditableGridColumnHeader extends FieldReferenceManager.FieldReference
+ {
+ private final Mutable _fieldLabel = new MutableObject<>();
+
+ public EditableGridColumnHeader(WebElement element, int domIndex)
+ {
+ super(element, domIndex);
+ }
+
+ public EditableGridColumnHeader(WebElement element, int domIndex, String label)
+ {
+ this(element, domIndex);
+ _fieldLabel.setValue(label);
+ }
+
+ @Override
+ public String getLabel()
+ {
+ if (_fieldLabel.getValue() == null)
+ {
+ _fieldLabel.setValue(getLabelFromHeaderCell(getElement()).trim());
+ }
+ return _fieldLabel.getValue();
+ }
+
+
+ /**
+ * Extract label from header cell. Editable grid header cells have several different layouts. What they have in
+ * common is that the label is the first text node in the cell, possibly within a <span>
+ */
+ private String getLabelFromHeaderCell(WebElement el)
+ {
+ // Use text nodes to ignore browser whitespace formatting
+ List textNodes = WebElementUtils.getTextNodesWithin(el);
+ if (textNodes.isEmpty())
+ {
+ List children = Locator.xpath("./*").findElements(el);
+ if (children.isEmpty())
+ {
+ return ""; // probably the selection checkbox column
+ }
+ else
+ {
+ // Depth-first search until we find some text
+ return getLabelFromHeaderCell(children.get(0));
+ }
+ }
+ else
+ {
+ boolean required = Locator.byClass("required-symbol").existsIn(el);
+ String label = textNodes.get(0).trim(); // trim trailing NBSP
+ return label + (required ? " *" : ""); // re-add required asterisk for tests that expect it
+ }
+ }
+ }
}
diff --git a/src/org/labkey/test/components/ui/grids/FieldReferenceManager.java b/src/org/labkey/test/components/ui/grids/FieldReferenceManager.java
new file mode 100644
index 0000000000..b8845cf93b
--- /dev/null
+++ b/src/org/labkey/test/components/ui/grids/FieldReferenceManager.java
@@ -0,0 +1,176 @@
+package org.labkey.test.components.ui.grids;
+
+import org.apache.commons.lang3.mutable.Mutable;
+import org.apache.commons.lang3.mutable.MutableObject;
+import org.labkey.test.params.FieldKey;
+import org.labkey.test.util.selenium.WebElementUtils;
+import org.openqa.selenium.NoSuchElementException;
+import org.openqa.selenium.WebElement;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Supplier;
+
+public class FieldReferenceManager
+{
+ private final List _fieldReferences;
+ private final Map fieldKeys = new LinkedHashMap<>();
+ private final Map fieldLabels = new LinkedHashMap<>();
+
+ public FieldReferenceManager(List columnHeaders)
+ {
+ this._fieldReferences = Collections.unmodifiableList(columnHeaders);
+ }
+
+ public List getColumnHeaders()
+ {
+ return _fieldReferences;
+ }
+
+ /**
+ * Find field by uncertain field identifier in order of precedence:
+ *
+ *
FieldKey object
+ *
Encoded fieldKey
+ *
Unencoded fieldKey
+ *
Field Label
+ *
+ */
+ public final FieldReference findFieldReference(CharSequence fieldIdentifier)
+ {
+ List> options;
+
+ if (fieldIdentifier instanceof FieldKey fk)
+ {
+ options = List.of(() -> findColumnHeaderByFieldKey(fk)); // We know it is a FieldKey
+ }
+ else
+ {
+ options = List.of(
+ () -> findColumnHeaderByFieldKey(FieldKey.fromFieldKey(fieldIdentifier)), // encoded fieldKey
+ () -> findColumnHeaderByFieldKey(FieldKey.fromName(fieldIdentifier)), // unencoded fieldKey
+ () -> findColumnHeaderByLabel(fieldIdentifier.toString()) // Field label
+ );
+ }
+
+ return options.stream()
+ .map(Supplier::get)
+ .filter(Objects::nonNull)
+ .findFirst()
+ .orElseThrow(() -> new NoSuchElementException("Unable to locate field: " + fieldIdentifier));
+ }
+
+ private FieldReference findColumnHeaderByFieldKey(FieldKey fieldIdentifier)
+ {
+ if (fieldKeys.containsKey(fieldIdentifier))
+ {
+ return fieldKeys.get(fieldIdentifier);
+ }
+ else if (fieldKeys.size() < _fieldReferences.size())
+ {
+ for (FieldReference header : _fieldReferences)
+ {
+ if (!fieldKeys.containsValue(header))
+ {
+ FieldKey fieldKey = header.getFieldKey();
+ fieldKeys.put(fieldKey, header);
+ if (fieldKey.equals(fieldIdentifier))
+ {
+ return header;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private FieldReference findColumnHeaderByLabel(String label)
+ {
+ if (fieldLabels.containsKey(label))
+ {
+ return fieldLabels.get(label);
+ }
+ else if (fieldLabels.size() < _fieldReferences.size())
+ {
+ for (FieldReference header : _fieldReferences)
+ {
+ if (!fieldLabels.containsValue(header))
+ {
+ String columnLabel = header.getLabel();
+ fieldLabels.put(columnLabel, header);
+ if (columnLabel.equals(label))
+ {
+ return header;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public static class FieldReference
+ {
+ private final WebElement _element;
+ private final int _domIndex;
+ private final Mutable _fieldLabel = new MutableObject<>();
+ private final Mutable _fieldKey = new MutableObject<>();
+
+ public FieldReference(WebElement element, int domIndex)
+ {
+ _element = element;
+ _domIndex = domIndex;
+ }
+
+ public WebElement getElement()
+ {
+ return _element;
+ }
+
+ public FieldKey getFieldKey()
+ {
+ if (_fieldKey.getValue() == null)
+ {
+ String path = _element.getDomAttribute("data-fieldkey");
+ if (path == null)
+ {
+ // Some grids don't have a field key, but have a similar value in the ID attribute
+ path = _element.getDomAttribute("id");
+ }
+
+ if (path != null)
+ {
+ _fieldKey.setValue(FieldKey.fromFieldKey(path));
+ }
+ else
+ {
+ _fieldKey.setValue(FieldKey.EMPTY);
+ }
+ }
+ return _fieldKey.getValue();
+ }
+
+ public String getLabel()
+ {
+ if (_fieldLabel.getValue() == null)
+ {
+ _fieldLabel.setValue(WebElementUtils.getTextContent(getElement()).trim());
+ }
+ return _fieldLabel.getValue();
+ }
+
+ public String getName()
+ {
+ return getFieldKey().getName();
+ }
+
+ public int getDomIndex()
+ {
+ return _domIndex;
+ }
+ }
+}
diff --git a/src/org/labkey/test/components/ui/grids/FieldSelectionDialog.java b/src/org/labkey/test/components/ui/grids/FieldSelectionDialog.java
index 115fdf49df..c7c01bbcae 100644
--- a/src/org/labkey/test/components/ui/grids/FieldSelectionDialog.java
+++ b/src/org/labkey/test/components/ui/grids/FieldSelectionDialog.java
@@ -7,9 +7,10 @@
import org.labkey.test.components.UpdatingComponent;
import org.labkey.test.components.bootstrap.ModalDialog;
import org.labkey.test.components.html.Checkbox;
-import org.labkey.test.util.EscapeUtil;
+import org.labkey.test.params.FieldKey;
import org.labkey.test.util.selenium.WebElementUtils;
import org.openqa.selenium.Keys;
+import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.interactions.Actions;
@@ -17,7 +18,6 @@
import org.openqa.selenium.support.ui.WebDriverWait;
import java.time.Duration;
-import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;
@@ -103,13 +103,31 @@ public List getAvailableFieldLabels()
*/
public boolean isAvailableFieldSelected(String... fieldNameParts)
{
- WebElement listItem = elementCache().getListItemElementByFieldKey(expandAvailableFields(fieldNameParts));
+ return isAvailableFieldSelected(FieldKey.fromParts(fieldNameParts));
+ }
+
+ public boolean isAvailableFieldSelected(FieldKey fieldKey)
+ {
+ WebElement listItem = getAvailableFieldElement(fieldKey);
return Locator.tagWithClass("i", "fa-check").findWhenNeeded(listItem).isDisplayed();
}
public boolean isFieldAvailable(String... fieldNameParts)
{
- return elementCache().getListItemElementByFieldKeyOrNull(expandAvailableFields(fieldNameParts)) != null;
+ return isFieldAvailable(FieldKey.fromParts(fieldNameParts));
+ }
+
+ public boolean isFieldAvailable(FieldKey fieldKey)
+ {
+ try
+ {
+ getAvailableFieldElement(fieldKey);
+ return true;
+ }
+ catch (NoSuchElementException e)
+ {
+ return false;
+ }
}
/**
@@ -120,27 +138,15 @@ public boolean isFieldAvailable(String... fieldNameParts)
*/
public FieldSelectionDialog selectAvailableField(String... fieldNameParts)
{
- return addFieldByFieldKeyToGrid(expandAvailableFields(fieldNameParts));
+ return selectAvailableField(FieldKey.fromParts(fieldNameParts));
}
- public WebElement getAvailableFieldElement(String fieldName)
+ public FieldSelectionDialog selectAvailableField(FieldKey fieldKey)
{
- String fieldKey = expandAvailableFields(fieldName);
- return elementCache().getListItemElementByFieldKey(fieldKey);
- }
-
- /**
- * Private helper to add a field to the 'Shown in Grid' list. Use the data-fieldkey value to identify the item.
- *
- * @param fieldKey The value in the data-fieldkay attribute for the row.
- * @return This dialog.
- */
- private FieldSelectionDialog addFieldByFieldKeyToGrid(String fieldKey)
- {
- WebElement listItem = elementCache().getListItemElementByFieldKey(fieldKey);
+ WebElement listItem = getAvailableFieldElement(fieldKey);
Assert.assertTrue(String.format(FIELD_NOT_AVAILABLE, fieldKey),
- listItem.isDisplayed());
+ listItem.isDisplayed());
WebElement addIcon = Locator.tagWithClass("div", "view-field__action")
.withChild(Locator.tagWithClass("i", "fa-plus"))
@@ -151,36 +157,36 @@ private FieldSelectionDialog addFieldByFieldKeyToGrid(String fieldKey)
return this;
}
+ public WebElement getAvailableFieldElement(String... fieldNameParts)
+ {
+ return getAvailableFieldElement(FieldKey.fromParts(fieldNameParts));
+ }
+
/**
- * Expand a field or a hierarchy of fieldKeyParts. If a single field is passed in only it will be expanded. If multiple values
- * are passed in it is assumed to be a path and all fieldKeyParts will be expanded to the last field.
+ * Expand available field tree to the specified field
*
- * @param fieldNameParts The list of fieldKeyParts to expand.
- * @return key for the expanded field.
+ * @param fieldKey FieldKey for the target field
+ * @return row element for the specified field
*/
- private String expandAvailableFields(String... fieldNameParts)
+ public WebElement getAvailableFieldElement(FieldKey fieldKey)
{
- StringBuilder fieldKey = new StringBuilder();
-
- Iterator iterator = Arrays.stream(fieldNameParts).iterator();
+ Iterator iterator = fieldKey.getIterator();
while(iterator.hasNext())
{
- fieldKey.append(EscapeUtil.fieldKeyEncodePart(iterator.next().trim()));
+ fieldKey = iterator.next();
// If this isn't the last item in the collection keep expanding and building the expected data-fieldkey value.
if(iterator.hasNext())
{
// If the field is already expanded don't try to expand it.
- if(!isFieldKeyExpanded(elementCache().getListItemElementByFieldKey(fieldKey.toString())))
+ if(!isFieldKeyExpanded(elementCache().findAvailableField(fieldKey.toString())))
expandOrCollapseByFieldKey(fieldKey.toString(), true);
-
- fieldKey.append("/");
}
}
- return fieldKey.toString();
+ return elementCache().findAvailableField(fieldKey.toString());
}
/**
@@ -192,7 +198,7 @@ private String expandAvailableFields(String... fieldNameParts)
private void expandOrCollapseByFieldKey(String fieldKey, boolean expand)
{
- WebElement listItem = elementCache().getListItemElementByFieldKey(fieldKey);
+ WebElement listItem = elementCache().findAvailableField(fieldKey);
// Check to see if row is already in the desired state. If so don't do anything.
if((expand && isFieldKeyExpanded(listItem) || (!expand && !isFieldKeyExpanded(listItem))))
@@ -409,33 +415,30 @@ public FieldSelectionDialog removeAllSelectedFields()
/**
* Update the given field label to a new value.
*
- * @param currentFieldLabel The field to be updated.
+ * @param fieldName The field to be updated.
* @param newFieldLabel The new value to set the label to.
* @return This dialog.
*/
- public FieldSelectionDialog setFieldLabel(String currentFieldLabel, String newFieldLabel)
+ public FieldSelectionDialog setFieldLabel(String fieldName, String newFieldLabel)
{
- return setFieldLabel(currentFieldLabel, 0, newFieldLabel);
+ return setFieldLabel(FieldKey.fromParts(fieldName), newFieldLabel);
}
/**
- * Update the given field to a new label. If there are multiple fields with the same label in the list the index
- * parameter identifies which one to update.
+ * Update the given field to a new label.
*
- * @param currentFieldLabel The field to be updated.
- * @param index If multiple fields have the save label this identifies which one in the list to update.
+ * @param fieldKey The field to be updated.
* @param newFieldLabel The new value to set the label to.
* @return This dialog.
*/
- public FieldSelectionDialog setFieldLabel(String currentFieldLabel, int index, String newFieldLabel)
+ public FieldSelectionDialog setFieldLabel(FieldKey fieldKey, String newFieldLabel)
{
-
- WebElement listItem = getSelectedListItems(currentFieldLabel).get(index);
+ WebElement listItem = elementCache().findSelectedField(fieldKey.toString());
WebElement updateIcon = Locator.tagWithClass("span", "edit-inline-field__toggle").findWhenNeeded(listItem);
updateIcon.click();
WebDriverWrapper.waitFor(()->elementCache().fieldLabelEdit.isDisplayed(),
- String.format("Input for field '%s' was not shown.", currentFieldLabel), 1_500);
+ String.format("Input for field '%s' was not shown.", fieldKey), 1_500);
// Unfortunately using setFormElement doesn't work in this case. That method calls WebElement.clear which clears
// the current text but also causes the focus to the input control to be lost. When the focus is lost the input
@@ -448,6 +451,8 @@ public FieldSelectionDialog setFieldLabel(String currentFieldLabel, int index, S
.sendKeys(Keys.TAB)
.perform();
+ getWrapper().mouseOver(elementCache().title); // Dismiss tooltip
+
WebDriverWrapper.waitFor(()->!elementCache().fieldLabelEdit.isDisplayed() &&
elementCache().getListItemElement(elementCache().selectedFieldsPanel, newFieldLabel).isDisplayed(),
String.format("New field label '%s' is not in the list.", newFieldLabel), 500);
@@ -636,20 +641,21 @@ protected WebElement getListItemElement(WebElement panel, String fieldLabel)
.findElement(panel);
}
- // The data-fieldkey attribute is only present in items in the Available Fields panel.
- // Similar value to field-name (no spaces, but casing is the same). For child fields it will contain the parent path.
- protected WebElement getListItemElementByFieldKey(String fieldKey)
+ protected WebElement findSelectedField(String fieldKey)
{
- return Locator.tagWithClass("div", "list-group-item")
- .withAttributeIgnoreCase("data-fieldkey", fieldKey)
- .findElement(availableFieldsPanel);
+ return findFieldRow(fieldKey, selectedFieldsPanel);
+ }
+
+ protected WebElement findAvailableField(String fieldKey)
+ {
+ return findFieldRow(fieldKey, availableFieldsPanel);
}
- protected WebElement getListItemElementByFieldKeyOrNull(String fieldKey)
+ protected WebElement findFieldRow(String fieldKey, WebElement panel)
{
return Locator.tagWithClass("div", "list-group-item")
- .withAttributeIgnoreCase("data-fieldkey", fieldKey)
- .findElementOrNull(availableFieldsPanel);
+ .withAttributeIgnoreCase("data-fieldkey", fieldKey)
+ .findElement(panel);
}
// Get the displayed names/labels of list items in the given panel.
diff --git a/src/org/labkey/test/components/ui/grids/GridRow.java b/src/org/labkey/test/components/ui/grids/GridRow.java
index f2218323cd..932b3bf1bf 100644
--- a/src/org/labkey/test/components/ui/grids/GridRow.java
+++ b/src/org/labkey/test/components/ui/grids/GridRow.java
@@ -1,6 +1,5 @@
package org.labkey.test.components.ui.grids;
-import org.labkey.api.collections.CaseInsensitiveHashMap;
import org.labkey.test.Locator;
import org.labkey.test.WebDriverWrapper;
import org.labkey.test.components.Component;
@@ -8,24 +7,28 @@
import org.labkey.test.components.react.ReactCheckBox;
import org.labkey.test.components.ui.files.AttachmentCard;
import org.labkey.test.components.ui.files.ImageFileViewDialog;
+import org.labkey.test.components.ui.grids.FieldReferenceManager.FieldReference;
+import org.labkey.test.params.FieldKey;
import org.labkey.test.util.LogMethod;
import org.labkey.test.util.LoggedParam;
+import org.labkey.test.util.TestLogger;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedConditions;
import java.io.File;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.function.Function;
import static org.junit.Assert.assertTrue;
import static org.labkey.test.WebDriverWrapper.WAIT_FOR_JAVASCRIPT;
public class GridRow extends WebDriverComponent
{
- final WebElement _el;
- final ResponsiveGrid> _grid;
- private Map _rowMapByLabel = null;
+ private final WebElement _el;
+ protected final ResponsiveGrid> _grid;
protected GridRow(ResponsiveGrid> grid, WebElement element)
{
@@ -73,20 +76,18 @@ public GridRow select(boolean checked)
/**
* gets the cell at the specified index
- * use fieldLabel, which computes the appropriate index
- * This method is intended for short-term use, until we can offload usages in the heatmap to its own component
*/
public WebElement getCell(int colIndex)
{
- return Locator.tag("td").index(colIndex).findElement(this);
+ return elementCache().findCells().get(colIndex);
}
/**
* gets the cell corresponding to the specified column
*/
- public WebElement getCell(String fieldLabel)
+ public WebElement getCell(CharSequence columnIdentifier)
{
- return getCell(_grid.getColumnIndex(fieldLabel));
+ return getCell(_grid.getColumnIndex(columnIdentifier));
}
/**
@@ -128,25 +129,25 @@ public void clickLinkWithTitle(String text)
/**
* finds a AttachmentCard in the specified column, clicks it, and waits for the image to display in a modal
*/
- public ImageFileViewDialog clickImgFile(String fieldLabel)
+ public ImageFileViewDialog clickImgFile(CharSequence columnIdentifier)
{
- return elementCache().waitForAttachment(fieldLabel).viewImgFile();
+ return elementCache().waitForAttachment(columnIdentifier).viewImgFile();
}
/**
* finds a AttachmentCard specified filename, clicks it, and waits for the file to download
*/
- public File clickNonImgFile(String fieldLabel)
+ public File clickNonImgFile(CharSequence columnIdentifier)
{
- return elementCache().waitForAttachment(fieldLabel).clickOnNonImgFile();
+ return elementCache().waitForAttachment(columnIdentifier).clickOnNonImgFile();
}
/**
* Returns the text in the row for the specified column
*/
- public String getText(String fieldLabel)
+ public String getText(CharSequence columnIdentifier)
{
- return getCell(fieldLabel).getText();
+ return getCell(columnIdentifier).getText();
}
/**
@@ -154,8 +155,7 @@ public String getText(String fieldLabel)
*/
public List getTexts()
{
- List columnValues = getWrapper().getTexts(Locator.css("td")
- .findElements(this));
+ List columnValues = elementCache().getCellTexts();
if (hasSelectColumn())
columnValues.remove(0);
return columnValues;
@@ -166,17 +166,48 @@ public List getTexts()
*/
public Map getRowMapByLabel()
{
- if (_rowMapByLabel == null)
+ return getRowMap(FieldReference::getLabel);
+ }
+
+ /**
+ * gets a map of the row's values, keyed by column name
+ */
+ public Map getRowMapByName()
+ {
+ return getRowMap(FieldReference::getName);
+ }
+
+ /**
+ * gets a map of the row's values, keyed by column fieldKey
+ */
+ public Map getRowMapByFieldKey()
+ {
+ return getRowMap(FieldReference::getFieldKey);
+ }
+
+ Map getRowMap(Function keyMapper)
+ {
+ List columnValues = elementCache().getCellTexts();
+ List headers = _grid.getHeaders();
+
+ Map rowMap = new LinkedHashMap<>();
+
+ for (FieldReference header : headers)
{
- _rowMapByLabel = new CaseInsensitiveHashMap<>();
- List columns = _grid.getColumnLabels();
- List rowCellTexts = getTexts();
- for (int i = 0; i < columns.size(); i++)
+ T key = keyMapper.apply(header);
+ String value = columnValues.get(header.getDomIndex());
+
+ if (rowMap.containsKey(key))
+ {
+ TestLogger.warn("Column identifier '%s' is ambiguous, omitting value '%s', consider getting data by name or fieldKey (e.g. %s)".formatted(key, value, header.getFieldKey()));
+ }
+ else
{
- _rowMapByLabel.put(columns.get(i), rowCellTexts.get(i));
+ rowMap.put(key, value);
}
}
- return _rowMapByLabel;
+
+ return rowMap;
}
@Override
@@ -202,9 +233,29 @@ protected class ElementCache extends Component>.ElementCache
public ReactCheckBox selectCheckbox = new ReactCheckBox(Locator.tagWithAttribute("input", "type", "checkbox")
.findWhenNeeded(this));
- public AttachmentCard waitForAttachment(String fieldLabel)
+ private List _cells = null;
+ protected List findCells()
+ {
+ if (_cells == null)
+ {
+ _cells = Locator.xpath("./td").findElements(this);
+ }
+ return _cells;
+ }
+
+ private List cellTexts = null;
+ protected List getCellTexts()
+ {
+ if (cellTexts == null)
+ {
+ cellTexts = getWrapper().getTexts(findCells());
+ }
+ return cellTexts;
+ }
+
+ public AttachmentCard waitForAttachment(CharSequence columnIdentifier)
{
- return new AttachmentCard.FileAttachmentCardFinder(getDriver()).waitFor(getCell(fieldLabel));
+ return new AttachmentCard.FileAttachmentCardFinder(getDriver()).waitFor(getCell(columnIdentifier));
}
}
diff --git a/src/org/labkey/test/components/ui/grids/QueryGrid.java b/src/org/labkey/test/components/ui/grids/QueryGrid.java
index 78e6eb21eb..9775a4666d 100644
--- a/src/org/labkey/test/components/ui/grids/QueryGrid.java
+++ b/src/org/labkey/test/components/ui/grids/QueryGrid.java
@@ -79,14 +79,13 @@ public Map getRowMapByLabel(String text)
/**
* Returns the first row with the supplied text in the specified column
- * @param fieldLabel The text in the column header cell
+ * @param columnIdentifier The text in the column header cell
* @param text text in the data cell
* @return row data
*/
- public Map getRowMapByLabel(String fieldLabel, String text)
+ public Map getRowMapByLabel(CharSequence columnIdentifier, String text)
{
- GridRow row = getRow(fieldLabel, text);
- return row.getRowMapByLabel();
+ return getRow(columnIdentifier, text).getRowMapByLabel();
}
/**
@@ -113,15 +112,15 @@ public Map getRowMapByLabel(Locator.XPathLocator containing)
/**
* Selects or un-selects the first row with the specified text in the specified column
- * @param fieldLabel The exact text of the column header
+ * @param columnIdentifier The exact text of the column header
* @param text The full text of the cell to match
* @param checked whether or not to check the box
* @return this grid
*/
@Override
- public QueryGrid selectRow(String fieldLabel, String text, boolean checked)
+ public QueryGrid selectRow(CharSequence columnIdentifier, String text, boolean checked)
{
- getRow(fieldLabel, text).select(checked);
+ getRow(columnIdentifier, text).select(checked);
return this;
}
@@ -225,7 +224,10 @@ public void doAndWaitForUpdate(Runnable func)
func.run();
- optionalStatus.ifPresent(el -> getWrapper().shortWait().until(ExpectedConditions.stalenessOf(el)));
+ optionalStatus.ifPresent(el -> {
+ getWrapper().shortWait().until(ExpectedConditions.stalenessOf(el));
+ elementCache().selectionStatusContainerLoc.waitForElement(this, 5_000);
+ });
waitForLoaded();
clearElementCache();
@@ -535,7 +537,7 @@ public boolean isManageViewsEnabled()
for(WebElement menuItem : menuItems)
{
// Why does menuItem.getText() return an empty string here?
- if(menuItem.getAttribute("text").contains("Manage Saved Views"))
+ if(menuItem.getDomProperty("text").contains("Manage Saved Views"))
return false;
}
diff --git a/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java b/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java
index 70d7aefab9..2fd6fba318 100644
--- a/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java
+++ b/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java
@@ -14,10 +14,11 @@
import org.labkey.test.components.WebDriverComponent;
import org.labkey.test.components.html.RadioButton;
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.params.FieldKey;
import org.labkey.test.util.selenium.WebElementUtils;
import org.openqa.selenium.Keys;
-import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.NotFoundException;
import org.openqa.selenium.StaleElementReferenceException;
import org.openqa.selenium.WebDriver;
@@ -27,7 +28,7 @@
import java.util.ArrayList;
import java.util.Collection;
-import java.util.HashMap;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -38,7 +39,7 @@
import static org.junit.Assert.assertEquals;
import static org.labkey.test.WebDriverWrapper.waitFor;
-public class ResponsiveGrid extends WebDriverComponent.ElementCache> implements UpdatingComponent
+public class ResponsiveGrid> extends WebDriverComponent.ElementCache> implements UpdatingComponent
{
final WebElement _gridElement;
final WebDriver _driver;
@@ -99,7 +100,7 @@ public boolean hasLockedColumn()
{
return Locator.tagWithClass("div", "grid-panel__grid")
.findElement(getComponentElement())
- .getAttribute("class").contains("grid-panel__lock-left");
+ .getDomAttribute("class").contains("grid-panel__lock-left");
}
/**
@@ -113,39 +114,49 @@ public void scrollToOrigin()
/**
* Sorts from the grid header menu
- * @param fieldLabel column header for
+ * @param columnIdentifier fieldKey, name, or label of column
* @return this grid
*/
- public T sortColumnAscending(String fieldLabel)
+ public T sortColumnAscending(CharSequence columnIdentifier)
{
- sortColumn(fieldLabel, SortDirection.ASC);
+ sortColumn(columnIdentifier, SortDirection.ASC);
return getThis();
}
/**
* Sorts from the grid header menu
- * @param fieldLabel Text of column
+ * @param columnIdentifier fieldKey, name, or label of column
* @return this grid
*/
- public T sortColumnDescending(String fieldLabel)
+ public T sortColumnDescending(CharSequence columnIdentifier)
{
- sortColumn(fieldLabel, SortDirection.DESC);
+ sortColumn(columnIdentifier, SortDirection.DESC);
return getThis();
}
- public void sortColumn(String fieldLabel, SortDirection direction)
+ /**
+ * Sorts from the grid header menu
+ * @param columnIdentifier fieldKey, name, or label of column
+ */
+ public void sortColumn(CharSequence columnIdentifier, SortDirection direction)
{
- clickColumnMenuItem(fieldLabel, direction.equals(SortDirection.DESC) ? "Sort descending" : "Sort ascending", true);
+ clickColumnMenuItem(columnIdentifier, direction.equals(SortDirection.DESC) ? "Sort descending" : "Sort ascending", true);
}
- public void clearSort(String fieldLabel)
+ /**
+ * @param columnIdentifier fieldKey, name, or label of column
+ */
+ public void clearSort(CharSequence columnIdentifier)
{
- clickColumnMenuItem(fieldLabel, "Clear sort", true);
+ clickColumnMenuItem(columnIdentifier, "Clear sort", true);
}
- public boolean hasColumnSortIcon(String fieldLabel)
+ /**
+ * @param columnIdentifier fieldKey, name, or label of column
+ */
+ public boolean hasColumnSortIcon(CharSequence columnIdentifier)
{
- WebElement headerCell = elementCache().getColumnHeaderCell(fieldLabel);
+ WebElement headerCell = elementCache().getColumnHeaderCell(columnIdentifier);
Optional colHeaderIcon = Locator.XPathLocator.union(
Locator.tagWithClass("span", "grid-panel__col-header-icon").withClass("fa-sort-amount-asc"),
Locator.tagWithClass("span", "grid-panel__col-header-icon").withClass("fa-sort-amount-desc")
@@ -154,23 +165,32 @@ public boolean hasColumnSortIcon(String fieldLabel)
}
- public T filterColumn(String fieldLabel, Filter.Operator operator)
+ /**
+ * @param columnIdentifier fieldKey, name, or label of column
+ */
+ public T filterColumn(CharSequence columnIdentifier, Filter.Operator operator)
{
- return filterColumn(fieldLabel, operator, null);
+ return filterColumn(columnIdentifier, operator, null);
}
- public T filterColumn(String fieldLabel, Filter.Operator operator, Object value)
+ /**
+ * @param columnIdentifier fieldKey, name, or label of column
+ */
+ public T filterColumn(CharSequence columnIdentifier, Filter.Operator operator, Object value)
{
T _this = getThis();
- doAndWaitForUpdate(()->initFilterColumn(fieldLabel, operator, value).confirm());
+ doAndWaitForUpdate(()->initFilterColumn(columnIdentifier, operator, value).confirm());
return _this;
}
- public T filterColumn(String fieldLabel, Filter.Operator operator1, Object value1, Filter.Operator operator2, Object value2)
+ /**
+ * @param columnIdentifier fieldKey, name, or label of column
+ */
+ public T filterColumn(CharSequence columnIdentifier, Filter.Operator operator1, Object value1, Filter.Operator operator2, Object value2)
{
T _this = getThis();
doAndWaitForUpdate(()-> {
- GridFilterModal filterModal = initFilterColumn(fieldLabel, null, null);
+ GridFilterModal filterModal = initFilterColumn(columnIdentifier, null, null);
filterModal.selectExpressionTab().setFilters(
new FilterExpressionPanel.Expression(operator1, value1),
new FilterExpressionPanel.Expression(operator2, value2)
@@ -181,11 +201,14 @@ public T filterColumn(String fieldLabel, Filter.Operator operator1, Object value
return _this;
}
- public T filterBooleanColumn(String fieldLabel, boolean value)
+ /**
+ * @param columnIdentifier fieldKey, name, or label of column
+ */
+ public T filterBooleanColumn(CharSequence columnIdentifier, boolean value)
{
T _this = getThis();
doAndWaitForUpdate(() -> {
- clickColumnMenuItem(fieldLabel, "Filter...", false);
+ clickColumnMenuItem(columnIdentifier, "Filter...", false);
GridFilterModal filterModal = new GridFilterModal(getDriver(), this);
new RadioButton.RadioButtonFinder().withNameAndValue("field-value-bool-0", value?"true":"false").find(filterModal).set(true);
filterModal.confirm();
@@ -193,32 +216,41 @@ public T filterBooleanColumn(String fieldLabel, boolean value)
return _this;
}
- public String filterColumnExpectingError(String fieldLabel, Filter.Operator operator, Object value)
+ /**
+ * @param columnIdentifier fieldKey, name, or label of column
+ */
+ public String filterColumnExpectingError(CharSequence columnIdentifier, Filter.Operator operator, Object value)
{
- GridFilterModal filterModal = initFilterColumn(fieldLabel, operator, value);
+ GridFilterModal filterModal = initFilterColumn(columnIdentifier, operator, value);
String errorMsg = filterModal.confirmExpectingError();
filterModal.cancel();
return errorMsg;
}
- private GridFilterModal initFilterColumn(String fieldLabel, Filter.Operator operator, Object value)
+ private GridFilterModal initFilterColumn(CharSequence columnIdentifier, Filter.Operator operator, Object value)
{
- clickColumnMenuItem(fieldLabel, "Filter...", false);
+ clickColumnMenuItem(columnIdentifier, "Filter...", false);
GridFilterModal filterModal = new GridFilterModal(getDriver(), this);
if (operator != null)
filterModal.selectExpressionTab().setFilter(new FilterExpressionPanel.Expression(operator, value));
return filterModal;
}
- public T removeColumnFilter(String fieldLabel)
+ /**
+ * @param columnIdentifier fieldKey, name, or label of column
+ */
+ public T removeColumnFilter(CharSequence columnIdentifier)
{
- clickColumnMenuItem(fieldLabel, "Remove filter", true);
+ clickColumnMenuItem(columnIdentifier, "Remove filter", true);
return getThis();
}
- public boolean hasColumnFilterIcon(String fieldLabel)
+ /**
+ * @param columnIdentifier fieldKey, name, or label of column
+ */
+ public boolean hasColumnFilterIcon(CharSequence columnIdentifier)
{
- WebElement headerCell = elementCache().getColumnHeaderCell(fieldLabel);
+ WebElement headerCell = elementCache().getColumnHeaderCell(columnIdentifier);
Optional colHeaderIcon = Locator.tagWithClass("span", "grid-panel__col-header-icon")
.withClass("fa-filter")
.findOptionalElement(headerCell);
@@ -229,13 +261,13 @@ public boolean hasColumnFilterIcon(String fieldLabel)
/**
* use the column menu to hide the given column.
*
- * @param fieldLabel Column to hide.
+ * @param columnIdentifier fieldKey, name, or label of column
* @return This grid.
*/
- public T hideColumn(String fieldLabel)
+ public T hideColumn(CharSequence columnIdentifier)
{
// Because this will remove the column wait for the grid to update.
- clickColumnMenuItem(fieldLabel, "Hide Column", true);
+ clickColumnMenuItem(columnIdentifier, "Hide Column", true);
return getThis();
}
@@ -246,24 +278,24 @@ public T hideColumn(String fieldLabel)
*/
public FieldSelectionDialog insertColumn()
{
- return insertColumn(getColumnLabels().get(0));
+ return insertColumn(getHeaders().get(0).getFieldKey().toString());
}
/**
* Use the column menu to show a Field Selection dialog {@link FieldSelectionDialog}. This will use the given column to
* get the menu. This should insert the column after (to the right) of this column.
*
- * @param fieldLabel The column to get the menu from.
+ * @param columnIdentifier fieldKey, name, or label of column
* @return A {@link FieldSelectionDialog}
*/
- public FieldSelectionDialog insertColumn(String fieldLabel)
+ public FieldSelectionDialog insertColumn(CharSequence columnIdentifier)
{
// Because this is going to show the customize grid dialog don't wait for a grid update. the dialog will wait for the update.
- clickColumnMenuItem(fieldLabel, "Insert Column", false);
+ clickColumnMenuItem(columnIdentifier, "Insert Column", false);
return new FieldSelectionDialog(getDriver(), this);
}
- protected void clickColumnMenuItem(String fieldLabel, String menuText, boolean waitForUpdate)
+ protected void clickColumnMenuItem(CharSequence columnIdentifier, String menuText, boolean waitForUpdate)
{
if(hasLockedColumn())
@@ -271,7 +303,7 @@ protected void clickColumnMenuItem(String fieldLabel, String menuText, boolean w
scrollToOrigin();
}
- WebElement headerCell = elementCache().getColumnHeaderCell(fieldLabel);
+ WebElement headerCell = elementCache().getColumnHeaderCell(columnIdentifier);
// Scroll to middle in order to make room for the dropdown menu
getWrapper().scrollToMiddle(headerCell);
@@ -292,13 +324,17 @@ protected void clickColumnMenuItem(String fieldLabel, String menuText, boolean w
waitFor(()-> !menuItem.isDisplayed(), 1000);
}
- public void editColumnLabel(String fieldLabel, String newColumnLabel)
+ /**
+ * @param columnIdentifier fieldKey, name, or label of column
+ * @param newColumnLabel new label for the column
+ */
+ public void editColumnLabel(CharSequence columnIdentifier, String newColumnLabel)
{
// Get the column header.
- WebElement headerCell = elementCache().getColumnHeaderCell(fieldLabel);
+ WebElement headerCell = elementCache().getColumnHeaderCell(columnIdentifier);
// Select the edit menu.
- clickColumnMenuItem(fieldLabel, "Edit Label", false);
+ clickColumnMenuItem(columnIdentifier, "Edit Label", false);
// Get the textbox.
WebElement textEdit = Locator.tag("input").findWhenNeeded(headerCell);
@@ -341,7 +377,7 @@ 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, value-text in that column
+ * @param partialMap key-column (fieldKey, name, or label), value-text in that column
* @param checked the desired checkbox state
* @return this grid
*/
@@ -356,16 +392,16 @@ public T selectRow(Map partialMap, boolean checked)
/**
* Finds the first row with the specified text in the specified column and sets its checkbox
- * @param fieldLabel header text of the specified column
+ * @param columnIdentifier fieldKey, name, or label of column
* @param text Text to be found in the specified column
* @param checked true for checked, false for unchecked
* @return this grid
*/
- public ResponsiveGrid> selectRow(String fieldLabel, String text, boolean checked)
+ public ResponsiveGrid> selectRow(CharSequence columnIdentifier, String text, boolean checked)
{
- GridRow row = getRow(fieldLabel, text);
+ GridRow row = getRow(columnIdentifier, text);
selectRowAndVerifyCheckedCounts(row, checked);
- getWrapper().log("Row at column ["+fieldLabel+"] with text ["+text+"] selection state set to + ["+row.isSelected()+"]");
+ getWrapper().log("Row at column ["+columnIdentifier+"] with text ["+text+"] selection state set to + ["+row.isSelected()+"]");
return getThis();
}
@@ -390,16 +426,16 @@ else if (!checked && row.isSelected())
/**
* Sets the specified rows' selector checkboxes to the requested select state
- * @param fieldLabel Header text of the column to search
+ * @param columnIdentifier fieldKey, name, or label of column
* @param texts Text to search for in the specified column
* @param checked True for checked, false for unchecked
* @return this grid
*/
- public T selectRows(String fieldLabel, Collection texts, boolean checked)
+ public T selectRows(CharSequence columnIdentifier, Collection texts, boolean checked)
{
for (String text : texts)
{
- selectRow(fieldLabel, text, checked);
+ selectRow(columnIdentifier, text, checked);
}
return getThis();
}
@@ -414,29 +450,6 @@ public boolean isRowSelected(int index)
return new GridRow.GridRowFinder(this).index(index).find(this).isSelected();
}
- /**
- * finds the first row containing the specified text and returns the checked state
- * @param text A value in the row, used to identify the row. (preferably a key)
- * @return whether or not the selector checkbox is checked
- */
- public boolean isRowSelected(String text)
- {
- return new GridRow.GridRowFinder(this).withCellWithText(text)
- .find(this).isSelected();
- }
-
- /**
- * finds the first row containing the specified text in the specified column and returns the checked state
- * @param fieldLabel the text in the column to search
- * @param text the value in the row to find
- * @return true if the checkbox is checked, otherwise false
- */
- public boolean isRowSelected(String fieldLabel, String text)
- {
- return new GridRow.GridRowFinder(this).withTextAtColumn(text, getColumnIndex(fieldLabel))
- .find(this).isSelected();
- }
-
protected ReactCheckBox selectAllBox()
{
ReactCheckBox box = elementCache().selectAllCheckbox;
@@ -531,29 +544,29 @@ public Optional getOptionalRow(String text)
/**
* Returns the first row with matching text in the specified column
- * @param fieldLabel The exact text of the column header
+ * @param columnIdentifier fieldKey, name, or label of column to search
* @param text The full text of the cell to match
* @return the first row that matches
*/
- public GridRow getRow(String fieldLabel, String text)
+ public GridRow getRow(CharSequence columnIdentifier, String text)
{
- return elementCache().getRow(fieldLabel, text);
+ return elementCache().getRow(columnIdentifier, text);
}
/**
* Returns the first row with matching text in the specified column
- * @param fieldLabel the column to search
+ * @param columnIdentifier fieldKey, name, or label of column of the column to search
* @param text exact text to match in that column
* @return the first row matching the search criteria
*/
- public Optional getOptionalRow(String fieldLabel, String text)
+ public Optional getOptionalRow(CharSequence columnIdentifier, String text)
{
- return elementCache().getOptionalRow(fieldLabel, text);
+ return elementCache().getOptionalRow(columnIdentifier, text);
}
/**
* Returns the first row with matching text in the specified columns
- * @param partialMap Map of key (column), value (text)
+ * @param partialMap Map of key (fieldKey, name, or label of column), value (text)
* @return the first row with matching column/text for all of the supplied key/value pairs, or NotFoundException
*/
public GridRow getRow(Map partialMap)
@@ -580,12 +593,15 @@ public List getRows()
return elementCache().getRows();
}
- public List getColumnDataAsText(String fieldLabel)
+ /**
+ * @param columnIdentifier fieldKey, name, or label of column
+ */
+ public List getColumnDataAsText(CharSequence columnIdentifier)
{
List columnData = new ArrayList<>();
for (GridRow row : getRows())
{
- columnData.add(row.getText(fieldLabel));
+ columnData.add(row.getText(columnIdentifier));
}
return columnData;
}
@@ -600,13 +616,12 @@ public boolean hasSelectColumn()
}
/**
- * used to find the raw index of a given column as rendered in the dom.
- * To get the normalized index (which excludes selector rows if present) use
- * elementCache().indexes.get(column).getNormalizedIndex()
+ * @param columnIdentifier fieldKey, name, or label of column
+ * @return the DOM index of the specified column (e.g. '0' for the row selection column)
*/
- protected Integer getColumnIndex(String fieldLabel)
+ protected Integer getColumnIndex(CharSequence columnIdentifier)
{
- return elementCache().getColumnIndex(fieldLabel);
+ return elementCache().getColumnIndex(columnIdentifier);
}
/**
@@ -644,23 +659,35 @@ public Map getRowMapByLabel(int rowIndex)
/**
* Get text from the specified column in the specified row
+ * @param columnIdentifier fieldKey, name, or label of column
*/
- public String getCellText(int rowIndex, String fieldLabel)
+ public String getCellText(int rowIndex, CharSequence columnIdentifier)
{
- return getRow(rowIndex).getText(fieldLabel);
+ return getRow(rowIndex).getText(columnIdentifier);
}
/**
- *
* @return a list of Map<String, String> containing keys and values for each row
*/
public List> getRowMapsByLabel()
{
- if(null == elementCache().mapList)
- {
- elementCache().mapList = elementCache()._initGridData();
- }
- return elementCache().mapList;
+ return elementCache().getRows().stream().map(GridRow::getRowMapByLabel).toList();
+ }
+
+ /**
+ * @return a list of Map<String, String> containing keys and values for each row
+ */
+ public List> getRowMapsByName()
+ {
+ return elementCache().getRows().stream().map(GridRow::getRowMapByName).toList();
+ }
+
+ /**
+ * @return a list of Map<String, String> containing keys and values for each row
+ */
+ public List> getRowMapsByFieldKey()
+ {
+ return elementCache().getRows().stream().map(GridRow::getRowMapByFieldKey).toList();
}
/**
@@ -674,12 +701,12 @@ public void clickLink(String text)
/**
* locates the first link in the specified column, clicks it, and waits for the URL to update
- * @param fieldLabel column in which to search
+ * @param columnIdentifier fieldKey, name, or label of column
* @param text text for link to match
*/
- public void clickLink(String fieldLabel, String text)
+ public void clickLink(CharSequence columnIdentifier, String text)
{
- getRow(fieldLabel, text).clickLink(text);
+ getRow(columnIdentifier, text).clickLink(text);
}
public boolean gridMessagePresent()
@@ -707,25 +734,13 @@ public String getGridError()
/** The responsiveGrid now supports redacting fields
*
- * @param fieldLabel the column label. (uses starts-with matching)
+ * @param columnIdentifier fieldKey, name, or label of column
* @return true if the specified grid header cell has the 'phi-protected' class on it
*/
- public boolean getColumnPHIProtected(String fieldLabel)
- {
- WebElement columnHeader = Locator.tagWithClass("th", "grid-header-cell")
- .withDescendant(Locators.headerCellBody(fieldLabel)).findElement(this);
- return columnHeader.getAttribute("class").contains("phi-protected");
- }
-
- /**
- * Gets the title attribute of the column header cell, if it has one
- * @param fieldLabel The text with which to find the cell (uses startswith matching)
- * @return the contents of the 'title' attribute of the cell, or null if the attribute is
- * not present.
- */
- public String getColumnTitleAttribute(String fieldLabel)
+ public boolean getColumnPHIProtected(CharSequence columnIdentifier)
{
- return elementCache().getColumnHeaderCell(fieldLabel).getAttribute("title");
+ WebElement columnHeader = elementCache().getColumnHeaderCell(columnIdentifier);
+ return columnHeader.getDomAttribute("class").contains("phi-protected");
}
public Optional getGridEmptyMessage()
@@ -749,6 +764,11 @@ public Optional getGridEmptyMessage()
return msg;
}
+ List getHeaders()
+ {
+ return Collections.unmodifiableList(elementCache().findHeaders());
+ }
+
/**
* supports chaining between base and derived instances
* @return magic
@@ -758,22 +778,6 @@ protected T getThis()
return (T) this;
}
- /**
- * Call this function to force a re-initialization of the internal data representation of the grid data.
- * When trying to be more efficient the grid data is stored in an internal variable (so this is a stateful object).
- * On creates the internal grid data is initialize by calling waitForLoaded. The waitForLoaded function is also
- * called when the page/grid is navigated, but it is not when a search, or ordering is done. As a temporary work
- * around this function is made public so the calling function can update the data.
- *
- * The real fix would be to add an event listener to the grid and reinitialize the internal data when it detects a change.
- *
- */
- public void initGridData()
- {
- waitForLoaded();
- elementCache().mapList = elementCache()._initGridData();
- }
-
@Override
protected ElementCache newElementCache()
{
@@ -806,74 +810,51 @@ public void toggle()
}
};
- private final Map headerCells = new HashMap<>();
- protected final WebElement getColumnHeaderCell(String headerText)
+ protected final WebElement getColumnHeaderCell(CharSequence columnIdentifier)
{
- if (!headerCells.containsKey(headerText))
- {
- WebElement headerCell = Locators.headerCellBody(headerText).findElement(this);
- headerCells.put(headerText, headerCell);
- }
- return headerCells.get(headerText);
+ return findColumnHeader(columnIdentifier).getElement();
}
- protected List fieldLabels;
- protected Map indexes;
- protected Map initColumnsAndIndices()
+ private FieldReferenceManager _fieldReferenceManager;
+ protected FieldReferenceManager getGridHeaderManager()
{
- if (fieldLabels == null || indexes == null)
+ if (_fieldReferenceManager == null)
{
+ List fieldReferences = new ArrayList<>();
List headerCellElements = Locators.headerCells.findElements(this);
- int offset = 0;
- if (hasSelectColumn())
- {
- headerCellElements.remove(0);
- offset = 1;
- }
- fieldLabels = headerCellElements.stream().map(el -> WebElementUtils.getTextContent(el).trim()).toList();
- indexes = new HashMap<>();
- for (int i = 0; i < headerCellElements.size(); i++)
+ for (int domIndex = hasSelectColumn() ? 1 : 0; domIndex < headerCellElements.size(); domIndex++)
{
- headerCells.put(fieldLabels.get(i), headerCellElements.get(i)); // Fill out the headerCells Map since we have them all
- indexes.put(fieldLabels.get(i), new ColumnIndex(fieldLabels.get(i), i+offset, i));
+ fieldReferences.add(new FieldReference(headerCellElements.get(domIndex), domIndex));
}
+
+ _fieldReferenceManager = new FieldReferenceManager(fieldReferences);
}
- return indexes;
+ return _fieldReferenceManager;
}
- protected int getColumnIndex(String fieldLabel)
+ protected List findHeaders()
{
- final ColumnIndex columnIndex = initColumnsAndIndices().get(fieldLabel);
- if (columnIndex == null)
- {
- throw new NoSuchElementException(String.format("Column not found: '%s'.\nKnown columns: %s",
- fieldLabel, String.join(", ", initColumnsAndIndices().keySet())));
- }
- return columnIndex.getRawIndex();
+ return getGridHeaderManager().getColumnHeaders();
}
- protected List getColumnLabels()
+ protected FieldReference findColumnHeader(CharSequence columnIdentifier)
{
- initColumnsAndIndices();
- return fieldLabels;
+ return getGridHeaderManager().findFieldReference(columnIdentifier);
}
- protected List> mapList;
- protected List gridRows;
- private List> _initGridData()
+ protected int getColumnIndex(CharSequence columnIdentifier)
{
- List> rowMaps = new ArrayList<>();
- gridRows = getRows();
- for(GridRow row : gridRows)
- {
- rowMaps.add(row.getRowMapByLabel());
- }
- return rowMaps;
+ return findColumnHeader(columnIdentifier).getDomIndex();
+ }
+
+ protected List getColumnLabels()
+ {
+ return findHeaders().stream().map(FieldReferenceManager.FieldReference::getLabel).collect(Collectors.toList());
}
protected GridRow getRow(int index)
{
- return new GridRow.GridRowFinder(ResponsiveGrid.this).index(index).find(this);
+ return getRows().get(index);
}
protected GridRow getRow(String text)
@@ -886,18 +867,16 @@ protected Optional getOptionalRow(String text)
return new GridRow.GridRowFinder(ResponsiveGrid.this).withCellWithText(text).findOptional(this);
}
- protected GridRow getRow(String fieldLabel, String text)
+ protected GridRow getRow(CharSequence columnIdentifier, String text)
{
- // try to normalize column index to start at 0, excluding row selector column
- Integer columnIndex = getColumnIndex(fieldLabel);
+ int columnIndex = getColumnIndex(columnIdentifier);
return new GridRow.GridRowFinder(ResponsiveGrid.this).withTextAtColumn(text, columnIndex)
.find(this);
}
- protected Optional getOptionalRow(String fieldLabel, String text)
+ protected Optional getOptionalRow(CharSequence columnIdentifier, String text)
{
- // try to normalize column index to start at 0, excluding row selector column
- Integer columnIndex = getColumnIndex(fieldLabel);
+ int columnIndex = getColumnIndex(columnIdentifier);
return new GridRow.GridRowFinder(ResponsiveGrid.this).withTextAtColumn(text, columnIndex)
.findOptional(this);
}
@@ -914,9 +893,14 @@ protected GridRow getRow(Locator.XPathLocator containing)
return new GridRow.GridRowFinder(ResponsiveGrid.this).withDescendant(containing).find();
}
+ private List gridRows;
protected List getRows()
{
- return new GridRow.GridRowFinder(ResponsiveGrid.this).findAll(getComponentElement());
+ if (gridRows == null)
+ {
+ gridRows = new GridRow.GridRowFinder(ResponsiveGrid.this).findAll(this);
+ }
+ return gridRows;
}
}
@@ -989,36 +973,3 @@ protected Locator locator()
}
}
}
-
- class ColumnIndex
- {
- private final Integer _rawIndex;
- private final Integer _normalizedIndex;
- private final String _fieldLabel;
-
- /**
- * Helper to
- * @param fieldLabel text of the column header
- * @param rawIndex dom-oriented index of the column
- * @param normalizedIndex index of the list of columns
- */
- public ColumnIndex(String fieldLabel, int rawIndex, int normalizedIndex)
- {
- _fieldLabel = fieldLabel;
- _rawIndex = rawIndex;
- _normalizedIndex = normalizedIndex;
- }
-
- public String getColumnLabel()
- {
- return _fieldLabel;
- }
- public Integer getRawIndex()
- {
- return _rawIndex;
- }
- public Integer getNormalizedIndex()
- {
- return _normalizedIndex;
- }
- }
diff --git a/src/org/labkey/test/params/FieldDefinition.java b/src/org/labkey/test/params/FieldDefinition.java
index 7c6717c9ee..79ba2ea9b1 100644
--- a/src/org/labkey/test/params/FieldDefinition.java
+++ b/src/org/labkey/test/params/FieldDefinition.java
@@ -22,6 +22,7 @@
import org.json.JSONObject;
import org.junit.Assert;
import org.labkey.api.exp.query.ExpSchema;
+import org.labkey.remoteapi.domain.ConditionalFormat;
import org.labkey.remoteapi.domain.PropertyDescriptor;
import org.labkey.remoteapi.query.Filter;
import org.labkey.test.components.html.OptionSelect;
@@ -216,6 +217,13 @@ public FieldDefinition setHidden(Boolean hidden)
return this;
}
+ @Override
+ public FieldDefinition setConditionalFormats(List conditionalFormats)
+ {
+ super.setConditionalFormats(conditionalFormats);
+ return this;
+ }
+
public LookupInfo getLookup()
{
return _type.getLookupInfo();
diff --git a/src/org/labkey/test/params/FieldInfo.java b/src/org/labkey/test/params/FieldInfo.java
new file mode 100644
index 0000000000..ebd28a0019
--- /dev/null
+++ b/src/org/labkey/test/params/FieldInfo.java
@@ -0,0 +1,110 @@
+package org.labkey.test.params;
+
+import java.util.Objects;
+import java.util.function.Consumer;
+
+/**
+ * Immutable alternative to 'FieldDefinition'
+ * Use this for shared global field information
+ */
+public class FieldInfo
+{
+ private final FieldKey _fieldKey;
+ private final String _label;
+ private final FieldDefinition.ColumnType _columnType;
+ private final Consumer _fieldDefinitionMutator;
+
+ private FieldInfo(FieldKey fieldKey, String label, FieldDefinition.ColumnType columnType, Consumer fieldDefinitionMutator)
+ {
+ _fieldKey = fieldKey;
+ _label = label;
+ _columnType = Objects.requireNonNullElse(columnType, FieldDefinition.ColumnType.String);
+ _fieldDefinitionMutator = fieldDefinitionMutator;
+ }
+
+ public FieldInfo(String name, String label, FieldDefinition.ColumnType columnType)
+ {
+ this(FieldKey.fromParts(name.trim()), label, columnType, null);
+ }
+
+ public FieldInfo(String name, String label)
+ {
+ this(name, label, null);
+ }
+
+ public FieldInfo(String name, FieldDefinition.ColumnType columnType)
+ {
+ this(name, null, columnType);
+ }
+
+ public FieldInfo(String name)
+ {
+ this(name, null, null);
+ }
+
+ public FieldInfo customizeFieldDefinition(Consumer fieldDefinitionMutator)
+ {
+ return new FieldInfo(_fieldKey, _label, _columnType, fieldDefinitionMutator);
+ }
+
+ protected String getRawLabel()
+ {
+ return _label;
+ }
+
+ public String getLabel()
+ {
+ return Objects.requireNonNullElseGet(getRawLabel(), () -> FieldDefinition.labelFromName(_fieldKey.getName()));
+ }
+
+ public FieldKey getFieldKey()
+ {
+ return _fieldKey;
+ }
+
+ public String getName()
+ {
+ return _fieldKey.getName();
+ }
+
+ public FieldKey child(String name)
+ {
+ return _fieldKey.child(name);
+ }
+
+ public FieldDefinition getFieldDefinition()
+ {
+ return getFieldDefinition(_columnType);
+ }
+
+ public FieldDefinition getFieldDefinition(String lookupContainerPath)
+ {
+ if (!_columnType.isLookup())
+ {
+ throw new IllegalArgumentException("Unable to set lookup container for %s column: %s".formatted(_columnType.getLabel(), getName()));
+ }
+ else
+ {
+ String schema = _columnType.getLookupInfo().getSchema();
+ String table = _columnType.getLookupInfo().getTable();
+ FieldDefinition.ColumnType columnType = _columnType.getRangeURI().equals(FieldDefinition.ColumnType.Integer.getRangeURI())
+ ? new FieldDefinition.IntLookup(lookupContainerPath, schema, table)
+ : new FieldDefinition.StringLookup(lookupContainerPath, schema, table);
+ return getFieldDefinition(columnType);
+ }
+ }
+
+ private FieldDefinition getFieldDefinition(FieldDefinition.ColumnType columnType)
+ {
+ FieldDefinition fieldDefinition = new FieldDefinition(getName(), columnType);
+ if (getRawLabel() != null)
+ {
+ fieldDefinition.setLabel(getRawLabel());
+ }
+ if (_fieldDefinitionMutator != null)
+ {
+ _fieldDefinitionMutator.accept(fieldDefinition);
+ }
+ return fieldDefinition;
+ }
+}
diff --git a/src/org/labkey/test/params/FieldKey.java b/src/org/labkey/test/params/FieldKey.java
index 1a666faf8d..6070cbde5f 100644
--- a/src/org/labkey/test/params/FieldKey.java
+++ b/src/org/labkey/test/params/FieldKey.java
@@ -1,56 +1,188 @@
package org.labkey.test.params;
+import org.apache.commons.lang3.StringUtils;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
import java.util.List;
-import java.util.stream.Collectors;
-public class FieldKey
+public class FieldKey implements CharSequence
{
- private final List _parts;
- private String _label;
+ public static final FieldKey EMPTY = new FieldKey(""); // Useful as a sort of FieldKey builder starting point
+ public static final FieldKey SOURCES_FK = new FieldKey("DataInputs");
+ public static final FieldKey PARENTS_FK = new FieldKey("MaterialInputs");
+
+ private static final String SEPARATOR = "/";
+
+ private final FieldKey _parent;
+ private final String _name;
+ private final String _fieldKey;
+
+ private FieldKey(String name)
+ {
+ _parent = null;
+ _name = name;
+ _fieldKey = encodePart(name);
+ }
+
+ private FieldKey(FieldKey parent, String child)
+ {
+ _parent = parent;
+ _name = parent.getName() + SEPARATOR + child;
+ _fieldKey = parent + SEPARATOR + encodePart(child);
+ }
- private FieldKey(String... parts)
+ public static FieldKey fromParts(List parts)
{
- if (parts.length == 0)
+ FieldKey fieldKey = EMPTY;
+
+ for (String part : parts)
{
- throw new IllegalArgumentException("No field key parts were provided.");
+ if (StringUtils.isBlank(part))
+ throw new IllegalArgumentException("FieldKey contains a blank part: " + parts);
+ fieldKey = fieldKey.child(part);
}
- // '/' is used as a separator character in fieldKeys. Slashes in field names are encoded as '$S'
- _parts = Arrays.stream(parts)
- .map(part -> part.replace("/", "$S"))
- .collect(Collectors.toList());
- _label = parts[parts.length - 1];
+
+ return fieldKey;
}
public static FieldKey fromParts(String... parts)
{
- return new FieldKey(parts);
+ return fromParts(Arrays.asList(parts));
}
- public static FieldKey fromPath(String path)
+ /**
+ * Construct a FieldKey from a CharSequence that might be an encoded fieldKey
+ * @param fieldKey String or FieldKey
+ * @return FieldKey representation of the String, or the identity if a FieldKey was provided
+ */
+ public static @Nullable FieldKey fromFieldKey(CharSequence fieldKey)
{
- return new FieldKey(path.split("/"));
+ if (fieldKey instanceof FieldKey fk)
+ {
+ return fk;
+ }
+ else
+ {
+ try
+ {
+ return fromParts(Arrays.stream(fieldKey.toString().split(SEPARATOR)).map(FieldKey::decodePart).toList());
+ }
+ catch (IllegalArgumentException iae)
+ {
+ return null;
+ }
+ }
}
- public String getLabel()
+ /**
+ * Construct a FieldKey from a CharSequence that might be a field name
+ * @param nameOrFieldKey unencoded field name or an existing FieldKey object
+ * @return fieldKey encoded name, or the identity if one was provided
+ */
+ public static FieldKey fromName(CharSequence nameOrFieldKey)
{
- return _label;
+ if (nameOrFieldKey instanceof FieldKey fk)
+ return fk;
+ else
+ return fromParts(nameOrFieldKey.toString());
}
- public FieldKey setLabel(String label)
+ private static final String[] ILLEGAL = {"$", "/", "&", "}", "~", ",", "."};
+ private static final String[] REPLACEMENT = {"$D", "$S", "$A", "$B", "$T", "$C", "$P"};
+
+ public static String encodePart(String str)
{
- _label = label;
- return this;
+ return StringUtils.replaceEach(str, ILLEGAL, REPLACEMENT);
}
- public List getParts()
+ public static String decodePart(String str)
{
- return _parts;
+ return StringUtils.replaceEach(str, REPLACEMENT, ILLEGAL);
+ }
+
+ public FieldKey getParent()
+ {
+ return _parent;
+ }
+
+ public FieldKey child(String name)
+ {
+ if (StringUtils.isBlank(getName()))
+ {
+ return new FieldKey(name);
+ }
+ else
+ {
+ return new FieldKey(this, name);
+ }
+ }
+
+ public Iterator getIterator()
+ {
+ List ancestors = new ArrayList<>();
+ FieldKey temp = this;
+
+ while (temp != null)
+ {
+ ancestors.add(temp);
+ temp = temp.getParent();
+ }
+
+ Collections.reverse(ancestors);
+
+ return ancestors.iterator();
+ }
+
+ public String getName()
+ {
+ return _name;
+ }
+
+ public String[] getNameArray()
+ {
+ return Arrays.stream(_fieldKey.split(SEPARATOR)).map(FieldKey::decodePart).toArray(String[]::new);
+ }
+
+ @Override
+ public @NotNull String toString()
+ {
+ return _fieldKey;
+ }
+
+ @Override
+ public int length()
+ {
+ return _fieldKey.length();
+ }
+
+ @Override
+ public char charAt(int index)
+ {
+ return _fieldKey.charAt(index);
+ }
+
+ @Override
+ public @NotNull CharSequence subSequence(int start, int end)
+ {
+ return _fieldKey.subSequence(start, end);
+ }
+
+ @Override
+ public final boolean equals(Object o)
+ {
+ if (!(o instanceof FieldKey fieldKey)) return false;
+
+ return _fieldKey.equalsIgnoreCase(fieldKey._fieldKey); // FieldKeys aren't case-sensitive?
}
@Override
- public String toString()
+ public int hashCode()
{
- return String.join("/", getParts());
+ return _fieldKey.toLowerCase().hashCode(); // FieldKeys aren't case-sensitive?
}
}
diff --git a/src/org/labkey/test/params/list/IntListDefinition.java b/src/org/labkey/test/params/list/IntListDefinition.java
index c9f30ffdd6..4c755e4ffe 100644
--- a/src/org/labkey/test/params/list/IntListDefinition.java
+++ b/src/org/labkey/test/params/list/IntListDefinition.java
@@ -6,6 +6,8 @@
import org.labkey.test.util.TestDataGenerator;
import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
public class IntListDefinition extends ListDefinition
{
@@ -53,6 +55,21 @@ protected String getKeyType()
@Override
public TestDataGenerator getTestDataGenerator(String containerPath)
{
- return super.getTestDataGenerator(containerPath).setAutoGeneratedFields(getKeyName());
+ if (isAutoIncrementKey)
+ {
+ return super.getTestDataGenerator(containerPath).setAutoGeneratedFields(getKeyName());
+ }
+ else
+ {
+ // supply key values but ensure they don't collide
+ return super.getTestDataGenerator(containerPath).addDataSupplier(getKeyName(), new Supplier<>() {
+ private final AtomicInteger keys = new AtomicInteger(1);
+ @Override
+ public Object get()
+ {
+ return keys.getAndIncrement();
+ }
+ });
+ }
}
}
diff --git a/src/org/labkey/test/stress/RequestInfoTsvWriter.java b/src/org/labkey/test/stress/RequestInfoTsvWriter.java
index 4318e6c8c3..77d8a0231c 100644
--- a/src/org/labkey/test/stress/RequestInfoTsvWriter.java
+++ b/src/org/labkey/test/stress/RequestInfoTsvWriter.java
@@ -2,7 +2,7 @@
import org.labkey.remoteapi.miniprofiler.RequestInfo;
import org.labkey.serverapi.writer.PrintWriters;
-import org.labkey.test.util.TestDataUtils;
+import org.labkey.test.util.data.TestDataUtils;
import java.io.File;
import java.io.FileNotFoundException;
diff --git a/src/org/labkey/test/tests/ClientAPITest.java b/src/org/labkey/test/tests/ClientAPITest.java
index 6d2c451df2..2d43a013b1 100644
--- a/src/org/labkey/test/tests/ClientAPITest.java
+++ b/src/org/labkey/test/tests/ClientAPITest.java
@@ -57,7 +57,7 @@
import org.labkey.test.util.PermissionsHelper;
import org.labkey.test.util.PortalHelper;
import org.labkey.test.util.StudyHelper;
-import org.labkey.test.util.TestDataUtils;
+import org.labkey.test.util.data.TestDataUtils;
import org.labkey.test.util.UIUserHelper;
import org.labkey.test.util.WikiHelper;
import org.labkey.test.util.query.QueryUtils;
@@ -964,7 +964,7 @@ public void webpartTest()
assertTextPresent("Webpart Title");
for (FieldDefinition column : LIST_COLUMNS)
- assertTextPresent(column.getLabel());
+ assertTextPresent(column.getEffectiveLabel());
}
@Test
diff --git a/src/org/labkey/test/tests/FilterTest.java b/src/org/labkey/test/tests/FilterTest.java
index 7d24166cc9..561d5c3a1a 100644
--- a/src/org/labkey/test/tests/FilterTest.java
+++ b/src/org/labkey/test/tests/FilterTest.java
@@ -586,7 +586,7 @@ private void validFilterGeneratesCorrectResultsTest(FieldDefinition columnDef, S
DataRegionTable region = new DataRegionTable(TABLE_NAME, this);
region.setFilter(fieldKey, filter1Type, filter1, filter2Type, filter2);
- checkFilterWasApplied(textPresentAfterFilter, textNotPresentAfterFilter, columnDef.getLabel(), filter1Type, filter1, filter2Type, filter2);
+ checkFilterWasApplied(textPresentAfterFilter, textNotPresentAfterFilter, columnDef.getEffectiveLabel(), filter1Type, filter1, filter2Type, filter2);
log("** Checking filter present in R view");
region.goToReport(R_VIEW);
diff --git a/src/org/labkey/test/tests/SampleTypeNameExpressionTest.java b/src/org/labkey/test/tests/SampleTypeNameExpressionTest.java
index 39dac35804..ef82650e95 100644
--- a/src/org/labkey/test/tests/SampleTypeNameExpressionTest.java
+++ b/src/org/labkey/test/tests/SampleTypeNameExpressionTest.java
@@ -61,7 +61,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.labkey.test.params.FieldDefinition.DOMAIN_TRICKY_CHARACTERS;
-import static org.labkey.test.util.TestDataUtils.getEscapedNameExpression;
+import static org.labkey.test.util.data.TestDataUtils.getEscapedNameExpression;
@Category({Daily.class})
@BaseWebDriverTest.ClassTimeout(minutes = 5)
diff --git a/src/org/labkey/test/tests/SampleTypeRemoteAPITest.java b/src/org/labkey/test/tests/SampleTypeRemoteAPITest.java
index 552bf0e267..1d165115fe 100644
--- a/src/org/labkey/test/tests/SampleTypeRemoteAPITest.java
+++ b/src/org/labkey/test/tests/SampleTypeRemoteAPITest.java
@@ -173,7 +173,7 @@ public void importMissingValueSampleType() throws IOException, CommandException
dgen.addCustomRow(Map.of("name", "Eighth","mvStringData", "ActualData", "volume", 17.5));
// write the domain data into TSV format, for import via the UI
- String importTsv = dgen.writeTsvContents();
+ String importTsv = dgen.getDataAsTsv();
refresh();
DataRegionTable sampleTypeList = DataRegionTable.DataRegion(getDriver()).withName(SAMPLE_TYPE_DATA_REGION_NAME).waitFor();
@@ -365,7 +365,7 @@ public void exportMissingValueSampleTypeToTSV() throws CommandException, IOExcep
dgen.insertRows(createDefaultConnection(), dgen.getRows()); // insert data via API rather than UI
// prepare expected values-
- String expectedTSVData = dgen.writeTsvContents();
+ String expectedTSVData = dgen.getDataAsTsv();
String[] tsvRows = expectedTSVData.split("\n");
List dataRows = new ArrayList();
for (int i=1; i < tsvRows.length; i++) // don't validate columns; we expect labels instead of column names
@@ -775,7 +775,7 @@ private void insertAssayData(String assayName, List dataGener
{
AssayImportPage page = new AssayImportPage(getDriver())
.setNamedTextAreaValue("TextAreaDataCollector.textArea",
- dataGen.writeTsvContents());
+ dataGen.getDataAsTsv());
imported++;
if(imported < limit)
diff --git a/src/org/labkey/test/tests/assay/UploadLargeExcelAssayTest.java b/src/org/labkey/test/tests/assay/UploadLargeExcelAssayTest.java
index c813094936..167abd2662 100644
--- a/src/org/labkey/test/tests/assay/UploadLargeExcelAssayTest.java
+++ b/src/org/labkey/test/tests/assay/UploadLargeExcelAssayTest.java
@@ -7,39 +7,28 @@
import org.labkey.remoteapi.domain.PropertyDescriptor;
import org.labkey.test.BaseWebDriverTest;
import org.labkey.test.Locator;
-import org.labkey.test.TestFileUtils;
import org.labkey.test.WebDriverWrapper;
import org.labkey.test.categories.Assays;
import org.labkey.test.categories.Daily;
-import org.labkey.test.pages.ImportDataPage;
import org.labkey.test.pages.ReactAssayDesignerPage;
-import org.labkey.test.pages.assay.AssayImportPage;
-import org.labkey.test.pages.assay.AssayRunsPage;
import org.labkey.test.pages.assay.AssayUploadJobsPage;
import org.labkey.test.pages.query.SourceQueryPage;
import org.labkey.test.params.FieldDefinition;
import org.labkey.test.params.assay.GeneralAssayDesign;
-import org.labkey.test.params.experiment.SampleTypeDefinition;
import org.labkey.test.util.AbstractDataRegionExportOrSignHelper;
-import org.labkey.test.util.DataRegionTable;
-import org.labkey.test.util.SampleTypeHelper;
import org.labkey.test.util.TestDataGenerator;
-import org.labkey.test.util.TestLogger;
-import org.labkey.test.util.exp.SampleTypeAPIHelper;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
-import static org.junit.Assert.*;
-
@Category({Assays.class, Daily.class})
public class UploadLargeExcelAssayTest extends BaseWebDriverTest
{
public static String LARGE_ASSAY = "chaos_assay";
public static String LARGE_ASSAY_2 = "large_assay_2";
- public static List ASSAY_FIELDS = new ArrayList();
+ public static List ASSAY_FIELDS = new ArrayList<>();
@Override
protected void doCleanup(boolean afterTest)
@@ -96,8 +85,9 @@ public void testUpload200kRows() throws Exception
String fileName = "200kXlsxFile.xlsx";
var dgen = new TestDataGenerator("samples", "chaos_sample", getProjectName())
.withColumns(ASSAY_FIELDS);
+ dgen.generateRows(200_000);
log("writing large .xlsx file");
- var largeExcelFile = dgen.writeGeneratedDataToExcel(200000, "chaos", fileName);
+ var largeExcelFile = dgen.writeData(fileName);
log("finished writing large .xlsx file");
// import large generated excel to assay1
diff --git a/src/org/labkey/test/tests/component/EditableGridTest.java b/src/org/labkey/test/tests/component/EditableGridTest.java
index 122b4e0d47..c2d4804ead 100644
--- a/src/org/labkey/test/tests/component/EditableGridTest.java
+++ b/src/org/labkey/test/tests/component/EditableGridTest.java
@@ -1294,7 +1294,7 @@ private void checkSelectedStyle(EditableGrid editableGrid,
private String getActualPaste(EditableGrid testGrid)
{
- List> gridData = testGrid.getGridData(PASTE_1, PASTE_2, PASTE_3, PASTE_4, PASTE_5);
+ List> gridData = testGrid.getGridDataByLabel(PASTE_1, PASTE_2, PASTE_3, PASTE_4, PASTE_5);
List> rows = gridData.stream().map(r -> List.of(r.get(PASTE_1), r.get(PASTE_2), r.get(PASTE_3), r.get(PASTE_4), r.get(PASTE_5))).toList();
return rowsToString(rows);
}
diff --git a/src/org/labkey/test/tests/component/GridPanelViewTest.java b/src/org/labkey/test/tests/component/GridPanelViewTest.java
index a3992c8ca3..c7452ef834 100644
--- a/src/org/labkey/test/tests/component/GridPanelViewTest.java
+++ b/src/org/labkey/test/tests/component/GridPanelViewTest.java
@@ -19,6 +19,7 @@
import org.labkey.test.components.ui.search.FilterExpressionPanel;
import org.labkey.test.components.ui.search.FilterFacetedPanel;
import org.labkey.test.params.FieldDefinition;
+import org.labkey.test.params.FieldKey;
import org.labkey.test.params.experiment.SampleTypeDefinition;
import org.labkey.test.util.APIUserHelper;
import org.labkey.test.util.ApiPermissionsHelper;
@@ -1022,7 +1023,7 @@ public void testShowAllLabelEditAndUndo()
log(String.format("Change the label of the field '%s' to '%s'.", materialNameField, newFieldLabel));
// Adding the 'Material Source Id / Name' field creates two fields with the label 'Name' in the 'Shown in Grid' panel, make sure the expected one is updated.
- customizeModal.setFieldLabel(materialNameField, 1, newFieldLabel);
+ customizeModal.setFieldLabel(FieldKey.fromParts(materialIDField, materialNameField), newFieldLabel);
checker().fatal().verifyTrue("'Update' button is not enabled, cannot save changes. Fatal error.",
customizeModal.isUpdateGridEnabled());
diff --git a/src/org/labkey/test/util/EscapeUtil.java b/src/org/labkey/test/util/EscapeUtil.java
index 82a67be918..652fef926f 100644
--- a/src/org/labkey/test/util/EscapeUtil.java
+++ b/src/org/labkey/test/util/EscapeUtil.java
@@ -15,9 +15,9 @@
*/
package org.labkey.test.util;
-import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringEscapeUtils;
import org.eclipse.jetty.util.URIUtil;
+import org.labkey.test.params.FieldKey;
import java.net.URLDecoder;
import java.net.URLEncoder;
@@ -119,17 +119,14 @@ public static String decodeUriPath(String path)
return URIUtil.decodePath(path);
}
- private static final String[] ILLEGAL = {"$", "/", "&", "}", "~", ",", "."};
- private static final String[] REPLACEMENT = {"$D", "$S", "$A", "$B", "$T", "$C", "$P"};
-
- static public String fieldKeyEncodePart(String str)
+ public static String fieldKeyEncodePart(String str)
{
- return StringUtils.replaceEach(str, ILLEGAL, REPLACEMENT);
+ return FieldKey.encodePart(str);
}
- static public String fieldKeyDecodePart(String str)
+ public static String fieldKeyDecodePart(String str)
{
- return StringUtils.replaceEach(str, REPLACEMENT, ILLEGAL);
+ return FieldKey.decodePart(str);
}
public static String getTextChoiceValidatorExpression(List options)
diff --git a/src/org/labkey/test/util/TestDataGenerator.java b/src/org/labkey/test/util/TestDataGenerator.java
index 86da97976d..5ca901f3f0 100644
--- a/src/org/labkey/test/util/TestDataGenerator.java
+++ b/src/org/labkey/test/util/TestDataGenerator.java
@@ -15,6 +15,7 @@
*/
package org.labkey.test.util;
+import org.apache.commons.csv.CSVFormat;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
@@ -36,16 +37,16 @@
import org.labkey.remoteapi.query.SelectRowsResponse;
import org.labkey.remoteapi.query.Sort;
import org.labkey.serverapi.reader.TabLoader;
-import org.labkey.serverapi.writer.PrintWriters;
import org.labkey.test.TestFileUtils;
import org.labkey.test.WebTestHelper;
import org.labkey.test.params.FieldDefinition;
+import org.labkey.test.util.data.ColumnNameMapper;
+import org.labkey.test.util.data.TestDataUtils;
import org.labkey.test.util.query.QueryApiHelper;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
-import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
@@ -59,12 +60,11 @@
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.Supplier;
import java.util.regex.Pattern;
-import java.util.stream.Collectors;
import static org.labkey.test.BaseWebDriverTest.ALL_ILLEGAL_QUERY_KEY_CHARACTERS;
-import static org.labkey.test.util.TestDataUtils.REALISTIC_ASSAY_FIELDS;
-import static org.labkey.test.util.TestDataUtils.REALISTIC_SAMPLE_FIELDS;
-import static org.labkey.test.util.TestDataUtils.REALISTIC_SOURCE_FIELDS;
+import static org.labkey.test.util.data.TestDataUtils.REALISTIC_ASSAY_FIELDS;
+import static org.labkey.test.util.data.TestDataUtils.REALISTIC_SAMPLE_FIELDS;
+import static org.labkey.test.util.data.TestDataUtils.REALISTIC_SOURCE_FIELDS;
/**
@@ -72,8 +72,11 @@
*/
public class TestDataGenerator
{
+ private static final String WIDE_CHAR = "👾";
+ private static final char WIDE_PLACEHOLDER = 'Π'; // Wide character can't be picked from the string with 'charAt'
+ private static final String NON_LATIN_STRING = "и안は";
// chose a Character random from this String
- public static final String CHARSET_STRING = "ABCDEFG01234abcdefvxyz~!@#$%^&*()-+=_{}[]|:;\"',.<>";
+ public static final String CHARSET_STRING = "ABCDEFG01234abcdefvxyz~!@#$%^&*()-+=_{}[]|:;\"',.<>" + NON_LATIN_STRING + WIDE_PLACEHOLDER;
public static final String ALPHANUMERIC_STRING = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvxyz";
public static final String DOMAIN_SPECIAL_STRING = "+- _.:&()/";
public static final String ILLEGAL_DOMAIN_NAME_CHARSET = "<>[]{};,`\"~!@#$%^*=|?\\";
@@ -98,7 +101,6 @@ public class TestDataGenerator
private final String _containerPath;
private String _excludedChars;
private boolean _alphaNumericStr;
- private final TestDataUtils.TsvQuoter _tsvQuoter = new TestDataUtils.TsvQuoter(',');
/**
* use TestDataGenerator to generate data to a specific fieldSet
@@ -121,24 +123,10 @@ public TestDataGenerator(FieldDefinition.LookupInfo lookupInfo)
public static File writeCsvFile(List fields, List> entityData, String fileName) throws IOException
{
- List> fileData = new ArrayList<>();
- List fieldNamesForFile = fields.stream().map(FieldDefinition::getName).collect(Collectors.toList());
- for (Map row : entityData)
- {
- Map fileRow = new HashMap<>();
- for (FieldDefinition field : fields)
- {
- String key = field.getLabel() == null ? field.getName() : field.getLabel();
- Object value = row.get(key);
- Object valueToWrite = value;
- if (value instanceof List>)
- valueToWrite = StringUtils.join((List>)value, ",");
- fileRow.put(field.getName(), valueToWrite);
- }
- fileData.add(fileRow);
- }
- var fileContents = TestDataUtils.csvStringFromRowMaps(fileData, fieldNamesForFile, true);
- return TestFileUtils.writeTempFile(fileName, fileContents);
+ List> rows = TestDataUtils.replaceColumnHeaders(
+ TestDataUtils.rowListsFromMaps(entityData), ColumnNameMapper.labelToName(fields)); // Use field names
+
+ return TestDataUtils.writeRowsToCsv(fileName, rows);
}
public static List> generateEntityData(List fields, String nameField, String namePrefix, int startingCount, int size, boolean addLineage, boolean includeAliquots, String queryName, boolean forGridInsert)
@@ -162,7 +150,7 @@ public static List> generateEntityData(List
{
if (!fieldDefinition.getName().equalsIgnoreCase(nameField)) // Name already set
{
- String key = fieldDefinition.getLabel() != null ? fieldDefinition.getLabel() : FieldDefinition.labelFromName(fieldDefinition.getName());
+ String key = fieldDefinition.getEffectiveLabel();
if (fieldDefinition.getType().equals(FieldDefinition.ColumnType.Date))
entityData.put(key, UI_DATE_FORMAT.get().format(TestDateUtils.diffFromTodaysDate(Calendar.HOUR, i * 24)));
else if (fieldDefinition.getType().equals(FieldDefinition.ColumnType.DateAndTime))
@@ -265,14 +253,6 @@ else if (i % 5 == 3)
return fields;
}
- public static void setFieldCaptionsFromNames(List fields)
- {
- fields.forEach(f -> {
- if (f.getLabel() == null)
- f.setLabel(FieldDefinition.labelFromName(f.getName()));
- });
- }
-
public String getSchema()
{
return _schemaName;
@@ -468,6 +448,12 @@ private Supplier