From 34c52c00230e54c7fc96b26446c778e681712549 Mon Sep 17 00:00:00 2001
From: io42630 <ivan@olexyn.com>
Date: Sat, 26 Aug 2023 12:28:18 +0200
Subject: [PATCH] improve creation

---
 README.md                                     |  14 +-
 pom.xml                                       |  37 +--
 .../java/com/olexyn/tabdriver/Constants.java  |   1 -
 .../java/com/olexyn/tabdriver/TabDriver.java  | 283 ++++++++++++++++++
 .../olexyn/tabdriver/TabDriverBuilder.java    |  59 ++++
 src/main/resources/tabriver.properties        |   3 -
 6 files changed, 374 insertions(+), 23 deletions(-)
 create mode 100644 src/main/java/com/olexyn/tabdriver/TabDriver.java
 create mode 100644 src/main/java/com/olexyn/tabdriver/TabDriverBuilder.java
 delete mode 100644 src/main/resources/tabriver.properties

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 @@
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+		 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 		 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 	<modelVersion>4.0.0</modelVersion>
 
 	<groupId>com.olexyn</groupId>
 	<artifactId>tabdriver</artifactId>
-	<version>1.1</version>
+	<version>1.2</version>
 	<packaging>jar</packaging>
 
-	<name>tabriver</name>
+	<name>tabdriver</name>
 
 	<properties>
 		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 		<xx.java.version>17</xx.java.version>
+		<xx.selenium.version>3.141.59</xx.selenium.version>
+		<xx.webdriver.version>0.9.7376</xx.webdriver.version>
 	</properties>
 
-
 	<dependencyManagement>
 		<dependencies>
 			<dependency>
 				<groupId>com.olexyn</groupId>
 				<artifactId>zeebom</artifactId>
-				<version>1.0-SNAPSHOT</version>
+				<version>1.1</version>
 				<type>pom</type>
 				<scope>import</scope>
 			</dependency>
@@ -37,43 +39,43 @@
 			<!-- must come before selenium -->
 			<groupId>org.seleniumhq.selenium</groupId>
 			<artifactId>selenium-api</artifactId>
-			<version>3.141.59</version>
+			<version>${xx.selenium.version}</version>
 		</dependency>
 		<dependency>
 			<groupId>org.seleniumhq.webdriver</groupId>
 			<artifactId>webdriver-selenium</artifactId>
-			<version>0.9.7376</version>
+			<version>${xx.webdriver.version}</version>
 		</dependency>
 		<dependency>
 			<groupId>org.seleniumhq.selenium</groupId>
 			<artifactId>selenium-chrome-driver</artifactId>
-			<version>3.141.59</version>
+			<version>${xx.selenium.version}</version>
 		</dependency>
 		<dependency>
 			<groupId>org.seleniumhq.webdriver</groupId>
 			<artifactId>webdriver-common</artifactId>
-			<version>0.9.7376</version>
+			<version>${xx.webdriver.version}</version>
 		</dependency>
 		<dependency>
 			<groupId>org.seleniumhq.webdriver</groupId>
 			<artifactId>webdriver-support</artifactId>
-			<version>0.9.7376</version>
-		</dependency>
-		<dependency>
-			<groupId>org.projectlombok</groupId>
-			<artifactId>lombok</artifactId>
+			<version>${xx.webdriver.version}</version>
 		</dependency>
+
 		<dependency>
 			<groupId>org.seleniumhq.selenium</groupId>
 			<artifactId>selenium-java</artifactId>
-			<version>3.141.59</version>
+			<version>${xx.selenium.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>org.projectlombok</groupId>
+			<artifactId>lombok</artifactId>
 		</dependency>
 
 		<dependency>
 			<groupId>com.olexyn</groupId>
 			<artifactId>propconf</artifactId>
-			<version>1.0-SNAPSHOT</version>
-			<scope>compile</scope>
+			<version>1.1</version>
 		</dependency>
 	</dependencies>
 
@@ -101,5 +103,4 @@
 		</pluginManagement>
 	</build>
 
-
 </project>
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<String, Tab> 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<Tab> getTabByPurpose(String purpose) {
+        return TABS.values().stream()
+            .filter(x -> x.getPurpose() == purpose)
+            .collect(Collectors.toList());
+    }
+
+    public synchronized String registerBlankTab(String purpose) {
+        Set<String> 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<String, Tab> 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<Tab> 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<WebElement> 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<String, String> collectAllFrames() throws NoSuchFrameException {
+        Map<String, String> 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<String, String> mapOfCollectedSources, String string) {
+        for (Entry<String, String> 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<WebElement> elements = findElements(By.className(className));
+        return filterElementListBy(elements, criteria, text);
+    }
+
+    public synchronized WebElement getWhere(String className) {
+        switchToFrameContainingCharSeq(className);
+        List<WebElement> 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<String, Object> 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