Skip to content

Commit b8664f6

Browse files
committed
chore: modernize ci and lint config
1 parent 2c78623 commit b8664f6

14 files changed

Lines changed: 85 additions & 67 deletions

‎.github/workflows/monthly.yml‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,11 @@ jobs:
5555
uses: astral-sh/setup-uv@v8.2.0
5656
with:
5757
enable-cache: true
58-
- name: Create venv & install dependencies
59-
run: |
60-
uv venv
61-
uv sync --all-extras --dev
58+
- name: Install dependencies
59+
run: uv sync --locked --all-extras --dev
6260
- name: Run Tests
6361
run: >
64-
xvfb-run .venv/bin/python -m pytest
62+
xvfb-run uv run pytest
6563
${{ github.event.inputs.pytest_command || '-m "not devRun"' }}
6664
--base-url ${{ vars.BASE_URL }}
6765
--splits ${{ github.event.inputs.parallelism || 2 }}
Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Pre merge test
1+
name: Pull Request Tests
22

33
on:
44
pull_request:
@@ -7,7 +7,7 @@ on:
77
- "**/*.py"
88
- "**/*.toml"
99
- "**/*.lock"
10-
- ".github/workflows/devRun.yml"
10+
- ".github/workflows/pr.yml"
1111

1212
env:
1313
FLAKINESS_PROJECT: nirtal85/Selenium-Python-Example
@@ -33,15 +33,17 @@ jobs:
3333
uses: astral-sh/setup-uv@v8.2.0
3434
with:
3535
enable-cache: true
36-
- name: Create venv & install dependencies
37-
run: |
38-
uv venv
39-
uv sync --all-extras --dev
36+
- name: Install dependencies
37+
run: uv sync --locked --all-extras --dev
38+
- name: Ruff check
39+
run: uv run ruff check .
40+
- name: Ruff format check
41+
run: uv run ruff format --check .
4042
- name: Test with pytest
4143
env:
4244
EMAIL: ${{ secrets.EMAIL }}
4345
PASSWORD: ${{ secrets.PASSWORD }}
44-
run: xvfb-run .venv/bin/python -m pytest -m devRun --base-url ${{ vars.BASE_URL }}
46+
run: xvfb-run uv run pytest -m devRun --base-url ${{ vars.BASE_URL }}
4547
- name: Auto-assign reviewers
4648
uses: kentaro-m/auto-assign-action@v2.0.2
4749
if: success()

