From 8e412fb6bc0a4bca9f8aed0078f1021a21104349 Mon Sep 17 00:00:00 2001 From: Mikhail Nestuley <94833793+mnestuley@users.noreply.github.com> Date: Fri, 1 Jul 2022 10:23:43 +0300 Subject: [PATCH] Update examples --- .gitignore | 136 +++++++++++++++++- conftest.py | 3 +- logs/.gitkeep | 0 readme.md | 6 +- requirements.txt | 10 +- .../image_proccessing/image_processor.py | 11 +- screenshot_tests/tests/simple_test.py | 11 +- screenshot_tests/utils/common.py | 4 +- screenshot_tests/utils/screenshots.py | 57 ++++---- 9 files changed, 187 insertions(+), 51 deletions(-) create mode 100644 logs/.gitkeep diff --git a/.gitignore b/.gitignore index 2e62100..a83593c 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/conftest.py b/conftest.py index 9b38dff..79d36e7 100644 --- a/conftest.py +++ b/conftest.py @@ -2,6 +2,7 @@ import pytest import logging import allure from selenium.webdriver import Chrome, ChromeOptions +from webdriver_manager.chrome import ChromeDriverManager class Config: @@ -13,7 +14,7 @@ class Config: def driver(): options = ChromeOptions() options.add_argument("--headless") - webdriver = Chrome(desired_capabilities=options.to_capabilities()) + webdriver = Chrome(ChromeDriverManager().install(), desired_capabilities=options.to_capabilities()) webdriver.implicitly_wait(5) yield webdriver allure.attach(webdriver.current_url, "last url", allure.attachment_type.URI_LIST) diff --git a/logs/.gitkeep b/logs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/readme.md b/readme.md index 770e04e..a53ab52 100644 --- a/readme.md +++ b/readme.md @@ -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` diff --git a/requirements.txt b/requirements.txt index b42e112..1448920 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/screenshot_tests/image_proccessing/image_processor.py b/screenshot_tests/image_proccessing/image_processor.py index f6c2325..22b8c6c 100644 --- a/screenshot_tests/image_proccessing/image_processor.py +++ b/screenshot_tests/image_proccessing/image_processor.py @@ -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() diff --git a/screenshot_tests/tests/simple_test.py b/screenshot_tests/tests/simple_test.py index 77c0cbf..44da6a7 100644 --- a/screenshot_tests/tests/simple_test.py +++ b/screenshot_tests/tests/simple_test.py @@ -1,6 +1,7 @@ -from screenshot_tests.utils.screenshots import TestCase from selenium.webdriver.common.by import By +from screenshot_tests.utils.screenshots import TestCase + class TestExample(TestCase): """Tests for https://go.mail.ru""" @@ -10,14 +11,14 @@ class TestExample(TestCase): 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) 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") + # Чтобы посмотреть как выглядит сломанный тeест в отчетe + self.driver.find_element(By.XPATH, "//input[not(@type='hidden')]").send_keys("foo") self.check_by_screenshot(None, full_page=True) def test_search_block(self): @@ -25,6 +26,6 @@ class TestExample(TestCase): 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) diff --git a/screenshot_tests/utils/common.py b/screenshot_tests/utils/common.py index ab07607..3776972 100644 --- a/screenshot_tests/utils/common.py +++ b/screenshot_tests/utils/common.py @@ -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.""" diff --git a/screenshot_tests/utils/screenshots.py b/screenshot_tests/utils/screenshots.py index 6c7534e..28e97a3 100644 --- a/screenshot_tests/utils/screenshots.py +++ b/screenshot_tests/utils/screenshots.py @@ -1,17 +1,19 @@ """Screenshot TestCase.""" import logging +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 +85,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) @@ -100,13 +102,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 +140,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 +165,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,9 +187,9 @@ class TestCase(common.TestCase): return diff, saved_url, prod_url - def get_diff(self, *args, **kwargs): - diff, _, _ = self._get_diff(*args, **kwargs) - return diff + # def get_diff(self, *args, **kwargs): + # diff, _, _ = self._get_diff(*args, **kwargs) + # return diff def check_by_screenshot(self, element, *args, **kwargs): diff, saved_url, prod_url = self._get_diff(element, *args, **kwargs)