✅ Решения: Page Object Model

← К оглавлению урока

⚡ Ключевые паттерны из решений

  • TestInventory(BaseTest) — наследование и доступ к self.inventory_page
  • get_item_price(item_name) — XPath с ancestor
  • get_items_prices() — список всех цен в корзине
  • Drag & Drop: переключение в iframe → ActionChains → default_content

Решение 1: TestInventory

# tests/test_inventory.py
from tests.base_test import BaseTest


class TestInventory(BaseTest):

    def test_items_amount(self):
        self.login_page.open()
        self.login_page.success_login("standard_user", "secret_sauce")
        assert self.inventory_page.get_items_amount() == 6, \
            "Количество товаров не совпадает."

    def test_all_items_are_displayed(self):
        self.login_page.open()
        self.login_page.success_login("standard_user", "secret_sauce")
        assert self.inventory_page.all_items_are_displayed(), \
            "Не все товары отображаются."

    def test_all_items_names_are_displayed(self):
        self.login_page.open()
        self.login_page.success_login("standard_user", "secret_sauce")
        assert self.inventory_page.all_items_names_are_displayed(), \
            "Не все названия товаров отображаются."

    def test_all_item_names_are_not_empty(self):
        self.login_page.open()
        self.login_page.success_login("standard_user", "secret_sauce")
        assert self.inventory_page.all_item_names_are_not_empty(), \
            "Есть товары с пустыми названиями."

    def test_all_item_names_contains_sauce_labs(self):
        self.login_page.open()
        self.login_page.success_login("standard_user", "secret_sauce")
        assert self.inventory_page.all_item_names_contains_sauce_labs(), \
            "Не все товары начинаются с 'Sauce Labs'."
Логика: каждый тест вызывает open() + success_login() для начала с чистого состояния. При scope="class" драйвер создаётся один раз, но логин выполняется в каждом тесте, чтобы гарантировать нахождение на странице инвентаря.

Решение 2: Сравнение цен трёх товаров (из лекции)

Сначала метод для получения нескольких цен из корзины в CartPage:

# pages/cart_page.py (фрагмент)
from selenium.webdriver.common.by import By
from pages.base_page import BasePage


class CartPage(BasePage):
    CHECKOUT_BUTTON = (By.ID, "checkout")
    ITEM_PRICES     = (By.CLASS_NAME, "inventory_item_price")

    def get_items_prices(self):
        """Возвращает список строк цен всех товаров в корзине."""
        return [el.text for el in self.find_all(self.ITEM_PRICES)]

    def proceed_to_checkout(self):
        self.click(self.CHECKOUT_BUTTON)

Тест:

# tests/test_prices.py
from tests.base_test import BaseTest


class TestAllItemsCost(BaseTest):

    def test_all_items_cost_are_correct(self):
        # 1. Открываем сайт и авторизуемся
        self.login_page.open()
        self.login_page.success_login("standard_user", "secret_sauce")

        # 2. Проверяем URL после входа
        assert self.driver.current_url == \
            "https://www.saucedemo.com/inventory.html", \
            "URL страницы после входа неверен."

        # 3. Запоминаем цены на странице Inventory
        items_cost_from_inventory = [
            self.inventory_page.get_item_price("Sauce Labs Backpack"),
            self.inventory_page.get_item_price("Sauce Labs Bike Light"),
            self.inventory_page.get_item_price("Sauce Labs Bolt T-Shirt"),
        ]

        # 4. Выводим цены в консоль
        for item_cost in items_cost_from_inventory:
            print(f"Цена товара: {item_cost}")

        # 5. Добавляем товары в корзину
        self.inventory_page.add_item_to_cart("Sauce Labs Backpack")
        self.inventory_page.add_item_to_cart("Sauce Labs Bike Light")
        self.inventory_page.add_item_to_cart("Sauce Labs Bolt T-Shirt")

        # 6. Переходим в корзину
        self.inventory_page.go_to_cart()

        # 7. Запоминаем цены в корзине
        items_cost_from_cart = self.cart_page.get_items_prices()

        # 8. Сравниваем
        assert items_cost_from_inventory == items_cost_from_cart, \
            "Цены в корзине не совпадают с ценами на странице Inventory."
Логика: get_item_price() находит цену через XPath с ancestor, get_items_prices() возвращает список по CLASS_NAME. Порядок добавления товаров в корзину соответствует порядку в списке цен.

Решение 3: Drag & Drop через POM

# pages/drag_and_drop_page.py
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver import ActionChains
from pages.base_page import BasePage


class DragAndDropPage(BasePage):
    """Page Object для страницы drag-and-drop на globalsqa.com."""

    URL = "https://www.globalsqa.com/demo-site/draganddrop/"

    # iframe содержит всё взаимодействие
    IFRAME       = (By.CSS_SELECTOR, "iframe[src*='draganddrop']")
    PHOTOS       = (By.CSS_SELECTOR, "#gallery li")
    TRASH_AREA   = (By.ID, "trash")
    TRASH_PHOTOS = (By.CSS_SELECTOR, "#trash li")

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

    def switch_to_iframe(self):
        """Переключается внутрь iframe с drag-and-drop содержимым."""
        iframe = self.find(self.IFRAME)
        self.driver.switch_to.frame(iframe)

    def switch_to_default(self):
        self.driver.switch_to.default_content()

    def get_photos_count(self):
        """Возвращает количество фото в галерее (без корзины)."""
        return len(self.find_all(self.PHOTOS))

    def get_trash_photos_count(self):
        """Возвращает количество фото в корзине."""
        return len(self.find_all(self.TRASH_PHOTOS))

    def drag_first_photo_to_trash(self):
        """Перетаскивает первую фото из галереи в корзину."""
        first_photo = self.find_all(self.PHOTOS)[0]
        trash = self.find(self.TRASH_AREA)
        ActionChains(self.driver)\
            .drag_and_drop(first_photo, trash)\
            .perform()
# tests/test_drag_and_drop.py
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.service import Service as ChromeService
from webdriver_manager.chrome import ChromeDriverManager
from pages.drag_and_drop_page import DragAndDropPage


class TestDragAndDrop:

    @pytest.fixture(scope="class", autouse=True)
    def setup(self):
        self.driver = webdriver.Chrome(
            service=ChromeService(ChromeDriverManager().install())
        )
        self.driver.maximize_window()
        self.page = DragAndDropPage(self.driver)
        yield
        self.driver.quit()

    def test_drag_photo_to_trash(self):
        self.page.open()

        # Переключаемся в iframe
        self.page.switch_to_iframe()

        # Перетаскиваем первую фото
        self.page.drag_first_photo_to_trash()

        # Проверяем корзину: должна быть 1 фото
        assert self.page.get_trash_photos_count() == 1, \
            "В корзине должна быть 1 фотография"

        # Проверяем галерею: должно остаться 3 фото
        assert self.page.get_photos_count() == 3, \
            "В галерее должно остаться 3 фотографии"

        # Возвращаемся из iframe
        self.page.switch_to_default()
Примечание: drag & drop через ActionChains может не работать на некоторых сайтах из-за JavaScript-перехвата событий. В таких случаях используют JS-эмуляцию. ⚠️ Проверить: если ActionChains.drag_and_drop() не работает на globalsqa.com, попробуйте метод через move_to_element + click_and_hold + move_to_element + release, или JS-подход.