Skip to content

Commit e58a5f3

Browse files
Apkawamatslindh
andauthored
xpath and windows compatibly (#3)
* feature: allow xpath for test regression against element + Windows support 1) Allow a user to give an optional `xpath` qualifier when calling `screenshot_regression`. The regression will then be tested against this single element instead of the whole page. 2) Make pytest-image-diff with splinter work properly on Windows (see issue #1). Since Windows requires us to close the temp file first, we make the temp file persistent, close it, and then tells splinter to write to it. Afterwards we clean up the temporary file before returning from the function. * Added windows to CI * Drop chromedriver-binary from `[splinter]` extra requirement Co-authored-by: Mats Lindh <[email protected]>
1 parent 7f74780 commit e58a5f3

File tree

9 files changed

+104
-26
lines changed

9 files changed

+104
-26
lines changed

.github/workflows/ci.yml

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,38 +9,58 @@ on:
99

1010
jobs:
1111
test:
12-
runs-on: ubuntu-20.04
13-
name: "python ${{ matrix.python-version }} ${{ matrix.toxenv }}"
1412
strategy:
1513
fail-fast: false
1614
matrix:
15+
os: [ubuntu-20.04]
1716
python-version: [ 3.7, 3.8, 3.9, "3.10"]
1817
toxenv: [""]
1918
experimental: [ false ]
19+
chrome: ["stable"]
2020
include:
2121
- toxenv: qa
2222
python-version: 3.7
2323
experimental: false
24+
os: ubuntu-20.04
2425
- toxenv: type
2526
python-version: 3.7
2627
experimental: false
27-
- toxenv: splinter
28+
os: ubuntu-20.04
29+
- toxenv: splinter-linux
2830
experimental: false
2931
python-version: 3.7
32+
os: ubuntu-20.04
33+
34+
- toxenv: splinter-windows
35+
# Do not support selenium-windows
36+
experimental: true
37+
python-version: 3.7
38+
os: windows-latest
39+
chrome: "959905"
40+
41+
- experimental: false
42+
python-version: "3.9"
43+
os: windows-latest
3044
- experimental: true
3145
python-version: "3.11.0-alpha - 3.11"
46+
os: ubuntu-20.04
3247
- experimental: true
3348
python-version: "pypy-3.7"
49+
os: ubuntu-20.04
3450

51+
runs-on: ${{ matrix.os }}
52+
name: "${{ matrix.os }} python ${{ matrix.python-version }} ${{ matrix.toxenv }}"
3553

3654
continue-on-error: ${{ matrix.experimental }}
3755
env:
3856
TOXENV: ${{ matrix.toxenv }}
3957
steps:
4058
# chrome headless
4159
- uses: browser-actions/setup-chrome@latest
60+
with:
61+
chrome-version: ${{ matrix.chrome }}
4262
- uses: actions/checkout@v2
43-
- name: Set up python ${{ matrix.python-version}}
63+
- name: Set up python ${{ matrix.python-version }}
4464
uses: actions/setup-python@v2
4565
with:
4666
python-version: ${{ matrix.python-version }}

pytest_image_diff/_types.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
from pathlib import Path
12
from typing import BinaryIO, Union, Tuple, Optional
23

34
from PIL.Image import Image
45
from typing_extensions import Literal, Protocol
56

6-
PathOrFileType = Union[str, bytes, BinaryIO]
7+
PathOrFileType = Union[str, bytes, Path, BinaryIO]
78
ImageFileType = Union[Image, PathOrFileType]
89
ImageSize = Tuple[int, int]
910

pytest_image_diff/helpers.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import os
2+
import pathlib
23
import shutil
34
import typing
5+
from contextlib import contextmanager
6+
from tempfile import NamedTemporaryFile
47

58
from PIL.Image import Image
69
from _pytest import junitxml
@@ -27,6 +30,19 @@ def build_filename(
2730
)
2831

2932

33+
@contextmanager
34+
def temp_file(suffix: typing.Optional[str] = None) -> typing.Iterator[pathlib.Path]:
35+
with NamedTemporaryFile(suffix=suffix, delete=False) as tf:
36+
temp_image_path = pathlib.Path(tf.name)
37+
try:
38+
yield temp_image_path
39+
finally:
40+
try:
41+
temp_image_path.unlink()
42+
except FileNotFoundError:
43+
pass
44+
45+
3046
class TestInfo:
3147
test_name: str
3248
class_name: str
@@ -58,13 +74,13 @@ def get_test_info(
5874
return TestInfo.get_test_info(request, suffix, prefix)
5975

6076

61-
def image_save(image: ImageFileType, path: str) -> None:
77+
def image_save(image: ImageFileType, path: typing.Union[str, pathlib.Path]) -> None:
6278
if isinstance(image, Image):
6379
image.save(path)
6480
elif isinstance(image, str):
6581
if not os.path.exists(image):
6682
raise ValueError("Image maybe path. Path not exists!")
67-
shutil.copyfile(image, path)
83+
shutil.copyfile(image, str(path))
6884
elif hasattr(image, "read"):
6985
with open(path, "wb") as f:
7086
image = typing.cast(typing.BinaryIO, image)

pytest_image_diff/plugin.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import os
2-
from tempfile import NamedTemporaryFile
32
from typing import Optional, NamedTuple, cast, Callable, Generator
43

54
import pytest
@@ -8,7 +7,7 @@
87
from _pytest.runner import CallInfo
98

109
from ._types import ImageFileType, ImageRegressionCallableType, ImageDiffCallableType
11-
from .helpers import get_test_info, build_filename, image_save, ensure_dirs
10+
from .helpers import get_test_info, build_filename, image_save, ensure_dirs, temp_file
1211
from .image_diff import _diff
1312

1413
try:
@@ -18,7 +17,7 @@
1817

1918

2019
@pytest.hookimpl(tryfirst=True, hookwrapper=True) # type: ignore
21-
def pytest_runtest_makereport(item: Function, call: CallInfo) -> None:
20+
def pytest_runtest_makereport(item: Function, call: CallInfo): # type: ignore
2221
# execute all other hooks to obtain the report object
2322
outcome = yield
2423
rep = outcome.get_result()
@@ -180,8 +179,6 @@ def _factory(
180179
) -> bool:
181180
if threshold is None:
182181
threshold = image_diff_threshold
183-
image_temp_file = NamedTemporaryFile(suffix=".jpg")
184-
image_2_temp_file = NamedTemporaryFile(suffix=".jpg")
185182

186183
_info = _image_diff_info(image, suffix)
187184
diff_path = cast(str, _info.diff_name)
@@ -198,9 +195,12 @@ def _cleanup() -> None:
198195

199196
request.addfinalizer(_cleanup)
200197

201-
image_save(image, path=image_temp_file.name)
202-
image_save(image_2, path=image_2_temp_file.name)
203-
diff_ratio = _diff(image_temp_file.name, image_2_temp_file.name, diff_path)
198+
with temp_file(suffix=".jpg") as image_temp_file, temp_file(
199+
suffix=".jpg"
200+
) as image_2_temp_file:
201+
image_save(image, path=image_temp_file)
202+
image_save(image_2, path=image_2_temp_file)
203+
diff_ratio = _diff(image_temp_file, image_2_temp_file, diff_path)
204204
assert diff_ratio <= threshold, "Image not equals! See %s" % diff_path # noqa
205205
return True
206206

pytest_image_diff/splinter.py

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import pytest
2-
from tempfile import NamedTemporaryFile
1+
import os
32
from typing import Optional, Generator
3+
4+
import pytest
45
from typing_extensions import Protocol
56

7+
from .helpers import temp_file
68

79
try:
810
# Check pytest-splinter
@@ -19,6 +21,7 @@ def __call__(
1921
browser: Optional[Browser] = None,
2022
threshold: Optional[float] = None,
2123
suffix: Optional[str] = None,
24+
xpath: Optional[str] = "",
2225
) -> bool:
2326
pass
2427

@@ -36,22 +39,43 @@ def screenshot_regression(
3639
:param browser: optional, by default from `browser` fixture
3740
:param threshold: float, by default from `image_diff_threshold`
3841
:param suffix: str, need for multiple checks by one test
42+
:param xpath: str, optional xpath expression to select an element to
43+
screenshot instead of page
3944
"""
4045
default_browser = browser
4146

4247
def _factory(
4348
browser: Optional[Browser] = None,
4449
threshold: Optional[float] = None,
4550
suffix: Optional[str] = "",
51+
xpath: Optional[str] = "",
4652
) -> bool:
4753
if browser is None:
4854
browser = default_browser
4955

5056
if threshold is None:
5157
threshold = image_diff_threshold
52-
tf = NamedTemporaryFile(suffix=".png")
53-
image = tf.name
54-
browser.driver.save_screenshot(image)
55-
return image_regression(image, threshold, suffix)
58+
59+
with temp_file(suffix=".png") as temp_image_path:
60+
screenshot_path = os.fspath(temp_image_path)
61+
62+
if xpath:
63+
# `unique_file=False` since we already have a temporary file
64+
#
65+
# Since an xpath screenshot composes its own file name,
66+
# we need to give it the prefix and
67+
# suffix as separate parameters. `:-4` for the path without extension,
68+
# then suffix given manually.
69+
browser.find_by_xpath(xpath).first.screenshot(
70+
screenshot_path[:-4],
71+
suffix=screenshot_path[-4:],
72+
unique_file=False,
73+
full=True,
74+
)
75+
else:
76+
browser.driver.save_screenshot(screenshot_path)
77+
78+
result = image_regression(screenshot_path, threshold, suffix)
79+
return result
5680

5781
yield _factory

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ universal = 1
2020

2121
[flake8]
2222
ignore = D203
23-
exclude =
23+
exclude =
2424
.git/,
2525
.tox/,
2626
docs/,

setup.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,7 @@ def read(fname):
5454
python_requires=">=3.6, <4",
5555
entry_points={"pytest11": ["image_diff = pytest_image_diff.plugin"]},
5656
install_requires=["pytest", "typing_extensions", "diffimg", "imgdiff"],
57-
extras_require={
58-
"splinter": ["pytest-splinter>=2.1.0", "chromedriver-binary==2.40.1"]
59-
},
57+
extras_require={"splinter": ["pytest-splinter>=2.1.0"]},
6058
zip_safe=False,
6159
include_package_data=True,
6260
keywords=["pytest"],

tests/test_splinter.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,14 @@ def test_splinter(browser, screenshot_regression):
5050
screenshot_regression()
5151
browser.driver.set_window_size(800, 600)
5252
screenshot_regression(suffix="small_window")
53+
54+
55+
@with_splinter
56+
def test_splinter_with_xpath(browser, screenshot_regression):
57+
tf = tempfile.NamedTemporaryFile(suffix=".html")
58+
tf.write(b"<html><body><h1>Example</h1></body></html>")
59+
tf.flush()
60+
61+
browser.driver.set_window_size(1280, 1024)
62+
browser.visit("file://" + tf.name)
63+
screenshot_regression(xpath="//h1")

tox.ini

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,21 @@
22
minversion = 2.3
33
skip_missing_interpreters = true
44
envlist =
5-
py3{6,7,8,9,10,11}
5+
py3{6,7,8,9,10,11}-{linux,windows}
66

77
[testenv]
8+
# environment will be skipped if regular expression does not match against the sys.platform string
9+
platform = linux: linux
10+
macos: darwin
11+
windows: win32
12+
813
changedir = {toxinidir}
914
deps =
1015
-r{toxinidir}/requirements-dev.txt
1116
splinter: -e .[splinter]
17+
splinter-linux: chromedriver-binary-auto
18+
splinter-windows: chromedriver-binary==95.0.4638.69.0
19+
1220
setenv =
1321
PYTHONPATH = {toxinidir}
1422
passenv =

0 commit comments

Comments
 (0)