From d6209d5ca91c59010fabdaec9e46f74b2542174e Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Mon, 5 May 2025 16:57:36 -0700 Subject: [PATCH 01/34] Improve synchronization of editable grid column operations --- .../labkey/test/components/react/Tabs.java | 44 +++++++--- .../ui/entities/EntityInsertPanel.java | 51 ++++++++--- .../components/ui/grids/EditableGrid.java | 84 +++++++++++++------ src/org/labkey/test/params/FieldKey.java | 84 ++++++++++++++----- .../test/params/list/IntListDefinition.java | 19 ++++- src/org/labkey/test/util/EscapeUtil.java | 13 ++- .../test/util/query/QueryApiHelper.java | 4 + 7 files changed, 222 insertions(+), 77 deletions(-) diff --git a/src/org/labkey/test/components/react/Tabs.java b/src/org/labkey/test/components/react/Tabs.java index 3336a8c9c2..f0f36e09bf 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 tabKey) + { + return elementCache().findTab(tabKey); + } + 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,11 @@ public List getTabText() .stream().map(WebElement::getText).toList(); } + public String getSelectedTabText() + { + return elementCache().findSelectedTab().getText(); + } + @Override protected ElementCache newElementCache() { @@ -102,6 +117,11 @@ public ElementCache() } } + protected WebElement findSelectedTab() + { + return tabLoc.withAttribute("aria-selected", "true").findElement(this); + } + List findAllTabs() { if (tabs.isEmpty()) @@ -111,31 +131,31 @@ List findAllTabs() return tabs; } - WebElement findTab(String tabText) + WebElement findTab(String tabKey) { - if (!tabMap.containsKey(tabText)) + if (!tabMap.containsKey(tabKey)) { WebElement tabEl; try { // Use 'containing' here because it may happen that the counts get loaded into the tabs after the call to this method, // which causes the name to change from, say 'Included Samples' to 'Included Samples (7)'. - tabEl = tabLoc.containing(tabText).findElement(tabList); + tabEl = tabLoc.withAttribute("data-event-key", tabKey).findElement(tabList); } catch (NoSuchElementException ex) { throw new NoSuchElementException(String.format("'%s' not among available tabs: %s", - tabText, getWrapper().getTexts(findAllTabs())), ex); + tabKey, getWrapper().getTexts(findAllTabs())), ex); } - tabMap.put(tabText, tabEl); + tabMap.put(tabKey, tabEl); } - return tabMap.get(tabText); + return tabMap.get(tabKey); } // 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 +163,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/EntityInsertPanel.java b/src/org/labkey/test/components/ui/entities/EntityInsertPanel.java index 4852013037..620d8353ba 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().doAndWaitForUpdate(() -> elementCache().addParent.doMenuAction(parentType)); return this; } @@ -89,16 +93,20 @@ 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().doAndWaitForUpdate(() -> elementCache().addSource.doMenuAction(sourceType)); return this; } @@ -317,7 +325,7 @@ public EntityInsertPanel showGrid() WebDriverWrapper.waitFor(this::isGridVisible, "the grid did bot become visible", 2000); } - elementCache().grid.waitForLoaded(); + elementCache().grid.waitForReady(); return this; } public ResponsiveGrid uploadFileExpectingPreview(File file) @@ -407,14 +415,14 @@ protected class ElementCache extends Component.ElementCache WebElement targetTab = navTab.findWhenNeeded(this); MultiMenu addParent = new MultiMenu.MultiMenuFinder(getDriver()).containsText("Add Parent") - .timeout(WAIT_FOR_JAVASCRIPT).refindWhenNeeded(); + .timeout(WAIT_FOR_JAVASCRIPT).refindWhenNeeded(this); MultiMenu addSource = new MultiMenu.MultiMenuFinder(getDriver()).containsText("Add Source") - .timeout(WAIT_FOR_JAVASCRIPT).refindWhenNeeded(); + .timeout(WAIT_FOR_JAVASCRIPT).refindWhenNeeded(this); RadioButton allowMergeRadio = RadioButton.RadioButton(Locator.radioButtonByNameAndValue("insertOption", "true")).findWhenNeeded(this); RadioButton notAllowMergeRadio = RadioButton.RadioButton(Locator.radioButtonByNameAndValue("insertOption", "false")).findWhenNeeded(this); - EditableGrid grid = new EditableGrid.EditableGridFinder(_driver).timeout(WAIT_FOR_JAVASCRIPT).findWhenNeeded(); + EditableGrid grid = new EditableGrid.EditableGridFinder(_driver).timeout(WAIT_FOR_JAVASCRIPT).findWhenNeeded(this); private Optional optionalGrid() { @@ -423,7 +431,7 @@ private Optional optionalGrid() private Optional optionalFileUploadPanel() { - return new FileUploadPanel.FileUploadPanelFinder(getDriver()).findOptional(); + return new FileUploadPanel.FileUploadPanelFinder(getDriver()).findOptional(this); } protected FileUploadPanel fileUploadPanel() @@ -436,7 +444,7 @@ protected FileUploadPanel fileUploadPanel() public boolean hasTabs() { - return Locator.tagWithClassContaining("ul", "list-group").existsIn(elementCache()); + return Locator.tagWithClassContaining("ul", "list-group").existsIn(this); } } @@ -463,3 +471,26 @@ protected Locator locator() } } } + +class ReadOnlyMenu extends MultiMenu +{ + private final String _entityType; + + ReadOnlyMenu(MultiMenu menu, String entityType) + { + super(menu.getComponentElement(), menu.getWrapper().getDriver()); + _entityType = entityType; + } + + @Override + public void doMenuAction(String toggleText, String menuAction) + { + throw new UnsupportedOperationException("Use add%s()".formatted(_entityType)); + } + + @Override + public void doMenuAction(String menuAction) + { + throw new UnsupportedOperationException("Use add%s()".formatted(_entityType)); + } +} \ No newline at end of file diff --git a/src/org/labkey/test/components/ui/grids/EditableGrid.java b/src/org/labkey/test/components/ui/grids/EditableGrid.java index ca33d6a355..b253198cf9 100644 --- a/src/org/labkey/test/components/ui/grids/EditableGrid.java +++ b/src/org/labkey/test/components/ui/grids/EditableGrid.java @@ -6,6 +6,7 @@ import org.labkey.test.Locator; import org.labkey.test.WebDriverWrapper; import org.labkey.test.components.Component; +import org.labkey.test.components.UpdatingComponent; import org.labkey.test.components.WebDriverComponent; import org.labkey.test.components.html.Checkbox; import org.labkey.test.components.html.Input; @@ -14,6 +15,7 @@ import org.labkey.test.components.ui.entities.EntityBulkInsertDialog; import org.labkey.test.components.ui.entities.EntityBulkUpdateDialog; import org.labkey.test.params.FieldDefinition; +import org.labkey.test.params.FieldKey; import org.labkey.test.util.selenium.ScrollUtils; import org.labkey.test.util.selenium.WebElementUtils; import org.openqa.selenium.By; @@ -35,11 +37,13 @@ import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.TimeZone; import java.util.stream.Collectors; @@ -51,7 +55,7 @@ import static org.labkey.test.util.selenium.ScrollUtils.Alignment.center; import static org.labkey.test.util.selenium.WebDriverUtils.MODIFIER_KEY; -public class EditableGrid extends WebDriverComponent +public class EditableGrid extends WebDriverComponent implements UpdatingComponent { public static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); public static final String SELECT_COLUMN_HEADER = ""; @@ -138,7 +138,7 @@ protected Integer getColumnIndex(String fieldLabel) public EditableGrid removeColumn(String fieldLabel) { - doAndWaitForUpdate(() -> + doAndWaitForColumnUpdate(() -> { WebElement headerCell = elementCache().getGridCellHeader(fieldLabel); Locator.byClass("fa-chevron-circle-down").findElement(headerCell).click(); @@ -230,7 +230,7 @@ private List getRows() return Locators.rows.findElements(elementCache().table); } - public List> getGridData(String... fieldLabels) + public List> getGridDataByLabel(String... fieldLabels) { List> gridData = new ArrayList<>(); @@ -277,7 +277,7 @@ public List> getGridData(String... fieldLabels) public List getColumnDataByLabel(String fieldLabel) { - return getGridData(fieldLabel).stream().map(a-> a.get(fieldLabel)).collect(Collectors.toList()); + return getGridDataByLabel(fieldLabel).stream().map(a-> a.get(fieldLabel)).collect(Collectors.toList()); } private WebElement getRow(int index) @@ -1110,8 +1110,7 @@ private void doAndWaitForRowCountUpdate(Runnable func) /** * Wait for column count to change after the provided action */ - @Override - public void doAndWaitForUpdate(Runnable func) + public void doAndWaitForColumnUpdate(Runnable func) { int initialCount = elementCache().findColumnHeaders().size(); @@ -1141,7 +1140,7 @@ protected class ElementCache extends Component.ElementCache public WebElement getGridCellHeader(String label) { - return Locator.byClass("grid-header-cell").withDescendant(Locator.tagContainingText("span", label)).findWhenNeeded(table); + return Locator.byClass("grid-header-cell").withDescendant(Locator.tagContainingText("span", label)).findElement(table); } private List columnHeaders = Collections.emptyList(); 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); } From efcea9e4acbcffb2f481c2722be4164d68a62873 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Tue, 6 May 2025 15:11:13 -0700 Subject: [PATCH 03/34] Fix tabs and unify CSV/TSV generation --- .../labkey/test/components/react/Tabs.java | 16 ++-- .../labkey/test/util/TestDataGenerator.java | 95 ++++--------------- src/org/labkey/test/util/TestDataUtils.java | 30 ++++-- 3 files changed, 47 insertions(+), 94 deletions(-) diff --git a/src/org/labkey/test/components/react/Tabs.java b/src/org/labkey/test/components/react/Tabs.java index f0f36e09bf..cb8e1bae0c 100644 --- a/src/org/labkey/test/components/react/Tabs.java +++ b/src/org/labkey/test/components/react/Tabs.java @@ -53,9 +53,9 @@ public WebDriver getDriver() return _driver; } - public WebElement findTab(String tabKey) + public WebElement findTab(String tabText) { - return elementCache().findTab(tabKey); + return elementCache().findTab(tabText); } public WebElement findPanelForTab(String tabText) @@ -131,25 +131,25 @@ List findAllTabs() return tabs; } - WebElement findTab(String tabKey) + WebElement findTab(String tabText) { - if (!tabMap.containsKey(tabKey)) + if (!tabMap.containsKey(tabText)) { WebElement tabEl; try { // Use 'containing' here because it may happen that the counts get loaded into the tabs after the call to this method, // which causes the name to change from, say 'Included Samples' to 'Included Samples (7)'. - tabEl = tabLoc.withAttribute("data-event-key", tabKey).findElement(tabList); + tabEl = tabLoc.containing(tabText).findElement(tabList); } catch (NoSuchElementException ex) { throw new NoSuchElementException(String.format("'%s' not among available tabs: %s", - tabKey, getWrapper().getTexts(findAllTabs())), ex); + tabText, getWrapper().getTexts(findAllTabs())), ex); } - tabMap.put(tabKey, tabEl); + tabMap.put(tabText, tabEl); } - return tabMap.get(tabKey); + return tabMap.get(tabText); } // Tab panels can be updated and changed when flipping between tabs. Don't persist the panel element find it each time. diff --git a/src/org/labkey/test/util/TestDataGenerator.java b/src/org/labkey/test/util/TestDataGenerator.java index b624fa2555..62dacdf92f 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,7 +37,6 @@ 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; @@ -45,7 +45,6 @@ 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; @@ -98,7 +97,7 @@ public class TestDataGenerator private final String _containerPath; private String _excludedChars; private boolean _alphaNumericStr; - private final TestDataUtils.TsvQuoter _tsvQuoter = new TestDataUtils.TsvQuoter(','); + private final CSVFormat _format = CSVFormat.DEFAULT; /** * use TestDataGenerator to generate data to a specific fieldSet @@ -648,31 +647,11 @@ public boolean randomBoolean() return ThreadLocalRandom.current().nextBoolean(); } - public StringBuilder writeTsvHeaders() + private @NotNull List getFieldsForFile() { - StringBuilder builder = new StringBuilder(); List fieldNames = new ArrayList<>(_columns.keySet()); fieldNames.removeAll(_autoGeneratedFields); - - List headers = new ArrayList<>(); - for (String fieldName : fieldNames) - headers.add(getTsvQuotedValue(fieldName, _tsvQuoter)); - - builder.append(String.join("\t", headers)); - builder.append("\n"); - - return builder; - } - - public static String getTsvQuotedValue(Object value, TestDataUtils.TsvQuoter tsvQuoter) - { - String strVal; - if (value instanceof String s) - strVal = tsvQuoter.quoteValue(s); - else - strVal = value != null ? String.valueOf(value) : ""; - - return strVal; + return fieldNames; } public List getPasteColumnValues(String fieldName) @@ -681,70 +660,29 @@ public List getPasteColumnValues(String fieldName) for (Map row : _rows) { Object value = row.get(fieldName); - String strVal = getTsvQuotedValue(value, _tsvQuoter); + String strVal = _format.format(value); values.add(strVal); } return values; } - public String rowToString(List fieldNames, Map row) - { - StringBuilder builder = new StringBuilder(); - List values = new ArrayList<>(); - String firstField = fieldNames.get(0); - for (String fieldName : fieldNames) - { - Object value = row.get(fieldName); - String strVal = getTsvQuotedValue(value, _tsvQuoter); - - if (strVal.startsWith("#") && fieldName.equals(firstField)) // don't generate comment lines - strVal = "\"" + strVal + "\""; - - values.add(strVal); - } - builder.append(String.join("\t", values)); - builder.append("\n"); - - return builder.toString(); - } - /** - * generates tsv-formatted content using the rows in the current instance; - * @return TSV formatted representation of generated rows + * generates tabular text content (CSV or TSV) using the rows in the current instance; + * @return CSV formatted representation of generated rows */ public String writeTsvContents() { - List fieldNames = new ArrayList<>(_columns.keySet()); - fieldNames.removeAll(_autoGeneratedFields); - StringBuilder builder = writeTsvHeaders(); - - for (Map row : _rows) - { - builder.append(rowToString(fieldNames, row)); - } - return builder.toString(); + return TestDataUtils.stringFromRowMaps(_rows, getFieldsForFile(), true, _format); } public File writeGeneratedDataToFile(int numberOfRowsToGenerate, String fileName) throws IOException { - String headers = writeTsvHeaders().toString(); - File file = new File(TestFileUtils.getTestTempDir(), fileName); - FileUtils.forceMkdirParent(file); - - try(PrintWriter stream = PrintWriters.getPrintWriter(file)) + List> rows = new ArrayList<>(); + for (int i = 0; i < numberOfRowsToGenerate; i++) { - List fieldNames = new ArrayList<>(_columns.keySet()); - fieldNames.removeAll(_autoGeneratedFields); - stream.write(headers); - - for (int i = 0; i < numberOfRowsToGenerate; i++) - { - Map newRow = generateRow(); - var rowMapString = rowToString(fieldNames, newRow); - stream.write(rowMapString); - } + rows.add(generateRow()); } - return file; + return TestDataUtils.writeRowsToFile(fileName, TestDataUtils.rowListsFromMaps(rows, getFieldsForFile(), true, true), _format); } public File writeGeneratedDataToExcel(int numberOfRowsToGenerate, String sheetName, String fileName) throws IOException @@ -782,10 +720,10 @@ public File writeGeneratedDataToExcel(int numberOfRowsToGenerate, String sheetNa } /** - * Creates a file containing the contents of the current rows, formatted in TSV. + * Creates a file containing the contents of the current rows, formatted in CSV. * The file is written to the test temp dir - * @param fileName the name of the file, e.g. 'testDataFileForMyTest.tsv' - * @return File object pointing at created TSV + * @param fileName the name of the file, e.g. 'testDataFileForMyTest.csv' + * @return File object pointing at created file */ public File writeData(String fileName) { @@ -795,8 +733,7 @@ public File writeData(String fileName) } catch (IOException e) { - e.printStackTrace(); - return null; + throw new RuntimeException(e); } } diff --git a/src/org/labkey/test/util/TestDataUtils.java b/src/org/labkey/test/util/TestDataUtils.java index c6e9886e3e..b3f3ffc375 100644 --- a/src/org/labkey/test/util/TestDataUtils.java +++ b/src/org/labkey/test/util/TestDataUtils.java @@ -5,6 +5,7 @@ import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; import org.labkey.serverapi.reader.TabLoader; import org.labkey.test.TestFileUtils; import org.labkey.test.params.FieldDefinition; @@ -148,16 +149,21 @@ public static List> rowMapsFromCsv(String tsvString) throws } } + public static String stringFromRowMaps(List> rowMaps, List columns, boolean includeHeaders, CSVFormat format) + { + return stringFromRows(rowListsFromMaps(rowMaps, columns, includeHeaders, true), format); + } + public static String tsvStringFromRowMaps(List> rowMaps, List columns, boolean includeHeaders) { - return writeRowsToString(rowListsFromMaps(rowMaps, columns, includeHeaders, true), CSVFormat.TDF); + return stringFromRowMaps(rowMaps, columns, includeHeaders, CSVFormat.TDF); } public static String csvStringFromRowMaps(List> rowMaps, List columns, boolean includeHeaders) { - return writeRowsToString(rowListsFromMaps(rowMaps, columns, includeHeaders, true), CSVFormat.DEFAULT); + return stringFromRowMaps(rowMaps, columns, includeHeaders, CSVFormat.DEFAULT); } @@ -204,11 +210,21 @@ public static List> rowListsFromMaps(List> rowM } public static File writeRowsToTsv(String fileName, List> rows) throws IOException + { + return writeRowsToFile(fileName, rows, CSVFormat.TDF); + } + + public static File writeRowsToFile(String fileName, List> rows) throws IOException + { + return writeRowsToFile(fileName, rows, CSVFormat.DEFAULT); + } + + public static @NotNull File writeRowsToFile(String fileName, List> rows, CSVFormat format) throws IOException { File file = new File(TestFileUtils.getTestTempDir(), fileName); FileUtils.forceMkdirParent(file); - try (CSVPrinter printer = new CSVPrinter(new FileWriter(file, StandardCharsets.UTF_8), CSVFormat.TDF)) { + try (CSVPrinter printer = new CSVPrinter(new FileWriter(file, StandardCharsets.UTF_8), format)) { for (List row : rows) { printer.printRecord(row); @@ -218,17 +234,17 @@ public static File writeRowsToTsv(String fileName, List> rows) thro return file; } - public static String writeRowsToTsvString(List> rows) throws IOException + public static String stringFromRows(List> rows) { - return writeRowsToString(rows, CSVFormat.TDF); + return stringFromRows(rows, CSVFormat.DEFAULT); } - public static String writeRowsToString(List> rows, CSVFormat format) + public static String stringFromRows(List> rows, CSVFormat format) { StringWriter stringWriter = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(stringWriter, format)) { - for (List row : rows) + for (List row : rows) { printer.printRecord(row); } From 63cebedf3c401604bcc9fce238314f016553fd74 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Wed, 7 May 2025 12:06:09 -0700 Subject: [PATCH 04/34] Use FieldDefinition.getEffectiveLabel --- .../ui/entities/EntityBulkInsertDialog.java | 2 +- .../components/ui/grids/EditableGrid.java | 2 +- .../test/stress/RequestInfoTsvWriter.java | 2 +- src/org/labkey/test/tests/ClientAPITest.java | 4 +- src/org/labkey/test/tests/FilterTest.java | 2 +- .../tests/SampleTypeNameExpressionTest.java | 2 +- .../labkey/test/util/TestDataGenerator.java | 40 +++--------- .../test/util/data/ColumnNameMapper.java | 61 +++++++++++++++++++ .../test/util/{ => data}/TestDataUtils.java | 41 ++++++++++++- 9 files changed, 116 insertions(+), 40 deletions(-) create mode 100644 src/org/labkey/test/util/data/ColumnNameMapper.java rename src/org/labkey/test/util/{ => data}/TestDataUtils.java (91%) diff --git a/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java b/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java index d2bdff4ce9..b0c4f625bf 100644 --- a/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java +++ b/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java @@ -225,7 +225,7 @@ public void setInsertFieldValues(List fields, Map >data, List { Map rowData = data.get(i); for (FieldDefinition field : fields) { - String key = field.getLabel() != null ? field.getLabel() : field.getName(); + String key = field.getEffectiveLabel(); Object value = rowData.get(key); if (value != null) setCellValue(i, key, value); 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 71d21e438e..c2c5b7a850 100644 --- a/src/org/labkey/test/tests/SampleTypeNameExpressionTest.java +++ b/src/org/labkey/test/tests/SampleTypeNameExpressionTest.java @@ -60,7 +60,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/util/TestDataGenerator.java b/src/org/labkey/test/util/TestDataGenerator.java index 62dacdf92f..ddb11dfe45 100644 --- a/src/org/labkey/test/util/TestDataGenerator.java +++ b/src/org/labkey/test/util/TestDataGenerator.java @@ -40,6 +40,8 @@ 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; @@ -61,9 +63,9 @@ 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; /** @@ -120,24 +122,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.rowListsFromMaps(entityData); + TestDataUtils.replaceColumnHeaders(rows, ColumnNameMapper.labelToName(fields)); // Use field names + + return TestDataUtils.writeRowsToFile(fileName, rows); } public static List> generateEntityData(List fields, String nameField, String namePrefix, int startingCount, int size, boolean addLineage, boolean includeAliquots, String queryName, boolean forGridInsert) @@ -161,7 +149,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)) @@ -264,14 +252,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; diff --git a/src/org/labkey/test/util/data/ColumnNameMapper.java b/src/org/labkey/test/util/data/ColumnNameMapper.java new file mode 100644 index 0000000000..e8160139e3 --- /dev/null +++ b/src/org/labkey/test/util/data/ColumnNameMapper.java @@ -0,0 +1,61 @@ +package org.labkey.test.util.data; + +import org.labkey.test.params.FieldDefinition; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +public class ColumnNameMapper implements Function +{ + private final Function _nameMapper; + private final Map _mappingOverrides = new HashMap<>(); + + public ColumnNameMapper(Function nameMapper) + { + _nameMapper = nameMapper; + } + + public ColumnNameMapper(Map mappings) + { + this(Function.identity()); + _mappingOverrides.putAll(mappings); + } + + public static ColumnNameMapper labelToName(Collection fields) + { + return new ColumnNameMapper(label -> fields.stream() + .collect(Collectors.toMap(FieldDefinition::getEffectiveLabel, FieldDefinition::getName)) + .getOrDefault(label, label)); + } + + public static ColumnNameMapper nameToLabel(Collection fields) + { + Map map = fields.stream().collect(Collectors.toMap(FieldDefinition::getName, FieldDefinition::getEffectiveLabel)); + return new ColumnNameMapper(name -> Objects.requireNonNullElseGet(map.get(name), () -> FieldDefinition.labelFromName(name))); + } + + public ColumnNameMapper addMapping(String from, String to) + { + _mappingOverrides.put(from, to); + return this; + } + + @Override + public String apply(String s) + { + List> options = List.of( + () -> _mappingOverrides.get(s), + () -> _nameMapper.apply(s)); + return options.stream() + .map(Supplier::get) + .filter(Objects::nonNull) + .findFirst() + .orElse(s); + } +} diff --git a/src/org/labkey/test/util/TestDataUtils.java b/src/org/labkey/test/util/data/TestDataUtils.java similarity index 91% rename from src/org/labkey/test/util/TestDataUtils.java rename to src/org/labkey/test/util/data/TestDataUtils.java index b3f3ffc375..e16648e014 100644 --- a/src/org/labkey/test/util/TestDataUtils.java +++ b/src/org/labkey/test/util/data/TestDataUtils.java @@ -1,4 +1,4 @@ -package org.labkey.test.util; +package org.labkey.test.util.data; import org.apache.commons.csv.CSVFormat; import org.apache.commons.csv.CSVPrinter; @@ -9,6 +9,7 @@ import org.labkey.serverapi.reader.TabLoader; import org.labkey.test.TestFileUtils; import org.labkey.test.params.FieldDefinition; +import org.labkey.test.util.TestDataGenerator; import java.io.File; import java.io.FileWriter; @@ -17,9 +18,14 @@ import java.io.StringWriter; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.function.Function; import java.util.function.Supplier; +import java.util.stream.Collectors; public class TestDataUtils { @@ -172,13 +178,28 @@ public static List> rowListsFromMaps(List> rowM return rowListsFromMaps(rowMaps, columns, false, true); } + public static List> rowListsFromMaps(List> rowMaps, List columns, boolean includeHeaders) + { + return rowListsFromMaps(rowMaps, columns, includeHeaders, true); + } + + public static List> rowListsFromMaps(List> rowMaps) + { + Set columns = new LinkedHashSet<>(); + for (Map row : rowMaps) + { + columns.addAll(row.keySet()); + } + return rowListsFromMaps(rowMaps, new ArrayList<>(columns), true, true); + } + /** * convert a List of Map to a list of List * @param rowMaps Source data * @param columns keys contained in each map, will copy values associated with them to the resulting list * @return A List> containing values */ - public static List> rowListsFromMaps(List> rowMaps, List columns, boolean includeHeaders, boolean preserveEmptyValues) + public static List> rowListsFromMaps(List> rowMaps, List columns, boolean includeHeaders, boolean allowMissingValues) { List> lists = new ArrayList<>(); @@ -197,11 +218,15 @@ public static List> rowListsFromMaps(List> rowM var value = rowMap.get(column); if (value == null) { - if (preserveEmptyValues) + if (allowMissingValues) value = ""; else throw new IllegalArgumentException("Missing value for column '" + column + "' in row: " + rowMap); } + if (value instanceof Collection c) + { + value = c.stream().map(Object::toString).collect(Collectors.joining(",")); + } rowList.add(value.toString()); } lists.add(rowList); @@ -209,6 +234,16 @@ public static List> rowListsFromMaps(List> rowM return lists; } + public static List> replaceColumnHeaders(List> rowLists, Function columnMapper) + { + List headerRow = rowLists.get(0); + for (int i = 1; i < rowLists.size(); i++) + { + headerRow.set(i, columnMapper.apply(headerRow.get(i))); + } + return rowLists; + } + public static File writeRowsToTsv(String fileName, List> rows) throws IOException { return writeRowsToFile(fileName, rows, CSVFormat.TDF); From b3f3f89552df41a062ec2070b06483b86d4ec685 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Wed, 7 May 2025 15:17:41 -0700 Subject: [PATCH 05/34] DetailTableEdit updates to accept fielkeys and names --- src/org/labkey/test/WebDriverWrapper.java | 2 +- .../components/ui/grids/DetailTableEdit.java | 264 +++++++++++------- .../labkey/test/util/TestDataGenerator.java | 2 +- 3 files changed, 163 insertions(+), 105 deletions(-) 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/ui/grids/DetailTableEdit.java b/src/org/labkey/test/components/ui/grids/DetailTableEdit.java index 432284e7ca..2450e528a1 100644 --- a/src/org/labkey/test/components/ui/grids/DetailTableEdit.java +++ b/src/org/labkey/test/components/ui/grids/DetailTableEdit.java @@ -28,7 +28,11 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Supplier; import java.util.stream.Collectors; import static org.labkey.test.WebDriverWrapper.WAIT_FOR_JAVASCRIPT; @@ -85,89 +89,84 @@ public DetailTableEdit adjustChangeCounter(int change) return this; } - public boolean isFieldPresent(String fieldLabel) + public boolean isFieldPresent(String nameOrLabel) { - return elementCache().valueCellWithLabel(fieldLabel) != null; + try + { + elementCache().findValueCell(nameOrLabel); + return true; + } + catch (NoSuchElementException e) + { + return false; + } } /** * Check to see if a field is editable. Could be state dependent, that is it returns false if the field is * loading but if checked later could return true. * - * @param fieldLabel The label of the field to check. + * @param nameOrLabel The name or label of the field to check. * @return True if it is false otherwise. **/ - public boolean isFieldEditable(String fieldLabel) + public boolean isFieldEditable(String nameOrLabel) { // TODO Could put a check here to see if a field is loading then return false, or wait. - WebElement fieldValueElement = elementCache().valueCellWithLabel(fieldLabel); + WebElement fieldValueElement = elementCache().findValueCell(nameOrLabel); return isEditableField(fieldValueElement); } private boolean isEditableField(WebElement element) { // If the div does not have the class value of 'field__un-editable' then it is an editable field. - return Locator.css("div:not(.field__un-editable)").findOptionalElement(element).isPresent(); + return !Locator.byClass("field__un-editable").existsIn(element); } /** * Get the value of a read only field. * - * @param fieldLabel The label of the field to get. + * @param nameOrLabel The name or label of the field to get. * @return The value in the field. **/ - public String getReadOnlyField(String fieldLabel) + public String getReadOnlyField(String nameOrLabel) { - WebElement fieldValueElement = elementCache().valueCellWithLabel(fieldLabel); - return fieldValueElement.findElement(By.xpath("./div/*")).getText(); + WebElement fieldValueElement = elementCache().findValueCell(nameOrLabel); + return Locator.xpath("./div/*").findElement(fieldValueElement).getText(); } /** * Get the value of a text field. * - * @param fieldLabel The label of the field to get. + * @param nameOrLabel The name or label of the field to get. * @return The value in the field. **/ - public String getTextField(String fieldLabel) + public String getTextField(String nameOrLabel) { - WebElement fieldValueElement = elementCache().valueCellWithLabel(fieldLabel); - WebElement textElement = fieldValueElement.findElement(By.xpath("./div/div/*")); - if (textElement.getTagName().equalsIgnoreCase("textarea")) - return textElement.getText(); - else - return textElement.getAttribute("value"); + return elementCache().findInput(nameOrLabel).get(); } /** * Set a text field. * - * @param fieldLabel The label of the field to set. + * @param nameOrLabel The name or label of the field to set. * @param value The value to set the field to. * @return A reference to this editable detail table. **/ - public DetailTableEdit setTextField(String fieldLabel, String value) + public DetailTableEdit setTextField(String nameOrLabel, String value) { - if (isFieldEditable(fieldLabel)) + if (isFieldEditable(nameOrLabel)) { - WebElement fieldValueElement = elementCache().valueCellWithLabel(fieldLabel); - - WebElement editableElement = fieldValueElement.findElement(By.xpath("./div/div/*")); - String elementType = editableElement.getTagName().toLowerCase().trim(); - - switch (elementType) - { - case "textarea": - case "input": - editableElement.clear(); - WebDriverWrapper.waitFor(()->editableElement.getText().isEmpty(), 500); - editableElement.sendKeys(value); - break; - default: - throw new NoSuchElementException("This doesn't look like an 'input' or 'textarea' element, are you sure you are calling the correct method?"); - } + elementCache().findInput(nameOrLabel); + WebElement fieldValueElement = elementCache().findValueCell(nameOrLabel); + + WebElement editableElement = Locator.xpath("./div/div/*[self::input or self::textarea]").findElement(fieldValueElement); + getWrapper().setFormElement(editableElement, value); + editableElement.clear(); + WebDriverWrapper.waitFor(()->editableElement.getText().isEmpty(), 500); + editableElement.sendKeys(value); } else { - throw new IllegalArgumentException("Field with label '" + fieldLabel + "' is read-only. This field can not be set."); + throw new IllegalArgumentException("Field with label '" + nameOrLabel + "' is read-only. This field can not be set."); } _changeCounter++; @@ -199,16 +198,16 @@ public DetailTableEdit setTextareaByFieldName(String fieldName, String value) /** * Get the value of a boolean field. * - * @param fieldLabel The label of the field to get. + * @param nameOrLabel The name or label of the field to get. * @return The value of the field. **/ - public boolean getBooleanField(String fieldLabel) + public boolean getBooleanField(String nameOrLabel) { // The text used in the field label and the value of the name attribute in the checkbox don't always have the same case. - WebElement editableElement = Locator.tag("input").findElement(elementCache().valueCellWithLabel(fieldLabel)); + WebElement editableElement = Locator.tag("input").findElement(elementCache().findValueCell(nameOrLabel)); String elementType = editableElement.getDomAttribute("type").toLowerCase().trim(); - Assert.assertEquals(String.format("Field '%s' is not a checkbox. Cannot be get true/false value.", fieldLabel), "checkbox", elementType); + Assert.assertEquals(String.format("Field '%s' is not a checkbox. Cannot be get true/false value.", nameOrLabel), "checkbox", elementType); return new Checkbox(editableElement).isChecked(); } @@ -216,21 +215,21 @@ public boolean getBooleanField(String fieldLabel) /** * Set a boolean field (a checkbox). * - * @param fieldLabel The label of the field to set. + * @param nameOrLabel The label of the field to set. * @param value True will check it, false will uncheck it. * @return A reference to this editable detail table. **/ - public DetailTableEdit setBooleanField(String fieldLabel, boolean value) + public DetailTableEdit setBooleanField(String nameOrLabel, boolean value) { - WebElement fieldValueElement = elementCache().valueCellWithLabel(fieldLabel); - Assert.assertTrue(String.format("Field '%s' is not editable and cannot be set.", fieldLabel), isEditableField(fieldValueElement)); + WebElement fieldValueElement = elementCache().findValueCell(nameOrLabel); + Assert.assertTrue(String.format("Field '%s' is not editable and cannot be set.", nameOrLabel), isEditableField(fieldValueElement)); getWrapper().scrollIntoView(fieldValueElement); WebElement editableElement = fieldValueElement.findElement(By.xpath("./div/div/input")); String elementType = editableElement.getDomAttribute("type").toLowerCase().trim(); - Assert.assertEquals(String.format("Field '%s' is not a checkbox. Cannot be set to true/false.", fieldLabel), "checkbox", elementType); + Assert.assertEquals(String.format("Field '%s' is not a checkbox. Cannot be set to true/false.", nameOrLabel), "checkbox", elementType); Checkbox checkbox = new Checkbox(editableElement); @@ -243,24 +242,24 @@ public DetailTableEdit setBooleanField(String fieldLabel, boolean value) /** * Get the value of an int field. You could also call getTextField * - * @param fieldLabel The label of the field to get. + * @param nameOrLabel The name or label of the field to get. * @return The value of the field as an int. **/ - public int getIntField(String fieldLabel) + public int getIntField(String nameOrLabel) { - return Integer.getInteger(getTextField(fieldLabel)); + return Integer.getInteger(getTextField(nameOrLabel)); } /** * Set an int field. * - * @param fieldLabel The label of the field to set. + * @param nameOrLabel The name or label of the field to set. * @param value The int value to set the field to. * @return A reference to this editable detail table. **/ - public DetailTableEdit setIntField(String fieldLabel, int value) + public DetailTableEdit setIntField(String nameOrLabel, int value) { - return setTextField(fieldLabel, Integer.toString(value)); + return setTextField(nameOrLabel, Integer.toString(value)); } public FileUploadField getFileField(String fieldLabel) @@ -268,18 +267,18 @@ public FileUploadField getFileField(String fieldLabel) return elementCache().fileField(fieldLabel); } - public DetailTableEdit setFileField(String fieldLabel, File file) + public DetailTableEdit setFileField(String nameOrLabel, File file) { - getFileField(fieldLabel) + getFileField(nameOrLabel) .setFile(file); _changeCounter++; return this; } - public DetailTableEdit removeFileField(String fieldLabel) + public DetailTableEdit removeFileField(String nameOrLabel) { - getFileField(fieldLabel).removeFile(); + getFileField(nameOrLabel).removeFile(); _changeCounter++; return this; @@ -291,50 +290,49 @@ public boolean isFileFieldBlank(String fieldLabel) .hasAttachedFile(); } - public FilteringReactSelect getSelectField(String fieldLabel) + public FilteringReactSelect getSelectField(String nameOrLabel) { - return elementCache().findSelect(fieldLabel); + return elementCache().findSelect(nameOrLabel); } /** * Get the value of a select field. * - * @param fieldLabel The label of the field to get. + * @param nameOrLabel The name or label of the field to get. * @return The selected value. **/ - public String getSelectedValue(String fieldLabel) + public String getSelectedValue(String nameOrLabel) { - return getSelectField(fieldLabel).getValue(); + return getSelectField(nameOrLabel).getValue(); } /** * This allows you to query a given select in the edit panel to see what options it offers. * - * @param fieldLabel The label of the field to get. + * @param nameOrLabel The name or label of the field to get. * @return List of strings for the values in the list. **/ - public List getSelectOptions(String fieldLabel) + public List getSelectOptions(String nameOrLabel) { - return getSelectField(fieldLabel).getOptions(); + return getSelectField(nameOrLabel).getOptions(); } /** * Select a single value from a select list. * - * @param fieldLabel The label of the field to set. + * @param nameOrLabel The name or label of the field to set. * @param selectValue The value to select from the list. * @return A reference to this editable detail table. **/ - public DetailTableEdit setSelectValue(String fieldLabel, String selectValue) + public DetailTableEdit setSelectValue(String nameOrLabel, String selectValue) { List selection = Arrays.asList(selectValue); - return setSelectValue(fieldLabel, selection); + return setSelectValue(nameOrLabel, selection); } - public DetailTableEdit createSelectValue(String fieldLabel, String value) + public DetailTableEdit createSelectValue(String nameOrLabel, String value) { - WebElement container = Locator.tag("td").withAttribute("data-caption", fieldLabel).findElement(this); - var select = ReactSelect.finder(getDriver()).waitFor(container); + var select = ReactSelect.finder(getDriver()).waitFor(elementCache().findValueCell(nameOrLabel)); select.createValue(value); return this; } @@ -342,13 +340,13 @@ public DetailTableEdit createSelectValue(String fieldLabel, String value) /** * Select multiple values from a select list. * - * @param fieldLabel The label of the field to set. + * @param nameOrLabel The name or label of the field to set. * @param selectValues The value to select from the list. * @return A reference to this editable detail table. **/ - public DetailTableEdit setSelectValue(String fieldLabel, List selectValues) + public DetailTableEdit setSelectValue(String nameOrLabel, List selectValues) { - FilteringReactSelect reactSelect = getSelectField(fieldLabel); + FilteringReactSelect reactSelect = getSelectField(nameOrLabel); selectValues.forEach(reactSelect::typeAheadSelect); _changeCounter++; return this; @@ -357,31 +355,31 @@ public DetailTableEdit setSelectValue(String fieldLabel, List selectValu /** * Clear a given select field. * - * @param fieldLabel The label of the field to clear. + * @param nameOrLabel The name or label of the field to clear. * @return A reference to this editable detail table. **/ - public DetailTableEdit clearSelectValue(String fieldLabel) + public DetailTableEdit clearSelectValue(String nameOrLabel) { - return clearSelectValue(fieldLabel, true, true); + return clearSelectValue(nameOrLabel, true, true); } /** * Clear a given select field. * - * @param fieldLabel The label of the field to clear. + * @param nameOrLabel The name or label of the field to clear. * @param waitForSelection If true, wait for the select to have a selection before clearing it * @param assertSelection If true, assert if no selection appears (note: does nothing if waitForSelection is not true) * @return A reference to this editable detail table. */ - public DetailTableEdit clearSelectValue(String fieldLabel, boolean waitForSelection, boolean assertSelection) + public DetailTableEdit clearSelectValue(String nameOrLabel, boolean waitForSelection, boolean assertSelection) { - var select = getSelectField(fieldLabel); + var select = getSelectField(nameOrLabel); if (waitForSelection) { if (assertSelection) { WebDriverWrapper.waitFor(select::hasSelection, - String.format("The %s select did not have any selection in time", fieldLabel), _readyTimeout); + String.format("The %s select did not have any selection in time", nameOrLabel), _readyTimeout); } else WebDriverWrapper.waitFor(select::hasSelection, 1_000); @@ -393,7 +391,7 @@ public DetailTableEdit clearSelectValue(String fieldLabel, boolean waitForSelect /** * Set a DateTime, Date or Time field. - * @param fieldName The name of the field to set. + * @param nameOrLabel The name or label of the field to set. * @param dateTime Will be used to determine what kind of field is being set and how to set it. If the parameter * is a LocalDateTime object then it is assumed that field is a DateTime field. If the parameter is * a LocalDate object then it is assumed to be a date-only field. And I think you can guess what @@ -401,9 +399,9 @@ public DetailTableEdit clearSelectValue(String fieldLabel, boolean waitForSelect * is typed into the field (no picker is used). * @return A reference to this DetailTableEdit object. */ - public DetailTableEdit setDateTimeField(String fieldName, Object dateTime) + public DetailTableEdit setDateTimeField(String nameOrLabel, Object dateTime) { - ReactDateTimePicker dateTimePicker = getDateTimePicker(fieldName); + ReactDateTimePicker dateTimePicker = getDateTimePicker(nameOrLabel); if (dateTime instanceof LocalDateTime localDateTime) { dateTimePicker.select(localDateTime); @@ -430,22 +428,22 @@ else if (dateTime instanceof String setValue) return this; } - public String getDateTimeField(String fieldName) + public String getDateTimeField(String nameOrLabel) { - ReactDateTimePicker dateTimePicker = getDateTimePicker(fieldName); + ReactDateTimePicker dateTimePicker = getDateTimePicker(nameOrLabel); return dateTimePicker.get(); } - public void clearDateTimeField(String fieldName) + public void clearDateTimeField(String nameOrLabel) { - ReactDateTimePicker dateTimePicker = getDateTimePicker(fieldName); + ReactDateTimePicker dateTimePicker = getDateTimePicker(nameOrLabel); dateTimePicker.clear(); _changeCounter++; } - private ReactDateTimePicker getDateTimePicker(String fieldName) + private ReactDateTimePicker getDateTimePicker(String nameOrLabel) { - return new ReactDateTimePicker.ReactDateTimeInputFinder(getDriver()).find(elementCache().valueCellWithName(fieldName)); + return new ReactDateTimePicker.ReactDateTimeInputFinder(getDriver()).find(elementCache().findValueCell(nameOrLabel)); } // For use when the field is of an unknown type, as can occur in fuzz tests @@ -455,13 +453,13 @@ public void setDetails(FieldDefinition field, Object newValue) return; if (field.getType() == FieldDefinition.ColumnType.TextChoice) - setSelectValue(field.getLabel(), (List) newValue); + setSelectValue(field.getName(), (List) 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.getLabel(), (Boolean) newValue); + setBooleanField(field.getName(), (Boolean) newValue); else - setTextField(field.getLabel(), String.valueOf(newValue)); + setTextField(field.getName(), String.valueOf(newValue)); } /** @@ -584,24 +582,80 @@ public ElementCache() .invisibilityOfAllElements(Locator.byClass("select-input__loading-indicator").findElements(this))); } - public WebElement header = Locator.tagWithClass("div", "panel-heading") + public final WebElement header = Locator.tagWithClass("div", "panel-heading") .findWhenNeeded(this); - public WebElement editPanel = Locator.tagWithClass("div", "detail__editing") + public final WebElement editPanel = Locator.tagWithClass("div", "detail__editing") .findWhenNeeded(this); - public WebElement valueCellWithLabel(String label) + public WebElement findValueCell(String nameOrLabel) { - return Locator.tagWithAttribute("td", "data-caption", label).findElementOrNull(editPanel); + List> options = List.of( + () -> valueCellWithFieldKey(nameOrLabel), + () -> valueCellWithName(nameOrLabel), + () -> valueCellWithLabel(nameOrLabel)); + + return options.stream() + .map(Supplier::get) + .filter(Objects::nonNull) + .findFirst() + .orElseThrow(() -> new NoSuchElementException("Unable to locate cell: " + nameOrLabel)); } + public WebElement valueCellWithLabel(String label) + { + initFieldInfo(); + return valueCellsByLabel.get(label); + } public WebElement valueCellWithName(String fieldName) { - return Locator.tagWithAttribute("td", "data-fieldkey", EscapeUtil.fieldKeyEncodePart(fieldName)).findElement(editPanel); + return valueCellWithFieldKey(EscapeUtil.fieldKeyEncodePart(fieldName)); + } + + public WebElement valueCellWithFieldKey(String fieldKey) + { +// if (!valueCellsByFieldKey.containsKey(fieldKey)) +// { +// valueCellsByFieldKey.put(fieldKey, +// Locator.tagWithAttribute("td", "data-fieldkey", fieldKey) +// .findElementOrNull(editPanel)); +// } + initFieldInfo(); + return valueCellsByFieldKey.get(fieldKey); + } + + private final Map valueCellsByLabel = new HashMap<>(); + private final Map valueCellsByFieldKey = new HashMap<>(); + private void initFieldInfo() + { + if (valueCellsByFieldKey.isEmpty()) + { + List valueCells = Locator.tagWithAttribute("td", "data-fieldkey").findElements(this); + List> captionsAndKeys = getWrapper().executeScript( + """ + var cells = arguments[0]; + var captions = []; + var fieldkeys = []; + for (var i = 0; i < cells.length; i++) + { + captions.push(cells[i].dataset.caption); + fieldkeys.push(cells[i].dataset.fieldkey); + } + return [captions, fieldkeys]; + """, List.class, + valueCells); + List captions = captionsAndKeys.get(0); + List fieldkeys = captionsAndKeys.get(1); + for (int i = 0; i < valueCells.size(); i++) + { + valueCellsByFieldKey.put(fieldkeys.get(i), valueCells.get(i)); + valueCellsByLabel.put(captions.get(i), valueCells.get(i)); + } + } } - public FileUploadField fileField(String label) + public FileUploadField fileField(String nameOrLabel) { - return new FileUploadField(valueCellWithLabel(label), getDriver()); + return new FileUploadField(findValueCell(nameOrLabel), getDriver()); } public Locator validationMsg = Locator.tagWithClass("span", "validation-message"); @@ -613,10 +667,14 @@ public FileUploadField fileField(String label) public WebElement commentInput = Locator.tagWithId("textarea", "actionComments").refindWhenNeeded(getDriver()); - public FilteringReactSelect findSelect(String fieldLabel) + public FilteringReactSelect findSelect(String nameOrLabel) + { + return FilteringReactSelect.finder(_driver).timeout(_readyTimeout).waitFor(findValueCell(nameOrLabel)); + } + + public Input findInput(String nameOrLabel) { - WebElement container = Locator.tag("td").withAttribute("data-caption", fieldLabel).findElement(this); - return FilteringReactSelect.finder(_driver).timeout(_readyTimeout).waitFor(container); + return Input.Input(Locator.xpath("./div/div/*[self::input or self::textarea]"), getDriver()).find(findValueCell(nameOrLabel)); } } diff --git a/src/org/labkey/test/util/TestDataGenerator.java b/src/org/labkey/test/util/TestDataGenerator.java index ddb11dfe45..8df98456c5 100644 --- a/src/org/labkey/test/util/TestDataGenerator.java +++ b/src/org/labkey/test/util/TestDataGenerator.java @@ -99,7 +99,7 @@ public class TestDataGenerator private final String _containerPath; private String _excludedChars; private boolean _alphaNumericStr; - private final CSVFormat _format = CSVFormat.DEFAULT; + private final CSVFormat _format = CSVFormat.TDF; /** * use TestDataGenerator to generate data to a specific fieldSet From 4d82b3efb22a3038a2c5ee4cb16615da94350439 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Wed, 7 May 2025 17:30:02 -0700 Subject: [PATCH 06/34] Need tabs for editable grid --- src/org/labkey/test/TestFileUtils.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) 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); From 90723c929be1d0e516746f8ad266b3cdde2a5e6a Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Thu, 8 May 2025 13:25:05 -0700 Subject: [PATCH 07/34] Fix some quoting for pasting --- .../components/ui/grids/EditableGrid.java | 8 ++-- .../assay/UploadLargeExcelAssayTest.java | 14 +------ .../labkey/test/util/TestDataGenerator.java | 39 ++++++++++--------- .../labkey/test/util/data/TestDataUtils.java | 14 ++----- 4 files changed, 30 insertions(+), 45 deletions(-) diff --git a/src/org/labkey/test/components/ui/grids/EditableGrid.java b/src/org/labkey/test/components/ui/grids/EditableGrid.java index f41b889968..d57159c124 100644 --- a/src/org/labkey/test/components/ui/grids/EditableGrid.java +++ b/src/org/labkey/test/components/ui/grids/EditableGrid.java @@ -1,5 +1,6 @@ package org.labkey.test.components.ui.grids; +import org.apache.commons.csv.CSVFormat; import org.apache.commons.lang3.StringUtils; import org.assertj.core.api.Assertions; import org.jetbrains.annotations.Nullable; @@ -782,17 +783,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; }) diff --git a/src/org/labkey/test/tests/assay/UploadLargeExcelAssayTest.java b/src/org/labkey/test/tests/assay/UploadLargeExcelAssayTest.java index c813094936..30f1743cb5 100644 --- a/src/org/labkey/test/tests/assay/UploadLargeExcelAssayTest.java +++ b/src/org/labkey/test/tests/assay/UploadLargeExcelAssayTest.java @@ -7,33 +7,22 @@ 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 { @@ -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.writeGeneratedDataToExcel("chaos", fileName); log("finished writing large .xlsx file"); // import large generated excel to assay1 diff --git a/src/org/labkey/test/util/TestDataGenerator.java b/src/org/labkey/test/util/TestDataGenerator.java index 8df98456c5..200d6d6826 100644 --- a/src/org/labkey/test/util/TestDataGenerator.java +++ b/src/org/labkey/test/util/TestDataGenerator.java @@ -99,6 +99,7 @@ public class TestDataGenerator private final String _containerPath; private String _excludedChars; private boolean _alphaNumericStr; + private final TestDataUtils.TsvQuoter _tsvQuoter = new TestDataUtils.TsvQuoter(','); private final CSVFormat _format = CSVFormat.TDF; /** @@ -125,7 +126,7 @@ public static File writeCsvFile(List fields, List> rows = TestDataUtils.rowListsFromMaps(entityData); TestDataUtils.replaceColumnHeaders(rows, ColumnNameMapper.labelToName(fields)); // Use field names - return TestDataUtils.writeRowsToFile(fileName, rows); + 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) @@ -640,32 +641,32 @@ public List getPasteColumnValues(String fieldName) for (Map row : _rows) { Object value = row.get(fieldName); - String strVal = _format.format(value); + String strVal = CSVFormat.DEFAULT.format(value); // Just quote commas values.add(strVal); } return values; } /** - * generates tabular text content (CSV or TSV) using the rows in the current instance; - * @return CSV formatted representation of generated rows + * generates tsv-formatted content using the rows in the current instance; + * @return TSV formatted representation of generated rows */ public String writeTsvContents() { return TestDataUtils.stringFromRowMaps(_rows, getFieldsForFile(), true, _format); } - public File writeGeneratedDataToFile(int numberOfRowsToGenerate, String fileName) throws IOException + public File writeGeneratedDataToFile(String fileName) throws IOException { - List> rows = new ArrayList<>(); - for (int i = 0; i < numberOfRowsToGenerate; i++) - { - rows.add(generateRow()); - } - return TestDataUtils.writeRowsToFile(fileName, TestDataUtils.rowListsFromMaps(rows, getFieldsForFile(), true, true), _format); + return writeGeneratedDataToFile(fileName, _format); + } + + public File writeGeneratedDataToFile(String fileName, CSVFormat format) throws IOException + { + return TestDataUtils.writeRowsToFile(fileName, TestDataUtils.rowListsFromMaps(_rows, getFieldsForFile(), true, true), format); } - public File writeGeneratedDataToExcel(int numberOfRowsToGenerate, String sheetName, String fileName) throws IOException + public File writeGeneratedDataToExcel(String sheetName, String fileName) throws IOException { File file = new File(TestFileUtils.getTestTempDir(), fileName); FileUtils.forceMkdirParent(file); @@ -684,10 +685,10 @@ public File writeGeneratedDataToExcel(int numberOfRowsToGenerate, String sheetNa } // write content - for (int i = 1; i < numberOfRowsToGenerate +1; i++) + for (int i = 0; i < _rows.size(); i++) { - Map row = generateRow(); - SXSSFRow currentRow = sheet.createRow(i); + Map row = _rows.get(i); + SXSSFRow currentRow = sheet.createRow(i - 1); for (int j = 0; j < columnNames.length; j++) { currentRow.createCell(j).setCellValue(row.get(columnNames[j]).toString()); @@ -700,16 +701,16 @@ public File writeGeneratedDataToExcel(int numberOfRowsToGenerate, String sheetNa } /** - * Creates a file containing the contents of the current rows, formatted in CSV. + * Creates a file containing the contents of the current rows, formatted in TSV. * The file is written to the test temp dir - * @param fileName the name of the file, e.g. 'testDataFileForMyTest.csv' - * @return File object pointing at created file + * @param fileName the name of the file, e.g. 'testDataFileForMyTest.tsv' + * @return File object pointing at created TSV */ public File writeData(String fileName) { try { - return TestFileUtils.writeTempFile(fileName, writeTsvContents()); + return writeGeneratedDataToFile(fileName); } catch (IOException e) { diff --git a/src/org/labkey/test/util/data/TestDataUtils.java b/src/org/labkey/test/util/data/TestDataUtils.java index e16648e014..f295139238 100644 --- a/src/org/labkey/test/util/data/TestDataUtils.java +++ b/src/org/labkey/test/util/data/TestDataUtils.java @@ -166,12 +166,6 @@ public static String tsvStringFromRowMaps(List> rowMaps, Lis return stringFromRowMaps(rowMaps, columns, includeHeaders, CSVFormat.TDF); } - public static String csvStringFromRowMaps(List> rowMaps, List columns, - boolean includeHeaders) - { - return stringFromRowMaps(rowMaps, columns, includeHeaders, CSVFormat.DEFAULT); - } - public static List> rowListsFromMaps(List> rowMaps, List columns) { @@ -244,23 +238,23 @@ public static List> replaceColumnHeaders(List> rowList return rowLists; } - public static File writeRowsToTsv(String fileName, List> rows) throws IOException + public static File writeRowsToTsv(String fileName, List> rows) throws IOException { return writeRowsToFile(fileName, rows, CSVFormat.TDF); } - public static File writeRowsToFile(String fileName, List> rows) throws IOException + public static File writeRowsToCsv(String fileName, List> rows) throws IOException { return writeRowsToFile(fileName, rows, CSVFormat.DEFAULT); } - public static @NotNull File writeRowsToFile(String fileName, List> rows, CSVFormat format) throws IOException + public static @NotNull File writeRowsToFile(String fileName, List> rows, CSVFormat format) throws IOException { File file = new File(TestFileUtils.getTestTempDir(), fileName); FileUtils.forceMkdirParent(file); try (CSVPrinter printer = new CSVPrinter(new FileWriter(file, StandardCharsets.UTF_8), format)) { - for (List row : rows) + for (List row : rows) { printer.printRecord(row); } From 50f77107150f617b50b79ce8e3540bdbd7907ab5 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Thu, 8 May 2025 15:18:55 -0700 Subject: [PATCH 08/34] Some updates to FieldSelectionDialog --- .../ui/grids/FieldSelectionDialog.java | 88 +++++++++---------- src/org/labkey/test/params/FieldKey.java | 37 +++++--- .../tests/component/GridPanelViewTest.java | 3 +- 3 files changed, 68 insertions(+), 60 deletions(-) diff --git a/src/org/labkey/test/components/ui/grids/FieldSelectionDialog.java b/src/org/labkey/test/components/ui/grids/FieldSelectionDialog.java index 115fdf49df..24339b6a76 100644 --- a/src/org/labkey/test/components/ui/grids/FieldSelectionDialog.java +++ b/src/org/labkey/test/components/ui/grids/FieldSelectionDialog.java @@ -7,9 +7,11 @@ import org.labkey.test.components.UpdatingComponent; import org.labkey.test.components.bootstrap.ModalDialog; import org.labkey.test.components.html.Checkbox; +import org.labkey.test.params.FieldKey; import org.labkey.test.util.EscapeUtil; 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; @@ -103,13 +105,21 @@ public List getAvailableFieldLabels() */ public boolean isAvailableFieldSelected(String... fieldNameParts) { - WebElement listItem = elementCache().getListItemElementByFieldKey(expandAvailableFields(fieldNameParts)); + WebElement listItem = expandAvailableFields(fieldNameParts); return Locator.tagWithClass("i", "fa-check").findWhenNeeded(listItem).isDisplayed(); } public boolean isFieldAvailable(String... fieldNameParts) { - return elementCache().getListItemElementByFieldKeyOrNull(expandAvailableFields(fieldNameParts)) != null; + try + { + expandAvailableFields(fieldNameParts); + return true; + } + catch (NoSuchElementException e) + { + return false; + } } /** @@ -120,27 +130,10 @@ public boolean isFieldAvailable(String... fieldNameParts) */ public FieldSelectionDialog selectAvailableField(String... fieldNameParts) { - return addFieldByFieldKeyToGrid(expandAvailableFields(fieldNameParts)); - } + WebElement listItem = expandAvailableFields(fieldNameParts); - public WebElement getAvailableFieldElement(String fieldName) - { - 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); - - Assert.assertTrue(String.format(FIELD_NOT_AVAILABLE, fieldKey), - listItem.isDisplayed()); + Assert.assertTrue(String.format(FIELD_NOT_AVAILABLE, FieldKey.fromParts(fieldNameParts)), + listItem.isDisplayed()); WebElement addIcon = Locator.tagWithClass("div", "view-field__action") .withChild(Locator.tagWithClass("i", "fa-plus")) @@ -151,6 +144,11 @@ private FieldSelectionDialog addFieldByFieldKeyToGrid(String fieldKey) return this; } + public WebElement getAvailableFieldElement(String... fieldNameParts) + { + return expandAvailableFields(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. @@ -158,7 +156,7 @@ private FieldSelectionDialog addFieldByFieldKeyToGrid(String fieldKey) * @param fieldNameParts The list of fieldKeyParts to expand. * @return key for the expanded field. */ - private String expandAvailableFields(String... fieldNameParts) + private WebElement expandAvailableFields(String... fieldNameParts) { StringBuilder fieldKey = new StringBuilder(); @@ -172,7 +170,7 @@ private String expandAvailableFields(String... fieldNameParts) 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("/"); @@ -180,7 +178,7 @@ private String expandAvailableFields(String... fieldNameParts) } - return fieldKey.toString(); + return elementCache().findAvailableField(fieldKey.toString()); } /** @@ -192,7 +190,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 +407,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 @@ -636,20 +631,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/params/FieldKey.java b/src/org/labkey/test/params/FieldKey.java index 9221a2988e..f0b49ea1bf 100644 --- a/src/org/labkey/test/params/FieldKey.java +++ b/src/org/labkey/test/params/FieldKey.java @@ -5,11 +5,11 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; public class FieldKey { + public static final FieldKey ROOT = new FieldKey(null, null); + private final FieldKey _parent; private final String _name; private final String _encodedName; @@ -24,7 +24,7 @@ private FieldKey(FieldKey parent, String name) public static FieldKey fromParts(List parts) { if (parts.isEmpty()) - return null; + return ROOT; if (parts.stream().anyMatch(StringUtils::isBlank)) throw new IllegalArgumentException("parts contains blank: " + parts); @@ -66,26 +66,37 @@ public FieldKey getParent() return _parent; } + public FieldKey child(String fieldName) + { + return new FieldKey(_parent, fieldName); + } + public List getParts(boolean encode) { - List parts = new ArrayList<>(); - if (_parent != null) + if (this == ROOT) { - parts.addAll(_parent.getParts(encode)); + return new ArrayList<>(); + } + else + { + List parts = _parent.getParts(encode); + parts.add(encode ? _encodedName : _name); + return parts; } - parts.add(encode ? _encodedName : _name); - return parts; } public List getHierarchy() { - List parts = new ArrayList<>(); - if (_parent != null) + if (this == ROOT) + { + return new ArrayList<>(); + } + else { - parts.addAll(_parent.getHierarchy()); + List parts = _parent.getHierarchy(); + parts.add(this); + return parts; } - parts.add(this); - return parts; } @Override 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()); From 784308f346f71293f7ac8db75775f25348f65f18 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Fri, 9 May 2025 09:29:37 -0700 Subject: [PATCH 09/34] Fix paste text and field key --- src/org/labkey/test/params/FieldKey.java | 2 +- src/org/labkey/test/util/TestDataGenerator.java | 2 +- src/org/labkey/test/util/data/TestDataUtils.java | 5 ----- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/org/labkey/test/params/FieldKey.java b/src/org/labkey/test/params/FieldKey.java index f0b49ea1bf..85232d5b8f 100644 --- a/src/org/labkey/test/params/FieldKey.java +++ b/src/org/labkey/test/params/FieldKey.java @@ -68,7 +68,7 @@ public FieldKey getParent() public FieldKey child(String fieldName) { - return new FieldKey(_parent, fieldName); + return new FieldKey(this, fieldName); } public List getParts(boolean encode) diff --git a/src/org/labkey/test/util/TestDataGenerator.java b/src/org/labkey/test/util/TestDataGenerator.java index 200d6d6826..30ef09e81b 100644 --- a/src/org/labkey/test/util/TestDataGenerator.java +++ b/src/org/labkey/test/util/TestDataGenerator.java @@ -688,7 +688,7 @@ public File writeGeneratedDataToExcel(String sheetName, String fileName) throws for (int i = 0; i < _rows.size(); i++) { Map row = _rows.get(i); - SXSSFRow currentRow = sheet.createRow(i - 1); + SXSSFRow currentRow = sheet.createRow(i + 1); for (int j = 0; j < columnNames.length; j++) { currentRow.createCell(j).setCellValue(row.get(columnNames[j]).toString()); diff --git a/src/org/labkey/test/util/data/TestDataUtils.java b/src/org/labkey/test/util/data/TestDataUtils.java index f295139238..f6f9819d78 100644 --- a/src/org/labkey/test/util/data/TestDataUtils.java +++ b/src/org/labkey/test/util/data/TestDataUtils.java @@ -263,11 +263,6 @@ public static File writeRowsToCsv(String fileName, List> rows) throw return file; } - public static String stringFromRows(List> rows) - { - return stringFromRows(rows, CSVFormat.DEFAULT); - } - public static String stringFromRows(List> rows, CSVFormat format) { StringWriter stringWriter = new StringWriter(); From 1fc088963850a7a811a5f7b432e94db5e0f7dc7e Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Fri, 9 May 2025 13:47:20 -0700 Subject: [PATCH 10/34] More cleanup of TestDataUtils --- .../components/ui/grids/ResponsiveGrid.java | 10 +-- .../test/tests/SampleTypeRemoteAPITest.java | 6 +- .../assay/UploadLargeExcelAssayTest.java | 4 +- .../labkey/test/util/TestDataGenerator.java | 63 ++++++++++------- .../labkey/test/util/data/TestDataUtils.java | 70 ++++++++++++++----- 5 files changed, 103 insertions(+), 50 deletions(-) diff --git a/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java b/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java index 70d7aefab9..a2e0ea0e26 100644 --- a/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java +++ b/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java @@ -835,7 +835,7 @@ protected Map initColumnsAndIndices() for (int i = 0; i < headerCellElements.size(); i++) { 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)); + indexes.put(fieldLabels.get(i), new ColumnIndex(fieldLabels.get(i), i + offset, i)); } } return indexes; @@ -889,7 +889,7 @@ protected Optional getOptionalRow(String text) protected GridRow getRow(String fieldLabel, String text) { // try to normalize column index to start at 0, excluding row selector column - Integer columnIndex = getColumnIndex(fieldLabel); + int columnIndex = getColumnIndex(fieldLabel); return new GridRow.GridRowFinder(ResponsiveGrid.this).withTextAtColumn(text, columnIndex) .find(this); } @@ -897,7 +897,7 @@ protected GridRow getRow(String fieldLabel, String text) protected Optional getOptionalRow(String fieldLabel, String text) { // try to normalize column index to start at 0, excluding row selector column - Integer columnIndex = getColumnIndex(fieldLabel); + int columnIndex = getColumnIndex(fieldLabel); return new GridRow.GridRowFinder(ResponsiveGrid.this).withTextAtColumn(text, columnIndex) .findOptional(this); } @@ -988,9 +988,8 @@ protected Locator locator() return _locator; } } -} - class ColumnIndex + static class ColumnIndex { private final Integer _rawIndex; private final Integer _normalizedIndex; @@ -1022,3 +1021,4 @@ public Integer getNormalizedIndex() return _normalizedIndex; } } +} 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 30f1743cb5..167abd2662 100644 --- a/src/org/labkey/test/tests/assay/UploadLargeExcelAssayTest.java +++ b/src/org/labkey/test/tests/assay/UploadLargeExcelAssayTest.java @@ -28,7 +28,7 @@ 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) @@ -87,7 +87,7 @@ public void testUpload200kRows() throws Exception .withColumns(ASSAY_FIELDS); dgen.generateRows(200_000); log("writing large .xlsx file"); - var largeExcelFile = dgen.writeGeneratedDataToExcel("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/util/TestDataGenerator.java b/src/org/labkey/test/util/TestDataGenerator.java index 30ef09e81b..92bd4f8710 100644 --- a/src/org/labkey/test/util/TestDataGenerator.java +++ b/src/org/labkey/test/util/TestDataGenerator.java @@ -60,7 +60,6 @@ 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.data.TestDataUtils.REALISTIC_ASSAY_FIELDS; @@ -99,8 +98,6 @@ public class TestDataGenerator private final String _containerPath; private String _excludedChars; private boolean _alphaNumericStr; - private final TestDataUtils.TsvQuoter _tsvQuoter = new TestDataUtils.TsvQuoter(','); - private final CSVFormat _format = CSVFormat.TDF; /** * use TestDataGenerator to generate data to a specific fieldSet @@ -635,6 +632,10 @@ public boolean randomBoolean() return fieldNames; } + /** + * Get generated data for the specified column. + * Values will be quoted appropriately for pasting into editable grid lookups. + */ public List getPasteColumnValues(String fieldName) { List values = new ArrayList<>(); @@ -649,21 +650,11 @@ public List getPasteColumnValues(String fieldName) /** * generates tsv-formatted content using the rows in the current instance; - * @return TSV formatted representation of generated rows + * @return TSV formatted representation of generated data */ - public String writeTsvContents() - { - return TestDataUtils.stringFromRowMaps(_rows, getFieldsForFile(), true, _format); - } - - public File writeGeneratedDataToFile(String fileName) throws IOException + public String getDataAsTsv() { - return writeGeneratedDataToFile(fileName, _format); - } - - public File writeGeneratedDataToFile(String fileName, CSVFormat format) throws IOException - { - return TestDataUtils.writeRowsToFile(fileName, TestDataUtils.rowListsFromMaps(_rows, getFieldsForFile(), true, true), format); + return TestDataUtils.stringFromRowMaps(_rows, getFieldsForFile(), true, CSVFormat.TDF); } public File writeGeneratedDataToExcel(String sheetName, String fileName) throws IOException @@ -677,11 +668,11 @@ public File writeGeneratedDataToExcel(String sheetName, String fileName) throws var sheet = workbook.createSheet(sheetName); // write headers as row 0 - String[] columnNames = _columns.keySet().toArray(new String[0]); + List columnNames = getFieldsForFile(); var headerRow = sheet.createRow(0); - for (int i = 0; i < columnNames.length; i++) + for (int i = 0; i < columnNames.size(); i++) { - headerRow.createCell(i).setCellValue(columnNames[i]); + headerRow.createCell(i).setCellValue(columnNames.get(i)); } // write content @@ -689,9 +680,9 @@ public File writeGeneratedDataToExcel(String sheetName, String fileName) throws { Map row = _rows.get(i); SXSSFRow currentRow = sheet.createRow(i + 1); - for (int j = 0; j < columnNames.length; j++) + for (int j = 0; j < columnNames.size(); j++) { - currentRow.createCell(j).setCellValue(row.get(columnNames[j]).toString()); + currentRow.createCell(j).setCellValue(row.getOrDefault(columnNames.get(j), "").toString()); } } workbook.write(out); @@ -701,16 +692,40 @@ public File writeGeneratedDataToExcel(String sheetName, String fileName) throws } /** - * Creates a file containing the contents of the current rows, formatted in TSV. + * Creates a file containing the contents of the current rows, formatted in TSV, CSV, or xlsx. * The file is written to the test temp dir * @param fileName the name of the file, e.g. 'testDataFileForMyTest.tsv' - * @return File object pointing at created TSV + * @return File object pointing at created file */ public File writeData(String fileName) + { + String fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1); + switch (fileExtension) + { + case "xlsx": + case "xls": + try + { + return writeGeneratedDataToExcel("sheet1", fileName); + } + catch (IOException e) + { + throw new RuntimeException(e); + } + case "csv": + return writeData(fileName, CSVFormat.DEFAULT); + case "tsv": + return writeData(fileName, CSVFormat.TDF); + default: + throw new IllegalArgumentException("Unsupported file extension: " + fileExtension); + } + } + + public File writeData(String fileName, CSVFormat format) { try { - return writeGeneratedDataToFile(fileName); + return TestDataUtils.writeRowsToFile(fileName, TestDataUtils.rowListsFromMaps(_rows, getFieldsForFile()), format); } catch (IOException e) { diff --git a/src/org/labkey/test/util/data/TestDataUtils.java b/src/org/labkey/test/util/data/TestDataUtils.java index f6f9819d78..a06c45358b 100644 --- a/src/org/labkey/test/util/data/TestDataUtils.java +++ b/src/org/labkey/test/util/data/TestDataUtils.java @@ -6,12 +6,15 @@ import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; +import org.labkey.serverapi.reader.DataLoader; import org.labkey.serverapi.reader.TabLoader; import org.labkey.test.TestFileUtils; import org.labkey.test.params.FieldDefinition; import org.labkey.test.util.TestDataGenerator; import java.io.File; +import java.io.FileInputStream; +import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; @@ -19,6 +22,8 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -42,10 +47,8 @@ public class TestDataUtils () -> new FieldDefinition("Consumption Rate, Glucose", FieldDefinition.ColumnType.Decimal), () -> new FieldDefinition("Measurement Date/Time", FieldDefinition.ColumnType.DateAndTime), () -> new FieldDefinition("A260/A280", FieldDefinition.ColumnType.Decimal), - () -> new FieldDefinition("Nucleic Acid (ng/uL)", FieldDefinition.ColumnType.Decimal) - .setLabel("Nucleic Acid (ng/uL)"), - () -> new FieldDefinition("Concentration (by Qubit ng/uL)", FieldDefinition.ColumnType.Decimal) - .setLabel("Concentration (by Qubit ng/uL)"), + () -> new FieldDefinition("Nucleic Acid (ng/uL)", FieldDefinition.ColumnType.Decimal), + () -> new FieldDefinition("Concentration (by Qubit ng/uL)", FieldDefinition.ColumnType.Decimal), () -> new FieldDefinition("Dead (cells/ml)", FieldDefinition.ColumnType.Decimal), () -> new FieldDefinition("PDGF-AA/BB", FieldDefinition.ColumnType.Decimal), () -> new FieldDefinition("Run End Data/Time", FieldDefinition.ColumnType.DateAndTime), @@ -57,14 +60,10 @@ public class TestDataUtils () -> new FieldDefinition("FAM-Lambda..cp.Rxn."), () -> new FieldDefinition("VIC-Precision...1"), () -> new FieldDefinition("Product.Type"), - () -> new FieldDefinition("Weight.Balance_%", FieldDefinition.ColumnType.Decimal) - .setLabel("Weight.Balance %"), - () -> new FieldDefinition("Cumulative.Yield.DCW/Glucose.Consumed_g/g", FieldDefinition.ColumnType.Decimal) - .setLabel("Cumulative.Yield.DCW/Glucose.Consumed g/g"), - () -> new FieldDefinition("Average.Volume.Productivity_g/L/day", FieldDefinition.ColumnType.Decimal) - .setLabel("Average.Volume.Productivity g/L/day"), + () -> new FieldDefinition("Weight.Balance_%", FieldDefinition.ColumnType.Decimal), + () -> new FieldDefinition("Cumulative.Yield.DCW/Glucose.Consumed_g/g", FieldDefinition.ColumnType.Decimal), + () -> new FieldDefinition("Average.Volume.Productivity_g/L/day", FieldDefinition.ColumnType.Decimal), () -> new FieldDefinition("Cmol.Biomass/Cmol.Glucose.Consumed_%", FieldDefinition.ColumnType.Decimal) - .setLabel("Cmol.Biomass/Cmol.Glucose.Consumed %") ); public static final List> REALISTIC_SAMPLE_FIELDS = List.of( () -> new FieldDefinition("MW (g/mol)", FieldDefinition.ColumnType.Decimal), @@ -139,6 +138,14 @@ public static String getRealisticPlateName() return REALISTIC_PLATE_NAMES.get(TestDataGenerator.randomInt(0, REALISTIC_PLATE_NAMES.size() - 1)); } + public static List> rowMapsFromTsv(File tsvFile) throws IOException + { + try (DataLoader loader = new TabLoader.TsvFactory().createLoader(tsvFile, true)) + { + return loader.load(); + } + } + public static List> rowMapsFromTsv(String tsvString) throws IOException { try (InputStream dataStream = IOUtils.toInputStream(tsvString, StandardCharsets.UTF_8)) @@ -147,6 +154,14 @@ public static List> rowMapsFromTsv(String tsvString) throws } } + public static List> rowMapsFromCsv(File csvFile) throws IOException + { + try (DataLoader loader = new TabLoader.CsvFactory().createLoader(csvFile, true)) + { + return loader.load(); + } + } + public static List> rowMapsFromCsv(String tsvString) throws IOException { try (InputStream dataStream = IOUtils.toInputStream(tsvString, StandardCharsets.UTF_8)) @@ -166,15 +181,33 @@ public static String tsvStringFromRowMaps(List> rowMaps, Lis return stringFromRowMaps(rowMaps, columns, includeHeaders, CSVFormat.TDF); } - - public static List> rowListsFromMaps(List> rowMaps, List columns) + public static List> mapsFromRows(List> allRows) { - return rowListsFromMaps(rowMaps, columns, false, true); + List> rowMaps = new ArrayList<>(); + + if (allRows != null && !allRows.isEmpty()) + { + List header = allRows.get(0); + + for (int i = 1; i != allRows.size(); i++) + { + List row = allRows.get(i); + Map rowMap = new LinkedHashMap<>(); + int end = Math.min(header.size(), row.size()); + for (int col = 0; col < end; col++) + { + rowMap.put(header.get(col).toString(), row.get(col)); + } + rowMaps.add(rowMap); + } + } + + return rowMaps; } - public static List> rowListsFromMaps(List> rowMaps, List columns, boolean includeHeaders) + public static List> dataRowsFromMaps(List> rowMaps, List columns) { - return rowListsFromMaps(rowMaps, columns, includeHeaders, true); + return rowListsFromMaps(rowMaps, columns, false, true); } public static List> rowListsFromMaps(List> rowMaps) @@ -187,6 +220,11 @@ public static List> rowListsFromMaps(List> rowM return rowListsFromMaps(rowMaps, new ArrayList<>(columns), true, true); } + public static List> rowListsFromMaps(List> rowMaps, List columns) + { + return rowListsFromMaps(rowMaps, columns, true, true); + } + /** * convert a List of Map to a list of List * @param rowMaps Source data From 6539f480e208117a8641a3ffd8eb6bccd6148a83 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Fri, 9 May 2025 17:24:28 -0700 Subject: [PATCH 11/34] ResponsiveGrid update --- .../test/components/ui/grids/GridRow.java | 74 ++++-- .../components/ui/grids/ResponsiveGrid.java | 240 ++++++++++-------- src/org/labkey/test/params/FieldKey.java | 19 +- 3 files changed, 196 insertions(+), 137 deletions(-) diff --git a/src/org/labkey/test/components/ui/grids/GridRow.java b/src/org/labkey/test/components/ui/grids/GridRow.java index f2218323cd..46225b19c3 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; @@ -15,17 +14,18 @@ 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 +73,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(String fieldIdentifier) { - return getCell(_grid.getColumnIndex(fieldLabel)); + return getCell(_grid.getColumnIndex(fieldIdentifier)); } /** @@ -136,17 +134,17 @@ public ImageFileViewDialog clickImgFile(String fieldLabel) /** * finds a AttachmentCard specified filename, clicks it, and waits for the file to download */ - public File clickNonImgFile(String fieldLabel) + public File clickNonImgFile(String fieldIdentifier) { - return elementCache().waitForAttachment(fieldLabel).clickOnNonImgFile(); + return elementCache().waitForAttachment(fieldIdentifier).clickOnNonImgFile(); } /** * Returns the text in the row for the specified column */ - public String getText(String fieldLabel) + public String getText(String fieldIdentifier) { - return getCell(fieldLabel).getText(); + return getCell(fieldIdentifier).getText(); } /** @@ -154,8 +152,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 +163,22 @@ public List getTexts() */ public Map getRowMapByLabel() { - if (_rowMapByLabel == null) + return getRowMap(ResponsiveGrid.ColumnHeader::getColumnLabel); + } + + Map getRowMap(Function keyMapper) + { + List columnValues = elementCache().getCellTexts(); + List headers = _grid.getHeaders(); + + Map rowMap = new LinkedHashMap<>(); + + for (ResponsiveGrid.ColumnHeader header : headers) { - _rowMapByLabel = new CaseInsensitiveHashMap<>(); - List columns = _grid.getColumnLabels(); - List rowCellTexts = getTexts(); - for (int i = 0; i < columns.size(); i++) - { - _rowMapByLabel.put(columns.get(i), rowCellTexts.get(i)); - } + rowMap.put(keyMapper.apply(header), columnValues.get(header.getDomIndex())); } - return _rowMapByLabel; + + return rowMap; } @Override @@ -202,9 +204,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(String fieldIdentifier) { - return new AttachmentCard.FileAttachmentCardFinder(getDriver()).waitFor(getCell(fieldLabel)); + return new AttachmentCard.FileAttachmentCardFinder(getDriver()).waitFor(getCell(fieldIdentifier)); } } diff --git a/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java b/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java index a2e0ea0e26..5aa727c1e9 100644 --- a/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java +++ b/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java @@ -4,6 +4,9 @@ */ package org.labkey.test.components.ui.grids; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.mutable.Mutable; +import org.apache.commons.lang3.mutable.MutableObject; import org.jetbrains.annotations.Nullable; import org.labkey.remoteapi.query.Filter; import org.labkey.test.Locator; @@ -15,6 +18,7 @@ import org.labkey.test.components.html.RadioButton; import org.labkey.test.components.react.ReactCheckBox; 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; @@ -27,7 +31,8 @@ import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -38,7 +43,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 +104,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"); } /** @@ -656,11 +661,7 @@ public String getCellText(int rowIndex, String fieldLabel) */ public List> getRowMapsByLabel() { - if(null == elementCache().mapList) - { - elementCache().mapList = elementCache()._initGridData(); - } - return elementCache().mapList; + return elementCache().getRows().stream().map(GridRow::getRowMapByLabel).toList(); } /** @@ -714,18 +715,7 @@ 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) - { - return elementCache().getColumnHeaderCell(fieldLabel).getAttribute("title"); + return columnHeader.getDomAttribute("class").contains("phi-protected"); } public Optional getGridEmptyMessage() @@ -749,6 +739,11 @@ public Optional getGridEmptyMessage() return msg; } + List getHeaders() + { + return Collections.unmodifiableList(elementCache().findHeaders()); + } + /** * supports chaining between base and derived instances * @return magic @@ -758,22 +753,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,69 +785,106 @@ public void toggle() } }; - private final Map headerCells = new HashMap<>(); - protected final WebElement getColumnHeaderCell(String headerText) + protected final WebElement getColumnHeaderCell(String fieldIdentifier) { - if (!headerCells.containsKey(headerText)) - { - WebElement headerCell = Locators.headerCellBody(headerText).findElement(this); - headerCells.put(headerText, headerCell); - } - return headerCells.get(headerText); + return findColumnHeader(fieldIdentifier).getElement(); } - protected List fieldLabels; - protected Map indexes; - protected Map initColumnsAndIndices() + private final List columnHeaders = new ArrayList<>(); + private final Map fieldKeys = new LinkedHashMap<>(); + private final Map fieldLabels = new LinkedHashMap<>(); + protected List findHeaders() { - if (fieldLabels == null || indexes == null) + if (columnHeaders.isEmpty()) { 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)); + columnHeaders.add(new ColumnHeader(headerCellElements.get(domIndex), domIndex)); } } - return indexes; + return columnHeaders; } - protected int getColumnIndex(String fieldLabel) + /** + * Find field by uncertain field identifier in order of precedence: + *
    + *
  1. Encoded fieldKey
  2. + *
  3. Unencoded fieldKey
  4. + *
  5. Field Label
  6. + *
+ */ + protected ColumnHeader findColumnHeader(String fieldIdentifier) { - final ColumnIndex columnIndex = initColumnsAndIndices().get(fieldLabel); - if (columnIndex == null) + FieldKey possibleFieldKey = FieldKey.fromParts(fieldIdentifier); + + List headers = findHeaders(); + if (fieldKeys.containsKey(fieldIdentifier)) + { + return fieldKeys.get(fieldIdentifier); + } + else if (fieldKeys.containsKey(possibleFieldKey.toString())) + { + return fieldKeys.get(possibleFieldKey.toString()); + } + else if (fieldLabels.containsKey(fieldIdentifier)) { - throw new NoSuchElementException(String.format("Column not found: '%s'.\nKnown columns: %s", - fieldLabel, String.join(", ", initColumnsAndIndices().keySet()))); + return fieldLabels.get(fieldIdentifier); + } + else + { + if (fieldKeys.size() < headers.size()) + { + // Check whether fieldIdentifier is an encoded fieldKey + for (ColumnHeader header : headers) + { + if (!fieldKeys.containsValue(header)) + { + String fieldKey = header.getFieldKey(); + fieldKeys.put(fieldKey, header); + if (fieldKey.equals(fieldIdentifier)) + { + return header; + } + } + } + + // Check whether fieldIdentifier is an unencoded fieldKey + if (fieldKeys.containsKey(possibleFieldKey.toString())) + { + return fieldKeys.get(possibleFieldKey.toString()); + } + } + + if (fieldLabels.size() < headers.size()) + { + // Check whether fieldIdentifier is a field label + for (ColumnHeader header : headers) + { + if (!fieldLabels.containsValue(header)) + { + String columnLabel = header.getColumnLabel(); + fieldLabels.put(columnLabel, header); + if (columnLabel.equals(fieldIdentifier)) + { + return header; + } + } + } + } + + throw new NoSuchElementException("No such column with fieldKey or label: " + fieldIdentifier); } - return columnIndex.getRawIndex(); } - protected List getColumnLabels() + protected int getColumnIndex(String fieldIdentifier) { - initColumnsAndIndices(); - return fieldLabels; + return findColumnHeader(fieldIdentifier).getDomIndex(); } - protected List> mapList; - protected List gridRows; - private List> _initGridData() + protected List getColumnLabels() { - List> rowMaps = new ArrayList<>(); - gridRows = getRows(); - for(GridRow row : gridRows) - { - rowMaps.add(row.getRowMapByLabel()); - } - return rowMaps; + return findHeaders().stream().map(ColumnHeader::getColumnLabel).collect(Collectors.toList()); } protected GridRow getRow(int index) @@ -886,18 +902,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(String fieldIdentifier, String text) { - // try to normalize column index to start at 0, excluding row selector column - int columnIndex = getColumnIndex(fieldLabel); + int columnIndex = getColumnIndex(fieldIdentifier); return new GridRow.GridRowFinder(ResponsiveGrid.this).withTextAtColumn(text, columnIndex) .find(this); } - protected Optional getOptionalRow(String fieldLabel, String text) + protected Optional getOptionalRow(String fieldIdentifier, String text) { - // try to normalize column index to start at 0, excluding row selector column - int columnIndex = getColumnIndex(fieldLabel); + int columnIndex = getColumnIndex(fieldIdentifier); return new GridRow.GridRowFinder(ResponsiveGrid.this).withTextAtColumn(text, columnIndex) .findOptional(this); } @@ -914,9 +928,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 +1008,45 @@ protected Locator locator() } } - static class ColumnIndex + protected static class ColumnHeader { - private final Integer _rawIndex; - private final Integer _normalizedIndex; - private final String _fieldLabel; + private final WebElement _element; + private final Integer _domIndex; + private final Mutable _fieldLabel = new MutableObject<>(); + private final Mutable _fieldKey = new MutableObject<>(); - /** - * 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) + private ColumnHeader(WebElement element, int domIndex) { - _fieldLabel = fieldLabel; - _rawIndex = rawIndex; - _normalizedIndex = normalizedIndex; + _element = element; + _domIndex = domIndex; } - public String getColumnLabel() + public WebElement getElement() + { + return _element; + } + + public String getFieldKey() { - return _fieldLabel; + if (_fieldKey.getValue() == null) + { + _fieldKey.setValue(StringUtils.trimToEmpty(_element.getDomAttribute("data-fieldkey"))); + } + return _fieldKey.getValue(); } - public Integer getRawIndex() + + public String getColumnLabel() { - return _rawIndex; + if (_fieldLabel.getValue() == null) + { + _fieldLabel.setValue(WebElementUtils.getTextContent(getElement()).trim()); + } + return _fieldLabel.getValue(); } - public Integer getNormalizedIndex() + + public Integer getDomIndex() { - return _normalizedIndex; + return _domIndex; } } } diff --git a/src/org/labkey/test/params/FieldKey.java b/src/org/labkey/test/params/FieldKey.java index 85232d5b8f..8150d3305b 100644 --- a/src/org/labkey/test/params/FieldKey.java +++ b/src/org/labkey/test/params/FieldKey.java @@ -5,6 +5,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Objects; public class FieldKey { @@ -38,11 +39,6 @@ public static FieldKey fromParts(String... parts) return fromParts(Arrays.asList(parts)); } - public static FieldKey fromPath(String path) - { - return fromParts(path.split("/")); - } - public static FieldKey fromFieldKey(String path) { return fromParts(Arrays.stream(path.split("/")).map(FieldKey::decodePart).toList()); @@ -104,4 +100,17 @@ public String toString() { return String.join("/", getParts(true)); } + + @Override + public boolean equals(Object o) + { + if (!(o instanceof FieldKey fieldKey)) return false; + return Objects.equals(_name, fieldKey._name) && Objects.equals(_parent, fieldKey._parent); + } + + @Override + public int hashCode() + { + return Objects.hash(_parent, _name); + } } From 0057e84c080df81baba06949e2e00f0e1081bf0a Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Fri, 9 May 2025 17:50:31 -0700 Subject: [PATCH 12/34] Update parameter names --- .../test/components/ui/grids/GridRow.java | 34 +++- .../test/components/ui/grids/QueryGrid.java | 13 +- .../components/ui/grids/ResponsiveGrid.java | 162 ++++++++---------- src/org/labkey/test/params/FieldKey.java | 5 + 4 files changed, 111 insertions(+), 103 deletions(-) diff --git a/src/org/labkey/test/components/ui/grids/GridRow.java b/src/org/labkey/test/components/ui/grids/GridRow.java index 46225b19c3..6cd99e7be7 100644 --- a/src/org/labkey/test/components/ui/grids/GridRow.java +++ b/src/org/labkey/test/components/ui/grids/GridRow.java @@ -7,8 +7,10 @@ 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.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; @@ -126,9 +128,9 @@ 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(String columnIdentifier) { - return elementCache().waitForAttachment(fieldLabel).viewImgFile(); + return elementCache().waitForAttachment(columnIdentifier).viewImgFile(); } /** @@ -166,6 +168,22 @@ public Map getRowMapByLabel() return getRowMap(ResponsiveGrid.ColumnHeader::getColumnLabel); } + /** + * gets a map of the row's values, keyed by column name + */ + public Map getRowMapByName() + { + return getRowMap(columnHeader -> columnHeader.getFieldKey().getName()); + } + + /** + * gets a map of the row's values, keyed by column fieldKey + */ + public Map getRowMapByFieldKey() + { + return getRowMap(columnHeader -> columnHeader.getFieldKey()); + } + Map getRowMap(Function keyMapper) { List columnValues = elementCache().getCellTexts(); @@ -175,7 +193,17 @@ Map getRowMap(Function keyMapper) for (ResponsiveGrid.ColumnHeader header : headers) { - rowMap.put(keyMapper.apply(header), columnValues.get(header.getDomIndex())); + 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 + { + rowMap.put(key, value); + } } return rowMap; diff --git a/src/org/labkey/test/components/ui/grids/QueryGrid.java b/src/org/labkey/test/components/ui/grids/QueryGrid.java index 78e6eb21eb..86cd8e3d0a 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(String 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(String columnIdentifier, String text, boolean checked) { - getRow(fieldLabel, text).select(checked); + getRow(columnIdentifier, text).select(checked); return this; } diff --git a/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java b/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java index 5aa727c1e9..aad741d063 100644 --- a/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java +++ b/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java @@ -118,39 +118,39 @@ public void scrollToOrigin() /** * Sorts from the grid header menu - * @param fieldLabel column header for + * @param columnIdentifier column header for * @return this grid */ - public T sortColumnAscending(String fieldLabel) + public T sortColumnAscending(String columnIdentifier) { - sortColumn(fieldLabel, SortDirection.ASC); + sortColumn(columnIdentifier, SortDirection.ASC); return getThis(); } /** * Sorts from the grid header menu - * @param fieldLabel Text of column + * @param columnIdentifier Text of column * @return this grid */ - public T sortColumnDescending(String fieldLabel) + public T sortColumnDescending(String columnIdentifier) { - sortColumn(fieldLabel, SortDirection.DESC); + sortColumn(columnIdentifier, SortDirection.DESC); return getThis(); } - public void sortColumn(String fieldLabel, SortDirection direction) + public void sortColumn(String 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) + public void clearSort(String columnIdentifier) { - clickColumnMenuItem(fieldLabel, "Clear sort", true); + clickColumnMenuItem(columnIdentifier, "Clear sort", true); } - public boolean hasColumnSortIcon(String fieldLabel) + public boolean hasColumnSortIcon(String 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") @@ -159,23 +159,23 @@ public boolean hasColumnSortIcon(String fieldLabel) } - public T filterColumn(String fieldLabel, Filter.Operator operator) + public T filterColumn(String columnIdentifier, Filter.Operator operator) { - return filterColumn(fieldLabel, operator, null); + return filterColumn(columnIdentifier, operator, null); } - public T filterColumn(String fieldLabel, Filter.Operator operator, Object value) + public T filterColumn(String 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) + public T filterColumn(String 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) @@ -186,11 +186,11 @@ public T filterColumn(String fieldLabel, Filter.Operator operator1, Object value return _this; } - public T filterBooleanColumn(String fieldLabel, boolean value) + public T filterBooleanColumn(String 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(); @@ -198,32 +198,32 @@ public T filterBooleanColumn(String fieldLabel, boolean value) return _this; } - public String filterColumnExpectingError(String fieldLabel, Filter.Operator operator, Object value) + public String filterColumnExpectingError(String 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(String 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) + public T removeColumnFilter(String columnIdentifier) { - clickColumnMenuItem(fieldLabel, "Remove filter", true); + clickColumnMenuItem(columnIdentifier, "Remove filter", true); return getThis(); } - public boolean hasColumnFilterIcon(String fieldLabel) + public boolean hasColumnFilterIcon(String 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); @@ -234,13 +234,13 @@ public boolean hasColumnFilterIcon(String fieldLabel) /** * use the column menu to hide the given column. * - * @param fieldLabel Column to hide. + * @param columnIdentifier Column to hide. * @return This grid. */ - public T hideColumn(String fieldLabel) + public T hideColumn(String 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(); } @@ -251,24 +251,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 The column to get the menu from. * @return A {@link FieldSelectionDialog} */ - public FieldSelectionDialog insertColumn(String fieldLabel) + public FieldSelectionDialog insertColumn(String 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(String columnIdentifier, String menuText, boolean waitForUpdate) { if(hasLockedColumn()) @@ -276,7 +276,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); @@ -297,13 +297,13 @@ protected void clickColumnMenuItem(String fieldLabel, String menuText, boolean w waitFor(()-> !menuItem.isDisplayed(), 1000); } - public void editColumnLabel(String fieldLabel, String newColumnLabel) + public void editColumnLabel(String 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); @@ -361,16 +361,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 header text of the specified 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(String 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(); } @@ -395,16 +395,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 Header text of the column to search * @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(String columnIdentifier, Collection texts, boolean checked) { for (String text : texts) { - selectRow(fieldLabel, text, checked); + selectRow(columnIdentifier, text, checked); } return getThis(); } @@ -419,29 +419,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; @@ -536,24 +513,24 @@ 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 The exact text of the column header * @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(String 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 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(String columnIdentifier, String text) { - return elementCache().getOptionalRow(fieldLabel, text); + return elementCache().getOptionalRow(columnIdentifier, text); } /** @@ -585,12 +562,12 @@ public List getRows() return elementCache().getRows(); } - public List getColumnDataAsText(String fieldLabel) + public List getColumnDataAsText(String columnIdentifier) { List columnData = new ArrayList<>(); for (GridRow row : getRows()) { - columnData.add(row.getText(fieldLabel)); + columnData.add(row.getText(columnIdentifier)); } return columnData; } @@ -609,9 +586,9 @@ public boolean hasSelectColumn() * To get the normalized index (which excludes selector rows if present) use * elementCache().indexes.get(column).getNormalizedIndex() */ - protected Integer getColumnIndex(String fieldLabel) + protected Integer getColumnIndex(String columnIdentifier) { - return elementCache().getColumnIndex(fieldLabel); + return elementCache().getColumnIndex(columnIdentifier); } /** @@ -650,9 +627,9 @@ public Map getRowMapByLabel(int rowIndex) /** * Get text from the specified column in the specified row */ - public String getCellText(int rowIndex, String fieldLabel) + public String getCellText(int rowIndex, String columnIdentifier) { - return getRow(rowIndex).getText(fieldLabel); + return getRow(rowIndex).getText(columnIdentifier); } /** @@ -675,12 +652,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 column in which to search * @param text text for link to match */ - public void clickLink(String fieldLabel, String text) + public void clickLink(String columnIdentifier, String text) { - getRow(fieldLabel, text).clickLink(text); + getRow(columnIdentifier, text).clickLink(text); } public boolean gridMessagePresent() @@ -708,13 +685,12 @@ public String getGridError() /** The responsiveGrid now supports redacting fields * - * @param fieldLabel the column label. (uses starts-with matching) + * @param columnIdentifier the column label. * @return true if the specified grid header cell has the 'phi-protected' class on it */ - public boolean getColumnPHIProtected(String fieldLabel) + public boolean getColumnPHIProtected(String columnIdentifier) { - WebElement columnHeader = Locator.tagWithClass("th", "grid-header-cell") - .withDescendant(Locators.headerCellBody(fieldLabel)).findElement(this); + WebElement columnHeader = elementCache().getColumnHeaderCell(columnIdentifier); return columnHeader.getDomAttribute("class").contains("phi-protected"); } @@ -840,7 +816,7 @@ else if (fieldLabels.containsKey(fieldIdentifier)) { if (!fieldKeys.containsValue(header)) { - String fieldKey = header.getFieldKey(); + String fieldKey = header.getFieldKey().toString(); fieldKeys.put(fieldKey, header); if (fieldKey.equals(fieldIdentifier)) { @@ -1013,7 +989,7 @@ protected static class ColumnHeader private final WebElement _element; private final Integer _domIndex; private final Mutable _fieldLabel = new MutableObject<>(); - private final Mutable _fieldKey = new MutableObject<>(); + private final Mutable _fieldKey = new MutableObject<>(); private ColumnHeader(WebElement element, int domIndex) { @@ -1026,11 +1002,11 @@ public WebElement getElement() return _element; } - public String getFieldKey() + public FieldKey getFieldKey() { if (_fieldKey.getValue() == null) { - _fieldKey.setValue(StringUtils.trimToEmpty(_element.getDomAttribute("data-fieldkey"))); + _fieldKey.setValue(FieldKey.fromFieldKey(StringUtils.trimToEmpty(_element.getDomAttribute("data-fieldkey")))); } return _fieldKey.getValue(); } diff --git a/src/org/labkey/test/params/FieldKey.java b/src/org/labkey/test/params/FieldKey.java index 8150d3305b..7d5494530a 100644 --- a/src/org/labkey/test/params/FieldKey.java +++ b/src/org/labkey/test/params/FieldKey.java @@ -95,6 +95,11 @@ public List getHierarchy() } } + public String getName() + { + return _name; + } + @Override public String toString() { From 70160fc7ef413a9ef6d8f1675b1cb27da3ca0167 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Mon, 12 May 2025 14:14:23 -0700 Subject: [PATCH 13/34] More fixes --- .../components/ui/grids/ResponsiveGrid.java | 18 ++++++++++++++++-- .../labkey/test/util/TestDataGenerator.java | 2 +- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java b/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java index aad741d063..2166c912b6 100644 --- a/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java +++ b/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java @@ -865,7 +865,7 @@ protected List getColumnLabels() protected GridRow getRow(int index) { - return new GridRow.GridRowFinder(ResponsiveGrid.this).index(index).find(this); + return getRows().get(index); } protected GridRow getRow(String text) @@ -1006,7 +1006,21 @@ public FieldKey getFieldKey() { if (_fieldKey.getValue() == null) { - _fieldKey.setValue(FieldKey.fromFieldKey(StringUtils.trimToEmpty(_element.getDomAttribute("data-fieldkey")))); + 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 + _element.getDomAttribute("id"); + } + + if (path != null) + { + _fieldKey.setValue(FieldKey.fromFieldKey(path)); + } + else + { + _fieldKey.setValue(FieldKey.ROOT); + } } return _fieldKey.getValue(); } diff --git a/src/org/labkey/test/util/TestDataGenerator.java b/src/org/labkey/test/util/TestDataGenerator.java index 92bd4f8710..f83441c300 100644 --- a/src/org/labkey/test/util/TestDataGenerator.java +++ b/src/org/labkey/test/util/TestDataGenerator.java @@ -699,7 +699,7 @@ public File writeGeneratedDataToExcel(String sheetName, String fileName) throws */ public File writeData(String fileName) { - String fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1); + String fileExtension = fileName.toLowerCase().substring(fileName.lastIndexOf('.') + 1); switch (fileExtension) { case "xlsx": From b059de910c303e6a135e142cd1dee8e7226c9f14 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Mon, 12 May 2025 16:10:31 -0700 Subject: [PATCH 14/34] EditableGrid --- .../ui/entities/EntityBulkInsertDialog.java | 2 +- .../ui/entities/EntityInsertPanel.java | 4 +- .../components/ui/grids/EditableGrid.java | 198 ++++++++++++++---- .../test/components/ui/grids/GridRow.java | 2 +- .../components/ui/grids/ResponsiveGrid.java | 4 +- 5 files changed, 164 insertions(+), 46 deletions(-) diff --git a/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java b/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java index b0c4f625bf..dfa589580e 100644 --- a/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java +++ b/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java @@ -235,7 +235,7 @@ else if (field.getType() == FieldDefinition.ColumnType.Integer || field.getType( 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); + setSelectionField(fieldKey, (List) value); else setTextField(fieldKey, (String) value); } diff --git a/src/org/labkey/test/components/ui/entities/EntityInsertPanel.java b/src/org/labkey/test/components/ui/entities/EntityInsertPanel.java index fb98170008..68054c65d4 100644 --- a/src/org/labkey/test/components/ui/entities/EntityInsertPanel.java +++ b/src/org/labkey/test/components/ui/entities/EntityInsertPanel.java @@ -110,10 +110,10 @@ public EntityInsertPanel addSource(String sourceType) return this; } - public EntityInsertPanel removeColumn(String columnName) + public EntityInsertPanel removeColumn(String columnIdentifier) { showGrid(); - elementCache().grid.removeColumn(columnName); + elementCache().grid.removeColumn(columnIdentifier); return this; } diff --git a/src/org/labkey/test/components/ui/grids/EditableGrid.java b/src/org/labkey/test/components/ui/grids/EditableGrid.java index d57159c124..c884f0d727 100644 --- a/src/org/labkey/test/components/ui/grids/EditableGrid.java +++ b/src/org/labkey/test/components/ui/grids/EditableGrid.java @@ -1,13 +1,13 @@ package org.labkey.test.components.ui.grids; -import org.apache.commons.csv.CSVFormat; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.mutable.Mutable; +import org.apache.commons.lang3.mutable.MutableObject; import org.assertj.core.api.Assertions; import org.jetbrains.annotations.Nullable; import org.labkey.test.Locator; import org.labkey.test.WebDriverWrapper; import org.labkey.test.components.Component; -import org.labkey.test.components.UpdatingComponent; import org.labkey.test.components.WebDriverComponent; import org.labkey.test.components.html.Checkbox; import org.labkey.test.components.html.Input; @@ -38,13 +38,12 @@ import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.TimeZone; import java.util.stream.Collectors; @@ -137,11 +136,11 @@ protected Integer getColumnIndex(String fieldLabel) throw new NotFoundException("Column not found in grid: " + fieldLabel + ". Found: " + fieldLabels); } - public EditableGrid removeColumn(String fieldLabel) + public EditableGrid removeColumn(String columnIdentifier) { doAndWaitForColumnUpdate(() -> { - WebElement headerCell = elementCache().getGridCellHeader(fieldLabel); + WebElement headerCell = elementCache().getColumnHeaderCell(columnIdentifier); Locator.byClass("fa-chevron-circle-down").findElement(headerCell).click(); Locator.tagWithText("a", "Remove Column").findElement(headerCell).click(); }); @@ -1112,13 +1111,13 @@ private void doAndWaitForRowCountUpdate(Runnable func) */ public void doAndWaitForColumnUpdate(Runnable func) { - int initialCount = elementCache().findColumnHeaders().size(); + int initialCount = elementCache().findHeaders().size(); func.run(); waitFor(() -> { clearElementCache(); - return elementCache().findColumnHeaders().size() != initialCount; + return elementCache().findHeaders().size() != initialCount; }, "Failed to add/remove column", 5_000); } @@ -1138,64 +1137,121 @@ 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(String fieldIdentifier) { - return Locator.byClass("grid-header-cell").withDescendant(Locator.tagContainingText("span", label)).findElement(table); + return findColumnHeader(fieldIdentifier).getElement(); } - private List columnHeaders = Collections.emptyList(); - private List fieldLabels = Collections.emptyList(); - private List fieldKeys = Collections.emptyList(); - - public List getColumnLabels() + private final List columnHeaders = new ArrayList<>(); + private final Map fieldKeys = new LinkedHashMap<>(); + private final Map fieldLabels = new LinkedHashMap<>(); + protected List findHeaders() { - if (fieldLabels.isEmpty()) + if (columnHeaders.isEmpty()) { - fieldLabels = new ArrayList<>(findColumnHeaders().stream().map(this::getLabelFromHeaderCell).toList()); - int rowNumberColumn = 0; + List headerCellElements = Locators.headerCells.waitForElements(table, WAIT_FOR_JAVASCRIPT); + int domIndex = 0; + if (hasSelectColumn()) { - fieldLabels.set(0, SELECT_COLUMN_HEADER); - rowNumberColumn = 1; + columnHeaders.add(new ColumnHeader(headerCellElements.get(0), domIndex, SELECT_COLUMN_HEADER)); + domIndex++; } - if (fieldLabels.get(rowNumberColumn).trim().isEmpty()) + for (; domIndex < headerCellElements.size(); domIndex++) { - fieldLabels.set(rowNumberColumn, ROW_NUMBER_COLUMN_HEADER); + columnHeaders.add(new ColumnHeader(headerCellElements.get(domIndex), domIndex)); } - fieldLabels = Collections.unmodifiableList(fieldLabels); } - return fieldLabels; + return columnHeaders; } - public List getColumnFieldKeys() + /** + * Find field by uncertain field identifier in order of precedence: + *
    + *
  1. Encoded fieldKey
  2. + *
  3. Unencoded fieldKey
  4. + *
  5. Field Label
  6. + *
+ */ + protected ColumnHeader findColumnHeader(String fieldIdentifier) { - if (fieldKeys.isEmpty()) + FieldKey possibleFieldKey = FieldKey.fromParts(fieldIdentifier); + + List headers = findHeaders(); + if (fieldKeys.containsKey(fieldIdentifier)) + { + return fieldKeys.get(fieldIdentifier); + } + else if (fieldKeys.containsKey(possibleFieldKey.toString())) { - fieldKeys = findColumnHeaders().stream().map(el -> - Optional.ofNullable(el.getDomAttribute("id")) - .map(FieldKey::fromFieldKey) - .orElse(null)).toList(); + return fieldKeys.get(possibleFieldKey.toString()); + } + else if (fieldLabels.containsKey(fieldIdentifier)) + { + return fieldLabels.get(fieldIdentifier); + } + else + { + if (fieldKeys.size() < headers.size()) + { + // Check whether fieldIdentifier is an encoded fieldKey + for (ColumnHeader header : headers) + { + if (!fieldKeys.containsValue(header)) + { + String fieldKey = header.getFieldKey().toString(); + fieldKeys.put(fieldKey, header); + if (fieldKey.equals(fieldIdentifier)) + { + return header; + } + } + } + + // Check whether fieldIdentifier is an unencoded fieldKey + if (fieldKeys.containsKey(possibleFieldKey.toString())) + { + return fieldKeys.get(possibleFieldKey.toString()); + } + } + + if (fieldLabels.size() < headers.size()) + { + // Check whether fieldIdentifier is a field label + for (ColumnHeader header : headers) + { + if (!fieldLabels.containsValue(header)) + { + String columnLabel = header.getColumnLabel(); + fieldLabels.put(columnLabel, header); + if (columnLabel.equals(fieldIdentifier)) + { + return header; + } + } + } + } + + throw new NoSuchElementException("No such column with fieldKey or label: " + fieldIdentifier); } - return fieldKeys; } - protected List findColumnHeaders() + protected int getColumnIndex(String fieldIdentifier) { - if (columnHeaders.isEmpty()) - { - columnHeaders = Locators.headerCells.waitForElements(table, WAIT_FOR_JAVASCRIPT); - fieldLabels = new ArrayList<>(); - fieldKeys = new ArrayList<>(); - } - return columnHeaders; + return findColumnHeader(fieldIdentifier).getDomIndex(); + } + + protected List getColumnLabels() + { + return findHeaders().stream().map(ColumnHeader::getColumnLabel).collect(Collectors.toList()); } /** * 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) + static String getLabelFromHeaderCell(WebElement el) { // Use text nodes to ignore browser whitespace formatting List textNodes = WebElementUtils.getTextNodesWithin(el); @@ -1279,4 +1335,66 @@ protected Locator locator() return _locator; } } + + protected static class ColumnHeader + { + private final WebElement _element; + private final Integer _domIndex; + private final Mutable _fieldLabel = new MutableObject<>(); + private final Mutable _fieldKey = new MutableObject<>(); + + private ColumnHeader(WebElement element, int domIndex) + { + _element = element; + _domIndex = domIndex; + } + + private ColumnHeader(WebElement element, int domIndex, String label) + { + this(element, domIndex); + _fieldLabel.setValue(label); + } + + 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.ROOT); + } + } + return _fieldKey.getValue(); + } + + public String getColumnLabel() + { + if (_fieldLabel.getValue() == null) + { + _fieldLabel.setValue(ElementCache.getLabelFromHeaderCell(getElement()).trim()); + } + return _fieldLabel.getValue(); + } + + public Integer getDomIndex() + { + return _domIndex; + } + } } diff --git a/src/org/labkey/test/components/ui/grids/GridRow.java b/src/org/labkey/test/components/ui/grids/GridRow.java index 6cd99e7be7..7f6b11e6e5 100644 --- a/src/org/labkey/test/components/ui/grids/GridRow.java +++ b/src/org/labkey/test/components/ui/grids/GridRow.java @@ -181,7 +181,7 @@ public Map getRowMapByName() */ public Map getRowMapByFieldKey() { - return getRowMap(columnHeader -> columnHeader.getFieldKey()); + return getRowMap(ResponsiveGrid.ColumnHeader::getFieldKey); } Map getRowMap(Function keyMapper) diff --git a/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java b/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java index 2166c912b6..ab79221e4b 100644 --- a/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java +++ b/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java @@ -1007,10 +1007,10 @@ public FieldKey getFieldKey() if (_fieldKey.getValue() == null) { String path = _element.getDomAttribute("data-fieldkey"); - if (path != null) + if (path == null) { // Some grids don't have a field key, but have a similar value in the ID attribute - _element.getDomAttribute("id"); + path = _element.getDomAttribute("id"); } if (path != null) From 22f83eca468a59001a40dbad3dad5f15628cbbad Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Mon, 12 May 2025 16:54:57 -0700 Subject: [PATCH 15/34] Field selection fixes --- .../test/components/ui/grids/FieldSelectionDialog.java | 2 ++ src/org/labkey/test/params/FieldKey.java | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/src/org/labkey/test/components/ui/grids/FieldSelectionDialog.java b/src/org/labkey/test/components/ui/grids/FieldSelectionDialog.java index 24339b6a76..7e17c3b9bf 100644 --- a/src/org/labkey/test/components/ui/grids/FieldSelectionDialog.java +++ b/src/org/labkey/test/components/ui/grids/FieldSelectionDialog.java @@ -443,6 +443,8 @@ public FieldSelectionDialog setFieldLabel(FieldKey fieldKey, String newFieldLabe .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); diff --git a/src/org/labkey/test/params/FieldKey.java b/src/org/labkey/test/params/FieldKey.java index 7d5494530a..351051c8b1 100644 --- a/src/org/labkey/test/params/FieldKey.java +++ b/src/org/labkey/test/params/FieldKey.java @@ -100,6 +100,11 @@ public String getName() return _name; } + public String[] toArray() + { + return getParts(false).toArray(new String[0]); + } + @Override public String toString() { From 91807fecbcd2bd8d6429a34374fb599f438f4afe Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Tue, 13 May 2025 15:51:55 -0700 Subject: [PATCH 16/34] Caching and file format fixes --- .../labkey/test/components/react/Tabs.java | 5 +++ .../test/components/ui/grids/GridRow.java | 16 ++++---- .../components/ui/grids/ResponsiveGrid.java | 41 +++++++++---------- src/org/labkey/test/params/FieldKey.java | 4 +- 4 files changed, 35 insertions(+), 31 deletions(-) diff --git a/src/org/labkey/test/components/react/Tabs.java b/src/org/labkey/test/components/react/Tabs.java index cb8e1bae0c..2d0e5c51c5 100644 --- a/src/org/labkey/test/components/react/Tabs.java +++ b/src/org/labkey/test/components/react/Tabs.java @@ -95,6 +95,11 @@ public String getSelectedTabText() return elementCache().findSelectedTab().getText(); } + public String getSelectedTabKey() + { + return elementCache().findSelectedTab().getDomAttribute("data-event-key"); + } + @Override protected ElementCache newElementCache() { diff --git a/src/org/labkey/test/components/ui/grids/GridRow.java b/src/org/labkey/test/components/ui/grids/GridRow.java index 7f6b11e6e5..06a51ba248 100644 --- a/src/org/labkey/test/components/ui/grids/GridRow.java +++ b/src/org/labkey/test/components/ui/grids/GridRow.java @@ -84,9 +84,9 @@ public WebElement getCell(int colIndex) /** * gets the cell corresponding to the specified column */ - public WebElement getCell(String fieldIdentifier) + public WebElement getCell(String columnIdentifier) { - return getCell(_grid.getColumnIndex(fieldIdentifier)); + return getCell(_grid.getColumnIndex(columnIdentifier)); } /** @@ -136,17 +136,17 @@ public ImageFileViewDialog clickImgFile(String columnIdentifier) /** * finds a AttachmentCard specified filename, clicks it, and waits for the file to download */ - public File clickNonImgFile(String fieldIdentifier) + public File clickNonImgFile(String columnIdentifier) { - return elementCache().waitForAttachment(fieldIdentifier).clickOnNonImgFile(); + return elementCache().waitForAttachment(columnIdentifier).clickOnNonImgFile(); } /** * Returns the text in the row for the specified column */ - public String getText(String fieldIdentifier) + public String getText(String columnIdentifier) { - return getCell(fieldIdentifier).getText(); + return getCell(columnIdentifier).getText(); } /** @@ -252,9 +252,9 @@ protected List getCellTexts() return cellTexts; } - public AttachmentCard waitForAttachment(String fieldIdentifier) + public AttachmentCard waitForAttachment(String columnIdentifier) { - return new AttachmentCard.FileAttachmentCardFinder(getDriver()).waitFor(getCell(fieldIdentifier)); + return new AttachmentCard.FileAttachmentCardFinder(getDriver()).waitFor(getCell(columnIdentifier)); } } diff --git a/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java b/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java index ab79221e4b..118d77dbef 100644 --- a/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java +++ b/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java @@ -4,7 +4,6 @@ */ package org.labkey.test.components.ui.grids; -import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.mutable.Mutable; import org.apache.commons.lang3.mutable.MutableObject; import org.jetbrains.annotations.Nullable; @@ -761,9 +760,9 @@ public void toggle() } }; - protected final WebElement getColumnHeaderCell(String fieldIdentifier) + protected final WebElement getColumnHeaderCell(String columnIdentifier) { - return findColumnHeader(fieldIdentifier).getElement(); + return findColumnHeader(columnIdentifier).getElement(); } private final List columnHeaders = new ArrayList<>(); @@ -790,42 +789,42 @@ protected List findHeaders() *
  • Field Label
  • * */ - protected ColumnHeader findColumnHeader(String fieldIdentifier) + protected ColumnHeader findColumnHeader(String columnIdentifier) { - FieldKey possibleFieldKey = FieldKey.fromParts(fieldIdentifier); + FieldKey possibleFieldKey = FieldKey.fromParts(columnIdentifier); List headers = findHeaders(); - if (fieldKeys.containsKey(fieldIdentifier)) + if (fieldKeys.containsKey(columnIdentifier)) { - return fieldKeys.get(fieldIdentifier); + return fieldKeys.get(columnIdentifier); } else if (fieldKeys.containsKey(possibleFieldKey.toString())) { return fieldKeys.get(possibleFieldKey.toString()); } - else if (fieldLabels.containsKey(fieldIdentifier)) + else if (fieldLabels.containsKey(columnIdentifier)) { - return fieldLabels.get(fieldIdentifier); + return fieldLabels.get(columnIdentifier); } else { if (fieldKeys.size() < headers.size()) { - // Check whether fieldIdentifier is an encoded fieldKey + // Check whether columnIdentifier is an encoded fieldKey for (ColumnHeader header : headers) { if (!fieldKeys.containsValue(header)) { String fieldKey = header.getFieldKey().toString(); fieldKeys.put(fieldKey, header); - if (fieldKey.equals(fieldIdentifier)) + if (fieldKey.equals(columnIdentifier)) { return header; } } } - // Check whether fieldIdentifier is an unencoded fieldKey + // Check whether columnIdentifier is an unencoded fieldKey if (fieldKeys.containsKey(possibleFieldKey.toString())) { return fieldKeys.get(possibleFieldKey.toString()); @@ -834,14 +833,14 @@ else if (fieldLabels.containsKey(fieldIdentifier)) if (fieldLabels.size() < headers.size()) { - // Check whether fieldIdentifier is a field label + // Check whether columnIdentifier is a field label for (ColumnHeader header : headers) { if (!fieldLabels.containsValue(header)) { String columnLabel = header.getColumnLabel(); fieldLabels.put(columnLabel, header); - if (columnLabel.equals(fieldIdentifier)) + if (columnLabel.equals(columnIdentifier)) { return header; } @@ -849,13 +848,13 @@ else if (fieldLabels.containsKey(fieldIdentifier)) } } - throw new NoSuchElementException("No such column with fieldKey or label: " + fieldIdentifier); + throw new NoSuchElementException("No such column with fieldKey or label: " + columnIdentifier); } } - protected int getColumnIndex(String fieldIdentifier) + protected int getColumnIndex(String columnIdentifier) { - return findColumnHeader(fieldIdentifier).getDomIndex(); + return findColumnHeader(columnIdentifier).getDomIndex(); } protected List getColumnLabels() @@ -878,16 +877,16 @@ protected Optional getOptionalRow(String text) return new GridRow.GridRowFinder(ResponsiveGrid.this).withCellWithText(text).findOptional(this); } - protected GridRow getRow(String fieldIdentifier, String text) + protected GridRow getRow(String columnIdentifier, String text) { - int columnIndex = getColumnIndex(fieldIdentifier); + int columnIndex = getColumnIndex(columnIdentifier); return new GridRow.GridRowFinder(ResponsiveGrid.this).withTextAtColumn(text, columnIndex) .find(this); } - protected Optional getOptionalRow(String fieldIdentifier, String text) + protected Optional getOptionalRow(String columnIdentifier, String text) { - int columnIndex = getColumnIndex(fieldIdentifier); + int columnIndex = getColumnIndex(columnIdentifier); return new GridRow.GridRowFinder(ResponsiveGrid.this).withTextAtColumn(text, columnIndex) .findOptional(this); } diff --git a/src/org/labkey/test/params/FieldKey.java b/src/org/labkey/test/params/FieldKey.java index 351051c8b1..3634370a4f 100644 --- a/src/org/labkey/test/params/FieldKey.java +++ b/src/org/labkey/test/params/FieldKey.java @@ -39,9 +39,9 @@ public static FieldKey fromParts(String... parts) return fromParts(Arrays.asList(parts)); } - public static FieldKey fromFieldKey(String path) + public static FieldKey fromFieldKey(String fieldKey) { - return fromParts(Arrays.stream(path.split("/")).map(FieldKey::decodePart).toList()); + return fromParts(Arrays.stream(fieldKey.split("/")).map(FieldKey::decodePart).toList()); } private static final String[] ILLEGAL = {"$", "/", "&", "}", "~", ",", "."}; From cc27e83fe41500bcc1aba80ee201e4aa1a483add Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Tue, 13 May 2025 17:02:33 -0700 Subject: [PATCH 17/34] Use correct helper for CSVs --- .../labkey/test/util/data/TestDataUtils.java | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/org/labkey/test/util/data/TestDataUtils.java b/src/org/labkey/test/util/data/TestDataUtils.java index a06c45358b..132ce50c94 100644 --- a/src/org/labkey/test/util/data/TestDataUtils.java +++ b/src/org/labkey/test/util/data/TestDataUtils.java @@ -1,7 +1,9 @@ package org.labkey.test.util.data; import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVParser; import org.apache.commons.csv.CSVPrinter; +import org.apache.commons.csv.CSVRecord; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; @@ -13,16 +15,15 @@ import org.labkey.test.util.TestDataGenerator; import java.io.File; -import java.io.FileInputStream; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; +import java.io.Reader; import java.io.StringWriter; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -301,6 +302,26 @@ public static File writeRowsToCsv(String fileName, List> rows) throw return file; } + public static List> readRowsFromTsv(File file) throws IOException + { + return readRowsFromFile(file, CSVFormat.TDF); + } + + public static List> readRowsFromCsv(File file) throws IOException + { + return readRowsFromFile(file, CSVFormat.DEFAULT); + } + + public static List> readRowsFromFile(File file, CSVFormat format) throws IOException + { + try (Reader in = new FileReader(file, StandardCharsets.UTF_8)) + { + CSVParser parser = CSVParser.builder().setFormat(format).setReader(in).get(); + List records = parser.getRecords(); + return records.stream().map(CSVRecord::toList).toList(); + } + } + public static String stringFromRows(List> rows, CSVFormat format) { StringWriter stringWriter = new StringWriter(); From d2fdaf852203a3a9f4a3bcab55fe21c9b57e40b5 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Wed, 14 May 2025 09:56:47 -0700 Subject: [PATCH 18/34] Minor fixes --- .../test/components/ui/entities/ParentEntityEditPanel.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/org/labkey/test/components/ui/entities/ParentEntityEditPanel.java b/src/org/labkey/test/components/ui/entities/ParentEntityEditPanel.java index 24a81b7030..e71980e918 100644 --- a/src/org/labkey/test/components/ui/entities/ParentEntityEditPanel.java +++ b/src/org/labkey/test/components/ui/entities/ParentEntityEditPanel.java @@ -317,7 +317,7 @@ private BaseReactSelect.BaseReactSelectFinder getParentFin */ public FilteringReactSelect getParent(String typeName) { - return getParentFinder(typeName).find(elementCache()); + return getParentFinder(typeName).waitFor(elementCache()); } /** From 92c2b0b1d97aba3880933a4db502d1fca9000f99 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Wed, 14 May 2025 15:53:47 -0700 Subject: [PATCH 19/34] EntityBulkInsertDialog --- .../ui/entities/EntityBulkInsertDialog.java | 153 +++++++++++------- .../components/ui/grids/EditableGrid.java | 2 +- .../components/ui/grids/ResponsiveGrid.java | 20 ++- src/org/labkey/test/params/FieldKey.java | 119 +++++++++----- 4 files changed, 192 insertions(+), 102 deletions(-) diff --git a/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java b/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java index dfa589580e..26a388e8f6 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,13 +9,15 @@ 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.List; import java.util.Map; @@ -29,7 +30,7 @@ public EntityBulkInsertDialog(WebDriver driver) this(new ModalDialogFinder(driver).withTitle("Bulk Add")); } - private EntityBulkInsertDialog(ModalDialogFinder finder) + protected EntityBulkInsertDialog(ModalDialogFinder finder) { super(finder); } @@ -161,83 +162,94 @@ public String getDescription() return getWrapper().getFormElement(elementCache().description); } - public EntityBulkInsertDialog setTextField(String fieldKey, String value) + public EntityBulkInsertDialog setTextArea(CharSequence fieldIdentifier, String value) { - elementCache().textInput(fieldKey).set(value); + elementCache().textArea(fieldIdentifier).set(value); return this; } - public String getTextField(String fieldKey) + public String getTextArea(CharSequence fieldIdentifier) { - return elementCache().textInput(fieldKey).get(); + return elementCache().textArea(fieldIdentifier).get(); } - public EntityBulkInsertDialog setNumericField(String fieldKey, String value) + public EntityBulkInsertDialog setTextField(CharSequence fieldIdentifier, String value) { - elementCache().numericInput(fieldKey).set(value); + elementCache().textInput(fieldIdentifier).set(value); return this; } - public String getNumericField(String fieldKey) + public String getTextField(CharSequence fieldIdentifier) { - return elementCache().numericInput(fieldKey).get(); + return elementCache().textInput(fieldIdentifier).get(); } - public EntityBulkInsertDialog setSelectionField(String fieldCaption, List selectValues) + public EntityBulkInsertDialog setNumericField(CharSequence fieldIdentifier, String value) { - FilteringReactSelect reactSelect = elementCache().selectionField(fieldCaption); + elementCache().numericInput(fieldIdentifier).set(value); + return this; + } + + public String getNumericField(CharSequence fieldIdentifier) + { + return elementCache().numericInput(fieldIdentifier).get(); + } + + public EntityBulkInsertDialog setSelectionField(CharSequence fieldIdentifier, List selectValues) + { + FilteringReactSelect reactSelect = elementCache().selectionField(fieldIdentifier); selectValues.forEach(reactSelect::filterSelect); return this; } - public List getSelectionField(String fieldCaption) + 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 Name for the field. * @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) + // For use when the field is of an unknown type, as can occur in fuzz tests + public void setValue(FieldDefinition field, Object newValue) { - getWrapper().setFormElement(Locator.tagWithId("input", id), value); - return this; - } - - public String getFieldWithId(String id) - { - 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()); + String fieldName = 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(fieldKey, (List) value); - else - setTextField(fieldKey, (String) value); + + setValue(field, value); } } @@ -246,32 +258,44 @@ 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 Field to update. * @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) + public String getDateTimeField(CharSequence fieldIdentifier) { - return elementCache().dateInput(fieldKey).get(); + return elementCache().dateInput(fieldIdentifier).get(); } - public EntityBulkInsertDialog setBooleanField(String fieldKey, boolean checked) + 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) + public boolean getBooleanField(CharSequence fieldIdentifier) + { + return elementCache().checkBox(fieldIdentifier).get(); + } + + public EntityBulkInsertDialog attachFile(CharSequence fieldIdentifier, File file) + { + elementCache().fileUploadField(fieldIdentifier).attachFile(file); + return this; + } + + public EntityBulkInsertDialog removeFile(CharSequence fieldIdentifier) { - return elementCache().checkBox(fieldKey).get(); + elementCache().fileUploadField(fieldIdentifier).removeFile(); + return this; } /** @@ -374,42 +398,53 @@ 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"); - public WebElement formRow(String fieldKey) + public WebElement formRow(CharSequence fieldIdentifier) { + String fieldKey = fieldIdentifier instanceof FieldKey ? fieldIdentifier.toString() : FieldKey.encodePart(fieldIdentifier.toString()); return Locator.tagWithClass("div", "row") .withChild(Locator.tagWithAttribute("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(FieldKey.fromFieldKey(fieldIdentifier + "-fileUpload")), getDriver()); } public List fieldLabels() diff --git a/src/org/labkey/test/components/ui/grids/EditableGrid.java b/src/org/labkey/test/components/ui/grids/EditableGrid.java index c884f0d727..5aeef1ee14 100644 --- a/src/org/labkey/test/components/ui/grids/EditableGrid.java +++ b/src/org/labkey/test/components/ui/grids/EditableGrid.java @@ -1377,7 +1377,7 @@ public FieldKey getFieldKey() } else { - _fieldKey.setValue(FieldKey.ROOT); + _fieldKey.setValue(FieldKey.EMPTY); } } return _fieldKey.getValue(); diff --git a/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java b/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java index 118d77dbef..3ce02374e4 100644 --- a/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java +++ b/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java @@ -640,6 +640,24 @@ public List> getRowMapsByLabel() 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(); + } + /** * locates the first link in the grid with matching text * @param text the text to match @@ -1018,7 +1036,7 @@ public FieldKey getFieldKey() } else { - _fieldKey.setValue(FieldKey.ROOT); + _fieldKey.setValue(FieldKey.EMPTY); } } return _fieldKey.getValue(); diff --git a/src/org/labkey/test/params/FieldKey.java b/src/org/labkey/test/params/FieldKey.java index 3634370a4f..92489c6e2b 100644 --- a/src/org/labkey/test/params/FieldKey.java +++ b/src/org/labkey/test/params/FieldKey.java @@ -1,37 +1,52 @@ package org.labkey.test.params; import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; import java.util.List; -import java.util.Objects; -public class FieldKey +public class FieldKey implements CharSequence { - public static final FieldKey ROOT = new FieldKey(null, null); + public static final FieldKey EMPTY = new FieldKey(""); + 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 _encodedName; + private final String _fieldKey; - private FieldKey(FieldKey parent, String name) + private FieldKey(String name) { - _parent = parent; + _parent = null; _name = name; - _encodedName = encodePart(name); + _fieldKey = encodePart(name); + } + + private FieldKey(FieldKey parent, String child) + { + _parent = parent; + _name = parent.getName() + SEPARATOR + child; + _fieldKey = parent + SEPARATOR + encodePart(child); } public static FieldKey fromParts(List parts) { - if (parts.isEmpty()) - return ROOT; + FieldKey fieldKey = EMPTY; - if (parts.stream().anyMatch(StringUtils::isBlank)) - throw new IllegalArgumentException("parts contains blank: " + parts); + for (String part : parts) + { + if (StringUtils.isBlank(part)) + throw new IllegalArgumentException("FieldKey contains a blank part: " + parts); + fieldKey = fieldKey.child(part); + } - FieldKey parent = FieldKey.fromParts(parts.subList(0, parts.size() - 1)); - return new FieldKey(parent, parts.get(parts.size() - 1)); + return fieldKey; } public static FieldKey fromParts(String... parts) @@ -41,7 +56,15 @@ public static FieldKey fromParts(String... parts) public static FieldKey fromFieldKey(String fieldKey) { - return fromParts(Arrays.stream(fieldKey.split("/")).map(FieldKey::decodePart).toList()); + return fromParts(Arrays.stream(fieldKey.split(SEPARATOR)).map(FieldKey::decodePart).toList()); + } + + public static FieldKey fromChars(CharSequence fieldKey) + { + if (fieldKey instanceof FieldKey fk) + return fk; + else + return fromParts(fieldKey.toString()); } private static final String[] ILLEGAL = {"$", "/", "&", "}", "~", ",", "."}; @@ -62,37 +85,32 @@ public FieldKey getParent() return _parent; } - public FieldKey child(String fieldName) - { - return new FieldKey(this, fieldName); - } - - public List getParts(boolean encode) + public FieldKey child(String name) { - if (this == ROOT) + if (StringUtils.isBlank(getName())) { - return new ArrayList<>(); + return new FieldKey(name); } else { - List parts = _parent.getParts(encode); - parts.add(encode ? _encodedName : _name); - return parts; + return new FieldKey(this, name); } } - public List getHierarchy() + public Iterator getIterator() { - if (this == ROOT) - { - return new ArrayList<>(); - } - else + List ancestors = new ArrayList<>(); + FieldKey temp = this; + + while (temp.getParent() != null) { - List parts = _parent.getHierarchy(); - parts.add(this); - return parts; + ancestors.add(temp); + temp = temp.getParent(); } + + Collections.reverse(ancestors); + + return ancestors.iterator(); } public String getName() @@ -100,27 +118,46 @@ public String getName() return _name; } - public String[] toArray() + public String[] getNameArray() { - return getParts(false).toArray(new String[0]); + return _name.split(SEPARATOR); } @Override - public String toString() + public @NotNull String toString() { - return String.join("/", getParts(true)); + return _fieldKey; } @Override - public boolean equals(Object o) + 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 Objects.equals(_name, fieldKey._name) && Objects.equals(_parent, fieldKey._parent); + + return _fieldKey.equals(fieldKey._fieldKey); } @Override public int hashCode() { - return Objects.hash(_parent, _name); + return _fieldKey.hashCode(); } } From 05c33bdf68192565bd9a8575e4aeec1fbe18809d Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Wed, 14 May 2025 17:13:40 -0700 Subject: [PATCH 20/34] BatchUpdateSamplesDialog --- .../components/ui/entities/EntityBulkInsertDialog.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java b/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java index 26a388e8f6..dbaf9438c7 100644 --- a/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java +++ b/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java @@ -19,6 +19,7 @@ import java.io.File; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -400,12 +401,15 @@ protected class ElementCache extends ModalDialog.ElementCache { public final Locator validationMessage = Locator.tagWithClass("span", "validation-message"); + private final Map _rows = new HashMap<>(); + public WebElement formRow(CharSequence fieldIdentifier) { - String fieldKey = fieldIdentifier instanceof FieldKey ? fieldIdentifier.toString() : FieldKey.encodePart(fieldIdentifier.toString()); - return Locator.tagWithClass("div", "row") + String fieldKey = FieldKey.fromChars(fieldIdentifier).toString(); + return _rows.computeIfAbsent(fieldKey, fk -> + Locator.tagWithClass("div", "row") .withChild(Locator.tagWithAttribute("label", "for", fieldKey)) - .findElement(this); + .findElement(this)); } public FilteringReactSelect selectionField(CharSequence fieldIdentifier) From 224bc246a24e75a9dc9487574ca619671abf60d8 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Thu, 15 May 2025 08:28:09 -0700 Subject: [PATCH 21/34] FieldKey fixes --- .../components/ui/grids/FieldSelectionDialog.java | 11 +++-------- src/org/labkey/test/params/FieldKey.java | 4 ++-- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/org/labkey/test/components/ui/grids/FieldSelectionDialog.java b/src/org/labkey/test/components/ui/grids/FieldSelectionDialog.java index 7e17c3b9bf..37083d8a4b 100644 --- a/src/org/labkey/test/components/ui/grids/FieldSelectionDialog.java +++ b/src/org/labkey/test/components/ui/grids/FieldSelectionDialog.java @@ -8,7 +8,6 @@ import org.labkey.test.components.bootstrap.ModalDialog; import org.labkey.test.components.html.Checkbox; import org.labkey.test.params.FieldKey; -import org.labkey.test.util.EscapeUtil; import org.labkey.test.util.selenium.WebElementUtils; import org.openqa.selenium.Keys; import org.openqa.selenium.NoSuchElementException; @@ -19,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; @@ -158,13 +156,12 @@ public WebElement getAvailableFieldElement(String... fieldNameParts) */ private WebElement expandAvailableFields(String... fieldNameParts) { - StringBuilder fieldKey = new StringBuilder(); - - Iterator iterator = Arrays.stream(fieldNameParts).iterator(); + FieldKey fieldKey = FieldKey.fromParts(fieldNameParts); + 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()) @@ -172,8 +169,6 @@ private WebElement expandAvailableFields(String... fieldNameParts) // If the field is already expanded don't try to expand it. if(!isFieldKeyExpanded(elementCache().findAvailableField(fieldKey.toString()))) expandOrCollapseByFieldKey(fieldKey.toString(), true); - - fieldKey.append("/"); } } diff --git a/src/org/labkey/test/params/FieldKey.java b/src/org/labkey/test/params/FieldKey.java index 92489c6e2b..cbfd5c4118 100644 --- a/src/org/labkey/test/params/FieldKey.java +++ b/src/org/labkey/test/params/FieldKey.java @@ -102,7 +102,7 @@ public Iterator getIterator() List ancestors = new ArrayList<>(); FieldKey temp = this; - while (temp.getParent() != null) + while (temp != null) { ancestors.add(temp); temp = temp.getParent(); @@ -120,7 +120,7 @@ public String getName() public String[] getNameArray() { - return _name.split(SEPARATOR); + return Arrays.stream(_fieldKey.split(SEPARATOR)).map(FieldKey::decodePart).toArray(String[]::new); } @Override From bc49ab78317308e748dd1b3a94cf78940ef4fc3f Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Thu, 15 May 2025 12:17:02 -0700 Subject: [PATCH 22/34] EntityBulkUpdateDialog --- .../ui/entities/EntityBulkInsertDialog.java | 6 +- .../ui/entities/EntityBulkUpdateDialog.java | 157 ++++++++++-------- 2 files changed, 91 insertions(+), 72 deletions(-) diff --git a/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java b/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java index dbaf9438c7..de92bd20ed 100644 --- a/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java +++ b/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java @@ -24,6 +24,9 @@ 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) @@ -408,7 +411,8 @@ public WebElement formRow(CharSequence fieldIdentifier) String fieldKey = FieldKey.fromChars(fieldIdentifier).toString(); return _rows.computeIfAbsent(fieldKey, fk -> Locator.tagWithClass("div", "row") - .withChild(Locator.tagWithAttribute("label", "for", fieldKey)) + // TODO: Shouldn't need to be case-insensitive. Parent/source lookups have weird casing + .withChild(Locator.tagWithAttributeIgnoreCase("label", "for", fieldKey)) .findElement(this)); } diff --git a/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java b/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java index fa1db324e2..d4570c2321 100644 --- a/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java +++ b/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java @@ -14,20 +14,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 { @@ -57,18 +59,20 @@ public EntityBulkUpdateDialog adjustChangeCounter(int change) return this; } - // enable/disable field editable state - - public boolean isFieldEnabled(String fieldKey) + public boolean isFieldEnabled(CharSequence fieldIdentifier) { - return elementCache().getToggle(fieldKey).isOn(); + return elementCache().getToggle(fieldIdentifier).isOn(); } - public EntityBulkUpdateDialog setEditableState(String fieldKey, boolean enable) + 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; } @@ -81,121 +85,132 @@ 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) + public EntityBulkUpdateDialog setSelectionField(CharSequence fieldIdentifier, List selectValues) { - setEditableState(fieldKey, true); - FilteringReactSelect reactSelect = elementCache().getSelect(fieldKey); + setEditableState(fieldIdentifier, true); + FilteringReactSelect reactSelect = elementCache().getSelect(fieldIdentifier); WebDriverWrapper.waitFor(reactSelect::isEnabled, - "the ["+fieldKey+"] reactSelect did not become enabled in time", WAIT_TIMEOUT); + "the ["+fieldIdentifier+"] reactSelect did not become enabled in time", WAIT_TIMEOUT); selectValues.forEach(reactSelect::filterSelect); return this; } - public List getSelectionOptions(String fieldKey) + public EntityBulkUpdateDialog setSelectionField(CharSequence fieldIdentifier, String selectValue) + { + return setSelectionField(fieldIdentifier, List.of(selectValue)); + } + + public List getSelectionOptions(CharSequence fieldIdentifier) { - return enableAndWait(fieldKey, elementCache().getSelect(fieldKey)).getOptions(); + return enableAndWait(fieldIdentifier, elementCache().getSelect(fieldIdentifier)).getOptions(); } - public List getSelectionFieldValues(String fieldKey) + public List getSelectionFieldValues(CharSequence fieldIdentifier) { - return enableAndWait(fieldKey, elementCache().getSelect(fieldKey)).getSelections(); + return enableAndWait(fieldIdentifier, elementCache().getSelect(fieldIdentifier)).getSelections(); } - public EntityBulkUpdateDialog setTextArea(String fieldKey, String text) + public EntityBulkUpdateDialog setTextArea(CharSequence fieldIdentifier, String text) { - enableAndWait(fieldKey, elementCache().textArea(fieldKey)).set(text); + enableAndWait(fieldIdentifier, elementCache().textArea(fieldIdentifier)).set(text); return this; } - public String getTextArea(String fieldKey) + public String getTextArea(CharSequence fieldIdentifier) { - return elementCache().textArea(fieldKey).get(); + return elementCache().textArea(fieldIdentifier).get(); } // get/set text fields with ID - public EntityBulkUpdateDialog setTextField(String fieldKey, String value) + 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) + 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) + 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) + public String getNumericField(CharSequence fieldIdentifier) { - return elementCache().numericInput(fieldKey).get(); + return elementCache().numericInput(fieldIdentifier).get(); } - public EntityBulkUpdateDialog setDateField(String fieldKey, String dateString) + 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) + public String getDateField(CharSequence fieldIdentifier) { - return elementCache().dateInput(fieldKey).get(); + return elementCache().dateInput(fieldIdentifier).get(); } - public FileAttachmentContainer getFileField(String fieldKey) + public FileAttachmentContainer getFileField(CharSequence fieldIdentifier) { - return elementCache().fileUploadField(fieldKey); + fieldIdentifier = FieldKey.fromFieldKey(FieldKey.fromChars(fieldIdentifier) + "-fileUpload"); + return enableAndWait(fieldIdentifier, elementCache().fileUploadField(fieldIdentifier)); + } + + public EntityBulkUpdateDialog attachFile(CharSequence fieldIdentifier, File file) + { + getFileField(fieldIdentifier).attachFile(file); + return this; } - public EntityBulkUpdateDialog removeFile(String fieldKey) + public EntityBulkUpdateDialog removeFile(CharSequence fieldIdentifier) { - getFileField(fieldKey).removeFile(); - _changeCounter++; + getFileField(fieldIdentifier).removeFile(); return this; } - public EntityBulkUpdateDialog setBooleanField(String fieldKey, boolean checked) + public EntityBulkUpdateDialog setBooleanField(CharSequence fieldIdentifier, boolean checked) { - enableAndWait(fieldKey, getCheckBox(fieldKey)).set(checked); + enableAndWait(fieldIdentifier, getCheckBox(fieldIdentifier)).set(checked); return this; } - private > T enableAndWait(String fieldKey, T formItem) + private > T enableAndWait(CharSequence fieldIdentifier, T formItem) { - setEditableState(fieldKey, true); + setEditableState(fieldIdentifier, true); // "Clickable" means visible and enabled waiter().until(ExpectedConditions.elementToBeClickable(formItem.getComponentElement())); return formItem; } - public boolean getBooleanField(String fieldKey) + 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)); } @@ -214,7 +229,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) @@ -325,50 +340,50 @@ protected ElementCache elementCache() protected class ElementCache extends ModalDialog.ElementCache { - public WebElement formRow(String fieldKey) + public WebElement formRow(CharSequence fieldIdentifier) { + String fieldKey = FieldKey.fromChars(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 FilteringReactSelect.finder(getDriver()).refindWhenNeeded(formRow(fieldIdentifier)); } - 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"); From 3454ae51edbe0502107e844f14f8fc5717130903 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Thu, 15 May 2025 14:12:22 -0700 Subject: [PATCH 23/34] File and select fixes --- .../components/ManageSampleStatusesPanel.java | 2 +- .../ui/entities/EntityBulkInsertDialog.java | 5 ++--- .../ui/entities/EntityBulkUpdateDialog.java | 21 ++++++++++++------- 3 files changed, 17 insertions(+), 11 deletions(-) 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/ui/entities/EntityBulkInsertDialog.java b/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java index de92bd20ed..bdf06446da 100644 --- a/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java +++ b/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java @@ -96,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"); } } @@ -248,7 +248,6 @@ public void setInsertFieldValues(List fields, Map selectValues) { - setEditableState(fieldIdentifier, true); - FilteringReactSelect reactSelect = elementCache().getSelect(fieldIdentifier); - WebDriverWrapper.waitFor(reactSelect::isEnabled, - "the ["+fieldIdentifier+"] reactSelect did not become enabled in time", WAIT_TIMEOUT); + FilteringReactSelect reactSelect = enableSelectionField(fieldIdentifier); selectValues.forEach(reactSelect::filterSelect); return this; } @@ -117,12 +115,21 @@ public EntityBulkUpdateDialog setSelectionField(CharSequence fieldIdentifier, St public List getSelectionOptions(CharSequence fieldIdentifier) { - return enableAndWait(fieldIdentifier, elementCache().getSelect(fieldIdentifier)).getOptions(); + return enableSelectionField(fieldIdentifier).getOptions(); } public List getSelectionFieldValues(CharSequence fieldIdentifier) { - return enableAndWait(fieldIdentifier, elementCache().getSelect(fieldIdentifier)).getSelections(); + return enableSelectionField(fieldIdentifier).getSelections(); + } + + private @NotNull FilteringReactSelect enableSelectionField(CharSequence fieldIdentifier) + { + 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; } public EntityBulkUpdateDialog setTextArea(CharSequence fieldIdentifier, String text) @@ -355,7 +362,7 @@ public ToggleButton getToggle(CharSequence fieldIdentifier) public FilteringReactSelect getSelect(CharSequence fieldIdentifier) { - return FilteringReactSelect.finder(getDriver()).refindWhenNeeded(formRow(fieldIdentifier)); + return new FilteringReactSelect(formRow(fieldIdentifier), getDriver()); } public Input textInput(CharSequence fieldIdentifier) From 50a82e8f20d69d4ea65300dd5240c3d7b3ab6e3e Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Thu, 15 May 2025 17:11:15 -0700 Subject: [PATCH 24/34] DetailTable and conversion helpers --- .../test/components/ui/grids/DetailTable.java | 51 +++++++++++- .../components/ui/grids/EditableGrid.java | 79 ++++++++++++------- .../labkey/test/util/TestDataGenerator.java | 4 +- .../labkey/test/util/data/TestDataUtils.java | 32 +++++++- 4 files changed, 131 insertions(+), 35 deletions(-) diff --git a/src/org/labkey/test/components/ui/grids/DetailTable.java b/src/org/labkey/test/components/ui/grids/DetailTable.java index 41ceaa78e4..5bf3c13446 100644 --- a/src/org/labkey/test/components/ui/grids/DetailTable.java +++ b/src/org/labkey/test/components/ui/grids/DetailTable.java @@ -4,6 +4,7 @@ import org.labkey.test.WebDriverWrapper; import org.labkey.test.components.Component; import org.labkey.test.components.WebDriverComponent; +import org.labkey.test.params.FieldKey; import org.openqa.selenium.By; import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.StaleElementReferenceException; @@ -11,6 +12,7 @@ import org.openqa.selenium.WebElement; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -115,7 +117,7 @@ public String getFieldValue(String fieldlabel) } /** - * Gets the value of a cell identified by it's data-fieldKey attribute + * Gets the value of a cell identified by its data-fieldKey attribute * @param fieldKey value of the data-fieldKey attribute on the intended element * @return Text value of the specified element */ @@ -150,7 +152,7 @@ public void clickField(String fieldLabel) **/ public Map getTableDataByLabel() { - Map tableData = new HashMap<>(); + Map tableData = new LinkedHashMap<>(); for(WebElement tableRow : getComponentElement().findElements(By.cssSelector("tr"))) { @@ -162,6 +164,51 @@ public Map getTableDataByLabel() return tableData; } + /** + * Returns a map of the values in the grid. Data is keyed by column FieldKeys. + * + * @return A map with string values. + **/ + public Map getTableDataByFieldKey() + { + Map tableData = new LinkedHashMap<>(); + + for(WebElement tableRow : Locator.tag("tr").findElements(getComponentElement())) + { + WebElement dataCell = Locator.tag("td").withAttribute("data-fieldkey").findElement(tableRow); + + tableData.put(FieldKey.fromFieldKey(dataCell.getDomAttribute("data-fieldkey")), dataCell.getText()); + } + + return tableData; + } + + /** + * Returns a map of the values in the grid. Data is keyed by column names. + * Warning: Names are not guaranteed to be unique. + * + * @return A map with string values. + **/ + public Map getTableDataByName() + { + Map tableDataByFieldKey = getTableDataByFieldKey(); + Map tableDataByName = new LinkedHashMap<>(); + Map collisionChecker = new HashMap<>(); + + for (Map.Entry entry : tableDataByFieldKey.entrySet()) + { + String name = entry.getKey().getName(); + if (collisionChecker.containsKey(name)) + { + throw new IllegalStateException("Ambiguous field name '%s' from FieldKeys '%s' and '%s'." + .formatted(name, collisionChecker.get(name), entry.getKey())); + } + collisionChecker.put(name, entry.getKey()); + tableDataByName.put(name, entry.getValue()); + } + return tableDataByName; + } + protected static abstract class Locators { static final Locator.XPathLocator detailTable = Locator.tagWithClass("table", "detail-component--table__fixed"); diff --git a/src/org/labkey/test/components/ui/grids/EditableGrid.java b/src/org/labkey/test/components/ui/grids/EditableGrid.java index 5aeef1ee14..4bf442ecfe 100644 --- a/src/org/labkey/test/components/ui/grids/EditableGrid.java +++ b/src/org/labkey/test/components/ui/grids/EditableGrid.java @@ -39,13 +39,13 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TimeZone; +import java.util.function.Function; import java.util.stream.Collectors; import static org.awaitility.Awaitility.await; @@ -58,7 +58,8 @@ public class EditableGrid extends WebDriverComponent { public static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); - public static final String SELECT_COLUMN_HEADER = ""; + public static final FieldKey SELECT_COLUMN_ID = FieldKey.fromParts("__select__"); public static final String ROW_NUMBER_COLUMN_HEADER = ""; private final WebElement _gridElement; @@ -230,43 +231,60 @@ private List getRows() return Locators.rows.findElements(elementCache().table); } - public List> getGridDataByLabel(String... fieldLabels) + public List> getGridDataByLabel(String... columnIdentifiers) { - List> gridData = new ArrayList<>(); + return getGridData(ColumnHeader::getColumnLabel, columnIdentifiers); + } + + public List> getGridDataByFieldKey(String... columnIdentifiers) + { + return getGridData(ColumnHeader::getFieldKey, columnIdentifiers); + } + + public List> getGridDataByName(String... columnIdentifiers) + { + return getGridData(columnHeader -> columnHeader.getFieldKey().getName(), columnIdentifiers); + } - List allFieldLabels = getColumnLabels(); - Set includedColIndices = new HashSet<>(); - if (fieldLabels.length > 0) + private List> getGridData(Function keyGenerator, String... columnIdentifiers) + { + List> gridData = new ArrayList<>(); + + Set includedColHeaders = new LinkedHashSet<>(); + if (columnIdentifiers.length == 0) + { + includedColHeaders.addAll(elementCache().findHeaders()); + } + else { - Assertions.assertThat(allFieldLabels).as("Editable grid columns").contains(fieldLabels); - for (String col : fieldLabels) + for (String columnIdentifier : columnIdentifiers) { - int colIndex = allFieldLabels.indexOf(col); - includedColIndices.add(colIndex); + includedColHeaders.add(elementCache().findColumnHeader(columnIdentifier)); } } for (WebElement row : getRows()) { List cells = row.findElements(By.tagName("td")); - Map rowMap = new HashMap<>(); + Map rowMap = new LinkedHashMap<>(includedColHeaders.size()); - for (int i = 0; i < cells.size(); i++) + for (ColumnHeader columnHeader : includedColHeaders) { - if (includedColIndices.isEmpty() || includedColIndices.contains(i)) - { - WebElement cell = cells.get(i); - String fieldLabel = allFieldLabels.get(i); + WebElement cell = cells.get(columnHeader.getDomIndex()); - if (fieldLabel.equals(SELECT_COLUMN_HEADER)) - { - rowMap.put(fieldLabel, String.valueOf(cell.findElement(By.tagName("input")).isSelected())); - } - else - { - rowMap.put(fieldLabel, cell.getText()); - } + T key = keyGenerator.apply(columnHeader); + String value; + + if (columnHeader.getDomIndex() == 0 && hasSelectColumn()) + { + value = String.valueOf(Locator.tag("input").findElement(cell).isSelected()); } + else + { + value = cell.getText(); + } + + rowMap.put(key, value); } gridData.add(rowMap); @@ -277,7 +295,12 @@ public List> getGridDataByLabel(String... fieldLabels) public List getColumnDataByLabel(String fieldLabel) { - return getGridDataByLabel(fieldLabel).stream().map(a-> a.get(fieldLabel)).collect(Collectors.toList()); + return getColumnData(fieldLabel); + } + + public List getColumnData(String fieldIdentifier) + { + return getGridData(ch -> 1, fieldIdentifier).stream().map(a-> a.get(1)).collect(Collectors.toList()); } private WebElement getRow(int index) @@ -1154,7 +1177,7 @@ protected List findHeaders() if (hasSelectColumn()) { - columnHeaders.add(new ColumnHeader(headerCellElements.get(0), domIndex, SELECT_COLUMN_HEADER)); + columnHeaders.add(new ColumnHeader(headerCellElements.get(0), domIndex, SELECT_COLUMN_LABEL_PLACEHOLDER)); domIndex++; } diff --git a/src/org/labkey/test/util/TestDataGenerator.java b/src/org/labkey/test/util/TestDataGenerator.java index f83441c300..ac321eaa39 100644 --- a/src/org/labkey/test/util/TestDataGenerator.java +++ b/src/org/labkey/test/util/TestDataGenerator.java @@ -120,8 +120,8 @@ public TestDataGenerator(FieldDefinition.LookupInfo lookupInfo) public static File writeCsvFile(List fields, List> entityData, String fileName) throws IOException { - List> rows = TestDataUtils.rowListsFromMaps(entityData); - TestDataUtils.replaceColumnHeaders(rows, ColumnNameMapper.labelToName(fields)); // Use field names + List> rows = TestDataUtils.replaceColumnHeaders( + TestDataUtils.rowListsFromMaps(entityData), ColumnNameMapper.labelToName(fields)); // Use field names return TestDataUtils.writeRowsToCsv(fileName, rows); } diff --git a/src/org/labkey/test/util/data/TestDataUtils.java b/src/org/labkey/test/util/data/TestDataUtils.java index 132ce50c94..da1c1701b4 100644 --- a/src/org/labkey/test/util/data/TestDataUtils.java +++ b/src/org/labkey/test/util/data/TestDataUtils.java @@ -270,11 +270,37 @@ public static List> rowListsFromMaps(List> rowM public static List> replaceColumnHeaders(List> rowLists, Function columnMapper) { List headerRow = rowLists.get(0); - for (int i = 1; i < rowLists.size(); i++) + List updatedHeaderRow = new ArrayList<>(); + for (String oldHeader : headerRow) { - headerRow.set(i, columnMapper.apply(headerRow.get(i))); + updatedHeaderRow.add(columnMapper.apply(oldHeader)); } - return rowLists; + + List> updatedRows = new ArrayList<>(); + updatedRows.add(updatedHeaderRow); + updatedRows.addAll(rowLists.subList(1, rowLists.size())); + + return updatedRows; + } + + public static List> replaceMapKeys(List> rowMaps, Function columnMapper) + { + List> updatedRows = new ArrayList<>(); + for (Map original : rowMaps) + { + Map updatedRow = new LinkedHashMap<>(); + for (Map.Entry entry : original.entrySet()) + { + String updatedKey = columnMapper.apply(entry.getKey()); + if (updatedRow.containsKey(updatedKey)) + { + throw new IllegalArgumentException("Duplicate key mapping for '" + updatedKey + "' in row: " + original); + } + updatedRow.put(updatedKey, entry.getValue()); + } + updatedRows.add(updatedRow); + } + return updatedRows; } public static File writeRowsToTsv(String fileName, List> rows) throws IOException From 8601d3d7951ee5ba8c44f276979a20dec183e34e Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Fri, 16 May 2025 08:29:01 -0700 Subject: [PATCH 25/34] Fix and centralize file field identifiers --- .../components/ui/entities/EntityBulkInsertDialog.java | 7 ++++++- .../components/ui/entities/EntityBulkUpdateDialog.java | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java b/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java index bdf06446da..4fc7434390 100644 --- a/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java +++ b/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java @@ -387,6 +387,11 @@ protected void waitForReady() getWrapper().shortWait().until(ExpectedConditions.elementToBeClickable( elementCache().addRowsButton )); } + public static FieldKey fileUploadFieldKey(CharSequence fieldIdentifier) + { + return FieldKey.fromFieldKey(fieldIdentifier + "-fileUpload"); + } + @Override protected ElementCache newElementCache() { @@ -451,7 +456,7 @@ public ReactDateTimePicker dateInput(CharSequence fieldIdentifier) public FileAttachmentContainer fileUploadField(CharSequence fieldIdentifier) { - return new FileAttachmentContainer(formRow(FieldKey.fromFieldKey(fieldIdentifier + "-fileUpload")), getDriver()); + 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 6056bc830a..663627810f 100644 --- a/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java +++ b/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java @@ -180,7 +180,7 @@ public String getDateField(CharSequence fieldIdentifier) public FileAttachmentContainer getFileField(CharSequence fieldIdentifier) { - fieldIdentifier = FieldKey.fromFieldKey(FieldKey.fromChars(fieldIdentifier) + "-fileUpload"); + fieldIdentifier = EntityBulkInsertDialog.fileUploadFieldKey(fieldIdentifier); return enableAndWait(fieldIdentifier, elementCache().fileUploadField(fieldIdentifier)); } From 974a5ca9ce86b51e01648d22f8c2926cbc98f15c Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Wed, 21 May 2025 17:41:04 -0700 Subject: [PATCH 26/34] Add more javadoc and create FieldReferenceManager --- .../ui/entities/EntityBulkInsertDialog.java | 69 +++++- .../ui/entities/EntityBulkUpdateDialog.java | 2 +- .../ui/entities/EntityInsertPanel.java | 8 + .../components/ui/grids/DetailTableEdit.java | 95 +++---- .../components/ui/grids/EditableGrid.java | 231 ++++++------------ .../ui/grids/FieldReferenceManager.java | 178 ++++++++++++++ .../test/components/ui/grids/GridRow.java | 10 +- .../components/ui/grids/ResponsiveGrid.java | 224 ++++++----------- src/org/labkey/test/params/FieldKey.java | 23 +- 9 files changed, 476 insertions(+), 364 deletions(-) create mode 100644 src/org/labkey/test/components/ui/grids/FieldReferenceManager.java diff --git a/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java b/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java index 4fc7434390..6469bedf5c 100644 --- a/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java +++ b/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java @@ -166,39 +166,71 @@ public String getDescription() return getWrapper().getFormElement(elementCache().description); } + /** + * @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(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 getTextField(CharSequence fieldIdentifier) { return elementCache().textInput(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 setNumericField(CharSequence fieldIdentifier, String value) { elementCache().numericInput(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 getNumericField(CharSequence fieldIdentifier) { return elementCache().numericInput(fieldIdentifier).get(); } + /** + * @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(fieldIdentifier); @@ -206,6 +238,10 @@ public EntityBulkInsertDialog setSelectionField(CharSequence fieldIdentifier, Li return this; } + /** + * @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(fieldIdentifier); @@ -215,7 +251,7 @@ public List getSelectionField(CharSequence fieldIdentifier) /** * Clear the value(s) from a field that is a drop down selection field. * - * @param fieldIdentifier Name for the field. + * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey}) * @return This insert dialog. */ public EntityBulkInsertDialog clearSelectionField(CharSequence fieldIdentifier) @@ -261,7 +297,7 @@ public void setInsertFieldValues(List fields, Map Locator.tagWithClass("div", "row") // TODO: Shouldn't need to be case-insensitive. Parent/source lookups have weird casing diff --git a/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java b/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java index 663627810f..88c36cb0c5 100644 --- a/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java +++ b/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java @@ -349,7 +349,7 @@ protected class ElementCache extends ModalDialog.ElementCache { public WebElement formRow(CharSequence fieldIdentifier) { - String fieldKey = FieldKey.fromChars(fieldIdentifier).toString(); + String fieldKey = FieldKey.fromName(fieldIdentifier).toString(); return Locator.tagWithClass("div", "row") .withChild(Locator.tagWithAttribute("label", "for", fieldKey)) .waitForElement(this, WAIT_TIMEOUT); diff --git a/src/org/labkey/test/components/ui/entities/EntityInsertPanel.java b/src/org/labkey/test/components/ui/entities/EntityInsertPanel.java index 68054c65d4..b60ba679d4 100644 --- a/src/org/labkey/test/components/ui/entities/EntityInsertPanel.java +++ b/src/org/labkey/test/components/ui/entities/EntityInsertPanel.java @@ -110,6 +110,10 @@ public EntityInsertPanel addSource(String sourceType) return this; } + /** + * @param columnIdentifier fieldKey, name, or label + * @return this component + */ public EntityInsertPanel removeColumn(String columnIdentifier) { showGrid(); @@ -472,6 +476,10 @@ protected Locator locator() } } +/** + * Provides read-only access to parent and source menus + * Prevents tests from using them to add parents or sources without proper grid synchronization + */ class ReadOnlyMenu extends MultiMenu { private final String _entityType; diff --git a/src/org/labkey/test/components/ui/grids/DetailTableEdit.java b/src/org/labkey/test/components/ui/grids/DetailTableEdit.java index 2450e528a1..fb412d9292 100644 --- a/src/org/labkey/test/components/ui/grids/DetailTableEdit.java +++ b/src/org/labkey/test/components/ui/grids/DetailTableEdit.java @@ -14,8 +14,9 @@ import org.labkey.test.components.react.ReactSelect; import org.labkey.test.components.ui.files.FileUploadField; 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.labkey.test.util.LogMethod; import org.openqa.selenium.By; import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.WebDriver; @@ -27,12 +28,9 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.function.Supplier; import java.util.stream.Collectors; import static org.labkey.test.WebDriverWrapper.WAIT_FOR_JAVASCRIPT; @@ -589,50 +587,24 @@ public ElementCache() public WebElement findValueCell(String nameOrLabel) { - List> options = List.of( - () -> valueCellWithFieldKey(nameOrLabel), - () -> valueCellWithName(nameOrLabel), - () -> valueCellWithLabel(nameOrLabel)); - - return options.stream() - .map(Supplier::get) - .filter(Objects::nonNull) - .findFirst() - .orElseThrow(() -> new NoSuchElementException("Unable to locate cell: " + nameOrLabel)); + return getFieldManager().findFieldReference(nameOrLabel).getElement(); } - public WebElement valueCellWithLabel(String label) - { - initFieldInfo(); - return valueCellsByLabel.get(label); - } - public WebElement valueCellWithName(String fieldName) - { - return valueCellWithFieldKey(EscapeUtil.fieldKeyEncodePart(fieldName)); - } - - public WebElement valueCellWithFieldKey(String fieldKey) - { -// if (!valueCellsByFieldKey.containsKey(fieldKey)) -// { -// valueCellsByFieldKey.put(fieldKey, -// Locator.tagWithAttribute("td", "data-fieldkey", fieldKey) -// .findElementOrNull(editPanel)); -// } - initFieldInfo(); - return valueCellsByFieldKey.get(fieldKey); - } + private FieldReferenceManager _fieldReferenceManager; - private final Map valueCellsByLabel = new HashMap<>(); - private final Map valueCellsByFieldKey = new HashMap<>(); - private void initFieldInfo() + @LogMethod + private FieldReferenceManager getFieldManager() { - if (valueCellsByFieldKey.isEmpty()) + if (_fieldReferenceManager == null) { + List columnHeaders = new ArrayList<>(); + List valueCells = Locator.tagWithAttribute("td", "data-fieldkey").findElements(this); + // Use JavaScript to get fieldKeys and captions in one operation, rather than making 2N calls to 'WebElement.getDomAttribute' List> captionsAndKeys = getWrapper().executeScript( """ - var cells = arguments[0]; + + var cells = arguments[0]; var captions = []; var fieldkeys = []; for (var i = 0; i < cells.length; i++) @@ -641,16 +613,20 @@ private void initFieldInfo() fieldkeys.push(cells[i].dataset.fieldkey); } return [captions, fieldkeys]; - """, List.class, + """, List. + class, valueCells); List captions = captionsAndKeys.get(0); List fieldkeys = captionsAndKeys.get(1); for (int i = 0; i < valueCells.size(); i++) { - valueCellsByFieldKey.put(fieldkeys.get(i), valueCells.get(i)); - valueCellsByLabel.put(captions.get(i), valueCells.get(i)); + columnHeaders.add(new DetailTableEditFieldReference(valueCells.get(i), i, fieldkeys.get(i), captions.get(i))); } + + _fieldReferenceManager = new FieldReferenceManager(columnHeaders); } + + return _fieldReferenceManager; } public FileUploadField fileField(String nameOrLabel) @@ -709,4 +685,35 @@ protected Locator locator() return _locator; } } + + private static class DetailTableEditFieldReference extends FieldReferenceManager.FieldReference + { + private final FieldKey _fieldKey; + private final String _label; + + public DetailTableEditFieldReference(WebElement element, Integer domIndex, String fieldKey, String label) + { + super(element, domIndex); + _fieldKey = FieldKey.fromFieldKey(fieldKey); + _label = label; + } + + @Override + public FieldKey getFieldKey() + { + return _fieldKey; + } + + @Override + public String getLabel() + { + return _label; + } + + @Override + public Integer getDomIndex() + { + return null; + } + } } diff --git a/src/org/labkey/test/components/ui/grids/EditableGrid.java b/src/org/labkey/test/components/ui/grids/EditableGrid.java index 4bf442ecfe..746ee98a69 100644 --- a/src/org/labkey/test/components/ui/grids/EditableGrid.java +++ b/src/org/labkey/test/components/ui/grids/EditableGrid.java @@ -15,6 +15,7 @@ import org.labkey.test.components.react.ReactSelect; import org.labkey.test.components.ui.entities.EntityBulkInsertDialog; import org.labkey.test.components.ui.entities.EntityBulkUpdateDialog; +import org.labkey.test.components.ui.grids.FieldReferenceManager.FieldReference; import org.labkey.test.params.FieldDefinition; import org.labkey.test.params.FieldKey; import org.labkey.test.util.selenium.ScrollUtils; @@ -137,6 +138,11 @@ protected Integer getColumnIndex(String fieldLabel) throw new NotFoundException("Column not found in grid: " + fieldLabel + ". Found: " + fieldLabels); } + /** + * Remove the specified column from the grid + * @param columnIdentifier fieldKey, name, or label + * @return this component + */ public EditableGrid removeColumn(String columnIdentifier) { doAndWaitForColumnUpdate(() -> @@ -231,26 +237,38 @@ private List getRows() return Locators.rows.findElements(elementCache().table); } + /** + * @param columnIdentifiers fieldKeys, names, or labels of columns + * @return grid data for the specified columns, keyed by column label + */ public List> getGridDataByLabel(String... columnIdentifiers) { - return getGridData(ColumnHeader::getColumnLabel, columnIdentifiers); + return getGridData(FieldReferenceManager.FieldReference::getLabel, columnIdentifiers); } + /** + * @param columnIdentifiers fieldKeys, names, or labels of columns + * @return grid data for the specified columns, keyed by column fieldKey + */ public List> getGridDataByFieldKey(String... columnIdentifiers) { - return getGridData(ColumnHeader::getFieldKey, columnIdentifiers); + return getGridData(FieldReferenceManager.FieldReference::getFieldKey, columnIdentifiers); } + /** + * @param columnIdentifiers fieldKeys, names, or labels of columns + * @return grid data for the specified columns, keyed by column name + */ public List> getGridDataByName(String... columnIdentifiers) { - return getGridData(columnHeader -> columnHeader.getFieldKey().getName(), columnIdentifiers); + return getGridData(FieldReference::getName, columnIdentifiers); } - private List> getGridData(Function keyGenerator, String... columnIdentifiers) + private List> getGridData(Function keyGenerator, String... columnIdentifiers) { List> gridData = new ArrayList<>(); - Set includedColHeaders = new LinkedHashSet<>(); + Set includedColHeaders = new LinkedHashSet<>(); if (columnIdentifiers.length == 0) { includedColHeaders.addAll(elementCache().findHeaders()); @@ -268,14 +286,14 @@ private List> getGridData(Function keyGenera List cells = row.findElements(By.tagName("td")); Map rowMap = new LinkedHashMap<>(includedColHeaders.size()); - for (ColumnHeader columnHeader : includedColHeaders) + for (FieldReference fieldReference : includedColHeaders) { - WebElement cell = cells.get(columnHeader.getDomIndex()); + WebElement cell = cells.get(fieldReference.getDomIndex()); - T key = keyGenerator.apply(columnHeader); + T key = keyGenerator.apply(fieldReference); String value; - if (columnHeader.getDomIndex() == 0 && hasSelectColumn()) + if (fieldReference.getDomIndex() == 0 && hasSelectColumn()) { value = String.valueOf(Locator.tag("input").findElement(cell).isSelected()); } @@ -293,11 +311,15 @@ private List> getGridData(Function keyGenera return gridData; } + @Deprecated public List getColumnDataByLabel(String fieldLabel) { return getColumnData(fieldLabel); } + /** + * @param fieldIdentifier fieldKey, name, or label of column + */ public List getColumnData(String fieldIdentifier) { return getGridData(ch -> 1, fieldIdentifier).stream().map(a-> a.get(1)).collect(Collectors.toList()); @@ -312,15 +334,15 @@ private WebElement getRow(int index) * Find the first row index containing the text value in the given column. * If not found -1 is returned. * - * @param fieldLabel Column label to look at. + * @param fieldIdentifier fieldKey, name, or label of column * @param text Text to look for (must match exactly). * @return The first row index where found, -1 if not found. */ - public Integer getRowIndex(String fieldLabel, String text) + public Integer getRowIndex(String fieldIdentifier, String text) { int index = -1; - List columnData = getColumnDataByLabel(fieldLabel); + List columnData = getColumnData(fieldIdentifier); for (int i = 0; i < columnData.size(); i++) { if (columnData.get(i).equals(text)) @@ -1165,99 +1187,39 @@ protected WebElement getColumnHeaderCell(String fieldIdentifier) return findColumnHeader(fieldIdentifier).getElement(); } - private final List columnHeaders = new ArrayList<>(); - private final Map fieldKeys = new LinkedHashMap<>(); - private final Map fieldLabels = new LinkedHashMap<>(); - protected List findHeaders() + private FieldReferenceManager _fieldReferenceManager; + protected FieldReferenceManager getGridHeaderManager() { - if (columnHeaders.isEmpty()) + if (_fieldReferenceManager == null) { + List columnHeaders = new ArrayList<>(); List headerCellElements = Locators.headerCells.waitForElements(table, WAIT_FOR_JAVASCRIPT); int domIndex = 0; if (hasSelectColumn()) { - columnHeaders.add(new ColumnHeader(headerCellElements.get(0), domIndex, SELECT_COLUMN_LABEL_PLACEHOLDER)); + columnHeaders.add(new EditableGridColumnHeader(headerCellElements.get(0), domIndex, SELECT_COLUMN_LABEL_PLACEHOLDER)); domIndex++; } for (; domIndex < headerCellElements.size(); domIndex++) { - columnHeaders.add(new ColumnHeader(headerCellElements.get(domIndex), domIndex)); + columnHeaders.add(new EditableGridColumnHeader(headerCellElements.get(domIndex), domIndex)); } + + _fieldReferenceManager = new FieldReferenceManager(columnHeaders); } - return columnHeaders; + return _fieldReferenceManager; } - /** - * Find field by uncertain field identifier in order of precedence: - *
      - *
    1. Encoded fieldKey
    2. - *
    3. Unencoded fieldKey
    4. - *
    5. Field Label
    6. - *
    - */ - protected ColumnHeader findColumnHeader(String fieldIdentifier) + protected List findHeaders() { - FieldKey possibleFieldKey = FieldKey.fromParts(fieldIdentifier); - - List headers = findHeaders(); - if (fieldKeys.containsKey(fieldIdentifier)) - { - return fieldKeys.get(fieldIdentifier); - } - else if (fieldKeys.containsKey(possibleFieldKey.toString())) - { - return fieldKeys.get(possibleFieldKey.toString()); - } - else if (fieldLabels.containsKey(fieldIdentifier)) - { - return fieldLabels.get(fieldIdentifier); - } - else - { - if (fieldKeys.size() < headers.size()) - { - // Check whether fieldIdentifier is an encoded fieldKey - for (ColumnHeader header : headers) - { - if (!fieldKeys.containsValue(header)) - { - String fieldKey = header.getFieldKey().toString(); - fieldKeys.put(fieldKey, header); - if (fieldKey.equals(fieldIdentifier)) - { - return header; - } - } - } - - // Check whether fieldIdentifier is an unencoded fieldKey - if (fieldKeys.containsKey(possibleFieldKey.toString())) - { - return fieldKeys.get(possibleFieldKey.toString()); - } - } - - if (fieldLabels.size() < headers.size()) - { - // Check whether fieldIdentifier is a field label - for (ColumnHeader header : headers) - { - if (!fieldLabels.containsValue(header)) - { - String columnLabel = header.getColumnLabel(); - fieldLabels.put(columnLabel, header); - if (columnLabel.equals(fieldIdentifier)) - { - return header; - } - } - } - } + return getGridHeaderManager().getColumnHeaders(); + } - throw new NoSuchElementException("No such column with fieldKey or label: " + fieldIdentifier); - } + protected FieldReference findColumnHeader(String fieldIdentifier) + { + return getGridHeaderManager().findFieldReference(fieldIdentifier); } protected int getColumnIndex(String fieldIdentifier) @@ -1267,36 +1229,7 @@ protected int getColumnIndex(String fieldIdentifier) protected List getColumnLabels() { - return findHeaders().stream().map(ColumnHeader::getColumnLabel).collect(Collectors.toList()); - } - - /** - * 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> - */ - static 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 - } + return findHeaders().stream().map(FieldReference::getLabel).collect(Collectors.toList()); } public WebElement inputCell() @@ -1359,65 +1292,59 @@ protected Locator locator() } } - protected static class ColumnHeader + protected static class EditableGridColumnHeader extends FieldReferenceManager.FieldReference { - private final WebElement _element; - private final Integer _domIndex; private final Mutable _fieldLabel = new MutableObject<>(); - private final Mutable _fieldKey = new MutableObject<>(); - private ColumnHeader(WebElement element, int domIndex) + public EditableGridColumnHeader(WebElement element, int domIndex) { - _element = element; - _domIndex = domIndex; + super(element, domIndex); } - private ColumnHeader(WebElement element, int domIndex, String label) + public EditableGridColumnHeader(WebElement element, int domIndex, String label) { this(element, domIndex); _fieldLabel.setValue(label); } - public WebElement getElement() + @Override + public String getLabel() { - return _element; + if (_fieldLabel.getValue() == null) + { + _fieldLabel.setValue(getLabelFromHeaderCell(getElement()).trim()); + } + return _fieldLabel.getValue(); } - public FieldKey getFieldKey() + + /** + * 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) { - if (_fieldKey.getValue() == null) + // Use text nodes to ignore browser whitespace formatting + List textNodes = WebElementUtils.getTextNodesWithin(el); + if (textNodes.isEmpty()) { - 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) + List children = Locator.xpath("./*").findElements(el); + if (children.isEmpty()) { - _fieldKey.setValue(FieldKey.fromFieldKey(path)); + return ""; // probably the selection checkbox column } else { - _fieldKey.setValue(FieldKey.EMPTY); + // Depth-first search until we find some text + return getLabelFromHeaderCell(children.get(0)); } } - return _fieldKey.getValue(); - } - - public String getColumnLabel() - { - if (_fieldLabel.getValue() == null) + else { - _fieldLabel.setValue(ElementCache.getLabelFromHeaderCell(getElement()).trim()); + 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 _fieldLabel.getValue(); - } - - public Integer getDomIndex() - { - return _domIndex; } } } 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..3bf023fa3a --- /dev/null +++ b/src/org/labkey/test/components/ui/grids/FieldReferenceManager.java @@ -0,0 +1,178 @@ +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: + *
      + *
    1. FieldKey object
    2. + *
    3. Encoded fieldKey
    4. + *
    5. Unencoded fieldKey
    6. + *
    7. Field Label
    8. + *
    + */ + public final FieldReference findFieldReference(CharSequence columnIdentifier) + { + List> options; + + if (columnIdentifier instanceof FieldKey fk) + { + options = List.of(() -> findColumnHeaderByFieldKey(fk)); // We know it is a FieldKey + } + else + { + options = List.of( + () -> findColumnHeaderByFieldKey(FieldKey.fromFieldKey(columnIdentifier)), // encoded fieldKey + () -> findColumnHeaderByFieldKey(FieldKey.fromName(columnIdentifier)), // unencoded fieldKey + () -> findColumnHeaderByLabel(columnIdentifier.toString()) // Field label + ); + } + + return options.stream() + .map(Supplier::get) + .filter(Objects::nonNull) + .findFirst() + .orElseThrow(() -> new NoSuchElementException("Unable to locate field: " + columnIdentifier)); + } + + private FieldReference findColumnHeaderByFieldKey(FieldKey columnIdentifier) + { + if (fieldKeys.containsKey(columnIdentifier)) + { + return fieldKeys.get(columnIdentifier); + } + else if (fieldKeys.size() < _fieldReferences.size()) + { + // Check whether columnIdentifier is an encoded fieldKey + for (FieldReference header : _fieldReferences) + { + if (!fieldKeys.containsValue(header)) + { + FieldKey fieldKey = header.getFieldKey(); + fieldKeys.put(fieldKey, header); + if (fieldKey.equals(columnIdentifier)) + { + return header; + } + } + } + } + + return null; + } + + private FieldReference findColumnHeaderByLabel(String label) + { + if (fieldLabels.containsKey(label)) + { + return fieldLabels.get(label); + } + else if (fieldLabels.size() < _fieldReferences.size()) + { + // Check whether columnIdentifier is a field label + 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 Integer _domIndex; + private final Mutable _fieldLabel = new MutableObject<>(); + private final Mutable _fieldKey = new MutableObject<>(); + + public FieldReference(WebElement element, Integer 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 Integer getDomIndex() + { + return _domIndex; + } + } +} diff --git a/src/org/labkey/test/components/ui/grids/GridRow.java b/src/org/labkey/test/components/ui/grids/GridRow.java index 06a51ba248..2a1887c717 100644 --- a/src/org/labkey/test/components/ui/grids/GridRow.java +++ b/src/org/labkey/test/components/ui/grids/GridRow.java @@ -165,7 +165,7 @@ public List getTexts() */ public Map getRowMapByLabel() { - return getRowMap(ResponsiveGrid.ColumnHeader::getColumnLabel); + return getRowMap(FieldReferenceManager.FieldReference::getLabel); } /** @@ -181,17 +181,17 @@ public Map getRowMapByName() */ public Map getRowMapByFieldKey() { - return getRowMap(ResponsiveGrid.ColumnHeader::getFieldKey); + return getRowMap(FieldReferenceManager.FieldReference::getFieldKey); } - Map getRowMap(Function keyMapper) + Map getRowMap(Function keyMapper) { List columnValues = elementCache().getCellTexts(); - List headers = _grid.getHeaders(); + List headers = _grid.getHeaders(); Map rowMap = new LinkedHashMap<>(); - for (ResponsiveGrid.ColumnHeader header : headers) + for (FieldReferenceManager.FieldReference header : headers) { T key = keyMapper.apply(header); String value = columnValues.get(header.getDomIndex()); diff --git a/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java b/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java index 3ce02374e4..8ce13d8b7a 100644 --- a/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java +++ b/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java @@ -4,8 +4,6 @@ */ package org.labkey.test.components.ui.grids; -import org.apache.commons.lang3.mutable.Mutable; -import org.apache.commons.lang3.mutable.MutableObject; import org.jetbrains.annotations.Nullable; import org.labkey.remoteapi.query.Filter; import org.labkey.test.Locator; @@ -16,11 +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; @@ -31,7 +29,6 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -117,7 +114,7 @@ public void scrollToOrigin() /** * Sorts from the grid header menu - * @param columnIdentifier column header for + * @param columnIdentifier fieldKey, name, or label of column * @return this grid */ public T sortColumnAscending(String columnIdentifier) @@ -128,7 +125,7 @@ public T sortColumnAscending(String columnIdentifier) /** * Sorts from the grid header menu - * @param columnIdentifier Text of column + * @param columnIdentifier fieldKey, name, or label of column * @return this grid */ public T sortColumnDescending(String columnIdentifier) @@ -137,16 +134,26 @@ public T sortColumnDescending(String columnIdentifier) return getThis(); } + /** + * Sorts from the grid header menu + * @param columnIdentifier fieldKey, name, or label of column + */ public void sortColumn(String columnIdentifier, SortDirection direction) { clickColumnMenuItem(columnIdentifier, direction.equals(SortDirection.DESC) ? "Sort descending" : "Sort ascending", true); } + /** + * @param columnIdentifier fieldKey, name, or label of column + */ public void clearSort(String columnIdentifier) { clickColumnMenuItem(columnIdentifier, "Clear sort", true); } + /** + * @param columnIdentifier fieldKey, name, or label of column + */ public boolean hasColumnSortIcon(String columnIdentifier) { WebElement headerCell = elementCache().getColumnHeaderCell(columnIdentifier); @@ -158,11 +165,17 @@ public boolean hasColumnSortIcon(String columnIdentifier) } + /** + * @param columnIdentifier fieldKey, name, or label of column + */ public T filterColumn(String columnIdentifier, Filter.Operator operator) { return filterColumn(columnIdentifier, operator, null); } + /** + * @param columnIdentifier fieldKey, name, or label of column + */ public T filterColumn(String columnIdentifier, Filter.Operator operator, Object value) { T _this = getThis(); @@ -170,6 +183,9 @@ public T filterColumn(String columnIdentifier, Filter.Operator operator, Object return _this; } + /** + * @param columnIdentifier fieldKey, name, or label of column + */ public T filterColumn(String columnIdentifier, Filter.Operator operator1, Object value1, Filter.Operator operator2, Object value2) { T _this = getThis(); @@ -185,6 +201,9 @@ public T filterColumn(String columnIdentifier, Filter.Operator operator1, Object return _this; } + /** + * @param columnIdentifier fieldKey, name, or label of column + */ public T filterBooleanColumn(String columnIdentifier, boolean value) { T _this = getThis(); @@ -197,6 +216,9 @@ public T filterBooleanColumn(String columnIdentifier, boolean value) return _this; } + /** + * @param columnIdentifier fieldKey, name, or label of column + */ public String filterColumnExpectingError(String columnIdentifier, Filter.Operator operator, Object value) { GridFilterModal filterModal = initFilterColumn(columnIdentifier, operator, value); @@ -214,12 +236,18 @@ private GridFilterModal initFilterColumn(String columnIdentifier, Filter.Operato return filterModal; } + /** + * @param columnIdentifier fieldKey, name, or label of column + */ public T removeColumnFilter(String columnIdentifier) { clickColumnMenuItem(columnIdentifier, "Remove filter", true); return getThis(); } + /** + * @param columnIdentifier fieldKey, name, or label of column + */ public boolean hasColumnFilterIcon(String columnIdentifier) { WebElement headerCell = elementCache().getColumnHeaderCell(columnIdentifier); @@ -233,7 +261,7 @@ public boolean hasColumnFilterIcon(String columnIdentifier) /** * use the column menu to hide the given column. * - * @param columnIdentifier Column to hide. + * @param columnIdentifier fieldKey, name, or label of column * @return This grid. */ public T hideColumn(String columnIdentifier) @@ -257,7 +285,7 @@ public FieldSelectionDialog insertColumn() * 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 columnIdentifier The column to get the menu from. + * @param columnIdentifier fieldKey, name, or label of column * @return A {@link FieldSelectionDialog} */ public FieldSelectionDialog insertColumn(String columnIdentifier) @@ -296,6 +324,10 @@ protected void clickColumnMenuItem(String columnIdentifier, String menuText, boo waitFor(()-> !menuItem.isDisplayed(), 1000); } + /** + * @param columnIdentifier fieldKey, name, or label of column + * @param newColumnLabel new label for the column + */ public void editColumnLabel(String columnIdentifier, String newColumnLabel) { // Get the column header. @@ -345,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 */ @@ -360,7 +392,7 @@ public T selectRow(Map partialMap, boolean checked) /** * Finds the first row with the specified text in the specified column and sets its checkbox - * @param columnIdentifier 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 @@ -394,7 +426,7 @@ else if (!checked && row.isSelected()) /** * Sets the specified rows' selector checkboxes to the requested select state - * @param columnIdentifier 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 @@ -512,7 +544,7 @@ public Optional getOptionalRow(String text) /** * Returns the first row with matching text in the specified column - * @param columnIdentifier 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 */ @@ -523,7 +555,7 @@ public GridRow getRow(String columnIdentifier, String text) /** * Returns the first row with matching text in the specified column - * @param columnIdentifier 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 */ @@ -534,7 +566,7 @@ public Optional getOptionalRow(String columnIdentifier, String 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) @@ -561,6 +593,9 @@ public List getRows() return elementCache().getRows(); } + /** + * @param columnIdentifier fieldKey, name, or label of column + */ public List getColumnDataAsText(String columnIdentifier) { List columnData = new ArrayList<>(); @@ -581,9 +616,8 @@ 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 columnIdentifier) { @@ -625,6 +659,7 @@ 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 columnIdentifier) { @@ -632,7 +667,6 @@ public String getCellText(int rowIndex, String columnIdentifier) } /** - * * @return a list of Map<String, String> containing keys and values for each row */ public List> getRowMapsByLabel() @@ -641,7 +675,6 @@ public List> getRowMapsByLabel() } /** - * * @return a list of Map<String, String> containing keys and values for each row */ public List> getRowMapsByName() @@ -650,7 +683,6 @@ public List> getRowMapsByName() } /** - * * @return a list of Map<String, String> containing keys and values for each row */ public List> getRowMapsByFieldKey() @@ -669,7 +701,7 @@ public void clickLink(String text) /** * locates the first link in the specified column, clicks it, and waits for the URL to update - * @param columnIdentifier column in which to search + * @param columnIdentifier fieldKey, name, or label of column * @param text text for link to match */ public void clickLink(String columnIdentifier, String text) @@ -702,7 +734,7 @@ public String getGridError() /** The responsiveGrid now supports redacting fields * - * @param columnIdentifier the column label. + * @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 columnIdentifier) @@ -732,7 +764,7 @@ public Optional getGridEmptyMessage() return msg; } - List getHeaders() + List getHeaders() { return Collections.unmodifiableList(elementCache().findHeaders()); } @@ -783,91 +815,31 @@ protected final WebElement getColumnHeaderCell(String columnIdentifier) return findColumnHeader(columnIdentifier).getElement(); } - private final List columnHeaders = new ArrayList<>(); - private final Map fieldKeys = new LinkedHashMap<>(); - private final Map fieldLabels = new LinkedHashMap<>(); - protected List findHeaders() + private FieldReferenceManager _fieldReferenceManager; + protected FieldReferenceManager getGridHeaderManager() { - if (columnHeaders.isEmpty()) + if (_fieldReferenceManager == null) { + List fieldReferences = new ArrayList<>(); List headerCellElements = Locators.headerCells.findElements(this); for (int domIndex = hasSelectColumn() ? 1 : 0; domIndex < headerCellElements.size(); domIndex++) { - columnHeaders.add(new ColumnHeader(headerCellElements.get(domIndex), domIndex)); + fieldReferences.add(new FieldReference(headerCellElements.get(domIndex), domIndex)); } + + _fieldReferenceManager = new FieldReferenceManager(fieldReferences); } - return columnHeaders; + return _fieldReferenceManager; } - /** - * Find field by uncertain field identifier in order of precedence: - *
      - *
    1. Encoded fieldKey
    2. - *
    3. Unencoded fieldKey
    4. - *
    5. Field Label
    6. - *
    - */ - protected ColumnHeader findColumnHeader(String columnIdentifier) + protected List findHeaders() { - FieldKey possibleFieldKey = FieldKey.fromParts(columnIdentifier); - - List headers = findHeaders(); - if (fieldKeys.containsKey(columnIdentifier)) - { - return fieldKeys.get(columnIdentifier); - } - else if (fieldKeys.containsKey(possibleFieldKey.toString())) - { - return fieldKeys.get(possibleFieldKey.toString()); - } - else if (fieldLabels.containsKey(columnIdentifier)) - { - return fieldLabels.get(columnIdentifier); - } - else - { - if (fieldKeys.size() < headers.size()) - { - // Check whether columnIdentifier is an encoded fieldKey - for (ColumnHeader header : headers) - { - if (!fieldKeys.containsValue(header)) - { - String fieldKey = header.getFieldKey().toString(); - fieldKeys.put(fieldKey, header); - if (fieldKey.equals(columnIdentifier)) - { - return header; - } - } - } - - // Check whether columnIdentifier is an unencoded fieldKey - if (fieldKeys.containsKey(possibleFieldKey.toString())) - { - return fieldKeys.get(possibleFieldKey.toString()); - } - } - - if (fieldLabels.size() < headers.size()) - { - // Check whether columnIdentifier is a field label - for (ColumnHeader header : headers) - { - if (!fieldLabels.containsValue(header)) - { - String columnLabel = header.getColumnLabel(); - fieldLabels.put(columnLabel, header); - if (columnLabel.equals(columnIdentifier)) - { - return header; - } - } - } - } + return getGridHeaderManager().getColumnHeaders(); + } - throw new NoSuchElementException("No such column with fieldKey or label: " + columnIdentifier); - } + protected FieldReference findColumnHeader(String columnIdentifier) + { + return getGridHeaderManager().findFieldReference(columnIdentifier); } protected int getColumnIndex(String columnIdentifier) @@ -877,7 +849,7 @@ protected int getColumnIndex(String columnIdentifier) protected List getColumnLabels() { - return findHeaders().stream().map(ColumnHeader::getColumnLabel).collect(Collectors.toList()); + return findHeaders().stream().map(FieldReferenceManager.FieldReference::getLabel).collect(Collectors.toList()); } protected GridRow getRow(int index) @@ -1000,60 +972,4 @@ protected Locator locator() return _locator; } } - - protected static class ColumnHeader - { - private final WebElement _element; - private final Integer _domIndex; - private final Mutable _fieldLabel = new MutableObject<>(); - private final Mutable _fieldKey = new MutableObject<>(); - - private ColumnHeader(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 getColumnLabel() - { - if (_fieldLabel.getValue() == null) - { - _fieldLabel.setValue(WebElementUtils.getTextContent(getElement()).trim()); - } - return _fieldLabel.getValue(); - } - - public Integer getDomIndex() - { - return _domIndex; - } - } } diff --git a/src/org/labkey/test/params/FieldKey.java b/src/org/labkey/test/params/FieldKey.java index cbfd5c4118..84971e527d 100644 --- a/src/org/labkey/test/params/FieldKey.java +++ b/src/org/labkey/test/params/FieldKey.java @@ -54,17 +54,30 @@ public static FieldKey fromParts(String... parts) return fromParts(Arrays.asList(parts)); } - public static FieldKey fromFieldKey(String fieldKey) + /** + * 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 FieldKey fromFieldKey(CharSequence fieldKey) { - return fromParts(Arrays.stream(fieldKey.split(SEPARATOR)).map(FieldKey::decodePart).toList()); + if (fieldKey instanceof FieldKey fk) + return fk; + else + return fromParts(Arrays.stream(fieldKey.toString().split(SEPARATOR)).map(FieldKey::decodePart).toList()); } - public static FieldKey fromChars(CharSequence fieldKey) + /** + * 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) { - if (fieldKey instanceof FieldKey fk) + if (nameOrFieldKey instanceof FieldKey fk) return fk; else - return fromParts(fieldKey.toString()); + return fromParts(nameOrFieldKey.toString()); } private static final String[] ILLEGAL = {"$", "/", "&", "}", "~", ",", "."}; From 0c4f3900c8894099aa8704508ebad5bef52ff985 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Wed, 21 May 2025 18:29:46 -0700 Subject: [PATCH 27/34] update EditableGrid to support fieldkeys --- .../components/ui/grids/DetailTableEdit.java | 8 +- .../components/ui/grids/EditableGrid.java | 233 +++++++++--------- .../ui/grids/FieldReferenceManager.java | 28 +-- .../test/components/ui/grids/GridRow.java | 23 +- .../test/components/ui/grids/QueryGrid.java | 6 +- .../components/ui/grids/ResponsiveGrid.java | 62 ++--- 6 files changed, 178 insertions(+), 182 deletions(-) diff --git a/src/org/labkey/test/components/ui/grids/DetailTableEdit.java b/src/org/labkey/test/components/ui/grids/DetailTableEdit.java index fb412d9292..ca162d0b7c 100644 --- a/src/org/labkey/test/components/ui/grids/DetailTableEdit.java +++ b/src/org/labkey/test/components/ui/grids/DetailTableEdit.java @@ -691,7 +691,7 @@ private static class DetailTableEditFieldReference extends FieldReferenceManager private final FieldKey _fieldKey; private final String _label; - public DetailTableEditFieldReference(WebElement element, Integer domIndex, String fieldKey, String label) + public DetailTableEditFieldReference(WebElement element, int domIndex, String fieldKey, String label) { super(element, domIndex); _fieldKey = FieldKey.fromFieldKey(fieldKey); @@ -709,11 +709,5 @@ public String getLabel() { return _label; } - - @Override - public Integer getDomIndex() - { - return null; - } } } diff --git a/src/org/labkey/test/components/ui/grids/EditableGrid.java b/src/org/labkey/test/components/ui/grids/EditableGrid.java index 746ee98a69..0a0dbeef31 100644 --- a/src/org/labkey/test/components/ui/grids/EditableGrid.java +++ b/src/org/labkey/test/components/ui/grids/EditableGrid.java @@ -127,15 +127,9 @@ public List getColumnLabels() return elementCache().getColumnLabels(); } - protected Integer getColumnIndex(String fieldLabel) + protected Integer getColumnIndex(CharSequence columnIdentifier) { - List fieldLabels = getColumnLabels(); - for (int i=0; i< fieldLabels.size(); i++ ) - { - if (fieldLabels.get(i).equalsIgnoreCase(fieldLabel)) - return i; - } - throw new NotFoundException("Column not found in grid: " + fieldLabel + ". Found: " + fieldLabels); + return elementCache().getColumnIndex(columnIdentifier); } /** @@ -143,7 +137,7 @@ protected Integer getColumnIndex(String fieldLabel) * @param columnIdentifier fieldKey, name, or label * @return this component */ - public EditableGrid removeColumn(String columnIdentifier) + public EditableGrid removeColumn(CharSequence columnIdentifier) { doAndWaitForColumnUpdate(() -> { @@ -241,7 +235,7 @@ private List getRows() * @param columnIdentifiers fieldKeys, names, or labels of columns * @return grid data for the specified columns, keyed by column label */ - public List> getGridDataByLabel(String... columnIdentifiers) + public List> getGridDataByLabel(CharSequence... columnIdentifiers) { return getGridData(FieldReferenceManager.FieldReference::getLabel, columnIdentifiers); } @@ -250,7 +244,7 @@ public List> getGridDataByLabel(String... columnIdentifiers) * @param columnIdentifiers fieldKeys, names, or labels of columns * @return grid data for the specified columns, keyed by column fieldKey */ - public List> getGridDataByFieldKey(String... columnIdentifiers) + public List> getGridDataByFieldKey(CharSequence... columnIdentifiers) { return getGridData(FieldReferenceManager.FieldReference::getFieldKey, columnIdentifiers); } @@ -259,12 +253,12 @@ public List> getGridDataByFieldKey(String... columnIdentif * @param columnIdentifiers fieldKeys, names, or labels of columns * @return grid data for the specified columns, keyed by column name */ - public List> getGridDataByName(String... columnIdentifiers) + public List> getGridDataByName(CharSequence... columnIdentifiers) { return getGridData(FieldReference::getName, columnIdentifiers); } - private List> getGridData(Function keyGenerator, String... columnIdentifiers) + private List> getGridData(Function keyGenerator, CharSequence... columnIdentifiers) { List> gridData = new ArrayList<>(); @@ -275,7 +269,7 @@ private List> getGridData(Function List> getGridData(Function getColumnDataByLabel(String fieldLabel) + public List getColumnDataByLabel(CharSequence columnIdentifier) { - return getColumnData(fieldLabel); + return getColumnData(columnIdentifier); } /** - * @param fieldIdentifier fieldKey, name, or label of column + * @param columnIdentifier fieldKey, name, or label of column */ - public List getColumnData(String fieldIdentifier) + public List getColumnData(CharSequence columnIdentifier) { - return getGridData(ch -> 1, fieldIdentifier).stream().map(a-> a.get(1)).collect(Collectors.toList()); + return getGridData(ch -> 1, columnIdentifier).stream().map(a-> a.get(1)).collect(Collectors.toList()); } private WebElement getRow(int index) @@ -334,15 +328,15 @@ private WebElement getRow(int index) * Find the first row index containing the text value in the given column. * If not found -1 is returned. * - * @param fieldIdentifier fieldKey, name, or label of column + * @param columnIdentifier fieldKey, name, or label of column * @param text Text to look for (must match exactly). * @return The first row index where found, -1 if not found. */ - public Integer getRowIndex(String fieldIdentifier, String text) + public Integer getRowIndex(CharSequence columnIdentifier, String text) { int index = -1; - List columnData = getColumnData(fieldIdentifier); + List columnData = getColumnData(columnIdentifier); for (int i = 0; i < columnData.size(); i++) { if (columnData.get(i).equals(text)) @@ -359,18 +353,18 @@ public Integer getRowIndex(String fieldIdentifier, String text) * Get the td element for a cell. * * @param row The 0 based row index. - * @param fieldLabel The label of the column to get the cell. + * @param columnIdentifier fieldKey, name, or label of column * @return A {@link WebElement} that is the td for the cell. */ - public WebElement getCell(int row, String fieldLabel) + public WebElement getCell(int row, CharSequence columnIdentifier) { - int columNumber = getColumnIndex(fieldLabel) + 1; + int columNumber = getColumnIndex(columnIdentifier) + 1; return Locator.css("td:nth-of-type(" + columNumber + ")").findElement(getRow(row)); } - public boolean isCellReadOnly(int row, String fieldLabel) + public boolean isCellReadOnly(int row, CharSequence columnIdentifier) { - WebElement div = Locator.tag("div").findElement(getCell(row, fieldLabel)); + WebElement div = Locator.tag("div").findElement(getCell(row, columnIdentifier)); String cellClass = div.getDomAttribute("class"); return cellClass != null && cellClass.contains("cell-read-only"); } @@ -382,44 +376,44 @@ public int getRowCount() /** *

    - * For a given column, 'fieldLabelToSet', set the lookup cell in the first row where the value in column 'fieldLabelToSearch' + * For a given column, 'columnToSet', set the lookup cell in the first row where the value in column 'columnToSearch' * equals 'valueToSearch'. The value chosen will be at the specified index in the lookup options. Supply a 'value' in order to * filter the set of options shown. *

    * - * @param fieldLabelToSearch The label of the column to check if a row should be updated or not. - * @param valueToSearch The value to check for in 'fieldLabelToSearch' to see if the row should be updated. - * @param fieldLabelToSet The column to update in a row. + * @param columnToSearch fieldKey, name, or label of column to check if a row should be updated or not. + * @param valueToSearch The value to check for in 'columnToSearch' to see if the row should be updated. + * @param columnToSet The column to update in a row. * @param value Optional value to supply for filtering lookup options before selection * @param index The 0-based index of the option to choose from the possibly filtered list of options. */ - public void setCellValueForLookup(String fieldLabelToSearch, String valueToSearch, String fieldLabelToSet, @Nullable String value, int index) + public void setCellValueForLookup(CharSequence columnToSearch, String valueToSearch, CharSequence columnToSet, @Nullable String value, int index) { - setCellValueForLookup(getRowIndex(fieldLabelToSearch, valueToSearch), fieldLabelToSet, value, index); + setCellValueForLookup(getRowIndex(columnToSearch, valueToSearch), columnToSet, value, index); } /** *

    - * For a given column, 'fieldLabelToSet', set the cell in the row if value in column 'fieldLabelToSearch' + * For a given column, 'columnToSet', set the cell in the row if value in column 'columnToSearch' * equals 'valueToSearch'. *

    *

    * Rather than set one cell in a specific row, this function will loop through all the rows in the grid and - * will update the value in column 'fieldLabelToSet' only if the value in the column 'fieldLabelToSearch' equal + * will update the value in column 'columnToSet' only if the value in the column 'columnToSearch' equal * 'valueToSearch' in that row. *

    *

    * The check for equality for 'valueToSearch' is case sensitive. *

    * - * @param fieldLabelToSearch The label of the column to check if a row should be updated or not. - * @param valueToSearch The value to check for in 'fieldLabelToSearch' to see if the row should be updated. - * @param fieldLabelToSet The column to update in a row. - * @param valueToSet The new value to put into column 'fieldLabelToSet'. + * @param columnToSearch fieldKey, name, or label of column to check if a row should be updated or not. + * @param valueToSearch The value to check for in 'columnToSearch' to see if the row should be updated. + * @param columnToSet The column to update in a row. + * @param valueToSet The new value to put into column 'columnToSet'. */ - public void setCellValue(String fieldLabelToSearch, String valueToSearch, String fieldLabelToSet, Object valueToSet) + public void setCellValue(CharSequence columnToSearch, String valueToSearch, CharSequence columnToSet, Object valueToSet) { - setCellValue(getRowIndex(fieldLabelToSearch, valueToSearch), fieldLabelToSet, valueToSet); + setCellValue(getRowIndex(columnToSearch, valueToSearch), columnToSet, valueToSet); } /** @@ -432,13 +426,13 @@ public void setCellValue(String fieldLabelToSearch, String valueToSearch, String *

    * * @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); } /** @@ -447,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); @@ -464,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; } @@ -479,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) @@ -494,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) { @@ -587,10 +581,9 @@ public void setEntityData(List >data, List { Map rowData = data.get(i); for (FieldDefinition field : fields) { - String key = field.getEffectiveLabel(); - Object value = rowData.get(key); + Object value = rowData.get(field.getEffectiveLabel()); if (value != null) - setCellValue(i, key, value); + setCellValue(i, field.getName(), value); } } } @@ -600,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; } @@ -611,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. @@ -637,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. @@ -650,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; } @@ -658,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); @@ -671,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(); } @@ -698,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) @@ -727,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); } /** @@ -740,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); @@ -767,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); @@ -866,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); @@ -883,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); @@ -986,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); @@ -1043,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"); } } @@ -1064,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"); } /** @@ -1081,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); } @@ -1092,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)) { @@ -1182,9 +1185,9 @@ 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); - protected WebElement getColumnHeaderCell(String fieldIdentifier) + protected WebElement getColumnHeaderCell(CharSequence columnIdentifier) { - return findColumnHeader(fieldIdentifier).getElement(); + return findColumnHeader(columnIdentifier).getElement(); } private FieldReferenceManager _fieldReferenceManager; @@ -1217,14 +1220,14 @@ protected List findHeaders() return getGridHeaderManager().getColumnHeaders(); } - protected FieldReference findColumnHeader(String fieldIdentifier) + protected FieldReference findColumnHeader(CharSequence columnIdentifier) { - return getGridHeaderManager().findFieldReference(fieldIdentifier); + return getGridHeaderManager().findFieldReference(columnIdentifier); } - protected int getColumnIndex(String fieldIdentifier) + protected int getColumnIndex(CharSequence columnIdentifier) { - return findColumnHeader(fieldIdentifier).getDomIndex(); + return findColumnHeader(columnIdentifier).getDomIndex(); } protected List getColumnLabels() diff --git a/src/org/labkey/test/components/ui/grids/FieldReferenceManager.java b/src/org/labkey/test/components/ui/grids/FieldReferenceManager.java index 3bf023fa3a..b8845cf93b 100644 --- a/src/org/labkey/test/components/ui/grids/FieldReferenceManager.java +++ b/src/org/labkey/test/components/ui/grids/FieldReferenceManager.java @@ -39,20 +39,20 @@ public List getColumnHeaders() *
  • Field Label
  • * */ - public final FieldReference findFieldReference(CharSequence columnIdentifier) + public final FieldReference findFieldReference(CharSequence fieldIdentifier) { List> options; - if (columnIdentifier instanceof FieldKey fk) + if (fieldIdentifier instanceof FieldKey fk) { options = List.of(() -> findColumnHeaderByFieldKey(fk)); // We know it is a FieldKey } else { options = List.of( - () -> findColumnHeaderByFieldKey(FieldKey.fromFieldKey(columnIdentifier)), // encoded fieldKey - () -> findColumnHeaderByFieldKey(FieldKey.fromName(columnIdentifier)), // unencoded fieldKey - () -> findColumnHeaderByLabel(columnIdentifier.toString()) // Field label + () -> findColumnHeaderByFieldKey(FieldKey.fromFieldKey(fieldIdentifier)), // encoded fieldKey + () -> findColumnHeaderByFieldKey(FieldKey.fromName(fieldIdentifier)), // unencoded fieldKey + () -> findColumnHeaderByLabel(fieldIdentifier.toString()) // Field label ); } @@ -60,25 +60,24 @@ public final FieldReference findFieldReference(CharSequence columnIdentifier) .map(Supplier::get) .filter(Objects::nonNull) .findFirst() - .orElseThrow(() -> new NoSuchElementException("Unable to locate field: " + columnIdentifier)); + .orElseThrow(() -> new NoSuchElementException("Unable to locate field: " + fieldIdentifier)); } - private FieldReference findColumnHeaderByFieldKey(FieldKey columnIdentifier) + private FieldReference findColumnHeaderByFieldKey(FieldKey fieldIdentifier) { - if (fieldKeys.containsKey(columnIdentifier)) + if (fieldKeys.containsKey(fieldIdentifier)) { - return fieldKeys.get(columnIdentifier); + return fieldKeys.get(fieldIdentifier); } else if (fieldKeys.size() < _fieldReferences.size()) { - // Check whether columnIdentifier is an encoded fieldKey for (FieldReference header : _fieldReferences) { if (!fieldKeys.containsValue(header)) { FieldKey fieldKey = header.getFieldKey(); fieldKeys.put(fieldKey, header); - if (fieldKey.equals(columnIdentifier)) + if (fieldKey.equals(fieldIdentifier)) { return header; } @@ -97,7 +96,6 @@ private FieldReference findColumnHeaderByLabel(String label) } else if (fieldLabels.size() < _fieldReferences.size()) { - // Check whether columnIdentifier is a field label for (FieldReference header : _fieldReferences) { if (!fieldLabels.containsValue(header)) @@ -118,11 +116,11 @@ else if (fieldLabels.size() < _fieldReferences.size()) public static class FieldReference { private final WebElement _element; - private final Integer _domIndex; + private final int _domIndex; private final Mutable _fieldLabel = new MutableObject<>(); private final Mutable _fieldKey = new MutableObject<>(); - public FieldReference(WebElement element, Integer domIndex) + public FieldReference(WebElement element, int domIndex) { _element = element; _domIndex = domIndex; @@ -170,7 +168,7 @@ public String getName() return getFieldKey().getName(); } - public Integer getDomIndex() + public int getDomIndex() { return _domIndex; } diff --git a/src/org/labkey/test/components/ui/grids/GridRow.java b/src/org/labkey/test/components/ui/grids/GridRow.java index 2a1887c717..932b3bf1bf 100644 --- a/src/org/labkey/test/components/ui/grids/GridRow.java +++ b/src/org/labkey/test/components/ui/grids/GridRow.java @@ -7,6 +7,7 @@ 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; @@ -84,7 +85,7 @@ public WebElement getCell(int colIndex) /** * gets the cell corresponding to the specified column */ - public WebElement getCell(String columnIdentifier) + public WebElement getCell(CharSequence columnIdentifier) { return getCell(_grid.getColumnIndex(columnIdentifier)); } @@ -128,7 +129,7 @@ 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 columnIdentifier) + public ImageFileViewDialog clickImgFile(CharSequence columnIdentifier) { return elementCache().waitForAttachment(columnIdentifier).viewImgFile(); } @@ -136,7 +137,7 @@ public ImageFileViewDialog clickImgFile(String columnIdentifier) /** * finds a AttachmentCard specified filename, clicks it, and waits for the file to download */ - public File clickNonImgFile(String columnIdentifier) + public File clickNonImgFile(CharSequence columnIdentifier) { return elementCache().waitForAttachment(columnIdentifier).clickOnNonImgFile(); } @@ -144,7 +145,7 @@ public File clickNonImgFile(String columnIdentifier) /** * Returns the text in the row for the specified column */ - public String getText(String columnIdentifier) + public String getText(CharSequence columnIdentifier) { return getCell(columnIdentifier).getText(); } @@ -165,7 +166,7 @@ public List getTexts() */ public Map getRowMapByLabel() { - return getRowMap(FieldReferenceManager.FieldReference::getLabel); + return getRowMap(FieldReference::getLabel); } /** @@ -173,7 +174,7 @@ public Map getRowMapByLabel() */ public Map getRowMapByName() { - return getRowMap(columnHeader -> columnHeader.getFieldKey().getName()); + return getRowMap(FieldReference::getName); } /** @@ -181,17 +182,17 @@ public Map getRowMapByName() */ public Map getRowMapByFieldKey() { - return getRowMap(FieldReferenceManager.FieldReference::getFieldKey); + return getRowMap(FieldReference::getFieldKey); } - Map getRowMap(Function keyMapper) + Map getRowMap(Function keyMapper) { List columnValues = elementCache().getCellTexts(); - List headers = _grid.getHeaders(); + List headers = _grid.getHeaders(); Map rowMap = new LinkedHashMap<>(); - for (FieldReferenceManager.FieldReference header : headers) + for (FieldReference header : headers) { T key = keyMapper.apply(header); String value = columnValues.get(header.getDomIndex()); @@ -252,7 +253,7 @@ protected List getCellTexts() return cellTexts; } - public AttachmentCard waitForAttachment(String columnIdentifier) + public AttachmentCard waitForAttachment(CharSequence columnIdentifier) { 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 86cd8e3d0a..ff7f0b7458 100644 --- a/src/org/labkey/test/components/ui/grids/QueryGrid.java +++ b/src/org/labkey/test/components/ui/grids/QueryGrid.java @@ -83,7 +83,7 @@ public Map getRowMapByLabel(String text) * @param text text in the data cell * @return row data */ - public Map getRowMapByLabel(String columnIdentifier, String text) + public Map getRowMapByLabel(CharSequence columnIdentifier, String text) { return getRow(columnIdentifier, text).getRowMapByLabel(); } @@ -118,7 +118,7 @@ public Map getRowMapByLabel(Locator.XPathLocator containing) * @return this grid */ @Override - public QueryGrid selectRow(String columnIdentifier, String text, boolean checked) + public QueryGrid selectRow(CharSequence columnIdentifier, String text, boolean checked) { getRow(columnIdentifier, text).select(checked); return this; @@ -534,7 +534,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 8ce13d8b7a..2fd6fba318 100644 --- a/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java +++ b/src/org/labkey/test/components/ui/grids/ResponsiveGrid.java @@ -117,7 +117,7 @@ public void scrollToOrigin() * @param columnIdentifier fieldKey, name, or label of column * @return this grid */ - public T sortColumnAscending(String columnIdentifier) + public T sortColumnAscending(CharSequence columnIdentifier) { sortColumn(columnIdentifier, SortDirection.ASC); return getThis(); @@ -128,7 +128,7 @@ public T sortColumnAscending(String columnIdentifier) * @param columnIdentifier fieldKey, name, or label of column * @return this grid */ - public T sortColumnDescending(String columnIdentifier) + public T sortColumnDescending(CharSequence columnIdentifier) { sortColumn(columnIdentifier, SortDirection.DESC); return getThis(); @@ -138,7 +138,7 @@ public T sortColumnDescending(String columnIdentifier) * Sorts from the grid header menu * @param columnIdentifier fieldKey, name, or label of column */ - public void sortColumn(String columnIdentifier, SortDirection direction) + public void sortColumn(CharSequence columnIdentifier, SortDirection direction) { clickColumnMenuItem(columnIdentifier, direction.equals(SortDirection.DESC) ? "Sort descending" : "Sort ascending", true); } @@ -146,7 +146,7 @@ public void sortColumn(String columnIdentifier, SortDirection direction) /** * @param columnIdentifier fieldKey, name, or label of column */ - public void clearSort(String columnIdentifier) + public void clearSort(CharSequence columnIdentifier) { clickColumnMenuItem(columnIdentifier, "Clear sort", true); } @@ -154,7 +154,7 @@ public void clearSort(String columnIdentifier) /** * @param columnIdentifier fieldKey, name, or label of column */ - public boolean hasColumnSortIcon(String columnIdentifier) + public boolean hasColumnSortIcon(CharSequence columnIdentifier) { WebElement headerCell = elementCache().getColumnHeaderCell(columnIdentifier); Optional colHeaderIcon = Locator.XPathLocator.union( @@ -168,7 +168,7 @@ public boolean hasColumnSortIcon(String columnIdentifier) /** * @param columnIdentifier fieldKey, name, or label of column */ - public T filterColumn(String columnIdentifier, Filter.Operator operator) + public T filterColumn(CharSequence columnIdentifier, Filter.Operator operator) { return filterColumn(columnIdentifier, operator, null); } @@ -176,7 +176,7 @@ public T filterColumn(String columnIdentifier, Filter.Operator operator) /** * @param columnIdentifier fieldKey, name, or label of column */ - public T filterColumn(String columnIdentifier, Filter.Operator operator, Object value) + public T filterColumn(CharSequence columnIdentifier, Filter.Operator operator, Object value) { T _this = getThis(); doAndWaitForUpdate(()->initFilterColumn(columnIdentifier, operator, value).confirm()); @@ -186,7 +186,7 @@ public T filterColumn(String columnIdentifier, Filter.Operator operator, Object /** * @param columnIdentifier fieldKey, name, or label of column */ - public T filterColumn(String columnIdentifier, Filter.Operator operator1, Object value1, Filter.Operator operator2, Object value2) + public T filterColumn(CharSequence columnIdentifier, Filter.Operator operator1, Object value1, Filter.Operator operator2, Object value2) { T _this = getThis(); doAndWaitForUpdate(()-> { @@ -204,7 +204,7 @@ public T filterColumn(String columnIdentifier, Filter.Operator operator1, Object /** * @param columnIdentifier fieldKey, name, or label of column */ - public T filterBooleanColumn(String columnIdentifier, boolean value) + public T filterBooleanColumn(CharSequence columnIdentifier, boolean value) { T _this = getThis(); doAndWaitForUpdate(() -> { @@ -219,7 +219,7 @@ public T filterBooleanColumn(String columnIdentifier, boolean value) /** * @param columnIdentifier fieldKey, name, or label of column */ - public String filterColumnExpectingError(String columnIdentifier, Filter.Operator operator, Object value) + public String filterColumnExpectingError(CharSequence columnIdentifier, Filter.Operator operator, Object value) { GridFilterModal filterModal = initFilterColumn(columnIdentifier, operator, value); String errorMsg = filterModal.confirmExpectingError(); @@ -227,7 +227,7 @@ public String filterColumnExpectingError(String columnIdentifier, Filter.Operato return errorMsg; } - private GridFilterModal initFilterColumn(String columnIdentifier, Filter.Operator operator, Object value) + private GridFilterModal initFilterColumn(CharSequence columnIdentifier, Filter.Operator operator, Object value) { clickColumnMenuItem(columnIdentifier, "Filter...", false); GridFilterModal filterModal = new GridFilterModal(getDriver(), this); @@ -239,7 +239,7 @@ private GridFilterModal initFilterColumn(String columnIdentifier, Filter.Operato /** * @param columnIdentifier fieldKey, name, or label of column */ - public T removeColumnFilter(String columnIdentifier) + public T removeColumnFilter(CharSequence columnIdentifier) { clickColumnMenuItem(columnIdentifier, "Remove filter", true); return getThis(); @@ -248,7 +248,7 @@ public T removeColumnFilter(String columnIdentifier) /** * @param columnIdentifier fieldKey, name, or label of column */ - public boolean hasColumnFilterIcon(String columnIdentifier) + public boolean hasColumnFilterIcon(CharSequence columnIdentifier) { WebElement headerCell = elementCache().getColumnHeaderCell(columnIdentifier); Optional colHeaderIcon = Locator.tagWithClass("span", "grid-panel__col-header-icon") @@ -264,7 +264,7 @@ public boolean hasColumnFilterIcon(String columnIdentifier) * @param columnIdentifier fieldKey, name, or label of column * @return This grid. */ - public T hideColumn(String columnIdentifier) + public T hideColumn(CharSequence columnIdentifier) { // Because this will remove the column wait for the grid to update. clickColumnMenuItem(columnIdentifier, "Hide Column", true); @@ -288,14 +288,14 @@ public FieldSelectionDialog insertColumn() * @param columnIdentifier fieldKey, name, or label of column * @return A {@link FieldSelectionDialog} */ - public FieldSelectionDialog insertColumn(String columnIdentifier) + 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(columnIdentifier, "Insert Column", false); return new FieldSelectionDialog(getDriver(), this); } - protected void clickColumnMenuItem(String columnIdentifier, String menuText, boolean waitForUpdate) + protected void clickColumnMenuItem(CharSequence columnIdentifier, String menuText, boolean waitForUpdate) { if(hasLockedColumn()) @@ -328,7 +328,7 @@ protected void clickColumnMenuItem(String columnIdentifier, String menuText, boo * @param columnIdentifier fieldKey, name, or label of column * @param newColumnLabel new label for the column */ - public void editColumnLabel(String columnIdentifier, String newColumnLabel) + public void editColumnLabel(CharSequence columnIdentifier, String newColumnLabel) { // Get the column header. WebElement headerCell = elementCache().getColumnHeaderCell(columnIdentifier); @@ -397,7 +397,7 @@ public T selectRow(Map partialMap, boolean checked) * @param checked true for checked, false for unchecked * @return this grid */ - public ResponsiveGrid selectRow(String columnIdentifier, String text, boolean checked) + public ResponsiveGrid selectRow(CharSequence columnIdentifier, String text, boolean checked) { GridRow row = getRow(columnIdentifier, text); selectRowAndVerifyCheckedCounts(row, checked); @@ -431,7 +431,7 @@ else if (!checked && row.isSelected()) * @param checked True for checked, false for unchecked * @return this grid */ - public T selectRows(String columnIdentifier, Collection texts, boolean checked) + public T selectRows(CharSequence columnIdentifier, Collection texts, boolean checked) { for (String text : texts) { @@ -548,7 +548,7 @@ public Optional getOptionalRow(String text) * @param text The full text of the cell to match * @return the first row that matches */ - public GridRow getRow(String columnIdentifier, String text) + public GridRow getRow(CharSequence columnIdentifier, String text) { return elementCache().getRow(columnIdentifier, text); } @@ -559,7 +559,7 @@ public GridRow getRow(String columnIdentifier, String text) * @param text exact text to match in that column * @return the first row matching the search criteria */ - public Optional getOptionalRow(String columnIdentifier, String text) + public Optional getOptionalRow(CharSequence columnIdentifier, String text) { return elementCache().getOptionalRow(columnIdentifier, text); } @@ -596,7 +596,7 @@ public List getRows() /** * @param columnIdentifier fieldKey, name, or label of column */ - public List getColumnDataAsText(String columnIdentifier) + public List getColumnDataAsText(CharSequence columnIdentifier) { List columnData = new ArrayList<>(); for (GridRow row : getRows()) @@ -619,7 +619,7 @@ public boolean hasSelectColumn() * @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 columnIdentifier) + protected Integer getColumnIndex(CharSequence columnIdentifier) { return elementCache().getColumnIndex(columnIdentifier); } @@ -661,7 +661,7 @@ 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 columnIdentifier) + public String getCellText(int rowIndex, CharSequence columnIdentifier) { return getRow(rowIndex).getText(columnIdentifier); } @@ -704,7 +704,7 @@ public void clickLink(String text) * @param columnIdentifier fieldKey, name, or label of column * @param text text for link to match */ - public void clickLink(String columnIdentifier, String text) + public void clickLink(CharSequence columnIdentifier, String text) { getRow(columnIdentifier, text).clickLink(text); } @@ -737,7 +737,7 @@ public String getGridError() * @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 columnIdentifier) + public boolean getColumnPHIProtected(CharSequence columnIdentifier) { WebElement columnHeader = elementCache().getColumnHeaderCell(columnIdentifier); return columnHeader.getDomAttribute("class").contains("phi-protected"); @@ -810,7 +810,7 @@ public void toggle() } }; - protected final WebElement getColumnHeaderCell(String columnIdentifier) + protected final WebElement getColumnHeaderCell(CharSequence columnIdentifier) { return findColumnHeader(columnIdentifier).getElement(); } @@ -837,12 +837,12 @@ protected List findHeaders() return getGridHeaderManager().getColumnHeaders(); } - protected FieldReference findColumnHeader(String columnIdentifier) + protected FieldReference findColumnHeader(CharSequence columnIdentifier) { return getGridHeaderManager().findFieldReference(columnIdentifier); } - protected int getColumnIndex(String columnIdentifier) + protected int getColumnIndex(CharSequence columnIdentifier) { return findColumnHeader(columnIdentifier).getDomIndex(); } @@ -867,14 +867,14 @@ protected Optional getOptionalRow(String text) return new GridRow.GridRowFinder(ResponsiveGrid.this).withCellWithText(text).findOptional(this); } - protected GridRow getRow(String columnIdentifier, String text) + protected GridRow getRow(CharSequence columnIdentifier, String text) { int columnIndex = getColumnIndex(columnIdentifier); return new GridRow.GridRowFinder(ResponsiveGrid.this).withTextAtColumn(text, columnIndex) .find(this); } - protected Optional getOptionalRow(String columnIdentifier, String text) + protected Optional getOptionalRow(CharSequence columnIdentifier, String text) { int columnIndex = getColumnIndex(columnIdentifier); return new GridRow.GridRowFinder(ResponsiveGrid.this).withTextAtColumn(text, columnIndex) From 964fc05a5f9a9d0fdc9af4497934636b288c9089 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Thu, 22 May 2025 16:25:27 -0700 Subject: [PATCH 28/34] FieldKey improvements and creation of FieldInfo --- .../ui/entities/EntityBulkInsertDialog.java | 2 +- .../ui/entities/EntityInsertPanel.java | 2 +- .../ui/grids/FieldSelectionDialog.java | 35 +++++--- .../test/components/ui/grids/QueryGrid.java | 5 +- src/org/labkey/test/params/FieldInfo.java | 88 +++++++++++++++++++ src/org/labkey/test/params/FieldKey.java | 24 +++-- .../labkey/test/util/TestDataGenerator.java | 15 ++-- 7 files changed, 146 insertions(+), 25 deletions(-) create mode 100644 src/org/labkey/test/params/FieldInfo.java diff --git a/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java b/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java index 6469bedf5c..5cc5c5ee4f 100644 --- a/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java +++ b/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java @@ -452,7 +452,7 @@ protected void waitForReady() */ public static FieldKey fileUploadFieldKey(CharSequence fieldIdentifier) { - return FieldKey.fromFieldKey(fieldIdentifier + "-fileUpload"); + return FieldKey.fromFieldKey(FieldKey.fromName(fieldIdentifier) + "-fileUpload"); } @Override diff --git a/src/org/labkey/test/components/ui/entities/EntityInsertPanel.java b/src/org/labkey/test/components/ui/entities/EntityInsertPanel.java index b60ba679d4..cb80a53b0b 100644 --- a/src/org/labkey/test/components/ui/entities/EntityInsertPanel.java +++ b/src/org/labkey/test/components/ui/entities/EntityInsertPanel.java @@ -114,7 +114,7 @@ public EntityInsertPanel addSource(String sourceType) * @param columnIdentifier fieldKey, name, or label * @return this component */ - public EntityInsertPanel removeColumn(String columnIdentifier) + public EntityInsertPanel removeColumn(CharSequence columnIdentifier) { showGrid(); elementCache().grid.removeColumn(columnIdentifier); diff --git a/src/org/labkey/test/components/ui/grids/FieldSelectionDialog.java b/src/org/labkey/test/components/ui/grids/FieldSelectionDialog.java index 37083d8a4b..c7c01bbcae 100644 --- a/src/org/labkey/test/components/ui/grids/FieldSelectionDialog.java +++ b/src/org/labkey/test/components/ui/grids/FieldSelectionDialog.java @@ -103,15 +103,25 @@ public List getAvailableFieldLabels() */ public boolean isAvailableFieldSelected(String... fieldNameParts) { - WebElement listItem = 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 isFieldAvailable(FieldKey.fromParts(fieldNameParts)); + } + + public boolean isFieldAvailable(FieldKey fieldKey) { try { - expandAvailableFields(fieldNameParts); + getAvailableFieldElement(fieldKey); return true; } catch (NoSuchElementException e) @@ -128,9 +138,14 @@ public boolean isFieldAvailable(String... fieldNameParts) */ public FieldSelectionDialog selectAvailableField(String... fieldNameParts) { - WebElement listItem = expandAvailableFields(fieldNameParts); + return selectAvailableField(FieldKey.fromParts(fieldNameParts)); + } + + public FieldSelectionDialog selectAvailableField(FieldKey fieldKey) + { + WebElement listItem = getAvailableFieldElement(fieldKey); - Assert.assertTrue(String.format(FIELD_NOT_AVAILABLE, FieldKey.fromParts(fieldNameParts)), + Assert.assertTrue(String.format(FIELD_NOT_AVAILABLE, fieldKey), listItem.isDisplayed()); WebElement addIcon = Locator.tagWithClass("div", "view-field__action") @@ -144,19 +159,17 @@ public FieldSelectionDialog selectAvailableField(String... fieldNameParts) public WebElement getAvailableFieldElement(String... fieldNameParts) { - return expandAvailableFields(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 WebElement expandAvailableFields(String... fieldNameParts) + public WebElement getAvailableFieldElement(FieldKey fieldKey) { - FieldKey fieldKey = FieldKey.fromParts(fieldNameParts); Iterator iterator = fieldKey.getIterator(); while(iterator.hasNext()) diff --git a/src/org/labkey/test/components/ui/grids/QueryGrid.java b/src/org/labkey/test/components/ui/grids/QueryGrid.java index ff7f0b7458..9775a4666d 100644 --- a/src/org/labkey/test/components/ui/grids/QueryGrid.java +++ b/src/org/labkey/test/components/ui/grids/QueryGrid.java @@ -224,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(); diff --git a/src/org/labkey/test/params/FieldInfo.java b/src/org/labkey/test/params/FieldInfo.java new file mode 100644 index 0000000000..9bfa3df589 --- /dev/null +++ b/src/org/labkey/test/params/FieldInfo.java @@ -0,0 +1,88 @@ +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() + { + 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 84971e527d..6536604363 100644 --- a/src/org/labkey/test/params/FieldKey.java +++ b/src/org/labkey/test/params/FieldKey.java @@ -2,6 +2,7 @@ import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Arrays; @@ -11,7 +12,7 @@ public class FieldKey implements CharSequence { - public static final FieldKey EMPTY = new FieldKey(""); + 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"); @@ -21,7 +22,7 @@ public class FieldKey implements CharSequence private final String _name; private final String _fieldKey; - private FieldKey(String name) + protected FieldKey(String name) { _parent = null; _name = name; @@ -59,12 +60,23 @@ public static FieldKey fromParts(String... parts) * @param fieldKey String or FieldKey * @return FieldKey representation of the String, or the identity if a FieldKey was provided */ - public static FieldKey fromFieldKey(CharSequence fieldKey) + public static @Nullable FieldKey fromFieldKey(CharSequence fieldKey) { if (fieldKey instanceof FieldKey fk) + { return fk; + } else - return fromParts(Arrays.stream(fieldKey.toString().split(SEPARATOR)).map(FieldKey::decodePart).toList()); + { + try + { + return fromParts(Arrays.stream(fieldKey.toString().split(SEPARATOR)).map(FieldKey::decodePart).toList()); + } + catch (IllegalArgumentException iae) + { + return null; + } + } } /** @@ -165,12 +177,12 @@ public final boolean equals(Object o) { if (!(o instanceof FieldKey fieldKey)) return false; - return _fieldKey.equals(fieldKey._fieldKey); + return _fieldKey.equalsIgnoreCase(fieldKey._fieldKey); // FieldKeys aren't case-sensitive? } @Override public int hashCode() { - return _fieldKey.hashCode(); + return _fieldKey.toLowerCase().hashCode(); // FieldKeys aren't case-sensitive? } } diff --git a/src/org/labkey/test/util/TestDataGenerator.java b/src/org/labkey/test/util/TestDataGenerator.java index ac321eaa39..eb999fbbe1 100644 --- a/src/org/labkey/test/util/TestDataGenerator.java +++ b/src/org/labkey/test/util/TestDataGenerator.java @@ -72,8 +72,10 @@ */ 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' // chose a Character random from this String - public static final String CHARSET_STRING = "ABCDEFG01234abcdefvxyz~!@#$%^&*()-+=_{}[]|:;\"',.<>"; + public static final String CHARSET_STRING = "ABCDEFG01234abcdefvxyz~!@#$%^&*()-+=_{}[]|:;\"',.<>и안は" + WIDE_PLACEHOLDER; public static final String ALPHANUMERIC_STRING = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvxyz"; public static final String DOMAIN_SPECIAL_STRING = "+- _.:&()/"; public static final String ILLEGAL_DOMAIN_NAME_CHARSET = "<>[]{};,`\"~!@#$%^*=|?\\"; @@ -470,7 +472,11 @@ public static String randomString(int size, @Nullable String exclusion, @Nullabl for (int i=0; i List shuffleSelect(List allFields, int selectCount) return shuffled.subList(0, selectCount); } - // Require suppliers to provide reassurance that mutable objects are only reused intentionally - public static List randomSelect(List> allFields, int selectCount) + public static List randomSelect(List allOptions, int selectCount) { List selected = new ArrayList<>(); for (int i = 0; i < selectCount; i++) { - selected.add(allFields.get(randomInt(0, allFields.size())).get()); + selected.add(allOptions.get(randomInt(0, allOptions.size()))); } return selected; } From 6df25df96325d052d333a1a3dadba23448e6cd51 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Thu, 22 May 2025 18:31:32 -0700 Subject: [PATCH 29/34] Add more FieldInfo examples --- src/org/labkey/test/params/FieldInfo.java | 24 ++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/org/labkey/test/params/FieldInfo.java b/src/org/labkey/test/params/FieldInfo.java index 9bfa3df589..ebd28a0019 100644 --- a/src/org/labkey/test/params/FieldInfo.java +++ b/src/org/labkey/test/params/FieldInfo.java @@ -74,7 +74,29 @@ public FieldKey child(String name) public FieldDefinition getFieldDefinition() { - FieldDefinition fieldDefinition = new FieldDefinition(getName(), _columnType); + 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()); From 0e20bd48837a6d12104681ad6525e07242554f12 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Fri, 23 May 2025 12:28:07 -0700 Subject: [PATCH 30/34] Add JavaDoc --- .../ui/entities/EntityBulkInsertDialog.java | 7 +- .../ui/entities/EntityBulkUpdateDialog.java | 88 ++++++++++++++++++- 2 files changed, 85 insertions(+), 10 deletions(-) diff --git a/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java b/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java index 5cc5c5ee4f..49b7c87ad3 100644 --- a/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java +++ b/src/org/labkey/test/components/ui/entities/EntityBulkInsertDialog.java @@ -109,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; } diff --git a/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java b/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java index 88c36cb0c5..efa57bc161 100644 --- a/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java +++ b/src/org/labkey/test/components/ui/entities/EntityBulkUpdateDialog.java @@ -60,11 +60,17 @@ public EntityBulkUpdateDialog adjustChangeCounter(int change) return this; } + /** + * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey}) + */ public boolean isFieldEnabled(CharSequence fieldIdentifier) { return elementCache().getToggle(fieldIdentifier).isOn(); } + /** + * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey}) + */ public EntityBulkUpdateDialog setEditableState(CharSequence fieldIdentifier, boolean enable) { ToggleButton toggle = elementCache().getToggle(fieldIdentifier); @@ -101,6 +107,11 @@ else if (field.getType() == FieldDefinition.ColumnType.MultiLine) // interact with selection fields + /** + * @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) { FilteringReactSelect reactSelect = enableSelectionField(fieldIdentifier); @@ -108,16 +119,29 @@ public EntityBulkUpdateDialog setSelectionField(CharSequence fieldIdentifier, Li return this; } + /** + * @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 setSelectionField(fieldIdentifier, List.of(selectValue)); } + /** + * @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 enableSelectionField(fieldIdentifier).getOptions(); } + /** + * @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) { return enableSelectionField(fieldIdentifier).getSelections(); @@ -132,70 +156,122 @@ public List getSelectionFieldValues(CharSequence fieldIdentifier) return reactSelect; } - public EntityBulkUpdateDialog setTextArea(CharSequence fieldIdentifier, String text) + /** + * @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(text); + 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(); } - // 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 setTextField(CharSequence fieldIdentifier, String value) { enableAndWait(fieldIdentifier, elementCache().textInput(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 getTextField(CharSequence fieldIdentifier) { return enableAndWait(fieldIdentifier, elementCache().textInput(fieldIdentifier)).get(); } + /** + * @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(fieldIdentifier, elementCache().numericInput(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 getNumericField(CharSequence fieldIdentifier) { return elementCache().numericInput(fieldIdentifier).get(); } + /** + * @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(fieldIdentifier, elementCache().dateInput(fieldIdentifier)).set(dateString); return this; } + /** + * @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(fieldIdentifier).get(); } + /** + * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey}) + * @return file attachment component + */ public FileAttachmentContainer getFileField(CharSequence fieldIdentifier) { fieldIdentifier = EntityBulkInsertDialog.fileUploadFieldKey(fieldIdentifier); return enableAndWait(fieldIdentifier, elementCache().fileUploadField(fieldIdentifier)); } + /** + * @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(fieldIdentifier).attachFile(file); return this; } + /** + * @param fieldIdentifier Identifier for the field; name ({@link String}) or fieldKey ({@link FieldKey}) + * @return this component + */ public EntityBulkUpdateDialog removeFile(CharSequence fieldIdentifier) { getFileField(fieldIdentifier).removeFile(); return this; } + /** + * @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) { enableAndWait(fieldIdentifier, getCheckBox(fieldIdentifier)).set(checked); @@ -210,6 +286,10 @@ private > T enableAndWait(CharSequence fieldIdentifier, T return formItem; } + /** + * @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(fieldIdentifier).get(); From a59e451a0e94370be99f3df05b29842f6912396c Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Fri, 23 May 2025 15:48:33 -0700 Subject: [PATCH 31/34] Fix build after merge --- src/org/labkey/test/params/FieldKey.java | 2 +- src/org/labkey/test/util/TestDataGenerator.java | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/org/labkey/test/params/FieldKey.java b/src/org/labkey/test/params/FieldKey.java index 6536604363..6070cbde5f 100644 --- a/src/org/labkey/test/params/FieldKey.java +++ b/src/org/labkey/test/params/FieldKey.java @@ -22,7 +22,7 @@ public class FieldKey implements CharSequence private final String _name; private final String _fieldKey; - protected FieldKey(String name) + private FieldKey(String name) { _parent = null; _name = name; diff --git a/src/org/labkey/test/util/TestDataGenerator.java b/src/org/labkey/test/util/TestDataGenerator.java index a12f80228e..4e36c8079e 100644 --- a/src/org/labkey/test/util/TestDataGenerator.java +++ b/src/org/labkey/test/util/TestDataGenerator.java @@ -447,6 +447,12 @@ private Supplier getDefaultDataSupplier(String columnType) case "date": case "datetime": return ()-> randomDateString(DateUtils.addWeeks(new Date(), -39), new Date()); + case "time": + return ()-> + randomInt(0, 23) + ":" + // hour + StringUtils.leftPad(String.valueOf(randomInt(0, 59)), 2, "0") + ":" + // minute + StringUtils.leftPad(String.valueOf(randomInt(0, 59)), 2, "0") + "." + // second + StringUtils.leftPad(String.valueOf(randomInt(0, 999)), 3, "0"); // millisecond default: throw new IllegalArgumentException("ColumnType " + columnType + " isn't implemented yet"); } @@ -663,11 +669,6 @@ public String getDataAsTsv() return TestDataUtils.stringFromRowMaps(_rows, getFieldsForFile(), true, CSVFormat.TDF); } - public String writeTsvContents(boolean includeHeaders) - { - return TestDataUtils.stringFromRowMaps(_rows, getFieldsForFile(), false, CSVFormat.TDF); - } - public File writeGeneratedDataToExcel(String sheetName, String fileName) throws IOException { File file = new File(TestFileUtils.getTestTempDir(), fileName); From 4f415c79e9207be5cd7263429e817b66eb93707e Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Fri, 23 May 2025 17:01:41 -0700 Subject: [PATCH 32/34] Support FieldKey in DetailTableEdit --- .../components/ui/grids/DetailTableEdit.java | 175 +++++++++--------- 1 file changed, 85 insertions(+), 90 deletions(-) diff --git a/src/org/labkey/test/components/ui/grids/DetailTableEdit.java b/src/org/labkey/test/components/ui/grids/DetailTableEdit.java index ca162d0b7c..0e1bf10e7f 100644 --- a/src/org/labkey/test/components/ui/grids/DetailTableEdit.java +++ b/src/org/labkey/test/components/ui/grids/DetailTableEdit.java @@ -87,11 +87,14 @@ public DetailTableEdit adjustChangeCounter(int change) return this; } - public boolean isFieldPresent(String nameOrLabel) + /** + * @param columnIdentifier fieldKey, name, or label + */ + public boolean isFieldPresent(CharSequence columnIdentifier) { try { - elementCache().findValueCell(nameOrLabel); + elementCache().findValueCell(columnIdentifier); return true; } catch (NoSuchElementException e) @@ -103,13 +106,13 @@ public boolean isFieldPresent(String nameOrLabel) * Check to see if a field is editable. Could be state dependent, that is it returns false if the field is * loading but if checked later could return true. * - * @param nameOrLabel The name or label of the field to check. + * @param columnIdentifier fieldKey, name, or label * @return True if it is false otherwise. **/ - public boolean isFieldEditable(String nameOrLabel) + public boolean isFieldEditable(CharSequence columnIdentifier) { // TODO Could put a check here to see if a field is loading then return false, or wait. - WebElement fieldValueElement = elementCache().findValueCell(nameOrLabel); + WebElement fieldValueElement = elementCache().findValueCell(columnIdentifier); return isEditableField(fieldValueElement); } @@ -122,49 +125,43 @@ private boolean isEditableField(WebElement element) /** * Get the value of a read only field. * - * @param nameOrLabel The name or label of the field to get. + * @param columnIdentifier fieldKey, name, or label * @return The value in the field. **/ - public String getReadOnlyField(String nameOrLabel) + public String getReadOnlyField(CharSequence columnIdentifier) { - WebElement fieldValueElement = elementCache().findValueCell(nameOrLabel); + WebElement fieldValueElement = elementCache().findValueCell(columnIdentifier); return Locator.xpath("./div/*").findElement(fieldValueElement).getText(); } /** * Get the value of a text field. * - * @param nameOrLabel The name or label of the field to get. + * @param columnIdentifier fieldKey, name, or label * @return The value in the field. **/ - public String getTextField(String nameOrLabel) + public String getTextField(CharSequence columnIdentifier) { - return elementCache().findInput(nameOrLabel).get(); + return elementCache().findInput(columnIdentifier).get(); } /** * Set a text field. * - * @param nameOrLabel The name or label of the field to set. + * @param columnIdentifier fieldKey, name, or label * @param value The value to set the field to. * @return A reference to this editable detail table. **/ - public DetailTableEdit setTextField(String nameOrLabel, String value) + public DetailTableEdit setTextField(CharSequence columnIdentifier, String value) { - if (isFieldEditable(nameOrLabel)) + if (isFieldEditable(columnIdentifier)) { - elementCache().findInput(nameOrLabel); - WebElement fieldValueElement = elementCache().findValueCell(nameOrLabel); - - WebElement editableElement = Locator.xpath("./div/div/*[self::input or self::textarea]").findElement(fieldValueElement); - getWrapper().setFormElement(editableElement, value); - editableElement.clear(); - WebDriverWrapper.waitFor(()->editableElement.getText().isEmpty(), 500); - editableElement.sendKeys(value); + Input input = elementCache().findInput(columnIdentifier); + input.setValue(value); } else { - throw new IllegalArgumentException("Field with label '" + nameOrLabel + "' is read-only. This field can not be set."); + throw new IllegalArgumentException("Field '" + columnIdentifier + "' is read-only. This field can not be set."); } _changeCounter++; @@ -196,16 +193,16 @@ public DetailTableEdit setTextareaByFieldName(String fieldName, String value) /** * Get the value of a boolean field. * - * @param nameOrLabel The name or label of the field to get. + * @param columnIdentifier fieldKey, name, or label * @return The value of the field. **/ - public boolean getBooleanField(String nameOrLabel) + public boolean getBooleanField(CharSequence columnIdentifier) { // The text used in the field label and the value of the name attribute in the checkbox don't always have the same case. - WebElement editableElement = Locator.tag("input").findElement(elementCache().findValueCell(nameOrLabel)); + WebElement editableElement = Locator.tag("input").findElement(elementCache().findValueCell(columnIdentifier)); String elementType = editableElement.getDomAttribute("type").toLowerCase().trim(); - Assert.assertEquals(String.format("Field '%s' is not a checkbox. Cannot be get true/false value.", nameOrLabel), "checkbox", elementType); + Assert.assertEquals(String.format("Field '%s' is not a checkbox. Cannot be get true/false value.", columnIdentifier), "checkbox", elementType); return new Checkbox(editableElement).isChecked(); } @@ -213,21 +210,21 @@ public boolean getBooleanField(String nameOrLabel) /** * Set a boolean field (a checkbox). * - * @param nameOrLabel The label of the field to set. + * @param columnIdentifier fieldKey, name, or label * @param value True will check it, false will uncheck it. * @return A reference to this editable detail table. **/ - public DetailTableEdit setBooleanField(String nameOrLabel, boolean value) + public DetailTableEdit setBooleanField(CharSequence columnIdentifier, boolean value) { - WebElement fieldValueElement = elementCache().findValueCell(nameOrLabel); - Assert.assertTrue(String.format("Field '%s' is not editable and cannot be set.", nameOrLabel), isEditableField(fieldValueElement)); + WebElement fieldValueElement = elementCache().findValueCell(columnIdentifier); + Assert.assertTrue(String.format("Field '%s' is not editable and cannot be set.", columnIdentifier), isEditableField(fieldValueElement)); getWrapper().scrollIntoView(fieldValueElement); WebElement editableElement = fieldValueElement.findElement(By.xpath("./div/div/input")); String elementType = editableElement.getDomAttribute("type").toLowerCase().trim(); - Assert.assertEquals(String.format("Field '%s' is not a checkbox. Cannot be set to true/false.", nameOrLabel), "checkbox", elementType); + Assert.assertEquals(String.format("Field '%s' is not a checkbox. Cannot be set to true/false.", columnIdentifier), "checkbox", elementType); Checkbox checkbox = new Checkbox(editableElement); @@ -240,97 +237,97 @@ public DetailTableEdit setBooleanField(String nameOrLabel, boolean value) /** * Get the value of an int field. You could also call getTextField * - * @param nameOrLabel The name or label of the field to get. + * @param columnIdentifier fieldKey, name, or label * @return The value of the field as an int. **/ - public int getIntField(String nameOrLabel) + public int getIntField(CharSequence columnIdentifier) { - return Integer.getInteger(getTextField(nameOrLabel)); + return Integer.getInteger(getTextField(columnIdentifier)); } /** * Set an int field. * - * @param nameOrLabel The name or label of the field to set. + * @param columnIdentifier fieldKey, name, or label * @param value The int value to set the field to. * @return A reference to this editable detail table. **/ - public DetailTableEdit setIntField(String nameOrLabel, int value) + public DetailTableEdit setIntField(CharSequence columnIdentifier, int value) { - return setTextField(nameOrLabel, Integer.toString(value)); + return setTextField(columnIdentifier, Integer.toString(value)); } - public FileUploadField getFileField(String fieldLabel) + public FileUploadField getFileField(CharSequence columnIdentifier) { - return elementCache().fileField(fieldLabel); + return elementCache().fileField(columnIdentifier); } - public DetailTableEdit setFileField(String nameOrLabel, File file) + public DetailTableEdit setFileField(CharSequence columnIdentifier, File file) { - getFileField(nameOrLabel) + getFileField(columnIdentifier) .setFile(file); _changeCounter++; return this; } - public DetailTableEdit removeFileField(String nameOrLabel) + public DetailTableEdit removeFileField(CharSequence columnIdentifier) { - getFileField(nameOrLabel).removeFile(); + getFileField(columnIdentifier).removeFile(); _changeCounter++; return this; } - public boolean isFileFieldBlank(String fieldLabel) + public boolean isFileFieldBlank(CharSequence columnIdentifier) { - return !getFileField(fieldLabel) + return !getFileField(columnIdentifier) .hasAttachedFile(); } - public FilteringReactSelect getSelectField(String nameOrLabel) + public FilteringReactSelect getSelectField(CharSequence columnIdentifier) { - return elementCache().findSelect(nameOrLabel); + return elementCache().findSelect(columnIdentifier); } /** * Get the value of a select field. * - * @param nameOrLabel The name or label of the field to get. + * @param columnIdentifier fieldKey, name, or label * @return The selected value. **/ - public String getSelectedValue(String nameOrLabel) + public String getSelectedValue(CharSequence columnIdentifier) { - return getSelectField(nameOrLabel).getValue(); + return getSelectField(columnIdentifier).getValue(); } /** * This allows you to query a given select in the edit panel to see what options it offers. * - * @param nameOrLabel The name or label of the field to get. + * @param columnIdentifier fieldKey, name, or label * @return List of strings for the values in the list. **/ - public List getSelectOptions(String nameOrLabel) + public List getSelectOptions(CharSequence columnIdentifier) { - return getSelectField(nameOrLabel).getOptions(); + return getSelectField(columnIdentifier).getOptions(); } /** * Select a single value from a select list. * - * @param nameOrLabel The name or label of the field to set. + * @param columnIdentifier fieldKey, name, or label * @param selectValue The value to select from the list. * @return A reference to this editable detail table. **/ - public DetailTableEdit setSelectValue(String nameOrLabel, String selectValue) + public DetailTableEdit setSelectValue(CharSequence columnIdentifier, String selectValue) { List selection = Arrays.asList(selectValue); - return setSelectValue(nameOrLabel, selection); + return setSelectValue(columnIdentifier, selection); } - public DetailTableEdit createSelectValue(String nameOrLabel, String value) + public DetailTableEdit createSelectValue(CharSequence columnIdentifier, String value) { - var select = ReactSelect.finder(getDriver()).waitFor(elementCache().findValueCell(nameOrLabel)); + var select = ReactSelect.finder(getDriver()).waitFor(elementCache().findValueCell(columnIdentifier)); select.createValue(value); return this; } @@ -338,13 +335,13 @@ public DetailTableEdit createSelectValue(String nameOrLabel, String value) /** * Select multiple values from a select list. * - * @param nameOrLabel The name or label of the field to set. + * @param columnIdentifier fieldKey, name, or label * @param selectValues The value to select from the list. * @return A reference to this editable detail table. **/ - public DetailTableEdit setSelectValue(String nameOrLabel, List selectValues) + public DetailTableEdit setSelectValue(CharSequence columnIdentifier, List selectValues) { - FilteringReactSelect reactSelect = getSelectField(nameOrLabel); + FilteringReactSelect reactSelect = getSelectField(columnIdentifier); selectValues.forEach(reactSelect::typeAheadSelect); _changeCounter++; return this; @@ -353,31 +350,31 @@ public DetailTableEdit setSelectValue(String nameOrLabel, List selectVal /** * Clear a given select field. * - * @param nameOrLabel The name or label of the field to clear. + * @param columnIdentifier fieldKey, name, or label * @return A reference to this editable detail table. **/ - public DetailTableEdit clearSelectValue(String nameOrLabel) + public DetailTableEdit clearSelectValue(CharSequence columnIdentifier) { - return clearSelectValue(nameOrLabel, true, true); + return clearSelectValue(columnIdentifier, true, true); } /** * Clear a given select field. * - * @param nameOrLabel The name or label of the field to clear. + * @param columnIdentifier fieldKey, name, or label * @param waitForSelection If true, wait for the select to have a selection before clearing it * @param assertSelection If true, assert if no selection appears (note: does nothing if waitForSelection is not true) * @return A reference to this editable detail table. */ - public DetailTableEdit clearSelectValue(String nameOrLabel, boolean waitForSelection, boolean assertSelection) + public DetailTableEdit clearSelectValue(CharSequence columnIdentifier, boolean waitForSelection, boolean assertSelection) { - var select = getSelectField(nameOrLabel); + var select = getSelectField(columnIdentifier); if (waitForSelection) { if (assertSelection) { WebDriverWrapper.waitFor(select::hasSelection, - String.format("The %s select did not have any selection in time", nameOrLabel), _readyTimeout); + String.format("The %s select did not have any selection in time", columnIdentifier), _readyTimeout); } else WebDriverWrapper.waitFor(select::hasSelection, 1_000); @@ -389,7 +386,7 @@ public DetailTableEdit clearSelectValue(String nameOrLabel, boolean waitForSelec /** * Set a DateTime, Date or Time field. - * @param nameOrLabel The name or label of the field to set. + * @param columnIdentifier fieldKey, name, or label * @param dateTime Will be used to determine what kind of field is being set and how to set it. If the parameter * is a LocalDateTime object then it is assumed that field is a DateTime field. If the parameter is * a LocalDate object then it is assumed to be a date-only field. And I think you can guess what @@ -397,9 +394,9 @@ public DetailTableEdit clearSelectValue(String nameOrLabel, boolean waitForSelec * is typed into the field (no picker is used). * @return A reference to this DetailTableEdit object. */ - public DetailTableEdit setDateTimeField(String nameOrLabel, Object dateTime) + public DetailTableEdit setDateTimeField(CharSequence columnIdentifier, Object dateTime) { - ReactDateTimePicker dateTimePicker = getDateTimePicker(nameOrLabel); + ReactDateTimePicker dateTimePicker = getDateTimePicker(columnIdentifier); if (dateTime instanceof LocalDateTime localDateTime) { dateTimePicker.select(localDateTime); @@ -426,22 +423,22 @@ else if (dateTime instanceof String setValue) return this; } - public String getDateTimeField(String nameOrLabel) + public String getDateTimeField(CharSequence columnIdentifier) { - ReactDateTimePicker dateTimePicker = getDateTimePicker(nameOrLabel); + ReactDateTimePicker dateTimePicker = getDateTimePicker(columnIdentifier); return dateTimePicker.get(); } - public void clearDateTimeField(String nameOrLabel) + public void clearDateTimeField(CharSequence columnIdentifier) { - ReactDateTimePicker dateTimePicker = getDateTimePicker(nameOrLabel); + ReactDateTimePicker dateTimePicker = getDateTimePicker(columnIdentifier); dateTimePicker.clear(); _changeCounter++; } - private ReactDateTimePicker getDateTimePicker(String nameOrLabel) + private ReactDateTimePicker getDateTimePicker(CharSequence columnIdentifier) { - return new ReactDateTimePicker.ReactDateTimeInputFinder(getDriver()).find(elementCache().findValueCell(nameOrLabel)); + return new ReactDateTimePicker.ReactDateTimeInputFinder(getDriver()).find(elementCache().findValueCell(columnIdentifier)); } // For use when the field is of an unknown type, as can occur in fuzz tests @@ -585,9 +582,9 @@ public ElementCache() public final WebElement editPanel = Locator.tagWithClass("div", "detail__editing") .findWhenNeeded(this); - public WebElement findValueCell(String nameOrLabel) + public WebElement findValueCell(CharSequence columnIdentifier) { - return getFieldManager().findFieldReference(nameOrLabel).getElement(); + return getFieldManager().findFieldReference(columnIdentifier).getElement(); } private FieldReferenceManager _fieldReferenceManager; @@ -603,8 +600,7 @@ private FieldReferenceManager getFieldManager() // Use JavaScript to get fieldKeys and captions in one operation, rather than making 2N calls to 'WebElement.getDomAttribute' List> captionsAndKeys = getWrapper().executeScript( """ - - var cells = arguments[0]; + var cells = arguments[0]; var captions = []; var fieldkeys = []; for (var i = 0; i < cells.length; i++) @@ -613,8 +609,7 @@ private FieldReferenceManager getFieldManager() fieldkeys.push(cells[i].dataset.fieldkey); } return [captions, fieldkeys]; - """, List. - class, + """, List.class, valueCells); List captions = captionsAndKeys.get(0); List fieldkeys = captionsAndKeys.get(1); @@ -629,9 +624,9 @@ private FieldReferenceManager getFieldManager() return _fieldReferenceManager; } - public FileUploadField fileField(String nameOrLabel) + public FileUploadField fileField(CharSequence columnIdentifier) { - return new FileUploadField(findValueCell(nameOrLabel), getDriver()); + return new FileUploadField(findValueCell(columnIdentifier), getDriver()); } public Locator validationMsg = Locator.tagWithClass("span", "validation-message"); @@ -643,14 +638,14 @@ public FileUploadField fileField(String nameOrLabel) public WebElement commentInput = Locator.tagWithId("textarea", "actionComments").refindWhenNeeded(getDriver()); - public FilteringReactSelect findSelect(String nameOrLabel) + public FilteringReactSelect findSelect(CharSequence columnIdentifier) { - return FilteringReactSelect.finder(_driver).timeout(_readyTimeout).waitFor(findValueCell(nameOrLabel)); + return FilteringReactSelect.finder(_driver).timeout(_readyTimeout).waitFor(findValueCell(columnIdentifier)); } - public Input findInput(String nameOrLabel) + public Input findInput(CharSequence columnIdentifier) { - return Input.Input(Locator.xpath("./div/div/*[self::input or self::textarea]"), getDriver()).find(findValueCell(nameOrLabel)); + return Input.Input(Locator.xpath("./div/div/*[self::input or self::textarea]"), getDriver()).find(findValueCell(columnIdentifier)); } } From c632e09c216481690c7eb04ef3403e1de20ae12d Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Fri, 23 May 2025 17:17:43 -0700 Subject: [PATCH 33/34] Fix some :Date field names --- src/org/labkey/test/BaseWebDriverTest.java | 4 ++-- src/org/labkey/test/util/TestDataGenerator.java | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) 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/util/TestDataGenerator.java b/src/org/labkey/test/util/TestDataGenerator.java index 4e36c8079e..5ca901f3f0 100644 --- a/src/org/labkey/test/util/TestDataGenerator.java +++ b/src/org/labkey/test/util/TestDataGenerator.java @@ -74,8 +74,9 @@ 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~!@#$%^&*()-+=_{}[]|:;\"',.<>и안は" + WIDE_PLACEHOLDER; + 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 = "<>[]{};,`\"~!@#$%^*=|?\\"; @@ -581,7 +582,7 @@ public static String randomFieldName(String part, int numStartChars, int numEndC public static String randomFieldName(@NotNull String part, int numStartChars, int numEndChars, @Nullable String exclusion) { // use the characters that we know are encoded in fieldKeys plus characters that we know clients are using - String chars = ALL_ILLEGAL_QUERY_KEY_CHARACTERS + " %()=+-[]_|*`'\":;<>?!@#^"; + String chars = ALL_ILLEGAL_QUERY_KEY_CHARACTERS + " %()=+-[]_|*`'\":;<>?!@#^" + NON_LATIN_STRING + WIDE_PLACEHOLDER ; String randomFieldName = randomName(part, numStartChars, numEndChars, chars, exclusion); TestLogger.log("Generated random field name: " + randomFieldName); From 3f08747d52e4d14fcc4296be379899f78a9a84b9 Mon Sep 17 00:00:00 2001 From: labkey-tchad Date: Wed, 28 May 2025 08:28:44 -0700 Subject: [PATCH 34/34] Minor cache fixes --- src/org/labkey/test/params/FieldDefinition.java | 8 ++++++++ 1 file changed, 8 insertions(+) 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();