‎README.md‎

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,25 @@
99
[![Twitter Follow](https://img.shields.io/twitter/follow/NirTal2?style=social)](https://twitter.com/NirTal2)
1010
[![YouTube](https://img.shields.io/youtube/channel/subscribers/UCQjS-eoKl0a1nuP_dvpLsjQ?style=social)](https://www.youtube.com/channel/UCQjS-eoKl0a1nuP_dvpLsjQ)
1111
[![Architecture by TestShift](https://raw.githubusercontent.com/nirtal85/TestShift-AI/main/assets/testshift-architecture-badge.svg)](https://www.test-shift.com/)
12-
![CI Status](https://github.com/nirtal85/Selenium-Python-Example/actions/workflows/devRun.yml/badge.svg)
12+
![CI Status](https://github.com/nirtal85/Selenium-Python-Example/actions/workflows/pr.yml/badge.svg)
1313
![Monthly Build](https://github.com/nirtal85/Selenium-Python-Example/actions/workflows/monthly.yml/badge.svg)
1414
[![Tests](https://img.shields.io/endpoint?url=https%3A%2F%2Fflakiness.io%2Fapi%2Fbadge%3Finput%3D%257B%2522badgeToken%2522%253A%2522badge-AlsJROG5SIAxYaV68SL8K%2522%257D)](https://flakiness.io/nirtal85/Selenium-Python-Example)
1515
[![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)
1616
[![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)
1717

18-
[View Live Report](https://nirtal85.github.io/Selenium-Python-Example/) • [Read The Docs](https://www.test-shift.com) • [Report Bug](https://github.com/nirtal85/Selenium-Python-Example/issues)
18+
[View Live Report](https://nirtal85.github.io/Selenium-Python-Example/) - [Read The Docs](https://www.test-shift.com) - [Report Bug](https://github.com/nirtal85/Selenium-Python-Example/issues)
1919

2020
</div>
2121

2222
---
2323

24-
## 🚀 About The Project
24+
## About The Project
2525

2626
This repository serves as a **Production-Ready Reference Architecture** for building high-scale automated testing frameworks using Python.
2727

2828
It demonstrates advanced design patterns, seamless CI/CD integration, and rich reporting capabilities that define modern Quality Engineering.
2929

30-
### ✨ Key Features
30+
### Key Features
3131

3232
* **Modern Python Tooling:** Built with `uv` for lightning-fast dependency management and `Ruff` for linting.
3333
* **Robust Reporting:** Full integration with **Allure Report**, including screenshots, logs, and video.
@@ -36,7 +36,7 @@ It demonstrates advanced design patterns, seamless CI/CD integration, and rich r
3636

3737
---
3838

39-
## 📃 Articles written about this project
39+
## Articles written about this project
4040

4141
This project implements the concepts discussed in the following **TestShift** articles:
4242

@@ -52,11 +52,11 @@ This project implements the concepts discussed in the following **TestShift** ar
5252
* [Test Automation - How to Use Dynamic Base URLs with Selenium And Playwright Python in GitHub Actions](https://www.linkedin.com/pulse/test-automation-how-use-dynamic-base-urls-selenium-playwright-tal-klq5f/)
5353
* [Test Automation Best Practices: Pinning Browser Version in Selenium Python for Stability](https://www.linkedin.com/pulse/test-automation-best-practices-pinning-browser-version-nir-tal-b5b8f/)
5454
* [Test Automation - Capturing Console Logs and JavaScript Errors with Selenium WebDriver BiDi in Python](https://www.linkedin.com/pulse/test-automation-capturing-console-logs-javascript-errors-nir-tal-g3zgf/)
55-
* [Allure 3 Isn’t Just an Upgrade. It’s the Final Piece of the Quality Gate Architecture](https://www.test-shift.com/posts/allure-3-leave-the-swamp-ride-the-dragon)
55+
* [Allure 3 Isn't Just an Upgrade. It's the Final Piece of the Quality Gate Architecture](https://www.test-shift.com/posts/allure-3-leave-the-swamp-ride-the-dragon)
5656

5757
---
5858

59-
## 🛠️ Tech Stack
59+
## Tech Stack
6060

6161
| Tool | Description & Why We Use It |
6262
|----------------------------------------------------------------------------------|-----------------------------------------------------------------------|
@@ -72,7 +72,7 @@ This project implements the concepts discussed in the following **TestShift** ar
7272

7373
---
7474

75-
## ⚙️ Getting Started
75+
## Getting Started
7676

7777
### 1. Clone
7878

@@ -90,7 +90,7 @@ Windows:
9090
```bash
9191
pip install uv
9292
uv venv
93-
.\env\Scripts\activate
93+
.\.venv\Scripts\activate
9494
uv sync --all-extras --dev
9595
```
9696

@@ -113,33 +113,33 @@ PASSWORD="your_secret_password"
113113
# ... add other variables as needed (see .env.example)
114114
```
115115

116-
## 🏃‍♂️ Execution
116+
## Execution
117117
Run all tests (Headless Chrome by default):
118118

119119
```bash
120-
pytest
120+
uv run pytest
121121
```
122122

123123
Run with specific browser:
124124

125125
```bash
126-
pytest --driver firefox
126+
uv run pytest --driver firefox
127127
```
128128

129129
Run specific suite (Tags):
130130

131131
```bash
132-
pytest -m sanity
132+
uv run pytest -m sanity
133133
```
134134

135-
## 📊 Results & Reporting
135+
## Results & Reporting
136136
We use Allure for reporting. To generate and open the report locally:
137137

138138
```bash
139139
npx -y allure generate allure-results --output allure-report --open
140140
```
141141

142-
👉 [See a Live Example of the Report Here](https://nirtal85.github.io/Selenium-Python-Example/)
142+
[See a Live Example of the Report Here](https://nirtal85.github.io/Selenium-Python-Example/)
143143

144144
---
145145

‎pyproject.toml‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ dev = [
99
name = "selenium-python-example"
1010
description = "Selenium Python example project with pytest and Allure report"
1111
version = "0.1.0"
12+
readme = "README.md"
13+
license = "MIT"
14+
authors = [{ name = "Nir Tal" }]
1215
dependencies = [
1316
"allure-pytest==2.16.0",
1417
"assertpy==1.1",
@@ -51,22 +54,27 @@ addopts = [
5154
base_url = 'https://www.involve.me/'
5255
markers = [
5356
"security: marks security tests",
54-
"devRun: marks tests that run before merge to the main branch"
57+
"devRun: marks tests that run before merge to the main branch",
58+
"flaky: marks tests that may be rerun",
59+
"run: marks ordered test execution",
60+
"sanity: marks sanity tests"
5561
]
5662
testpaths = [
5763
"tests"
5864
]
5965

6066
[tool.ruff]
6167
exclude = [".venv", "env"]
68+
line-length = 100
69+
target-version = "py311"
70+
71+
[tool.ruff.lint]
6272
ignore = [
6373
"D203", # One blank line required before class docstring (conflicts with D211)
6474
"D213", # Multi-line docstring summary should start at the second line
6575
"COM812"
6676
]
67-
line-length = 100
68-
select = ["ALL"]
69-
target-version = "py311"
77+
select = ["E", "F", "B", "I", "UP", "SIM", "PT"]
7078

7179
[tool.ruff.format]
7280
docstring-code-format = true

‎src/pages/base_page.py‎

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ def set_geo_location(self, latitude: float, longitude: float) -> None:
4646
4747
Note:
4848
This method uses the Chrome DevTools Protocol (CDP) to override the geolocation
49-
in the web browser, allowing simulation of a specific geographic location for testing purposes.
49+
in the web browser, allowing simulation of a specific geographic location for
50+
testing purposes.
5051
The accuracy is set to 1 for simplicity in this method.
5152
5253
"""
@@ -99,15 +100,21 @@ def is_elem_displayed(self, webelement: WebElement) -> bool:
99100
def _highlight_element(self, webelement: WebElement, color: str) -> None:
100101
original_style = webelement.get_attribute("style")
101102
new_style = f"background-color:yellow;border: 1px solid {color}{original_style}"
103+
set_style_script = (
104+
"var tmpArguments = arguments;"
105+
"setTimeout(function () {"
106+
"tmpArguments[0].setAttribute('style', tmpArguments[1]);"
107+
"}, tmpArguments[2]);"
108+
)
102109
self.driver.execute_script(
103-
"var tmpArguments = arguments;setTimeout(function () {tmpArguments[0].setAttribute('style', '"
104-
+ new_style
105-
+ "');},0);",
110+
set_style_script,
106111
webelement,
112+
new_style,
113+
0,
107114
)
108115
self.driver.execute_script(
109-
"var tmpArguments = arguments;setTimeout(function () {tmpArguments[0].setAttribute('style', '"
110-
+ original_style
111-
+ "');},400);",
116+
set_style_script,
112117
webelement,
118+
original_style,
119+
400,
113120
)

‎src/utilities/excel_parser.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def read_from_excel(self, sheet_name):
1919
num_cols = sheet.ncols # Number of columns
2020
for row_idx, col_idx in itertools.product(range(1, sheet.nrows), range(num_cols)):
2121
cell_obj = sheet.cell(row_idx, col_idx) # Get cell object by row, col
22-
# Convert cell to string,split it according to "'" and take the second cell in the array created
23-
# e.g.: cell_obj == "text:'something'" --> after convert and splitting == "something"
22+
# Convert cell to string, split on "'" and take the value segment.
23+
# e.g.: cell_obj == "text:'something'" --> "something"
2424
rows_val.append(str(cell_obj).split("'")[1])
2525
return rows_val

‎src/utilities/mailinator_helper.py‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ def __get_message_id(self, user_email: str, email_subject: str) -> str:
5555
email_subject (str): The subject of the email to wait for.
5656
5757
Returns:
58-
str or None: The message ID of the email with the specified subject, or None if the email
59-
is not found within the inbox.
58+
str or None: The message ID of the email with the specified subject, or
59+
None if the email is not found within the inbox.
6060
6161
Raises:
6262
Any exceptions raised by the underlying `self.mailinator.request` method when
@@ -66,13 +66,13 @@ def __get_message_id(self, user_email: str, email_subject: str) -> str:
6666
messages = self.mailinator.request(
6767
GetInboxRequest(
6868
domain=self.mailinator_domain,
69-
inbox=user_email.split("@")[0],
69+
inbox=user_email.split("@", maxsplit=1)[0],
7070
)
7171
).msgs
7272
filtered_messages: list[Message] = [
7373
message
7474
for message in messages
75-
if message.to == user_email.split("@")[0]
75+
if message.to == user_email.split("@", maxsplit=1)[0]
7676
and message.subject.casefold() == email_subject.casefold()
7777
]
7878
return filtered_messages[0].id if filtered_messages else None
@@ -160,7 +160,7 @@ def count_messages_by_subject(self, user_email: str) -> dict[str, int]:
160160
messages = self.mailinator.request(
161161
GetInboxRequest(
162162
domain=self.mailinator_domain,
163-
inbox=user_email.split("@")[0],
163+
inbox=user_email.split("@", maxsplit=1)[0],
164164
)
165165
).msgs
166166
# Use a list comprehension to extract the subjects from each message

‎src/utilities/vrt_helper.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ def shoot_page(self, baseline_name: str) -> None:
5757
TestRunStatus.OK.name,
5858
)
5959

60-
def shoot_page_ang_ignore_elements(self, baseline_name: str, elements: list[WebElement]) -> None:
60+
def shoot_page_ang_ignore_elements(
61+
self, baseline_name: str, elements: list[WebElement]
62+
) -> None:
6163
"""Capture a screenshot of the current page, define areas to be ignored
6264
within the screenshot, compare the captured screenshot with a baseline
6365
image stored in Visual Regression tracker.

‎tests/conftest.py‎

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from mysql.connector import MySQLConnection
1717
from requests_toolbelt.utils import dump
1818
from selenium import webdriver
19+
from selenium.common.exceptions import WebDriverException
1920
from selenium.webdriver.support.event_firing_webdriver import EventFiringWebDriver
2021
from selenium.webdriver.support.wait import WebDriverWait
2122
from visual_regression_tracker import VisualRegressionTracker
@@ -28,11 +29,11 @@
2829
from src.pages.projects_page import ProjectsPage
2930
from src.pages.templates_page import TemplatesPage
3031
from src.utilities.constants import Constants
31-
from src.utilities.mailinator_helper import MailinatorHelper
32-
from src.utilities.web_driver_listener import DriverEventListener
33-
from src.utilities.excel_parser import ExcelParser
3432
from src.utilities.data import Data
33+
from src.utilities.excel_parser import ExcelParser
34+
from src.utilities.mailinator_helper import MailinatorHelper
3535
from src.utilities.vrt_helper import VrtHelper
36+
from src.utilities.web_driver_listener import DriverEventListener
3637

3738
drivers = ("chrome", "firefox", "chrome_headless", "remote")
3839

@@ -158,8 +159,8 @@ def pytest_runtest_setup(item: Item) -> None:
158159
},
159160
)
160161
chrome_options.enable_bidi = True
161-
# https://forum.robotframework.org/t/maximize-window-chromedriver-133/8416/3
162-
chrome_options.browser_version = "132"
162+
if chrome_browser_version := os.getenv("CHROME_BROWSER_VERSION"):
163+
chrome_options.browser_version = chrome_browser_version
163164
chrome_options.add_argument("disable-dev-shm-usage")
164165
chrome_options.add_argument("no-sandbox")
165166
chrome_options.add_argument("allow-file-access-from-files")
@@ -194,7 +195,6 @@ def pytest_runtest_setup(item: Item) -> None:
194195
# https://stackoverflow.com/questions/76430192/getting-typeerror-webdriver-init-got-an-unexpected-keyword-argument-desi
195196
case "remote":
196197
chrome_options = webdriver.ChromeOptions()
197-
# https://aerokube.com/images/latest/#_chrome
198198
chrome_options.browser_version = "128.0"
199199
chrome_options.set_capability(
200200
"selenoid:options",
@@ -215,7 +215,10 @@ def pytest_runtest_setup(item: Item) -> None:
215215
else:
216216
driver = webdriver.Chrome(options=chrome_options)
217217
item.cls.driver = driver
218-
driver.maximize_window()
218+
try:
219+
driver.maximize_window()
220+
except WebDriverException:
221+
driver.set_window_size(1920, 1080)
219222
driver.get(base_url)
220223
wait = WebDriverWait(driver, 10)
221224
if browser != "remote":
@@ -268,9 +271,9 @@ def pytest_sessionstart() -> None:
268271
def pytest_exception_interact(node: Item) -> None:
269272
"""Pytest hook for interacting with exceptions during test execution.
270273
271-
If the 'driver' variable is present in the local or global namespace, this function performs various
272-
actions for reporting using the 'allure' reporting framework. If 'driver' is not present, the function
273-
returns without taking any action.
274+
If the 'driver' variable is present, this function performs reporting actions
275+
using the 'allure' reporting framework. If 'driver' is not present, the
276+
function returns without taking any action.
274277
275278
Args:
276279
node (Item): The pytest Item representing the test item.

‎tests/dependency_class_test.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class TestDependencyExample(BaseTest):
1111
"""
1212

1313
def test_e(self) -> None:
14-
"""Placeholder test function with the dependency name "e" and depends on "TestDependencyExample::b".
14+
"""Placeholder with dependency name "e" and depends on "TestDependencyExample::b".
1515
1616
This test case is designed for demonstration purposes only.
1717
"""

0 commit comments

Comments
 (0)