diff --git a/.gitignore b/.gitignore index e69209f9..6d43a8f8 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,6 @@ database/ .DS_store *.orig venv/ -.sublime-project -.sublime-workspace +*.sublime-project +*.sublime-workspace diff --git a/shopelectro/selenium/analytics_goals.py b/shopelectro/selenium/analytics_goals.py index 7f82dd82..968b073b 100644 --- a/shopelectro/selenium/analytics_goals.py +++ b/shopelectro/selenium/analytics_goals.py @@ -23,6 +23,9 @@ def __iter__(self): def __getitem__(self, index: int): raise NotImplementedError + def __bool__(self): + return bool(list(self)) + class YandexEcommerceGoals(Goals): # Ignore PyDocStyleBear """ diff --git a/shopelectro/selenium/elements/__init__.py b/shopelectro/selenium/elements/__init__.py index bb9e4aaf..6fdb59c0 100644 --- a/shopelectro/selenium/elements/__init__.py +++ b/shopelectro/selenium/elements/__init__.py @@ -1,5 +1,6 @@ -from .button import Button from .exceptions import Unavailable +from .button import Button from .input import Input -from .product import CatalogCard, ProductCard, CartPosition +from .product import * from .cart import Cart +from .positions import Positions diff --git a/shopelectro/selenium/elements/cart.py b/shopelectro/selenium/elements/cart.py index 15aea396..290fa86f 100644 --- a/shopelectro/selenium/elements/cart.py +++ b/shopelectro/selenium/elements/cart.py @@ -1,6 +1,3 @@ -from contextlib import contextmanager - -from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.by import By from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.support import expected_conditions as EC @@ -15,6 +12,11 @@ class Cart: def __init__(self, driver: SiteDriver): self.driver = driver + self.positions = elements.Positions( + driver, + elements.CartPosition, + (By.CLASS_NAME, 'basket-item'), + ) def _hover(self): cart = self.driver.wait.until(EC.visibility_of_element_located( @@ -25,36 +27,10 @@ def _hover(self): (By.CLASS_NAME, 'js-cart-wrapper') )) - # @todo #920:15m Document the Cart.wait_changes. - # Cover corner cases with TimeoutException. - - @contextmanager - def wait_changes(self): - def wait_changes(browser): - try: - return positions_before != self.positions() - except TimeoutException: - return False - - positions_before = self.positions() - yield - self.driver.wait.until(wait_changes) - - def positions(self) -> [elements.CartPosition]: - try: - # use short_wait to avoid long pauses in case of the empty cart - positions_count = len(self.driver.short_wait.until(EC.presence_of_all_elements_located( - (By.CLASS_NAME, 'basket-item') - ))) - except TimeoutException: - positions_count = 0 - - return [elements.CartPosition(self.driver, i) for i in range(positions_count)] - def remove(self, position: elements.CartPosition): - with self.wait_changes(): + with self.positions.wait_changes(): self._hover() - position.remove_from_cart() + position.remove() def clear(self): self._hover() diff --git a/shopelectro/selenium/elements/positions.py b/shopelectro/selenium/elements/positions.py new file mode 100644 index 00000000..fd7526df --- /dev/null +++ b/shopelectro/selenium/elements/positions.py @@ -0,0 +1,45 @@ +from contextlib import contextmanager + +from selenium.common.exceptions import TimeoutException +from selenium.webdriver.support import expected_conditions as EC + +from shopelectro.selenium import SiteDriver, elements + + +class Positions: + + def __init__(self, driver: SiteDriver, position_type: elements.Product, locator): + self.driver = driver + self.position_type = position_type + self.condition = EC.presence_of_all_elements_located(locator) + + @contextmanager + def wait_changes(self): + def are_changed(_): + try: + return positions_before != self.all() + except TimeoutException: + """ + An exception can be raised from a position's equality method. + In most cases this means that some positions are stale, + so we continue waiting changes. + """ + return False + + positions_before = self.all() + yield + self.driver.wait.until(are_changed) + + def first(self) -> elements.Product: + return self.position_type(self.driver, 0) + + def all(self) -> [elements.Product]: + try: + # use short_wait to avoid long pauses in case of the empty cart + positions_count = len(self.driver.short_wait.until( + self.condition + )) + except TimeoutException: + positions_count = 0 + + return [self.position_type(self.driver, i) for i in range(positions_count)] diff --git a/shopelectro/selenium/elements/product.py b/shopelectro/selenium/elements/product.py index d2fe9317..9af98f18 100644 --- a/shopelectro/selenium/elements/product.py +++ b/shopelectro/selenium/elements/product.py @@ -21,12 +21,18 @@ def price(self): def quantity(self): raise Unavailable('determine the product quantity.') - def add_to_cart(self): + def add(self): raise Unavailable('add the product to the card.') - def remove_from_cart(self): + def remove(self): raise Unavailable('remove the product from the card.') + def __hash__(self): + raise NotImplementedError('Provide __hash__ implementation for the class.') + + def __eq__(self, other: 'Product'): + return hash(self) == hash(other) + class CatalogCard(Product): @@ -79,7 +85,7 @@ def vendor_code(self): (By.XPATH, self._build_xpath('div[2]/div[1]')) )).text.split(' ')[1] - def add_to_cart(self): + def add(self): Button(self.driver, (By.XPATH, self._build_xpath('div[2]/div[5]/button'))).click() @@ -88,7 +94,7 @@ class ProductCard(Product): def __init__(self, driver: SiteDriver): self.driver = driver - def add_to_cart(self): + def add(self): Button(self.driver, (By.CLASS_NAME, 'js-to-cart-on-product-page')).click() @@ -107,9 +113,6 @@ def __hash__(self): + el.get_attribute('data-product-count') ) - def __eq__(self, other: 'CartPosition'): - return hash(self) == hash(other) - def _data_element(self): # use short_wait, because a position could be stale return self.driver.short_wait.until(EC.presence_of_element_located( @@ -125,5 +128,43 @@ def price(self): def quantity(self): return self._data_element().get_attribute('data-product-count') - def remove_from_cart(self): + def remove(self): Button(self.driver, (By.XPATH, f'{self.xpath}i')).click() + + +class OrderPosition(Product): + """Represent a product position on order page.""" + + def __init__(self, driver: SiteDriver, index: int): + self.driver = driver + # xpath indexes starts from 1 + self.xpath = f'//div[@id="js-order-list"]/div[2]/div[{index + 1}]/' + + def __hash__(self): + return hash( + self.vendor_code() + + '/' + + self.quantity() + ) + + def vendor_code(self): + return self.driver.short_wait.until(EC.visibility_of_element_located( + (By.XPATH, f'{self.xpath}div[1]') + )).text + + def quantity(self): + return self.driver.short_wait.until(EC.visibility_of_element_located( + (By.XPATH, f'{self.xpath}//input') + )).value + + def set(self, quantity: int): + raise NotImplementedError + + def increase(self, times=1): + raise NotImplementedError + + def decrease(self, times=1): + raise NotImplementedError + + def remove(self): + Button(self.driver, (By.XPATH, f'{self.xpath}div[6]/div')).click() diff --git a/shopelectro/selenium/pages/category.py b/shopelectro/selenium/pages/category.py index ceb41e3c..5bcd6260 100644 --- a/shopelectro/selenium/pages/category.py +++ b/shopelectro/selenium/pages/category.py @@ -51,6 +51,6 @@ def add_to_cart(self, products: typing.List[elements.CatalogCard] = None): default = [elements.CatalogCard.with_index(self.driver, i) for i in range(6)] products = products or default - with self.cart().wait_changes(): + with self.cart().positions.wait_changes(): for product in products: - product.add_to_cart() + product.add() diff --git a/shopelectro/selenium/pages/order.py b/shopelectro/selenium/pages/order.py index 551378b6..85e3b800 100644 --- a/shopelectro/selenium/pages/order.py +++ b/shopelectro/selenium/pages/order.py @@ -1,25 +1,45 @@ -from shopelectro.models import PaymentOptions -from shopelectro.selenium.elements import Input, Button -from shopelectro.selenium.pages import Page - from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from pages.models import CustomPage +from shopelectro.models import PaymentOptions +from shopelectro.selenium import elements, SiteDriver +from shopelectro.selenium.pages import Page # @todo #682:120m Implement and reuse shopelectro.selenium.OrderPage for selenium tests. class OrderPage(Page): - def __init__(self, driver): + def __init__(self, driver: SiteDriver): super().__init__(driver) - self.submit_button = Button(self.driver, (By.ID, 'submit-order')) + self.submit_button = elements.Button(self.driver, (By.ID, 'submit-order')) + self.positions = elements.Positions( + driver, + elements.OrderPosition, + (By.XPATH, '//div[@id="js-order-list"]/div[2]/div'), + ) @property def path(self): return CustomPage.objects.get(slug='order').url + def set(self, position: elements.OrderPosition, quantity: int): + with self.positions.wait_changes(): + position.set(quantity) + + def increase(self, position: elements.OrderPosition): + with self.positions.wait_changes(): + position.increase() + + def decrease(self, position: elements.OrderPosition): + with self.positions.wait_changes(): + position.decrease() + + def remove(self, position: elements.OrderPosition): + with self.positions.wait_changes(): + position.remove() + def fill_contacts( self, name='Name', city='Санкт-Петербург', phone='2222222222', email='test@test.test', ): @@ -31,7 +51,7 @@ def fill_contacts( } for id_, value in contacts.items(): - Input(self.driver, (By.ID, id_)).send_keys(value) + elements.Input(self.driver, (By.ID, id_)).send_keys(value) def make_order(self): self.submit_button.click() @@ -44,7 +64,7 @@ def select_payment_type(self, payment_option: PaymentOptions): f'It should be one of: {PaymentOptions}' ) - item = Button( + item = elements.Button( self.driver, (By.CSS, f'input[name="payment_type"][value="{payment_option.name}"]'), ) diff --git a/shopelectro/selenium/pages/product.py b/shopelectro/selenium/pages/product.py index 097ab372..b15e49bd 100644 --- a/shopelectro/selenium/pages/product.py +++ b/shopelectro/selenium/pages/product.py @@ -18,5 +18,5 @@ def path(self): return reverse('product', args=(self.vendor_code,)) def add_to_cart(self): - with self.cart().wait_changes(): - elements.ProductCard(self.driver).add_to_cart() + with self.cart().positions.wait_changes(): + elements.ProductCard(self.driver).add() diff --git a/shopelectro/tests/tests_js_analytics.py b/shopelectro/tests/tests_js_analytics.py index 0ba30e27..d9925fce 100644 --- a/shopelectro/tests/tests_js_analytics.py +++ b/shopelectro/tests/tests_js_analytics.py @@ -87,8 +87,8 @@ class YandexEcommerce(Ecommerce): fixtures = ['dump.json'] - # @todo #820:120m Test Yandex ecommerce add and remove goals from the order page. - # Get rid of code duplications. + # @todo #939:120m Test Yandex ecommerce increase/decrease/set goals from the order page. + # Create assertion for cart clear goal. def tearDown(self): # delete the session to clear the cart @@ -100,6 +100,53 @@ def get_goals(self) -> selenium.Goals: goals.fetch() return goals + def assert_add(self, product: Product, goal_position: int): # Ignore CPDBear + reached_goals = self.get_goals() + self.assertTrue(reached_goals) + reached = reached_goals[goal_position] + + self.assertIn('add', reached) + self.assertEqual(reached['currencyCode'], 'RUB') + + reached_detail = reached['add'] # Ignore CPDBear + self.assertEqual( + len(reached_detail['products']), + 1, + ) + + self.assertEqual( + reached_detail['products'][0], + { + 'id': product.id, + 'name': product.name, + 'brand': product.get_brand_name(), + 'quantity': 1, + 'category': product.category.name, + } + ) + + def assert_remove(self, product: Product, goal_position: int): + reached_goals = self.get_goals() + self.assertTrue(reached_goals) + + reached = reached_goals[goal_position] + self.assertIn('remove', reached) + self.assertEqual(reached['currencyCode'], 'RUB') + + reached_remove = reached['remove'] + self.assertEqual( + len(reached_remove['products']), + 1, + ) + + self.assertEqual( + reached_remove['products'][0], + { + 'id': product.id, + 'quantity': 1, + } + ) + def test_purchase(self): self.buy() order = self.last_order() @@ -139,7 +186,7 @@ def test_product_detail(self): self.assertIn('detail', reached) self.assertEqual(reached['currencyCode'], 'RUB') - reached_detail = reached['detail'] # Ignore CPDBear + reached_detail = reached['detail'] self.assertEqual( len(reached_detail['products']), 1, @@ -161,28 +208,9 @@ def test_clear_cart(self): # Ignore CPDBear page = selenium.Product(self.browser, product.vendor_code) page.load() page.add_to_cart() - page.cart().clear() # Ignore CPDBear - - reached_goals = self.get_goals() - self.assertTrue(reached_goals) + page.cart().clear() - reached = reached_goals[2] - self.assertIn('remove', reached) - self.assertEqual(reached['currencyCode'], 'RUB') - - reached_remove = reached['remove'] - self.assertEqual( - len(reached_remove['products']), - 1, - ) - - self.assertEqual( - reached_remove['products'][0], - { - 'id': product.id, - 'quantity': 1, - } - ) + self.assert_remove(product, 2) def test_remove_from_cart(self): product = Product.objects.first() @@ -190,28 +218,9 @@ def test_remove_from_cart(self): page.load() page.add_to_cart() cart = page.cart() - cart.remove(cart.positions()[0]) - - reached_goals = self.get_goals() - self.assertTrue(reached_goals) - - reached = reached_goals[2] - self.assertIn('remove', reached) - self.assertEqual(reached['currencyCode'], 'RUB') - - reached_remove = reached['remove'] - self.assertEqual( - len(reached_remove['products']), - 1, - ) + cart.remove(cart.positions.first()) - self.assertEqual( - reached_remove['products'][0], - { - 'id': product.id, - 'quantity': 1, - } - ) + self.assert_remove(product, 2) def test_add_from_product_page(self): product = Product.objects.first() @@ -219,29 +228,7 @@ def test_add_from_product_page(self): page.load() page.add_to_cart() - reached_goals = self.get_goals() - self.assertTrue(reached_goals) - - reached = reached_goals[1] # Ignore CPDBear - self.assertIn('add', reached) - self.assertEqual(reached['currencyCode'], 'RUB') - - reached_detail = reached['add'] - self.assertEqual( - len(reached_detail['products']), - 1, - ) - - self.assertEqual( - reached_detail['products'][0], - { - 'id': product.id, - 'name': product.name, - 'brand': product.get_brand_name(), - 'quantity': 1, - 'category': product.category.name, - } - ) + self.assert_add(product, 1) def test_add_from_category_page(self): product = Product.objects.first() @@ -250,29 +237,28 @@ def test_add_from_category_page(self): card = page.find_card(product.id) page.add_to_cart([card]) - reached_goals = self.get_goals() - self.assertTrue(reached_goals) + self.assert_add(product, 0) - reached = reached_goals[0] - self.assertIn('add', reached) - self.assertEqual(reached['currencyCode'], 'RUB') + def test_remove_from_order_page(self): + product = Product.objects.first() + product_page = selenium.Product(self.browser, product.vendor_code) + product_page.load() + product_page.add_to_cart() - reached_detail = reached['add'] - self.assertEqual( - len(reached_detail['products']), - 1, - ) + order_page = selenium.OrderPage(self.browser) + order_page.load() + order_page.remove(order_page.positions.first()) - self.assertEqual( - reached_detail['products'][0], - { - 'id': product.id, - 'name': product.name, - 'brand': product.get_brand_name(), - 'quantity': 1, - 'category': product.category.name, - } - ) + self.assert_remove(product, 0) + + def test_increase_from_order_page(self): + pass + + def test_decrease_from_order_page(self): + pass + + def test_set_from_order_page(self): + pass @tag('slow')