@xylentis/testgen 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +189 -14
- package/dist/checks.js +100 -0
- package/dist/cli.js +93 -22
- package/dist/editing.js +167 -0
- package/dist/generate.js +25 -6
- package/dist/generators/index.js +2 -2
- package/dist/generators/javascript-body.js +91 -13
- package/dist/generators/playwright-go.js +85 -21
- package/dist/generators/playwright-ts.js +44 -7
- package/dist/generators/selenium-java.js +201 -109
- package/dist/generators/selenium-js.js +39 -10
- package/dist/generators/test-data.js +171 -0
- package/dist/locator.js +17 -0
- package/dist/recording.js +60 -2
- package/dist/replay.js +268 -0
- package/dist/secrets.js +169 -0
- package/dist/text.js +28 -0
- package/dist/ui/server.js +220 -21
- package/dist/vendor/playwright/locatorParser.js +235 -0
- package/dist/vendor/playwright/locatorUtils.js +67 -0
- package/docs/testgen-ui.png +0 -0
- package/package.json +4 -2
- package/runtime/playwright-go/testgen_data_test.go +64 -0
- package/runtime/playwright-ts/testgen-data.ts +70 -0
- package/runtime/selenium-java/TestgenSupport.java +213 -29
- package/runtime/selenium-js/testgen-support.js +162 -22
- package/ui/app.js +392 -25
- package/ui/index.html +21 -4
- package/ui/style.css +81 -9
|
@@ -4,16 +4,25 @@
|
|
|
4
4
|
// permission: as part of tests generated by testgen, this file may be used, modified and distributed under
|
|
5
5
|
// terms of your choice (see NOTICE in @xylentis/testgen).
|
|
6
6
|
|
|
7
|
+
import java.io.IOException;
|
|
8
|
+
import java.io.InputStream;
|
|
9
|
+
import java.io.UncheckedIOException;
|
|
7
10
|
import java.net.MalformedURLException;
|
|
8
11
|
import java.net.URI;
|
|
12
|
+
import java.nio.charset.StandardCharsets;
|
|
9
13
|
import java.nio.file.Path;
|
|
10
14
|
import java.time.Duration;
|
|
15
|
+
import java.time.LocalTime;
|
|
16
|
+
import java.time.format.DateTimeFormatter;
|
|
11
17
|
import java.util.ArrayList;
|
|
18
|
+
import java.util.Arrays;
|
|
12
19
|
import java.util.LinkedHashMap;
|
|
13
20
|
import java.util.List;
|
|
14
21
|
import java.util.Map;
|
|
15
22
|
import java.util.Set;
|
|
16
23
|
import java.util.TreeMap;
|
|
24
|
+
import java.util.concurrent.ConcurrentHashMap;
|
|
25
|
+
import java.util.concurrent.atomic.AtomicInteger;
|
|
17
26
|
import java.util.function.Consumer;
|
|
18
27
|
import java.util.function.Function;
|
|
19
28
|
import java.util.function.Predicate;
|
|
@@ -58,7 +67,8 @@ import org.openqa.selenium.support.ui.WebDriverWait;
|
|
|
58
67
|
* ({@link #ENGINE}), so getByRole/getByText/getByLabel find the elements the Playwright recorder pointed at.
|
|
59
68
|
*
|
|
60
69
|
* <p>Environment variables: HEADED=1 shows the browser, SELENIUM_REMOTE_URL runs on a Selenium Grid,
|
|
61
|
-
* TESTGEN_TIMEOUT sets the auto-wait timeout in milliseconds (default 10000).
|
|
70
|
+
* TESTGEN_TIMEOUT sets the auto-wait timeout in milliseconds (default 10000). {@link #secret(String)} reads
|
|
71
|
+
* a value typed into a password field, e.g. TEST_PASSWORD.
|
|
62
72
|
*/
|
|
63
73
|
public final class TestgenSupport {
|
|
64
74
|
private TestgenSupport() {}
|
|
@@ -71,6 +81,32 @@ public final class TestgenSupport {
|
|
|
71
81
|
return value == null || value.isEmpty() ? fallback : value;
|
|
72
82
|
}
|
|
73
83
|
|
|
84
|
+
// Values returned by secret(), as typed and as written in logs: logs and errors show them as ***.
|
|
85
|
+
private static final Set<String> SECRETS = ConcurrentHashMap.newKeySet();
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Value of the environment variable (or system property) {@code name}, for a value typed into a password
|
|
89
|
+
* field: the value typed while recording is not kept in the test. Logs and assertion messages show it as ***.
|
|
90
|
+
*/
|
|
91
|
+
public static String secret(String name) {
|
|
92
|
+
String value = System.getProperty(name, System.getenv(name));
|
|
93
|
+
if (value == null)
|
|
94
|
+
throw new IllegalStateException("testgen: set the environment variable " + name + ", the value to type into a password field");
|
|
95
|
+
if (!value.isEmpty()) {
|
|
96
|
+
SECRETS.add(value);
|
|
97
|
+
String quoted = quote(value);
|
|
98
|
+
SECRETS.add(quoted.substring(1, quoted.length() - 1));
|
|
99
|
+
}
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
static String mask(String text) {
|
|
104
|
+
String result = text;
|
|
105
|
+
for (String secret : SECRETS.stream().sorted((a, b) -> b.length() - a.length()).toList())
|
|
106
|
+
result = result.replace(secret, "***");
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
|
|
74
110
|
/** Starts "chrome", "edge", "firefox" or "safari", locally or on SELENIUM_REMOTE_URL; headless unless HEADED is set. */
|
|
75
111
|
public static WebDriver createDriver(String browser, String... arguments) {
|
|
76
112
|
boolean headed = !env("HEADED", "").isEmpty();
|
|
@@ -412,40 +448,48 @@ public final class TestgenSupport {
|
|
|
412
448
|
reason = firstLine(e.getMessage());
|
|
413
449
|
}
|
|
414
450
|
if (System.nanoTime() > deadline)
|
|
415
|
-
throw new TimeoutException(name + ": timeout " + page.timeout.toMillis() + "ms exceeded waiting for " + this + " (" + reason + ")");
|
|
451
|
+
throw new TimeoutException(mask(name + ": timeout " + page.timeout.toMillis() + "ms exceeded waiting for " + this + " (" + reason + ")"));
|
|
416
452
|
sleep(100);
|
|
417
453
|
}
|
|
418
454
|
}
|
|
419
455
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
456
|
+
// Runs `action` on the first matching element once it is ready; `arguments` describe the call in the log.
|
|
457
|
+
private void act(String name, String arguments, Consumer<WebElement> action, boolean visible, boolean enabled) {
|
|
458
|
+
String call = this + "." + name + "(" + arguments + ")";
|
|
459
|
+
page().log(call);
|
|
460
|
+
try {
|
|
461
|
+
query(name, element -> {
|
|
462
|
+
action.accept(element);
|
|
463
|
+
return null;
|
|
464
|
+
}, visible, enabled);
|
|
465
|
+
} catch (RuntimeException e) {
|
|
466
|
+
page().log(call + " FAILED: " + firstLine(e.getMessage()));
|
|
467
|
+
throw e;
|
|
468
|
+
}
|
|
425
469
|
}
|
|
426
470
|
|
|
427
471
|
public void click() {
|
|
428
|
-
act("click", WebElement::click, true, true);
|
|
472
|
+
act("click", "", WebElement::click, true, true);
|
|
429
473
|
}
|
|
430
474
|
|
|
431
475
|
public void click(ClickOptions options) {
|
|
432
|
-
act("click", element -> options.perform(page().driver, element, true), true, true);
|
|
476
|
+
act("click", options.toString(), element -> options.perform(page().driver, element, true), true, true);
|
|
433
477
|
}
|
|
434
478
|
|
|
435
479
|
public void dblclick() {
|
|
436
|
-
act("dblclick", element -> new Actions(page().driver).doubleClick(element).perform(), true, true);
|
|
480
|
+
act("dblclick", "", element -> new Actions(page().driver).doubleClick(element).perform(), true, true);
|
|
437
481
|
}
|
|
438
482
|
|
|
439
483
|
public void hover() {
|
|
440
|
-
act("hover", element -> new Actions(page().driver).moveToElement(element).perform(), true, false);
|
|
484
|
+
act("hover", "", element -> new Actions(page().driver).moveToElement(element).perform(), true, false);
|
|
441
485
|
}
|
|
442
486
|
|
|
443
487
|
public void hover(ClickOptions options) {
|
|
444
|
-
act("hover", element -> options.perform(page().driver, element, false), true, false);
|
|
488
|
+
act("hover", options.toString(), element -> options.perform(page().driver, element, false), true, false);
|
|
445
489
|
}
|
|
446
490
|
|
|
447
491
|
public void fill(String value) {
|
|
448
|
-
act("fill", element -> {
|
|
492
|
+
act("fill", quote(value), element -> {
|
|
449
493
|
element.clear();
|
|
450
494
|
if (!value.isEmpty())
|
|
451
495
|
element.sendKeys(value);
|
|
@@ -453,7 +497,7 @@ public final class TestgenSupport {
|
|
|
453
497
|
}
|
|
454
498
|
|
|
455
499
|
public void press(String key) {
|
|
456
|
-
act("press", element -> element.sendKeys(keys(key)), true, false);
|
|
500
|
+
act("press", quote(key), element -> element.sendKeys(keys(key)), true, false);
|
|
457
501
|
}
|
|
458
502
|
|
|
459
503
|
public void check() {
|
|
@@ -465,14 +509,14 @@ public final class TestgenSupport {
|
|
|
465
509
|
}
|
|
466
510
|
|
|
467
511
|
private void setChecked(boolean checked) {
|
|
468
|
-
act(checked ? "check" : "uncheck", element -> {
|
|
512
|
+
act(checked ? "check" : "uncheck", "", element -> {
|
|
469
513
|
if (isChecked(element) != checked)
|
|
470
514
|
element.click();
|
|
471
515
|
}, true, true);
|
|
472
516
|
}
|
|
473
517
|
|
|
474
518
|
public void selectOption(String... values) {
|
|
475
|
-
act("selectOption", element -> {
|
|
519
|
+
act("selectOption", String.join(", ", Arrays.stream(values).map(TestgenSupport::quote).toList()), element -> {
|
|
476
520
|
Select select = new Select(element);
|
|
477
521
|
if (select.isMultiple())
|
|
478
522
|
select.deselectAll();
|
|
@@ -490,7 +534,8 @@ public final class TestgenSupport {
|
|
|
490
534
|
List<String> paths = new ArrayList<>();
|
|
491
535
|
for (String file : files)
|
|
492
536
|
paths.add(Path.of(file).toAbsolutePath().toString());
|
|
493
|
-
act("setInputFiles",
|
|
537
|
+
act("setInputFiles", String.join(", ", Arrays.stream(files).map(TestgenSupport::quote).toList()),
|
|
538
|
+
element -> element.sendKeys(String.join("\n", paths)), false, false);
|
|
494
539
|
}
|
|
495
540
|
|
|
496
541
|
public void shouldBeVisible() {
|
|
@@ -524,23 +569,30 @@ public final class TestgenSupport {
|
|
|
524
569
|
// Polls until `check` passes for the first matching element.
|
|
525
570
|
private void expect(String name, String expected, Predicate<WebElement> check, Function<WebElement, String> actual) {
|
|
526
571
|
Page page = page();
|
|
572
|
+
String call = this + "." + name + "(" + (expected == null ? "" : quote(expected)) + ")";
|
|
527
573
|
long deadline = System.nanoTime() + page.timeout.toNanos();
|
|
528
574
|
String received = "<element not found>";
|
|
529
575
|
while (true) {
|
|
530
576
|
try {
|
|
531
577
|
List<WebElement> found = elements();
|
|
532
578
|
if (!found.isEmpty()) {
|
|
533
|
-
if (check.test(found.get(0)))
|
|
579
|
+
if (check.test(found.get(0))) {
|
|
580
|
+
page.log(call + " passed");
|
|
534
581
|
return;
|
|
582
|
+
}
|
|
535
583
|
received = actual.apply(found.get(0));
|
|
536
584
|
}
|
|
537
585
|
} catch (WebDriverException e) {
|
|
538
|
-
if (!isRetryable(e))
|
|
586
|
+
if (!isRetryable(e)) {
|
|
587
|
+
page.log(call + " FAILED: " + firstLine(e.getMessage()));
|
|
539
588
|
throw e;
|
|
589
|
+
}
|
|
540
590
|
received = firstLine(e.getMessage());
|
|
541
591
|
}
|
|
542
|
-
if (System.nanoTime() > deadline)
|
|
543
|
-
|
|
592
|
+
if (System.nanoTime() > deadline) {
|
|
593
|
+
page.log(call + " FAILED, received: " + received);
|
|
594
|
+
throw new AssertionError(mask(call + " failed after " + page.timeout.toMillis() + "ms\nReceived: " + received));
|
|
595
|
+
}
|
|
544
596
|
sleep(100);
|
|
545
597
|
}
|
|
546
598
|
}
|
|
@@ -548,20 +600,26 @@ public final class TestgenSupport {
|
|
|
548
600
|
|
|
549
601
|
/** One browser window or tab, like Playwright's Page; actions switch WebDriver to it automatically. */
|
|
550
602
|
public static final class Page extends Locator {
|
|
603
|
+
private static final DateTimeFormatter LOG_TIME = DateTimeFormatter.ofPattern("HH:mm:ss.SSS");
|
|
604
|
+
|
|
551
605
|
final WebDriver driver;
|
|
552
606
|
final String handle;
|
|
553
607
|
String testIdAttribute = "data-testid";
|
|
554
608
|
Duration timeout = DEFAULT_TIMEOUT;
|
|
609
|
+
private Consumer<String> logger;
|
|
610
|
+
// Pages opened later are named page1, page2, ... like the variables of the generated test.
|
|
611
|
+
private final AtomicInteger opened;
|
|
555
612
|
private final List<Consumer<Dialog>> dialogHandlers = new ArrayList<>();
|
|
556
613
|
|
|
557
614
|
public Page(WebDriver driver) {
|
|
558
|
-
this(driver, driver.getWindowHandle());
|
|
615
|
+
this(driver, driver.getWindowHandle(), "page", new AtomicInteger());
|
|
559
616
|
}
|
|
560
617
|
|
|
561
|
-
private Page(WebDriver driver, String handle) {
|
|
562
|
-
super(null, List.of(),
|
|
618
|
+
private Page(WebDriver driver, String handle, String name, AtomicInteger opened) {
|
|
619
|
+
super(null, List.of(), name);
|
|
563
620
|
this.driver = driver;
|
|
564
621
|
this.handle = handle;
|
|
622
|
+
this.opened = opened;
|
|
565
623
|
}
|
|
566
624
|
|
|
567
625
|
public Page setTestIdAttribute(String attribute) {
|
|
@@ -569,17 +627,34 @@ public final class TestgenSupport {
|
|
|
569
627
|
return this;
|
|
570
628
|
}
|
|
571
629
|
|
|
630
|
+
/**
|
|
631
|
+
* Receives a line for every action, with its arguments, and for the result of every assertion, e.g.
|
|
632
|
+
* {@code page.setLogger(System.out::println)} or {@code page.setLogger(message -> Reporter.log(message, true))}
|
|
633
|
+
* with TestNG. Pages opened later log to it as well.
|
|
634
|
+
*/
|
|
635
|
+
public Page setLogger(Consumer<String> logger) {
|
|
636
|
+
this.logger = logger;
|
|
637
|
+
return this;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
void log(String message) {
|
|
641
|
+
if (logger != null)
|
|
642
|
+
logger.accept(LocalTime.now().format(LOG_TIME) + " " + mask(message));
|
|
643
|
+
}
|
|
644
|
+
|
|
572
645
|
public Page setDefaultTimeout(Duration timeout) {
|
|
573
646
|
this.timeout = timeout;
|
|
574
647
|
return this;
|
|
575
648
|
}
|
|
576
649
|
|
|
577
650
|
public void navigate(String url) {
|
|
651
|
+
log(this + ".navigate(" + quote(url) + ")");
|
|
578
652
|
activate();
|
|
579
653
|
driver.get(url);
|
|
580
654
|
}
|
|
581
655
|
|
|
582
656
|
public void close() {
|
|
657
|
+
log(this + ".close()");
|
|
583
658
|
activate();
|
|
584
659
|
driver.close();
|
|
585
660
|
}
|
|
@@ -587,7 +662,9 @@ public final class TestgenSupport {
|
|
|
587
662
|
/** Opens a new tab. */
|
|
588
663
|
public Page newPage() {
|
|
589
664
|
driver.switchTo().newWindow(WindowType.TAB);
|
|
590
|
-
|
|
665
|
+
Page page = sibling(driver.getWindowHandle());
|
|
666
|
+
log(page + " = " + this + ".newPage()");
|
|
667
|
+
return page;
|
|
591
668
|
}
|
|
592
669
|
|
|
593
670
|
/** Runs `action` and returns the window it opens. */
|
|
@@ -598,8 +675,11 @@ public final class TestgenSupport {
|
|
|
598
675
|
long deadline = System.nanoTime() + timeout.toNanos();
|
|
599
676
|
while (true) {
|
|
600
677
|
for (String candidate : driver.getWindowHandles()) {
|
|
601
|
-
if (!before.contains(candidate))
|
|
602
|
-
|
|
678
|
+
if (!before.contains(candidate)) {
|
|
679
|
+
Page popup = sibling(candidate);
|
|
680
|
+
log(popup + " opened as a popup of " + this);
|
|
681
|
+
return popup;
|
|
682
|
+
}
|
|
603
683
|
}
|
|
604
684
|
if (System.nanoTime() > deadline)
|
|
605
685
|
throw new TimeoutException("waitForPopup: timeout " + timeout.toMillis() + "ms exceeded");
|
|
@@ -621,7 +701,9 @@ public final class TestgenSupport {
|
|
|
621
701
|
} catch (TimeoutException e) {
|
|
622
702
|
return;
|
|
623
703
|
}
|
|
624
|
-
|
|
704
|
+
Dialog dialog = new Dialog(alert);
|
|
705
|
+
log(this + " dialog " + quote(dialog.message()));
|
|
706
|
+
dialogHandlers.remove(0).accept(dialog);
|
|
625
707
|
}
|
|
626
708
|
|
|
627
709
|
void activate() {
|
|
@@ -646,9 +728,10 @@ public final class TestgenSupport {
|
|
|
646
728
|
}
|
|
647
729
|
|
|
648
730
|
private Page sibling(String handle) {
|
|
649
|
-
Page page = new Page(driver, handle);
|
|
731
|
+
Page page = new Page(driver, handle, "page" + opened.incrementAndGet(), opened);
|
|
650
732
|
page.testIdAttribute = testIdAttribute;
|
|
651
733
|
page.timeout = timeout;
|
|
734
|
+
page.logger = logger;
|
|
652
735
|
return page;
|
|
653
736
|
}
|
|
654
737
|
}
|
|
@@ -709,6 +792,20 @@ public final class TestgenSupport {
|
|
|
709
792
|
return this;
|
|
710
793
|
}
|
|
711
794
|
|
|
795
|
+
@Override
|
|
796
|
+
public String toString() {
|
|
797
|
+
StringBuilder result = new StringBuilder("new ClickOptions()");
|
|
798
|
+
if (!"left".equals(button))
|
|
799
|
+
result.append(".setButton(").append(quote(button)).append(")");
|
|
800
|
+
if (modifiers.length > 0)
|
|
801
|
+
result.append(".setModifiers(").append(String.join(", ", Arrays.stream(modifiers).map(TestgenSupport::quote).toList())).append(")");
|
|
802
|
+
if (clickCount != 1)
|
|
803
|
+
result.append(".setClickCount(").append(clickCount).append(")");
|
|
804
|
+
if (x != null)
|
|
805
|
+
result.append(".setPosition(").append(x).append(", ").append(y).append(")");
|
|
806
|
+
return result.toString();
|
|
807
|
+
}
|
|
808
|
+
|
|
712
809
|
void perform(WebDriver driver, WebElement element, boolean click) {
|
|
713
810
|
if ("middle".equals(button))
|
|
714
811
|
throw new UnsupportedOperationException("testgen: middle click is not supported on Selenium");
|
|
@@ -735,6 +832,93 @@ public final class TestgenSupport {
|
|
|
735
832
|
}
|
|
736
833
|
}
|
|
737
834
|
|
|
835
|
+
/**
|
|
836
|
+
* Rows of the CSV data file of a data-driven test, as test arguments in the order of {@code columns}.
|
|
837
|
+
* The file is a classpath resource next to {@code testClass} (Maven: src/test/resources/<package>/).
|
|
838
|
+
* It is UTF-8, with or without a BOM, separated by commas or semicolons (as Excel saves it in some
|
|
839
|
+
* languages), and its first line names the columns; other columns are ignored, blank lines skipped.
|
|
840
|
+
*/
|
|
841
|
+
public static Object[][] readCsv(Class<?> testClass, String resource, String... columns) {
|
|
842
|
+
String text;
|
|
843
|
+
try (InputStream input = testClass.getResourceAsStream(resource)) {
|
|
844
|
+
if (input == null) {
|
|
845
|
+
String folder = testClass.getPackageName().replace('.', '/');
|
|
846
|
+
throw new IllegalStateException("testgen: data file " + resource + " not found next to " + testClass.getName()
|
|
847
|
+
+ " on the classpath (Maven: src/test/resources/" + (folder.isEmpty() ? "" : folder + "/") + resource + ")");
|
|
848
|
+
}
|
|
849
|
+
text = new String(input.readAllBytes(), StandardCharsets.UTF_8);
|
|
850
|
+
} catch (IOException e) {
|
|
851
|
+
throw new UncheckedIOException(e);
|
|
852
|
+
}
|
|
853
|
+
if (text.startsWith("\uFEFF"))
|
|
854
|
+
text = text.substring(1);
|
|
855
|
+
String firstLine = text.split("\r?\n", 2)[0];
|
|
856
|
+
char separator = firstLine.indexOf(',') < 0 && firstLine.indexOf(';') >= 0 ? ';' : ',';
|
|
857
|
+
List<List<String>> rows = parseCsv(text, separator);
|
|
858
|
+
if (rows.isEmpty())
|
|
859
|
+
throw new IllegalStateException("testgen: data file " + resource + " is empty");
|
|
860
|
+
|
|
861
|
+
List<String> header = rows.get(0).stream().map(String::trim).toList();
|
|
862
|
+
int[] indexes = new int[columns.length];
|
|
863
|
+
for (int i = 0; i < columns.length; i++) {
|
|
864
|
+
indexes[i] = header.indexOf(columns[i]);
|
|
865
|
+
if (indexes[i] < 0)
|
|
866
|
+
throw new IllegalStateException("testgen: data file " + resource + " has no column \"" + columns[i] + "\" (it has " + String.join(", ", header) + ")");
|
|
867
|
+
}
|
|
868
|
+
List<Object[]> result = new ArrayList<>();
|
|
869
|
+
for (List<String> row : rows.subList(1, rows.size())) {
|
|
870
|
+
if (row.stream().allMatch(String::isBlank))
|
|
871
|
+
continue;
|
|
872
|
+
Object[] arguments = new Object[columns.length];
|
|
873
|
+
for (int i = 0; i < columns.length; i++)
|
|
874
|
+
arguments[i] = indexes[i] < row.size() ? row.get(indexes[i]) : "";
|
|
875
|
+
result.add(arguments);
|
|
876
|
+
}
|
|
877
|
+
if (result.isEmpty())
|
|
878
|
+
throw new IllegalStateException("testgen: data file " + resource + " has no data rows");
|
|
879
|
+
return result.toArray(new Object[0][]);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// RFC 4180: quoted fields may contain separators, line breaks and doubled quotes.
|
|
883
|
+
static List<List<String>> parseCsv(String text, char separator) {
|
|
884
|
+
List<List<String>> rows = new ArrayList<>();
|
|
885
|
+
List<String> row = new ArrayList<>();
|
|
886
|
+
StringBuilder field = new StringBuilder();
|
|
887
|
+
boolean quoted = false;
|
|
888
|
+
for (int i = 0; i < text.length(); i++) {
|
|
889
|
+
char c = text.charAt(i);
|
|
890
|
+
if (quoted) {
|
|
891
|
+
if (c == '"' && i + 1 < text.length() && text.charAt(i + 1) == '"') {
|
|
892
|
+
field.append('"');
|
|
893
|
+
i++;
|
|
894
|
+
} else if (c == '"') {
|
|
895
|
+
quoted = false;
|
|
896
|
+
} else {
|
|
897
|
+
field.append(c);
|
|
898
|
+
}
|
|
899
|
+
} else if (c == '"') {
|
|
900
|
+
quoted = true;
|
|
901
|
+
} else if (c == separator) {
|
|
902
|
+
row.add(field.toString());
|
|
903
|
+
field.setLength(0);
|
|
904
|
+
} else if (c == '\n' || c == '\r') {
|
|
905
|
+
if (c == '\r' && i + 1 < text.length() && text.charAt(i + 1) == '\n')
|
|
906
|
+
i++;
|
|
907
|
+
row.add(field.toString());
|
|
908
|
+
field.setLength(0);
|
|
909
|
+
rows.add(row);
|
|
910
|
+
row = new ArrayList<>();
|
|
911
|
+
} else {
|
|
912
|
+
field.append(c);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
if (field.length() > 0 || !row.isEmpty()) {
|
|
916
|
+
row.add(field.toString());
|
|
917
|
+
rows.add(row);
|
|
918
|
+
}
|
|
919
|
+
return rows;
|
|
920
|
+
}
|
|
921
|
+
|
|
738
922
|
static boolean isChecked(WebElement element) {
|
|
739
923
|
String type = String.valueOf(element.getDomAttribute("type")).toLowerCase();
|
|
740
924
|
if ("input".equalsIgnoreCase(element.getTagName()) && (type.equals("checkbox") || type.equals("radio")))
|