Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/PULL_REQUEST_TEMPLATE/new_icon.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
<!-- Tick the checkboxes to ensure you've done everything correctly -->
- [ ] PR does not match another non-stale PR currently opened
- [ ] PR name matches the format *new icon: <i>Icon name</i> (<i>versions separated by comma</i>)*. More details [here](https://github.com/devicons/devicon/wiki/Overview-on-Submitting-Icons)
- [ ] PR's base is the `develop` branch.
- [ ] PR's base is the `master` branch.
- [ ] Your icons are inside a folder as seen [here](https://github.com/devicons/devicon/wiki/Organizing-SVGs)
- [ ] SVG matches the standards laid out [here](https://github.com/devicons/devicon/wiki/SVG-Standards)
- [ ] A new object is added in the `devicon.json` file at the correct alphabetic position as seen [here](https://github.com/devicons/devicon/wiki/Updating-%60devicon.json%60)

<!-- Refer to the [contributing](https://github.com/devicons/devicon/blob/develop/CONTRIBUTING.md#contributing-to-devicon) guidelines for more details. -->
<!-- Refer to the [contributing](https://github.com/devicons/devicon/blob/master/CONTRIBUTING.md#contributing-to-devicon) guidelines for more details. -->

**This PR closes NONE**
<!-- List issues that this PR would close above. Ex: This PR closes #1, #2, #3. -->
Expand Down
78 changes: 0 additions & 78 deletions .github/scripts/build_assets/api_handler.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import requests
import sys
import re
from typing import List
from io import FileIO


Expand Down Expand Up @@ -105,83 +104,6 @@ def find_all_authors(pull_req_data, token):
return ", ".join(["@" + author for author in list(authors)])


def label_issues(token: str, issues: List[str], labels: List[str]):
"""
Label the issues specified with the label specified.
:param token: the GitHub API token.
:param issues: the issue numbers (as str) that we are labelling.
:param labels: the labels that we are labelling.
"""
headers = {
"Authorization": f"token {token}",
"accept": "application/vnd.github.v3+json"
}
url = base_url + "issues/{}/labels"
for issue in issues:
body = {
"labels": labels
}
response = requests.post(url.format(issue), headers=headers, json=body)
if not response:
raise Exception(f"Can't label the Issue provided. Issue: {issue}, labels: {labels}, API response: " + response.text)
else:
print(f"Successfully labelled issue {issue}")


def close_issues(token: str, issues: List[str]):
"""
Close issues.
:param token: the GitHub API token.
:param issues: the issue numbers (as str) that we are labelling.
"""
headers = {
"Authorization": f"token {token}",
"accept": "application/vnd.github.v3+json"
}
url = base_url + "issues/{}"
body = {
"state": "closed"
}
for issue in issues:
response = requests.patch(url.format(issue), headers=headers, json=body)
if not response:
raise Exception(f"Can't close Issue provided. Issue: {issue}, API response: " + response.text)
else:
print(f"Successfully closed issue {issue}")


def get_issues_by_labels(token: str, labels: List[str]):
"""
Get a list of issues based on their labels.
:param token: the GitHub API token.
:param labels: the labels that we are labelling.
"""
url = base_url + "issues?per_page=100&labels={}&page={}"
headers = {
"Authorization": f"token {token}",
"accept": "application/vnd.github.v3+json"
}
issues = []
done = False
page_num = 1
while not done:
response = requests.get(url.format(",".join(labels), page_num), headers=headers)
if not response:
raise Exception(f"Can't access API. Can't get issues for labels: {labels}, API response: " + response.text)
else:
results = response.json()
if len(results) < 100:
done = True # we are done
else:
page_num += 1 # page is full => might need to check another page

# GitHub API also returns PRs for issues queries => have to check
issues_only = [issue for issue in results if issue.get("pull_request") is None]
issues.extend(issues_only)

return issues


def get_pr_by_number(token: str, pr_num: str):
url = base_url + "pulls/" + pr_num
headers = {
Expand Down
32 changes: 13 additions & 19 deletions .github/scripts/build_assets/arg_getters.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from build_assets.PathResolverAction import PathResolverAction


def get_selenium_runner_args(has_token=True, peek_mode=False):
def get_selenium_runner_args(has_token=True, peek_mode=False, build_since=False):
"""
Get the commandline arguments for the icomoon_peek.py and
icomoon_build.py.
Expand Down Expand Up @@ -34,11 +34,16 @@ def get_selenium_runner_args(has_token=True, peek_mode=False):
action=PathResolverAction)

if peek_mode:
parser.add_argument("pr_title",
help="The title of the PR that we are peeking at")
parser.add_argument("--base",
help="Commit to diff against to find the PR's changes (HEAD^1 in a PR merge commit)",
default="HEAD^1")
if has_token != False:
parser.add_argument("token",
help="The GitHub token to access the GitHub REST API.")
if build_since:
parser.add_argument("--since",
help="Commit to diff against to find updated SVGs (usually the last build commit)",
default=None)

return parser.parse_args()

Expand All @@ -50,7 +55,7 @@ def get_check_icon_pr_args():
parser = ArgumentParser(description="Check the SVGs to ensure their attributes are correct. Run whenever a PR is opened")

parser.add_argument("pr_title",
help="The title of the PR that we are peeking at")
help="The title of the PR that we are checking")

parser.add_argument("icons_folder_path",
help="The path to the icons folder",
Expand All @@ -60,6 +65,10 @@ def get_check_icon_pr_args():
help="The path to the devicon.json",
action=PathResolverAction)

parser.add_argument("--base",
help="Commit to diff against to find the PR's changes (HEAD^1 in a PR merge commit)",
default="HEAD^1")

return parser.parse_args()


Expand All @@ -71,18 +80,3 @@ def get_release_message_args():
parser.add_argument("token",
help="The GitHub token to access the GitHub REST API.")
return parser.parse_args()


def get_in_develop_labeler_args():
"""
Get the commandline arguments for in_develop_labeler.py.
"""
parser = ArgumentParser(description="Parse the PR body to find the issue(s) we are labelling.")
parser.add_argument("token",
help="The GitHub token to access the GitHub REST API.",
type=str)

parser.add_argument("pr_num",
help="The PR's number",
type=str)
return parser.parse_args()
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@ 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
# Deselect all existing icons BEFORE uploading new SVGs.
# Newly uploaded SVGs are auto-selected by icomoon.io, so after this
# only the new icons will be selected — giving clean "new icons only" screenshots.
# download_icomoon_fonts() calls select_all before generating the font,
# so the full icon set is always used for font generation.
self.deselect_all_icons_in_top_set()
self.upload_svgs(svgs, screenshot_folder)
self.take_icon_screenshot(screenshot_folder)
Expand Down Expand Up @@ -90,10 +93,10 @@ def upload_svgs(self, svgs: List[str], screenshot_folder: str):
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
# Take a screenshot of only the newly uploaded SVGs.
# Because we called deselect_all before uploading, only the new icons
# are selected at this point — no need to call select_all here.
self.switch_toolbar_option(IcomoonOptionState.SELECT)
self.select_all_icons_in_top_set()
new_svgs_path = str(Path(screenshot_folder, "new_svgs.png").resolve())
self.driver.save_screenshot(new_svgs_path)

Expand All @@ -117,7 +120,8 @@ def take_icon_screenshot(self, screenshot_folder: str):

# wait a bit for all the icons to load before we take a pic
time.sleep(SeleniumRunner.MED_WAIT_IN_SEC)
main_content.screenshot(new_icons_path)
# Use full-page screenshot to avoid canvas taint restrictions
self.driver.save_screenshot(new_icons_path)
print("Saved screenshot of the new icons...", file=self.log_output)

def go_to_generate_font_page(self):
Expand Down Expand Up @@ -145,6 +149,9 @@ def download_icomoon_fonts(self, zip_path: Path):
if self.current_page != IcomoonPage.SELECTION:
self.go_to_page(IcomoonPage.SELECTION)

# select_all is required here — after deselect_all + new SVG uploads,
# only the new icons are selected. We must select everything before
# generating the font or the existing icons will be missing from the output.
self.select_all_icons_in_top_set()
self.go_to_generate_font_page()

Expand Down
52 changes: 16 additions & 36 deletions .github/scripts/build_assets/selenium_runner/PeekSeleniumRunner.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ def peek(self, svgs: List[str], screenshot_folder: str, icon_info: dict):
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.
:param screenshot_folder: the name of the screenshot_folder.
:param icon_info: a dictionary containing info on an icon. Taken from the devicon.json.
: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(screenshot_folder, icon_info)
messages = self.peek_svgs(svgs, screenshot_folder)
self.peek_icons(screenshot_folder, icon_info, len(svgs))
return messages

def peek_svgs(self, svgs: List[str], screenshot_folder: str):
Expand Down Expand Up @@ -52,24 +52,19 @@ def peek_svgs(self, svgs: List[str], screenshot_folder: str):

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...", file=self.log_output)
return svgs_with_strokes

def peek_icons(self, screenshot_folder: str, icon_info: dict):
def peek_icons(self, screenshot_folder: str, icon_info: dict, num_svgs: int):
"""
Peek at the icon versions of the SVGs that were uploaded.
:param screenshot_folder: the name of the screenshot_folder.
:param screenshot_folder: the name of the screenshot_folder.
:param icon_info: a dictionary containing info on an icon. Taken from the devicon.json.
:param num_svgs: the number of SVGs that were uploaded (used to guard against
extra icons from icomoon's default sets leaking into the font preview).
"""
print("Begin peeking at the icons...", file=self.log_output)
# ensure all icons in the set is selected.
# Select only the newly uploaded icons in the top set before generating font.
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)
Expand All @@ -80,43 +75,28 @@ def peek_icons(self, screenshot_folder: str, icon_info: dict):
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 in reverse order so we get the oldest icon first
# Collect glyph divs; reverse so screenshots are ordered oldest-first
# (matching the SVG upload order). Cap at num_svgs as a safety guard
# in case any icons from icomoon's default sets bleed into glyphSet0.
icon_divs_xpath = f'//div[@id="glyphSet0"]/div'
icon_divs = self.driver.find_elements_by_xpath(icon_divs_xpath)
icon_divs.reverse()
i = 0
for icon_div in icon_divs:
if not icon_div.is_displayed():
continue
all_icon_divs = self.driver.find_elements_by_xpath(icon_divs_xpath)
all_icon_divs.reverse()
icon_divs = [d for d in all_icon_divs if d.is_displayed()][:num_svgs]

for i, icon_div in enumerate(icon_divs):
icon_screenshot = str(
Path(screenshot_folder, f"new_icon_{i}.png").resolve()
)
icon_div.screenshot(icon_screenshot)

i += 1

# test the colors
style = "#glyphSet0 span:first-of-type {color: " + icon_info["color"] + "}"
script = f"document.styleSheets[0].insertRule('{style}', 0)"
self.driver.execute_script(script)
i = 0
for icon_div in icon_divs:
if not icon_div.is_displayed():
continue

for i, icon_div in enumerate(icon_divs):
icon_screenshot = str(
Path(screenshot_folder, f"new_colored_icon_{i}.png").resolve()
)
icon_div.screenshot(icon_screenshot)

i += 1

print("Finished peeking the icons...", file=self.log_output)
33 changes: 27 additions & 6 deletions .github/scripts/build_assets/selenium_runner/SeleniumRunner.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,29 @@ def set_browser_options(self, download_path: str, geckodriver_path: str,
self.driver = self.create_driver_instance(options, geckodriver_path)

self.driver.get(self.ICOMOON_URL)
# 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", file=self.log_output)

# wait until the whole web page is loaded
# Try original selector first, fall back to a more generic check
try:
WebDriverWait(self.driver, self.MED_WAIT_IN_SEC).until(
ec.element_to_be_clickable((By.XPATH, "(//i[@class='icon-menu'])[2]"))
)
print("Accessed icomoon.io (via icon-menu selector)", file=self.log_output)
except SeleniumTimeoutException:
print("icon-menu selector not found, trying fallback selectors...", file=self.log_output)
# Fallback: wait up to 90s for any file input (the core upload mechanism)
# icomoon.io can be slow to initialize the Angular app
try:
WebDriverWait(self.driver, 90).until(
ec.presence_of_element_located((By.CSS_SELECTOR, "input[type='file']"))
)
print("Accessed icomoon.io (via file input fallback)", file=self.log_output)
except SeleniumTimeoutException:
self.driver.save_screenshot("./screenshots/icomoon_load_failed.png")
raise Exception(
f"Could not load icomoon.io after 90s. Page title: '{self.driver.title}'. "
"The page may have changed structure or blocked headless browsing."
)

def create_driver_instance(self, options: Options, geckodriver_path: str):
"""
Expand Down Expand Up @@ -227,7 +245,10 @@ def click_hamburger_input(self):
input two times before the menu appears.
:return: None.
"""
top_set_hamburger_input_xpath = '//*[@id="setH2"]/button[1]/i'
# Target the first icon-menu button inside any h1 set header.
# icomoon always renders the most recently uploaded (top) set first in the DOM,
# so [1] reliably selects it without relying on a hardcoded element ID.
top_set_hamburger_input_xpath = '(//h1//i[@class="icon-menu"])[1]'
hamburger_input = self.driver.find_element_by_xpath(
top_set_hamburger_input_xpath
)
Expand Down
Loading
Loading