Update examples

This commit is contained in:
Mikhail Nestuley
2022-07-01 10:23:43 +03:00
parent 1ef0520d1e
commit 8e412fb6bc
9 changed files with 187 additions and 51 deletions

136
.gitignore vendored
View File

@@ -1,3 +1,135 @@
venv # Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# Pycharm
.idea .idea
__pycache__
# Logs
logs/*-report

View File

@@ -2,6 +2,7 @@ import pytest
import logging import logging
import allure import allure
from selenium.webdriver import Chrome, ChromeOptions from selenium.webdriver import Chrome, ChromeOptions
from webdriver_manager.chrome import ChromeDriverManager
class Config: class Config:
@@ -13,7 +14,7 @@ class Config:
def driver(): def driver():
options = ChromeOptions() options = ChromeOptions()
options.add_argument("--headless") options.add_argument("--headless")
webdriver = Chrome(desired_capabilities=options.to_capabilities()) webdriver = Chrome(ChromeDriverManager().install(), desired_capabilities=options.to_capabilities())
webdriver.implicitly_wait(5) webdriver.implicitly_wait(5)
yield webdriver yield webdriver
allure.attach(webdriver.current_url, "last url", allure.attachment_type.URI_LIST) allure.attach(webdriver.current_url, "last url", allure.attachment_type.URI_LIST)

0
logs/.gitkeep Normal file
View File

View File

@@ -1,7 +1,7 @@
### Example of screenshot testing in python ### Example of screenshot testing in python
How to: How to:
1) Install python 3.6+ 1) Install python 3.10+
2) Install deps `pip install requirements.txt` 2) Install deps `pip install requirements.txt`
3) Run tests pytest screenshot_tests/tests --alluredir=report 3) Run tests `pytest screenshot_tests/tests --alluredir=logs/allure-report`
4) Generate and open report with Allure CLI [link](https://docs.qameta.io/allure/#_commandline) 4) Generate and open report: `allure serve logs/allure-report`

View File

@@ -1,5 +1,5 @@
pytest==5.1.2 pytest==7.1.2
flaky==3.6.1 flaky==3.7.0
selenium==3.141.0 selenium==4.3.0
allure-pytest==2.8.2 allure-pytest==2.9.45
Pillow==6.2.2 Pillow==9.1.1

View File

@@ -136,9 +136,8 @@ class ImageProcessor(object):
max_width = image.size[0] if image.size[0] > max_width else max_width max_width = image.size[0] if image.size[0] > max_width else max_width
max_height += image.size[1] max_height += image.size[1]
# Склейка работает так: сначала создаем одно "пустое" изображение равное размеру всех скелееных, и вставляем в # Склейка работает так: сначала создаем одно "пустое" изображение равное размеру всех скелееных, и вставляем в него по одному все скриншоты.
# в него по одному все скриншоты. # Чтобы в финальном скрине не получилось что скриншоты заняли меньше места, чем картинка, снизу отрезаем over_height.
# Чтобы в финальном скрине не получилось что скриншоты заняли меньше места, чем картинка, снизу отрезаем over_height
max_height = max_height - over_height max_height = max_height - over_height
result = Image.new('RGB', (max_width, max_height)) result = Image.new('RGB', (max_width, max_height))
logging.info(f'Screen size: ({max_width}, {max_height})') logging.info(f'Screen size: ({max_width}, {max_height})')
@@ -161,14 +160,16 @@ class ImageProcessor(object):
return result return result
def load_image_from_bytes(self, data: bytes) -> Image.Image: @staticmethod
def load_image_from_bytes(data: bytes) -> Image.Image:
"""Загрузить изображение из байтовой строки.""" """Загрузить изображение из байтовой строки."""
with BytesIO(data) as fp: with BytesIO(data) as fp:
image: Image.Image = Image.open(fp) image: Image.Image = Image.open(fp)
image.load() image.load()
return image return image
def image_to_bytes(self, image: Image.Image) -> bytes: @staticmethod
def image_to_bytes(image: Image.Image) -> bytes:
with BytesIO() as fp: with BytesIO() as fp:
image.save(fp, "PNG") image.save(fp, "PNG")
return fp.getvalue() return fp.getvalue()

View File

@@ -1,6 +1,7 @@
from screenshot_tests.utils.screenshots import TestCase
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from screenshot_tests.utils.screenshots import TestCase
class TestExample(TestCase): class TestExample(TestCase):
"""Tests for https://go.mail.ru""" """Tests for https://go.mail.ru"""
@@ -10,14 +11,14 @@ class TestExample(TestCase):
def action(): def action():
# Убираем фокус с инпута, чтобы тест не флакал из-за курсора # Убираем фокус с инпута, чтобы тест не флакал из-за курсора
self.driver.find_element_by_xpath("//*[text()='найти']").click() self.driver.find_element(By.XPATH, "//*[text()='найти']").click()
self.check_by_screenshot(None, action=action, full_page=True) self.check_by_screenshot(None, action=action, full_page=True)
def test_main_page_flaky(self): def test_main_page_flaky(self):
self.driver.get("https://go.mail.ru/") self.driver.get("https://go.mail.ru/")
# Чтобы посмотреть как выглядит сломанный тест в отчетe # Чтобы посмотреть как выглядит сломанный тeест в отчетe
self.driver.find_element_by_xpath("//input[not(@type='hidden')]").send_keys("foo") self.driver.find_element(By.XPATH, "//input[not(@type='hidden')]").send_keys("foo")
self.check_by_screenshot(None, full_page=True) self.check_by_screenshot(None, full_page=True)
def test_search_block(self): def test_search_block(self):
@@ -25,6 +26,6 @@ class TestExample(TestCase):
def action(): def action():
# Тестируем подсветку таба после переключения на другую вертикаль # Тестируем подсветку таба после переключения на другую вертикаль
self.driver.find_element_by_xpath("//span[contains(text(), 'Соцсети')]").click() self.driver.find_element(By.XPATH, "//span[contains(text(), 'Соцсети')]").click()
self.check_by_screenshot((By.CSS_SELECTOR, ".MainVerticalsNav-listItemActive"), action=action) self.check_by_screenshot((By.CSS_SELECTOR, ".MainVerticalsNav-listItemActive"), action=action)

View File

@@ -1,9 +1,9 @@
import pytest import pytest
import os
from conftest import Config from conftest import Config
from typing import Type
# noinspection PyAttributeOutsideInit
class TestCase: class TestCase:
"""Base class for all tests.""" """Base class for all tests."""

View File

@@ -1,17 +1,19 @@
"""Screenshot TestCase.""" """Screenshot TestCase."""
import logging import logging
import time
from typing import Tuple
from urllib.parse import urlparse
import allure import allure
import pytest import pytest
import time from PIL import Image
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support.ui import WebDriverWait
from urllib.parse import urlparse
from screenshot_tests.utils import common
from screenshot_tests.image_proccessing.image_processor import ImageProcessor from screenshot_tests.image_proccessing.image_processor import ImageProcessor
from typing import Tuple from screenshot_tests.utils import common
from PIL import Image
# noinspection PyAttributeOutsideInit # noinspection PyAttributeOutsideInit
# аннотируем все классы всех скриншот тестов для работы плагина # аннотируем все классы всех скриншот тестов для работы плагина
@@ -83,7 +85,7 @@ class TestCase(common.TestCase):
"""Без учета плотности пикселей.""" """Без учета плотности пикселей."""
wait = WebDriverWait(self.driver, timeout=10, ignored_exceptions=Exception) wait = WebDriverWait(self.driver, timeout=10, ignored_exceptions=Exception)
wait.until(lambda _: self.driver.find_element(locator_type, query_string).is_displayed(), wait.until(lambda _: self.driver.find_element(locator_type, query_string).is_displayed(),
message="Невозможно получить размеры элемента, элемент не отображается") message="Невозможно получить размеры элемента, элемент не отображается")
# После того, как дождались видимости элемента, ждем еще 2 секунды, чтобы точно завершились разные анимации # После того, как дождались видимости элемента, ждем еще 2 секунды, чтобы точно завершились разные анимации
time.sleep(2) time.sleep(2)
el = self.driver.find_element(locator_type, query_string) el = self.driver.find_element(locator_type, query_string)
@@ -100,13 +102,14 @@ class TestCase(common.TestCase):
x, y, width, height = self._get_raw_coords_by_locator(locator_type, query_string) 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 return x * self.pixel_ratio, y * self.pixel_ratio, width * self.pixel_ratio, height * self.pixel_ratio
def _get_element_screenshot(self, def _get_element_screenshot(
locator_type, self,
query_string, locator_type,
action, query_string,
finalize, action,
scroll_and_screen) \ finalize,
-> Tuple[Image.Image, Tuple[int, int, int, int]]: scroll_and_screen
) -> Tuple[Image.Image, Tuple[int, int, int, int]]:
"""Сделать скриншот страницы и кропнуть до скриншота элемента. """Сделать скриншот страницы и кропнуть до скриншота элемента.
Не получится использовать метод session/{sessionId}/element/{elementId}/screenshot Не получится использовать метод session/{sessionId}/element/{elementId}/screenshot
@@ -137,16 +140,16 @@ class TestCase(common.TestCase):
return screen.crop(coordinates), coordinates return screen.crop(coordinates), coordinates
def _get_diff(self, element, action=None, full_screen=True, full_page=False, finalize=None, scroll_and_screen=True): def _get_diff(self, element=None, action=None, full_screen=True, full_page=False, finalize=None, scroll_and_screen=True):
"""Получит скриншоты с текущей страницы, и с эталонной. """Получит скриншоты с текущей страницы, и с эталонной.
Поблочно сравнит их, и вернет количество отличающихся блоков. Поблочно сравнит их, и вернет количество отличающихся блоков.
:param element: тюпл с типом локатора и локатором :param element: tuple с типом локатора и локатором.
:param action: функция которая подготовит страницу к снятию скриншота :param action: функция, которая подготовит страницу к снятию скриншота.
:param full_screen: ресайзить ли браузер до максимума :param full_screen: resize ли браузер до максимума.
:param full_page: скринить всю страницу, а не только переданный элемент :param full_page: скринить всю страницу, а не только переданный элемент.
:param finalize: финализация после сравнения скриншотов :param finalize: финализация после сравнения скриншотов.
:param scroll_and_screen: скролить страницу (сверху к низу) и склеивать участки в один скриншот :param scroll_and_screen: скролить страницу (сверху к низу) и склеивать участки в один скриншот.
""" """
if full_screen: if full_screen:
self._use_full_screen() self._use_full_screen()
@@ -162,13 +165,11 @@ class TestCase(common.TestCase):
prod_url = saved_url._replace(netloc=self.staging) prod_url = saved_url._replace(netloc=self.staging)
# На текущей странице делаем первый скриншот # На текущей странице делаем первый скриншот
first_image, coords_test = self._get_element_screenshot(locator_type, query_string, action, finalize, first_image, coords_test = self._get_element_screenshot(locator_type, query_string, action, finalize, scroll_and_screen)
scroll_and_screen)
logging.info('Done screen on test stand') logging.info('Done screen on test stand')
# Теперь делаем скриншот в проде # Теперь делаем скриншот в проде
self.driver.get(prod_url.geturl()) self.driver.get(prod_url.geturl())
second_image, coords_prod = self._get_element_screenshot(locator_type, query_string, action, finalize, second_image, coords_prod = self._get_element_screenshot(locator_type, query_string, action, finalize, scroll_and_screen)
scroll_and_screen)
logging.info('Done screen on stage stand') logging.info('Done screen on stage stand')
# Возращаемся на тестовый стенд. Всегда нужно возвращаться на тестовый стенд. На это завязаны тесты и отчеты # Возращаемся на тестовый стенд. Всегда нужно возвращаться на тестовый стенд. На это завязаны тесты и отчеты
@@ -186,9 +187,9 @@ class TestCase(common.TestCase):
return diff, saved_url, prod_url return diff, saved_url, prod_url
def get_diff(self, *args, **kwargs): # def get_diff(self, *args, **kwargs):
diff, _, _ = self._get_diff(*args, **kwargs) # diff, _, _ = self._get_diff(*args, **kwargs)
return diff # return diff
def check_by_screenshot(self, element, *args, **kwargs): def check_by_screenshot(self, element, *args, **kwargs):
diff, saved_url, prod_url = self._get_diff(element, *args, **kwargs) diff, saved_url, prod_url = self._get_diff(element, *args, **kwargs)