Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Harmin committed Nov 6, 2023
1 parent de8e00c commit 8e65a38
Show file tree
Hide file tree
Showing 9 changed files with 719 additions and 1 deletion.
1 change: 1 addition & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
custom: ['https://www.buymeacoffee.com/harmin']
18 changes: 18 additions & 0 deletions .readthedocs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Required
version: 2

# Set the OS, Python version and other tools you might need
build:
os: "ubuntu-22.04"
tools:
python: "3.11"

# Build documentation in the "docs/" directory with Sphinx
sphinx:
configuration: docs/conf.py

# Explicitly set the version of Python and its requirements
python:
install:
- requirements: docs/requirements.txt

21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2023 Harmin Parra Rueda

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# pytest-webtest-extras

pytest plugin to enhance pytest-html reports of webtest projects with screenshots, comments and webpages source code.
Pytest plugin to enhance pytest-html reports of webtest projects with screenshots, comments and webpage sources.

![](https://img.shields.io/badge/license-MIT%202.0-blue.svg)

Expand Down
42 changes: 42 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
[build-system]
requires = ["flit_core >=3.4"]
build-backend = "flit_core.buildapi"


[project]
name = "pytest-webtest-extras"
version = "0.0.1rc1"
description = "Pytest plugin to enhance pytest-html reports of webtest projects with screenshots, comments and webpage sources."
readme = "README.md"
#license = "MIT"
authors = [
{name = "Harmin Parra Rueda", email="[email protected]"},
]
requires-python = ">=3.8"
dependencies = [
'pytest >= 7.0.0',
'pytest-html >= 4.0.0',
]
classifiers = [
"Framework :: Pytest",
"License :: OSI Approved :: MIT License",
"Topic :: Software Development :: Quality Assurance",
"Topic :: Software Development :: Testing",
]
keywords = [
"pytest",
"selenium",
"playwright",
"webtest",
"webtesting",
]


[project.entry-points.pytest11]
webtest_extras = "pytest_webtest_extras.plugin"


#[project.urls]
#Homepage = "https://pypi.org/project/pytest_webtest_extras"
#Documentation = "https://pytest_webtest_extras.readthedocs.io/en/stable/"
#Source = "https://github.com/harmin-parra/pytest_webtest_extras"
Empty file.
89 changes: 89 additions & 0 deletions src/pytest_webtest_extras/extras.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
from . import utils


# Counter used for image and page source files naming
count = 0


def counter():
""" Returns a suffix used for image and page source file naming """
global count
count += 1
return count


class Extras():
"""
Class to hold pytest-html 'extras' to be added for each test in the HTML report
"""

def __init__(self, report_folder, fx_screenshots, fx_comments, fx_sources):
self.images = []
self.sources = []
self.comments = []
self._fx_screenshots = fx_screenshots
self._fx_comments = fx_comments
self._fx_sources = fx_sources
self._folder = report_folder

def save_extras(self, image: bytes, comment=None, source=None):
"""
Saves the pytest-html 'extras': screenshot, comment and webpage source.
The screenshot is saved in <forder_report>/screenshots folder.
The webpage source is saved in <forder_report>/sources folder.
image (bytes): The screenshot.
comment (str): The comment of the screenshot.
source (str): The webpage source code.
"""
if self._fx_screenshots == 'none':
return
index = counter()
link_image = utils.save_image(self._folder, index, image)
self.images.append(link_image)
link_source = None
if source is not None:
link_source = utils.save_source(self._folder, index, source)
self.sources.append(link_source)
if self._fx_comments:
comment = "" if comment is None else comment
self.comments.append(comment)

def save_for_selenium(self, driver, comment=None, full_page=True):
"""
Saves the pytest-html 'extras': screenshot, comment and webpage source.
driver (WebDriver): The webdriver.
comment (str): The comment for the screenshot to take.
full_page (bool): Whether to take a full-page screenshot.
Only works for Firefox.
Defaults to True.
"""
from selenium.webdriver.remote.webdriver import WebDriver
if self._fx_screenshots == 'none':
return
if hasattr(driver, "get_full_page_screenshot_as_png") and full_page:
image = driver.get_full_page_screenshot_as_png()
else:
image = driver.get_screenshot_as_png()
source = None
if self._fx_sources:
source = driver.page_source
self.save_extras(image, comment, source)

def save_for_playwright(self, page, comment=None, full_page=True):
"""
Saves the pytest-html 'extras': screenshot, comment and webpage source.
page (Page): The page.
comment (str): The comment for the screenshot to take.
full_page (bool): Whether to take a full-page screenshot.
Defaults to True.
"""
if self._fx_screenshots == 'none':
return
image = page.screenshot(full_page=full_page)
source = None
if self._fx_sources:
source = page.content()
self.save_extras(image, comment, source)
169 changes: 169 additions & 0 deletions src/pytest_webtest_extras/plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import importlib
import os
import pytest
import re
from . import utils
from .extras import Extras


#
# Definition of test options
#
def pytest_addoption(parser):
parser.addini(
"extras_screenshots",
type="string",
default="all",
help="The screenshots to include in the report. Accepted values: all, last, none."
)
parser.addini(
"extras_comments",
type="bool",
default=False,
help="Whether to include comments."
)
parser.addini(
"extras_sources",
type="bool",
default=False,
help="Whether to include webpage sources."
)
parser.addini(
"extras_description_tag",
type="string",
default="h2",
help="HTML tag for the test description. Accepted values: h1, h2, h3, p or pre.",
)


#
# Read test parameters
#
@pytest.fixture(scope='session')
def screenshots(request):
value = request.config.getini("extras_screenshots")
if value in ("all", "last", "none"):
return value
else:
return "all"


@pytest.fixture(scope='session')
def report_folder(request):
htmlpath = request.config.getoption("--html")
return utils.get_folder(htmlpath)


@pytest.fixture(scope='session')
def report_css(request):
return request.config.getoption("--css")


@pytest.fixture(scope='session')
def description_tag(request):
tag = request.config.getini("extras_description_tag")
return tag if tag in ("h1", "h2", "h3", "p", "pre") else "h2"


@pytest.fixture(scope='session')
def comments(request):
return request.config.getini("extras_comments")


@pytest.fixture(scope='session')
def sources(request):
return request.config.getini("extras_sources")


@pytest.fixture(scope='session')
def check_options(request, report_folder):
utils.check_html_option(report_folder)
utils.create_assets(report_folder)


#
# Test fixture
#
@pytest.fixture(scope='function')
def webtest_extras(request, report_folder, screenshots, comments, sources, check_options):
return Extras(report_folder, screenshots, comments, sources)


#
# Hookers
#
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
""" Override report generation. """
pytest_html = item.config.pluginmanager.getplugin('html')
outcome = yield
report = outcome.get_result()
extras = getattr(report, 'extras', [])

# Let's deal with the HTML report
if report.when == 'call':
# Get function/method description
pkg = item.location[0].replace(os.sep, '.')[:-3]
index = pkg.rfind('.')
module = importlib.import_module(package=pkg[:index], name=pkg[index + 1:])
# Is the called test a function ?
match_cls = re.search(r"^[^\[]*\.", item.location[2])
if match_cls is None:
func = getattr(module, item.originalname)
else:
cls = getattr(module, match_cls[0][:-1])
func = getattr(cls, item.originalname)
description = getattr(func, "__doc__")

# Is the test item using the 'extras' fixtures?
if not ("request" in item.funcargs and "webtest_extras" in item.funcargs):
return
feature_request = item.funcargs['request']

# Get test fixture values
fx_extras = feature_request.getfixturevalue("webtest_extras")
fx_description_tag = feature_request.getfixturevalue("description_tag")
fx_screenshots = feature_request.getfixturevalue("screenshots")
fx_comments = feature_request.getfixturevalue("comments")

# Append test description and execution exception trace, if any.
utils.append_header(call, report, extras, pytest_html, description, fx_description_tag)

if fx_screenshots == "none" or len(fx_extras.images) == 0:
report.extras = extras
return

if not utils.check_lists_length(report, item, fx_extras):
return

links = ""
rows = ""
if fx_screenshots == "all":
if not fx_comments:
for i in range(len(fx_extras.images)):
links += utils.decorate_anchors(fx_extras.images[i], fx_extras.sources[i])
else:
for i in range(len(fx_extras.images)):
rows += utils.get_table_row_tag(fx_extras.comments[i], fx_extras.images[i], fx_extras.sources[i])
else: # fx_screenshots == "last"
if len(fx_extras.images) > 0:
if not fx_comments:
links = utils.decorate_anchors(fx_extras.images[-1], fx_extras.sources[-1])
else:
rows += utils.get_table_row_tag(fx_extras.comments[-1], fx_extras.images[-1], fx_extras.sources[-1])

# Add horizontal line between the header and the comments/screenshots
if len(extras) > 0 and len(links) + len(rows) > 0:
extras.append(pytest_html.extras.html(f'<hr class="selenium_separator">'))

# Append extras
if links != "":
extras.append(pytest_html.extras.html(links))
if rows != "":
rows = (
'<table style="width: 100%;">'
+ rows +
"</table>"
)
extras.append(pytest_html.extras.html(rows))
report.extras = extras
Loading

0 comments on commit 8e65a38

Please sign in to comment.