simplify tests

This commit is contained in:
a.krasnov
2020-12-01 23:41:11 +03:00
parent 3b5714d6ef
commit 44662ae0a4
61 changed files with 235 additions and 1591 deletions
-12
View File
@@ -1,6 +1,5 @@
import pytest
import os
from screenshot_tests.page_objects.elements import PageBoundGeneric
from conftest import Config
from typing import Type
@@ -16,14 +15,3 @@ class TestCase:
def configure(self, request):
self.base_url = request.config.getoption(Config.BASE_URL)
self.staging = request.config.getoption(Config.STAGING)
def get_page(self, page_class: Type[PageBoundGeneric]) -> PageBoundGeneric:
"""Create instance of web page and return it."""
path = page_class.path
if path is None:
raise TypeError(f"Path in {page_class} is None!")
page = page_class(self.driver)
self.driver.get(os.path.join(self.base_url, path))
return page
+114 -64
View File
@@ -1,29 +1,31 @@
import pytest
import time
"""Screenshot TestCase."""
import logging
import allure
import pytest
import time
from screenshot_tests.page_objects.custom_web_element import CustomWebElement
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from urllib.parse import urlparse
from screenshot_tests.page_objects.elements import Locators
from screenshot_tests.utils import common
from screenshot_tests.image_proccessing.image_processor import ImageProcessor
from typing import Tuple
from PIL import Image
# noinspection PyAttributeOutsideInit
# аннотируем все классы всех скриншот тестов для работы плагина
# https://github.com/allure-framework/allure2/tree/master/plugins/screen-diff-plugin
@allure.label('testType', 'screenshotDiff')
class TestCase(common.TestCase):
"""Base class for all screenshot tests."""
"""Screenshot TestCase."""
BODY_LOCATOR = (Locators.CSS_SELECTOR, "body")
# Для мобильных устройств и хрома в режиме эмуляции плотность пикселей будет отличаться.
pixel_ratio = 1
@pytest.fixture(autouse=True)
def screenshot_prepare(self):
self.image_processor = ImageProcessor()
# количество попыток для снятия скриншота
self.attempts = 5
def _scroll(self, x: int, y: int):
scroll_string = f"window.scrollTo({x}, {y})"
@@ -31,9 +33,26 @@ class TestCase(common.TestCase):
time.sleep(0.2)
logging.info(f"Scroll to «{scroll_string}»")
def _make_screenshot_whole_page(self):
total_width = self.driver.execute_script("return document.body.offsetWidth")
def _make_screenshot_whole_page(self, locator_type, query_string):
scroll_time = 0.2
# Нужно заставить отработать все что есть с автоподгрузкой, чтобы получить настоящую длину страницы
x, y, width, height = self._get_raw_coords_by_locator(locator_type, query_string)
total_height = self.driver.execute_script("return document.body.parentNode.scrollHeight")
logging.info(f"total height: {total_height}")
while True:
old_total_height = total_height
self._scroll(0, total_height + 9999)
time.sleep(scroll_time)
total_height = self.driver.execute_script("return document.body.parentNode.scrollHeight")
logging.info(f"new total height: {total_height}")
# Если высота перестала изменяться, или элемент уже попал на скриншот.
# Второе условие позволяет не скролить до конца на стрницах с "бесконечной" длинной (выдача видео, картинок)
if (old_total_height == total_height) or (total_height > y):
break
total_width = self.driver.execute_script("return document.body.offsetWidth")
viewport_width = self.driver.execute_script("return document.body.clientWidth")
viewport_height = self.driver.execute_script("return window.innerHeight")
screenshots = []
@@ -42,16 +61,31 @@ class TestCase(common.TestCase):
self._scroll(0, 0)
while offset <= total_height:
logging.info(f"offset: {offset}, total height: {total_height}")
screenshots.append(self.driver.get_screenshot_as_png())
offset += viewport_height
self._scroll(0, offset)
return self.image_processor.paste(screenshots)
# эта часть последнего скриншота, которая дублирует предпоследний скриншот
# так просходит потому что не всегда страница делится на целое количество вьюпортов
over_height = offset - total_height
logging.info(f"offset: {offset}, total height: {total_height}, over height: {over_height}, pixel density: {self.pixel_ratio}")
return self.image_processor.paste(screenshots, over_height * self.pixel_ratio)
def _get_coords_by_locator(self, by, locator) -> Tuple[int, int, int, int]:
def _use_full_screen(self):
# хак чтобы снять целиком элемент который не помещается на страницу
# https://stackoverflow.com/questions/44085722/how-to-get-screenshot-of-full-webpage-using-selenium-and-java
# https://gist.github.com/elcamino/5f562564ecd2fb86f559
self.driver.set_window_size(1425, 2900)
def _get_raw_coords_by_locator(self, locator_type, query_string):
"""Без учета плотности пикселей."""
wait = WebDriverWait(self.driver, timeout=10, ignored_exceptions=Exception)
wait.until(lambda _: self.driver.find_element(locator_type, query_string).is_displayed(),
message="Невозможно получить размеры элемента, элемент не отображается")
# После того, как дождались видимости элемента, ждем еще 2 секунды, чтобы точно завершились разные анимации
time.sleep(2)
el = self.driver.find_element(by, locator)
el = self.driver.find_element(locator_type, query_string)
location = el.location
size = el.size
x = location["x"]
@@ -60,76 +94,93 @@ class TestCase(common.TestCase):
height = location["y"] + size['height']
return x, y, width, height
def _get_element_screenshot(self, by, locator, action, finalize) \
-> Tuple[Image.Image, Tuple[int, int, int, int]]:
"""Get screenshot of element.
def _get_coords_by_locator(self, locator_type, query_string) -> Tuple[int, int, int, int]:
x, y, width, height = self._get_raw_coords_by_locator(locator_type, query_string)
return x * self.pixel_ratio, y * self.pixel_ratio, width * self.pixel_ratio, height * self.pixel_ratio
Can't use session/{sessionId}/element/{elementId}/screenshot because it's available only in Edge
def _get_element_screenshot(self,
locator_type,
query_string,
action,
finalize,
scroll_and_screen) \
-> Tuple[Image.Image, Tuple[int, int, int, int]]:
"""Сделать скриншот страницы и кропнуть до скриншота элемента.
Не получится использовать метод session/{sessionId}/element/{elementId}/screenshot
Потому что он имплементирован только в эдж.
https://stackoverflow.com/questions/36084257/im-trying-to-take-a-screenshot-of-an-element-with-selenium-webdriver-but-unsup
"""
if not scroll_and_screen:
# Иногда страница по дефолту открыта посередине, чтобы не ползли координаты
# элемента с scroll_and_screen=False, скролим до начала. Это нужно делать до вызова action, на случай если
# в action страницу нужно проскролить до определенной точки.
self._scroll(0, 0)
# Тут готовим страницу к снятию скриншота
if callable(action):
action()
coordinates = self._get_coords_by_locator(by, locator)
screen = self._make_screenshot_whole_page()
logging.info(f"element: {locator}, coordinates: {coordinates}")
if scroll_and_screen:
screen = self._make_screenshot_whole_page(locator_type, query_string)
else:
screen = self.image_processor.load_image_from_bytes(self.driver.get_screenshot_as_png())
coordinates = self._get_coords_by_locator(locator_type, query_string)
logging.info(f"element: {query_string}, coordinates: {coordinates}")
# Тут можно выполнить дополнительные проверки после снятия скрина
if callable(finalize):
finalize()
return screen.crop(coordinates), coordinates
def _get_diff(self, element: CustomWebElement, action=None, full_page=False, finalize=None):
"""Get screenshot from test environment and compare with production.
def _get_diff(self, element, action=None, full_screen=True, full_page=False, finalize=None, scroll_and_screen=True):
"""Получит скриншоты с текущей страницы, и с эталонной.
:param element: element for check (instance of CustomWebElement)
:param action: callback executed before making screenshot. Use it when need prepare page for screenshot.
:param full_page: ignore element, and compare whole page.
:param finalize: callback executed after screenshot.
Поблочно сравнит их, и вернет количество отличающихся блоков.
:param element: любой объект у которого есть свойства locator_type, и query_string (по ним будет найден элемент)
:param action: функция которая подготовит страницу к снятию скриншота
:param full_screen: ресайзить ли браузер до максимума
:param full_page: скринить всю страницу, а не только переданный элемент
:param finalize: финализация после сравнения скриншотов
:param scroll_and_screen: скролить страницу (сверху к низу) и склеивать участки в один скриншот
"""
if full_screen:
self._use_full_screen()
if full_page:
by, locator = self.BODY_LOCATOR
locator_type, query_string = (By.XPATH, "//body")
scroll_and_screen = False
else:
by, locator = element.by, element.locator
locator_type, query_string = element.locator_type, element.query_string
saved_url = urlparse(self.driver.current_url)
# noinspection PyProtectedMember
prod_url = saved_url._replace(netloc=urlparse(self.staging).netloc)
prod_url = saved_url._replace(netloc=self.staging)
# Открываем странички пока размеры элементов на них не совпадут
coords_equal = False
attempts = 0
while not coords_equal and attempts < self.attempts:
logging.info(f'Try make screenshots. Attempts: {attempts}')
# На текущей странице делаем первый скриншот
first_image, coords_test = self._get_element_screenshot(locator_type, query_string, action, finalize,
scroll_and_screen)
logging.info('Done screen on test stand')
# Теперь делаем скриншот в проде
self.driver.get(prod_url.geturl())
second_image, coords_prod = self._get_element_screenshot(locator_type, query_string, action, finalize,
scroll_and_screen)
logging.info('Done screen on stage stand')
# На текущей странице делаем первый скриншот
first_image, coords_test = self._get_element_screenshot(by, locator, action, finalize)
# Теперь делаем скриншот в проде
self.driver.get(prod_url.geturl())
second_image, coords_prod = self._get_element_screenshot(by, locator, action, finalize)
# Возращаемся на тестовый стенд. Всегда нужно возвращаться на тестовый стенд. На это завязаны тесты и отчеты
self.driver.get(saved_url.geturl())
# Если размеры элементов на странице не совпали, и выбраны не все попытки, пробуем снова
attempts += 1
# По размеру блока, оставим на случай, если по координатам способ будет не работать не очень
x1, y1, x2, y2 = coords_test
x_1, y_1, x_2, y_2 = coords_prod
size_test = round(((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5)
size_prod = round(((x_2 - x_1) ** 2 + (y_2 - y_1) ** 2) ** 0.5)
logging.info(f"Coords test: {coords_test}, coords prod: {coords_prod}")
logging.info(f"Size test: {size_test}, size prod: {size_prod}")
coords_equal = size_prod == size_test
# noinspection PyUnboundLocalVariable
diff, result, first, second = self.image_processor.get_images_diff(first_image, second_image)
# Возращаемся на тестовый стенд. Всегда нужно возвращаться на тестовый стенд. На это завязаны тесты и отчеты
self.driver.get(saved_url.geturl())
# Для добавления в отчет (https://github.com/allure-framework/allure2/tree/master/plugins/screen-diff-plugin)
allure.attach(result, "diff", allure.attachment_type.PNG)
allure.attach(first, "actual", allure.attachment_type.PNG)
allure.attach(second, "expected", allure.attachment_type.PNG)
# noinspection PyUnboundLocalVariable
allure.attach(self.image_processor.image_to_bytes(first_image), 'actual', allure.attachment_type.PNG)
# noinspection PyUnboundLocalVariable
allure.attach(self.image_processor.image_to_bytes(second_image), 'expected', allure.attachment_type.PNG)
# noinspection PyUnboundLocalVariable
diff, result = self.image_processor.get_images_diff(first_image, second_image)
allure.attach(result, 'diff', allure.attachment_type.PNG)
return diff, saved_url, prod_url
@@ -137,7 +188,6 @@ class TestCase(common.TestCase):
diff, _, _ = self._get_diff(*args, **kwargs)
return diff
def check_by_screenshot(self, element: CustomWebElement, *args, **kwargs):
def check_by_screenshot(self, element, *args, **kwargs):
diff, saved_url, prod_url = self._get_diff(element, *args, **kwargs)
info = element.description
assert diff == 0, f"{info} отличается на страницах:\n{saved_url.geturl()}\nи\n{prod_url.geturl()}"
assert diff == 0, f"Элемент отличается на страницах:\n{saved_url.geturl()}\nи\n{prod_url.geturl()}"