diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..7d60752 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.html linguist-generated diff --git a/.gitignore b/.gitignore index ee64614..5c0441b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,56 @@ -.idea/* -*__pycache__* +# ---- IDE / Editor ---- +.idea/ +.vscode/ + +# ---- Python bytecode ---- +__pycache__/ +*.py[cod] +*$py.class + +# ---- Virtual environments ---- +.venv/ +venv/ +env/ +ENV/ +.python-version + +# ---- Packages / build artifacts ---- +build/ +dist/ +wheelhouse/ +*.egg-info/ +.eggs/ +pip-wheel-metadata/ +*.egg + +# ---- Logs / reports ---- +*.log +logs/ +report.html +htmlcov/ +coverage.xml +.coverage* + +# ---- Test / tooling caches ---- +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ +.tox/ +.nox/ + +# ---- Environment / secrets ---- +.env +.env.* +!env.example + +# ---- OS / misc ---- +.DS_Store +Thumbs.db + +# ---- Selenium / WebDriver Manager ---- +.wdm/ +screenshots/ + +# ---- Project specific ---- +last_test_run.log + diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/README.md b/README.md index 819bc82..d14728a 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,15 @@ -# Selenium Page Object Model with Python +# Selenium Page Object Model with Python Page-object-model (POM) is a pattern that you can apply it to develop efficient automation framework. With page-model, it is possible to minimise maintenance cost. Basically page-object means that your every page is inherited from a base class which includes basic functionalities for every pages. If you have some new functionality that every pages have, you can simple add it to the base class. -`BasePage` class include basic functionality and driver initialization +## Overview + +This project demonstrates a minimal Page Object Model (POM) test framework using Python's built‑in `unittest` and Selenium. It now uses `webdriver-manager` to automatically download a compatible ChromeDriver, reducing friction from local browser / driver version mismatches, and a Python virtual environment for isolated dependencies. + +## Base Page + +`BasePage` class includes basic functionality and shared driver helpers (plus explicit waiting utilities now): ```python # base_page.py class BasePage(object): @@ -16,7 +22,9 @@ class BasePage(object): return self.driver.find_element(*locator) ``` -`MainPage` is derived from the `BasePage class, it contains methods related to this page, which will be used to create test steps. +## Main Page + +`MainPage` is derived from `BasePage`; it contains methods related to the Amazon home page (logo presence, search, sign in / sign up navigation with resilient fallbacks and waits). ```python # main_page.py class MainPage(BasePage): @@ -25,10 +33,16 @@ class MainPage(BasePage): super().__init__(driver) # Python3 version def check_page_loaded(self): - return True if self.find_element(*self.locator.LOGO) else False + try: + self.wait_element(*self.locator.LOGO) + return True + except Exception: + return False ``` -When you want to write tests, you should derive your test class from `BaseTest` which holds basic functionality for your tests. Then you can call page and related methods in accordance with the steps in the test cases +## Tests + +When you write tests, derive from `BaseTest` which sets up / tears down the WebDriver. The test classes use page objects for readable steps. ```python # test_sign_in_page.py class TestSignInPage(BaseTest): @@ -40,18 +54,80 @@ class TestSignInPage(BaseTest): self.assertIn("yourstore/home", result.get_url()) ``` -#### If you want to run all tests, you should type: -```sh -python -m unittest +## Project Structure + +``` +pages/ # Page Objects (Base, Main, Login, Signup, Home) +tests/ # unittest test modules +utils/ # Locators, users (test data), test case descriptions +requirements.txt +``` + +## Setup (Virtual Environment) + +Create and activate a virtual environment (recommended): + +```bash +python3 -m venv .venv +source .venv/bin/activate # macOS / Linux +# On Windows: .venv\\Scripts\\activate +pip install --upgrade pip +pip install -r requirements.txt +``` + +Deactivate with: + +```bash +deactivate ``` +## WebDriver Management -#### If you want to run just a class, you should type: -```sh +ChromeDriver version drift is a common source of failures. We use `webdriver-manager` so the correct driver binary is auto-downloaded at runtime: + +```python +from selenium.webdriver.chrome.service import Service +from webdriver_manager.chrome import ChromeDriverManager + +service = Service(ChromeDriverManager().install()) +driver = webdriver.Chrome(service=service, options=options) +``` + +You no longer need to manually place `chromedriver` on your PATH. For CI, caching the `.wdm/` directory (added to `.gitignore`) can speed up runs. + +## Running Tests + +Run all tests: +```bash +python -m unittest +``` + +Generate an HTML report (after installing requirements): +```bash +python run_tests.py +``` +The report will be created under `reports/` (timestamped). Open the generated `selenium_tests*.html` file in a browser. + +Run a single test class: +```bash python -m unittest tests.test_sign_in_page.TestSignInPage ``` -#### If you want to run just a test method, you should type: -```sh +Run a single test method: +```bash python -m unittest tests.test_sign_in_page.TestSignInPage.test_page_load ``` + +## Headless Mode + +Uncomment the `--headless=new` argument in `tests/base_test.py` to run without opening a browser window (useful for CI pipelines). + +## Notes / Future Improvements + +- Amazon DOM changes frequently; for reliability consider switching to a stable demo site (e.g. saucedemo.com) or introducing a layer of resilient locator strategies. +- Add reporting (HTML) or coverage if needed (not included currently to keep dependencies minimal). +- Implement retries or smarter waits for dynamic content. + +## License + +See `LICENSE`. diff --git a/pages/base_page.py b/pages/base_page.py index 3a2e6f9..5f9c066 100644 --- a/pages/base_page.py +++ b/pages/base_page.py @@ -1,5 +1,5 @@ from selenium.webdriver.common.action_chains import ActionChains -from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support.wait import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.common.exceptions import TimeoutException @@ -14,6 +14,12 @@ def __init__(self, driver, base_url='http://www.amazon.com/'): def find_element(self, *locator): return self.driver.find_element(*locator) + def wait_and_find(self, *locator, timeout=None): + """Wait for presence of element then return it.""" + to = timeout or self.timeout + WebDriverWait(self.driver, to).until(EC.presence_of_element_located(locator)) + return self.find_element(*locator) + def open(self, url): url = self.base_url + url self.driver.get(url) @@ -35,3 +41,4 @@ def wait_element(self, *locator): except TimeoutException: print("\n * ELEMENT NOT FOUND WITHIN GIVEN TIME! --> %s" %(locator[1])) self.driver.quit() + raise diff --git a/pages/login_page.py b/pages/login_page.py index 9210690..b2b2cfd 100644 --- a/pages/login_page.py +++ b/pages/login_page.py @@ -1,5 +1,6 @@ from utils.locators import * from pages.base_page import BasePage +from pages.home_page import HomePage from utils import users @@ -9,17 +10,38 @@ def __init__(self, driver): super(LoginPage, self).__init__(driver) # Python2 version def enter_email(self, email): - self.find_element(*self.locator.EMAIL).send_keys(email) + field = self.wait_and_find(*self.locator.EMAIL) + try: + field.clear() + except Exception: + pass + field.send_keys(email) def enter_password(self, password): - self.find_element(*self.locator.PASSWORD).send_keys(password) + field = self.wait_and_find(*self.locator.PASSWORD) + try: + field.clear() + except Exception: + pass + field.send_keys(password) def click_login_button(self): - self.find_element(*self.locator.SUBMIT).click() + self.wait_and_find(*self.locator.SUBMIT).click() def login(self, user): user = users.get_user(user) print(user) + if not user: + return + # Ensure we're on the sign in page + if 'ap/signin' not in self.get_url(): + self.driver.get('https://www.amazon.com/ap/signin') + # Accept cookies if banner appears + try: + btn = self.find_element(*self.locator.COOKIE_ACCEPT) + btn.click() + except Exception: + pass self.enter_email(user["email"]) self.enter_password(user["password"]) self.click_login_button() diff --git a/pages/main_page.py b/pages/main_page.py index 602afd3..ecb77ae 100644 --- a/pages/main_page.py +++ b/pages/main_page.py @@ -3,6 +3,7 @@ from pages.login_page import LoginPage from pages.signup_page import SignUpBasePage from utils.locators import * +from selenium.common.exceptions import NoSuchElementException, ElementNotInteractableException, TimeoutException # Page objects are written in this module. @@ -14,17 +15,41 @@ def __init__(self, driver): super().__init__(driver) # Python3 version def check_page_loaded(self): - return True if self.find_element(*self.locator.LOGO) else False + try: + self.wait_element(*self.locator.LOGO) + return True + except Exception: + return False def search_item(self, item): - self.find_element(*self.locator.SEARCH).send_keys(item) - self.find_element(*self.locator.SEARCH).send_keys(Keys.ENTER) - return self.find_element(*self.locator.SEARCH_LIST).text + search_box = self.wait_and_find(*self.locator.SEARCH) + search_box.clear() + search_box.send_keys(item) + search_box.send_keys(Keys.ENTER) + return self.wait_and_find(*self.locator.SEARCH_LIST).text def click_sign_up_button(self): - self.find_element(*self.locator.SIGNUP).click() + # Try opening the tooltip; fallback to direct URL if not interactable + try: + try: + self.hover(*self.locator.ACCOUNT) + except Exception: + pass + el = self.wait_and_find(*self.locator.SIGNUP) + el.click() + except (NoSuchElementException, ElementNotInteractableException, TimeoutException): + # Direct navigation fallback (more stable for Amazon dynamic header changes) + self.driver.get("https://www.amazon.com/ap/register") return SignUpBasePage(self.driver) def click_sign_in_button(self): - self.find_element(*self.locator.LOGIN).click() + try: + try: + self.hover(*self.locator.ACCOUNT) + except Exception: + pass + el = self.wait_and_find(*self.locator.LOGIN) + el.click() + except (NoSuchElementException, ElementNotInteractableException, TimeoutException): + self.driver.get("https://www.amazon.com/ap/signin") return LoginPage(self.driver) diff --git a/reports/extent_report_20250820_105604.html b/reports/extent_report_20250820_105604.html new file mode 100644 index 0000000..ea929ae --- /dev/null +++ b/reports/extent_report_20250820_105604.html @@ -0,0 +1,134 @@ +Extent Style Report + + +

Extent Style Selenium Report

PASS 4FAIL 0ERROR 2SKIP 0Total 6
+
+
+
Start
2025-08-20 13:52:47
+
Duration
196.78s
+
Pass Rate
66.7%
+
+

Tests

+
StatusClassNameDurationMessageTraceback
PASStest_sign_in_page.TestSignInPagetest_page_load8469 ms
PASStest_sign_in_page.TestSignInPagetest_search_item8805 ms
PASStest_sign_in_page.TestSignInPagetest_sign_in_button9672 ms
ERRORtest_sign_in_page.TestSignInPagetest_sign_in_with_in_valid_user66183 msMessage: +Stacktrace: +0 chromedriver 0x00000001050fcc20 cxxbridge1$str$ptr + 2747652 +1 chromedriver 0x00000001050f4b20 cxxbridge1$str$ptr + 2714628 +2 chromedriver 0x0000000104c3d0c8 cxxbridge1$string$len + 90536 +3 chromedriver 0x0000000104c84618 cxxbridge1$string$len + 382712 +4 chromedriver 0x0000000104cc599c cxxbridge1$string$len + 649852 +5 chromedriver 0x0000000104c78934 cxxbridge1$string$len + 334356 +6 chromedriver 0x00000001050bf88c cxxbridge1$str$ptr + 2496880 +7 chromedriver 0x00000001050c2ab8 cxxbridge1$str$ptr + 2509724 +8 chromedriver 0x00000001050a0510 cxxbridge1$str$ptr + 2369012 +9 chromedriver 0x00000001050c3360 cxxbridge1$str$ptr + 2511940 +10 chromedriver 0x0000000105091610 cxxbridge1$str$ptr + 2307828 +11 chromedriver 0x00000001050e3230 cxxbridge1$str$ptr + 2642708 +12 chromedriver 0x00000001050e33bc cxxbridge1$str$ptr + 2643104 +13 chromedriver 0x00000001050f476c cxxbridge1$str$ptr + 2713680 +14 libsystem_pthread.dylib 0x00000001819bfc0c _pthread_start + 136 +15 libsystem_pthread.dylib 0x00000001819bab80 thread_start + 8 +
Trace
Traceback (most recent call last):
+  File "/Users/mesutgunes/.pyenv/versions/3.11.0rc2/lib/python3.11/unittest/case.py", line 57, in testPartExecutor
+    yield
+  File "/Users/mesutgunes/.pyenv/versions/3.11.0rc2/lib/python3.11/unittest/case.py", line 623, in run
+    self._callTestMethod(testMethod)
+  File "/Users/mesutgunes/.pyenv/versions/3.11.0rc2/lib/python3.11/unittest/case.py", line 579, in _callTestMethod
+    if method() is not None:
+       ^^^^^^^^
+  File "/Users/mesutgunes/Projects/personal/page-object-python-selenium/tests/test_sign_in_page.py", line 47, in test_sign_in_with_in_valid_user
+    result = login_page.login_with_in_valid_user("invalid_user")
+             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+  File "/Users/mesutgunes/Projects/personal/page-object-python-selenium/pages/login_page.py", line 54, in login_with_in_valid_user
+    self.login(user)
+  File "/Users/mesutgunes/Projects/personal/page-object-python-selenium/pages/login_page.py", line 45, in login
+    self.enter_email(user["email"])
+  File "/Users/mesutgunes/Projects/personal/page-object-python-selenium/pages/login_page.py", line 13, in enter_email
+    field = self.wait_and_find(*self.locator.EMAIL)
+            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+  File "/Users/mesutgunes/Projects/personal/page-object-python-selenium/pages/base_page.py", line 20, in wait_and_find
+    WebDriverWait(self.driver, to).until(EC.presence_of_element_located(locator))
+  File "/Users/mesutgunes/Projects/personal/page-object-python-selenium/.venv/lib/python3.11/site-packages/selenium/webdriver/support/wait.py", line 138, in until
+    raise TimeoutException(message, screen, stacktrace)
+selenium.common.exceptions.TimeoutException: Message: 
+Stacktrace:
+0   chromedriver                        0x00000001050fcc20 cxxbridge1$str$ptr + 2747652
+1   chromedriver                        0x00000001050f4b20 cxxbridge1$str$ptr + 2714628
+2   chromedriver                        0x0000000104c3d0c8 cxxbridge1$string$len + 90536
+3   chromedriver                        0x0000000104c84618 cxxbridge1$string$len + 382712
+4   chromedriver                        0x0000000104cc599c cxxbridge1$string$len + 649852
+5   chromedriver                        0x0000000104c78934 cxxbridge1$string$len + 334356
+6   chromedriver                        0x00000001050bf88c cxxbridge1$str$ptr + 2496880
+7   chromedriver                        0x00000001050c2ab8 cxxbridge1$str$ptr + 2509724
+8   chromedriver                        0x00000001050a0510 cxxbridge1$str$ptr + 2369012
+9   chromedriver                        0x00000001050c3360 cxxbridge1$str$ptr + 2511940
+10  chromedriver                        0x0000000105091610 cxxbridge1$str$ptr + 2307828
+11  chromedriver                        0x00000001050e3230 cxxbridge1$str$ptr + 2642708
+12  chromedriver                        0x00000001050e33bc cxxbridge1$str$ptr + 2643104
+13  chromedriver                        0x00000001050f476c cxxbridge1$str$ptr + 2713680
+14  libsystem_pthread.dylib             0x00000001819bfc0c _pthread_start + 136
+15  libsystem_pthread.dylib             0x00000001819bab80 thread_start + 8
+
+
ERRORtest_sign_in_page.TestSignInPagetest_sign_in_with_valid_user67169 msMessage: +Stacktrace: +0 chromedriver 0x0000000102d58c20 cxxbridge1$str$ptr + 2747652 +1 chromedriver 0x0000000102d50b20 cxxbridge1$str$ptr + 2714628 +2 chromedriver 0x00000001028990c8 cxxbridge1$string$len + 90536 +3 chromedriver 0x00000001028e0618 cxxbridge1$string$len + 382712 +4 chromedriver 0x000000010292199c cxxbridge1$string$len + 649852 +5 chromedriver 0x00000001028d4934 cxxbridge1$string$len + 334356 +6 chromedriver 0x0000000102d1b88c cxxbridge1$str$ptr + 2496880 +7 chromedriver 0x0000000102d1eab8 cxxbridge1$str$ptr + 2509724 +8 chromedriver 0x0000000102cfc510 cxxbridge1$str$ptr + 2369012 +9 chromedriver 0x0000000102d1f360 cxxbridge1$str$ptr + 2511940 +10 chromedriver 0x0000000102ced610 cxxbridge1$str$ptr + 2307828 +11 chromedriver 0x0000000102d3f230 cxxbridge1$str$ptr + 2642708 +12 chromedriver 0x0000000102d3f3bc cxxbridge1$str$ptr + 2643104 +13 chromedriver 0x0000000102d5076c cxxbridge1$str$ptr + 2713680 +14 libsystem_pthread.dylib 0x00000001819bfc0c _pthread_start + 136 +15 libsystem_pthread.dylib 0x00000001819bab80 thread_start + 8 +
Trace
Traceback (most recent call last):
+  File "/Users/mesutgunes/.pyenv/versions/3.11.0rc2/lib/python3.11/unittest/case.py", line 57, in testPartExecutor
+    yield
+  File "/Users/mesutgunes/.pyenv/versions/3.11.0rc2/lib/python3.11/unittest/case.py", line 623, in run
+    self._callTestMethod(testMethod)
+  File "/Users/mesutgunes/.pyenv/versions/3.11.0rc2/lib/python3.11/unittest/case.py", line 579, in _callTestMethod
+    if method() is not None:
+       ^^^^^^^^
+  File "/Users/mesutgunes/Projects/personal/page-object-python-selenium/tests/test_sign_in_page.py", line 40, in test_sign_in_with_valid_user
+    result = login_page.login_with_valid_user("valid_user")
+             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+  File "/Users/mesutgunes/Projects/personal/page-object-python-selenium/pages/login_page.py", line 50, in login_with_valid_user
+    self.login(user)
+  File "/Users/mesutgunes/Projects/personal/page-object-python-selenium/pages/login_page.py", line 45, in login
+    self.enter_email(user["email"])
+  File "/Users/mesutgunes/Projects/personal/page-object-python-selenium/pages/login_page.py", line 13, in enter_email
+    field = self.wait_and_find(*self.locator.EMAIL)
+            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+  File "/Users/mesutgunes/Projects/personal/page-object-python-selenium/pages/base_page.py", line 20, in wait_and_find
+    WebDriverWait(self.driver, to).until(EC.presence_of_element_located(locator))
+  File "/Users/mesutgunes/Projects/personal/page-object-python-selenium/.venv/lib/python3.11/site-packages/selenium/webdriver/support/wait.py", line 138, in until
+    raise TimeoutException(message, screen, stacktrace)
+selenium.common.exceptions.TimeoutException: Message: 
+Stacktrace:
+0   chromedriver                        0x0000000102d58c20 cxxbridge1$str$ptr + 2747652
+1   chromedriver                        0x0000000102d50b20 cxxbridge1$str$ptr + 2714628
+2   chromedriver                        0x00000001028990c8 cxxbridge1$string$len + 90536
+3   chromedriver                        0x00000001028e0618 cxxbridge1$string$len + 382712
+4   chromedriver                        0x000000010292199c cxxbridge1$string$len + 649852
+5   chromedriver                        0x00000001028d4934 cxxbridge1$string$len + 334356
+6   chromedriver                        0x0000000102d1b88c cxxbridge1$str$ptr + 2496880
+7   chromedriver                        0x0000000102d1eab8 cxxbridge1$str$ptr + 2509724
+8   chromedriver                        0x0000000102cfc510 cxxbridge1$str$ptr + 2369012
+9   chromedriver                        0x0000000102d1f360 cxxbridge1$str$ptr + 2511940
+10  chromedriver                        0x0000000102ced610 cxxbridge1$str$ptr + 2307828
+11  chromedriver                        0x0000000102d3f230 cxxbridge1$str$ptr + 2642708
+12  chromedriver                        0x0000000102d3f3bc cxxbridge1$str$ptr + 2643104
+13  chromedriver                        0x0000000102d5076c cxxbridge1$str$ptr + 2713680
+14  libsystem_pthread.dylib             0x00000001819bfc0c _pthread_start + 136
+15  libsystem_pthread.dylib             0x00000001819bab80 thread_start + 8
+
+
PASStest_sign_in_page.TestSignInPagetest_sign_up_button36245 ms
+

Raw Data (JSON)

[{"name": "test_page_load", "class_name": "test_sign_in_page.TestSignInPage", "status": "PASS", "start": 1755687167.4689658, "stop": 1755687175.938129, "duration_ms": 8469, "message": null, "traceback": null}, {"name": "test_search_item", "class_name": "test_sign_in_page.TestSignInPage", "status": "PASS", "start": 1755687175.938207, "stop": 1755687184.743506, "duration_ms": 8805, "message": null, "traceback": null}, {"name": "test_sign_in_button", "class_name": "test_sign_in_page.TestSignInPage", "status": "PASS", "start": 1755687184.743597, "stop": 1755687194.416537, "duration_ms": 9672, "message": null, "traceback": null}, {"name": "test_sign_in_with_in_valid_user", "class_name": "test_sign_in_page.TestSignInPage", "status": "ERROR", "start": 1755687194.4166589, "stop": 1755687260.5999238, "duration_ms": 66183, "message": "Message: \nStacktrace:\n0   chromedriver                        0x00000001050fcc20 cxxbridge1$str$ptr + 2747652\n1   chromedriver                        0x00000001050f4b20 cxxbridge1$str$ptr + 2714628\n2   chromedriver                        0x0000000104c3d0c8 cxxbridge1$string$len + 90536\n3   chromedriver                        0x0000000104c84618 cxxbridge1$string$len + 382712\n4   chromedriver                        0x0000000104cc599c cxxbridge1$string$len + 649852\n5   chromedriver                        0x0000000104c78934 cxxbridge1$string$len + 334356\n6   chromedriver                        0x00000001050bf88c cxxbridge1$str$ptr + 2496880\n7   chromedriver                        0x00000001050c2ab8 cxxbridge1$str$ptr + 2509724\n8   chromedriver                        0x00000001050a0510 cxxbridge1$str$ptr + 2369012\n9   chromedriver                        0x00000001050c3360 cxxbridge1$str$ptr + 2511940\n10  chromedriver                        0x0000000105091610 cxxbridge1$str$ptr + 2307828\n11  chromedriver                        0x00000001050e3230 cxxbridge1$str$ptr + 2642708\n12  chromedriver                        0x00000001050e33bc cxxbridge1$str$ptr + 2643104\n13  chromedriver                        0x00000001050f476c cxxbridge1$str$ptr + 2713680\n14  libsystem_pthread.dylib             0x00000001819bfc0c _pthread_start + 136\n15  libsystem_pthread.dylib             0x00000001819bab80 thread_start + 8\n", "traceback": "Traceback (most recent call last):\n  File \"/Users/mesutgunes/.pyenv/versions/3.11.0rc2/lib/python3.11/unittest/case.py\", line 57, in testPartExecutor\n    yield\n  File \"/Users/mesutgunes/.pyenv/versions/3.11.0rc2/lib/python3.11/unittest/case.py\", line 623, in run\n    self._callTestMethod(testMethod)\n  File \"/Users/mesutgunes/.pyenv/versions/3.11.0rc2/lib/python3.11/unittest/case.py\", line 579, in _callTestMethod\n    if method() is not None:\n       ^^^^^^^^\n  File \"/Users/mesutgunes/Projects/personal/page-object-python-selenium/tests/test_sign_in_page.py\", line 47, in test_sign_in_with_in_valid_user\n    result = login_page.login_with_in_valid_user(\"invalid_user\")\n             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/Users/mesutgunes/Projects/personal/page-object-python-selenium/pages/login_page.py\", line 54, in login_with_in_valid_user\n    self.login(user)\n  File \"/Users/mesutgunes/Projects/personal/page-object-python-selenium/pages/login_page.py\", line 45, in login\n    self.enter_email(user[\"email\"])\n  File \"/Users/mesutgunes/Projects/personal/page-object-python-selenium/pages/login_page.py\", line 13, in enter_email\n    field = self.wait_and_find(*self.locator.EMAIL)\n            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/Users/mesutgunes/Projects/personal/page-object-python-selenium/pages/base_page.py\", line 20, in wait_and_find\n    WebDriverWait(self.driver, to).until(EC.presence_of_element_located(locator))\n  File \"/Users/mesutgunes/Projects/personal/page-object-python-selenium/.venv/lib/python3.11/site-packages/selenium/webdriver/support/wait.py\", line 138, in until\n    raise TimeoutException(message, screen, stacktrace)\nselenium.common.exceptions.TimeoutException: Message: \nStacktrace:\n0   chromedriver                        0x00000001050fcc20 cxxbridge1$str$ptr + 2747652\n1   chromedriver                        0x00000001050f4b20 cxxbridge1$str$ptr + 2714628\n2   chromedriver                        0x0000000104c3d0c8 cxxbridge1$string$len + 90536\n3   chromedriver                        0x0000000104c84618 cxxbridge1$string$len + 382712\n4   chromedriver                        0x0000000104cc599c cxxbridge1$string$len + 649852\n5   chromedriver                        0x0000000104c78934 cxxbridge1$string$len + 334356\n6   chromedriver                        0x00000001050bf88c cxxbridge1$str$ptr + 2496880\n7   chromedriver                        0x00000001050c2ab8 cxxbridge1$str$ptr + 2509724\n8   chromedriver                        0x00000001050a0510 cxxbridge1$str$ptr + 2369012\n9   chromedriver                        0x00000001050c3360 cxxbridge1$str$ptr + 2511940\n10  chromedriver                        0x0000000105091610 cxxbridge1$str$ptr + 2307828\n11  chromedriver                        0x00000001050e3230 cxxbridge1$str$ptr + 2642708\n12  chromedriver                        0x00000001050e33bc cxxbridge1$str$ptr + 2643104\n13  chromedriver                        0x00000001050f476c cxxbridge1$str$ptr + 2713680\n14  libsystem_pthread.dylib             0x00000001819bfc0c _pthread_start + 136\n15  libsystem_pthread.dylib             0x00000001819bab80 thread_start + 8\n\n"}, {"name": "test_sign_in_with_valid_user", "class_name": "test_sign_in_page.TestSignInPage", "status": "ERROR", "start": 1755687260.719886, "stop": 1755687327.889175, "duration_ms": 67169, "message": "Message: \nStacktrace:\n0   chromedriver                        0x0000000102d58c20 cxxbridge1$str$ptr + 2747652\n1   chromedriver                        0x0000000102d50b20 cxxbridge1$str$ptr + 2714628\n2   chromedriver                        0x00000001028990c8 cxxbridge1$string$len + 90536\n3   chromedriver                        0x00000001028e0618 cxxbridge1$string$len + 382712\n4   chromedriver                        0x000000010292199c cxxbridge1$string$len + 649852\n5   chromedriver                        0x00000001028d4934 cxxbridge1$string$len + 334356\n6   chromedriver                        0x0000000102d1b88c cxxbridge1$str$ptr + 2496880\n7   chromedriver                        0x0000000102d1eab8 cxxbridge1$str$ptr + 2509724\n8   chromedriver                        0x0000000102cfc510 cxxbridge1$str$ptr + 2369012\n9   chromedriver                        0x0000000102d1f360 cxxbridge1$str$ptr + 2511940\n10  chromedriver                        0x0000000102ced610 cxxbridge1$str$ptr + 2307828\n11  chromedriver                        0x0000000102d3f230 cxxbridge1$str$ptr + 2642708\n12  chromedriver                        0x0000000102d3f3bc cxxbridge1$str$ptr + 2643104\n13  chromedriver                        0x0000000102d5076c cxxbridge1$str$ptr + 2713680\n14  libsystem_pthread.dylib             0x00000001819bfc0c _pthread_start + 136\n15  libsystem_pthread.dylib             0x00000001819bab80 thread_start + 8\n", "traceback": "Traceback (most recent call last):\n  File \"/Users/mesutgunes/.pyenv/versions/3.11.0rc2/lib/python3.11/unittest/case.py\", line 57, in testPartExecutor\n    yield\n  File \"/Users/mesutgunes/.pyenv/versions/3.11.0rc2/lib/python3.11/unittest/case.py\", line 623, in run\n    self._callTestMethod(testMethod)\n  File \"/Users/mesutgunes/.pyenv/versions/3.11.0rc2/lib/python3.11/unittest/case.py\", line 579, in _callTestMethod\n    if method() is not None:\n       ^^^^^^^^\n  File \"/Users/mesutgunes/Projects/personal/page-object-python-selenium/tests/test_sign_in_page.py\", line 40, in test_sign_in_with_valid_user\n    result = login_page.login_with_valid_user(\"valid_user\")\n             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/Users/mesutgunes/Projects/personal/page-object-python-selenium/pages/login_page.py\", line 50, in login_with_valid_user\n    self.login(user)\n  File \"/Users/mesutgunes/Projects/personal/page-object-python-selenium/pages/login_page.py\", line 45, in login\n    self.enter_email(user[\"email\"])\n  File \"/Users/mesutgunes/Projects/personal/page-object-python-selenium/pages/login_page.py\", line 13, in enter_email\n    field = self.wait_and_find(*self.locator.EMAIL)\n            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n  File \"/Users/mesutgunes/Projects/personal/page-object-python-selenium/pages/base_page.py\", line 20, in wait_and_find\n    WebDriverWait(self.driver, to).until(EC.presence_of_element_located(locator))\n  File \"/Users/mesutgunes/Projects/personal/page-object-python-selenium/.venv/lib/python3.11/site-packages/selenium/webdriver/support/wait.py\", line 138, in until\n    raise TimeoutException(message, screen, stacktrace)\nselenium.common.exceptions.TimeoutException: Message: \nStacktrace:\n0   chromedriver                        0x0000000102d58c20 cxxbridge1$str$ptr + 2747652\n1   chromedriver                        0x0000000102d50b20 cxxbridge1$str$ptr + 2714628\n2   chromedriver                        0x00000001028990c8 cxxbridge1$string$len + 90536\n3   chromedriver                        0x00000001028e0618 cxxbridge1$string$len + 382712\n4   chromedriver                        0x000000010292199c cxxbridge1$string$len + 649852\n5   chromedriver                        0x00000001028d4934 cxxbridge1$string$len + 334356\n6   chromedriver                        0x0000000102d1b88c cxxbridge1$str$ptr + 2496880\n7   chromedriver                        0x0000000102d1eab8 cxxbridge1$str$ptr + 2509724\n8   chromedriver                        0x0000000102cfc510 cxxbridge1$str$ptr + 2369012\n9   chromedriver                        0x0000000102d1f360 cxxbridge1$str$ptr + 2511940\n10  chromedriver                        0x0000000102ced610 cxxbridge1$str$ptr + 2307828\n11  chromedriver                        0x0000000102d3f230 cxxbridge1$str$ptr + 2642708\n12  chromedriver                        0x0000000102d3f3bc cxxbridge1$str$ptr + 2643104\n13  chromedriver                        0x0000000102d5076c cxxbridge1$str$ptr + 2713680\n14  libsystem_pthread.dylib             0x00000001819bfc0c _pthread_start + 136\n15  libsystem_pthread.dylib             0x00000001819bab80 thread_start + 8\n\n"}, {"name": "test_sign_up_button", "class_name": "test_sign_in_page.TestSignInPage", "status": "PASS", "start": 1755687328.002583, "stop": 1755687364.248458, "duration_ms": 36245, "message": null, "traceback": null}]
+
+ + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 8f2eb85..c16f7c5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,19 +1,4 @@ -async-generator==1.10 -attrs==22.2.0 -certifi==2022.12.7 -charset-normalizer==3.1.0 -exceptiongroup==1.1.1 -h11==0.14.0 -idna==3.4 -outcome==1.2.0 -pip==22.2.2 -PySocks==1.7.1 -requests==2.31.0 -selenium==4.8.2 -setuptools==65.5.1 -sniffio==1.3.0 -sortedcontainers==2.4.0 -trio==0.22.0 -trio-websocket==0.10.0 -urllib3==1.26.15 -wsproto==1.2.0 \ No newline at end of file +selenium==4.35.0 +webdriver-manager==4.0.2 +# Only direct dependencies required for running unittest-based Selenium tests. +# unittest is in the Python standard library; no need to list it here. \ No newline at end of file diff --git a/run_tests.py b/run_tests.py new file mode 100644 index 0000000..ebcccb5 --- /dev/null +++ b/run_tests.py @@ -0,0 +1,134 @@ +"""Minimal Extent-style HTML report for unittest.""" + +import os, sys, time, html, json, datetime as dt, unittest +from dataclasses import dataclass, asdict + + +@dataclass +class TestRecord: + name: str + class_name: str + status: str + start: float + stop: float + duration_ms: int + message: str | None = None + traceback: str | None = None + + +class ExtentResult(unittest.TextTestResult): + def __init__(self, stream, descriptions, verbosity): + super().__init__(stream, descriptions, verbosity) + self.records: list[TestRecord] = [] + + def startTest(self, test): + setattr(test, '_started_at', time.time()) + super().startTest(test) + + def addSuccess(self, test): + self._store(test, 'PASS') + super().addSuccess(test) + + def addFailure(self, test, err): + self._store(test, 'FAIL', err) + super().addFailure(test, err) + + def addError(self, test, err): + self._store(test, 'ERROR', err) + super().addError(test, err) + + def addSkip(self, test, reason): + self._store(test, 'SKIP', (None, None, reason)) + super().addSkip(test, reason) + + def _store(self, test, status, err=None): + stop = time.time() + start = getattr(test, '_started_at', stop) + duration_ms = int((stop - start) * 1000) + tb = msg = None + if err and status in ('FAIL', 'ERROR'): + etype, evalue, etb = err + import traceback + tb = ''.join(traceback.format_exception(etype, evalue, etb)) + msg = str(evalue) + elif err and status == 'SKIP': + msg = err[2] + test_id = test.id() + class_name, name = test_id.rsplit('.', 1) + self.records.append(TestRecord(name, class_name, status, start, stop, duration_ms, msg, tb)) + + +class ExtentRunner(unittest.TextTestRunner): + def _makeResult(self): # override to return our ExtentResult + return ExtentResult(self.stream, self.descriptions, self.verbosity) + + +def discover(): + return unittest.defaultTestLoader.discover('tests', pattern='test_*.py') + + +def render_html(records: list[TestRecord], started: float, finished: float) -> str: + total = len(records) + counts = {k: sum(1 for r in records if r.status == k) for k in ['PASS', 'FAIL', 'ERROR', 'SKIP']} + passed = counts['PASS'] + duration = finished - started + started_human = dt.datetime.fromtimestamp(started).strftime('%Y-%m-%d %H:%M:%S') + pass_pct = (passed / total * 100) if total else 0 + fail_pct = (counts['FAIL'] / total * 100) if total else 0 + error_pct = (counts['ERROR'] / total * 100) if total else 0 + skip_pct = (counts['SKIP'] / total * 100) if total else 0 + bar_parts = [] + if pass_pct: bar_parts.append(f"#2e7d32 {pass_pct:.2f}%") + if fail_pct: bar_parts.append(f"#c62828 {fail_pct:.2f}%") + if error_pct: bar_parts.append(f"#ad1457 {error_pct:.2f}%") + if skip_pct: bar_parts.append(f"#f9a825 {skip_pct:.2f}%") + bar_gradient = ','.join(bar_parts) or '#555 100%' + + rows = [] + for r in records: + rows.append(f"{r.status}{html.escape(r.class_name)}{html.escape(r.name)}{r.duration_ms} ms{html.escape(r.message or '')}{('
Trace
'+html.escape(r.traceback)+'
') if r.traceback else ''}") + + data_json = html.escape(json.dumps([asdict(r) for r in records])) + + return f"""Extent Style Report + + +

Extent Style Selenium Report

PASS {counts['PASS']}FAIL {counts['FAIL']}ERROR {counts['ERROR']}SKIP {counts['SKIP']}Total {total}
+
+
+
Start
{started_human}
+
Duration
{duration:.2f}s
+
Pass Rate
{pass_pct:.1f}%
+
+

Tests

+{''.join(rows)}
StatusClassNameDurationMessageTraceback
+

Raw Data (JSON)

{data_json}
+
+ +""" + + +def write_report(content: str, directory: str = 'reports') -> str: + os.makedirs(directory, exist_ok=True) + name = dt.datetime.utcnow().strftime('extent_report_%Y%m%d_%H%M%S.html') + path = os.path.join(directory, name) + with open(path, 'w', encoding='utf-8') as f: + f.write(content) + return path + + +def main(): + suite = discover() + start = time.time() + runner = ExtentRunner(verbosity=1) + result = runner.run(suite) # type: ignore + stop = time.time() + html_report = render_html(result.records, start, stop) # type: ignore + path = write_report(html_report) + print(f"Extent-style report written to: {path}") + if result.failures or result.errors: # type: ignore + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/tests/base_test.py b/tests/base_test.py index 5d3b55d..8c6ad39 100644 --- a/tests/base_test.py +++ b/tests/base_test.py @@ -1,6 +1,8 @@ import unittest from selenium import webdriver from selenium.webdriver.chrome.options import Options +from selenium.webdriver.chrome.service import Service +from webdriver_manager.chrome import ChromeDriverManager # type: ignore # I am using python unittest for asserting cases. @@ -11,21 +13,23 @@ class BaseTest(unittest.TestCase): def setUp(self): options = Options() - # options.add_argument("--headless") # Runs Chrome in headless mode. - options.add_argument('--no-sandbox') # # Bypass OS security model + options.add_argument("--headless=new") # Uncomment for headless runs + options.add_argument('--no-sandbox') # Bypass OS security model options.add_argument('disable-infobars') - options.add_argument("--disable-extensions") - options.add_argument("--start-fullscreen") + options.add_argument('--disable-extensions') + options.add_argument('--start-maximized') options.add_argument('--disable-gpu') - self.driver = webdriver.Chrome(options=options) - # self.driver = webdriver.Firefox() - self.driver.get("http://www.amazon.com") + service = Service(ChromeDriverManager().install()) + self.driver = webdriver.Chrome(service=service, options=options) + self.driver.get("https://www.amazon.com") + + # Additional driver initializations (e.g., Firefox) can be added here if needed. def tearDown(self): self.driver.close() if __name__ == "__main__": - suite = unittest.TestLoader().loadTestsFromTestCase(TestPages) + suite = unittest.TestLoader().loadTestsFromTestCase(BaseTest) unittest.TextTestRunner(verbosity=1).run(suite) diff --git a/utils/locators.py b/utils/locators.py index 7908697..b5c549c 100644 --- a/utils/locators.py +++ b/utils/locators.py @@ -4,10 +4,12 @@ # for maintainability we can seperate web objects by page name class MainPageLocators(object): - LOGO = (By.ID, 'nav-logo') + # Amazon has used both #nav-logo and #nav-logo-sprites historically; use a CSS grouping selector. + LOGO = (By.CSS_SELECTOR, '#nav-logo, #nav-logo-sprites') ACCOUNT = (By.ID, 'nav-link-accountList') - SIGNUP = (By.CSS_SELECTOR, '#nav-signin-tooltip > div > a') - LOGIN = (By.CSS_SELECTOR, '#nav-signin-tooltip > a') + # These tooltip links often require hover to display; selectors may change. Consider refining later. + SIGNUP = (By.CSS_SELECTOR, '#nav-signin-tooltip a[href*="register"]') + LOGIN = (By.CSS_SELECTOR, '#nav-signin-tooltip a[href*="signin"]') SEARCH = (By.ID, 'twotabsearchtextbox') SEARCH_LIST = (By.CSS_SELECTOR, 'div[data-component-type="s-search-result"]') @@ -15,5 +17,6 @@ class MainPageLocators(object): class LoginPageLocators(object): EMAIL = (By.ID, 'ap_email') PASSWORD = (By.ID, 'ap_password') - SUBMIT = (By.ID, 'signInSubmit-input') - ERROR_MESSAGE = (By.ID, 'message_error') + SUBMIT = (By.ID, 'signInSubmit') + ERROR_MESSAGE = (By.ID, 'auth-error-message-box') + COOKIE_ACCEPT = (By.ID, 'sp-cc-accept')