mirror of
https://github.com/konpa/devicon.git
synced 2025-08-12 09:34:36 +02:00
Feature: Peek and Build-Bot Upgrade (#806)
* Refactored peek script into a class * Post-peek workflow now upload the new screenshots * Refactored BuildSeleniumRunner into a class * Updated build_icons.yml to reflect new changes * Fixed issue with building icons that were already in the app * Build script will take screenshot of new icons * Update post peek yaml message * Added alerts * Peek script now check for strokes in icons * Updated post_peek's strokes in svgs message * Updated post_peek script's message * Updated post_peek's message * Refactored get_release_message into icomoon_build * Change devicon.css name to devicon-base.css * Updated post_peek message * Added update icon as a valid PR title for bot-peek * Add \n char to SVG after it gets optimized * Fixed error with 'update icon' regex * Build script now batch issues when upload SVG * Addressed build-bot's screenshot order * Apply suggestions from code review Co-authored-by: David Leal <halfpacho@gmail.com> Co-authored-by: David Leal <halfpacho@gmail.com>
This commit is contained in:
267
.github/scripts/build_assets/SeleniumRunner.py
vendored
267
.github/scripts/build_assets/SeleniumRunner.py
vendored
@@ -1,267 +0,0 @@
|
||||
from typing import List
|
||||
from pathlib import Path
|
||||
import time
|
||||
|
||||
from selenium.webdriver.firefox.webdriver import WebDriver
|
||||
from selenium.webdriver.firefox.options import Options
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as ec
|
||||
from selenium.common.exceptions import TimeoutException as SeleniumTimeoutException
|
||||
|
||||
|
||||
class SeleniumRunner:
|
||||
"""
|
||||
A runner that upload and download Icomoon resources using Selenium.
|
||||
The WebDriver will use Firefox.
|
||||
"""
|
||||
|
||||
"""
|
||||
The long wait time for the driver in seconds.
|
||||
"""
|
||||
LONG_WAIT_IN_SEC = 25
|
||||
|
||||
"""
|
||||
The medium wait time for the driver in seconds.
|
||||
"""
|
||||
MED_WAIT_IN_SEC = 6
|
||||
|
||||
"""
|
||||
The short wait time for the driver in seconds.
|
||||
"""
|
||||
SHORT_WAIT_IN_SEC = 0.6
|
||||
|
||||
"""
|
||||
The Icomoon Url.
|
||||
"""
|
||||
ICOMOON_URL = "https://icomoon.io/app/#/select"
|
||||
|
||||
def __init__(self, download_path: str,
|
||||
geckodriver_path: str, headless: bool):
|
||||
"""
|
||||
Create a SeleniumRunner object.
|
||||
:param download_path: the location where you want to download
|
||||
the icomoon.zip to.
|
||||
:param geckodriver_path: the path to the firefox executable.
|
||||
:param headless: whether to run browser in headless (no UI) mode.
|
||||
"""
|
||||
self.driver = None
|
||||
self.set_options(download_path, geckodriver_path, headless)
|
||||
|
||||
def set_options(self, download_path: str, geckodriver_path: str,
|
||||
headless: bool):
|
||||
"""
|
||||
Build the WebDriver with Firefox Options allowing downloads and
|
||||
set download to download_path.
|
||||
:param download_path: the location where you want to download
|
||||
:param geckodriver_path: the path to the firefox executable.
|
||||
the icomoon.zip to.
|
||||
:param headless: whether to run browser in headless (no UI) mode.
|
||||
|
||||
:raises AssertionError: if the page title does not contain
|
||||
"IcoMoon App".
|
||||
"""
|
||||
options = Options()
|
||||
allowed_mime_types = "application/zip, application/gzip, application/octet-stream"
|
||||
# disable prompt to download from Firefox
|
||||
options.set_preference("browser.helperApps.neverAsk.saveToDisk", allowed_mime_types)
|
||||
options.set_preference("browser.helperApps.neverAsk.openFile", allowed_mime_types)
|
||||
|
||||
# set the default download path to downloadPath
|
||||
options.set_preference("browser.download.folderList", 2)
|
||||
options.set_preference("browser.download.dir", download_path)
|
||||
options.headless = headless
|
||||
|
||||
self.driver = WebDriver(options=options, executable_path=geckodriver_path)
|
||||
self.driver.get(self.ICOMOON_URL)
|
||||
assert "IcoMoon App" in self.driver.title
|
||||
# wait until the whole web page is loaded by testing the hamburger input
|
||||
WebDriverWait(self.driver, self.LONG_WAIT_IN_SEC).until(
|
||||
ec.element_to_be_clickable((By.XPATH, "(//i[@class='icon-menu'])[2]"))
|
||||
)
|
||||
print("Accessed icomoon.io")
|
||||
|
||||
|
||||
def upload_icomoon(self, icomoon_json_path: str):
|
||||
"""
|
||||
Upload the icomoon.json to icomoon.io.
|
||||
:param icomoon_json_path: a path to the iconmoon.json.
|
||||
:raises TimeoutException: happens when elements are not found.
|
||||
"""
|
||||
print("Uploading icomoon.json file...")
|
||||
self.click_hamburger_input()
|
||||
|
||||
# find the file input and enter the file path
|
||||
import_btn = self.driver.find_element(By.XPATH, "(//li[@class='file'])[1]//input")
|
||||
import_btn.send_keys(icomoon_json_path)
|
||||
|
||||
try:
|
||||
confirm_btn = WebDriverWait(self.driver, SeleniumRunner.MED_WAIT_IN_SEC).until(
|
||||
ec.element_to_be_clickable((By.XPATH, "//div[@class='overlay']//button[text()='Yes']"))
|
||||
)
|
||||
confirm_btn.click()
|
||||
except SeleniumTimeoutException as e:
|
||||
raise Exception("Cannot find the confirm button when uploading the icomoon.json" \
|
||||
"Ensure that the icomoon.json is in the correct format for Icomoon.io")
|
||||
|
||||
print("JSON file uploaded.")
|
||||
|
||||
def upload_svgs(self, svgs: List[str], screenshot_folder: str=""):
|
||||
"""
|
||||
Upload the SVGs provided in folder_info
|
||||
:param svgs: a list of svg Paths that we'll upload to icomoon.
|
||||
:param screenshot_folder: the name of the screenshot_folder. If
|
||||
the value is provided, it means the user want to take a screenshot
|
||||
of each icon.
|
||||
"""
|
||||
print("Uploading SVGs...")
|
||||
|
||||
edit_mode_btn = self.driver.find_element_by_css_selector(
|
||||
"div.btnBar button i.icon-edit"
|
||||
)
|
||||
edit_mode_btn.click()
|
||||
|
||||
self.click_hamburger_input()
|
||||
|
||||
for i in range(len(svgs)):
|
||||
import_btn = self.driver.find_element_by_css_selector(
|
||||
"li.file input[type=file]"
|
||||
)
|
||||
import_btn.send_keys(svgs[i])
|
||||
print(f"Uploaded {svgs[i]}")
|
||||
self.test_for_possible_alert(self.SHORT_WAIT_IN_SEC, "Dismiss")
|
||||
self.click_on_just_added_icon(screenshot_folder, i)
|
||||
|
||||
# take a screenshot of the icons that were just added
|
||||
new_icons_path = str(Path(screenshot_folder, "new_icons.png").resolve())
|
||||
self.driver.save_screenshot(new_icons_path);
|
||||
|
||||
print("Finished uploading the svgs...")
|
||||
|
||||
def click_hamburger_input(self):
|
||||
"""
|
||||
Click the hamburger input until the pop up menu appears. This
|
||||
method is needed because sometimes, we need to click the hamburger
|
||||
input two times before the menu appears.
|
||||
:return: None.
|
||||
"""
|
||||
hamburger_input = self.driver.find_element_by_xpath(
|
||||
"(//i[@class='icon-menu'])[2]"
|
||||
)
|
||||
|
||||
menu_appear_callback = ec.element_to_be_clickable(
|
||||
(By.CSS_SELECTOR, "h1 ul.menuList2")
|
||||
)
|
||||
|
||||
while not menu_appear_callback(self.driver):
|
||||
hamburger_input.click()
|
||||
|
||||
def test_for_possible_alert(self, wait_period: float, btn_text: str):
|
||||
"""
|
||||
Test for the possible alert when we upload the svgs.
|
||||
:param wait_period: the wait period for the possible alert
|
||||
in seconds.
|
||||
:param btn_text: the text that the alert's button will have.
|
||||
:return: None.
|
||||
"""
|
||||
try:
|
||||
dismiss_btn = WebDriverWait(self.driver, wait_period, 0.15).until(
|
||||
ec.element_to_be_clickable(
|
||||
(By.XPATH, f"//div[@class='overlay']//button[text()='{btn_text}']"))
|
||||
)
|
||||
dismiss_btn.click()
|
||||
except SeleniumTimeoutException:
|
||||
pass # nothing found => everything is good
|
||||
|
||||
def click_on_just_added_icon(self, screenshot_folder: str, index: int):
|
||||
"""
|
||||
Click on the most recently added icon so we can remove the colors
|
||||
and take a snapshot if needed.
|
||||
"""
|
||||
recently_uploaded_icon = WebDriverWait(self.driver, self.LONG_WAIT_IN_SEC).until(
|
||||
ec.element_to_be_clickable((By.XPATH, "//div[@id='set0']//mi-box[1]//div"))
|
||||
)
|
||||
recently_uploaded_icon.click()
|
||||
|
||||
self.remove_color_from_icon()
|
||||
|
||||
if screenshot_folder:
|
||||
screenshot_path = str(Path(screenshot_folder, f"screenshot_{index}.png").resolve())
|
||||
self.driver.save_screenshot(screenshot_path)
|
||||
print("Took screenshot and saved it at " + screenshot_path)
|
||||
|
||||
close_btn = self.driver \
|
||||
.find_element_by_css_selector("div.overlayWindow i.icon-close")
|
||||
close_btn.click()
|
||||
|
||||
def remove_color_from_icon(self):
|
||||
"""
|
||||
Remove the color from the most recent uploaded icon.
|
||||
This is because some SVG have colors in them and we don't want to
|
||||
force contributors to remove them in case people want the colored SVGs.
|
||||
The color removal is also necessary so that the Icomoon-generated
|
||||
icons fit within one font symbol/ligiature.
|
||||
"""
|
||||
try:
|
||||
color_tab = WebDriverWait(self.driver, self.SHORT_WAIT_IN_SEC).until(
|
||||
ec.element_to_be_clickable((By.CSS_SELECTOR, "div.overlayWindow i.icon-droplet"))
|
||||
)
|
||||
color_tab.click()
|
||||
|
||||
remove_color_btn = self.driver \
|
||||
.find_element_by_css_selector("div.overlayWindow i.icon-droplet-cross")
|
||||
remove_color_btn.click()
|
||||
except SeleniumTimeoutException:
|
||||
pass # do nothing cause sometimes, the color tab doesn't appear in the site
|
||||
|
||||
def download_icomoon_fonts(self, zip_path: Path):
|
||||
"""
|
||||
Download the icomoon.zip from icomoon.io.
|
||||
:param zip_path: the path to the zip file after it's downloaded.
|
||||
"""
|
||||
# select all the svgs so that the newly added svg are part of the collection
|
||||
self.click_hamburger_input()
|
||||
select_all_button = WebDriverWait(self.driver, self.LONG_WAIT_IN_SEC).until(
|
||||
ec.element_to_be_clickable((By.XPATH, "//button[text()='Select All']"))
|
||||
)
|
||||
select_all_button.click()
|
||||
|
||||
print("Downloading Font files...")
|
||||
font_tab = self.driver.find_element_by_css_selector(
|
||||
"a[href='#/select/font']"
|
||||
)
|
||||
font_tab.click()
|
||||
|
||||
self.test_for_possible_alert(self.MED_WAIT_IN_SEC, "Continue")
|
||||
download_btn = WebDriverWait(self.driver, SeleniumRunner.LONG_WAIT_IN_SEC).until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, "button.btn4 span"))
|
||||
)
|
||||
download_btn.click()
|
||||
if self.wait_for_zip(zip_path):
|
||||
print("Font files downloaded.")
|
||||
else:
|
||||
raise TimeoutError(f"Couldn't find {zip_path} after download button was clicked.")
|
||||
|
||||
def wait_for_zip(self, zip_path: Path) -> bool:
|
||||
"""
|
||||
Wait for the zip file to be downloaded by checking for its existence
|
||||
in the download path. Wait time is self.LONG_WAIT_IN_SEC and check time
|
||||
is 1 sec.
|
||||
:param zip_path: the path to the zip file after it's
|
||||
downloaded.
|
||||
:return: True if the file is found within the allotted time, else
|
||||
False.
|
||||
"""
|
||||
end_time = time.time() + self.LONG_WAIT_IN_SEC
|
||||
while time.time() <= end_time:
|
||||
if zip_path.exists():
|
||||
return True
|
||||
time.sleep(1)
|
||||
return False
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Close the SeleniumRunner instance.
|
||||
"""
|
||||
print("Closing down SeleniumRunner...")
|
||||
self.driver.quit()
|
57
.github/scripts/build_assets/api_handler.py
vendored
57
.github/scripts/build_assets/api_handler.py
vendored
@@ -2,10 +2,38 @@ import requests
|
||||
import sys
|
||||
import re
|
||||
|
||||
|
||||
def get_merged_pull_reqs_since_last_release(token):
|
||||
"""
|
||||
Get all the merged pull requests since the last release.
|
||||
"""
|
||||
stopPattern = r"^(r|R)elease v"
|
||||
pull_reqs = []
|
||||
found_last_release = False
|
||||
page = 1
|
||||
|
||||
print("Getting PRs since last release.")
|
||||
while not found_last_release:
|
||||
data = get_merged_pull_reqs(token, page)
|
||||
# assume we don't encounter it during the loop
|
||||
last_release_index = 101
|
||||
|
||||
for i in range(len(data)):
|
||||
if re.search(stopPattern, data[i]["title"]):
|
||||
found_last_release = True
|
||||
last_release_index = i
|
||||
break
|
||||
pull_reqs.extend(data[:last_release_index])
|
||||
page += 1
|
||||
|
||||
# should contain all the PRs since last release
|
||||
return pull_reqs
|
||||
|
||||
|
||||
def get_merged_pull_reqs(token, page):
|
||||
"""
|
||||
Get the merged pull requests based on page. There are
|
||||
100 results page. See https://docs.github.com/en/rest/reference/pulls
|
||||
100 results per page. See https://docs.github.com/en/rest/reference/pulls
|
||||
for more details on the parameters.
|
||||
:param token, a GitHub API token.
|
||||
:param page, the page number.
|
||||
@@ -71,30 +99,3 @@ def find_all_authors(pull_req_data, token):
|
||||
authors.add(commit["commit"]["author"]["name"])
|
||||
print(f"This URL didn't have an `author` attribute: {pull_req_data['commits_url']}")
|
||||
return ", ".join(["@" + author for author in list(authors)])
|
||||
|
||||
|
||||
def get_merged_pull_reqs_since_last_release(token):
|
||||
"""
|
||||
Get all the merged pull requests since the last release.
|
||||
"""
|
||||
stopPattern = r"^(r|R)elease v"
|
||||
pull_reqs = []
|
||||
found_last_release = False
|
||||
page = 1
|
||||
|
||||
print("Getting PRs since last release.")
|
||||
while not found_last_release:
|
||||
data = get_merged_pull_reqs(token, page)
|
||||
# assume we don't encounter it during the loop
|
||||
last_release_index = 101
|
||||
|
||||
for i in range(len(data)):
|
||||
if re.search(stopPattern, data[i]["title"]):
|
||||
found_last_release = True
|
||||
last_release_index = i
|
||||
break
|
||||
pull_reqs.extend(data[:last_release_index])
|
||||
page += 1
|
||||
|
||||
# should contain all the PRs since last release
|
||||
return pull_reqs
|
||||
|
6
.github/scripts/build_assets/filehandler.py
vendored
6
.github/scripts/build_assets/filehandler.py
vendored
@@ -91,7 +91,7 @@ def get_icon_svgs_paths(folder_path: Path, icon_info: dict,
|
||||
for font_version in icon_info["versions"]["font"]:
|
||||
# if it's an alias, we don't want to make it into an icon
|
||||
if is_alias(font_version, aliases):
|
||||
print(f"Skipping this font since it's an alias: {icon_info['name']}-{font_version}.svg")
|
||||
print(f"Finding SVG filepaths: skipping this font since it's an alias: {icon_info['name']}-{font_version}.svg")
|
||||
continue
|
||||
|
||||
file_name = f"{icon_info['name']}-{font_version}.svg"
|
||||
@@ -177,7 +177,7 @@ def rename_extracted_files(extract_path: str):
|
||||
},
|
||||
{
|
||||
"old": Path(extract_path, "style.css"),
|
||||
"new": Path(extract_path, "devicon.css")
|
||||
"new": Path(extract_path, "devicon-base.css")
|
||||
}
|
||||
]
|
||||
|
||||
@@ -203,7 +203,7 @@ def create_screenshot_folder(dir, screenshot_name: str="screenshots/"):
|
||||
try:
|
||||
os.mkdir(screenshot_folder)
|
||||
except FileExistsError:
|
||||
print(f"{screenshot_folder} already exist. Script will do nothing.")
|
||||
print(f"{screenshot_folder} already exist. Not creating new folder.")
|
||||
finally:
|
||||
return str(screenshot_folder)
|
||||
|
||||
|
171
.github/scripts/build_assets/selenium_runner/BuildSeleniumRunner.py
vendored
Normal file
171
.github/scripts/build_assets/selenium_runner/BuildSeleniumRunner.py
vendored
Normal file
@@ -0,0 +1,171 @@
|
||||
from typing import List
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support import expected_conditions as ec
|
||||
from selenium.common.exceptions import TimeoutException as SeleniumTimeoutException
|
||||
|
||||
from build_assets.selenium_runner.SeleniumRunner import SeleniumRunner
|
||||
from build_assets.selenium_runner.enums import IcomoonPage, IcomoonAlerts, IcomoonOptionState
|
||||
|
||||
class BuildSeleniumRunner(SeleniumRunner):
|
||||
def build_icons(self, icomoon_json_path: str,
|
||||
zip_path: Path, svgs: List[str], screenshot_folder: str):
|
||||
self.upload_icomoon(icomoon_json_path)
|
||||
# necessary so we can take screenshot of only the
|
||||
# recently uploaded icons later
|
||||
self.deselect_all_icons_in_top_set()
|
||||
self.upload_svgs(svgs, screenshot_folder)
|
||||
self.take_icon_screenshot(screenshot_folder)
|
||||
self.download_icomoon_fonts(zip_path)
|
||||
|
||||
def upload_icomoon(self, icomoon_json_path: str):
|
||||
"""
|
||||
Upload the icomoon.json to icomoon.io.
|
||||
:param icomoon_json_path: a path to the iconmoon.json.
|
||||
:raises TimeoutException: happens when elements are not found.
|
||||
"""
|
||||
print("Uploading icomoon.json file...")
|
||||
|
||||
# find the file input and enter the file path
|
||||
import_btn = self.driver.find_element_by_css_selector(
|
||||
SeleniumRunner.GENERAL_IMPORT_BUTTON_CSS
|
||||
)
|
||||
import_btn.send_keys(icomoon_json_path)
|
||||
|
||||
try:
|
||||
confirm_btn = WebDriverWait(self.driver, SeleniumRunner.MED_WAIT_IN_SEC).until(
|
||||
ec.element_to_be_clickable((By.XPATH, "//div[@class='overlay']//button[text()='Yes']"))
|
||||
)
|
||||
confirm_btn.click()
|
||||
except SeleniumTimeoutException as e:
|
||||
raise Exception("Cannot find the confirm button when uploading the icomoon.json" \
|
||||
"Ensure that the icomoon.json is in the correct format for Icomoon.io")
|
||||
|
||||
print("JSON file uploaded.")
|
||||
|
||||
def upload_svgs(self, svgs: List[str], screenshot_folder: str):
|
||||
"""
|
||||
Upload the SVGs provided in svgs. This will upload the
|
||||
:param svgs: a list of svg Paths that we'll upload to icomoon.
|
||||
:param screenshot_folder: the name of the screenshot_folder.
|
||||
"""
|
||||
print("Uploading SVGs...")
|
||||
|
||||
import_btn = self.driver.find_element_by_css_selector(
|
||||
SeleniumRunner.SET_IMPORT_BUTTON_CSS
|
||||
)
|
||||
|
||||
# there could be at most 2 alerts when we upload an SVG.
|
||||
possible_alerts_amount = 2
|
||||
err_messages = []
|
||||
for i in range(len(svgs)):
|
||||
import_btn.send_keys(svgs[i])
|
||||
print(f"Uploading {svgs[i]}")
|
||||
|
||||
# see if there are stroke messages or replacing icon message
|
||||
# there should be none of the second kind
|
||||
for j in range(possible_alerts_amount):
|
||||
alert = self.test_for_possible_alert(self.SHORT_WAIT_IN_SEC)
|
||||
if alert == None:
|
||||
pass # all good
|
||||
elif alert == IcomoonAlerts.STROKES_GET_IGNORED_WARNING:
|
||||
message = f"SVG contained strokes: {svgs[i]}."
|
||||
err_messages.append(message)
|
||||
self.click_alert_button(self.ALERTS[alert]["buttons"]["DISMISS"])
|
||||
elif alert == IcomoonAlerts.REPLACE_OR_REIMPORT_ICON:
|
||||
message = f"Duplicated SVG: {svgs[i]}."
|
||||
err_messages.append(message)
|
||||
self.click_alert_button(self.ALERTS[alert]["buttons"]["REIMPORT"])
|
||||
else:
|
||||
raise Exception(f"Unexpected alert found: {alert}")
|
||||
|
||||
self.edit_svg()
|
||||
print(f"Finished editing icon.")
|
||||
|
||||
if err_messages != []:
|
||||
message = "BuildSeleniumRunner - Issues found when uploading SVGs:\n"
|
||||
raise Exception(message + '\n'.join(err_messages))
|
||||
|
||||
# take a screenshot of the svgs that were just added
|
||||
# select the latest icons
|
||||
self.switch_toolbar_option(IcomoonOptionState.SELECT)
|
||||
self.click_latest_icons_in_top_set(len(svgs))
|
||||
new_svgs_path = str(Path(screenshot_folder, "new_svgs.png").resolve())
|
||||
self.driver.save_screenshot(new_svgs_path);
|
||||
|
||||
print("Finished uploading the svgs...")
|
||||
|
||||
def take_icon_screenshot(self, screenshot_folder: str):
|
||||
"""
|
||||
Take the overview icon screenshot of the uploaded icons.
|
||||
:param svgs: a list of svg Paths that we'll upload to icomoon.
|
||||
:param screenshot_folder: the name of the screenshot_folder.
|
||||
"""
|
||||
# take pictures
|
||||
print("Taking screenshot of the new icons...")
|
||||
self.go_to_generate_font_page()
|
||||
|
||||
# take an overall screenshot of the icons that were just added
|
||||
# also include the glyph count
|
||||
new_icons_path = str(Path(screenshot_folder, "new_icons.png").resolve())
|
||||
main_content_xpath = "/html/body/div[4]/div[2]/div/div[1]"
|
||||
main_content = self.driver.find_element_by_xpath(main_content_xpath)
|
||||
main_content.screenshot(new_icons_path)
|
||||
print("Saved screenshot of the new icons...")
|
||||
|
||||
def go_to_generate_font_page(self):
|
||||
"""
|
||||
Go to the generate font page. Also handles the "Deselect Icons
|
||||
with Strokes" alert.
|
||||
"""
|
||||
self.go_to_page(IcomoonPage.GENERATE_FONT)
|
||||
alert = self.test_for_possible_alert(self.MED_WAIT_IN_SEC)
|
||||
if alert == None:
|
||||
pass # all good
|
||||
elif alert == IcomoonAlerts.DESELECT_ICONS_CONTAINING_STROKES:
|
||||
message = f"One of SVGs contained strokes. This should not happen."
|
||||
raise Exception(message)
|
||||
else:
|
||||
raise Exception(f"Unexpected alert found: {alert}")
|
||||
|
||||
def download_icomoon_fonts(self, zip_path: Path):
|
||||
"""
|
||||
Download the icomoon.zip from icomoon.io. Also take a picture of
|
||||
what the icons look like.
|
||||
:param zip_path: the path to the zip file after it's downloaded.
|
||||
"""
|
||||
print("Downloading Font files...")
|
||||
if self.current_page != IcomoonPage.SELECTION:
|
||||
self.go_to_page(IcomoonPage.SELECTION)
|
||||
|
||||
self.select_all_icons_in_top_set()
|
||||
self.go_to_generate_font_page()
|
||||
|
||||
download_btn = WebDriverWait(self.driver, SeleniumRunner.LONG_WAIT_IN_SEC).until(
|
||||
ec.presence_of_element_located((By.CSS_SELECTOR, "button.btn4 span"))
|
||||
)
|
||||
download_btn.click()
|
||||
if self.wait_for_zip(zip_path):
|
||||
print("Font files downloaded.")
|
||||
else:
|
||||
raise TimeoutError(f"Couldn't find {zip_path} after download button was clicked.")
|
||||
|
||||
def wait_for_zip(self, zip_path: Path) -> bool:
|
||||
"""
|
||||
Wait for the zip file to be downloaded by checking for its existence
|
||||
in the download path. Wait time is self.LONG_WAIT_IN_SEC and check time
|
||||
is 1 sec.
|
||||
:param zip_path: the path to the zip file after it's
|
||||
downloaded.
|
||||
:return: True if the file is found within the allotted time, else
|
||||
False.
|
||||
"""
|
||||
end_time = time.time() + self.LONG_WAIT_IN_SEC
|
||||
while time.time() <= end_time:
|
||||
if zip_path.exists():
|
||||
return True
|
||||
time.sleep(1) # wait so we don't waste sys resources
|
||||
return False
|
100
.github/scripts/build_assets/selenium_runner/PeekSeleniumRunner.py
vendored
Normal file
100
.github/scripts/build_assets/selenium_runner/PeekSeleniumRunner.py
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
from typing import List
|
||||
from pathlib import Path
|
||||
|
||||
from build_assets.selenium_runner.SeleniumRunner import SeleniumRunner
|
||||
from build_assets.selenium_runner.enums import IcomoonPage, IcomoonAlerts
|
||||
|
||||
class PeekSeleniumRunner(SeleniumRunner):
|
||||
def peek(self, svgs: List[str], screenshot_folder: str):
|
||||
"""
|
||||
Upload the SVGs and peek at how Icomoon interpret its SVGs and
|
||||
font versions.
|
||||
:param svgs: a list of svg Paths that we'll upload to icomoon.
|
||||
:param screenshot_folder: the name of the screenshot_folder.
|
||||
:return an array of svgs with strokes as strings. These show which icon
|
||||
contains stroke.
|
||||
"""
|
||||
messages = self.peek_svgs(svgs, screenshot_folder)
|
||||
self.peek_icons(svgs, screenshot_folder)
|
||||
return messages
|
||||
|
||||
def peek_svgs(self, svgs: List[str], screenshot_folder: str):
|
||||
"""
|
||||
Peek at the SVGs provided in svgs. This will look at how Icomoon
|
||||
interprets the SVGs as a font.
|
||||
:param svgs: a list of svg Paths that we'll upload to icomoon.
|
||||
:param screenshot_folder: the name of the screenshot_folder.
|
||||
:return an array of svgs with strokes as strings. These show which icon
|
||||
contains stroke.
|
||||
"""
|
||||
print("Peeking SVGs...")
|
||||
|
||||
import_btn = self.driver.find_element_by_css_selector(
|
||||
SeleniumRunner.GENERAL_IMPORT_BUTTON_CSS
|
||||
)
|
||||
|
||||
svgs_with_strokes = []
|
||||
for i in range(len(svgs)):
|
||||
import_btn.send_keys(svgs[i])
|
||||
print(f"Uploaded {svgs[i]}")
|
||||
|
||||
alert = self.test_for_possible_alert(self.SHORT_WAIT_IN_SEC)
|
||||
if alert == None:
|
||||
pass # all good
|
||||
elif alert == IcomoonAlerts.STROKES_GET_IGNORED_WARNING:
|
||||
print(f"- This icon contains strokes: {svgs[i]}")
|
||||
svg = Path(svgs[i])
|
||||
svgs_with_strokes.append(f"- {svg.name}")
|
||||
self.click_alert_button(self.ALERTS[alert]["buttons"]["DISMISS"])
|
||||
else:
|
||||
raise Exception(f"Unexpected alert found: {alert}")
|
||||
|
||||
self.edit_svg(screenshot_folder, i)
|
||||
|
||||
# take a screenshot of the svgs that were just added
|
||||
self.select_all_icons_in_top_set()
|
||||
new_svgs_path = str(Path(screenshot_folder, "new_svgs.png").resolve())
|
||||
icon_set_xpath = "/html/body/div[4]/div[1]/div[2]/div[1]"
|
||||
icon_set = self.driver.find_element_by_xpath(icon_set_xpath)
|
||||
icon_set.screenshot(new_svgs_path);
|
||||
|
||||
print("Finished peeking the svgs...")
|
||||
return svgs_with_strokes
|
||||
|
||||
def peek_icons(self, svgs: List[str], screenshot_folder: str):
|
||||
"""
|
||||
Peek at the icon versions of the SVGs that were uploaded.
|
||||
:param screenshot_folder: the name of the screenshot_folder.
|
||||
"""
|
||||
print("Begin peeking at the icons...")
|
||||
# ensure all icons in the set is selected.
|
||||
self.select_all_icons_in_top_set()
|
||||
self.go_to_page(IcomoonPage.GENERATE_FONT)
|
||||
alert = self.test_for_possible_alert(self.MED_WAIT_IN_SEC)
|
||||
if alert == None:
|
||||
pass # all good
|
||||
elif alert == IcomoonAlerts.DESELECT_ICONS_CONTAINING_STROKES:
|
||||
self.click_alert_button(self.ALERTS[alert]["buttons"]["CONTINUE"])
|
||||
else:
|
||||
raise Exception(f"Unexpected alert found: {alert}")
|
||||
|
||||
# take an overall screenshot of the icons that were just added
|
||||
# also include the glyph count
|
||||
new_icons_path = str(Path(screenshot_folder, "new_icons.png").resolve())
|
||||
main_content_xpath = "/html/body/div[4]/div[2]/div/div[1]"
|
||||
main_content = self.driver.find_element_by_xpath(main_content_xpath)
|
||||
main_content.screenshot(new_icons_path);
|
||||
|
||||
# go downward so we get the oldest icon first
|
||||
len_ = len(svgs)
|
||||
for i in range(len_, 0, -1):
|
||||
xpath = f'//*[@id="glyphSet0"]/div[{i}]'
|
||||
icon_div = self.driver.find_element_by_xpath(xpath)
|
||||
|
||||
# crop the div out from the screenshot
|
||||
icon_screenshot = str(
|
||||
Path(screenshot_folder, f"new_icon_{len_ - i}.png").resolve()
|
||||
)
|
||||
icon_div.screenshot(icon_screenshot)
|
||||
|
||||
print("Finished peeking the icons...")
|
315
.github/scripts/build_assets/selenium_runner/SeleniumRunner.py
vendored
Normal file
315
.github/scripts/build_assets/selenium_runner/SeleniumRunner.py
vendored
Normal file
@@ -0,0 +1,315 @@
|
||||
from pathlib import Path
|
||||
|
||||
from selenium.webdriver.firefox.webdriver import WebDriver
|
||||
from selenium.webdriver.firefox.options import Options
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as ec
|
||||
from selenium.common.exceptions import TimeoutException as SeleniumTimeoutException
|
||||
|
||||
from build_assets.selenium_runner.enums import IcomoonOptionState, IcomoonPage, IcomoonAlerts
|
||||
|
||||
|
||||
class SeleniumRunner:
|
||||
"""
|
||||
A runner that upload and download Icomoon resources using Selenium.
|
||||
The WebDriver will use Firefox.
|
||||
"""
|
||||
|
||||
"""
|
||||
The long wait time for the driver in seconds.
|
||||
"""
|
||||
LONG_WAIT_IN_SEC = 25
|
||||
|
||||
"""
|
||||
The medium wait time for the driver in seconds.
|
||||
"""
|
||||
MED_WAIT_IN_SEC = 6
|
||||
|
||||
"""
|
||||
The short wait time for the driver in seconds.
|
||||
"""
|
||||
SHORT_WAIT_IN_SEC = 2.5
|
||||
|
||||
"""
|
||||
The short wait time for the driver in seconds.
|
||||
"""
|
||||
BRIEF_WAIT_IN_SEC = 0.6
|
||||
|
||||
"""
|
||||
The Icomoon Url.
|
||||
"""
|
||||
ICOMOON_URL = "https://icomoon.io/app/#/select"
|
||||
|
||||
"""
|
||||
General import button CSS for Icomoon site.
|
||||
"""
|
||||
GENERAL_IMPORT_BUTTON_CSS = "div#file input[type=file]"
|
||||
|
||||
"""
|
||||
Set import button CSS for Icomoon site.
|
||||
"""
|
||||
SET_IMPORT_BUTTON_CSS = "li.file input[type=file]"
|
||||
|
||||
"""
|
||||
The CSS of the tool bar options. There are more but
|
||||
these are the ones that we actually use.
|
||||
"""
|
||||
TOOLBAR_OPTIONS_CSS = {
|
||||
IcomoonOptionState.SELECT: "div.btnBar button i.icon-select",
|
||||
IcomoonOptionState.EDIT: "div.btnBar button i.icon-edit"
|
||||
}
|
||||
|
||||
"""
|
||||
The URL to go to different pages within the Icomoon domain.
|
||||
There are more but these are the ones that we actually use.
|
||||
"""
|
||||
PAGES_URL = {
|
||||
IcomoonPage.SELECTION: ICOMOON_URL,
|
||||
IcomoonPage.GENERATE_FONT: ICOMOON_URL + "/font"
|
||||
}
|
||||
|
||||
"""
|
||||
The different types of alerts that this workflow will encounter.
|
||||
It contains part of the text in the actual alert and buttons
|
||||
available to press. It's up to the user to know what button to
|
||||
press for which alert.
|
||||
"""
|
||||
ALERTS = {
|
||||
IcomoonAlerts.STROKES_GET_IGNORED_WARNING: {
|
||||
"text": "Strokes get ignored when generating fonts or CSH files.",
|
||||
"buttons": {
|
||||
"DISMISS": "Dismiss",
|
||||
}
|
||||
},
|
||||
IcomoonAlerts.REPLACE_OR_REIMPORT_ICON : {
|
||||
"text": "Replace existing icons?",
|
||||
"buttons": {
|
||||
"REPLACE": "Replace",
|
||||
"REIMPORT": "Reimport"
|
||||
}
|
||||
},
|
||||
IcomoonAlerts.DESELECT_ICONS_CONTAINING_STROKES: {
|
||||
"text": "Strokes get ignored when generating fonts.",
|
||||
"buttons": {
|
||||
"DESELECT": "Deselect",
|
||||
"CONTINUE": "Continue"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def __init__(self, download_path: str,
|
||||
geckodriver_path: str, headless: bool):
|
||||
"""
|
||||
Create a SeleniumRunner object.
|
||||
:param download_path: the location where you want to download
|
||||
the icomoon.zip to.
|
||||
:param geckodriver_path: the path to the firefox executable.
|
||||
:param headless: whether to run browser in headless (no UI) mode.
|
||||
"""
|
||||
self.driver = None
|
||||
# default values when we open Icomoon
|
||||
self.current_option_state = IcomoonOptionState.SELECT
|
||||
self.current_page = IcomoonPage.SELECTION
|
||||
self.set_browser_options(download_path, geckodriver_path, headless)
|
||||
|
||||
def set_browser_options(self, download_path: str, geckodriver_path: str,
|
||||
headless: bool):
|
||||
"""
|
||||
Build the WebDriver with Firefox Options allowing downloads and
|
||||
set download to download_path.
|
||||
:param download_path: the location where you want to download
|
||||
:param geckodriver_path: the path to the firefox executable.
|
||||
the icomoon.zip to.
|
||||
:param headless: whether to run browser in headless (no UI) mode.
|
||||
|
||||
:raises AssertionError: if the page title does not contain
|
||||
"IcoMoon App".
|
||||
"""
|
||||
options = Options()
|
||||
allowed_mime_types = "application/zip, application/gzip, application/octet-stream"
|
||||
# disable prompt to download from Firefox
|
||||
options.set_preference("browser.helperApps.neverAsk.saveToDisk", allowed_mime_types)
|
||||
options.set_preference("browser.helperApps.neverAsk.openFile", allowed_mime_types)
|
||||
|
||||
# set the default download path to downloadPath
|
||||
options.set_preference("browser.download.folderList", 2)
|
||||
options.set_preference("browser.download.dir", download_path)
|
||||
options.headless = headless
|
||||
|
||||
print("Activating browser client...")
|
||||
self.driver = WebDriver(options=options, executable_path=geckodriver_path)
|
||||
self.driver.get(self.ICOMOON_URL)
|
||||
assert "IcoMoon App" in self.driver.title
|
||||
# wait until the whole web page is loaded by testing the hamburger input
|
||||
WebDriverWait(self.driver, self.LONG_WAIT_IN_SEC).until(
|
||||
ec.element_to_be_clickable((By.XPATH, "(//i[@class='icon-menu'])[2]"))
|
||||
)
|
||||
print("Accessed icomoon.io")
|
||||
|
||||
def switch_toolbar_option(self, option: IcomoonOptionState):
|
||||
"""
|
||||
Switch the toolbar option to the option argument.
|
||||
:param option: an option from the toolbar of Icomoon.
|
||||
"""
|
||||
if self.current_option_state == option:
|
||||
return
|
||||
|
||||
option_btn = self.driver.find_element_by_css_selector(
|
||||
SeleniumRunner.TOOLBAR_OPTIONS_CSS[option]
|
||||
)
|
||||
option_btn.click()
|
||||
self.current_option_state = option
|
||||
|
||||
def click_hamburger_input(self):
|
||||
"""
|
||||
Click the hamburger input until the pop up menu appears. This
|
||||
method is needed because sometimes, we need to click the hamburger
|
||||
input two times before the menu appears.
|
||||
:return: None.
|
||||
"""
|
||||
top_set_hamburger_input_xpath = '//*[@id="setH2"]/button[1]/i'
|
||||
hamburger_input = self.driver.find_element_by_xpath(
|
||||
top_set_hamburger_input_xpath
|
||||
)
|
||||
|
||||
menu_appear_callback = ec.element_to_be_clickable(
|
||||
(By.CSS_SELECTOR, "h1 ul.menuList2")
|
||||
)
|
||||
|
||||
while not menu_appear_callback(self.driver):
|
||||
hamburger_input.click()
|
||||
|
||||
def test_for_possible_alert(self, wait_period: float) -> IcomoonAlerts:
|
||||
"""
|
||||
Test for the possible alerts that might appear. Return the
|
||||
type of alert if one shows up.
|
||||
:param wait_period: the wait period for the possible alert
|
||||
in seconds.
|
||||
:return: an IcomoonAlerts enum representing the alert that was found.
|
||||
Else, return None.
|
||||
"""
|
||||
try:
|
||||
overlay_div = WebDriverWait(self.driver, wait_period, 0.15).until(
|
||||
ec.element_to_be_clickable(
|
||||
(By.XPATH, "//div[@class='overlay']"))
|
||||
)
|
||||
alert_message = overlay_div.text
|
||||
for alert in self.ALERTS.keys():
|
||||
if self.ALERTS[alert]["text"] in alert_message:
|
||||
return alert
|
||||
|
||||
return IcomoonAlerts.UNKNOWN
|
||||
except SeleniumTimeoutException:
|
||||
return None # nothing found => everything is good
|
||||
|
||||
def click_alert_button(self, btn_text: str):
|
||||
"""
|
||||
Click the button in the alert that matches the button text.
|
||||
:param btn_text: the text that the alert's button will have.
|
||||
"""
|
||||
try:
|
||||
button = WebDriverWait(self.driver, self.BRIEF_WAIT_IN_SEC, 0.15).until(
|
||||
ec.element_to_be_clickable(
|
||||
(By.XPATH, f"//div[@class='overlay']//button[text()='{btn_text}']"))
|
||||
)
|
||||
button.click()
|
||||
except SeleniumTimeoutException:
|
||||
return None # nothing found => everything is good
|
||||
|
||||
def edit_svg(self, screenshot_folder: str=None, index: int=None):
|
||||
"""
|
||||
Edit the SVG. This include removing the colors and take a
|
||||
snapshot if needed.
|
||||
:param screenshot_folder: a string or Path object. Point to
|
||||
where we store the screenshots. If truthy, take a screenshot
|
||||
and save it here.
|
||||
:param index: the index of the icon in its containing list.
|
||||
Used to differentiate the screenshots. Must be truthy if
|
||||
screenshot_folder is a truthy value.
|
||||
"""
|
||||
self.switch_toolbar_option(IcomoonOptionState.EDIT)
|
||||
self.click_latest_icons_in_top_set(1)
|
||||
|
||||
# strip the colors from the SVG.
|
||||
# This is because some SVG have colors in them and we don't want to
|
||||
# force contributors to remove them in case people want the colored SVGs.
|
||||
# The color removal is also necessary so that the Icomoon-generated
|
||||
# icons fit within one font symbol/ligiature.
|
||||
try:
|
||||
color_tab = WebDriverWait(self.driver, self.BRIEF_WAIT_IN_SEC).until(
|
||||
ec.element_to_be_clickable((By.CSS_SELECTOR, "div.overlayWindow i.icon-droplet"))
|
||||
)
|
||||
color_tab.click()
|
||||
|
||||
remove_color_btn = self.driver \
|
||||
.find_element_by_css_selector("div.overlayWindow i.icon-droplet-cross")
|
||||
remove_color_btn.click()
|
||||
except SeleniumTimeoutException:
|
||||
pass # do nothing cause sometimes, the color tab doesn't appear in the site
|
||||
|
||||
if screenshot_folder != None and index != None:
|
||||
edit_screen_selector = "div.overlay div.overlayWindow"
|
||||
screenshot_path = str(
|
||||
Path(screenshot_folder, f"new_svg_{index}.png").resolve()
|
||||
)
|
||||
edit_screen = self.driver.find_element_by_css_selector(
|
||||
edit_screen_selector)
|
||||
edit_screen.screenshot(screenshot_path)
|
||||
print("Took screenshot of svg and saved it at " + screenshot_path)
|
||||
|
||||
close_btn = self.driver \
|
||||
.find_element_by_css_selector("div.overlayWindow i.icon-close")
|
||||
close_btn.click()
|
||||
|
||||
def click_latest_icons_in_top_set(self, amount: int):
|
||||
"""
|
||||
Click on the latest icons in the top set based on the amount passed in.
|
||||
This is state option agnostic (doesn't care if it's in SELECT or EDIT mode).
|
||||
:param amount: the amount of icons to click on from top left of the top
|
||||
set. Must be > 0.
|
||||
"""
|
||||
icon_base_xpath = '//div[@id="set0"]//mi-box[{}]//div'
|
||||
for i in range(1, amount + 1):
|
||||
icon_xpath = icon_base_xpath.format(i)
|
||||
latest_icon = self.driver.find_element_by_xpath(icon_xpath)
|
||||
latest_icon.click()
|
||||
|
||||
def select_all_icons_in_top_set(self):
|
||||
"""
|
||||
Select all the svgs in the top most (latest) set.
|
||||
"""
|
||||
self.click_hamburger_input()
|
||||
select_all_button = WebDriverWait(self.driver, self.LONG_WAIT_IN_SEC).until(
|
||||
ec.element_to_be_clickable((By.XPATH, "//button[text()='Select All']"))
|
||||
)
|
||||
select_all_button.click()
|
||||
|
||||
def deselect_all_icons_in_top_set(self):
|
||||
"""
|
||||
Select all the svgs in the top most (latest) set.
|
||||
"""
|
||||
self.click_hamburger_input()
|
||||
select_all_button = WebDriverWait(self.driver, self.LONG_WAIT_IN_SEC).until(
|
||||
ec.element_to_be_clickable((By.XPATH, "//button[text()='Deselect']"))
|
||||
)
|
||||
select_all_button.click()
|
||||
|
||||
def go_to_page(self, page: IcomoonPage):
|
||||
"""
|
||||
Go to the specified page in Icomoon. This used the URL rather than UI
|
||||
elements due to the inconsistent UI rendering.
|
||||
:param page: a valid page that can be accessed in Icomoon.
|
||||
"""
|
||||
if self.current_page == page:
|
||||
return
|
||||
|
||||
self.driver.get(self.PAGES_URL[page])
|
||||
self.current_page = page
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
Close the SeleniumRunner instance.
|
||||
"""
|
||||
print("Closing down SeleniumRunner...")
|
||||
self.driver.quit()
|
28
.github/scripts/build_assets/selenium_runner/enums.py
vendored
Normal file
28
.github/scripts/build_assets/selenium_runner/enums.py
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class IcomoonOptionState(Enum):
|
||||
"""
|
||||
The state of the Icomoon toolbar options
|
||||
"""
|
||||
SELECT = 0,
|
||||
EDIT = 1
|
||||
|
||||
|
||||
class IcomoonPage(Enum):
|
||||
"""
|
||||
The available pages on the Icomoon website.
|
||||
"""
|
||||
SELECTION = 0,
|
||||
GENERATE_FONT = 1
|
||||
|
||||
|
||||
class IcomoonAlerts(Enum):
|
||||
"""
|
||||
The alerts that Icomoon displayed to the user. There
|
||||
could be more but these are the ones we usually see.
|
||||
"""
|
||||
STROKES_GET_IGNORED_WARNING = 0,
|
||||
REPLACE_OR_REIMPORT_ICON = 1,
|
||||
DESELECT_ICONS_CONTAINING_STROKES = 2,
|
||||
UNKNOWN = 3
|
14
.github/scripts/build_assets/util.py
vendored
14
.github/scripts/build_assets/util.py
vendored
@@ -56,9 +56,13 @@ def find_object_added_in_pr(icons: List[dict], pr_title: str):
|
||||
:raise If no object can be found, raise an Exception.
|
||||
"""
|
||||
try:
|
||||
pattern = re.compile(r"(?<=^new icon: )\w+ (?=\(.+\))", re.I)
|
||||
icon_name = pattern.findall(pr_title)[0].lower().strip() # should only have one match
|
||||
icon = [icon for icon in icons if icon["name"] == icon_name][0]
|
||||
pattern = re.compile(r"(?<=^new icon: )\w+ (?=\(.+\))|(?<=^update icon: )\w+ (?=\(.+\))", re.I)
|
||||
icon_name_index = 0
|
||||
icon_name = pattern.findall(pr_title)[icon_name_index].lower().strip() # should only have one match
|
||||
icon = [icon for icon in icons if icon["name"] == icon_name][0]
|
||||
return icon
|
||||
except IndexError: # there are no match in the findall()
|
||||
raise Exception("Couldn't find an icon matching the name in the PR title.")
|
||||
except IndexError as e: # there are no match in the findall()
|
||||
print(e)
|
||||
message = "util.find_object_added_in_pr: Couldn't find an icon matching the name in the PR title.\n" \
|
||||
f"PR title is: '{pr_title}'"
|
||||
raise Exception(message)
|
||||
|
Reference in New Issue
Block a user