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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Run Python Tests

on:
push:
branches: [ dev, main ]
pull_request:
branches: [ dev, main ]

jobs:
unit_tests:
name: Tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt

- name: Run unit tests
run: pytest -m "not e2e" --maxfail=1 --disable-warnings -q

e2e_tests:
name: Selenium Tests
runs-on: ubuntu-latest
needs: unit_tests
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
sudo apt-get update
sudo apt-get install -y chromium-browser chromium-chromedriver

- name: Run E2E tests
run: pytest -m e2e --maxfail=1 --disable-warnings -q
21 changes: 21 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Python
__pycache__/
*.pyc
*.pyo
*.pyd

# Virtual environment
.venv/
env/
venv/

# Environment variables
.env

# IDE
.idea/
.vscode/

# Selenium logs / reports
reports/
screenshots/
22 changes: 18 additions & 4 deletions pages/cart_page.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC


class CartPage:
CHECKOUT_BUTTON = "/html/body/div/div/div/div[2]/div[2]/div/div[2]/div/div/div/div[3]/div/div[2]/button"

def __init__(self, driver):
def __init__(self, driver, timeout=20):
self.driver = driver
self.wait = WebDriverWait(driver, timeout)

@property
def cart_title(self):
return self.wait.until(
EC.visibility_of_element_located(
(By.XPATH, "//h2[normalize-space()='Ваш кошик товарів']")
)
)

def checkout_button_exists(self):
return len(self.driver.find_elements(By.XPATH, self.CHECKOUT_BUTTON)) > 0
def cart_title_exists(self):
try:
return self.cart_title.is_displayed()
except:
return False
38 changes: 30 additions & 8 deletions pages/home_page.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,43 @@

class HomePage:
URL = "https://epicentrk.ua/"
FIRST_PRODUCT = "/html/body/div/div/div/main/div/div/div[3]/div[2]/div[5]/div/div/ul/li[1]/div/div/a"
SEARCH_BUTTON = "//button[@aria-label='Пошук']//*[name()='svg']"

def __init__(self, driver, timeout=10):
def __init__(self, driver, timeout=15):
self.driver = driver
self.wait = WebDriverWait(driver, timeout)

def open(self):
self.driver.get(self.URL)
# Очікуємо появу першого товару саме через ТВОЙ локатор ✅
self.wait.until(
EC.presence_of_element_located(
(By.XPATH, "(//div[@itemtype='https://schema.org/Product']//p[@itemprop='name']/a)[1]")
)
)

@property
def first_product(self):
return self.wait.until(
EC.element_to_be_clickable(
(By.XPATH, "(//div[@itemtype='https://schema.org/Product']//p[@itemprop='name']/a)[1]")
)
)

@property
def search_button(self):
return self.wait.until(
EC.element_to_be_clickable(
(By.XPATH, "//button[@aria-label='Пошук']//*[name()='svg']")
)
)

def search_button_exists(self):
return len(self.driver.find_elements(By.XPATH, self.SEARCH_BUTTON)) > 0
try:
return self.search_button.is_displayed()
except:
return False

def click_first_product(self):
product = self.wait.until(
EC.element_to_be_clickable((By.XPATH, self.FIRST_PRODUCT))
)
ActionChains(self.driver).move_to_element(product).pause(0.1).click().perform()
product = self.first_product
ActionChains(self.driver).scroll_to_element(product).perform()
product.click()
63 changes: 40 additions & 23 deletions pages/login_page.py
Original file line number Diff line number Diff line change
@@ -1,47 +1,64 @@
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException
import time


class LoginPage:
LOGIN_BUTTON = "//button[@data-testid='login']"
INPUT_PHONE = "//input[@name='login']"
INPUT_PASSWORD = "//input[@type='password']"
SUBMIT_BUTTON = "//button[@data-auth-type='login']"
ERROR_MESSAGE = "//*[contains(text(),'Невірний') or contains(text(),'помилка')]"

def __init__(self, driver, timeout=12):
self.driver = driver
self.wait = WebDriverWait(driver, timeout)

@property
def login_button(self):
return self.wait.until(EC.element_to_be_clickable((
By.CSS_SELECTOR, "button[data-testid='login']"
)))

@property
def phone_input(self):
return self.wait.until(EC.element_to_be_clickable((
By.CSS_SELECTOR, "input[name='login']"
)))

@property
def password_input(self):
return self.wait.until(EC.element_to_be_clickable((
By.CSS_SELECTOR, "input[type='password']"
)))

@property
def submit_button(self):
return self.wait.until(EC.element_to_be_clickable((
By.CSS_SELECTOR, "button[data-auth-type='login']"
)))

