-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathEditableGrid.java
More file actions
1440 lines (1254 loc) · 54.8 KB
/
EditableGrid.java
File metadata and controls
1440 lines (1254 loc) · 54.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
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.WebDriverComponent;
import org.labkey.test.components.html.Checkbox;
import org.labkey.test.components.html.Input;
import org.labkey.test.components.react.ReactDateTimePicker;
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.CachingSupplier;
import org.labkey.test.util.selenium.ScrollUtils;
import org.labkey.test.util.selenium.WebElementUtils;
import org.openqa.selenium.By;
import org.openqa.selenium.Keys;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.NotFoundException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.IOException;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TimeZone;
import java.util.function.Function;
import java.util.stream.Collectors;
import static org.awaitility.Awaitility.await;
import static org.labkey.test.BaseWebDriverTest.WAIT_FOR_JAVASCRIPT;
import static org.labkey.test.WebDriverWrapper.waitFor;
import static org.labkey.test.util.TestLogger.log;
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<EditableGrid.ElementCache>
{
public static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
public static final String SELECT_COLUMN_LABEL_PLACEHOLDER = "<select>";
public static final FieldKey SELECT_COLUMN_ID = FieldKey.fromParts("__select__");
public static final String ROW_NUMBER_COLUMN_HEADER = "<row number>";
private final WebElement _gridElement;
private final WebDriver _driver;
protected EditableGrid(WebElement editableGrid, WebDriver driver)
{
_gridElement = editableGrid;
_driver = driver;
}
protected EditableGrid(EditableGrid wrappedGrid)
{
this(wrappedGrid.getComponentElement(), wrappedGrid.getDriver());
}
@Override
protected WebDriver getDriver()
{
return _driver;
}
@Override
public WebElement getComponentElement()
{
return _gridElement;
}
@Override
public void waitForReady()
{
Locators.loadingGrid.waitForElementToDisappear(this, 30000);
Locators.spinner.waitForElementToDisappear(this, 30000);
}
/**
* Quote values to be pasted into lookup columns. Prevents a value containing a comma from being interpreted as
* multiple values.
* @param values the raw values
* @return The values, quoted if necessary for pasting into a single lookup cell
*/
public static String quoteForPaste(String... values)
{
return Arrays.stream(values).map(CSVFormat.DEFAULT::format).collect(Collectors.joining(","));
}
public void clickDelete()
{
doAndWaitForRowCountUpdate(() -> elementCache().deleteRowsBtn.click());
}
public EntityBulkInsertDialog clickBulkAdd()
{
getWrapper().shortWait().until(ExpectedConditions.elementToBeClickable(elementCache().bulkAddBtn));
elementCache().bulkAddBtn.click();
return new EntityBulkInsertDialog(getDriver());
}
public EntityBulkUpdateDialog clickBulkUpdate()
{
getWrapper().shortWait().until(ExpectedConditions.elementToBeClickable(elementCache().bulkUpdateBtn));
elementCache().bulkUpdateBtn.click();
return new EntityBulkUpdateDialog(getDriver());
}
public ExportMenu getExportMenu()
{
return elementCache().exportMenu;
}
public List<String> getColumnLabels()
{
return elementCache().getColumnLabels();
}
public Integer getColumnIndex(CharSequence columnIdentifier)
{
return elementCache().getColumnIndex(columnIdentifier);
}
/**
* Remove the specified column from the grid
* @param columnIdentifier fieldKey, name, or label
* @return this component
*/
public EditableGrid removeColumn(CharSequence columnIdentifier)
{
doAndWaitForColumnUpdate(() ->
{
WebElement headerCell = elementCache().getColumnHeaderCell(columnIdentifier);
Locator.byClass("fa-chevron-circle-down").findElement(headerCell).click();
Locator.tagWithText("a", "Remove Column").findElement(headerCell).click();
});
return this;
}
public boolean canRemoveColumn(CharSequence columnIdentifier)
{
WebElement headerCell = elementCache().getColumnHeaderCell(columnIdentifier);
WebElement downBtn = Locator.byClass("fa-chevron-circle-down").findElementOrNull(headerCell);
if (downBtn == null)
return false;
downBtn.click();
WebElement removeBtn = Locator.tagWithText("a", "Remove Column").findElementOrNull(headerCell);
boolean canRemove = removeBtn != null && removeBtn.isDisplayed() && removeBtn.isEnabled();
downBtn.click(); // close dropdown
return canRemove;
}
private boolean hasSelectColumn()
{
return elementCache().hasSelectColumn.get();
}
public EditableGrid selectRow(int index, boolean checked)
{
elementCache().getCheckbox(index).set(checked);
return this;
}
public boolean isRowSelected(int index)
{
return elementCache().getCheckbox(index).isSelected();
}
public EditableGrid selectAll(boolean checked)
{
elementCache().selectAllCheckbox.set(checked);
return this;
}
public boolean areAllRowsSelected()
{
return elementCache().selectAllCheckbox.isSelected();
}
/**
* Selects a range of rows in the current view.
* If the range is within a range of already-selected rows, will deselect the specified range
* @param start the starting index (0-based), of non-header rows with checkboxes
* @param end the ending index
* @return the current instance
*/
public EditableGrid shiftSelectRange(int start, int end)
{
if (!hasSelectColumn())
throw new NoSuchElementException("there is no selection column for grid");
var checkBoxes = Locator.tag("tr").child("td")
.child(Locator.byClass("table-cell-content"))
.child(Locator.tagWithAttribute("input", "type", "checkbox"))
.findElements(elementCache().table);
getWrapper().scrollIntoView(checkBoxes.get(start)); // Make sure the header isn't in the way
checkBoxes.get(start).click();
getWrapper().scrollIntoView(checkBoxes.get(end)); // Actions.click() doesn't scroll
new Actions(getDriver())
.keyDown(Keys.SHIFT)
.click(checkBoxes.get(end))
.keyUp(Keys.SHIFT)
.perform();
return this;
}
/**
* @param columnIdentifiers fieldKeys, names, or labels of columns
* @return grid data for the specified columns, keyed by column label
*/
public List<Map<String, String>> getGridDataByLabel(CharSequence... 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<Map<FieldKey, String>> getGridDataByFieldKey(CharSequence... 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<Map<String, String>> getGridDataByName(CharSequence... columnIdentifiers)
{
return getGridData(FieldReference::getName, columnIdentifiers);
}
/**
* @param columnIdentifiers fieldKeys, names, or labels of columns
* @return grid data for the specified columns, ordered as the provided columnIdentifiers
*/
public List<List<String>> getGridData(CharSequence... columnIdentifiers)
{
List<Map<Integer, String>> rowMaps = getGridData(FieldReference::getDomIndex, columnIdentifiers);
List<List<String>> gridData = new ArrayList<>();
for (Map<Integer, String> gridMap : rowMaps)
{
gridData.add(new ArrayList<>(gridMap.values())); // row maps remember insertion order
}
return gridData;
}
private <T> List<Map<T, String>> getGridData(Function<FieldReferenceManager.FieldReference, T> keyGenerator, CharSequence... columnIdentifiers)
{
List<Map<T, String>> gridData = new ArrayList<>();
Set<FieldReference> includedColHeaders = new LinkedHashSet<>();
if (columnIdentifiers.length == 0)
{
includedColHeaders.addAll(elementCache().findHeaders());
}
else
{
for (CharSequence columnIdentifier : columnIdentifiers)
{
includedColHeaders.add(elementCache().findColumnHeader(columnIdentifier));
}
}
for (WebElement row : elementCache().getRows())
{
List<WebElement> cells = row.findElements(By.tagName("td"));
Map<T, String> rowMap = new LinkedHashMap<>(includedColHeaders.size());
for (FieldReference fieldReference : includedColHeaders)
{
WebElement cell = cells.get(fieldReference.getDomIndex());
T key = keyGenerator.apply(fieldReference);
String value;
if (fieldReference.getDomIndex() == 0 && hasSelectColumn())
{
value = String.valueOf(Locator.tag("input").findElement(cell).isSelected());
}
else
{
value = cell.getText();
}
rowMap.put(key, value);
}
gridData.add(rowMap);
}
return gridData;
}
@Deprecated
public List<String> getColumnDataByLabel(CharSequence columnIdentifier)
{
return getColumnData(columnIdentifier);
}
/**
* @param columnIdentifier fieldKey, name, or label of column
*/
public List<String> getColumnData(CharSequence columnIdentifier)
{
return getGridData(ch -> 1, columnIdentifier).stream().map(a-> a.get(1)).collect(Collectors.toList());
}
private WebElement getRow(int index)
{
return elementCache().getRows().get(index);
}
/**
* Find the first row index containing the text value in the given column.
* If not found -1 is returned.
*
* @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(CharSequence columnIdentifier, String text)
{
int index = -1;
List<String> columnData = getColumnData(columnIdentifier);
for (int i = 0; i < columnData.size(); i++)
{
if (columnData.get(i).equals(text))
{
index = i;
break;
}
}
return index;
}
/**
* Get the td element for a cell.
*
* @param row The 0 based row index.
* @param columnIdentifier fieldKey, name, or label of column
* @return A {@link WebElement} that is the td for the cell.
*/
public WebElement getCell(int row, CharSequence columnIdentifier)
{
int columNumber = getColumnIndex(columnIdentifier) + 1;
return Locator.css("td:nth-of-type(" + columNumber + ")").findElement(getRow(row));
}
public boolean isCellReadOnly(int row, CharSequence columnIdentifier)
{
WebElement div = Locator.tag("div").findElement(getCell(row, columnIdentifier));
String cellClass = div.getDomAttribute("class");
return cellClass != null && cellClass.contains("cell-read-only");
}
public int getRowCount()
{
return elementCache().getRows().size();
}
/**
* <p>
* 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.
* </p>
*
* @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(CharSequence columnToSearch, String valueToSearch, CharSequence columnToSet, @Nullable String value, int index)
{
setCellValueForLookup(getRowIndex(columnToSearch, valueToSearch), columnToSet, value, index);
}
/**
* <p>
* For a given column, 'columnToSet', set the cell in the row if value in column 'columnToSearch'
* equals 'valueToSearch'.
* </p>
* <p>
* 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 'columnToSet' only if the value in the column 'columnToSearch' equal
* 'valueToSearch' in that row.
* </p>
* <p>
* The check for equality for 'valueToSearch' is case sensitive.
* </p>
*
* @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(CharSequence columnToSearch, String valueToSearch, CharSequence columnToSet, Object valueToSet)
{
setCellValue(getRowIndex(columnToSearch, valueToSearch), columnToSet, valueToSet);
}
public void overwriteCellValue(CharSequence columnToSearch, String valueToSearch, CharSequence columnToSet, Object valueToSet)
{
clearCellValue(getRowIndex(columnToSearch, valueToSearch), columnToSet);
setCellValue(getRowIndex(columnToSearch, valueToSearch), columnToSet, valueToSet);
}
/**
* <p>
* For the identified row set the value in the identified column.
* </p>
* <p>
* If the column to be updated is a look-up, the value passed in must be a list, even if it is just one value.
* This is needed so the function knows how to set the value.
* </p>
*
* @param row Index of the row (0 based).
* @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, CharSequence columnIdentifier, Object value)
{
return setCellValue(row, columnIdentifier, value, true, false);
}
/**
* <p>
* For the identified row set the value in the identified lookup column by selecting the given index in the lookup list.
* </p>
*
* @param row Index of the row (0 based).
* @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, CharSequence columnIdentifier, @Nullable String value, int index)
{
WebElement gridCell = selectCell(row, columnIdentifier);
ReactSelect lookupSelect = elementCache().lookupSelect(gridCell);
lookupSelect.open();
if (value != null)
lookupSelect.enterValueInTextbox(value);
List<WebElement> elements = lookupSelect.getOptionElements();
if (elements.size() < index)
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;
}
/**
* <p>
* For the identified row set the value in the identified column.
* </p>
* <p>
* If the column to be updated is a look-up, the value passed in must be a list, even if it is just one value.
* This is needed so the function knows how to set the value.
* </p>
*
* @param row Index of the row (0 based).
* @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, CharSequence columnIdentifier, Object value, boolean checkContains, boolean centerSelectedCell)
{
// Normalize date values
if (value instanceof Date date)
{
value = LocalDateTime.ofInstant(date.toInstant(), TimeZone.getDefault().toZoneId());
}
if (centerSelectedCell)
ScrollUtils.scrollIntoView(getCell(row, columnIdentifier), center, center);
WebElement gridCell = selectCell(row, columnIdentifier);
if (value instanceof List)
{
// If this is a list assume that it will need a lookup.
List<String> values = (List) value;
ReactSelect lookupSelect = elementCache().lookupSelect(gridCell);
lookupSelect.open();
for (String _value : values)
{
lookupSelect.typeOptionThenSelect(_value);
}
}
else if (value instanceof LocalDateTime localDateTime)
{
// Activate the cell.
activateCell(gridCell);
ReactDateTimePicker dateTimePicker = elementCache().datePicker();
dateTimePicker.select(localDateTime);
}
else if (value instanceof LocalDate localDate)
{
activateCell(gridCell);
ReactDateTimePicker datePicker = elementCache().datePicker();
datePicker.selectDate(localDate);
}
else if (value instanceof LocalTime localTime)
{
activateCell(gridCell);
ReactDateTimePicker datePicker = elementCache().datePicker();
datePicker.selectTime(localTime);
}
else
{
String beforeText = gridCell.getText();
activateCell(gridCell);
String str = value.toString();
WebElement inputCell = elementCache().inputCell();
// Remove the text that is there.
inputCell.clear();
// If the cell had text calling '.clear()' requires a reactivation of the cell.
if(!inputCell.isDisplayed())
{
gridCell.click();
activateCell(gridCell);
inputCell = elementCache().inputCell();
}
if(!str.isEmpty())
{
inputCell.sendKeys(str);
}
inputCell.sendKeys(Keys.RETURN); // Close the inputCell.
getWrapper().shortWait().until(ExpectedConditions.stalenessOf(inputCell));
if (checkContains)
{
// Wait until the grid cell has the updated text. Check for contains, not equal, because when updating a cell
// the cell's new value will be the old value plus the new value and the cursor may not be placed at the end
// of the existing value so the new value should exist somewhere in the cell text value not necessarily
// at the end of it.
WebDriverWrapper.waitFor(() -> gridCell.getText().contains(str),
"Value entered into inputCell '" + value + "' did not appear in grid cell.", WAIT_FOR_JAVASCRIPT);
}
else
{
// Wait until the grid cell is not the same as before.
WebDriverWrapper.waitFor(() -> !gridCell.getText().equals(beforeText),
"Value entered into inputCell '" + value + "' did not appear in grid cell.", WAIT_FOR_JAVASCRIPT);
}
}
return gridCell;
}
public void setEntityData(List<Map<String, Object> >data, List<FieldDefinition> fields)
{
for (int i = 0; i < data.size(); i++)
{
Map<String, Object> rowData = data.get(i);
for (FieldDefinition field : fields) {
Object value = rowData.get(field.getEffectiveLabel());
if (value != null)
setCellValue(i, field.getName(), value);
}
}
}
public EditableGrid setRecordValues(List<Map<String, Object>> rowValues)
{
for (int i = 0; i < rowValues.size(); i++)
{
Map<String, Object> columnValues = rowValues.get(i);
for(String fieldIdentifier : columnValues.keySet())
setCellValue(i, fieldIdentifier, columnValues.get(fieldIdentifier), true, true);
}
return this;
}
/**
* Set the value of a multi-line field for the given row & column. This uses javascript to set the value, not sendKeys.
* Use '\n' for a new line.
*
* @param row Row to update.
* @param columnIdentifier fieldKey, name, or label of column
* @param value The value to set.
*/
public void setMultiLineCellValue(int row, CharSequence columnIdentifier, String value)
{
WebElement gridCell = getCell(row, columnIdentifier);
String beforeText = gridCell.getText();
WebElement textArea = activateCellUsingDoubleClick(row, columnIdentifier);
textArea.sendKeys(value, Keys.RETURN); // Add the RETURN to close the inputCell.
waitFor(()->getWrapper().shortWait().until(ExpectedConditions.stalenessOf(textArea)),
"TextArea did not go away.", 500);
// Wait until the cell shows some kind of update before leaving.
WebDriverWrapper.waitFor(() -> !gridCell.getText().equals(beforeText),
"Doesn't look like the multi-line field was updated.", WAIT_FOR_JAVASCRIPT);
}
/**
* 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 columnIdentifier fieldKey, name, or label of column
* @return The TextArea component that can be used to edit the field.
*/
public WebElement activateCellUsingDoubleClick(int row, CharSequence columnIdentifier)
{
WebElement gridCell = getCell(row, columnIdentifier);
WebElement textArea = Locator.tag("textarea").refindWhenNeeded(gridCell);
// Account for the cell already being active.
if(!textArea.isDisplayed())
{
getWrapper().scrollIntoView(gridCell);
getWrapper().doubleClick(gridCell);
waitFor(textArea::isDisplayed,
String.format("Table cell for row %d and column '%s' was not activated.", row, columnIdentifier), 1_000);
}
return textArea;
}
/**
* 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 columnIdentifier fieldKey, name, or label of column
* @param value value to insert
*/
public void setNewSelectValue(int row, CharSequence columnIdentifier, String value)
{
WebElement gridCell = selectCell(row, columnIdentifier);
ReactSelect createSelect = elementCache().lookupSelect(gridCell);
createSelect.createValue(value);
}
/**
* Search for a row and then clear the given cell (columnToClear) on the row.
*
* @param columnToSearch Column to search.
* @param valueToSearch Value in the column to search for.
* @param columnToClear Column to clear.
*/
public void clearCellValue(CharSequence columnToSearch, String valueToSearch, CharSequence columnToClear)
{
clearCellValue(getRowIndex(columnToSearch, valueToSearch), columnToClear);
}
/**
* Clear the cell (columnIdentifier) in the row.
*
* @param row Row of the cell to clear.
* @param columnIdentifier fieldKey, name, or label of column
*/
public void clearCellValue(int row, CharSequence columnIdentifier)
{
selectCell(row, columnIdentifier);
new Actions(getDriver()).sendKeys(Keys.DELETE).perform();
}
/**
* For a given row get the value in the given column.
*
* @param row The row index (0 based).
* @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, CharSequence columnIdentifier)
{
return getCellValue(getCell(row, columnIdentifier));
}
private String getCellValue(WebElement cell)
{
return cell.getText().trim();
}
/**
* Dismiss the dropdown list that is currently displayed on the grid.
*
* @return A reference to this EditableGrid.
*/
public EditableGrid dismissDropdownList()
{
ReactSelect.finder(getDriver()).find(getComponentElement()).close();
return this;
}
/**
* For the given row get the values displayed in the dropdown list for the given column.
*
* @param row The 0 based row index.
* @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<String> getDropdownListForCell(int row, CharSequence columnIdentifier)
{
return getFilteredDropdownListForCell(row, columnIdentifier, null);
}
/**
* For the given row and column type some text into the cell to get the 'filtered' values displayed in the dropdown list.
* 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 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<String> getFilteredDropdownListForCell(int row, CharSequence columnIdentifier, @Nullable String filterText)
{
WebElement gridCell = selectCell(row, columnIdentifier);
ReactSelect lookupSelect = elementCache().lookupSelect(gridCell);
// If the click did not expand the select this will.
// This will have no effect if the list is expended.
lookupSelect.open();
if (StringUtils.isNotBlank(filterText))
{
lookupSelect.enterValueInTextbox(filterText);
}
return lookupSelect.getOptions();
}
/**
* Values will be quoted appropriately for pasting into editable grid lookups.
*/
public static String getPastableColumn(List<?> values)
{
List<String> valueList = new ArrayList<>();
for (Object value : values)
{
String strVal = CSVFormat.DEFAULT.format(value); // Just quote commas
valueList.add(strVal);
}
return String.join("\n", valueList);
}
/**
* Pastes text to a single column of the grid.
* @param columnIdentifier fieldKey, name, or label of column
* @param pasteValues list of values to paste
* @return A Reference to this editableGrid object.
*/
public EditableGrid pasteColumn(CharSequence columnIdentifier, List<?> pasteValues)
{
if (pasteValues.isEmpty())
throw new IllegalArgumentException("No paste values provided");
return pasteFromCell(0, columnIdentifier, getPastableColumn(pasteValues), 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 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, CharSequence columnIdentifier, String pasteText)
{
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 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, CharSequence columnIdentifier, String pasteText, boolean validate)
{
int initialRowCount = getRowCount();
WebElement gridCell = getCell(row, columnIdentifier);
String indexValue = gridCell.getText();
selectCell(gridCell);
getWrapper().actionPaste(null, pasteText);
// wait for the cell value to change or the rowcount to change, and the target cell to go into highlight,
// ... or for a second and a half
WebDriverWrapper.waitFor(()-> (getRowCount() > initialRowCount || !indexValue.equals(gridCell.getText())) &&
isInSelection(gridCell), 1500);
if (validate)
waitForAnyPasteContent(pasteText);
return this;
}
/**
* Awaits any elements (except for empty, or space-only) of the pasted content to be present in the grid
* @param pasteContent tab-separated text
*/
protected void waitForAnyPasteContent(String pasteContent)
{
// split pasteContent into its parts
var contentParts = pasteContent.replace("\n", "\t").split("\t");
// filter out empty and space-only values
var filteredParts = Arrays.stream(contentParts).filter(a-> !a.isEmpty() && !a.equals(" ")).collect(Collectors.toList());
await().atMost(Duration.ofSeconds(2))
.untilAsserted(()-> Assertions.assertThat(getSelectionCellTexts())
.containsAnyElementsOf(filteredParts));
}
/**
* Awaits all elements (except empty or space-only) of the pasted content to be present in the grid.
* Use this to validate all expected content appears after pasting to the grid
* @param pasteContent tab-separated text of the sort usually pasted into the edit grid
*/
public void waitForPasteContent(String pasteContent)
{
// split pasteContent into its parts
var contentParts = pasteContent.split("\\s*[\n\t]\\s*");
// filter out empty and space-only values
var filteredParts = Arrays.stream(contentParts)
.filter(a-> !a.isBlank())
.map(str -> {
if (str.startsWith("\"") && str.endsWith("\""))
{
// reverse TsvQuoter.quote
str = str.replaceAll("\"\"", "\"");
str = str.substring(1, str.length() - 1); // remove surrounding quotes
}
return str;
})
.collect(Collectors.toList());
await().atMost(Duration.ofSeconds(2))
.untilAsserted(()-> Assertions.assertThat(getSelectionCellTexts())
.containsAll(filteredParts));
}
// captures the texts of any cells currently in selection
public List<String> getSelectionCellTexts()
{
var cells = Locator.tagWithClass("div", "cellular-display")
.withAttributeContaining("class","cell-selection").findElements(this);
return getWrapper().getTexts(cells);
}
public List<WebElement> getSelectedCells()
{
return Locator.tagWithClass("div", "cell-selection").parent("td").findElements(this);
}
/**
* Pastes a single value into as many cells as are selected, or supports pasting a square shaped blob of data
* of the same shape as the prescribed selection. If a single value is supplied, that value will be put into
* every cell in the selection. If the data doesn't match the selection dimensions (e.g., has fewer or more columns)
* the grid should produce an error/alert.
* @param pasteText The text to paste
* @param startRowIndex index of the starting row
* @param startColumn fieldKey, name, or label of the starting cell
* @param endRowIndex index of the ending row
* @param endColumn fieldKey, name, or label of the ending cell
* @return the current grid instance
*/
public EditableGrid pasteMultipleCells(String pasteText, int startRowIndex, CharSequence startColumn, int endRowIndex, CharSequence endColumn)
{
WebElement startCell = getCell(startRowIndex, startColumn);
WebElement endCell = getCell(endRowIndex, endColumn);
selectCellRange(startCell, endCell);
getWrapper().actionPaste(null, pasteText);
return this;
}
/**
* Copies text from the grid, b
* @param startRowIndex Index of the top-left cell's row
* @param startColumn fieldKey, name, or label of the top-left cell
* @param endRowIndex Index of the bottom-right cell's row
* @param endColumn fieldKey, name, or label of the bottom-right cell
* @return the text contained in the prescribed selection
*/
public String copyCellRange(int startRowIndex, CharSequence startColumn, int endRowIndex, CharSequence endColumn) throws IOException, UnsupportedFlavorException
{
WebElement startCell = getCell(startRowIndex, startColumn);
WebElement endCell = getCell(endRowIndex, endColumn);
selectCellRange(startCell, endCell);
return copyCurrentSelection();
}
/**
* Selects all cells in the table, then copies their contents into delimited text
* @return delimited text content of the cells in the grid
*/
public String copyAllCells() throws IOException, UnsupportedFlavorException
{
selectAllCells();
WebDriverWrapper.waitFor(this::areAllInSelection,
"expect all cells to be selected before copying grid values", 1500);
String selection = copyCurrentSelection();
if (selection.isEmpty())
{
log("initial attempt to copy current selection came up empty. re-trying after 3000 ms");
new WebDriverWait(getDriver(), Duration.ofSeconds(3));
return copyCurrentSelection();
}
return selection;
}
/**
* Selects all cells in the table, then deletes their content
*/
public void clearAllCells()
{
selectAllCells();
new Actions(getDriver()).sendKeys(Keys.DELETE).perform();
}
public String copyCurrentSelection() throws IOException, UnsupportedFlavorException
{
// now copy the contents of the current selection to the clipboard
Keys cmdKey = MODIFIER_KEY;
Actions actions = new Actions(getDriver());
actions.keyDown(cmdKey)
.sendKeys( "c")
.keyUp(cmdKey)
.build()
.perform();
return getWrapper().getClipboardContent();
}
public void dragFill(WebElement startCell, WebElement endCell)
{
dismissPopover();
Locator.XPathLocator selectionHandleLoc = Locator.byClass("cell-selection-handle");
WebElement selectionHandle = selectionHandleLoc.findElement(startCell);
dragToCell(selectionHandle, endCell);
selectionHandleLoc.waitForElement(endCell, 5_000);
}
public void selectCellRange(WebElement startCell, WebElement endCell)
{
dragToCell(startCell, endCell);
WebDriverWrapper.waitFor(()-> isInSelection(startCell) && isInSelection(endCell),
"Cell range did not become selected", 2000);
}
private void dragToCell(WebElement elementToDrag, WebElement destinationCell)