|
| 1 | +from typing import List |
| 2 | +from pathlib import Path |
| 3 | +import time |
| 4 | + |
| 5 | +from selenium.webdriver.firefox.webdriver import WebDriver |
| 6 | +from selenium.webdriver.firefox.options import Options |
| 7 | +from selenium.webdriver.common.by import By |
| 8 | +from selenium.webdriver.support.ui import WebDriverWait |
| 9 | +from selenium.webdriver.support import expected_conditions as ec |
| 10 | +from selenium.common.exceptions import TimeoutException as SeleniumTimeoutException |
| 11 | + |
| 12 | + |
| 13 | +class SeleniumRunner: |
| 14 | + """ |
| 15 | + A runner that upload and download Icomoon resources using Selenium. |
| 16 | + The WebDriver will use Firefox. |
| 17 | + """ |
| 18 | + |
| 19 | + """ |
| 20 | + The long wait time for the driver in seconds. |
| 21 | + """ |
| 22 | + LONG_WAIT_IN_SEC = 25 |
| 23 | + |
| 24 | + """ |
| 25 | + The medium wait time for the driver in seconds. |
| 26 | + """ |
| 27 | + MED_WAIT_IN_SEC = 6 |
| 28 | + |
| 29 | + """ |
| 30 | + The short wait time for the driver in seconds. |
| 31 | + """ |
| 32 | + SHORT_WAIT_IN_SEC = 0.6 |
| 33 | + |
| 34 | + """ |
| 35 | + The Icomoon Url. |
| 36 | + """ |
| 37 | + ICOMOON_URL = "https://icomoon.io/app/#/select" |
| 38 | + |
| 39 | + def __init__(self, icomoon_json_path: str, download_path: str, |
| 40 | + geckodriver_path: str, headless): |
| 41 | + """ |
| 42 | + Create a SeleniumRunner object. |
| 43 | + :param icomoon_json_path: a path to the iconmoon.json. |
| 44 | + :param download_path: the location where you want to download |
| 45 | + the icomoon.zip to. |
| 46 | + :param geckodriver_path: the path to the firefox executable. |
| 47 | + :param headless: whether to run browser in headless (no UI) mode. |
| 48 | + """ |
| 49 | + self.icomoon_json_path = icomoon_json_path |
| 50 | + self.download_path = download_path |
| 51 | + self.driver = None |
| 52 | + self.set_options(geckodriver_path, headless) |
| 53 | + |
| 54 | + def set_options(self, geckodriver_path: str, headless: bool): |
| 55 | + """ |
| 56 | + Build the WebDriver with Firefox Options allowing downloads and |
| 57 | + set download to download_path. |
| 58 | + :param geckodriver_path: the path to the firefox executable. |
| 59 | + :param headless: whether to run browser in headless (no UI) mode. |
| 60 | +
|
| 61 | + :raises AssertionError: if the page title does not contain |
| 62 | + "IcoMoon App". |
| 63 | + """ |
| 64 | + options = Options() |
| 65 | + allowed_mime_types = "application/zip, application/gzip, application/octet-stream" |
| 66 | + # disable prompt to download from Firefox |
| 67 | + options.set_preference("browser.helperApps.neverAsk.saveToDisk", allowed_mime_types) |
| 68 | + options.set_preference("browser.helperApps.neverAsk.openFile", allowed_mime_types) |
| 69 | + |
| 70 | + # set the default download path to downloadPath |
| 71 | + options.set_preference("browser.download.folderList", 2) |
| 72 | + options.set_preference("browser.download.dir", self.download_path) |
| 73 | + options.headless = headless |
| 74 | + |
| 75 | + self.driver = WebDriver(options=options, executable_path=geckodriver_path) |
| 76 | + self.driver.get(self.ICOMOON_URL) |
| 77 | + assert "IcoMoon App" in self.driver.title |
| 78 | + |
| 79 | + def upload_icomoon(self): |
| 80 | + """ |
| 81 | + Upload the icomoon.json to icomoon.io. |
| 82 | + :raises TimeoutException: happens when elements are not found. |
| 83 | + """ |
| 84 | + print("Uploading icomoon.json file...") |
| 85 | + try: |
| 86 | + # find the file input and enter the file path |
| 87 | + import_btn = WebDriverWait(self.driver, SeleniumRunner.LONG_WAIT_IN_SEC).until( |
| 88 | + ec.presence_of_element_located((By.CSS_SELECTOR, "div#file input")) |
| 89 | + ) |
| 90 | + import_btn.send_keys(self.icomoon_json_path) |
| 91 | + except Exception as e: |
| 92 | + self.close() |
| 93 | + raise e |
| 94 | + |
| 95 | + try: |
| 96 | + confirm_btn = WebDriverWait(self.driver, SeleniumRunner.MED_WAIT_IN_SEC).until( |
| 97 | + ec.element_to_be_clickable((By.XPATH, "//div[@class='overlay']//button[text()='Yes']")) |
| 98 | + ) |
| 99 | + confirm_btn.click() |
| 100 | + except SeleniumTimeoutException as e: |
| 101 | + print(e.stacktrace) |
| 102 | + print("Cannot find the confirm button when uploading the icomoon.json", |
| 103 | + "Ensure that the icomoon.json is in the correct format for Icomoon.io", |
| 104 | + sep='\n') |
| 105 | + self.close() |
| 106 | + |
| 107 | + print("JSON file uploaded.") |
| 108 | + |
| 109 | + def upload_svgs(self, svgs: List[str]): |
| 110 | + """ |
| 111 | + Upload the SVGs provided in folder_info |
| 112 | + :param svgs: a list of svg Paths that we'll upload to icomoon. |
| 113 | + """ |
| 114 | + try: |
| 115 | + print("Uploading SVGs...") |
| 116 | + |
| 117 | + edit_mode_btn = self.driver.find_element_by_css_selector( |
| 118 | + "div.btnBar button i.icon-edit" |
| 119 | + ) |
| 120 | + edit_mode_btn.click() |
| 121 | + |
| 122 | + self.click_hamburger_input() |
| 123 | + |
| 124 | + for svg in svgs: |
| 125 | + import_btn = self.driver.find_element_by_css_selector( |
| 126 | + "li.file input[type=file]" |
| 127 | + ) |
| 128 | + import_btn.send_keys(svg) |
| 129 | + print(f"Uploaded {svg}") |
| 130 | + self.test_for_possible_alert(self.SHORT_WAIT_IN_SEC, "Dismiss") |
| 131 | + self.remove_color_from_icon() |
| 132 | + |
| 133 | + self.click_hamburger_input() |
| 134 | + select_all_button = WebDriverWait(self.driver, self.LONG_WAIT_IN_SEC).until( |
| 135 | + ec.element_to_be_clickable((By.XPATH, "//button[text()='Select All']")) |
| 136 | + ) |
| 137 | + select_all_button.click() |
| 138 | + except Exception as e: |
| 139 | + self.close() |
| 140 | + raise e |
| 141 | + |
| 142 | + def click_hamburger_input(self): |
| 143 | + """ |
| 144 | + Click the hamburger input until the pop up menu appears. This |
| 145 | + method is needed because sometimes, we need to click the hamburger |
| 146 | + input two times before the menu appears. |
| 147 | + :return: None. |
| 148 | + """ |
| 149 | + try: |
| 150 | + hamburger_input = self.driver.find_element_by_css_selector( |
| 151 | + "button.btn5.lh-def.transparent i.icon-menu" |
| 152 | + ) |
| 153 | + |
| 154 | + menu_appear_callback = ec.element_to_be_clickable( |
| 155 | + (By.CSS_SELECTOR, "h1#setH2 ul") |
| 156 | + ) |
| 157 | + |
| 158 | + while not menu_appear_callback(self.driver): |
| 159 | + hamburger_input.click() |
| 160 | + except Exception as e: |
| 161 | + self.close() |
| 162 | + raise e |
| 163 | + |
| 164 | + def test_for_possible_alert(self, wait_period: float, btn_text: str): |
| 165 | + """ |
| 166 | + Test for the possible alert when we upload the svgs. |
| 167 | + :param wait_period: the wait period for the possible alert |
| 168 | + in seconds. |
| 169 | + :param btn_text: the text that the alert's button will have. |
| 170 | + :return: None. |
| 171 | + """ |
| 172 | + try: |
| 173 | + dismiss_btn = WebDriverWait(self.driver, wait_period, 0.15).until( |
| 174 | + ec.element_to_be_clickable( |
| 175 | + (By.XPATH, f"//div[@class='overlay']//button[text()='{btn_text}']")) |
| 176 | + ) |
| 177 | + dismiss_btn.click() |
| 178 | + except SeleniumTimeoutException: |
| 179 | + pass |
| 180 | + |
| 181 | + def remove_color_from_icon(self): |
| 182 | + """ |
| 183 | + Remove the color from the most recent uploaded icon. |
| 184 | + :return: None. |
| 185 | + """ |
| 186 | + try: |
| 187 | + recently_uploaded_icon = WebDriverWait(self.driver, self.LONG_WAIT_IN_SEC).until( |
| 188 | + ec.element_to_be_clickable((By.XPATH, "//div[@id='set0']//mi-box[1]//div")) |
| 189 | + ) |
| 190 | + recently_uploaded_icon.click() |
| 191 | + except Exception as e: |
| 192 | + self.close() |
| 193 | + raise e |
| 194 | + |
| 195 | + try: |
| 196 | + color_tab = WebDriverWait(self.driver, self.SHORT_WAIT_IN_SEC).until( |
| 197 | + ec.element_to_be_clickable((By.CSS_SELECTOR, "div.overlayWindow i.icon-droplet")) |
| 198 | + ) |
| 199 | + color_tab.click() |
| 200 | + |
| 201 | + remove_color_btn = self.driver \ |
| 202 | + .find_element_by_css_selector("div.overlayWindow i.icon-droplet-cross") |
| 203 | + remove_color_btn.click() |
| 204 | + except SeleniumTimeoutException: |
| 205 | + pass |
| 206 | + except Exception as e: |
| 207 | + self.close() |
| 208 | + raise e |
| 209 | + |
| 210 | + try: |
| 211 | + close_btn = self.driver \ |
| 212 | + .find_element_by_css_selector("div.overlayWindow i.icon-close") |
| 213 | + close_btn.click() |
| 214 | + except Exception as e: |
| 215 | + self.close() |
| 216 | + raise e |
| 217 | + |
| 218 | + def download_icomoon_fonts(self, zip_path: Path): |
| 219 | + """ |
| 220 | + Download the icomoon.zip from icomoon.io. |
| 221 | + :param zip_path: the path to the zip file after it's downloaded. |
| 222 | + """ |
| 223 | + try: |
| 224 | + print("Downloading Font files...") |
| 225 | + self.driver.find_element_by_css_selector( |
| 226 | + "a[href='#/select/font']" |
| 227 | + ).click() |
| 228 | + |
| 229 | + self.test_for_possible_alert(self.MED_WAIT_IN_SEC, "Continue") |
| 230 | + download_btn = WebDriverWait(self.driver, SeleniumRunner.LONG_WAIT_IN_SEC).until( |
| 231 | + ec.presence_of_element_located((By.CSS_SELECTOR, "button.btn4 span")) |
| 232 | + ) |
| 233 | + download_btn.click() |
| 234 | + if self.wait_for_zip(zip_path): |
| 235 | + print("Font files downloaded.") |
| 236 | + else: |
| 237 | + raise TimeoutError(f"Couldn't find {zip_path} after download button was clicked.") |
| 238 | + except Exception as e: |
| 239 | + self.close() |
| 240 | + raise e |
| 241 | + |
| 242 | + def wait_for_zip(self, zip_path: Path) -> bool: |
| 243 | + """ |
| 244 | + Wait for the zip file to be downloaded by checking for its existence |
| 245 | + in the download path. Wait time is self.LONG_WAIT_IN_SEC and check time |
| 246 | + is 1 sec. |
| 247 | + :param zip_path: the path to the zip file after it's |
| 248 | + downloaded. |
| 249 | + :return: True if the file is found within the allotted time, else |
| 250 | + False. |
| 251 | + """ |
| 252 | + end_time = time.time() + self.LONG_WAIT_IN_SEC |
| 253 | + while time.time() <= end_time: |
| 254 | + if zip_path.exists(): |
| 255 | + return True |
| 256 | + time.sleep(1) |
| 257 | + return False |
| 258 | + |
| 259 | + def close(self): |
| 260 | + """ |
| 261 | + Close the SeleniumRunner instance. |
| 262 | + """ |
| 263 | + print("Closing down SeleniumRunner...") |
| 264 | + self.driver.quit() |
0 commit comments