@property
def error_message(self):
return self.wait.until(EC.visibility_of_element_located((
By.XPATH, "//*[contains(text(),'Невірний') or contains(text(),'помилка')]"
)))

def open_login_form(self):
el = self.wait.until(EC.element_to_be_clickable((By.XPATH, self.LOGIN_BUTTON)))
ActionChains(self.driver).move_to_element(el).click().perform()
print("✅ Відкрили форму входу")
time.sleep(2)
self.login_button.click()

def enter_phone(self, phone):
field = self.wait.until(EC.element_to_be_clickable((By.XPATH, self.INPUT_PHONE)))
field.click()
field.clear()
field.send_keys(phone)
self.phone_input.clear()
self.phone_input.send_keys(phone)

def enter_password(self, password):
field = self.wait.until(EC.element_to_be_clickable((By.XPATH, self.INPUT_PASSWORD)))
field.click()
field.clear()
field.send_keys(password)
self.password_input.clear()
self.password_input.send_keys(password)

def submit_login(self):
btn = self.wait.until(EC.element_to_be_clickable((By.XPATH, self.SUBMIT_BUTTON)))
ActionChains(self.driver).move_to_element(btn).click().perform()
self.submit_button.click()

def error_displayed(self):
try:
self.wait.until(EC.visibility_of_element_located((By.XPATH, self.ERROR_MESSAGE)))
self.error_message
return True
except TimeoutException:
return False

def login_error_visible(self):
return self.error_displayed()
18 changes: 13 additions & 5 deletions pages/product_page.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException

class ProductPage:
BUY_BTN = "//button[@data-product-buy-button]"

def __init__(self, driver, timeout=10):
self.driver = driver
self.wait = WebDriverWait(driver, timeout)

@property
def buy_button(self):
return self.wait.until(EC.element_to_be_clickable((
By.CSS_SELECTOR, "button[data-product-buy-button]"
)))

def click_buy(self):
btn = self.wait.until(EC.element_to_be_clickable((By.XPATH, self.BUY_BTN)))
ActionChains(self.driver).move_to_element(btn).click().perform()
self.buy_button.click()

def buy_button_exists(self):
return len(self.driver.find_elements(By.XPATH, self.BUY_BTN)) > 0
try:
self.buy_button # Якщо не знайде -> TimeoutException
return True
except TimeoutException:
return False
17 changes: 8 additions & 9 deletions pages/search_page.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@

from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

class HomePage:
class SearchPage:
URL = "https://epicentrk.ua/"

FIRST_PRODUCT = "(//main//li//a[contains(@href,'/p/')])[1]"

def __init__(self, driver, timeout=10):
self.driver = driver
self.wait = WebDriverWait(driver, timeout)

@property
def first_product(self):
return self.wait.until(EC.element_to_be_clickable((
By.XPATH, "(//main//li//a[contains(@href,'/p/')])[1]"
)))

def open(self):
self.driver.get(self.URL)

def click_first_product(self):
product = self.wait.until(
EC.element_to_be_clickable((By.XPATH, self.FIRST_PRODUCT))
)
ActionChains(self.driver).move_to_element(product).click().perform()
self.first_product.click()
4 changes: 4 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[pytest]
markers =
e2e: end-to-end UI tests (slow, require live site)

64 changes: 64 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import pytest
import os
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from webdriver_manager.chrome import ChromeDriverManager
from dotenv import load_dotenv

from pages.home_page import HomePage
from pages.login_page import LoginPage
from pages.product_page import ProductPage
from pages.cart_page import CartPage
from pages.search_page import SearchPage


@pytest.fixture(scope="session")
def driver():
"""Ініціалізація WebDriver на всю сесію"""
options = Options()
options.add_argument("--start-maximized")
# options.add_argument("--headless") # для CI

driver = webdriver.Chrome(
service=Service(ChromeDriverManager().install()),
options=options
)
yield driver
driver.quit()


@pytest.fixture(scope="session")
def credentials():
load_dotenv()
phone = os.getenv("TEST_PHONE")
password = os.getenv("TEST_PASSWORD")

assert phone, "Не знайдено TEST_PHONE у .env"
assert password, "Не знайдено TEST_PASSWORD у .env"
return {"phone": phone, "password": password}


@pytest.fixture(scope="function")
def home_page(driver):
return HomePage(driver)


@pytest.fixture(scope="function")
def login_page(driver):
return LoginPage(driver)


@pytest.fixture(scope="function")
def product_page(driver):
return ProductPage(driver)


@pytest.fixture(scope="function")
def cart_page(driver):
return CartPage(driver)


@pytest.fixture(scope="function")
def search_page(driver):
return SearchPage(driver)
Loading