diff --git a/README.md b/README.md
index c242851..ad5be6f 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,16 @@
# TabDriver
* Wrapper for `selenium` to make it easier to use.
-* Must use `Chrome` (not `Chromium`).
\ No newline at end of file
+* Must use `Chrome` (not `Chromium`).
+* Must supply `.properties` file with.
+
+
+ chrome.driver.path=
+ headless=false
+ download.dir=
+
+* Usage:
+
+
+ var confPath = Path.of("/foo/tabdriver.properties");
+ var td = TabDriverBuilder.build(confPath);
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 0ad6641..0654df6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,26 +1,28 @@
-
4.0.0
com.olexyn
tabdriver
- 1.1
+ 1.2
jar
- tabriver
+ tabdriver
UTF-8
17
+ 3.141.59
+ 0.9.7376
-
com.olexyn
zeebom
- 1.0-SNAPSHOT
+ 1.1
pom
import
@@ -37,43 +39,43 @@
org.seleniumhq.selenium
selenium-api
- 3.141.59
+ ${xx.selenium.version}
org.seleniumhq.webdriver
webdriver-selenium
- 0.9.7376
+ ${xx.webdriver.version}
org.seleniumhq.selenium
selenium-chrome-driver
- 3.141.59
+ ${xx.selenium.version}
org.seleniumhq.webdriver
webdriver-common
- 0.9.7376
+ ${xx.webdriver.version}
org.seleniumhq.webdriver
webdriver-support
- 0.9.7376
-
-
- org.projectlombok
- lombok
+ ${xx.webdriver.version}
+
org.seleniumhq.selenium
selenium-java
- 3.141.59
+ ${xx.selenium.version}
+
+
+ org.projectlombok
+ lombok
com.olexyn
propconf
- 1.0-SNAPSHOT
- compile
+ 1.1
@@ -101,5 +103,4 @@
-
diff --git a/src/main/java/com/olexyn/tabdriver/Constants.java b/src/main/java/com/olexyn/tabdriver/Constants.java
index 9b01dbd..59a94b1 100644
--- a/src/main/java/com/olexyn/tabdriver/Constants.java
+++ b/src/main/java/com/olexyn/tabdriver/Constants.java
@@ -2,6 +2,5 @@ package com.olexyn.tabdriver;
public interface Constants {
- String WORKING_DIR = "user.dir";
String EMPTY = "";
}
diff --git a/src/main/java/com/olexyn/tabdriver/TabDriver.java b/src/main/java/com/olexyn/tabdriver/TabDriver.java
new file mode 100644
index 0000000..b585f9e
--- /dev/null
+++ b/src/main/java/com/olexyn/tabdriver/TabDriver.java
@@ -0,0 +1,283 @@
+package com.olexyn.tabdriver;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import com.olexyn.min.log.LogU;
+import org.openqa.selenium.By;
+import org.openqa.selenium.Capabilities;
+import org.openqa.selenium.JavascriptExecutor;
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.NoSuchFrameException;
+import org.openqa.selenium.SearchContext;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.chrome.ChromeDriver;
+import org.openqa.selenium.chrome.ChromeDriverService;
+
+public class TabDriver extends ChromeDriver implements JavascriptExecutor {
+
+ private final Map TABS = new HashMap<>();
+
+ public TabDriver(ChromeDriverService service, Capabilities capabilities) {
+ super(service, capabilities);
+ manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
+ }
+
+ public synchronized void registerCurrentTab(String purpose) {
+ Tab tab = new Tab(getWindowHandle());
+ tab.setName(getTitle());
+ tab.setUrl(getCurrentUrl());
+ tab.setPurpose(purpose);
+ TABS.put(tab.getHandle(), tab);
+ }
+
+ public synchronized Tab getCurrentTab() {
+ return TABS.get(getWindowHandle());
+ }
+
+ public synchronized List getTabByPurpose(String purpose) {
+ return TABS.values().stream()
+ .filter(x -> x.getPurpose() == purpose)
+ .collect(Collectors.toList());
+ }
+
+ public synchronized String registerBlankTab(String purpose) {
+ Set openTabHandles = getWindowHandles();
+ for (String openTabHandle : openTabHandles) {
+ if (!TABS.containsKey(openTabHandle)) {
+ Tab blankTab = new Tab(openTabHandle);
+ blankTab.setName("about:blank");
+ blankTab.setUrl("about:blank");
+ blankTab.setPurpose(purpose);
+ TABS.put(openTabHandle, blankTab);
+ return openTabHandle;
+ }
+ }
+ LogU.warnPlain("Not unregistered tab found.");
+ return null;
+ }
+
+ public synchronized String registerExistingTab(String purpose) {
+ String handle = getWindowHandle();
+ Tab tab = new Tab(handle);
+ tab.setName(getTitle());
+ tab.setUrl(getCurrentUrl());
+ tab.setPurpose(purpose);
+ TABS.put(handle, tab);
+ return handle;
+ }
+
+ public synchronized void switchToTab(String handle) {
+ for (Entry entry : TABS.entrySet()) {
+ String tabHandle = entry.getKey();
+ if (tabHandle.equals(handle)) {
+ switchTo().window(tabHandle);
+ }
+ }
+ }
+
+ public synchronized void switchToTab(Tab tab) {
+ switchTo().window(tab.getHandle());
+ }
+
+ /**
+ * Opens a new tab, and "moves" the WebDriver to the new tab.
+ * If the current tab is empty, it is registered - this happens usually only with the initial tab of the session.
+ */
+ public synchronized void newTab(String purpose) {
+ String currentUrl = getCurrentUrl();
+ if (currentUrl.isEmpty()
+ || currentUrl.equals("data:,")
+ || currentUrl.equals("about:blank")) {
+ registerExistingTab(purpose);
+ } else {
+ executeScript("window.open(arguments[0])");
+ switchToTab(registerBlankTab(purpose));
+ }
+ }
+
+ public synchronized void switchToTabByPurpose(String purpose) {
+ List existingTabs = getTabByPurpose(purpose);
+ if (!existingTabs.isEmpty()) {
+ switchToTab(existingTabs.get(0));
+ } else {
+ Tab currentTab = getCurrentTab();
+
+ }
+ }
+
+ public synchronized void refresh() {
+ navigate().refresh();
+ }
+
+ @Override
+ public synchronized void get(String url) {
+ super.get(url);
+ }
+
+ @Override
+ public synchronized WebElement findElement(By by) {
+ return super.findElement(by);
+ }
+
+ public synchronized void executeScript(String script) {
+ ((JavascriptExecutor) this).executeScript(script);
+ }
+
+ public synchronized void sendDeleteKeys(WebElement element, int n) {
+ for (int i = 0; i < n; i++) {
+ element.sendKeys(Keys.BACK_SPACE);
+ }
+ }
+
+ public synchronized WebElement filterElementListBy(List list, CRITERIA criteria, String text) {
+ for (WebElement element : list) {
+ String toEvaluate = switch (criteria) {
+ case CLASS -> element.getClass().getName();
+ case TEXT -> element.getText();
+ case TAG -> element.getTagName();
+ case HREF -> element.getAttribute("href");
+ case ID -> element.getAttribute("id");
+ case TITLE -> element.getAttribute("title");
+ case NONE -> text;
+ };
+ if (toEvaluate != null && toEvaluate.contains(text)) { return element; }
+ }
+ return null;
+ }
+
+ private static final String FRAME_ID_DEFAULT_CONTENT = "defaultContent";
+ private static final String FRAME_ID_NONE_FOUND = "noneFound";
+
+ /**
+ * Collects all frames accessible to WebDriver.
+ */
+ public synchronized Map collectAllFrames() throws NoSuchFrameException {
+ Map mapOfCollectedSources = new HashMap<>();
+ switchTo().defaultContent();
+ mapOfCollectedSources.put(FRAME_ID_DEFAULT_CONTENT, getPageSource());
+ for (int i = 0; i < 10; i++) {
+ try {
+ switchTo().defaultContent();
+ switchTo().frame(i);
+ mapOfCollectedSources.put(String.valueOf(i), getPageSource());
+ } catch (NoSuchFrameException e) {
+ return mapOfCollectedSources;
+ }
+ }
+ return mapOfCollectedSources;
+ }
+
+ public synchronized String findFrameContainingCharSeq(Map mapOfCollectedSources, String string) {
+ for (Entry entry : mapOfCollectedSources.entrySet()) {
+ if (entry.getValue().contains(string)) {
+ return entry.getKey();
+ }
+ }
+ return FRAME_ID_NONE_FOUND;
+ }
+
+// @Override
+// public boolean isJavascriptEnabled() {
+// return false;
+// }
+
+ public enum CRITERIA {
+ CLASS,
+ TEXT,
+ TAG,
+ HREF,
+ NONE,
+ ID,
+ TITLE
+ }
+
+ public synchronized void switchToFrameContainingCharSeq(String charSeq) {
+ switchTo().defaultContent();
+ sleep(500);
+ final String frameId = findFrameContainingCharSeq(collectAllFrames(), charSeq);
+ sleep(400);
+ switch (frameId) {
+ case FRAME_ID_DEFAULT_CONTENT:
+ switchTo().defaultContent();
+ break;
+ case FRAME_ID_NONE_FOUND:
+ break;
+ default:
+ switchTo().frame(Integer.parseInt(frameId));
+ }
+ }
+
+ public static void sleep(long milli) {
+ try {
+ Thread.sleep(milli);
+ } catch (InterruptedException e) {
+ LogU.warnPlain("SLEEP was INTERRUPED.");
+ }
+ }
+
+ /**
+ * Return the first occurrence of specified class that has specified label.
+ */
+ public synchronized WebElement getWhere(String className, CRITERIA criteria, String text) {
+ switchToFrameContainingCharSeq(text);
+ List elements = findElements(By.className(className));
+ return filterElementListBy(elements, criteria, text);
+ }
+
+ public synchronized WebElement getWhere(String className) {
+ switchToFrameContainingCharSeq(className);
+ List elements = findElements(By.className(className));
+ return filterElementListBy(elements, CRITERIA.NONE, Constants.EMPTY);
+ }
+
+ public synchronized void followContainedLink(WebElement element) {
+ String link = element.getAttribute("href");
+ if (link != null) { navigate().to(link); }
+ }
+
+ public synchronized void setRadio(By by, boolean checked) {
+ ((JavascriptExecutor) this).executeScript("arguments[0].checked = " + checked + ";", findElement(by));
+ }
+
+ public synchronized void setComboByDataValue(By comboBy, String dataValue) {
+ WebElement combo = findElement(comboBy);
+ combo.click();
+ combo.findElement(By.cssSelector("li[data-value='" + dataValue + "']")).click();
+ }
+
+ /**
+ * Any-Match.
+ */
+ public synchronized WebElement getByFieldValue(String type, String field, String value) {
+ return findElement(By.cssSelector(type + "[" + field + "*='" + value + "']"));
+ }
+
+ public synchronized void clickByFieldValue(String type, String field, String value) {
+ var we = getByFieldValue(type, field, value);
+ click(we);
+ }
+
+ /**
+ * @param field can contain wildcards like "class*" or "class^"
+ * @param value can contain partial matches like "last-"
+ */
+ public synchronized WebElement getByFieldValue(SearchContext searchContext, String type, String field, String value) {
+
+ return searchContext.findElement(By.cssSelector(type + "[" + field + "='" + value + "']"));
+ }
+
+ public synchronized WebElement getByText(String text) {
+ return findElement(By.xpath("//*[contains(text(),'" + text + "')]"));
+ }
+
+ public synchronized void click(WebElement we) {
+ executeScript("arguments[0].click();", we);
+ }
+
+}
diff --git a/src/main/java/com/olexyn/tabdriver/TabDriverBuilder.java b/src/main/java/com/olexyn/tabdriver/TabDriverBuilder.java
new file mode 100644
index 0000000..653dc81
--- /dev/null
+++ b/src/main/java/com/olexyn/tabdriver/TabDriverBuilder.java
@@ -0,0 +1,59 @@
+package com.olexyn.tabdriver;
+
+import java.nio.file.Path;
+import java.util.HashMap;
+
+import com.olexyn.PropConf;
+import lombok.experimental.UtilityClass;
+import org.openqa.selenium.chrome.ChromeDriverService;
+import org.openqa.selenium.chrome.ChromeOptions;
+import org.openqa.selenium.remote.CapabilityType;
+import org.openqa.selenium.remote.DesiredCapabilities;
+
+@UtilityClass
+public class TabDriverBuilder {
+
+ private static Path CONFIG_PATH;
+
+ public static TabDriver build(Path path) {
+ CONFIG_PATH = path;
+ if (CONFIG_PATH == null) {
+ throw new RuntimeException("CONFIG_PATH must be set to an absolute path.");
+ }
+ return new TabDriver(configureService(), configureCapabilities());
+ }
+
+
+ private static ChromeDriverService configureService() {
+ PropConf.loadProperties(CONFIG_PATH.toString());
+ var path = Path.of(PropConf.get("chrome.driver.path"));
+ return new ChromeDriverService.Builder()
+ .usingDriverExecutable(path.toFile())
+ .usingAnyFreePort()
+ .build();
+ }
+
+ private static DesiredCapabilities configureCapabilities() {
+ var cap = DesiredCapabilities.chrome();
+ cap.setCapability(CapabilityType.ACCEPT_SSL_CERTS, true);
+
+ ChromeOptions options = new ChromeOptions();
+ options.addArguments("--start-maximized");
+ if (PropConf.is("headless")) {
+ options.addArguments("--window-size=1920,1080");
+ options.addArguments("--headless");
+ }
+ // see also https://chromium.googlesource.com/chromium/src/+/master/chrome/common/pref_names.cc
+ HashMap chromePrefs = new HashMap<>();
+ chromePrefs.put("profile.default_content_settings.popups", 0);
+ chromePrefs.put("download.default_directory", PropConf.get("download.dir"));
+ chromePrefs.put("download.prompt_for_download", false);
+ options.setExperimentalOption("prefs", chromePrefs);
+ cap.setCapability(ChromeOptions.CAPABILITY, options);
+ return cap;
+ }
+
+
+
+
+}
diff --git a/src/main/resources/tabriver.properties b/src/main/resources/tabriver.properties
deleted file mode 100644
index 840dc7d..0000000
--- a/src/main/resources/tabriver.properties
+++ /dev/null
@@ -1,3 +0,0 @@
-chrome.driver.path=
-headless=false
-download.dir=
\ No newline at end of file