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