diff --git a/.github/workflows/devRun.yml b/.github/workflows/devRun.yml index 667e73025d..374870cff3 100644 --- a/.github/workflows/devRun.yml +++ b/.github/workflows/devRun.yml @@ -10,8 +10,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: psf/black@stable - - uses: isort/isort-action@v1 - name: Set up Python uses: actions/setup-python@v5 with: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e154624eac..b715f0c39e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,21 +8,14 @@ repos: - id: check-xml - id: end-of-file-fixer - id: trailing-whitespace - - id: check-docstring-first - - id: name-tests-test - - id: file-contents-sorter - - id: pretty-format-json - args: [ --autofix ] - id: check-ast - - id: check-builtin-literals - id: check-case-conflict - id: check-executables-have-shebangs - id: check-merge-conflict - id: check-shebang-scripts-are-executable - - id: debug-statements - id: detect-private-key - id: no-commit-to-branch - args: [ '--branch', 'main' ] + args: ['--branch', 'main'] - repo: https://github.com/python-jsonschema/check-jsonschema rev: 0.32.1 hooks: @@ -35,33 +28,14 @@ repos: - id: conventional-pre-commit stages: [commit-msg] args: [] - - repo: https://github.com/psf/black - rev: 25.1.0 - hooks: - - id: black - language_version: python3 - args: [ '--config', 'pyproject.toml' ] - - repo: https://github.com/PyCQA/autoflake - rev: v2.3.1 - hooks: - - id: autoflake - args: - [ - '--in-place', - '--remove-unused-variable', - '--remove-all-unused-imports', - '--expand-star-imports', - '--ignore-init-module-imports', - ] - - repo: https://github.com/PyCQA/isort - rev: 6.0.1 - hooks: - - id: isort - args: [ '--settings-file', 'pyproject.toml' ] - - repo: https://github.com/asottile/pyupgrade - rev: v3.19.1 - hooks: - - id: pyupgrade + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.11.4 + hooks: + - id: ruff + args: [ --fix ] + continue_on_error: true + - id: ruff-format + continue_on_error: true - repo: https://github.com/codespell-project/codespell rev: v2.4.1 hooks: @@ -72,11 +46,10 @@ repos: rev: v0.24.1 hooks: - id: validate-pyproject - # Optional extra validations from SchemaStore: - additional_dependencies: [ "validate-pyproject-schema-store[all]" ] + additional_dependencies: ["validate-pyproject-schema-store[all]"] - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks rev: v2.14.0 hooks: - id: pretty-format-toml exclude: poetry.lock - args: [ --autofix ] + args: [--autofix] diff --git a/.github/README.md b/README.md similarity index 96% rename from .github/README.md rename to README.md index af57134f61..af7335cd1f 100644 --- a/.github/README.md +++ b/README.md @@ -4,8 +4,8 @@ ![YouTube Channel](https://img.shields.io/youtube/channel/subscribers/UCQjS-eoKl0a1nuP_dvpLsjQ?label=YouTube%20Channel) ![dev run](https://github.com/nirtal85/Selenium-Python-Example/actions/workflows/devRun.yml/badge.svg) ![nightly](https://github.com/nirtal85/Selenium-Python-Example/actions/workflows/nightly.yml/badge.svg) -[![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/) -[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv) +[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) @@ -58,7 +58,7 @@ git clone https://github.com/nirtal85/Selenium-Python-Example.git cd selenium-python-example ``` -### Create and activate a virtual environment then Install project dependencies +### Create and activate a virtual environment then Install project dependencies #### For Windows: ```bash @@ -72,7 +72,7 @@ uv pip sync uv.lock ```bash python3 -m pip install uv uv venv -source .venv/bin/activate +source .venv/bin/activate uv pip sync uv.lock ``` diff --git a/pages/about_page.py b/pages/about_page.py index acbcb97838..ae3656b008 100644 --- a/pages/about_page.py +++ b/pages/about_page.py @@ -1,5 +1,3 @@ -from typing import Tuple - import allure from selenium.webdriver.common.by import By @@ -9,8 +7,8 @@ class AboutPage(BasePage): """About page - The first page that appears when navigating to base URL""" - LOGIN_LINK: Tuple[str, str] = (By.CSS_SELECTOR, ".login") - REGISTER_LINK: Tuple[str, str] = (By.CSS_SELECTOR, ".register") + LOGIN_LINK: tuple[str, str] = (By.CSS_SELECTOR, ".login") + REGISTER_LINK: tuple[str, str] = (By.CSS_SELECTOR, ".register") def __init__(self, driver, wait): super().__init__(driver, wait) diff --git a/pages/base_page.py b/pages/base_page.py index 18422afa7e..b41078ae3a 100644 --- a/pages/base_page.py +++ b/pages/base_page.py @@ -1,20 +1,16 @@ -from typing import Tuple, Union - from deprecated import deprecated from selenium.common.exceptions import NoSuchElementException from selenium.webdriver import ActionChains, Chrome, Edge, Firefox from selenium.webdriver.remote.webelement import WebElement from selenium.webdriver.support import expected_conditions -from selenium.webdriver.support.expected_conditions import ( - StaleElementReferenceException, -) +from selenium.webdriver.support.expected_conditions import StaleElementReferenceException from selenium.webdriver.support.wait import WebDriverWait class BasePage: """Wrapper for selenium operations.""" - def __init__(self, driver: Union[Chrome, Firefox, Edge], wait: WebDriverWait): + def __init__(self, driver: Chrome | Firefox | Edge, wait: WebDriverWait): self.driver = driver self.wait = wait @@ -39,42 +35,39 @@ def set_geo_location(self, latitude: float, longitude: float) -> None: """Sets the geolocation for the web browser using the Chrome DevTools Protocol (CDP). - Parameters: + Parameters + ---------- - latitude (float): The latitude of the desired geolocation. - longitude (float): The longitude of the desired geolocation. - Returns: + Returns + ------- None Note: This method uses the Chrome DevTools Protocol (CDP) to override the geolocation in the web browser, allowing simulation of a specific geographic location for testing purposes. The accuracy is set to 1 for simplicity in this method. + """ self.driver.execute_cdp_cmd( "Emulation.setGeolocationOverride", {"latitude": latitude, "longitude": longitude, "accuracy": 1}, ) - def click(self, locator: Tuple[str, str]) -> None: - el: WebElement = self.wait.until( - expected_conditions.element_to_be_clickable(locator) - ) + def click(self, locator: tuple[str, str]) -> None: + el: WebElement = self.wait.until(expected_conditions.element_to_be_clickable(locator)) self._highlight_element(el, "green") el.click() - def fill_text(self, locator: Tuple[str, str], txt: str) -> None: - el: WebElement = self.wait.until( - expected_conditions.element_to_be_clickable(locator) - ) + def fill_text(self, locator: tuple[str, str], txt: str) -> None: + el: WebElement = self.wait.until(expected_conditions.element_to_be_clickable(locator)) el.clear() self._highlight_element(el, "green") el.send_keys(txt) - def clear_text(self, locator: Tuple[str, str]) -> None: - el: WebElement = self.wait.until( - expected_conditions.element_to_be_clickable(locator) - ) + def clear_text(self, locator: tuple[str, str]) -> None: + el: WebElement = self.wait.until(expected_conditions.element_to_be_clickable(locator)) el.clear() def scroll_to_bottom(self) -> None: @@ -84,10 +77,8 @@ def submit(self, webelement: WebElement) -> None: self._highlight_element(webelement, "green") webelement.submit() - def get_text(self, locator: Tuple[str, str]) -> str: - el: WebElement = self.wait.until( - expected_conditions.visibility_of_element_located(locator) - ) + def get_text(self, locator: tuple[str, str]) -> str: + el: WebElement = self.wait.until(expected_conditions.visibility_of_element_located(locator)) self._highlight_element(el, "green") return el.text diff --git a/pages/forgot_password_page.py b/pages/forgot_password_page.py index 3a94741a10..1dfee6d8da 100644 --- a/pages/forgot_password_page.py +++ b/pages/forgot_password_page.py @@ -1,5 +1,3 @@ -from typing import Tuple - import allure from selenium.webdriver.common.by import By @@ -7,14 +5,14 @@ class ForgotPasswordPage(BasePage): - EMAIL_FIELD: Tuple[str, str] = (By.CSS_SELECTOR, "[name=email]") - SEND_PASSWORD_RESET_LINK_BUTTON: Tuple[str, str] = ( + EMAIL_FIELD: tuple[str, str] = (By.CSS_SELECTOR, "[name=email]") + SEND_PASSWORD_RESET_LINK_BUTTON: tuple[str, str] = ( By.CSS_SELECTOR, "[type=submit]", ) - ERROR_MSG: Tuple[str, str] = (By.CSS_SELECTOR, ".alert-danger") - SUCCESS_MSG: Tuple[str, str] = (By.CSS_SELECTOR, ".alert-success") - PAGE_TITLE: Tuple[str, str] = (By.CSS_SELECTOR, ".e-form-heading") + ERROR_MSG: tuple[str, str] = (By.CSS_SELECTOR, ".alert-danger") + SUCCESS_MSG: tuple[str, str] = (By.CSS_SELECTOR, ".alert-success") + PAGE_TITLE: tuple[str, str] = (By.CSS_SELECTOR, ".e-form-heading") def __init__(self, driver, wait): super().__init__(driver, wait) diff --git a/pages/login_page.py b/pages/login_page.py index a3119297d3..d74cd94c65 100644 --- a/pages/login_page.py +++ b/pages/login_page.py @@ -1,5 +1,3 @@ -from typing import Tuple - import allure from selenium.webdriver.common.by import By @@ -9,12 +7,12 @@ class LoginPage(TopMenuBar): """Login Page.""" - USERNAME_FIELD: Tuple[str, str] = (By.CSS_SELECTOR, "input[type=email]") - PASSWORD_FIELD: Tuple[str, str] = (By.CSS_SELECTOR, "input[type=password]") - LOGIN_BUTTON: Tuple[str, str] = (By.CSS_SELECTOR, "button[type=submit]") - LOGIN_ERROR_MESSAGE: Tuple[str, str] = (By.CSS_SELECTOR, "div.alert-danger") - PAGE_TITLE: Tuple[str, str] = (By.CSS_SELECTOR, ".e-form-heading") - FORGOT_PASSWORD_LINK: Tuple[str, str] = ( + USERNAME_FIELD: tuple[str, str] = (By.CSS_SELECTOR, "input[type=email]") + PASSWORD_FIELD: tuple[str, str] = (By.CSS_SELECTOR, "input[type=password]") + LOGIN_BUTTON: tuple[str, str] = (By.CSS_SELECTOR, "button[type=submit]") + LOGIN_ERROR_MESSAGE: tuple[str, str] = (By.CSS_SELECTOR, "div.alert-danger") + PAGE_TITLE: tuple[str, str] = (By.CSS_SELECTOR, ".e-form-heading") + FORGOT_PASSWORD_LINK: tuple[str, str] = ( By.CSS_SELECTOR, "[href='https://app.involve.me/password/reset']", ) diff --git a/pages/project_edit_page.py b/pages/project_edit_page.py index df6564ef72..b6d48104f6 100644 --- a/pages/project_edit_page.py +++ b/pages/project_edit_page.py @@ -1,5 +1,3 @@ -from typing import Tuple - import allure from selenium.webdriver.common.by import By @@ -9,20 +7,20 @@ class ProjectEditPage(BasePage): """Project Edit page - the page where adding to and editing projects is done""" - _PROJECT_NAME_FIELD: Tuple[str, str] = (By.CSS_SELECTOR, "input#project-name") - _THANK_YOU_PAGE_TYPE_BUTTON: Tuple[str, str] = ( + _PROJECT_NAME_FIELD: tuple[str, str] = (By.CSS_SELECTOR, "input#project-name") + _THANK_YOU_PAGE_TYPE_BUTTON: tuple[str, str] = ( By.CSS_SELECTOR, "[for=select-single-outcome]", ) - _OUTCOME_PAGES_TYPE_BUTTON: Tuple[str, str] = ( + _OUTCOME_PAGES_TYPE_BUTTON: tuple[str, str] = ( By.CSS_SELECTOR, "[for=select-outcomes]", ) - _START_EDITING_BUTTON: Tuple[str, str] = ( + _START_EDITING_BUTTON: tuple[str, str] = ( By.CSS_SELECTOR, ".swal-button.swal-button--confirm", ) - _SAVE_AND_EXIT_BUTTON: Tuple[str, str] = (By.CSS_SELECTOR, ".e-close.nav-link") + _SAVE_AND_EXIT_BUTTON: tuple[str, str] = (By.CSS_SELECTOR, ".e-close.nav-link") def __init__(self, driver, wait): super().__init__(driver, wait) diff --git a/pages/project_type_page.py b/pages/project_type_page.py index 2b68bd5683..f73972fe34 100644 --- a/pages/project_type_page.py +++ b/pages/project_type_page.py @@ -1,5 +1,3 @@ -from typing import Tuple - import allure from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions @@ -10,8 +8,8 @@ class ProjectTypePage(TopNavigateBar): """Project Type page - where one can choose which kind of templates to work with""" - _START_FROM_SCRATCH_BUTTON: Tuple[str, str] = (By.CSS_SELECTOR, ".blank div.icon") - _PROJECTS_BLOCK: Tuple[str, str] = ( + _START_FROM_SCRATCH_BUTTON: tuple[str, str] = (By.CSS_SELECTOR, ".blank div.icon") + _PROJECTS_BLOCK: tuple[str, str] = ( By.CSS_SELECTOR, "#app-layout div:nth-child(3) .title", ) diff --git a/pages/projects_page.py b/pages/projects_page.py index abc3210d98..5e17d9c582 100644 --- a/pages/projects_page.py +++ b/pages/projects_page.py @@ -1,5 +1,3 @@ -from typing import Tuple - import allure from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions @@ -11,58 +9,58 @@ class ProjectsPage(TopNavigateBar): """Projects page - Where projects are added and edited""" - _START_BUTTON: Tuple[str, str] = (By.CSS_SELECTOR, "#app .px-4 a") - _CREATE_NEW_WORKSPACE_BUTTON: Tuple[str, str] = ( + _START_BUTTON: tuple[str, str] = (By.CSS_SELECTOR, "#app .px-4 a") + _CREATE_NEW_WORKSPACE_BUTTON: tuple[str, str] = ( By.CSS_SELECTOR, ".font-medium button", ) - _WORKSPACE_EDIT_BUTTON: Tuple[str, str] = ( + _WORKSPACE_EDIT_BUTTON: tuple[str, str] = ( By.CSS_SELECTOR, "[data-icon='chevron-down']", ) - _RENAME_WORKSPACE_BUTTON: Tuple[str, str] = ( + _RENAME_WORKSPACE_BUTTON: tuple[str, str] = ( By.CSS_SELECTOR, ".mr-3 .hover\\:bg-gray-600", ) - _DELETE_WORKSPACE_BUTTON: Tuple[str, str] = (By.CSS_SELECTOR, ".mr-3 .text-red-600") - _RENAME_FIELD: Tuple[str, str] = (By.CSS_SELECTOR, ".vue-portal-target input") - _CONFIRMATION_BUTTON: Tuple[str, str] = (By.CSS_SELECTOR, "#confirm-create-button") - _NEW_WORKSPACE_NAME_FIELD: Tuple[str, str] = ( + _DELETE_WORKSPACE_BUTTON: tuple[str, str] = (By.CSS_SELECTOR, ".mr-3 .text-red-600") + _RENAME_FIELD: tuple[str, str] = (By.CSS_SELECTOR, ".vue-portal-target input") + _CONFIRMATION_BUTTON: tuple[str, str] = (By.CSS_SELECTOR, "#confirm-create-button") + _NEW_WORKSPACE_NAME_FIELD: tuple[str, str] = ( By.CSS_SELECTOR, "[placeholder='Workspace name']", ) - _DELETE_WORKSPACE_FIELD: Tuple[str, str] = (By.CSS_SELECTOR, ".h-12") - _CREATE_PROJECT_BUTTON: Tuple[str, str] = (By.CSS_SELECTOR, ".hidden.px-3") - _SEARCH_BUTTON: Tuple[str, str] = (By.CSS_SELECTOR, "[data-icon='search']") - _SEARCH_FIELD: Tuple[str, str] = (By.CSS_SELECTOR, "[type=text]") - _CONFIRM_DELETE_PROJECT_BUTTON: Tuple[str, str] = ( + _DELETE_WORKSPACE_FIELD: tuple[str, str] = (By.CSS_SELECTOR, ".h-12") + _CREATE_PROJECT_BUTTON: tuple[str, str] = (By.CSS_SELECTOR, ".hidden.px-3") + _SEARCH_BUTTON: tuple[str, str] = (By.CSS_SELECTOR, "[data-icon='search']") + _SEARCH_FIELD: tuple[str, str] = (By.CSS_SELECTOR, "[type=text]") + _CONFIRM_DELETE_PROJECT_BUTTON: tuple[str, str] = ( By.CSS_SELECTOR, "#confirm-delete-button", ) - _CANCEL_PROJECT_DELETION_BUTTON: Tuple[str, str] = ( + _CANCEL_PROJECT_DELETION_BUTTON: tuple[str, str] = ( By.CSS_SELECTOR, "form [type=button]", ) - _PROJECT_PAGE_TITLE: Tuple[str, str] = ( + _PROJECT_PAGE_TITLE: tuple[str, str] = ( By.CSS_SELECTOR, "#app h1.leading-tight.truncate", ) - _NO_PROJECT_FOUND_MESSAGE: Tuple[str, str] = (By.CSS_SELECTOR, "#app h1.block") - _NUMBER_OF_PROJECTS_IN_WORKSPACE_BLOCK: Tuple[str, str] = ( + _NO_PROJECT_FOUND_MESSAGE: tuple[str, str] = (By.CSS_SELECTOR, "#app h1.block") + _NUMBER_OF_PROJECTS_IN_WORKSPACE_BLOCK: tuple[str, str] = ( By.CSS_SELECTOR, "span:nth-child(2)", ) - _DROP_DOWN_BUTTON: Tuple[str, str] = (By.CSS_SELECTOR, ".justify-right button svg") - _DELETE_PROJECT_BUTTON: Tuple[str, str] = ( + _DROP_DOWN_BUTTON: tuple[str, str] = (By.CSS_SELECTOR, ".justify-right button svg") + _DELETE_PROJECT_BUTTON: tuple[str, str] = ( By.XPATH, "//button[text()='Delete Project']", ) - _WORKSPACE_LIST: Tuple[str, str] = (By.CSS_SELECTOR, ".mt-6 a") - _PROJECTS_BLOCK: Tuple[str, str] = ( + _WORKSPACE_LIST: tuple[str, str] = (By.CSS_SELECTOR, ".mt-6 a") + _PROJECTS_BLOCK: tuple[str, str] = ( By.CSS_SELECTOR, "#app .max-w-full div .mt-4 > .mt-8 > div", ) - _PROJECTS_TITLES: Tuple[str, str] = (By.CSS_SELECTOR, "h1 a") + _PROJECTS_TITLES: tuple[str, str] = (By.CSS_SELECTOR, "h1 a") def __init__(self, driver, wait): super().__init__(driver, wait) @@ -82,18 +80,14 @@ def delete_workspace(self) -> None: if len(workspaces) < 2: self.create_workspace("test") workspaces = self.wait.until( - expected_conditions.visibility_of_all_elements_located( - self._WORKSPACE_LIST - ) + expected_conditions.visibility_of_all_elements_located(self._WORKSPACE_LIST) ) # click on the second created workspace workspaces[1].click() self.click(self._WORKSPACE_EDIT_BUTTON) self.click(self._DELETE_WORKSPACE_BUTTON) # get the name of the workspace to delete from the background text in delete workspace field - name = self.driver.find_element(*self._DELETE_WORKSPACE_FIELD).get_attribute( - "placeholder" - ) + name = self.driver.find_element(*self._DELETE_WORKSPACE_FIELD).get_attribute("placeholder") self.fill_text(self._DELETE_WORKSPACE_FIELD, name) self.click(self._CONFIRMATION_BUTTON) @@ -104,18 +98,13 @@ def rename_workspace(self, old_name: str, new_name: str) -> None: ) # get workspaces as text workspaces_text_list = [workspace.text for workspace in workspaces] - flag = any( - old_name in workspaces_text_list[i] - for i in range(len(workspaces_text_list)) - ) + flag = any(old_name in workspaces_text_list[i] for i in range(len(workspaces_text_list))) # case the old workspace is not present if not flag: self.create_workspace(old_name) workspaces = self.wait.until( - expected_conditions.visibility_of_all_elements_located( - self._WORKSPACE_LIST - ) + expected_conditions.visibility_of_all_elements_located(self._WORKSPACE_LIST) ) for workspace in workspaces: if old_name in workspace.text: @@ -130,9 +119,7 @@ def rename_workspace(self, old_name: str, new_name: str) -> None: def create_new_project(self) -> None: if self.is_elem_displayed(self.driver.find_element(*self._START_BUTTON)): self.click(self._START_BUTTON) - elif self.is_elem_displayed( - self.driver.find_element(*self._CREATE_PROJECT_BUTTON) - ): + elif self.is_elem_displayed(self.driver.find_element(*self._CREATE_PROJECT_BUTTON)): self.click(self._CREATE_NEW_WORKSPACE_BUTTON) @allure.step("Search for project {project_name}") @@ -156,16 +143,12 @@ def delete_project(self, project_name: str, status="confirm") -> None: self.click(self._CANCEL_PROJECT_DELETION_BUTTON) elif status == Status.CONFIRM.value: self.click(self._CONFIRM_DELETE_PROJECT_BUTTON) - self.wait.until( - expected_conditions.invisibility_of_element(deleted_project) - ) + self.wait.until(expected_conditions.invisibility_of_element(deleted_project)) @allure.step("Get workspaces number") def get_workspaces_number(self) -> int: self.wait.until( - expected_conditions.invisibility_of_element_located( - self._NEW_WORKSPACE_NAME_FIELD - ) + expected_conditions.invisibility_of_element_located(self._NEW_WORKSPACE_NAME_FIELD) ) workspaces = self.wait.until( expected_conditions.visibility_of_all_elements_located(self._WORKSPACE_LIST) @@ -179,23 +162,17 @@ def get_projects_number_in_page(self) -> int: ) return len(projects) - @allure.step( - "Get number of projects displayed next to main workspace (My Workspace) name" - ) + @allure.step("Get number of projects displayed next to main workspace (My Workspace) name") def get_projects_number_from_workspace(self) -> int: workspaces = self.wait.until( expected_conditions.visibility_of_all_elements_located(self._WORKSPACE_LIST) ) - number = workspaces[0].find_element( - *self._NUMBER_OF_PROJECTS_IN_WORKSPACE_BLOCK - ) + number = workspaces[0].find_element(*self._NUMBER_OF_PROJECTS_IN_WORKSPACE_BLOCK) return int(number.text) @allure.step("Verify if workspace {workspace_name} exists") def is_workspace_found(self, workspace_name: str) -> bool: - self.wait.until( - expected_conditions.invisibility_of_element_located(self._RENAME_FIELD) - ) + self.wait.until(expected_conditions.invisibility_of_element_located(self._RENAME_FIELD)) workspaces = self.wait.until( expected_conditions.visibility_of_all_elements_located(self._WORKSPACE_LIST) ) @@ -211,14 +188,9 @@ def get_no_project_found_message(self) -> str: @allure.step("Check if {project_name} is present") def is_project_found(self, project_name: str) -> bool: projects_titles = self.wait.until( - expected_conditions.visibility_of_all_elements_located( - self._PROJECTS_TITLES - ) - ) - return all( - project_name == project_title.text.lower() - for project_title in projects_titles + expected_conditions.visibility_of_all_elements_located(self._PROJECTS_TITLES) ) + return any(project_name == project_title.text.lower() for project_title in projects_titles) # clicks on a specific project's drop down arrow def click_drop_down_menu(self, project) -> None: diff --git a/pages/templates_page.py b/pages/templates_page.py index c9341d0b2d..7b871de670 100644 --- a/pages/templates_page.py +++ b/pages/templates_page.py @@ -1,5 +1,3 @@ -from typing import Tuple - from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions @@ -9,22 +7,18 @@ class TemplatesPage(TopNavigateBar): """Templates page - contains variety of templates to select""" - _TEMPLATES_BLOCK: Tuple[str, str] = (By.CSS_SELECTOR, "#template-gallery tbody tr") - _CHOOSE_BUTTON: Tuple[str, str] = (By.CSS_SELECTOR, "a .btn.btn-primary") + _TEMPLATES_BLOCK: tuple[str, str] = (By.CSS_SELECTOR, "#template-gallery tbody tr") + _CHOOSE_BUTTON: tuple[str, str] = (By.CSS_SELECTOR, "a .btn.btn-primary") def __init__(self, driver, wait): super().__init__(driver, wait) def choose_template(self, template_name: str) -> None: self.wait.until( - expected_conditions.visibility_of_all_elements_located( - self._TEMPLATES_BLOCK - ) + expected_conditions.visibility_of_all_elements_located(self._TEMPLATES_BLOCK) ) templates = self.wait.until( - expected_conditions.visibility_of_all_elements_located( - self._TEMPLATES_BLOCK - ) + expected_conditions.visibility_of_all_elements_located(self._TEMPLATES_BLOCK) ) for template in templates: if template_name in template.text: diff --git a/pyproject.toml b/pyproject.toml index 10405358a9..16148da5c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,52 +1,47 @@ [build-system] -requires = ["hatchling"] build-backend = "hatchling.build" +requires = ["hatchling"] + +[dependency-groups] +dev = [ + "ruff==0.11.4", + "pre-commit==4.2.0" +] [project] -name = "selenium-python-example" -version = "0.1.0" -description = "Selenium Python example project with pytest and Allure report" -authors = [{ name = "Nir Tal", email = "nirt236@gmail.com" }] -requires-python = "~=3.11" -readme = "README.md" +authors = [{name = "Nir Tal", email = "nirt236@gmail.com"}] dependencies = [ - "allure-pytest==2.13.5", - "assertpy==1.1", - "dataclasses-json==0.6.7", - "deprecated==1.2.18", - "mailinator-python-client-2==0.0.8", - "mysql-connector-python==9.2.0", - "pytest==8.3.5", - "pytest-base-url==2.1.0", - "pytest-check==2.5.2", - "pytest-dependency==0.6.0", - "pytest-ordering==0.6", - "pytest-rerunfailures==15.0", - "pytest-split==0.10.0", - "python-dotenv==1.1.0", - "requests==2.32.3", - "requests-toolbelt==1.0.0", - "selenium==4.30.0", - "tenacity==9.0.0", - "visual-regression-tracker==4.9.0", - "xlrd==2.0.1", + "allure-pytest==2.13.5", + "assertpy==1.1", + "dataclasses-json==0.6.7", + "deprecated==1.2.18", + "mailinator-python-client-2==0.0.8", + "mysql-connector-python==9.2.0", + "pytest==8.3.5", + "pytest-base-url==2.1.0", + "pytest-check==2.5.2", + "pytest-dependency==0.6.0", + "pytest-ordering==0.6", + "pytest-rerunfailures==15.0", + "pytest-split==0.10.0", + "python-dotenv==1.1.0", + "requests==2.32.3", + "requests-toolbelt==1.0.0", + "selenium==4.30.0", + "tenacity==9.0.0", + "visual-regression-tracker==4.9.0", + "xlrd==2.0.1" ] +description = "Selenium Python example project with pytest and Allure report" +name = "selenium-python-example" +readme = "README.md" +requires-python = "~=3.11" +version = "0.1.0" [project.urls] Homepage = "https://github.com/nirtal85/Selenium-Python-Example" Repository = "https://github.com/nirtal85/Selenium-Python-Example" -[dependency-groups] -dev = [ - "black==25.1.0", - "isort==6.0.1", - "pre-commit==4.2.0", -] - -[tool.isort] -profile = "black" -skip = ["env", "venv"] - [tool.hatch.build.targets.sdist] include = ["selenium_python_example"] @@ -75,3 +70,18 @@ markers = [ testpaths = [ "tests" ] + +[tool.ruff] +exclude = [".venv", "env"] +ignore = [ + "D203", # One blank line required before class docstring (conflicts with D211) + "D213", # Multi-line docstring summary should start at the second line + "COM812" +] +line-length = 100 +select = ["ALL"] +target-version = "py311" + +[tool.ruff.format] +docstring-code-format = true +quote-style = "double" diff --git a/tests/base_test.py b/tests/base_test.py index 7ae22b342c..e694f13bec 100644 --- a/tests/base_test.py +++ b/tests/base_test.py @@ -1,5 +1,4 @@ from abc import ABC -from typing import Union from selenium.webdriver import Chrome, Edge, Firefox from selenium.webdriver.support.wait import WebDriverWait @@ -16,7 +15,7 @@ class BaseTest(ABC): - driver: Union[Chrome, Firefox, Edge] + driver: Chrome | Firefox | Edge wait: WebDriverWait about_page: AboutPage login_page: LoginPage diff --git a/tests/conftest.py b/tests/conftest.py index af8f4c63e0..513c04f525 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -81,6 +81,7 @@ def session_request(): Returns: requests.Session: A session object with a logging hook. + """ session = requests.Session() session.headers = {"User-Agent": Constants.AUTOMATION_USER_AGENT} @@ -139,9 +140,7 @@ def pytest_runtest_setup(item: Item) -> None: base_url = item.config.getoption("base_url") if browser in ("chrome", "chrome_headless"): chrome_options = webdriver.ChromeOptions() - chrome_options.set_capability( - "goog:loggingPrefs", {"performance": "ALL", "browser": "ALL"} - ) + chrome_options.set_capability("goog:loggingPrefs", {"performance": "ALL", "browser": "ALL"}) chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"]) chrome_options.add_experimental_option( "prefs", @@ -249,6 +248,7 @@ def pytest_runtest_teardown() -> None: Returns: None + """ if "driver" in locals() or "driver" in globals(): driver.quit() @@ -260,7 +260,7 @@ def pytest_sessionstart() -> None: and setting selenium logging """ load_dotenv() - logging.basicConfig(level=logging.WARN) + logging.basicConfig(level=logging.WARNING) logger = logging.getLogger("selenium") logger.setLevel(logging.DEBUG) @@ -277,6 +277,7 @@ def pytest_exception_interact(node: Item) -> None: Returns: None + """ session_request: requests.Session = node.funcargs["session_request"] if "driver" not in locals() and "driver" not in globals(): @@ -325,9 +326,7 @@ def pytest_exception_interact(node: Item) -> None: body=json.dumps( { item[0]: item[1] - for item in driver.execute_script( - "return Object.entries(sessionStorage);" - ) + for item in driver.execute_script("return Object.entries(sessionStorage);") }, indent=4, ), @@ -338,9 +337,7 @@ def pytest_exception_interact(node: Item) -> None: body=json.dumps( { item[0]: item[1] - for item in driver.execute_script( - "return Object.entries(localStorage);" - ) + for item in driver.execute_script("return Object.entries(localStorage);") }, indent=4, ), @@ -401,9 +398,7 @@ def get_response_body(request_id): def get_request_post_data(request_id): """Get the request post data for the specified request ID.""" - return driver.execute_cdp_cmd( - "Network.getRequestPostData", {"requestId": request_id} - ) + return driver.execute_cdp_cmd("Network.getRequestPostData", {"requestId": request_id}) def capture_full_page_screenshot() -> bytes: @@ -428,9 +423,7 @@ def capture_full_page_screenshot() -> bytes: def attach_network_logs(): network_logs = defaultdict(dict) - for item in [ - json.loads(log["message"])["message"] for log in driver.get_log("performance") - ]: + for item in [json.loads(log["message"])["message"] for log in driver.get_log("performance")]: params = item.get("params") if params.get("type") != "XHR": continue diff --git a/tests/db_test.py b/tests/db_test.py index aebb610818..e8fc96fc56 100644 --- a/tests/db_test.py +++ b/tests/db_test.py @@ -10,6 +10,6 @@ def test_verify_population_amount(self, db_connection): with db_connection.cursor() as cursor: cursor.execute("SELECT Population FROM city WHERE CountryCode='DNK'") population_amount = [item[0] for item in cursor.fetchall()] - assert_that(population_amount).described_as( - "population amount" - ).is_equal_to([495699, 284846, 183912, 161161, 90327]) + assert_that(population_amount).described_as("population amount").is_equal_to( + [495699, 284846, 183912, 161161, 90327] + ) diff --git a/tests/dependency_class_test.py b/tests/dependency_class_test.py index 2d19a0158d..b7756ecb25 100644 --- a/tests/dependency_class_test.py +++ b/tests/dependency_class_test.py @@ -7,11 +7,11 @@ @pytest.mark.dependency(name="e", depends=["TestDependencyExample::b"]) class TestDependencyExample(BaseTest): """Test suite for demonstrating test dependencies between different - classes.""" + classes. + """ def test_e(self): - """ - Placeholder test function with the dependency name "e" and depends on "TestDependencyExample::b". + """Placeholder test function with the dependency name "e" and depends on "TestDependencyExample::b". This test case is designed for demonstration purposes only. """ diff --git a/tests/email_test.py b/tests/email_test.py index 6df054eba4..a03f87dd50 100644 --- a/tests/email_test.py +++ b/tests/email_test.py @@ -15,9 +15,7 @@ def test_verify_email_count(self, mailinator_helper): @allure.title("Verify email content") def test_verify_email_body(self, mailinator_helper): - message = mailinator_helper.get_message( - "testautomation", "purchase is confirmed" - ) + message = mailinator_helper.get_message("testautomation", "purchase is confirmed") assert "Thank you for your purchase" in message.parts[0].body @allure.title("Get OTP code from email") diff --git a/tests/forgot_password_test.py b/tests/forgot_password_test.py index 44b9e70357..f6df883e38 100644 --- a/tests/forgot_password_test.py +++ b/tests/forgot_password_test.py @@ -46,9 +46,7 @@ def test_invalid_email(self, excel_reader, data: Data): @allure.title("Exception test") @allure.link("github.com/allure-examples/", name="Allure Examples") @allure.issue("github.com/allure-examples/allure-examples/issues/1", name="ISSUE-1") - @allure.testcase( - "github.com/allure-examples/allure-examples/issues/2", name="TESTCASE-2" - ) + @allure.testcase("github.com/allure-examples/allure-examples/issues/2", name="TESTCASE-2") def test_expected_exception_on_page_title(self): self.about_page.click_login_link() self.login_page.click_forgot_password() diff --git a/tests/login_test.py b/tests/login_test.py index ab0c39ba05..6ca5b514c9 100644 --- a/tests/login_test.py +++ b/tests/login_test.py @@ -42,9 +42,9 @@ def test_valid_login(self, data: Data): self.about_page.set_geo_location(30.3079823, -97.893803) self.about_page.click_login_link() self.login_page.login(os.getenv("EMAIL"), os.getenv("PASSWORD")) - assert_that(self.projects_page.get_title()).described_as( - "page title" - ).is_equal_to(data.workspace.page_title) + assert_that(self.projects_page.get_title()).described_as("page title").is_equal_to( + data.workspace.page_title + ) @allure.description("Log out from app") @allure.title("Logout of system test") @@ -75,9 +75,7 @@ def test_logout(self, data: Data): :return: None """ - allure.dynamic.parameter( - "password", "qwerty", mode=allure.parameter_mode.MASKED - ) + allure.dynamic.parameter("password", "qwerty", mode=allure.parameter_mode.MASKED) allure.dynamic.parameter( "hostname", socket.gethostname(), mode=allure.parameter_mode.HIDDEN ) @@ -130,9 +128,9 @@ def test_logout(self, data: Data): self.about_page.click_login_link() self.login_page.login(os.getenv("EMAIL"), os.getenv("PASSWORD")) self.projects_page.logout() - assert_that(self.login_page.get_page_title()).described_as( - "page title" - ).is_equal_to(data.login.page_title) + assert_that(self.login_page.get_page_title()).described_as("page title").is_equal_to( + data.login.page_title + ) @allure.description("Skip Test example") @allure.title("Skipped test example") diff --git a/tests/workspaces_test.py b/tests/workspaces_test.py index 8a63c91901..10ba33b39c 100644 --- a/tests/workspaces_test.py +++ b/tests/workspaces_test.py @@ -31,20 +31,16 @@ def test_create_new_workspace(self, data: Data): before = self.projects_page.get_workspaces_number() self.projects_page.create_workspace(data.workspace.name) after = self.projects_page.get_workspaces_number() - assert_that(after).described_as( - "number of displayed workspaces" - ).is_greater_than(before) + assert_that(after).described_as("number of displayed workspaces").is_greater_than(before) @allure.description("Rename an existing workspace") @allure.title("Rename an existing workspace test") @pytest.mark.run(order=2) def test_rename_workspace(self, data: Data): - self.projects_page.rename_workspace( - data.workspace.name, data.workspace.new_name - ) - assert_that( - self.projects_page.is_workspace_found(data.workspace.new_name) - ).described_as("status").is_true() + self.projects_page.rename_workspace(data.workspace.name, data.workspace.new_name) + assert_that(self.projects_page.is_workspace_found(data.workspace.new_name)).described_as( + "status" + ).is_true() @allure.description("Delete an existing workspace") @allure.title("Delete existing workspace") @@ -62,9 +58,7 @@ def test_delete_workspace(self): @pytest.mark.run(order=4) def test_number_of_existing_projects(self): number_of_displayed_projects = self.projects_page.get_projects_number_in_page() - number_of_projects_in_workspace = ( - self.projects_page.get_projects_number_from_workspace() - ) + number_of_projects_in_workspace = self.projects_page.get_projects_number_from_workspace() assert_that(number_of_displayed_projects).described_as( "number of displayed projects" ).is_equal_to(number_of_projects_in_workspace) @@ -83,18 +77,14 @@ def test_add_project_to_workspace(self, data: Data): ) self.project_edit_page.click_save_and_exit() after = self.projects_page.get_projects_number_in_page() - assert_that(before + 1).described_as( - "number of displayed projects" - ).is_equal_to(after) + assert_that(before + 1).described_as("number of displayed projects").is_equal_to(after) @allure.description("Search for an existing project") @allure.title("Search for existing project test") @pytest.mark.run(order=6) def test_search_project(self, data: Data): self.projects_page.search_project(data.workspace.project_name) - expected_status = self.projects_page.is_project_found( - data.workspace.project_name - ) + expected_status = self.projects_page.is_project_found(data.workspace.project_name) assert_that(expected_status).described_as("status").is_true() @allure.description("Search for a non existing project") @@ -111,13 +101,9 @@ def test_search_for_non_existing_project(self, data: Data): @pytest.mark.run(order=8) def test_cancel_project_deletion(self, data: Data): before = self.projects_page.get_projects_number_in_page() - self.projects_page.delete_project( - data.workspace.project_name, Status.CANCEL.value - ) + self.projects_page.delete_project(data.workspace.project_name, Status.CANCEL.value) after = self.projects_page.get_projects_number_in_page() - assert_that(before).described_as("number of displayed projects").is_equal_to( - after - ) + assert_that(before).described_as("number of displayed projects").is_equal_to(after) @allure.description("Deleting an existing project from workspace") @allure.title("Delete existing project") @@ -128,6 +114,6 @@ def test_delete_project(self, data: Data): before = self.projects_page.get_projects_number_in_page() self.projects_page.delete_project(data.workspace.project_name) after = self.projects_page.get_projects_number_in_page() - assert_that(before).described_as( - "number of displayed projects" - ).is_equal_to(after + 1) + assert_that(before).described_as("number of displayed projects").is_equal_to( + after + 1 + ) diff --git a/utilities/excel_parser.py b/utilities/excel_parser.py index cd5868e21b..d65c2704cf 100644 --- a/utilities/excel_parser.py +++ b/utilities/excel_parser.py @@ -17,9 +17,7 @@ def read_from_excel(self, sheet_name): # get all values, iterating through rows and columns num_cols = sheet.ncols # Number of columns - for row_idx, col_idx in itertools.product( - range(1, sheet.nrows), range(num_cols) - ): + for row_idx, col_idx in itertools.product(range(1, sheet.nrows), range(num_cols)): cell_obj = sheet.cell(row_idx, col_idx) # Get cell object by row, col # Convert cell to string,split it according to "'" and take the second cell in the array created # e.g.: cell_obj == "text:'something'" --> after convert and splitting == "something" diff --git a/utilities/mailinator_helper.py b/utilities/mailinator_helper.py index 6e6d8c347c..2a8ccc4e0b 100644 --- a/utilities/mailinator_helper.py +++ b/utilities/mailinator_helper.py @@ -30,6 +30,7 @@ class MailinatorHelper: Example: # Initialize the EmailActions class with the Mailinator client and domain name. email_actions = EmailActions(mailinator_client, "example.com") + """ def __init__(self, mailinator: Mailinator, mailinator_domain: str): @@ -60,6 +61,7 @@ def __get_message_id(self, user_email: str, email_subject: str) -> str: Raises: Any exceptions raised by the underlying `self.mailinator.request` method when fetching the inbox. + """ messages = self.mailinator.request( GetInboxRequest( @@ -92,6 +94,7 @@ def get_message(self, user_email: str, email_subject: str) -> Message: Raises: Any exceptions raised by the underlying `self.mailinator.request` method when fetching the email message. + """ return self.mailinator.request( GetMessageRequest( @@ -121,6 +124,7 @@ def get_otp_code(self, user_email: str) -> str | None: Raises: RuntimeError: If no OTP is found in the email message. + """ message: Message = self.get_message(user_email, "Verify your email address") if not message.parts: @@ -151,6 +155,7 @@ def count_messages_by_subject(self, user_email: str) -> dict[str, int]: from unittest import TestCase subject_counts = count_messages_by_subject(self, "user@example.com") TestCase().assertDictEqual({"some subject": 1}, subject_counts) + """ messages = self.mailinator.request( GetInboxRequest( diff --git a/utilities/vrt_helper.py b/utilities/vrt_helper.py index 6579993fa9..854f255596 100644 --- a/utilities/vrt_helper.py +++ b/utilities/vrt_helper.py @@ -1,7 +1,6 @@ import re import time from contextlib import suppress -from typing import Tuple, Union from pytest_check import check from selenium.webdriver import Chrome, Edge, Firefox @@ -9,12 +8,7 @@ from selenium.webdriver.remote.webelement import WebElement from selenium.webdriver.support import expected_conditions from selenium.webdriver.support.wait import WebDriverWait -from visual_regression_tracker import ( - IgnoreArea, - TestRun, - TestRunStatus, - VisualRegressionTracker, -) +from visual_regression_tracker import IgnoreArea, TestRun, TestRunStatus, VisualRegressionTracker from utilities.constants import Constants @@ -32,7 +26,7 @@ class VrtHelper: def __init__( self, - driver: Union[Chrome, Firefox, Edge], + driver: Chrome | Firefox | Edge, vrt_tracker: VisualRegressionTracker, wait: WebDriverWait, ): @@ -40,7 +34,7 @@ def __init__( self.vrt_tracker = vrt_tracker self.wait = wait - def shoot_page(self, baseline_name: str): + def shoot_page(self, baseline_name: str) -> None: """Capture a screenshot of the current page compare the captured screenshot with a baseline image stored in Visual Regression tracker. @@ -63,9 +57,7 @@ def shoot_page(self, baseline_name: str): TestRunStatus.OK.name, ) - def shoot_page_ang_ignore_elements( - self, baseline_name: str, elements: list[WebElement] - ): + def shoot_page_ang_ignore_elements(self, baseline_name: str, elements: list[WebElement]) -> None: """Capture a screenshot of the current page, define areas to be ignored within the screenshot, compare the captured screenshot with a baseline image stored in Visual Regression tracker. @@ -103,7 +95,7 @@ def shoot_page_ang_ignore_elements( ).testRunResponse.status.name, ) - def shoot_element(self, baseline_name: str, locator: Tuple[str, str]): + def shoot_element(self, baseline_name: str, locator: tuple[str, str]) -> None: """Capture a screenshot of a specific element on the current page compare the captured screenshot with a baseline image stored in Visual Regression tracker. diff --git a/utilities/web_driver_listener.py b/utilities/web_driver_listener.py index 40375c00d1..76a7d9e163 100644 --- a/utilities/web_driver_listener.py +++ b/utilities/web_driver_listener.py @@ -1,5 +1,3 @@ -from typing import List, Union - from selenium.webdriver import Chrome, Edge, Firefox from selenium.webdriver.common.by import By from selenium.webdriver.remote.webelement import WebElement @@ -9,10 +7,8 @@ class DriverEventListener(AbstractEventListener): - def after_find( - self, by: By, value: str, driver: Union[Chrome, Firefox, Edge] - ) -> None: - webelements: List[WebElement] = driver.find_elements(by=by, value=value) + def after_find(self, by: By, value: str, driver: Chrome | Firefox | Edge) -> None: + webelements: list[WebElement] = driver.find_elements(by=by, value=value) for element in webelements: if element.is_displayed(): driver.execute_script( @@ -20,8 +16,6 @@ def after_find( element, ) - def before_click( - self, element: WebElement, driver: Union[Chrome, Firefox, Edge] - ) -> None: + def before_click(self, element: WebElement, driver: Chrome | Firefox | Edge) -> None: wait = WebDriverWait(driver, 10) wait.until(expected_conditions.element_to_be_clickable(element)) diff --git a/uv.lock b/uv.lock index c14e63deef..408269a28a 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ # This file was autogenerated by uv via the following command: -# uv pip compile pyproject.toml -o uv.lock +# uv pip compile pyproject.toml --group dev -o uv.lock allure-pytest==2.13.5 # via selenium-python-example (pyproject.toml) allure-python-commons==2.13.5 @@ -15,6 +15,8 @@ certifi==2025.1.31 # via # requests # selenium +cfgv==3.4.0 + # via pre-commit charset-normalizer==3.4.1 # via requests dacite==1.9.2 @@ -23,8 +25,14 @@ dataclasses-json==0.6.7 # via selenium-python-example (pyproject.toml) deprecated==1.2.18 # via selenium-python-example (pyproject.toml) +distlib==0.3.9 + # via virtualenv +filelock==3.18.0 + # via virtualenv h11==0.14.0 # via wsproto +identify==2.6.9 + # via pre-commit idna==3.10 # via # requests @@ -39,6 +47,8 @@ mypy-extensions==1.0.0 # via typing-inspect mysql-connector-python==9.2.0 # via selenium-python-example (pyproject.toml) +nodeenv==1.9.1 + # via pre-commit outcome==1.3.0.post0 # via # trio @@ -48,10 +58,14 @@ packaging==24.2 # marshmallow # pytest # pytest-rerunfailures +platformdirs==4.3.7 + # via virtualenv pluggy==1.5.0 # via # allure-python-commons # pytest +pre-commit==4.2.0 + # via selenium-python-example (pyproject.toml:dev) pysocks==1.7.1 # via urllib3 pytest==8.3.5 @@ -78,6 +92,8 @@ pytest-split==0.10.0 # via selenium-python-example (pyproject.toml) python-dotenv==1.1.0 # via selenium-python-example (pyproject.toml) +pyyaml==6.0.2 + # via pre-commit requests==2.32.3 # via # selenium-python-example (pyproject.toml) @@ -87,6 +103,8 @@ requests==2.32.3 # visual-regression-tracker requests-toolbelt==1.0.0 # via selenium-python-example (pyproject.toml) +ruff==0.11.4 + # via selenium-python-example (pyproject.toml:dev) selenium==4.30.0 # via selenium-python-example (pyproject.toml) setuptools==78.1.0 @@ -113,6 +131,8 @@ urllib3==2.3.0 # via # requests # selenium +virtualenv==20.30.0 + # via pre-commit visual-regression-tracker==4.9.0 # via selenium-python-example (pyproject.toml) websocket-client==1.8.0