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