diff --git a/.github/workflows/quality-gates.yml b/.github/workflows/quality-gates.yml
new file mode 100644
index 0000000..632feec
--- /dev/null
+++ b/.github/workflows/quality-gates.yml
@@ -0,0 +1,74 @@
+name: Quality gates
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ framework:
+ name: Framework / Python ${{ matrix.python-version }}
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ python-version: ["3.10", "3.12"]
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+ cache: pip
+ - name: Install project
+ run: python -m pip install -e ".[test]"
+ - name: Lint
+ run: ruff check .
+ - name: Run API and unit tests
+ run: >-
+ pytest -m "api or unit"
+ --junitxml=artifacts/framework.xml
+ - name: Upload framework reports
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: framework-python-${{ matrix.python-version }}
+ path: artifacts/
+ if-no-files-found: ignore
+
+ selenium:
+ name: Selenium / ${{ matrix.browser }}
+ needs: framework
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ browser: [chrome, firefox]
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.12"
+ cache: pip
+ - name: Install project
+ run: python -m pip install -e ".[test]"
+ - name: Run browser tests
+ run: >-
+ pytest
+ --browser=${{ matrix.browser }}
+ --cov=automation_framework
+ --cov-report=term-missing
+ --cov-report=xml:artifacts/coverage-${{ matrix.browser }}.xml
+ --junitxml=artifacts/ui-${{ matrix.browser }}.xml
+ --html=artifacts/ui-${{ matrix.browser }}.html
+ --self-contained-html
+ - name: Upload browser reports and failure evidence
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: selenium-${{ matrix.browser }}
+ path: artifacts/
+ if-no-files-found: ignore
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..dbe78c1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+.venv/
+__pycache__/
+*.py[cod]
+*.egg-info/
+.pytest_cache/
+.ruff_cache/
+.coverage
+htmlcov/
+artifacts/
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..f0ec87b
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Yasir Savanur
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index 951d195..eb5360e 100644
--- a/README.md
+++ b/README.md
@@ -1,106 +1,147 @@
-# Python-Test-Automation-Framework
+# Orbit QA — Python Test Automation Framework
-# Description
+[](https://github.com/yasirsavanur/Python-Test-Automation-Framework/actions/workflows/quality-gates.yml)
+[](https://www.python.org/)
+[](https://www.selenium.dev/)
-Test Automation Framework using selenium and Python with the below features:
+A production-minded UI and API automation framework built with Python, pytest, and Selenium 4. It demonstrates maintainable page objects, data-driven testing, cross-browser execution, Grid support, failure diagnostics, contract testing, and CI quality gates—without depending on an unstable public demo site.
-- Framework is based on page object model.
-- Reporting using Allure report.
-- Reading locators from JSON file.
-- Reading test data from JSON file.
-- Integrated with TestRail to update test cases status after each run.
-- Framework can integrate with CodeShip.
+The repository includes **Orbit QA Store**, a small deterministic commerce system used as the test target. Clone the project and the whole suite is ready to run locally.
-# Install dependences
+## What this project demonstrates
-- Install the depended packages in `requirements.txt` using `pip install -r requirements.txt`
+- Selenium 4 sessions for Chrome, Firefox, or a remote Grid
+- explicit waits and stable `data-testid` selectors—no fixed sleeps or implicit waits
+- focused page objects that model user intent instead of test mechanics
+- pytest fixtures, custom CLI options, markers, and readable parametrized cases
+- UI journeys and API contract tests against the same system under test
+- automatic screenshot and page-source capture when a browser test fails
+- self-contained HTML, JUnit XML, and coverage reports
+- GitHub Actions matrices across Python 3.10/3.12 and Chrome/Firefox
+- Ruff linting and an enforced 80% branch-coverage threshold
-# Create new test case
+## Framework shape
-In order to create a new test case using the **Framework**, you have to follow the below steps:
+```text
+src/automation_framework/
+├── api_client.py # JSON API test client
+├── config.py # validated runtime settings
+├── driver_factory.py # local and remote WebDriver sessions
+└── pages/ # intent-focused page objects
+demo_app/ # deterministic UI + API test target
+tests/
+├── api/ # service contract checks
+├── data/ # versioned, non-secret test data
+├── support/ # ephemeral demo server
+├── ui/ # browser journeys
+└── unit/ # fast framework checks
+```
-- In **locators module**, create a new locator for the element you would like to use, as below:
+The fixture layer owns infrastructure and lifecycle; tests own assertions; page objects own browser interactions. That separation keeps failures clear and makes a new environment or page straightforward to add.
+## Quick start
- [{
- "pageName": "HomePage",
- "name": "link_login",
- "locateUsing": "xpath",
- "locator": "//a[contains(text(),'Log In')]"
- }]
+Prerequisites: Python 3.10+ and Chrome or Firefox.
-- In **test data module**, add the test data needed for your test case, as below:
+```bash
+python -m venv .venv
+source .venv/bin/activate # Windows: .venv\Scripts\activate
+python -m pip install --upgrade pip
+python -m pip install -e ".[test]"
- {
- "environment": "https://learn.letskodeit.com/",
- "browser": "firefox",
- "email": "test@email.com",
- "password": "abcabc"
- }
+pytest -m smoke --browser chrome
+```
-* If the element exist in more than one page (**Navigation element**), use **navigation module** to create a script for that navigation bar and add your navigation action to that element, as below:
+Selenium Manager resolves the matching local driver, so no machine-specific driver path is required. Tests start the bundled application on an available local port and shut it down after the session.
- def goToLoginPage(self):
- self.elementClick(*self.locator(self.homePage_locators, 'link_login'))
+## Useful test runs
-* If the element exists in only one page, go to **page module** and create a new script for that page e.g: `login_page.py` and add all the actions in that page, as below:
+```bash
+# Fast feedback, no browser
+pytest -m "unit or api"
- def login(self, email, password):
- self.sendKeys(email, *self.locator(self.loginPage_locators, 'input_email'))
- self.sendKeys(password, *self.locator(self.loginPage_locators, 'input_password'))
- self.elementClick(*self.locator(self.loginPage_locators, 'btn_login'))
+# Full Chrome suite with a self-contained report
+pytest --browser chrome --html=artifacts/report.html --self-contained-html
-* Then, in **test module**, create a new script for your test case(s) e.g: `test_login.py` and add your test case, as below:
+# Firefox regression suite with JUnit output
+pytest -m regression --browser firefox --junitxml=artifacts/regression.xml
- @allure.story('epic_1') # story of the test case
- @allure.severity(allure.severity_level.MINOR) # severity of the test case
- @pytestrail.case('C48') # test case id on test rail
- def test_login_successfully(self):
+# Watch the test execute
+pytest -m smoke --browser chrome --headed
- with allure.step('Navigate to login page'): # name of the test step
- self.homeNavigation.goToLoginPage()
- self.ts.markFinal(self.loginPage.isAt, "navigation to login page failed") # check if the navigation to login page occurs successfully
+# Run against a compatible remote environment
+pytest -m ui --browser chrome \
+ --grid-url http://localhost:4444/wd/hub \
+ --base-url https://your-accessible-test-environment.example
+```
- with allure.step('Login'): # name of the test step
- self.loginPage.login(email=td.testData("email"), password=td.testData("password"))
- self.ts.markFinal(self.dashboardPage.isAt, "login failed") # check if login successfully
+### Runtime options
-**Notes:**
+| Option | Default | Purpose |
+|---|---:|---|
+| `--browser` | `chrome` | Select `chrome` or `firefox` |
+| `--headed` | off | Display a local browser instead of using headless mode |
+| `--grid-url` | local | Send the session to Selenium Grid or a cloud provider |
+| `--base-url` | bundled app | Target another compatible environment |
+| `--timeout` | `10` | Set the explicit-wait timeout in seconds |
+| `--artifacts-dir` | `artifacts` | Choose where failure evidence is written |
-- use `@allure.story('[epic name]')` decorator before each test case to define the related epic / story.
-- use `@allure.severity(allure.severity_level.[severity])` decorator before each test case to define the severity of the test case Minor/Major/Critical/Blocker.
-- use `@pytestrail.case('[test case id on testrail]')` decorator before each test case to defione the related test case id on test rail to make the script update run status on test rail.
+## Example test
-# Run the test case
+```python
+@pytest.mark.ui
+@pytest.mark.smoke
+def test_user_can_complete_checkout(driver, base_url, framework_config):
+ login = LoginPage(driver, base_url, framework_config.timeout)
+ inventory = InventoryPage(driver, base_url, framework_config.timeout)
-In order to run the test case after creation, use on of the below commands:
+ login.load()
+ login.sign_in("qa.engineer@example.com", "quality-first")
+ inventory.wait_until_loaded()
+ inventory.add_product("Grid Compass")
-- To run the test case and create allure report but without update the status run on TestRail:
+ assert inventory.cart_count == 1
+```
-`py.test --alluredir=allure_report tests/test_login.py`
+The complete journey lives in [`tests/ui/test_checkout.py`](tests/ui/test_checkout.py).
-`allure serve allure_report`
+## Failure diagnostics
-- To run the test case, create allure report and update the status of run on TestRail:
+If a browser test reaches its assertion and fails, the `driver` fixture writes two files to `artifacts/`:
-`py.test --alluredir=allure_report tests/test_login.py --testrail`
+- a PNG screenshot showing the rendered state
+- the matching HTML source for DOM-level investigation
-`allure serve allure_report`
+CI uploads these alongside the HTML and JUnit reports even when the test job fails.
-**Note:**
+## Continuous integration
-- There are other options of run that you can search for them, as running all the test cases for specific epic/story or with specific severity
+The workflow separates fast framework feedback from browser compatibility:
-# Integration with TestRail
+| Job | Coverage |
+|---|---|
+| Framework | Ruff plus unit/API tests on Python 3.10 and 3.12 |
+| Selenium / Chrome | complete suite, HTML/JUnit reports, branch coverage |
+| Selenium / Firefox | complete suite, HTML/JUnit reports, branch coverage |
-In order to setup the integration with TestRail, edit `testrail.cfg` with your testrail domain and credentials, as below:
+This keeps the signal specific: a Python compatibility problem, a browser-specific problem, and a product regression appear as different checks.
- [API]
- url = https://[your testrail domain].testrail.io
- email = [testrail email]
- password = [testrail password]
+## Extending it
- [TESTRUN]
- project_id = [project id]
+1. Add a page object under `src/automation_framework/pages/` with selectors and user-level actions.
+2. Put reusable, non-secret cases under `tests/data/` and load them with `load_cases`.
+3. Mark tests by intent (`smoke`, `regression`, `ui`, `api`, or `unit`).
+4. Keep credentials outside the repository and inject them through your CI secret store.
+5. Add provider-specific capabilities to `DriverFactory._options()` when connecting a cloud Grid.
-========================
+## Design choices
+
+- **A bundled test target:** public demo sites change or disappear; this repository remains repeatable and portfolio-friendly.
+- **Explicit waits:** each synchronization point describes the condition the test actually needs.
+- **Native Selenium Manager:** local setup stays portable and avoids checked-in browser binaries.
+- **Small abstractions:** page objects remove repetition while Selenium exceptions retain useful stack traces.
+- **No live TestRail coupling:** results are emitted in portable JUnit XML so CI or a test-management adapter can consume them without putting account configuration in the framework.
+
+## License
+
+MIT
diff --git a/base/__init__.py b/base/__init__.py
deleted file mode 100755
index e69de29..0000000
diff --git a/base/basepage.py b/base/basepage.py
deleted file mode 100755
index 4cdd769..0000000
--- a/base/basepage.py
+++ /dev/null
@@ -1,50 +0,0 @@
-from base.selenium_driver import SeleniumDriver
-from traceback import print_stack
-from utilities.util import Util
-import utilities.read_json as RJ
-import os
-
-
-class BasePage(SeleniumDriver):
-
- def __init__(self, driver):
- super(BasePage, self).__init__(driver)
- self.driver = driver
- self.util = Util()
-
- def pageLocators(self, page):
- """
- read the locators of specific page
- :param page: page
- :return: list of all locators in specific page
- """
- locatorsPath = os.path.abspath("./locators/locators.json")
- locatorsJsonFile = RJ.readJson(locatorsPath)
- pageLocators = [locator for locator in locatorsJsonFile if locator['pageName'] in page]
- return pageLocators
-
- def locator(self, pageLocators, locatorName):
- """
- get specific locator in specific page
- :param pageLocators: specific page
- :param locatorName: locator name
- :return: locator and locator Type
- """
- for locator in pageLocators:
- if locatorName == locator['name']:
- return locator['locator'], locator['locateUsing']
-
- def verifyPageTitle(self, titleToVerify):
- """
- Verify the page Title
-
- :param titleToVerify: Title on the page that needs to be verified
- """
- try:
- actualTitle = self.getTitle()
- return self.util.verifyTextContains(actualTitle, titleToVerify)
- except:
- self.log.error("Failed to get page title")
- print_stack()
- return False
-
diff --git a/base/selenium_driver.py b/base/selenium_driver.py
deleted file mode 100755
index 1fc9170..0000000
--- a/base/selenium_driver.py
+++ /dev/null
@@ -1,405 +0,0 @@
-from selenium.webdriver.common.by import By
-from traceback import print_stack
-from selenium.webdriver.support.select import Select
-from selenium.webdriver.support.ui import WebDriverWait
-from selenium.webdriver.support import expected_conditions as EC
-from selenium.webdriver.common.action_chains import ActionChains
-from selenium.common.exceptions import *
-import utilities.custom_logger as cl
-import logging
-import time
-import os
-import allure
-
-
-class SeleniumDriver():
-
- log = cl.customLogger(logging.DEBUG)
-
- def __init__(self, driver):
- self.driver = driver
-
- def screenShot(self, resultMessage):
- """
- Take a screenshot of the current open web page
- """
- fileName = resultMessage + "." + str(round(time.time() *1000)) + ".png"
- if len(fileName) >= 200:
- fileName = str(round(time.time() *1000)) + ".png"
- screenshotDirectory = "../screenshots/"
- relativeFileName = screenshotDirectory + fileName
- currentDirectory = os.path.dirname(__file__)
- destinationFile = os.path.join(currentDirectory, relativeFileName)
- destinationDirectory = os.path.join(currentDirectory, screenshotDirectory)
-
- try:
- if not os.path.exists(destinationDirectory):
- os.makedirs(destinationDirectory)
- self.driver.save_screenshot(destinationFile)
- allure.attach(self.driver.get_screenshot_as_png(),
- name=fileName,
- attachment_type=allure.attachment_type.PNG)
- self.log.info("Screenshot save to directory: " + destinationFile)
- except:
- self.log.error("### Exception Occurred when taking screenshot")
- print_stack()
-
- def getTitle(self):
- return self.driver.title
-
- def getByType(self, locatorType):
- locatorType = locatorType.lower()
- if locatorType == "id":
- return By.ID
- elif locatorType == "name":
- return By.NAME
- elif locatorType == "xpath":
- return By.XPATH
- elif locatorType == "css":
- return By.CSS_SELECTOR
- elif locatorType == "class":
- return By.CLASS_NAME
- elif locatorType == "link":
- return By.LINK_TEXT
- else:
- self.log.info("Locator type" + locatorType + "not correct/supported")
- return False
-
- def dropdownSelectElement(self, locator, locatorType="id", selector="", selectorType="value"):
- try:
- element = self.getElement(locator, locatorType)
- sel = Select(element)
- if selectorType == "value":
- sel.select_by_value(selector)
- time.sleep(1)
- elif selectorType == "index":
- sel.select_by_index(selector)
- time.sleep(1)
- elif selectorType == "text":
- sel.select_by_visible_text(selector)
- time.sleep(1)
- self.log.info("Element selected with selector: " + str(selector) +
- " and selectorType: " + selectorType)
-
- except:
- self.log.error("Element not selected with selector: " + str(selector) +
- " and selectorType: " + selectorType)
- print_stack()
-
- def getDropdownOptionsCount(self, locator, locatorType="id"):
- '''
- get the number of options of drop down list
- :return: number of Options of drop down list
- '''
- options = None
- try:
- element = self.getElement(locator, locatorType)
- sel = Select(element)
- options = sel.options
- self.log.info("Element found with locator: " + locator +
- " and locatorType: " + locatorType)
- except:
- self.log.error("Element not found with locator: " + locator +
- " and locatorType: " + locatorType)
-
- return options
-
- def getDropdownSelectedOptionText(self, locator, locatorType="id"):
- '''
- get the text of selected option in drop down list
- :return: the text of selected option in drop down list
- '''
- selectedOption_text = None
- try:
- element = self.getElement(locator, locatorType)
- sel = Select(element)
- selectedOption_text = sel.first_selected_option.text
- self.log.info("Return the selected option of drop down list with locator: " + locator +
- " and locatorType: " + locatorType)
- except:
- self.log.error("Can not return the selected option of drop down list with locator: " + locator +
- " and locatorType: " + locatorType)
-
- return selectedOption_text
-
- def getDropdownSelectedOptionValue(self, locator, locatorType="id"):
- '''
- get the value of selected option in drop down list
- :return: the value of selected option in drop down list
- '''
- selectedOption_value = None
- try:
- element = self.getElement(locator, locatorType)
- sel = Select(element)
- selectedOption_value = sel.first_selected_option.get_attribute("value")
- self.log.info("Return the selected option of drop down list with locator: " + locator +
- " and locatorType: " + locatorType)
- except:
- self.log.error("Can not return the selected option of drop down list with locator: " + locator +
- " and locatorType: " + locatorType)
-
- return selectedOption_value
-
- def getElement(self, locator, locatorType="id"):
- element = None
- try:
- locatorType = locatorType.lower()
- byType = self.getByType(locatorType)
- element = self.driver.find_element(byType, locator)
- self.log.info("Element found with locator: " + locator +
- " and locatorType: " + locatorType)
- except:
- self.log.error("Element not found with locator: " + locator +
- " and locatorType: " + locatorType)
- return element
-
- def isElementSelected(self, locator, locatorType):
- isSelected = None
- try:
- element = self.getElement(locator, locatorType)
- isSelected = element.is_selected()
- self.log.info("Element found with locator: " + locator +
- " and locatorType: " + locatorType)
- except:
- self.log.error("Element not found with locator: " + locator +
- " and locatorType: " + locatorType)
-
- return isSelected
-
- def getElementList(self, locator, locatorType="id"):
- """
- Get list of elements
- """
- element = None
- try:
- locatorType = locatorType.lower()
- byType = self.getByType(locatorType)
- element = self.driver.find_elements(byType, locator)
- self.log.info("Element list found with locator: " + locator +
- " and locatorType: " + locatorType)
- except:
- self.log.error("Element list not found with locator: " + locator +
- " and locatorType: " + locatorType)
-
- return element
-
-
-
- def elementClick(self, locator="", locatorType="id", element=None):
- """
- Either provide element or a combination of locator and locatorType
- """
-
- try:
- if locator:
- element = self.getElement(locator, locatorType)
- element.click()
- self.log.info("clicked on element with locator: " + locator +
- " locatorType: " + locatorType)
- except:
- self.log.error("cannot click on the element with locator: " + locator +
- " locatorType: " + locatorType)
- print_stack()
-
- def elementHover(self, locator="", locatorType="id", element=None):
- """
- Either provide element or a combination of locator and locatorType
- """
-
- try:
- if locator:
- element = self.getElement(locator, locatorType)
- hover = ActionChains(self.driver).move_to_element(element)
- hover.perform()
- time.sleep(2)
- self.log.info("hover to element with locator: " + locator +
- " locatorType: " + locatorType)
- except:
- self.log.error("cannot hover to the element with locator: " + locator +
- " locatorType: " + locatorType)
- print_stack()
-
- def sendKeys(self, data, locator="", locatorType="id", element=None):
- """
- Send keys to an element
- Either provide element or a combination of locator and locatorType
- """
- try:
- if locator:
- element = self.getElement(locator, locatorType)
- element.send_keys(data)
- self.log.info("send data on element with locator: " + locator +
- " locatorType: " + locatorType)
- except:
- self.log.error("cannot send data on the element with locator: " + locator +
- " locatorType: " + locatorType)
- print_stack()
-
- def clearKeys(self, locator="", locatorType="id", element=None):
- """
- Clear keys of an element
- Either provide element or a combination of locator and locatorType
- """
- try:
- if locator:
- element = self.getElement(locator, locatorType)
- element.clear()
- self.log.info("Clear data of element with locator: " + locator +
- " locatorType: " + locatorType)
- except:
- self.log.error("cannot clear data of the element with locator: " + locator +
- " locatorType: " + locatorType)
- print_stack()
-
- def getText(self, locator="", locatorType="id", element=None, info=""):
- """
- Get 'Text' on an element
- Either provide element or a combination of locator and locatorType
- """
- try:
- if locator:
- self.log.debug("In locator condition")
- element = self.getElement(locator, locatorType)
- self.log.debug("Before finding text")
- text = element.text
- self.log.debug("After finding element, size is: " + str(len(text)))
- if len(text) == 0:
- text = element.get_attribute("innerText")
- if len(text) !=0:
- self.log.info("Getting text on element :: " + info)
- self.log.info("The text is :: '" + text + "'")
- text = text.strip()
- except:
- self.log.error("Failed to get text on element " + info)
- print_stack()
- text = None
- return text
-
- def isElementPresent(self, locator="", locatorType="id", element=None):
- """
- Check if element is present
- Either provide element or a combination of locator and locatorType
- """
- try:
- if locator:
- element = self.getElement(locator, locatorType)
- if element is not None:
- self.log.info("Element found with locator: " + locator +
- " and locatorType: " + locatorType)
- return True
- else:
- self.log.error("Element not found with locator: " + locator +
- " and locatorType: " + locatorType)
- return False
- except:
- self.log.error("Element not found with locator: " + locator +
- " and locatorType: " + locatorType)
- return False
-
- def isElementDisplayed(self, locator="", locatorType="id", element=None):
- """
- Check if element is displayed
- Either provide element or a combination of locator and locatorType
- """
- isDisplayed = False
- try:
- if locator:
- element = self.getElement(locator, locatorType)
- if element is not None:
- isDisplayed = element.is_displayed()
- self.log.info("Element is displayed with locator: " + locator +
- " and locatorType: " + locatorType)
- else:
- self.log.error("Element is not displayed with locator: " + locator +
- " and locatorType: " + locatorType)
- return isDisplayed
- except:
- self.log.error("Element is not displayed with locator: " + locator +
- " and locatorType: " + locatorType)
- return False
-
- def elementPresenceCheck(self, locator="", locatorType="id"):
- try:
- locatorType = locatorType.lower()
- byType = self.getByType(locatorType)
- elementList = self.driver.find_elements(byType, locator)
- if len(elementList) > 0:
- self.log.info("Element Found")
- return True
- else:
- self.log.info("Element not found")
- return False
- except:
- self.log.info("Element not found")
- return False
-
- def waitForElement(self, locator, locatorType = 'id', timeout = 10, pollFrequency = 0.5 ):
- element = None
- try:
- self.log.info("Waiting for maximum :: " + str(timeout) + " :: seconds for element to be clickable")
-
- wait = WebDriverWait(self.driver, timeout, poll_frequency=pollFrequency,
- ignored_exceptions=[NoSuchElementException,
- ElementNotVisibleException,
- ElementNotSelectableException])
- ByType = self.getByType(locatorType)
- element = wait.until(EC.element_to_be_clickable((ByType,locator)))
-
- self.log.info("Element appeared on the web page")
-
- except:
- self.log.info("Element not appeared on the web page")
- print_stack()
-
- return element
-
- def webScroll(self, direction="up"):
- if direction == "up":
- # Scroll Up
- self.driver.execute_script("window.scrollBy(0, -1000);")
- if direction == "down":
- # Scroll Down
- self.driver.execute_script("window.scrollBy(0, 1000);")
-
- def getURL(self):
- '''
- Get the current URL
- :return: current URL
- '''
- currentURL = self.driver.current_url
-
- return currentURL
-
- def pageBack(self):
- '''
- page back the browser
- '''
- self.driver.execute_script("window.history.go(-1)")
-
- def getAttributeValue(self, locator="", locatorType="id", element=None, attribute=""):
- '''
- get attribute value
- '''
- try:
- if locator:
- self.log.debug("In locator condition")
- element = self.getElement(locator, locatorType)
- attribute_value = element.get_attribute(attribute)
- except:
- self.log.error("Failed to get " + attribute + " in element with locator: " +
- locator + " and locatorType: " + locatorType)
- print_stack()
- attribute_value = None
- return attribute_value
-
- def refresh(self):
- self.driver.get(self.driver.current_url)
-
- def page_has_loaded(self):
- try:
- WebDriverWait(self.driver, 1000, poll_frequency=0.5).until(lambda driver: self.driver.execute_script('return document.readyState == "complete";'))
- WebDriverWait(self.driver, 1000, poll_frequency=0.5).until(lambda driver: self.driver.execute_script('return jQuery.active == 0'))
- WebDriverWait(self.driver, 1000, poll_frequency=0.5).until(lambda driver: self.driver.execute_script('return typeof jQuery != "undefined"'))
- WebDriverWait(self.driver, 1000, poll_frequency=0.5).until(lambda driver: self.driver.execute_script('return angular.element(document).injector().get("$http").pendingRequests.length === 0'))
- except:
- return False
diff --git a/base/webdriverfactory.py b/base/webdriverfactory.py
deleted file mode 100755
index 18a96e7..0000000
--- a/base/webdriverfactory.py
+++ /dev/null
@@ -1,52 +0,0 @@
-"""
-@package base
-
-WebDriver Factory class implementation
-It creates a web-driver instance based on browser configurations
-
-"""
-from selenium import webdriver
-import os
-import test_data.testData as td
-
-
-class WebDriverFactory:
-
- def __init__(self):
- """
- Inits WebDriverFactory class
- :Returns None:
- """
- self.browser = td.testData("browser")
- self.baseUrl = td.testData("environment")
-
- def getWebDriverInstance(self):
- """
- Get WebDriver Instance based on the browser configuration
-
- :return 'WebDriver Instance':
- """
-
- if self.browser == "firefox":
- driver = webdriver.Firefox()
-
- elif self.browser == "chrome":
- # Set Chrome driver
- driverLocation = "/users/karimelkomy/PycharmProjects/lib/chromedriver"
- os.environ["webdriver.chrome.driver"] = driverLocation
- driver = webdriver.Chrome(driverLocation)
- driver.set_window_size(1366, 768)
-
- else:
- driver = webdriver.Firefox()
-
- # Setting Driver Implicit Time out for An Element
- driver.implicitly_wait(15)
- # Maximize the window
- driver.maximize_window()
- # Loading browser with App URL
- driver.get(self.baseUrl)
-
- return driver
-
-
diff --git a/demo_app/app.js b/demo_app/app.js
new file mode 100644
index 0000000..36c812b
--- /dev/null
+++ b/demo_app/app.js
@@ -0,0 +1,97 @@
+const VALID_USER = "qa.engineer@example.com";
+const VALID_PASSWORD = "quality-first";
+const CART_KEY = "orbit-qa-cart";
+
+const readCart = () => JSON.parse(localStorage.getItem(CART_KEY) || "[]");
+const writeCart = (items) => localStorage.setItem(CART_KEY, JSON.stringify(items));
+
+function updateCartCount() {
+ const count = document.querySelector("[data-testid='cart-count']");
+ if (count) count.textContent = String(readCart().length);
+}
+
+function configureLogin() {
+ const form = document.querySelector("[data-testid='login-form']");
+ if (!form) return;
+
+ form.addEventListener("submit", (event) => {
+ event.preventDefault();
+ const email = document.querySelector("[data-testid='email']").value.trim();
+ const password = document.querySelector("[data-testid='password']").value;
+ const error = document.querySelector("[data-testid='login-error']");
+
+ if (email === VALID_USER && password === VALID_PASSWORD) {
+ sessionStorage.setItem("orbit-qa-user", email);
+ localStorage.removeItem(CART_KEY);
+ window.location.assign("inventory.html");
+ return;
+ }
+
+ error.textContent = "We couldn't sign you in with those credentials.";
+ });
+}
+
+function configureInventory() {
+ const cards = [...document.querySelectorAll("[data-testid='product-card']")];
+ if (!cards.length) return;
+
+ updateCartCount();
+ cards.forEach((card) => {
+ card.querySelector("[data-testid='add-product']").addEventListener("click", () => {
+ const cart = readCart();
+ cart.push({
+ name: card.dataset.productName,
+ price: Number(card.dataset.productPrice),
+ });
+ writeCart(cart);
+ updateCartCount();
+
+ const notice = document.querySelector("[data-testid='notice']");
+ notice.hidden = false;
+ notice.textContent = `${card.dataset.productName} added to cart`;
+ });
+ });
+
+ document.querySelector("[data-testid='product-search']").addEventListener("input", (event) => {
+ const term = event.target.value.toLowerCase();
+ cards.forEach((card) => {
+ card.hidden = !card.dataset.productName.toLowerCase().includes(term);
+ });
+ });
+}
+
+function configureCart() {
+ const list = document.querySelector("[data-testid='cart-list']");
+ if (!list) return;
+
+ const cart = readCart();
+ if (!cart.length) {
+ list.innerHTML = '