Compare commits

...

10 Commits

Author SHA1 Message Date
Ivan Vazhenin
cd1c20d054 Fix tests 2023-07-04 19:13:49 +03:00
Ivan Vazhenin
bde3749cd4 make screenshots 2022-10-04 08:27:24 +03:00
Ivan Vazhenin
30740df3e8 make screenshots 2022-09-29 08:41:22 +03:00
Ivan Vazhenin
9f89600070 make screenshots 2022-09-28 20:58:41 +03:00
Ivan Vazhenin
201b660077 make screenshots 2022-09-28 20:53:40 +03:00
Ivan Vazhenin
25a79ed807 make screenshots 2022-09-28 12:55:33 +03:00
aikrasnov
7c8a859f5e Merge pull request #8 from mnestuley-marfatech/bump_versions
Update examples
2022-07-05 13:42:33 +03:00
Mikhail Nestuley
411b7c4cf4 fix core 2022-07-04 14:07:22 +03:00
Mikhail Nestuley
8e412fb6bc Update examples 2022-07-04 14:04:42 +03:00
a.krasnov
1ef0520d1e remove unused import 2020-12-02 00:52:49 +03:00
10 changed files with 292 additions and 67 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
__pycache__
# Logs
logs/*-report

View File

@@ -1,7 +1,8 @@
import pytest
import logging
import allure
from selenium.webdriver import Chrome, ChromeOptions, Remote
from selenium.webdriver import Chrome, ChromeOptions
from selenium.webdriver.chrome.service import Service
class Config:
@@ -13,8 +14,8 @@ class Config:
def driver():
options = ChromeOptions()
options.add_argument("--headless")
webdriver = Chrome(desired_capabilities=options.to_capabilities())
webdriver.implicitly_wait(5)
webdriver = Chrome(service=Service(executable_path='/home/ashatora/yandexdriver'), options=options)
webdriver.implicitly_wait(2)
yield webdriver
allure.attach(webdriver.current_url, "last url", allure.attachment_type.URI_LIST)
webdriver.quit()
@@ -23,13 +24,13 @@ def driver():
def pytest_addoption(parser):
"""Command line parser."""
parser.addoption(f'--{Config.BASE_URL}',
default='https://go.mail.ru/',
default='https://s57test.ivazh.ru/',
dest=Config.BASE_URL,
action='store',
metavar='str',
help='Environment for run tests.')
parser.addoption(f'--{Config.STAGING}',
default='go.mail.ru',
default='s57test.ivazh.ru',
dest=Config.STAGING,
action='store',
metavar='str',

View File

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

View File

@@ -1,5 +1,5 @@
pytest==5.1.2
flaky==3.6.1
selenium==3.141.0
allure-pytest==2.8.2
Pillow==6.2.2
pytest==7.1.2
flaky==3.7.0
selenium==4.3.0
allure-pytest==2.9.45
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_height += image.size[1]
# Склейка работает так: сначала создаем одно "пустое" изображение равное размеру всех скелееных, и вставляем в
# в него по одному все скриншоты.
# Чтобы в финальном скрине не получилось что скриншоты заняли меньше места, чем картинка, снизу отрезаем over_height
# Склейка работает так: сначала создаем одно "пустое" изображение равное размеру всех скелееных, и вставляем в него по одному все скриншоты.
# Чтобы в финальном скрине не получилось что скриншоты заняли меньше места, чем картинка, снизу отрезаем 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})')
@@ -161,14 +160,16 @@ class ImageProcessor(object):
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:
image: Image.Image = Image.open(fp)
image.load()
return image
def image_to_bytes(self, image: Image.Image) -> bytes:
@staticmethod
def image_to_bytes(image: Image.Image) -> bytes:
with BytesIO() as fp:
image.save(fp, "PNG")
return fp.getvalue()

View File

@@ -1,30 +1,45 @@
from screenshot_tests.utils.screenshots import TestCase
import os
from selenium.webdriver.common.by import By
from selenium.webdriver import ActionChains
from screenshot_tests.utils.screenshots import TestCase
import time
class TestExample(TestCase):
"""Tests for https://go.mail.ru"""
# def test_main_page(self):
# self.driver.get("https://s57map.ivazh.ru/")
#
# def action():
# time.sleep(2)
#
# self.check_by_screenshot(None, full_page=True, action=action)
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/")
# Чтобы посмотреть как выглядит сломанный тест в отчетe
self.driver.find_element_by_xpath("//input[not(@type='hidden')]").send_keys("foo")
self.check_by_screenshot(None, full_page=True)
# def test_mouse_move(self):
# self.driver.get("https://s57map.ivazh.ru/")
#
# def action():
# time.sleep(2)
# a = ActionChains(self.driver)
# a.move_by_offset(100, 200)
# a.perform()
#
# self.check_by_screenshot(None, full_page=True, action=action)
def test_search_block(self):
self.driver.get("https://go.mail.ru/")
self.driver.get("https://s57test.ivazh.ru/")
def action():
# Тестируем подсветку таба после переключения на другую вертикаль
self.driver.find_element_by_xpath("//span[contains(text(), 'Соцсети')]").click()
time.sleep(2)
self.check_by_screenshot_file(None, screenshot_path=f'{os.path.dirname(__file__)}/test_s57_style.png',
full_page=True, action=action)
def test_save_screenshot(self):
self.driver.get("https://s57test.ivazh.ru/")
def action():
time.sleep(2)
self.save_screenshot(f'{os.path.dirname(__file__)}/test_s57_style.png', full_page=True, action=action)
self.check_by_screenshot((By.CSS_SELECTOR, ".MainVerticalsNav-listItemActive"), action=action)

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

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

View File

@@ -1,17 +1,20 @@
"""Screenshot TestCase."""
import logging
import os.path
import time
from typing import Tuple
from urllib.parse import urlparse
import allure
import pytest
import time
from PIL import Image
from selenium.webdriver.common.by import By
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 typing import Tuple
from PIL import Image
from screenshot_tests.utils import common
# noinspection PyAttributeOutsideInit
# аннотируем все классы всех скриншот тестов для работы плагина
@@ -83,7 +86,7 @@ class TestCase(common.TestCase):
"""Без учета плотности пикселей."""
wait = WebDriverWait(self.driver, timeout=10, ignored_exceptions=Exception)
wait.until(lambda _: self.driver.find_element(locator_type, query_string).is_displayed(),
message="Невозможно получить размеры элемента, элемент не отображается")
message="Невозможно получить размеры элемента, элемент не отображается")
# После того, как дождались видимости элемента, ждем еще 2 секунды, чтобы точно завершились разные анимации
time.sleep(2)
el = self.driver.find_element(locator_type, query_string)
@@ -93,6 +96,10 @@ class TestCase(common.TestCase):
y = location["y"]
width = location["x"] + size['width']
height = location["y"] + size['height']
if width == 0:
width = 1920
if height == 0:
height = 1080
# (312, 691, 1112, 691)
return x, y, width, height
@@ -100,13 +107,14 @@ class TestCase(common.TestCase):
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
def _get_element_screenshot(self,
locator_type,
query_string,
action,
finalize,
scroll_and_screen) \
-> Tuple[Image.Image, Tuple[int, int, int, int]]:
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
@@ -137,16 +145,16 @@ class TestCase(common.TestCase):
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 action: функция которая подготовит страницу к снятию скриншота
:param full_screen: ресайзить ли браузер до максимума
:param full_page: скринить всю страницу, а не только переданный элемент
:param finalize: финализация после сравнения скриншотов
:param scroll_and_screen: скролить страницу (сверху к низу) и склеивать участки в один скриншот
:param element: tuple с типом локатора и локатором.
:param action: функция, которая подготовит страницу к снятию скриншота.
:param full_screen: resize ли браузер до максимума.
:param full_page: скринить всю страницу, а не только переданный элемент.
:param finalize: финализация после сравнения скриншотов.
:param scroll_and_screen: скролить страницу (сверху к низу) и склеивать участки в один скриншот.
"""
if full_screen:
self._use_full_screen()
@@ -162,13 +170,11 @@ class TestCase(common.TestCase):
prod_url = saved_url._replace(netloc=self.staging)
# На текущей странице делаем первый скриншот
first_image, coords_test = self._get_element_screenshot(locator_type, query_string, action, finalize,
scroll_and_screen)
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)
second_image, coords_prod = self._get_element_screenshot(locator_type, query_string, action, finalize, scroll_and_screen)
logging.info('Done screen on stage stand')
# Возращаемся на тестовый стенд. Всегда нужно возвращаться на тестовый стенд. На это завязаны тесты и отчеты
@@ -186,6 +192,50 @@ class TestCase(common.TestCase):
return diff, saved_url, prod_url
def _get_screenshot_diff(self, element=None, screenshot_path=None, action=None, full_screen=True,
full_page=False, finalize=None, scroll_and_screen=True):
"""Получит скриншот с текущей страницы, поблочно сравнит с эталонным и вернет количество отличающихся блоков.
:param screenshot_path: путь к файлу с изображением.
:param element: tuple с типом локатора и локатором.
:param action: функция, которая подготовит страницу к снятию скриншота.
:param full_screen: resize ли браузер до максимума.
:param full_page: скринить всю страницу, а не только переданный элемент.
:param finalize: финализация после сравнения скриншотов.
:param scroll_and_screen: скролить страницу (сверху к низу) и склеивать участки в один скриншот.
"""
if full_screen:
self._use_full_screen()
if full_page:
locator_type, query_string = (By.XPATH, "//body")
scroll_and_screen = False
else:
locator_type, query_string = element[0], element[1]
saved_url = urlparse(self.driver.current_url)
test_url = self.base_url
self.driver.get(test_url)
test_image, coords_prod = self._get_element_screenshot(locator_type, query_string, action, finalize, scroll_and_screen)
logging.info('Done screen on test stand')
# Возращаемся на тестовый стенд. Всегда нужно возвращаться на тестовый стенд. На это завязаны тесты и отчеты
self.driver.get(saved_url.geturl())
second_image = Image.open(screenshot_path, formats=['PNG'])
# Для добавления в отчет (https://github.com/allure-framework/allure2/tree/master/plugins/screen-diff-plugin)
# noinspection PyUnboundLocalVariable
allure.attach(self.image_processor.image_to_bytes(test_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(test_image, second_image)
allure.attach(result, 'diff', allure.attachment_type.PNG)
return diff, test_url
def get_diff(self, *args, **kwargs):
diff, _, _ = self._get_diff(*args, **kwargs)
return diff
@@ -193,3 +243,29 @@ class TestCase(common.TestCase):
def check_by_screenshot(self, element, *args, **kwargs):
diff, saved_url, prod_url = self._get_diff(element, *args, **kwargs)
assert diff == 0, f"Элемент отличается на страницах:\n{saved_url.geturl()}\nи\n{prod_url.geturl()}"
def check_by_screenshot_file(self, element, *args, **kwargs):
diff, test_url = self._get_screenshot_diff(element, *args, **kwargs)
assert diff == 0, f"Элемент отличается на странице:\n{test_url}\n"
def save_screenshot(self, screenshot_path, element=None, action=None, full_screen=True, full_page=False, finalize=None, scroll_and_screen=True):
if full_screen:
self._use_full_screen()
if full_page:
locator_type, query_string = (By.XPATH, "//body")
scroll_and_screen = False
else:
locator_type, query_string = element[0], element[1]
saved_url = urlparse(self.driver.current_url)
test_url = self.base_url
self.driver.get(test_url)
test_image, coords_prod = self._get_element_screenshot(locator_type, query_string, action, finalize, scroll_and_screen)
self.driver.get(saved_url.geturl())
test_image.save(screenshot_path, format='png')
return coords_prod