diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..8c516de --- /dev/null +++ b/.github/workflows/tests.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5b34605 --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/pages/cart_page.py b/pages/cart_page.py index 07e3caa..bc0622b 100644 --- a/pages/cart_page.py +++ b/pages/cart_page.py @@ -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 diff --git a/pages/home_page.py b/pages/home_page.py index daaaea5..1cc785a 100644 --- a/pages/home_page.py +++ b/pages/home_page.py @@ -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() diff --git a/pages/login_page.py b/pages/login_page.py index d20621e..617d52e 100644 --- a/pages/login_page.py +++ b/pages/login_page.py @@ -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() diff --git a/pages/product_page.py b/pages/product_page.py index 29042b7..ac61211 100644 --- a/pages/product_page.py +++ b/pages/product_page.py @@ -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 \ No newline at end of file + try: + self.buy_button # Якщо не знайде -> TimeoutException + return True + except TimeoutException: + return False diff --git a/pages/search_page.py b/pages/search_page.py index 5ae7b27..4687ea6 100644 --- a/pages/search_page.py +++ b/pages/search_page.py @@ -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() diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..60608cc --- /dev/null +++ b/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +markers = + e2e: end-to-end UI tests (slow, require live site) + diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..f77a777 --- /dev/null +++ b/tests/conftest.py @@ -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) diff --git a/tests/test_buy_flow.py b/tests/test_buy_flow.py index 9d2c343..beb35c6 100644 --- a/tests/test_buy_flow.py +++ b/tests/test_buy_flow.py @@ -1,35 +1,31 @@ -import time -from selenium import webdriver -from selenium.webdriver.chrome.service import Service -from webdriver_manager.chrome import ChromeDriverManager +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +import pytest -from pages.home_page import HomePage -from pages.product_page import ProductPage -from pages.cart_page import CartPage - -driver = None -home = None -product = None -cart = None -def setup_module(): - global driver, home, product, cart - driver = webdriver.Chrome(service=Service(ChromeDriverManager().install())) - driver.maximize_window() - home = HomePage(driver) - product = ProductPage(driver) - cart = CartPage(driver) -def teardown_module(): - driver.quit() -def test_step1_open_product(): - home.open() - time.sleep(2) - home.click_first_product() - time.sleep(3) +@pytest.mark.e2e +def test_step1_open_product(home_page, driver): + wait = WebDriverWait(driver, 20) + home_page.open() + home_page.click_first_product() + wait.until(EC.url_matches(r"/ua/shop/.+\.html$")) assert "/ua/shop/" in driver.current_url and driver.current_url.endswith(".html"), \ - " Не потрапили на сторінку товару!" -def test_step2_buy_btn_exists(): - assert product.buy_button_exists(), " Кнопка 'Купити' не знайдена!" -def test_step3_buy_and_checkout_btn(): + "Не потрапили на сторінку товару!" + print("Сторінка товару успішно відкрита.") +@pytest.mark.e2e +def test_step2_buy_btn_exists(product, driver): + wait = WebDriverWait(driver, 15) + + wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "button[data-product-buy-button]"))) + + assert product.buy_button_exists(), "Кнопка 'Купити' не знайдена!" + +@pytest.mark.e2e +def test_step3_buy_and_checkout_btn(product, cart, driver): + wait = WebDriverWait(driver, 15) + product.click_buy() - time.sleep(5) - assert cart.checkout_button_exists(), " Кнопка 'Оформити покупку' не знайдена!" + + wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "[data-cart-product-item]"))) + + assert cart.cart_title_exists(), "Кошик не відкрився!" diff --git a/tests/test_buy_product.py b/tests/test_buy_product.py deleted file mode 100644 index a806801..0000000 --- a/tests/test_buy_product.py +++ /dev/null @@ -1,34 +0,0 @@ -import time -from selenium import webdriver -from selenium.webdriver.chrome.service import Service -from webdriver_manager.chrome import ChromeDriverManager - -from pages.home_page import HomePage -from pages.product_page import ProductPage -from pages.cart_page import CartPage - - -def test_buy_first_product(): - driver = webdriver.Chrome(service=Service(ChromeDriverManager().install())) - driver.maximize_window() - - home = HomePage(driver) - product = ProductPage(driver) - cart = CartPage(driver) - home.open() - time.sleep(2) - - assert home.search_button_exists(), " Кнопка пошуку не знайдена!" - print(" Кнопка пошуку знайдена") - home.click_first_product() - time.sleep(3) - - assert product.buy_button_exists(), " Кнопка 'Купити' не знайдена!" - print("Кнопка 'Купити' знайдена") - - product.click_buy() - time.sleep(3) - assert cart.checkout_button_exists(), " Кнопка Оформити покупку не знайдена!" - print(" Кнопка 'Оформити покупку' знайдена ") - - driver.quit() diff --git a/tests/test_login.py b/tests/test_login.py index a4c0b4c..477d9b7 100644 --- a/tests/test_login.py +++ b/tests/test_login.py @@ -1,40 +1,25 @@ -import time -from selenium import webdriver -from selenium.webdriver.chrome.service import Service -from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC +import pytest -from pages.home_page import HomePage -from pages.login_page import LoginPage - - -def test_login_invalid_credentials(): - driver = webdriver.Chrome(service=Service(ChromeDriverManager().install())) - driver.maximize_window() +@pytest.mark.e2e +def test_login_valid_credentials(driver, home_page, login_page, credentials): wait = WebDriverWait(driver, 10) - home = HomePage(driver) - login = LoginPage(driver) - home.open() - time.sleep(2) - login.open_login_form() - time.sleep(2) - login.enter_phone("+38 (097) 904-46-37") - login.enter_password(".WMWAzPp%w,/_6b") - time.sleep(1) - login.submit_login() - time.sleep(3) - driver.refresh() - time.sleep(3) - user_name_xpath = "/html/body/div/div/div/div[1]/header/div/div[1]/div[6]/div/button/span[2]" - try: - user_name = wait.until( - EC.visibility_of_element_located((By.XPATH, user_name_xpath)) + home_page.open() + login_page.open_login_form() + login_page.enter_phone(credentials["phone"]) + login_page.enter_password(credentials["password"]) + login_page.submit_login() + + profile_button = wait.until( + EC.visibility_of_element_located( + (By.CSS_SELECTOR, "button[data-testid='login']") ) - print(" Ім’я:", user_name.text) - except: - raise AssertionError("❌ Не знайдено ім’я користувача після входу!") + ) + + user_name_span = profile_button.find_element(By.CSS_SELECTOR, "span._cjioPkQR") - driver.quit() + assert user_name_span.text.strip() != "", "Ім’я користувача не відображено після входу!" + print(f" Успішний вхід") diff --git a/tests/test_open_product_page.py b/tests/test_open_product_page.py index 1d69ef9..c5743bf 100644 --- a/tests/test_open_product_page.py +++ b/tests/test_open_product_page.py @@ -1,35 +1,28 @@ -from selenium import webdriver from selenium.webdriver.common.by import By -from selenium.webdriver.chrome.service import Service from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC -from webdriver_manager.chrome import ChromeDriverManager -from selenium.webdriver.common.action_chains import ActionChains -import time +import pytest +@pytest.mark.e2e +def test_search_kley(driver): + wait = WebDriverWait(driver, 15) + driver.get("https://epicentrk.ua/ua/") -def test_search_kley(): - driver = webdriver.Chrome(service=Service(ChromeDriverManager().install())) - driver.maximize_window() - - driver.get("https://epicentrk.ua/") - time.sleep(3) - search_input = driver.find_element(By.CSS_SELECTOR, "input[type='search']") + search_input = wait.until( + EC.element_to_be_clickable((By.CSS_SELECTOR, "input[data-ui-input][type='search']")) + ) + search_input.clear() search_input.send_keys("клей") - time.sleep(1) - search_button = driver.find_element(By.CSS_SELECTOR, "button[aria-label='Пошук']") - driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", search_button) - time.sleep(0.5) - button_svg = driver.find_element(By.XPATH, "//button[@aria-label='Пошук']//*[name()='svg']") - ActionChains(driver).move_to_element(button_svg).pause(0.1).click().perform() - print("✅ Клік по кнопці пошуку виконано!") - WebDriverWait(driver, 10).until( - EC.url_contains("/ua/shop/kley/") + + search_button = wait.until( + EC.element_to_be_clickable((By.CSS_SELECTOR, "button[aria-label='Пошук']")) ) - current_url = driver.current_url - print("🔎 Поточний URL:", current_url) + search_button.click() - assert "/ua/shop/kley/" in current_url, \ - f"❌ Помилка переходу: {current_url}" + products = wait.until( + EC.presence_of_all_elements_located( + (By.CSS_SELECTOR, "a[data-category-link='true'][title*='Клей']") + ) + ) - time.sleep(3) - driver.quit() + assert any("клей" in p.text.lower() for p in products), "Не знайдено товарів із 'клей'" + print(f"✅ Знайдено {len(products)} товарів зі словом 'клей'")