simplify tests
This commit is contained in:
@@ -1,22 +1,36 @@
|
||||
from PIL import ImageDraw, Image
|
||||
from io import BytesIO
|
||||
from typing import Tuple, List
|
||||
|
||||
import logging
|
||||
from io import BytesIO
|
||||
from typing import Union, List
|
||||
|
||||
from PIL import ImageDraw, Image
|
||||
|
||||
|
||||
class ImageProcessor(object):
|
||||
"""Class for image comparison."""
|
||||
"""Класс для обработки изображений (нарезки и сравнения)"""
|
||||
|
||||
RED = "red"
|
||||
GREEN = "green"
|
||||
BLUE = "blue"
|
||||
ALPHA = "alpha"
|
||||
|
||||
# https://github.com/rsmbl/Resemble.js/blob/dec5ae1cf1d10c9027a94400a81c17d025a9d3b6/resemble.js#L121
|
||||
# https://github.com/rsmbl/Resemble.js/blob/dec5ae1cf1d10c9027a94400a81c17d025a9d3b6/resemble.js#L981
|
||||
tolerance = {
|
||||
RED: 32,
|
||||
GREEN: 32,
|
||||
BLUE: 32,
|
||||
ALPHA: 32,
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self._block_width = 20 # default
|
||||
self._block_height = 20
|
||||
self._accuracy = 0.0001 # less better
|
||||
self._block_width = 40 # default
|
||||
self._block_height = 40
|
||||
|
||||
def _slice_image(self, image: Image.Image) -> List[dict]:
|
||||
"""Slice image on small blocks."""
|
||||
"""Нарезать картинки на блоки"""
|
||||
max_width, max_height = image.size
|
||||
|
||||
# нижний правый угол для кропа
|
||||
width_change = self._block_width
|
||||
height_change = self._block_height
|
||||
|
||||
@@ -44,20 +58,53 @@ class ImageProcessor(object):
|
||||
|
||||
return result
|
||||
|
||||
def _get_image_pixel_sum(self, image: Image.Image) -> int:
|
||||
"""Get pixel sum for image."""
|
||||
image_total = 0
|
||||
max_width, max_height = image.size
|
||||
def _is_color_similar(self, a, b, color):
|
||||
"""Проверить похожесть цветов. Для того, чтобы тесты не тригеррились на антиалиазинг допуски
|
||||
|
||||
в self.tolerance.
|
||||
"""
|
||||
if a is None and b is None:
|
||||
return True
|
||||
|
||||
diff = abs(a - b)
|
||||
|
||||
if diff == 0:
|
||||
return True
|
||||
elif diff < self.tolerance[color]:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _compare_images(self, image_one: Image.Image, image_two: Image.Image) -> bool:
|
||||
"""Сравнить два изображения попиксельно"""
|
||||
assert image_one.size == image_two.size, \
|
||||
f"Картинки должны быть одинакового размера, {image_one.size} {image_two.size}"
|
||||
|
||||
max_width, max_height = image_one.size
|
||||
|
||||
for coord_y in range(0, max_height):
|
||||
for coord_x in range(0, max_width):
|
||||
pixel = image.getpixel((coord_x, coord_y))
|
||||
image_total += sum(pixel)
|
||||
pixel_one = image_one.getpixel((coord_x, coord_y))
|
||||
pixel_two = image_two.getpixel((coord_x, coord_y))
|
||||
equal = self._compare_pixels(pixel_one, pixel_two)
|
||||
if not equal:
|
||||
return False
|
||||
|
||||
return image_total
|
||||
return True
|
||||
|
||||
def get_images_diff(self, first_image: Image.Image, second_image: Image.Image) -> Tuple[int, bytes, bytes, bytes]:
|
||||
"""Compare two images."""
|
||||
def _compare_pixels(self, pixel_one, pixel_two) -> bool:
|
||||
"""Сравнить каждый цвет, каждого писклея."""
|
||||
assert len(pixel_one) == len(pixel_two), f"В одном из пикселей не хватает цветов: {pixel_one} {pixel_two}"
|
||||
|
||||
for item in zip(pixel_one, pixel_two, (self.RED, self.GREEN, self.BLUE, self.ALPHA)):
|
||||
color_one, color_two, color = item
|
||||
if not self._is_color_similar(color_one, color_two, color):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def get_images_diff(self, first_image: Image.Image, second_image: Image.Image) -> List[Union[int, bytes]]:
|
||||
"""Поблочно сравнить два изображения и вернуть количество блоков с несовпавшими пикселями"""
|
||||
result_image = first_image.copy()
|
||||
|
||||
first_image_blocks = self._slice_image(first_image)
|
||||
@@ -68,45 +115,60 @@ class ImageProcessor(object):
|
||||
mistaken_blocks = abs(len(first_image_blocks) - len(second_image_blocks))
|
||||
|
||||
for index in range(min(len(first_image_blocks), len(second_image_blocks))):
|
||||
first_pixels = self._get_image_pixel_sum(first_image_blocks[index]["image"])
|
||||
second_pixels = self._get_image_pixel_sum(second_image_blocks[index]["image"])
|
||||
image_equal = self._compare_images(first_image_blocks[index]["image"], second_image_blocks[index]["image"])
|
||||
|
||||
# если пиксели отличаются больше чем на self.accuracy -- помечаем блок как битый
|
||||
if (first_pixels != 0 and second_pixels != 0) and abs(1 - (first_pixels / second_pixels)) >= self._accuracy:
|
||||
if not image_equal:
|
||||
draw = ImageDraw.Draw(result_image)
|
||||
draw.rectangle(first_image_blocks[index]["box"], outline="red")
|
||||
mistaken_blocks += 1
|
||||
|
||||
result = BytesIO()
|
||||
first = BytesIO()
|
||||
second = BytesIO()
|
||||
return [mistaken_blocks, self.image_to_bytes(result_image)]
|
||||
|
||||
result_image.save(result, 'PNG')
|
||||
first_image.save(first, 'PNG')
|
||||
second_image.save(second, 'PNG')
|
||||
|
||||
return mistaken_blocks, result.getvalue(), first.getvalue(), second.getvalue()
|
||||
|
||||
def paste(self, screenshots: List[bytes]) -> Image.Image:
|
||||
"""Concatenate few images into one."""
|
||||
def paste(self, screenshots: List[bytes], over_height: int) -> Image.Image:
|
||||
"""Склеить массив скриншотов в одно изображение"""
|
||||
max_width = 0
|
||||
max_height = 0
|
||||
images = []
|
||||
|
||||
for screenshot in screenshots:
|
||||
image = self.load_image_from_bytes(screenshot)
|
||||
images.append(image)
|
||||
max_width = image.size[0] if image.size[0] > max_width else max_width
|
||||
max_height += image.size[1]
|
||||
|
||||
# Склейка работает так: сначала создаем одно "пустое" изображение равное размеру всех скелееных, и вставляем в
|
||||
# в него по одному все скриншоты.
|
||||
# Чтобы в финальном скрине не получилось что скриншоты заняли меньше места, чем картинка, снизу отрезаем over_height
|
||||
max_height = max_height - over_height
|
||||
result = Image.new('RGB', (max_width, max_height))
|
||||
logging.info(f'Screen size: ({max_width}, {max_height})')
|
||||
|
||||
offset = 0
|
||||
for image in images:
|
||||
last_image_index = len(images) - 1
|
||||
for index, image in enumerate(images):
|
||||
# Расскоментить если нужно посмотреть какие скрины склеиваются в один
|
||||
# with open(f"screen-{index}.png", "wb") as fp:
|
||||
# image.save(fp)
|
||||
|
||||
if last_image_index == index and over_height != 0:
|
||||
# с последнего скриншота срезаем ту часть, в которой он дублирует предпоследний
|
||||
logging.info(f"Crop over height: {over_height}")
|
||||
image = image.crop((0, over_height, image.size[0], image.size[1]))
|
||||
|
||||
result.paste(image, (0, offset))
|
||||
logging.info(f"Image added, offset is {offset}")
|
||||
offset += image.size[1]
|
||||
|
||||
return result
|
||||
|
||||
def load_image_from_bytes(self, data: bytes):
|
||||
def load_image_from_bytes(self, data: bytes) -> Image.Image:
|
||||
"""Загрузить изображение из байтовой строки."""
|
||||
return Image.open(BytesIO(data))
|
||||
with BytesIO(data) as fp:
|
||||
image: Image.Image = Image.open(fp)
|
||||
image.load()
|
||||
return image
|
||||
|
||||
def image_to_bytes(self, image: Image.Image) -> bytes:
|
||||
with BytesIO() as fp:
|
||||
image.save(fp, "PNG")
|
||||
return fp.getvalue()
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import allure
|
||||
from selenium.webdriver.remote.webelement import WebElement
|
||||
|
||||
|
||||
class CustomWebElement:
|
||||
"""Custom web element with allure logging."""
|
||||
|
||||
def __init__(self, by: str, locator: str, element: WebElement, description: str = None):
|
||||
self.by = by
|
||||
self.locator = locator
|
||||
self.element = element
|
||||
self.description = f"«{description}»" if description else "element"
|
||||
|
||||
def _execute_action(self, action, step):
|
||||
"""Execute action with allure logging.
|
||||
|
||||
:param action: Function to execute. Click, send_keys, etc
|
||||
:param step: Step description.
|
||||
"""
|
||||
@allure.step(step)
|
||||
def execute_action(locator_type=self.by, locator=self.locator):
|
||||
"""All arguments will be available in report."""
|
||||
return action()
|
||||
|
||||
return execute_action()
|
||||
|
||||
def click(self):
|
||||
self._execute_action(self.element.click,
|
||||
f"Click at {self.description}")
|
||||
|
||||
def send_keys(self, *value):
|
||||
self._execute_action(lambda: self.element.send_keys(*value),
|
||||
f"Send text {[v for v in value]} to {self.description}")
|
||||
|
||||
def __eq__(self, element):
|
||||
return self.element.__eq__(element)
|
||||
|
||||
def __ne__(self, element):
|
||||
return self.element.__ne__(element)
|
||||
|
||||
def __hash__(self):
|
||||
return self.element.__hash__()
|
||||
|
||||
def __getattr__(self, item):
|
||||
"""Missing methods will be executed from WebElement."""
|
||||
return getattr(self.element, item)
|
||||
@@ -1,42 +0,0 @@
|
||||
from selenium.webdriver import Remote
|
||||
from selenium.webdriver.common import by as selenium_by
|
||||
from screenshot_tests.page_objects.custom_web_element import CustomWebElement
|
||||
from typing import Union, TypeVar, Type
|
||||
|
||||
Locators = selenium_by.By
|
||||
|
||||
|
||||
class Page:
|
||||
"""Base page for all pages in PO."""
|
||||
|
||||
path = None
|
||||
|
||||
def __init__(self, driver: Remote):
|
||||
self.driver = driver
|
||||
|
||||
|
||||
PageBoundGeneric = TypeVar("PageBoundGeneric", bound=Page)
|
||||
|
||||
|
||||
class Element:
|
||||
"""Element descriptor for WebElement lazy init."""
|
||||
|
||||
def __init__(self, by: str, locator: str, description: str):
|
||||
self.by = by
|
||||
self.locator = locator
|
||||
self.description = description
|
||||
|
||||
def __get__(self,
|
||||
instance: PageBoundGeneric,
|
||||
owner: Type[PageBoundGeneric]) -> Union[CustomWebElement, 'Element']:
|
||||
"""
|
||||
https://docs.python.org/3/howto/descriptor.html
|
||||
:param instance: instance of owner
|
||||
:param owner: type of owner
|
||||
:return: self or CustomWebElement instance
|
||||
"""
|
||||
if isinstance(instance, Element):
|
||||
return self
|
||||
|
||||
return CustomWebElement(self.by, self.locator, instance.driver.find_element(self.by, self.locator),
|
||||
self.description)
|
||||
@@ -1,11 +0,0 @@
|
||||
from screenshot_tests.page_objects.elements import Page, Element, Locators
|
||||
|
||||
|
||||
class YandexMainPage(Page):
|
||||
"""https://yandex.ru"""
|
||||
|
||||
path = ""
|
||||
|
||||
news_header = Element(Locators.CSS_SELECTOR, ".news__header", "Хэдер с новостями")
|
||||
search_field = Element(Locators.CSS_SELECTOR, ".search2", "Поисковый блок")
|
||||
search_input = Element(Locators.CSS_SELECTOR, ".input__control", "Поисковый инпут")
|
||||
@@ -0,0 +1,20 @@
|
||||
from screenshot_tests.utils.screenshots import TestCase
|
||||
|
||||
|
||||
class TestExample(TestCase):
|
||||
"""Tests for https://go.mail.ru"""
|
||||
|
||||
def test_main_page(self):
|
||||
self.driver.get("https://go.mail.ru/")
|
||||
|
||||
def action():
|
||||
# Убираем фокус с инпута, чтобы тест не флакал из-за курсора
|
||||
self.driver.find_element_by_xpath("//*[text()='найти']").click()
|
||||
|
||||
self.check_by_screenshot(None, action=action, full_page=True)
|
||||
|
||||
def test_main_page_flaky(self):
|
||||
self.driver.get("https://go.mail.ru/")
|
||||
# Чтобы посмотреть как выглядит сломанный тест в отчетеы
|
||||
self.driver.find_element_by_xpath("//input[not(@type='hidden')]").send_keys("foo")
|
||||
self.check_by_screenshot(None, full_page=True)
|
||||
@@ -1,21 +0,0 @@
|
||||
from screenshot_tests.utils.screenshots import TestCase
|
||||
from screenshot_tests.page_objects.pages.yandex_main_page import YandexMainPage
|
||||
import random
|
||||
|
||||
|
||||
class TestYandexMainPage(TestCase):
|
||||
"""Tests for https://yandex.ru"""
|
||||
|
||||
def test_news_widget(self):
|
||||
"""Test for news widget."""
|
||||
page = self.get_page(YandexMainPage)
|
||||
self.check_by_screenshot(page.news_header)
|
||||
|
||||
def test_search_field(self):
|
||||
words = ["foo", "bar", "lol", "kek", "cheburek", "otus", "yandex", "google"]
|
||||
page = self.get_page(YandexMainPage)
|
||||
|
||||
def action():
|
||||
page.search_input.send_keys(random.choice(words))
|
||||
|
||||
self.check_by_screenshot(page.search_field, action)
|
||||
@@ -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
|
||||
|
||||
@@ -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()}"
|
||||
|
||||
Reference in New Issue
Block a user